算法通关 1 - 链表专题Ⅰ
总结
- 常见的数据结构: 数组、链表、栈、队列、哈希、树、堆
- 常用的算法思想: 查找、排序、双指针、递归、迭代、分治、贪心、回溯、动态规划
链表专题Ⅰ—链表基础
内容概览
题目 | 难度 |
---|---|
链表遍历 | 简单 |
链表插入 | 简单 |
删除链表 | 简单 |
链表头插法 | 简单 |
链表尾插法 | 简单 |
参考资料
单链表
单链表包含若干节点,每个节点具有指向后继结点的 next 指针,最后一个节点的 next 指向 NULL。
正确示范
错误示范
虚拟节点和头节点
节点和头节点
链表中每个节点都由值和指向下一个节点的地址组成,对于单链表,第一个元素节点称为头节点
虚拟节点
- 即
dummyNode
,其next
指针指向head
- 如果获取
head
节点,或者从方法里返回时,使用dummyNode.next
dummyNode
的val
不会被使用,通常初始化成 0 或 -1
创建链表
JVM 如何构建链表的?
JVM 下栈区存放实际对象的引用地址,堆区存放创建的对象:
1 | public class Course { |
此时的 teacher 和 student 就是指向堆的引用,若我们这样定义:
1 | public class Course { |
此时 next 指向下一个同为 Course 类的对象
这里通过栈中引用找到 val(1)
,然后 val(1)
节点存放指向 val(2)
的地址,val(3)
存放指向 val(4)
的地址。
DEBUG 图示:
链表从 head
开始,逐个访问,每次访问的对象类型完全一样的。
根据面向对象理论,Java 规范的链表定义:
1 | public class ListNode { |
LeetCode 链表结构参考:
1 | public class ListNode { |
LeetCode 结构违背面向对象设计要求,代码更为精简,算法题目中应用广泛。
链表增删改查
遍历链表
对于单链表,无论进行何操作,操作后是否还能找到表头非常重要。
1 | // 遍历链表长度 |
插入链表
单链表的插入需要考虑头部、中部、尾部情况。
在单链表的表头插入:
从表头插入,不要忘记 head
重新指向表头。
1 | // 新建点指向 head |
在单链表的中间插入:
在中间位置插入,必须遍历找到插入位置,然后将当前位置接入到前驱节点和后继节点之间,但是到了该位置后无法获取前驱节点,为此我们在目标节点的前一个位置停下,使用 cur.next
值进行判断:
例如我们在下图中 7 的前面插入,当 cur.next = node(7)
就应该停下来,具体操作如下。
1 | // 在 7 前面插入节点,则 cur.next = node(7) 就应该停下,此时 cur.val = 15 |
在单链表的尾部插入:
1 | // 尾节点指向新节点 |
单链表插入方法实现:
1 | /** |
扩展:若链表要求单调递增,请将元素插入合适位置,并使链表仍程单调递增,请写出算法实现?
删除链表
单链表的删除需要考虑头部、中部、尾部情况。
删除单链表的表头节点:
1 | // 一般执行 `head = head.next` 即可。 |
删除单链表的表尾节点:
找到待删除节点的前驱节点,然后执行 cur.next = null
即可。
例如删除 40,器前驱为 7,遍历时需要判断 cur.next
是否为 40,然后执行 cur.next = null
删除单链表的中间节点:
删除中间节点时,使用 cur.next
比较,找到位置后将 cur.next
的值 更新为 cur.next.next
单链表删除方法实现:
1 | /** |
扩展:若链表是有序的,请删除某元素,并使链表仍程单调递增,请写出算法实现?
头插法和尾插法
问题
假设有 n 个元素存储在数组 a 中,使用尾插法和头插法建立单链表 C
尾插法
1 | void createListR(LNode *&C, int a[], int n) { |
头插法
1 | void createListF(LNode *&C, int a[], int n) { |
链表专题Ⅰ—链表强化
内容概览
题目 | 难度 |
---|---|
两个链表第一个公共子节点 | 简单 |
判断链表是否是回文序列 | 中等 |
合并两个有序链表 | 简单 |
合并两个链表 | 简单 |
合并 K 个有序链表 | 中等 |
寻找链表的中间节点 | 简单 |
寻找倒数第 K 个元素 | 简单 |
旋转链表 | 中等 |
删除链表特定节点 | 简单 |
删除链表倒数第 n 个节点 | 简单 |
删除链表重复元素(重复的保留一个) | 简单 |
删除链表重复元素(重复的都删除) | 中等 |
节点结构
1 | public class ListNode { |
两个链表第一个公共子节点
题目
输入两个链表,请找出它们的第一个公共节点。两个链表的头节点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的节点数是未知的,请设计算法包找出两个链表的合并点
【推荐】使用哈希集合
算法思想
判断两个链表是否相交,可以使用哈希集合存储链表节点。
首先遍历链表 headA
,并将链表 headA
中的每个节点加入哈希集合中。然后遍历链表 headB
,对于遍历到的每个节点,判断该节点是否在哈希集合中:
- 如果当前节点不在哈希集合中,则继续遍历下一个节点;
- 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表
headB
中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。
如果链表 headB
中的所有节点都不在哈希集合中,则两个链表不相交,返回 null
。
参考答案
1 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
【推荐】使用双指针
使用双指针的方法,可以将空间复杂度降至 $O(1)$
算法思想
只有当链表 headA
和 headB
都不为空时,两个链表才可能相交。因此首先判断链表 headA
和 headB
是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null
。
当链表 headA
和 headB
都不为空时,创建两个指针 pA
和 pB
,初始时分别指向两个链表的头节点 headA
和 headB
,然后将两个指针依次遍历两个链表的每个节点
- 每步操作需要同时更新指针
pA
和pB
。 - 如果指针
pA
不为空,则将指针pA
移到下一个节点;如果指针pB
不为空,则将指针pB
移到下一个节点 - 如果指针
pA
为空,则将指针pA
移到链表headB
的头节点;如果指针pB
为空,则将指针pB
移到链表headA
的头节点。 - 当指针
pA
和pB
指向同一个节点或者都为空时,返回它们指向的节点或者null
。
证明过程
下面提供双指针方法的正确性证明。考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。
情况一:两个链表相交
链表 headA
和 headB
的长度分别是 m
和 n
。假设链表 headA
的不相交部分有 a
个节点,链表 headB
的不相交部分有 b
个节点,两个链表相交的部分有 c
个节点,则有 $a+c=m$,$b+c=n$。
- 如果 $a=b$,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点;
- 如果 $a≠b$,则指针
pA
会遍历完链表headA
,指针pB
会遍历完链表headB
,两个指针不会同时到达链表的尾节点,然后指针pA
移到链表headB
的头节点,指针pB
移到链表headA
的头节点,然后两个指针继续移动,在指针pA
移动了 $a+c+b$ 次、指针pB
移动了 $b+c+a$ 次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。
情况二:两个链表不相交
链表 headA
和 headB
的长度分别是 m
和 n
。考虑当 $m=n$ 和 $m≠n$ 时,两个指针分别会如何移动:
- 如果 $m=n$,则两个指针会同时到达两个链表的尾节点,然后同时变成空值
null
,此时返回null
; - 如果 $m≠n$,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针
pA
移动了 $m+n$ 次、指针pB
移动了 $n+m$ 次之后,两个指针会同时变成空值null
,此时返回null
。
参考答案
1 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
判断链表是否是回文序列
题目
【LeetCode 234】:给你一个单链表的头节点 head
,请你判断该链表是否为回文链表。如果是,返回 true
;否则,返回 false
。
思路 1:(暴力方案 | 利用栈)
- 将链表元素全部压栈
- 然后进行出栈操作,出栈同时遍历链表
- 比较出栈元素和遍历元素值
- 直到出栈完成,如果中途出现不相等情况,说明不是回文链表
思路 2:(优化方案 | 利用栈)
- 第一次遍历整个链表,获取链表总长度
- 第二次遍历链表,遍历链表同时进行压栈操作
- 到达链表长度一半后不进行压栈操作,而是一边出栈一边遍历
- 期间比较两个操作下的元素是否相等,直到出栈完成
- 如果出现不相等情况,说明不是回文链表
思路 3:(优化方案 | 利用栈)
- 第一次遍历整个链表,获取链表总长度,遍历链表同时压栈处理元素
- 第二次遍历链表,只需要遍历一半链表(同时仅需出栈一半元素)
- 期间比较两个操作下的元素是否相等,直到出栈完成
- 如果出现不相等情况,说明不是回文链表
思路 4:(暴力方案 | 反转链表)
- 开辟新存储空间,存储反转后的链表
- 遍历两个链表进行比较元素
思路 5:(优化方案 | 反转链表)
- 第一次遍历整个链表,获取链表长度
- 开辟存储空间准备存储半段链表,反转前半段链表
- 比较新的链表和剩下的链表的元素
思路 6:(优化方案 | 快慢指针、反转链表)
- 使用快慢指针找到中点
- 反转中点后的链表
- 比较原始链表的前半段和反转后的后半段的元素
思路 7:(优化方案 | 递归)
如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。
【推荐】使用快慢指针
算法思路 (若链表有奇数个节点,则中间的节点应该看作是前半部分。)
将链表的后半部分反转,然后将前半部分和后半部分进行比较。比较完成后应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。
- 找到前半部分链表的尾节点
- 反转后半部分链表
- 判断是否回文
- 恢复链表
- 返回结果
找到前半部分链表的尾节点
- 使用快慢指针在一次遍历中找到
- 慢指针一次走一步,快指针一次走两步,快慢指针同时出发
- 当快指针移动到链表的末尾时,慢指针恰好到链表的中间。通过慢指针将链表分为两部分。
若链表有奇数个节点,则中间的节点应该看作是前半部分。
反转后半部分链表
可以使用「206. 反转链表」问题中的解决方法来反转链表的后半部分。
参考答案
1 | // 快慢指针 + 反转链表 |
1 | // 反转链表(扩展:如果带虚拟节点可以这样写:) |
使用递归
算法思想
为想出使用空间复杂度为 $O(1)$ 的算法,你可能想过使用递归来解决,但是这仍然需要 $O(n)$ 的空间复杂度。
递归为我们提供了一种优雅的方式来方向遍历节点。
1 | function print_values_in_reverse(ListNode head) |
如果使用递归反向迭代节点,同时使用递归函数外的变量向前迭代,就可以判断链表是否为回文。
算法原理
currentNode
指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer
是递归函数外的指针。
若 currentNode.val != frontPointer.val
则返回 false
。反之,frontPointer
向前移动并返回 true
。
算法的正确性在于递归处理节点的顺序是相反的(回顾上面打印的算法),而我们在函数外又记录了一个变量,因此从本质上,我们同时在正向和逆向迭代匹配。
具体请看:官方动画解释
1 | class Solution { |
合并链表专题
合并两个有序链表
题目
【LeetCode 21】:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
【推荐】迭代归并
算法思想
构建新链表,分别遍历两个链表,每次选择最小的节点接到新链表上,不断迭代直到结束1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 新链表头节点(虚拟节点)
ListNode L = new ListNode(0);
// 保存尾指针
ListNode tail = L;
// 尾插法
while (list1 != null && list2 != null) { // 两个链表都不为空时
if (list1.val < list2.val) {
tail.next = list1;
list1 = list1.next;
} else if (list1.val > list2.val) {
tail.next = list2;
list2 = list2.next;
} else { // 相等时,两个节点都接上去
tail.next = list1;
list1 = list1.next;
tail = tail.next;
tail.next = list2;
list2 = list2.next;
}
tail = tail.next;
}
while (list1 != null) { // L1 链表未遍历完时
tail.next = list1;
list1 = list1.next;
tail = tail.next;
}
while (list2 != null) { // L2 链表未遍历完时
tail.next = list2;
list2 = list2.next;
tail = tail.next;
}
return L.next;
}
迭代优化
- 优化 1:第一处
while
三种情况中的如果出现两个相同元素,将此处情况归并到第一种情况if (list1.val <= list2.val)
,那么另一个相同的元素会被第二种情况处理掉。
比如list1[1, 5, 8, 2]
和list2[2, 5, 9, 13]
,当两个链表都到元素5
时,此时先合并list1
中的node(5)
,然后list
走下一个元素8
,此时list2
停留在node(5)
,它将会走第二个情况 - 优化 2:循环终止时,
list1
和list2
至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表非空,它包含的所有元素都比前面已经合并链表中的所有元素都要大,因此只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。
1 | public ListNode mergeTwoLists(ListNode list1, ListNode list2) { |
递归归并
如果 list1
或者 list2
一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 list1
和 list2
哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
1 | public ListNode mergeTwoLists(ListNode list1, ListNode list2) { |
合并 K 个链表
题目
【LeetCode 23】:给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
【推荐】顺序归并
算法思想
使用 ans
维护以及合并的链表,第 i
循环把第 i
个链表和 ans
合并,答案保存到 ans
中
1 | public ListNode mergeKLists(ListNode[] lists) { |
【推荐】分治归并
【归并排序】算法思想
采用分治发优化顺序合并:
- 将
k
个链表配对并将同一对中的链表合并 - 第一轮合并后,
k
个链表合并成k/2
个链表,然后是k/4
个链表,8/k
个链表等; - 重复这一过程,直到得到最终的有序链表
1 | public ListNode mergeKLists(ListNode[] lists) { |
小顶堆归并
算法思想
- 维护当前每个链表未合并的元素的最前面一个,k 个链表就最多有 k 个满足这样条件的元素
- 每次在这些元素里面选取 val 属性最小的元素合并到答案中
- 使用小顶堆数据结构选取最小元素
1 | // 优先队列合并 |
合并两个链表
题目
【LeetCode 1669】:给你两个链表 list1
和 list2
,它们包含的元素分别为 n
个和 m
个。请你将 list1
中下标从 a
到 b
的全部节点都删除,并将list2
接在被删除节点的位置。
下图中蓝色边和节点展示了操作后的结果:
请你返回结果链表的头指针。
算法思想
- 找到
list1
中第a-1
个节点preA
,以及第b+1
个节点atfB
- 由于 $1\le{a}\le{v}<{n-1}$,因此
preA
和atfB
一定存在 - 然后让
preA
的next
指向list2
的头节点,再让list2
的尾节点的next
指向atfB
即可
1 | public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) { |
快慢指针专题
寻找中间节点
题目
【LeetCode 876】:给你单链表的头结点 head
,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
【推荐】快慢指针
算法思想
用两个指针 slow
与 fast
一起遍历链表。slow
一次走一步,fast
一次走两步。那么当 fast
到达链表的末尾时,slow
必然位于中间。
备注:如果有两个中间结点,则返回第 1 个中间结点,请参考 判断链表是否是回文序列#【推荐】使用快慢指针
1 | // 不带虚拟节点 |
寻找倒数第 K 个元素
题目
【面试题 02.02.】:实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
示例
1 | input: list[1,2,3,4,5], k = 2 |
【推荐】快慢指针
算法思想
- 快指针先移动 $k$ 步,然后慢指针再从头开始和快指针同时移动
- 当快指针到链表末尾时,慢指针指向的节点即为目标节点
- 特别注意链表长度小于 $k$ 情况,寻找 $k$ 位置时必须判断
fast
是否为 null
1 | public int kthToLast(ListNode head, int k) { |
旋转链表
题目
【LeetCode 61】:给你一个链表的头节点 head
,旋转链表,将链表每个节点向右移动 k
个位置。
【推荐】快慢指针
算法思想
使用双指针找到倒数 $k$ 的位置,然后拼接两个链表即可。比如 [1,2,3,4,5]
拆成 [1,2,3]
和 [4,5]
,然后拼接两个链表
- 已知链表长度
len
,求k=k%len
如果
k==0
则无需旋转,否则进行下面操作- 快指针先走 $k$ 步,然后慢指针和快指针一起走
- 快指针走到表尾时,慢指针所在位置即为拆分位置
- 把快指针指向的节点连接至表头,然后慢指针执行断链操作
- 最终返回新链表的头节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47// 快慢指针
public ListNode rotateRight(ListNode head, int k) {
// base case
if (head == null || k == 0) {
return head;
}
int len = 0;
// H 记录 head 位置
ListNode H = head;
// 快指针
ListNode fast = head;
// 慢指针
ListNode slow = head;
while (head != null) {
head = head.next;
len++;
} // 此情况下的 head 指向 null
if (k % len == 0) {
return H;
}
// 此时 fast 从头开始走 K 步
// 要注意 K 超出 len 的部分,使用模运算抵消
// len = 5 时,k = 2 和 7 一样效果
while ((k % len) > 0) {
k--;
fast = fast.next;
}
// 在快指针走了 k 步后,快慢指针一起行动
// 当 fast 走到最后一个节点(fast.next == null)时
// slow 处于倒数 k 个节点的前驱
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 记录新链表头节点的位置
ListNode L = slow.next;
// 拆分链表
slow.next = null;
fast.next = H;
return L;
}
【推荐】构造循环链表
算法思想
记给定链表的长度为 n
,注意到当向右移动的次数 $k≥n$ 时,我们仅需要向右移动 $k%n$ 次即可(每移动 n
次都会使链表变为原状),新链表的最后一个节点为原链表的第 (n-1)-(k%n)
个节点(从 0 开始计数)。我们可以先将给定的链表连接成环,然后将指定位置断开。
- 首先计算链表长度
n
,并找到链表的尾节点,将其与头节点相连,构造循环链表 - 然后找到新链表的最后一个节点,将当前闭合尾环的链表断开即可
- 特别注意,当链表不大于
1
或k
为n
的倍数时,新链表与原链表相同,不需要任何处理
1 | public ListNode rotateRight(ListNode head, int k) { |
删除链表元素专题
更多题目
LeetCode 19.删除链表的倒数第 N 个结点
LeetCode 82.删除排序链表中的重复元素 II
LeetCode 83.删除排序链表中的重复元素
LeetCode 203.移除链表元素
LeetCode 237.删除链表中的节点
LeetCode 1474.删除链表 M 个节点之后的 N 个节点
删除特定节点
题目
【LeetCode 203】:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
迭代删除
算法思想
- 由于链表的头节点
head
有可能删除,因此创建虚拟节点dummyNode
,使其指向head
- 使用
cur
表示当前节点,如果cur
的下一个节点不为空且下一个节点值等于给定的val
,则需要删除下一个节点 - 如果
cur
的下一个节点值不等于给定的val
,则保留下一个节点,将cur
移至下一个节点即可 - 当
cur
的下一个节点为空时,链表遍历结束,此时所有节点值等于val
的节点都被删除
1 | public ListNode removeElements(ListNode head, int val) { |
递归删除
算法思想
- 首先对除了头节点
head
以外的节点进行删除操作,然后判断head
的节点值是否等于给定的val
。 - 如果
head
的节点值等于val
,则head
需要被删除,因此删除操作后的头节点为head.next
- 如果
head
的节点值不等于val
,则head
保留,因此删除操作后的头节点还是head
。
上述过程是一个递归的过程。递归的终止条件是 head
为空,此时直接返回 head
。当 head
不为空时,递归地进行删除操作,然后判断 head
的节点值是否等于 val
并决定是否要删除 head
。
1 | // 递归删除 |
删除倒数第 n 个节点
题目
【LeetCode 19】:给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
【进阶】:使用一趟扫描实现算法?
计算链表长度(节点编号从 1 开始)
算法思想
- 首先从头节点开始遍历链表获取长度
L
- 然后再次从头节点遍历链表,当遍历到第
L-n+1
个节点时,就是我们要找的节点
为方便删除操作,可以从虚拟节点开始遍历 L-n+1
个节点。当遍历到第 L-n+1
个节点时,它的下一个节点就是我们要删除的节点:
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
栈
算法思想
- 在遍历链表时将所有节点入栈
- 根据栈「先进后出」的原则,我们弹出栈的第
n
个节点就是需要删除的节点,并且目前栈顶的节点就是待删除节点的前驱节点。
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
【推荐】快慢指针
算法思想
- 由于要找到倒数第
n
个节点,可以使用快慢指针 - 先使用快指针走
n
步,此时快慢指针之间相隔n-1
个节点,即快指针比慢指针超前n
步 - 此后,同时使用快慢指针遍历链表,当快指针遍历到表尾(快指针为
null
)时,慢指针恰好指向倒数第n
个节点
由于得到倒数等 n
节点的前驱节点更利于删除操作,我们可以初始时将慢指针指向虚拟节点 dummyNode
,这样一来,当快指针遍历到链表末尾时,慢指针的下一个节点就是待删除的节点
1 | public ListNode removeNthFromEnd(ListNode head, int n) { |
删除重复元素
重复元素只保留一个
题目
【LeetCode 83】:给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
算法思想
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。
- 指针
cur
从表头开始遍历 - 当前
cur
与cur.next
对应的元素相同,则将cur.next
从链表中移除 - 否则说明链表中不存在其他与
cur
对应的元素相同的节点,可以将cur
指向cur.next
- 当遍历完整个链表后,返回链表的头节点即可,但当我们遍历到链表最后一个节点时,
cur.next
为空,如果不判断会报错。因此只需要遍历到链表的最后一个节点,不需要遍历完整个链表 - 特别注意,单链表删除操作一般定位前驱删除后继,因此本策略选择删除
cur.next
1 | public ListNode deleteDuplicates(ListNode head) { |
重复元素都不要
题目
【LeetCode 82】:给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
算法思想
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。由于链表的头节点可能会被删除,因此我们需要额外使用一个哑节点(dummy node)指向链表的头节点。
- 指针
cur
指向虚拟节点,开始遍历链表 - 如果当前
cur.next
与cur.next.next
对应的元素相同,就需要将cur.next
以及所有后面拥有相同元素值的链表节点全部删除 - 记录下元素值
x
,随后不断将cur.next
从链表中移除,直到cur.next
为空或者元素值不为x
为止,此时链表所有元素值为x
的节点全部删除 - 如果当前
cur.next
与cur.next.next
对应的元素不相同,那么说明链表中只有一个元素值为cur.next
的节点,直接将cur
指向cur.next
- 当遍历完整个链表之后,我们返回链表的的哑节点的下一个节点
dummy.next
即可。 - 特别注意,
cur.next
以及cur.next.next
可能为空节点,如果不加以判断,可能会产生运行错误。
1 |
|
链表专题Ⅰ—环问题与双链表设计
内容概览
题目 | 说明 |
---|---|
环形链表 | 中等 |
双链表设计 | 中等 |
链表环问题
参考资料
问题
【LeetCode 142】:给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
【推荐】哈希
算法思想
遍历链表中的每个节点,并将其记录下来;一旦遇到此前遍历过的节点,就可以判断链表中存在环。
1 | public ListNode detectCycle(ListNode head) { |
【推荐】快慢指针
算法思想
- 借助快慢指针
fast
和slow
,两只正初始位于链表的头部。随后,慢指针每次向后移动一个位置,快指针向后移动两个位置 - 如果链表存在环,则快指针最终将再次与慢指针在环中相遇
假设链表中环外部分的长度为 $a$,慢指针进入环后,又走了 $b$ 的距离与快指针相遇。此时快指针已经走完环的 $n$ 圈,因此它走过的总距离为 $a+n(b+c)+b=a+(n+1)b+nc$。
根据题意,任意时刻,快指针走过的距离都是慢指针的 $2$ 倍,因此,有如下式子:
有了 $a=c+(n−1)(b+c)$ 的等量关系,我们发现:从相遇点到入环点的距离加上 $n-1$ 圈的环长,恰好等于从链表头部到入环点的距离。
因此,当发现快慢指针相遇时再额外使用一个指针 ptr
。起始,它指向链表头部;随后,它和慢指针每次向后移动一个位置。最终,它们会在入环点相遇。
1 | public ListNode detectCycle(ListNode head) { |
双向链表设计
参考资料