携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情
链表的概念
一般的,在 静态语言 中,数组是 线性且连续 的,它意味着数组开辟的空间在内存中是 连续 的,这是因为静态语言变量的类型是不能随意更改的,每个类型的数据所占用的空间是固定的。
对于一个数组来说,有了首地址,那么根据首地址和单个数组元素所占内存大小,就能够根据下标推断出其他下标元素对应的值。但是 Javascript 属于动态语言,变量的类型是可以随意更改的,且数组中可以存放各种各样类型的数据,所以 Javascript 中数组的内存地址不是连续 的。
总结一下传统数组的特点:连续、线性。
秋豆麻袋,今天讲的不是链表吗,怎么扯那么多数组的概念?当然是因为它俩关系特殊啦。那么有了数组为什么还要链表呢?数组和链表的优劣势在哪里呢?
从上面可以知道,数组在内存空间中是连续的,如果你声明了一个长度为20的数组,但是只用了两个位置,那么剩下的位置就浪费了。
这有点像磁盘碎片,平时很久没有清理磁盘碎片的人应该知道,我们的内存明明够,但是某个文件就是载不下来,这是因为有的文件需要连续的一段磁盘空间去存储,我们日常使用中不断的删除文件下载文件,那么内存和内存之间就有一个个的小空缺,这些空缺也是内存的一部分,合起来可能够放你需要下载的这个文件,但是由于它们不是连续的,所以不能载下来,我们只好手动去磁盘碎片清理。
如图,其中黑色的代表某些文件删除之后腾出来的内存。
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到 O(1) 的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而线性表和顺序表相应的时间复杂度分别是 O(logn) 和 O(1)。
链表在内存上是 非线性且不连续 的。
那么链表既然是不连续的,怎么找到链表的下一个节点呢。传统的链表节点需要一个指针域来指向其下一个节点,这样通过这个指针域,就能形成一条链,每个节点都通过指针域链接,形成了链表。
由于链表的节点需要使用一个指针域来存放下一个节点的位置,所以 空间开销比较大。
链表的几个名词
节点
数组中的每个元素叫数组的项,而链表中的每个元素叫链表的节点。
链表的节点一般存储着 数据 以及 指向下一个节点的指针。
表头
表头就是链表的 开始节点。一般的,我们会用一个 head 指针指向表头。
有了表头,我们可以 通过每个节点的指针域遍历整个链表。
节点的值
Javascript 中的链表节点结构一般是这样的:
class ListNode {
constructor(val, node=null) {
this.val = val;
this.next = node;
}
}
链表的操作
链表的遍历
从上面我们可以知道,链表的每个节点有一个指针域,指向下一个节点。假设该指针域的名字为 next,那么我们从头节点开始,让 head 指向 head.next 所指向的节点,就实现了偏移。
这里有个需要注意的点:一旦我们将 head 的指向修改了,那么以后我们再也找不到链表的表头了,所以一般我们开始的时候,会通过一个临时变量也指向 head 所指向的表头,偏移的时候只要重新修改这个临时变量的指向就可以了。
链表的查询
链表的查询和链表的遍历是一样的,通过临时表头的偏移,我们不断的搜寻 target,直到找到为止。
链表的插入
链表的插入效率很高。不像数组,数组是连续的,如果要往数组某个插入一个元素,那么该位置后面的 所有元素都需要往后移动一位,效率很低的。而链表的插入只需要简单的修改指针域的指向,在 O(1) 的时间复杂度内就能完成插入,非常高效。
实际上不用这么复杂,有个更简单的步骤:
链表的删除
链表的删除效率也高。如果是数组,删除某个位置的元素后,该位置之后的所有元素 都要往前移动一位,效率很低。而链表的删除只要简单的修改指针域的指向。
手撕链表
class ListNode {
constructor(val, node = null) {
this.val = val;
this.next = node;
}
}
class List {
constructor() {
this.head = new ListNode(0); // 创建哨兵节点,无意义
}
append(val) {
let tHead = this.head;
while (tHead.next) {
tHead = tHead.next;
}
tHead.next = new ListNode(val);
}
insert(index, val) {
let tHead = this.head, temp = tHead;
while (tHead && index--) {
tHead = tHead.next;
}
temp = tHead.next;
const newNode = new ListNode(val);
tHead.next = newNode;
newNode.next = temp;
}
delete(target) {
let tHead = this.head;
while (tHead.next) {
if (tHead.next.val === target) {
tHead.next = tHead.next.next;
}
tHead = tHead.next;
}
}
search(target) {
let tHead = this.head;
while (tHead) {
if (tHead.val === target) {
return true;
}
tHead = tHead.next;
}
return false;
}
}
const data = [1, 2, 3, 4, 5];
const l = new List();
data.forEach(i => l.append(i));
console.log(JSON.stringify(l.head));
l.insert(5, 6);
console.log(JSON.stringify(l.head));
l.insert(1, 1.5);
console.log(l.search(1.5));
console.log(JSON.stringify(l.head));
l.delete(1.5);
console.log(JSON.stringify(l.head));
console.log(l.search(1));
console.log(l.search(1.5));
log:
{"val":0,"next":{"val":1,"next":{"val":2,"next":{"val":3,"next":{"val":4,"next":{"val":5,"next":null}}}}}}
{"val":0,"next":{"val":1,"next":{"val":2,"next":{"val":3,"next":{"val":4,"next":{"val":5,"next":{"val":6,"next":null}}}}}}}
true
{"val":0,"next":{"val":1,"next":{"val":1.5,"next":{"val":2,"next":{"val":3,"next":{"val":4,"next":{"val":5,"next":{"val":6,"next":null}}}}}}}}
{"val":0,"next":{"val":1,"next":{"val":2,"next":{"val":3,"next":{"val":4,"next":{"val":5,"next":{"val":6,"next":null}}}}}}}
true
false
巩固练习题
大家有需要巩固练习的也可以看看几道 LeetCode 上的题目: