什么是链表?
链表是一种稍微复杂一点的数据结构,他并不需要一块连续的内存空间,他通过指针将一组零散的内存块串起来使用。 常见的链表结构有三种分别是单链表、双链表、循环链表。
在链表中,我们把零散的内存空间叫做结点,为了将所有结点串起来,每个结点除了存储数据以外,还需要存储下一个结点的地址。我们把这个记录下个结点的地址的东西称为后继指针next,在内存中的表现如下图所示。
上图中的第一个结点被称为头结点(但是头结点不是必须的),头结点用来记录链表的基地址,可以通过遍历得到整条链表。最后一个节点null被称为尾结点,标志链表的结束。
链表的操作
链表也支持数据的查找、插入和删除操作,以单链表为例,一起来看看吧。 大家都说链表适合进行插入、删除操作,是为什么呢。
我们先来看看链表的插入操作,如下图,我们往链表里面插入一个c结点,只需要将b的后继指针指向c,c的后继指针在指向d,我们就完成了插入操作。时间复杂度O(1)
删除操作也是类似,如下图,删除d结点,我们只需要将c的后继指针指向e就完成了删除的操作。时间复杂度O(1)
现在来看看链表的随机访问,链表想要随机访问第k个元素,就没数组快了,因为链表中的数据并非连续存储,无法像数组一样通过基地址和寻址公式就能计算出对应的内存地址访问,链表只能通过指针一个结点一个结点的依次遍历,直到找到相应的结点。因此随机访问的时间复杂度为O(n)。
javascript中的链表
同样的在js中并没有对应的链表实现,但是不影响,我们可以自己实现一个链表结构。 不同的人对链表都有不同的实现,但是实现的思想都是一样的,链表特性也是一样的。 我的实现代码如下:
// 先定义一个结点类
class Node {
constructor(val, next){
this.val = val;
this.next = next || null;
}
}
// 定义链表类
class LinkedList {
constructor(){
this.head = new Node(null,null);
this.endNode = this.head;
}
// 添加新结点
add(val) {
const newNode = new Node(val,null);
this.endNode.next = newNode;
this.endNode = newNode;
}
// 删除结点
remove(val) {
const node = this.findPre(val);
if(node) {
// 删除对应结点
node.next = node.next.next;
}
}
// 查找结点
find(val) {
let p = this.head.next;
while(p){
if(p.val === val) {
return p;
}
p = p.next;
}
return null;
}
// 查找val的上一个结点
findPre(val) {
let p = this.head;
while(p.next){
if(p.next.val === val) {
return p;
}
}
return null;
}
}
// 创建链表
const linkedList = new LinkedList();
linkedList.add(1);
linkedList.remove(1);
其他常见链表介绍
循环链表
循环链表是一种特殊的单链表,他和单链表唯一的区别在尾结点,单链表的尾结点指向null,而循环链表的尾结点指向头结点,首位相连,优点是从链尾到链头方便,适合处理具有环型结构的数据。
双向链表
双向链表相对于单链表需要一个额外的空间来存储前驱结点的地址,所以存储同样多的数据,双向链表要比单链表占用更多的内存空间,但同时也能支持双向遍历,查找起来也更加的高效。