链表|图解Javascript数据结构

298 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情

链表的概念

一般的,在 静态语言 中,数组是 线性且连续 的,它意味着数组开辟的空间在内存中是 连续 的,这是因为静态语言变量的类型是不能随意更改的,每个类型的数据所占用的空间是固定的

对于一个数组来说,有了首地址,那么根据首地址和单个数组元素所占内存大小,就能够根据下标推断出其他下标元素对应的值。但是 Javascript 属于动态语言,变量的类型是可以随意更改的,且数组中可以存放各种各样类型的数据,所以 Javascript 中数组的内存地址不是连续 的。

总结一下传统数组的特点:连续、线性。

秋豆麻袋,今天讲的不是链表吗,怎么扯那么多数组的概念?当然是因为它俩关系特殊啦。那么有了数组为什么还要链表呢?数组和链表的优劣势在哪里呢?

从上面可以知道,数组在内存空间中是连续的,如果你声明了一个长度为20的数组,但是只用了两个位置,那么剩下的位置就浪费了。

这有点像磁盘碎片,平时很久没有清理磁盘碎片的人应该知道,我们的内存明明够,但是某个文件就是载不下来,这是因为有的文件需要连续的一段磁盘空间去存储,我们日常使用中不断的删除文件下载文件,那么内存和内存之间就有一个个的小空缺,这些空缺也是内存的一部分,合起来可能够放你需要下载的这个文件,但是由于它们不是连续的,所以不能载下来,我们只好手动去磁盘碎片清理。

磁盘碎片.PNG

如图,其中黑色的代表某些文件删除之后腾出来的内存。

相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到 O(1) 的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而线性表和顺序表相应的时间复杂度分别是 O(logn)O(1)

链表在内存上是 非线性且不连续 的。

那么链表既然是不连续的,怎么找到链表的下一个节点呢。传统的链表节点需要一个指针域来指向其下一个节点,这样通过这个指针域,就能形成一条链,每个节点都通过指针域链接,形成了链表。

由于链表的节点需要使用一个指针域来存放下一个节点的位置,所以 空间开销比较大

链表的几个名词

节点

数组中的每个元素叫数组的项,而链表中的每个元素叫链表的节点。

链表的节点一般存储着 数据 以及 指向下一个节点的指针链表节点.PNG

表头

表头就是链表的 开始节点。一般的,我们会用一个 head 指针指向表头。

有了表头,我们可以 通过每个节点的指针域遍历整个链表

表头.PNG

节点的值

Javascript 中的链表节点结构一般是这样的:

class ListNode {
    constructor(val, node=null) {
        this.val = val;
        this.next = node;
    }
}

链表的操作

链表的遍历

从上面我们可以知道,链表的每个节点有一个指针域,指向下一个节点。假设该指针域的名字为 next,那么我们从头节点开始,让 head 指向 head.next 所指向的节点,就实现了偏移。

链表.gif

这里有个需要注意的点:一旦我们将 head 的指向修改了,那么以后我们再也找不到链表的表头了,所以一般我们开始的时候,会通过一个临时变量也指向 head 所指向的表头,偏移的时候只要重新修改这个临时变量的指向就可以了。

链表的查询

链表的查询和链表的遍历是一样的,通过临时表头的偏移,我们不断的搜寻 target,直到找到为止。

链表的插入

链表的插入效率很高。不像数组,数组是连续的,如果要往数组某个插入一个元素,那么该位置后面的 所有元素都需要往后移动一位,效率很低的。而链表的插入只需要简单的修改指针域的指向,在 O(1) 的时间复杂度内就能完成插入,非常高效。 链表插入.gif

实际上不用这么复杂,有个更简单的步骤: 链表的插入2.gif

链表的删除

链表的删除效率也高。如果是数组,删除某个位置的元素后,该位置之后的所有元素 都要往前移动一位,效率很低。而链表的删除只要简单的修改指针域的指向。 链表的删除.gif

手撕链表

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 上的题目: