Appearance
leetCode 刷题解析
203.移除链表元素
题目描述
- 删除链表中等于给定值 val 的所有节点。
思路
- 虚拟头节点(Dummy Node):
- 创建虚拟头节点
dummy
,其next
指向原链表头节点。这样即使头节点被删除,也能统一处理所有节点。
- 创建虚拟头节点
- 双指针遍历:
pre
指针:指向当前节点的前驱节点,初始指向dummy
。cur
指针:遍历链表,初始指向原头节点。
- 删除逻辑:
- 若
cur.val === val
:跳过当前节点(pre.next = cur.next
),cur
后移。 - 若
cur.val !== val
:pre
和cur
同时后移。
- 若
- 返回结果:返回
dummy.next
(即新链表的头节点)。
关键点解析
- 虚拟头节点:
- 避免单独处理头节点被删除的边界情况。
- 双指针操作:
pre
始终指向当前节点的前驱节点,确保删除操作时链表不断裂。- 删除节点时仅更新指针(
pre.next
),不移动pre
,以应对连续删除场景。
- 时间复杂度:O(n),空间复杂度:O(1)。
示例演示
以链表 1 → 2 → 6 → 3 → 4 → 5 → 6 → null
删除 6
为例:
- 初始化:
dummy → 1 → 2 → 6 → 3 → 4 → 5 → 6 → null
- 删除第一个 6:
pre
停在2
,cur
指向第一个6
。- 执行
pre.next = cur.next
(2
直接指向3
)。
- 删除第二个 6:
pre
仍为2
,cur
指向末尾6
。- 执行
pre.next = cur.next
(5
的next
置为null
)。
- 结果:
1 → 2 → 3 → 4 → 5
。
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
const removeElements = function (head, val) {
if (head === null) return null;
// 创建虚拟头节点
let dummy = {
next: head,
};
// 双指针遍历
let pre = dummy; // 前驱节点
let cur = head; // 当前节点
while (cur) {
if (cur.val === val) {
// 如果相等,前驱跳过当前节点
pre.next = cur.next;
} else {
// cur节奏比pre快,所以直接指向cur
pre = cur;
}
// cur后移
cur = cur.next;
}
return dummy.next;
};
160.相交链表
题目描述
- 找到两个单链表相交的起始节点。如果两个链表没有交点,则返回
null
。
思路
- 双指针法:
- 初始化两个指针
pA
和pB
,分别指向链表A
和B
的头节点。 - 遍历链表,当指针到达链表末尾时,跳到另一个链表的头节点继续遍历。
- 当两个指针相遇时,即为相交节点。
- 初始化两个指针
- 原理:
- 假设链表
A
的长度为m
,链表B
的长度为n
,相交部分长度为c
。
- 假设链表
数学解释:操场比喻与路径平衡原理
- A 操场:总长 40 米(20 米独有跑道 + 20 米共享跑道)
- B 操场:总长 50 米(30 米独有跑道 + 20 米共享跑道)
双指针运动轨迹
- A,B 跑 70 米处相遇
指针 A 路径:
- 先跑完 A 操场独有 20 米
- 再跑完 B 操场独有 30 米
- 最后进入共享跑道
- 总路程 = 20 + 30 + 20 = 70 米
指针 B 路径:
- 先跑完 B 操场独有 30 米
- 再跑完 A 操场独有 20 米
- 最后进入共享跑道
- 总路程 = 30 + 20 + 20 = 70 米
示例演示
场景 1:相交链表
text
链表 A:`4 → 1 → 8 → 4 → 5`
链表 B:`5 → 6 → 1 → 8 → 4 → 5`(相交节点 `8`)
指针 A 路径:4→1→8→4→5→5→6→1→[8]
指针 B 路径:5→6→1→8→4→5→4→1→[8]
↑ 相遇点
场景 2:不相交链表
text
链表 A:`2 → 6 → 4`
链表 B:`1 → 5`
指针 A 路径:2→6→4→1→5→null
指针 B 路径:1→5→2→6→4→null
↑ 同时为 null
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
const getIntersectionNode = function (headA, headB) {
// 边界条件
if (headA === null || headB === null) return null;
let pointA = headA;
let pointB = headB;
// 如果两个链表相交,则两个指针一定会在相交节点处相遇
// 如果两个链表不相交,则两个指针最终会同时到达链表末尾(null)
while (pointA !== pointB) {
// if (pointA === null) {
// pointA = headB;
// } else {
// pointA = pointA.next;
// }
// if (pointB === null) {
// pointB = headA;
// } else {
// pointB = pointB.next;
// }
pointA = pointA ? pointA.next : headB;
pointB = pointB ? pointB.next : headA;
}
return pointA;
};
206.反转链表
题目描述
- 反转一个单链表。
思路
- 通过改变节点指针方向实现链表反转。使用双指针遍历链表,逐个反转节点指向。
初始化指针:
prev
:指向当前节点的前一个节点(初始为null
)curr
:指向当前遍历的节点(初始为头节点head
)
遍历链表:
- 保存当前节点的下一个节点
nextNode
(防止断链) - 将当前节点的
next
指针指向prev
(反转指针方向) - 将
prev
移动到curr
位置 - 将
curr
移动到下一个节点(nextNode)
- 保存当前节点的下一个节点
终止条件:
- 当
curr
变为null
时停止遍历 - 此时
prev
指向新链表的头节点
- 当
示例演示
以链表 1 → 2 → 3 → 4 → 5 → null
为例:
- 初始状态:
prev = null
,curr = 1
- 第一步:
- 保存
next = 2
- 反转:
1.next = null
→1 → null
- 移动指针:
prev = 1
,curr = 2
- 保存
- 第二步:
- 保存
next = 3
- 反转:
2.next = 1
→2 → 1 → null
- 移动指针:
prev = 2
,curr = 3
- 保存
- 重复直至结束:
- 最终得到
5 → 4 → 3 → 2 → 1 → null
- 返回
prev = 5
作为新头节点
- 最终得到
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
const reverseList = function (head) {
if (head === null) return null;
// 初始化指针
let pre = null;
let cur = head;
while (cur) {
// 保存当前节点的下一个节点
let next = cur.next;
// 反转指针方向
cur.next = pre;
// 移动指针
pre = cur;
// 移动到下一个节点
cur = next;
}
return pre;
};
328.奇偶链表
题目描述
- 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
思路
- 分离奇偶节点:
- 使用两个指针
odd
和even
分别遍历奇数节点和偶数节点。 - 奇数节点链:从头节点(节点 1)开始,连接所有奇数位置的节点。
- 偶数节点链:从第二个节点(节点 2)开始,连接所有偶数位置的节点。
- 使用两个指针
- 合并链表:
- 遍历结束后,将奇数链表的尾部连接到偶数链表的头部。
算法步骤
- 边界处理:若链表为空、只有一个节点或只有两个节点,直接返回
head
。 - 初始化指针:
odd
指向头节点(第一个奇数节点)。even
指向第二个节点(第一个偶数节点),并保存evenHead
作为偶数链表的头。
- 遍历链表:
- 将
odd.next
指向even.next
(下一个奇数节点)。 - 移动
odd
到下一个奇数节点。 - 将
even.next
指向odd.next
(下一个偶数节点)。 - 移动
even
到下一个偶数节点。
- 将
- 合并链表:将奇数链表的尾部(
odd
)连接到偶数链表的头部(evenHead
)。
示例演示
示例:1 → 2 → 3 → 4 → 5
- 分离奇偶链:
- 奇数链:
1 → 3 → 5
- 偶数链:
2 → 4
- 奇数链:
- 合并:将奇数链尾部(5)连接偶数链头部(2)
- 结果:
1 → 3 → 5 → 2 → 4
- 双指针
odd
(奇数节点)和even
(偶数节点)同步移动 - 每次操作:
odd.next = even.next
(链接下一个奇数节点)even.next = odd.next
(链接下一个偶数节点)
- 最后用
evenHead
连接两个链表
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var oddEvenList = function (head) {
// 边界条件
if (head === null || head.next === null || head.next.next === null)
return head;
let odd = head; // 奇数节点链表头
let even = head.next; // 偶数节点链表头
let evenHead = even; // 保存偶数节点链表头
// 如果偶数,后面还有节点(奇数)
while (even && even.next) {
// 奇数&偶数节点连接
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
// 奇数节点链表尾部连接偶数节点链表头部
odd.next = evenHead;
return head;
};
92.反转链表||
题目描述
- 反转从位置
m
到n
的链表。请使用一趟扫描完成反转。
思路
- 1、构建⼀个虚拟节点,让它指向原链表的头节点。
- 2、设置两个指针,pre 指针指向以虚拟头节点为链表的头部位置,cur 指针指向原链表的头部位置。
- 3、让着两个指针向前移动,直到 pre 指向了第⼀个要反转的节点的前⾯那个节点,⽽ cur 指向了第⼀个要反转的节点。
- 4、开始指向翻转操作
- 1)、设置临时变量 node,node 是 cur 的 next 位置,保存当前需要翻转节点的后⾯的节点,我们需要交换 node 和 cur
- 2)、让 cur 的 next 位置变成 node 的下⼀个节点
- 3)、让 node 的 next 位置变成 cur
- 4)、让 pre 的 next 位置变成 node
示例演示
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
const reverseBetween = function (head, left, right) {
// 边界条件
if (head === null) return null;
// 创建一个虚拟头节点
let dummy = {
next: head,
};
// 双两个指针
let pre = dummy;
let cur = head;
// 定位到left位置
for (let i = 0; i < left - 1; i++) {
pre = pre.next; // 因为需要反转,所以需要来的需要反转的前一个节点
cur = cur.next;
}
// 反转链表
for (let index = 0; index < right - left; index++) {
let node = cur.next; // 保存当前节点的下一个节点
cur.next = node.next; // 当前节点指向下一个节点的下一个节点
node.next = pre.next; // 下一个节点指向当前节点的下一个节点
pre.next = node; // 当前节点指向下一个节点
}
return dummy.next;
};
234.回文链表
题目描述
- 请判断一个链表是否为回文链表。(正序和倒序读值都一样)
思路
使用快慢指针法解决回文链表问题:
- 快慢指针定位中点:快指针每次走两步,慢指针每次走一步
- 反转后半部分:从慢指针位置开始反转链表后半部分
- 比较前后两半:比较前半部分和反转后的后半部分是否相同
- 恢复链表结构(可选):如果需要保持原链表结构,可以再次反转恢复
示例演示
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
const isPalindrome = function (head) {
// 空链表和单节点链表是回文链表
if (head === null) {
return true;
}
// 2个节点链表,判断是否相等
if (head.next !== null && head.next.next === null) {
return head.val === head.next.val;
}
let slow = head; // 慢
let fast = head; // 快
// 循环结束后,slow指向中间节点,fast指向末尾节点
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
let leftHead = head; // 前半部分链表头
let rightHead = reverseList(slow.next); // 后半部分链表头(反转之后的)
// 循环对比
while (rightHead) {
if (leftHead.val !== rightHead.val) {
return false;
}
leftHead = leftHead.next;
rightHead = rightHead.next;
}
return true;
};
const reverseList = (head) => {
let pre = null;
let cur = head;
while (cur) {
let next = cur.next;
cur.next = pre;
per = cur;
cur = next;
}
return pre;
};
82.删除排序链表中的重复元素||
题目描述
- 给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
思路
是使用哨兵节点(dummy node) 处理头节点可能被删除的情况,通过双指针遍历识别并删除重复元素:
- 哨兵节点:创建一个指向头节点的哨兵节点,解决头节点被删除时的边界问题
- 双指针遍历:
- 使用
cur
指针从哨兵节点开始遍历 - 当检测到重复时(
cur.next.val === cur.next.next.val
):- 记录重复值
x
- 循环删除所有值为
x
的节点
- 记录重复值
- 当无重复时,正常移动指针
- 使用
- 终止条件:当后续不足两个节点时停止检测
示例演示
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
const deleteDuplicates = function (head) {
// 边界条件
if (head === null) return null;
// 创建一个虚拟头节点
let dummy = {
next: head,
};
// 当前节点,从虚拟头节点开始
let cur = dummy;
while (cur.next && cur.next.next) {
// 如果当前节点的下一个节点和下下个节点值相等,则说明有重复节点
// 删除重复节点
if (cur.next.val === cur.next.next.val) {
let x = cur.next.val;
while (cur.next && cur.next.val === x) {
cur.next = cur.next.next;
}
} else {
// 否则,当前节点后移
cur = cur.next;
}
}
return dummy.next;
};
83.删除排序链表中的重复元素
题目描述
- 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
思路
题目要求删除排序链表中的重复元素,保留每个元素的第一次出现。由于链表已排序,重复元素必然相邻,因此只需遍历一次链表:
- 单指针遍历:使用指针
cur
从链表头开始遍历 - 重复检测:比较当前节点与下一节点的值
- 若相等:跳过下一节点(
cur.next = cur.next.next
) - 若不相等:移动指针到下一节点
- 若相等:跳过下一节点(
- 终止条件:当当前节点或下一节点为空时停止
示例演示
示例演示
以输入链表 [1, 1, 2, 3, 3]
为例:
- 第一轮:
1
与1
重复 → 跳过第二个1
,链表变为[1, 2, 3, 3]
- 第二轮:
1
与2
不重复 → 移动指针到2
- 第三轮:
2
与3
不重复 → 移动指针到3
- 第四轮:
3
与3
重复 → 跳过第二个3
,链表变为[1, 2, 3]
- 返回结果:
[1, 2, 3]
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function (head) {
// 边界条件
if (head === null) return null;
// 当从节点开始
let cur = head;
// 如果有下一个节点,循环
while (cur && cur.next) {
// 如果当前节点和下一个节点值相等,跳过下一个节点
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
// 否则,移动指针到下一个节点
cur = cur.next;
}
}
return head;
};
86.分割链表
题目描述
- 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
思路
创建两个虚拟(Dummy Nodes):
minHead
:用于链接所有小于x
的节点。maxHead
:用于链接所有大于等于x
的节点。
遍历原链表:
- 若当前节点值
< x
,将其链接到minHead
的链表末尾。 - 若当前节点值
>= x
,将其链接到maxHead
的链表末尾。
- 若当前节点值
合并链表:
- 将
minHead
链表的末尾指向maxHead
链表的头节点(跳过哑节点)。 - 将
maxHead
链表的末尾节点的next
置为null
(避免循环)。
- 将
返回结果:
- 返回
minHead.next
(跳过哑节点)。
- 返回
示例演示
以输入 head = [1,4,3,2,5,2]
, x = 3
为例:
- 初始化两个虚拟节点:
minHead
和maxHead
。 - 遍历链表:
1 < 3
→ 加入min
链表:minHead → 1
4 >= 3
→ 加入max
链表:maxHead → 4
3 >= 3
→ 加入max
链表:maxHead → 4 → 3
2 < 3
→ 加入min
链表:minHead → 1 → 2
5 >= 3
→ 加入max
链表:maxHead → 4 → 3 → 5
2 < 3
→ 加入min
链表:minHead → 1 → 2 → 2
- 合并链表:
min
链表末尾(节点2
)指向max
链表头(节点4
):1 → 2 → 2 → 4 → 3 → 5
- 将
max
链表末尾(节点5
)的next
置为null
。
- 返回
minHead.next
(即节点1
)。
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} x
* @return {ListNode}
*/
const partition = function (head, x) {
if (head === null) return null;
let minHead = {
next: null,
};
let min = minHead;
let maxHead = {
next: null,
};
let max = maxHead;
let cur = head;
while (cur) {
if (cur.val < x) {
min.next = cur;
min = min.next;
} else {
max.next = cur;
max = max.next;
}
cur = cur.next;
}
// 后面可能有小于x的节点,所以需要断开
max.next = null;
min.next = maxHead.next;
return minHead.next;
};
141.环形链表
题目描述
- 给定一个链表,判断链表中是否有环。
思路
快慢指针法(Floyd 判圈算法):
- 使用两个指针:
slow
(每次移动 1 步)和fast
(每次移动 2 步)。 - 如果链表有环,快指针最终会追上慢指针(相遇)。
- 如果链表无环,快指针会先到达链表尾部(
fast
或fast.next
为null
)。
示例演示
有环链表
1 → 2 → 3 → 4 → 5 → 2
(环入口在 2):- 初始:
slow=1, fast=1
- 步骤 1:
slow=2, fast=3
- 步骤 2:
slow=3, fast=5
- 步骤 3:
slow=4, fast=3
(5→2→3) - 步骤 4:
slow=5, fast=5
(相遇 → 返回true
)
- 初始:
无环链表
1 → 2 → 3 → 4 → 5 → null
:- 初始:
slow=1, fast=1
- 步骤 1:
slow=2, fast=3
- 步骤 2:
slow=3, fast=5
(fast.next=null
→ 退出循环 → 返回false
)
- 初始:
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
const hasCycle = function (head) {
if (head === null) return false; // 空链表直接返回false
let slow = head;
let fast = head;
// 循环条件确保快指针能安全移动两步
while (fast.next && fast.next.next) {
slow = slow.next; // 慢指针移动1步
fast = fast.next.next; // 快指针移动2步
if (slow === fast) {
// 相遇说明有环
return true;
}
}
return false; // 快指针到达尾部,无环
};
142.环形链表||
题目描述
- 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回
null
。
思路
快慢指针法(Floyd 判圈算法扩展):
- 阶段一:检测链表是否有环
- 使用快指针(每次 2 步)和慢指针(每次 1 步)遍历链表
- 若快慢指针相遇,说明链表有环
- 阶段二:定位环入口节点
- 将其中一个指针移回链表头部
- 两个指针以相同速度(每次 1 步)移动
- 再次相遇的节点即为环入口节点
示例演示
有环链表 1 → 2 → 3 → 4 → 5 → 2
(环入口为节点 2):
- 阶段一:快慢指针移动
- 初始:slow=1, fast=1
- 第 1 步:slow=2, fast=3
- 第 2 步:slow=3, fast=5
- 第 3 步:slow=4, fast=3(5→2→3)
- 第 4 步:slow=5, fast=5(相遇)
- 阶段二:定位环入口
- index1 从头节点 1 开始
- index2 从相遇点 5 开始
- 第 1 步:index1=2, index2=2(相遇)
- 返回节点 2
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
const detectCycle = function (head) {
if (head === null) return null;
let slow = head;
let fast = head;
// 阶段一:检测是否有环
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
// 检测到环
if (slow === fast) {
// 阶段二:定位环入口
let index1 = head; // 指针1从头节点出发
let index2 = fast; // 指针2从相遇点出发
// 两指针同步移动直到相遇
while (index1 !== index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1; // 相遇点即为环入口
}
}
return null; // 无环
};
21.合并 2 个有序链表
题目描述
- 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路
迭代法:
- 边界处理:
- 若两链表均为空 → 返回
null
- 若一链表为空 → 返回另一链表
- 若两链表均为空 → 返回
- 虚拟头节点:
- 创建
dummy
节点简化边界处理 - 用
head
指针追踪合并链表的尾部
- 创建
- 双指针比较:
- 同时遍历两链表
- 比较节点值,将较小值接入合并链表
- 移动较小值所在链表的指针
- 处理剩余节点:
- 当一链表遍历完,将另一链表剩余部分直接接入
- 返回结果:
- 返回
dummy.next
(真实头节点)
- 返回
示例演示
输入:list1 = 1 → 3 → 5
list2 = 2 → 4 → 6
合并过程:
- 比较
1
vs2
→ 接入1
:合并链表:dummy → 1
list1 → 3, list2 → 2
- 比较
3
vs2
→ 接入2
:合并链表:dummy → 1 → 2
list1 → 3, list2 → 4
- 比较
3
vs4
→ 接入3
:合并链表:dummy → 1 → 2 → 3
list1 → 5, list2 → 4
- 比较
5
vs4
→ 接入4
:合并链表:dummy → 1 → 2 → 3 → 4
list1 → 5, list2 → 6
- 比较
5
vs6
→ 接入5
:合并链表:dummy → 1 → 2 → 3 → 4 → 5
list1 → null
- 接入
list2
剩余节点6
:合并链表:dummy → 1 → 2 → 3 → 4 → 5 → 6
输出:1 → 2 → 3 → 4 → 5 → 6
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function (list1, list2) {
// 边界处理
if (list1 === null && list2 === null) return null;
// 若一链表为空 → 返回另一链表
if (list1 === null) return list2;
if (list2 === null) return list1;
// 虚拟头节点
let dummy = {
next: null,
};
let head = dummy;
/// 双指针比较
while (list1 && list2) {
if (list1.val <= list2.val) {
head.next = list1;
list1 = list1.next;
} else {
head.next = list2;
list2 = list2.next;
}
// 移动合并链表尾部指针
head = head.next;
}
// 直接连接剩余链表
head.next = list1 || list2;
return dummy.next;
};
138.随机链表的复制
题目描述
- 给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或null
。
思路
复制带随机指针的链表需要解决的关键问题是:当复制节点时,随机指针指向的节点可能尚未创建。使用哈希表(Map)存储原节点到新节点的映射,通过两次遍历解决:
- 第一次遍历:创建所有新节点,建立原节点到新节点的映射(包括 null 映射)
- 第二次遍历:根据映射关系,设置新节点的 next 和 random 指针
示例演示
代码实现
javascript
/**
* // Definition for a _Node.
* function _Node(val, next, random) {
* this.val = val;
* this.next = next;
* this.random = random;
* };
*/
/**
* @param {_Node} head
* @return {_Node}
*/
var copyRandomList = function (head) {
if (head === null) return null;
// 创建哈希表并处理null映射
const map = new Map();
map.set(null, null); // 关键:确保null映射到null
// 第一次遍历:创建所有新节点
let cur = head;
while (cur) {
map.set(cur, new _Node(cur.val, null, null));
cur = cur.next;
}
// 第二次遍历:获取所以的新节点映射
cur = head;
while (cur) {
const newNode = map.get(cur);
newNode.next = map.get(cur.next);
newNode.random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
};
24.两两交换链表中的节点
题目描述
- 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
思路
要两两交换链表中的节点,可以通过迭代的方式实现。使用一个虚拟节点(dummy node)简化头节点交换的操作。遍历链表时,每次处理两个相邻节点:
- 保存当前节点的下一个节点(第一个节点)和下一个节点的下下个节点(下一对的起始节点)。
- 将当前节点的下一个节点指向第二个节点。
- 将第二个节点的下一个节点指向第一个节点。
- 将第一个节点的下一个节点指向保存的下一对起始节点。
- 移动当前指针到交换后的第一个节点位置,继续下一轮交换。
示例演示
以链表 1->2->3->4
为例:
- 初始状态:dummy -> 1 -> 2 -> 3 -> 4
- 第一轮交换(处理节点 1 和 2):
- 当前节点为 dummy,交换后:dummy -> 2 -> 1 -> 3 -> 4
- 移动当前节点到 1(交换后的第二个节点)
- 第二轮交换(处理节点 3 和 4):
- 交换后:dummy -> 2 -> 1 -> 4 -> 3
- 移动当前节点到 3
- 最终结果:2->1->4->3
代码实现
js
const swapPairs = function (head) {
if (head === null) return null;
// 创建虚拟节点(需要交换节点的前一个节点)
let dummy = { next: head };
// 当前指针
let cur = dummy;
// 迭代,如果当前节点和下一个节点都存在,则进行交换
while (cur.next && cur.next.next) {
let temp1 = cur.next;
let temp2 = cur.next.next.next;
cur.next = cur.next.next;
cur.next.next = temp1;
temp1.next = temp2;
cur = temp1; // cur指针移动到下一个节点(需要交换节点的前一个节点)
}
return dummy.next;
};
61.旋转链表
题目描述
- 给你一个链表的头节点
head
,旋转链表,将链表每个节点向右移动k
个位置。
思路
示例演示
代码实现
js
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
const rotateRight = function (head, k) {
if (head === null || k === 0) return head;
let len = 1;
let temp = head;
while (temp.next) {
temp = temp.next;
len++;
}
k = k % len; // 处理k大于链表长度的情况
let slow = head;
let fast = head;
for (let i = 0; i < k; i++) {
slow = slow.next;
}
while (slow.next) {
slow = slow.next;
fast = fast.next;
}
slow.next = head;
let newHead = fast.next;
fast.next = null;
return newHead;
};
143.重排链表
题目描述
- 给定一个单链表 L:L0→L1→…→Ln-1→Ln ,将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
- 1->2->3->4->5 重排为 1->5->2->4->3
思路
重排链表的关键是将链表分成两半,反转后半部分,然后交替合并。具体步骤如下:
快慢指针找中点:
- 使用快慢指针技巧找到链表中点(慢指针每次走 1 步,快指针每次走 2 步)
- 当快指针到达末尾时,慢指针正好指向中点(奇数链表指向中间节点,偶数链表指向前半部分末尾)
断开并反转后半部分:
- 将链表从中点后断开,形成两个独立链表
- 反转后半部分链表(使最后节点变为头节点)
交替合并链表:
- 将前半部分和反转后的后半部分交替合并
- 每次取前半部分一个节点和后半部分一个节点连接
示例演示
示例 1:1->2->3->4
- 找中点:slow 停在 2(fast 停在 3)
- 断开:前半部分 1->2,后半部分 3->4
- 反转后半部分:4->3
- 交替合并:
- 1->4->2->3
示例 2:1->2->3->4->5
- 找中点:slow 停在 3(fast 停在 5)
- 断开:前半部分 1->2->3,后半部分 4->5
- 反转后半部分:5->4
- 交替合并:
- 1->5->2->4->3
代码实现
javascript
const reorderList = function (head) {
// 空链表或单节点直接返回
if (!head || !head.next) return;
// 1. 快慢指针找中点(slow停在前半部末尾)
let slow = head;
let fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 断开链表并反转后半部分
let secondHalf = slow.next; // 后半部分头节点
slow.next = null; // 关键:断开前后链表
// 反转后半部分
let prev = null;
let curr = secondHalf;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
secondHalf = prev; // prev成为反转后的头节点
// 3. 交替合并两个链表
let first = head;
let second = secondHalf;
while (first && second) {
// 保存下一节点
const firstNext = first.next;
const secondNext = second.next;
// 交替连接节点
first.next = second;
second.next = firstNext;
// 移动指针
first = firstNext;
second = secondNext;
}
};
876.链表的中间节点
题目描述
- 给定一个链表,返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。
思路
寻找链表中间节点的标准解法是使用快慢指针技巧:
- 快指针每次移动两步
- 慢指针每次移动一步
- 当快指针到达链表末尾时,慢指针正好位于中间位置
关键点:
- 链表节点数为奇数时,慢指针停在唯一中间节点
- 链表节点数为偶数时,慢指针停在第二个中间节点(题目要求)
示例演示
示例 1:奇数节点 (1→2→3→4→5)
步骤 slow fast
0: 1 1
1: 2 3
2: 3 5 → 停止(fast.next=null)
返回节点3
示例 2:偶数节点 (1→2→3→4→5→6)
步骤 slow fast
0: 1 1
1: 2 3
2: 3 5
3: 4 null → 停止
返回节点4
代码实现
javascript
const middleNode = function (head) {
let slow = head;
let fast = head;
// 快指针每次移动两步,慢指针每次移动一步
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
};
19.删除链表的倒数第 N 个节点
题目描述
- 给定一个链表,删除链表的倒数第
n
个节点,并返回链表的头节点。
思路
使用双指针(快慢指针)技巧:
- 虚拟节点(Dummy Node):创建虚拟节点指向头节点,简化删除头节点的边界处理
- 快指针先走:快指针先向前移动
n+1
步,使快慢指针间距为n+1
- 同步移动:同时移动快慢指针,当快指针到达尾部时,慢指针正好停在待删除节点的前驱位置
- 删除节点:修改慢指针的
next
指向,跳过待删除节点
示例演示
以 head = [1,2,3,4,5]
, n = 2
为例:
- 初始状态:
dummy(0) → 1 → 2 → 3 → 4 → 5
- 快指针前进 3 步:
fast
停在 3,slow
停在dummy
- 同步移动:
fast=4, slow=1
fast=5, slow=2
fast=null, slow=3
(此时slow
停在待删除节点 4 的前驱)
- 删除节点:
slow.next = slow.next.next
→ 链表变为[1,2,3,5]
代码实现
javascript
var removeNthFromEnd = function (head, n) {
// 创建虚拟节点
const dummy = {
next: head,
};
let fast = dummy,
slow = dummy;
// 快指针先走 n+1 步
for (let i = 0; i <= n; i++) {
fast = fast.next;
}
// 同步移动双指针
while (fast) {
fast = fast.next;
slow = slow.next;
}
// 删除目标节点
slow.next = slow.next.next;
return dummy.next; // 返回新头节点
};
704.二分查找
题目描述
- 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果 target 存在返回下标,否则返回 -1。
思路
示例演示
代码实现
js
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function (nums, target) {
let l = 0;
let r = nums.length - 1;
while (l <= r) {
let m = (l + r) >> 1;
if (nums[m] > target) {
r = m - 1;
} else if (nums[m] < target) {
l = m + 1;
} else {
return m;
}
}
return -1;
};
27.移除元素
题目描述
- 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。
思路
示例演示
代码实现
js
/**
* @param {number[]} nums
* @param {number} val
* @return {number}
*/
var removeElement = function (nums, val) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== val) {
nums[slow++] = nums[fast];
}
}
return slow;
};
977.有序数组的平方
题目描述
- 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
思路
示例演示
代码实现
js
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function (nums) {
let n = nums.length;
let res = new Array(n).fill(0);
let l = 0;
let r = n - 1;
let k = n - 1;
while (l <= r) {
let left = nums[l] * nums[l];
let right = nums[r] * nums[r];
if (left > right) {
res[k--] = left;
l++;
} else {
res[k--] = right;
r--;
}
}
return res;
};
209.长度最小的子数组
题目描述
- 给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
思路
示例演示
代码实现
js
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
const minSubArrayLen = function (target, nums) {
let sum = 0;
let left = 0;
let result = Infinity;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
}
return result === Infinity ? 0 : result;
};
3. 无重复字符的最长子串
题目描述
- 给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
思路
示例演示
代码实现
js
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function (s) {
let left = 0;
let result = 0;
let charSet = new Set();
for (let right = 0; right < s.length; right++) {
let str = s[right];
while (charSet.has(str)) {
charSet.delete(s[left++]);
}
result = Math.max(result, right - left + 1);
charSet.add(str);
}
return result;
};