本文介绍 JavaScript 中链表的数据结构和常用操作,通过算法题介绍链表的合并删除、反转和环形列表。
数据结构
创建结点
// 构造函数
function ListNode(val) {
this.val = val;
this.next = null;
}
const node = new ListNode(1);
node.next = new ListNode(2);
复制代码
操作结点
- 插入
// 在 node1 和 node2 中插入 node3
const node3 = new ListNode(3);
node3.next = node1.next;
node1.next = node3;
复制代码
- 删除
删除的标准是:链表的遍历过程中,无法遍历到某个结点的存在。
// 删除 node1 和 node2 之间的 node3
node1.next = node1.next.next;
复制代码
算法
链表的应用
关键词:前驱结点、dummy 结点
剑指 Offer 25. 合并两个排序的链表
**处理链表的本质,是处理链表结点之间的指针关系。**将指针想象成一根针,待排序的结点想象为扣子。比较两个链表的结点值的大小,将较小的结点串进去。
var mergeTwoLists = function(l1, l2) {
const dummy = new ListNode();
let cur = dummy;
while(l1 && l2){
if(l1.val<=l2.val){
cur.next = l1;
l1 = l1.next;
cur = cur.next;
} else {
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
cur.next = l1? l1:l2;
return dummy.next;
};
复制代码
83. 删除排序链表中的重复元素
本题考察遍历结点和删除结点的能力。
var deleteDuplicates = function(head) {
let cur = head;
while(cur!=null&&cur.next!=null){
if(cur.val===cur.next.val){
cur.next=cur.next.next;
} else {
cur=cur.next;
}
}
return head;
};
复制代码
82. 删除排序链表中的重复元素 II
本题相较于上一题的区别是,删除所有重复的结点,而不是留下一个。
所以我们必须知道待删除结点的前驱结点。为了方便处理链表的第一个结点,我们需要引入 dummy 结点,它是人为知道的第一个结点的逻辑结点。
var deleteDuplicates = function(head) {
const dummy = new ListNode();
dummy.next = head;
let cur = dummy;
while(cur.next && cur.next.next){
if (cur.next.val===cur.next.next.val){
let val= cur.next.val;
// 删除多个重复的结点
while(cur.next && cur.next.val===val){
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
};
复制代码
快慢指针与多指针
快慢指针是指两个指针往同一个方向遍历,一个快一个慢。
关键词:快慢指针、多指针、dummy 结点
19. 删除链表的倒数第 N 个结点
由于链表的数据结构,我们无法通过下标来确定链表中结点所在的位置。如果想要找到倒数第 n 个结点,我们需要遍历两次链表。
我们可以使用快慢指针,使快指针比满指针快 n 个结点,当快指针到达链表尾部时,慢指针指向所需结点的前驱结点。
这里我们需要使用 dummy 结点处理一些边界情况。我们需要养成使用 dummy 结点的习惯。
var removeNthFromEnd = function(head, n) {
const dummy = new ListNode();
let fast = dummy, slow = dummy;
dummy.next = head;
while(n--){
fast=fast.next;
}
while(fast.next){
fast=fast.next;
slow=slow.next;
}
slow.next = slow.next.next;
return dummy.next;
};
复制代码
206. 反转链表 - 力扣
这道题使用多指针,三个指针 pre,cur,next遍历链表。反转时将 cur 的后继结点指向 pre,然后在依次遍历。next 结点用于记录 cur 结点的原后继结点。
var reverseList = function(head) {
let pre = null;
let cur = head;
while(cur){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};
复制代码
还可以用递归来实现:
var reverseList = function(head) {
if(!head || !head.next) return head;
// 反转链表的首结点
const node = reverseList(head.next);
// 将传入结点的后继结点反转,置空自身的后继结点
head.next.next = head;
head.next = null;
return node;
};
复制代码
92. 反转链表 II
本题是反转局部链表,思路是先找到要反转的开始结点,再依次反转。除此之外,我们还要处理反转之后局部链表的首尾结点。
var reverseBetween = function(head, left, right) {
let dummy= new ListNode();
dummy.next = head;
// p 结点用于记录位置信息,用于连接反转后的首结点
let p = dummy;
// 找到要反转的起始结点的前驱结点
for(let i = 0; i < left - 1; i++){
p=p.next;
}
// 反转,是从起始结点的下一个结点开始反转
let pre = p.next, cur = pre.next;
for(let i = 0; i < right - left; i++){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 处理反转后的首尾结点
p.next.next = cur;
p.next = pre;
return dummy.next;
};
复制代码
环形链表
关键词:标记 flag、快慢指针
141. 环形链表
解题思路是用 flag 标记已经遍历过的结点,若遍历到的结点已被标记,则说明存在环:
var hasCycle = function(head) {
while(head){
if(head.flag===true){
return true;
} else {
head.flag=true;
head=head.next;
}
}
return false;
};
复制代码
142. 环形链表 II
本题需要定位环的起点,思路与上一道题一致,因为第一个被发现的 flag 标记结点一定是环的起点。区别是我们返回的是链表的结点。
var detectCycle = function(head) {
while(head){
if(head.flag===true){
return head;
} else {
head.flag=true;
head=head.next;
}
}
return null;
};
复制代码
这道题还可以使用快慢指针来做,慢指针走一步,快指针走两步。设快指针的路程为 2t
,慢指针路程为t
,环的长度为s
。由于 2t - t = s
,即 t = s
,快慢指针相遇时,慢指针的路程就是环的长度。此时,慢指针到环的起点的距离与快慢指针的起始结点到环的起点距离相等。
var detectCycle = function(head) {
const dummy = new ListNode(0);
dummy.next=head;
let fast=dummy;
let slow=dummy;
while(fast!==null&&fast.next!==null){
fast=fast.next.next;
slow=slow.next;
if(slow===fast){
let newSlow=dummy;
while(newSlow!==slow){
newSlow=newSlow.next;
slow=slow.next;
}
return slow;
}
}
return null;
};
复制代码
Think twice, code once.