点击蓝色“
五分钟学算法
”关注我哟
加个“
星标
”,天天中午 12:15,一起学算法
作者 | 不甘平凡的码农
来源公众号 | 不甘平凡的码农
对于数组的学习呢,在前端中,最重要的就是数组的一些方法的使用,数组的截取、查找、反转等常用的方法;除此之外,数组另一个稍微难点就是算法题。上一周我又拿起《剑指
offer
》把关于数组的算法题刷了一遍,又总结了很多的做题技巧和有关数组的解题思路,后期会分享。
那对于链表呢,我们在项目中用到的不如数组频繁,但是面试是个重点,为什么面试官喜欢考我们链表呢?想必大家对这个问题很感兴趣,因为链表灵活、涉及到的边界条件多,又加上很多细节点,对应聘者是一个考验。
不料,暑假参加的第一个公司的就让我手写一个双向链表,并完成插入数据和删除数据的操作。当时我很蒙蔽,懵逼的不是思路,而是手写,虽然写出来了,但是很多边界条件和代码规范自我感觉不好,所以有了这些细心的总结。那么今天的主题就是徒手写链表,应聘者该如何下手?
我们通常写链表准备应聘的时候,通常背加上理解,但是过了几天又让你写。就会陌生了,虽然有点思路。还是模模糊糊,小鹿也有这个记性的“毛病”,“有毛病”就要治,怎么治?我们必须在脑海里形成一套可行的步骤和方法,在遇到手写就不用手忙脚乱,而是稳稳当当,从头到尾写出一个漂亮的链表结构及操作。
首先我们要知道链表的结构以及每个节点的结构,这是我们手写链表的第一步,也是学习链表的第一步。我们知道,每个链表时这样表示的:
那每个节点结构是由
数据域
和
指
针域
组成,数据域是存放数据的,而指针域存放下一结点的地址。
我们可以通过数据域访问到我们要的数据,而通过指针域访问到当前结点以后的结点,那么将这些结点串起来,就是一个链表。
那么用代码怎么来表示呢?
我们通常会用到函数,我们如果将一个函数抽象成一个结点,那么我们给函数添加两个属性,一个属性是存放数据的属性
data
,另一个属性是存放指向下一个结点的指针属性
next
。
你可能会问,如果我们有成千上万个结点,要定义成千上万个函数?有一个函数叫做构造函数,想必大家都听说过,声明一个构造函数就可以创造出成千上万个结点实例。
1function Node(data){
2 this.data = data;
3 this.next = null;
4}
还有一个方法就是使用类的概念,在
JavaScript
中,类的概念在
ES6
才出现,使用
Class
来声明一个类,我们可以为类添加两个属性,同上,一样可以创造出多个结点实例。
1class
Node{
2 constructor(data){
3 this.data = data;
4 this.next = null;
5 }
6}
如果你把链表的结构搞得明明白白了,恭喜你,成功的进入第二关,但是你只超越了百分之
20
的人,继续加油。
既然链表的结构弄明白了,那么我们开始理思路,我们就先拿最简单的单链表开刀,我们要完成两个操作,插入数据和删除数据。
如果我想插入数据,你可能会问,往哪里插呢?有几种插入的方法?
开始想,插入到单链表的头部算一种。
所有可能的情况就三种。那么删除呢?想必你也想到了,也一共三种,删除头部、删除中间部分、删除尾部。
如果你觉的现在可以写代码了,那你就错了,虽然我们的思路非常清晰,但是面试官仅仅考我们思路吗?其实这一关你只打败了百分之
50%
的人,最重点、最主要的是在下一个部分,边界条件。
边界条件是这五个步骤最容易犯错的一部分,因为通常考虑的不全面,导致了最后的面试未通过。如果想做好这一部分,也不难,跟着小鹿的方法走。
3.1 输入边界
首先我们先考虑用户输入的参数,比如传入一个链表,我们首先要判断链表是否为空,如果为空我们就不能让它执行下边的程序。再比如插入一个结点到指定结点的后边,那么你也要判断输入的结点是否为空,而且还要判断该结点是否存在该链表中。对于这些输入值的判断,小鹿给他同一起个名字叫做输入边界。
3.2 特殊边界
特殊边界考虑到一些特殊情况,比如插入数据,我们插入数据一般考虑到插入尾部,但是突然面试官插入到头部,插入尾部的代码并不适用于插入到头部,所以呢需要考虑这种情况,删除节点也是同样思考。其实特殊边界最主要考虑到一些逻辑上的特殊情况,考察应聘者的考虑的是否全面。小鹿给他起个名字叫做特殊边界。
1class Node{
2 constructor(data){
3 this.data = data;
4
this.next = null;
5 }
6}
4.2 增加结点
咱们就以单链表中部添加数据为例子,分解成每个步骤,每个步骤对应代码如下:
1、
保存临时地址(
4
结点的地址),需要进行遍历查找到
3
结点,也就是下列代码的
currentNode
结点。
1
2let currentNode = this.findByValue(element);
3
4let pre = currentNode.next
2、
创建新结点,将新结点(
5
结点)的指针指向下一结点指针(
4
结点地址,已经在上一步骤保存下来了)
1let newNode = new Node(value);
2newNode.next = pre;
3、
将
3
的
结点地址指向新结点(
5
结点)
1 currentNode.next = newNode;
以上步骤分析完毕,剩下的两个在头部插入和在尾部插入同样的分析方式,将这两个作为练习题,课下自己试一试这个步骤。
4.3
删除节点
删除节点也分为三种,头部、中部、尾部,咱们就删除中间结点为例进行删除,我们详细看步骤操作。
我们先看删除的全部动画,然后再分步拆分。
断开
3
结点的指针(断开
3
结点相当于让
2
结点直接指向
4
结点)
1 let currentNode = this
.head;
2
3 let preNode = null;
4
5 while(currentNode !== null && currentNode.data !== value){
6
7 preNode = currentNode;
8
9 currentNode = currentNode.next;
10}
让结点
2
的
指针指向
4
结点,完成删除。
1preNode.next = currentNode.next;
剩下的删除头结点和删除尾结点同样的步骤,自己动手尝试下。
所有代码实现:
1
22
23class Node{
24 constructor(data){
25 this.data = data;
26 this.next = null;
27 }
28}
29
30
31class LinkList{
32 constructor(){
33
34 this.head = new Node('head');
35 }
36
37
38 findByValue = (value) =>{
39 let currentNode = this.head;
40 while(currentNode !== null && currentNode.data !== value){
41 currentNode = currentNode.next;
42 }
43
44 console.log(currentNode)
45 return currentNode === null ? -1 : currentNode;
46 }
47
48
49 findByIndex = (index) =>{
50 let pos = 0;
51 let currentNode = this.head;
52 while(currentNode !== null && pos !== index){
53 currentNode = currentNode.next;
54 pos++;
55 }
56
57 console.log(currentNode)
58 return currentNode === null ? -1 : currentNode;
59 }
60
61
62 insert = (value,element) =>{
63
64 let currentNode = this.findByValue(element);
65
66 if(currentNode == -1){
67 console