链表结构非常多的,今天学习下三种常见的链表:单链表、双向链表和循环链表。
单链表

链表通过指针将一组零散的内存块串联在一起。其中,内存块被称为链表的结点。为了将所有的结点串起来, 每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址 ,这个地址被称为后继指针next。结点中有两个结点比较特殊的,第一个结点被称为头结点,用来记录链表的基地址;最后一个结点被称为尾结点,尾结点是一个空地址NULL,表示这是链表上的最后一个结点。
用JavaScript实现一个单向链表,并且实现增删查:
首先定义一个类,构造函数中定义结点以及next:
class List{
constructor(val){
this.val = val;
this.next = null;
}
}
先写一个查询方法,查询结点是否在该链表中:
find(target){
let cur = this;
console.log(111,this)
while (cur.val !== target) {
cur = cur.next;
if (!cur) {
return false
}
}
return cur
}
再来一个添加方法
add(node, target){
let newNode = new List(node);
let cur = this.find(target);
newNode.next = cur.next;
cur.next = newNode
}
结点是由一个个的后继指针next串联起来的,添加方法就需要两个参数,一个待添加结点,一个添加的目标节点。在方法中首先将待添加的结点实例化为我们的结点对象,再将目标节点的next指针指向带添加结点。
测试一下:
let list = new List('nick')
list.add('tom', 'nick')
console.log(list)
//List {val: 'nick', next: List}
再来一个删除结点的方法,要删除某一个结点,根据链表的结构,直接将该链表的上一个结点的next指向目标结点的next,该结点就从整个链表中删除了。
findPre(target){
if(!this.find(target)){
return false;
}
let cur = this;
while (cur.next.val !== target) {
cur = cur.next
}
return cur;
}
delete(target){
let deleteNode = this.find(target);
this.findPre(deleteNode.val).next = deleteNode.next
}
测试一下
let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
list.delete('老三')
console.log(list)
//next: List
next: List
next: null
val: "老四"
[[Prototype]]: Object
val: "老二"
[[Prototype]]: Object
val: "老大"
[[Prototype]]: Object
完整代码:
class List{
constructor(val){
this.val = val;
this.next = null;
}
find(target){
let cur = this;
while (cur.val !== target) {
cur = cur.next;
if (!cur) {
return false
}
}
return cur
}
add(node, target){
let newNode = new List(node);
let cur = this.find(target);
newNode.next = cur.next;
cur.next = newNode
}
findPre(target){
if(!this.find(target)){
return false;
}
let cur = this;
while (cur.next.val !== target) {
cur = cur.next
}
return cur;
}
delete(target){
let deleteNode = this.find(target);
this.findPre(deleteNode.val).next = deleteNode.next
}
}
let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
list.delete('老三')
console.log(list)
循环链表

就像图中展示的那样,单链表的尾结点指向头部结点,即是一个循环链表。
双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
用JavaScript实现一个单向链表,并且实现增删查:
class List{
constructor(val){
this.val = val;
this.next = null;
this.pre = null;
}
find(target){
let cur = this;
while (cur.val !== target) {
cur = cur.next;
if (!cur) {
return false
}
}
return cur
}
add(node, target){
let newNode = new List(node);
let cur = this.find(target);
newNode.next = cur.next;
newNode.pre = cur;
cur.next = newNode
}
delete(target){
let cur = this.find(target);
cur.pre.next = cur.next;
cur.next.pre = cur.pre;
}
}
let list = new List('老大')
list.add('老二', '老大')
list.add('老三', '老二')
list.add('老四', '老三')
// list.delete('老三')
console.log(list)
有了单向链表的经验,双向链表就容易很多了,因为有了前驱指针 prev使得双向链表在增加删除时比单向链表方便太多。
反转链表
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr) {
const next = curr.next; //1
curr.next = prev; //2
prev = curr; //3
curr = next; //4
}
return prev;
};
这里是力扣的官方答案,代码很简洁,有点难以理解。核心代码就是循环体内的四行代码,第一行和第四行只是为了循环体走下去,主要就是第二行和第三行。第一次循环,因为prev初始化时为null,此时第一次循环结束,prev就是第一个链表结点;第二次循环 ,此时的curr就是链表的第二个结点至结束的链表,将他的next指向prev,也就是第一个结点,此时的curr就是第二个结点指向第一个结点的链表;接下来依次循环,改链表就被反转了。不得不叹服,真的牛,这个方法,相想出这个方法的人而是真的聪明。
还有一个递归的方法
var reverseList = function(head) {
if (head == null || head.next == null) {
return head;
}
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/reverse-linked-list/solution/fan-zhuan-lian-biao-by-leetcode-solution-d1k2/
来源:力扣(LeetCode)
这里也太难理解了,有个视频讲解的很好,以1->2->3->4为例,首先将1和2->3->4进行反转,就是head.next.next = head,head.next = null,随后递归反转。