1快慢指针:876 链表的中间结点
21. 前提条件
3题目给你一个单链表:
41 -> 2 -> 3 -> 4 -> 5
5要求返回:中间结点
6
7如果有两个中间结点,返回第二个。
8
9例如:1 -> 2 -> 3 -> 4 -> 5
10
11返回:3
12
13如果:1 -> 2 -> 3 -> 4 -> 5 -> 6
14
15中间有:3 和 4
16
17题目要求返回第二个:4
18
192. 核心概念:快慢指针
20
21定义两个指针:
22
23slow = head;
24fast = head;
25
26每次:
27
28slow = slow->next;
29fast = fast->next->next;
30
31也就是:
32
33slow 每次走一步
34fast 每次走两步
35
36当 fast 到达末尾时:slow 正好在中间。
37
383. 为什么 slow 会到中间?
39
40因为:fast 的速度是 slow 的 2 倍。
41
42如果 fast 走完整条链表,slow 就走了一半。
43
444. 代码模板
45struct ListNode* middleNode(struct ListNode* head) {
46 struct ListNode* slow = head;
47 struct ListNode* fast = head;
48
49 while (fast != NULL && fast->next != NULL) {
50 slow = slow->next;
51 fast = fast->next->next;
52 }
53
54 return slow;
55}
565. 奇数长度例子
571 -> 2 -> 3 -> 4 -> 5
58
59初始:slow = 1 fast = 1
60
61第一轮:slow = 2 fast = 3
62
63第二轮:slow = 3 fast = 5
64
65再下一轮:fast->next == NULL
66
67停止。
68
69返回:slow = 3
706. 偶数长度例子
711 -> 2 -> 3 -> 4 -> 5 -> 6
72
73初始:slow = 1 fast = 1
74
75第一轮:slow = 2 fast = 3
76
77第二轮:slow = 3 fast = 5
78
79第三轮:slow = 4 fast = NULL
80
81停止。
82
83返回:slow = 4
84
85这就是第二个中间结点。
86
877. while 条件
88while (fast != NULL && fast->next != NULL)
89
90因为循环里有:
91
92fast = fast->next->next;
93
94所以必须保证:
95
96fast 不为空
97fast->next 不为空
98
99否则访问:fast->next->next
100
101可能出错。
102
1038. 快慢指针题型总结
104
105快慢指针:slow 每次走一步,fast 每次走两步
106
107用途:
108
1091. 找链表中点
1102. 判断链表是否有环
1113. 找环入口
1124. 回文链表
1135. 链表归并排序找中点
114刚刚看的是最简单的问题,接下来我们看一下回文链表和链表回退k步的问题:
115快慢双指针多用于解决无法统计次数的循环问题,否则一旦有总的循环次数n,直接对应就能找到位置
116
117一、142 环形链表 II
1181. 前提条件
119
120题目给一个链表:
121
122struct ListNode {
123 int val;
124 struct ListNode *next;
125};
126
127要求:
128
129如果链表有环,返回环的入口节点;
130如果没有环,返回 NULL。
131
132例如:
133
1343 -> 2 -> 0 -> -4
135 ↑ ↓
136 ← ← ← ← ← ←
137
138环入口是:节点 2
139
1402. 核心概念
141
142快慢指针:
143
144slow 每次走 1 步
145fast 每次走 2 步
146
147如果链表有环:fast 一定会在环内追上 slow。
148
149相遇后:
150
151一个指针回到 head;
152另一个指针留在相遇点;
153两个指针每次都走 1 步;
154再次相遇的位置就是环入口。
155
1563. 为什么能找到入口?
157
158设:
159
160head 到环入口距离 = a
161环入口到相遇点距离 = b
162相遇点再回到入口距离 = c
163
164图:
165
166head ---- a ---- entry ---- b ---- meet
167 ↑ |
168 |---- c ----|
169
170slow 走的距离:
171
172a + b
173
174fast 走的距离:
175
176a + b + k(b + c)
177
178因为 fast 速度是 slow 的 2 倍:
179
180fast 距离 = 2 * slow 距离
181
182所以:
183
184a + b + k(b + c) = 2(a + b)
185
186化简可得到:
187
188a = k(b + c) - b
189
190也就是:
191
192a = (k - 1)(b + c) + c
193
194意思是:
195
196从 head 到入口的距离 a
197等价于
198
199从相遇点走 c 再绕若干圈到入口。
200
201所以:
202
203一个从 head 走;
204一个从 meet 走;
205每次都走一步;
206它们会在入口相遇。
207
208
209相遇后,一个回 head,一个留 meet,同速走,相遇点就是入口。
210
2114. 固定模板
212struct ListNode *detectCycle(struct ListNode *head) {
213 struct ListNode* slow = head;
214 struct ListNode* fast = head;
215//先都定位到head头
216 while (fast != NULL && fast->next != NULL) {
217 slow = slow->next;
218 fast = fast->next->next;
219//之后分配步调,slow走一步,fast走两步
220 if (slow == fast) {
221 struct ListNode* p1 = head;
222 struct ListNode* p2 = slow;
223//一旦相遇之后把p1再定位到head,然后p2定位到meet的地点,循环往后走直到相遇就是入口的位置
224 while (p1 != p2) {
225 p1 = p1->next;
226 p2 = p2->next;
227 }
228
229 return p1;
230 }
231 }
232
233 return NULL;
234}
2355. 例题流程
236
237链表:
238
2393 -> 2 -> 0 -> -4
240 ↑ ↓
241 ← ← ← ← ←
242
243也就是:
244
2453 -> 2 -> 0 -> -4
246 ^ |
247 |_________|
248
249初始:
250
251slow = 3
252fast = 3
253
254第一轮:
255
256slow = 2
257fast = 0
258
259第二轮:
260
261slow = 0
262fast = 2
263
264第三轮:
265
266slow = -4
267fast = -4
268
269相遇。
270
271然后:
272
273p1 = head = 3
274p2 = meet = -4
275
276一起走。
277
278第一步:
279
280p1 = 2
281p2 = 2
282
283相遇在节点 2。
284
285所以返回:2是环入口。
286
2876. 易错点
288易错点 1:while 条件必须写完整
289while (fast != NULL && fast->next != NULL)
290
291因为循环里有:
292
293fast = fast->next->next;
294
295所以必须保证:
296
297fast 不为空;
298fast->next 不为空。
299
300易错点 2:判断相遇要比较指针,不是比较值
301
302正确:if (slow == fast)
303
304错误:if (slow->val == fast->val)
305
306因为链表里不同节点可能值一样。
307
308易错点 3:相遇点不一定是入口
309
310第一次相遇的位置通常不是入口。
311
312必须再做:
313
314p1 = head
315p2 = meet
316一起走
317
318才能找入口。
319
3207. 总结
321142 环形链表 II
322
3231. slow、fast 从 head 出发。
3242. slow 每次 1 步,fast 每次 2 步。
3253. 如果 fast 或 fast->next 为 NULL,说明无环。
3264. 如果 slow == fast,说明有环。
3275. 一个指针回 head,另一个留在相遇点。
3286. 两个指针每次走 1 步。
3297. 再次相遇处就是环入口。
330
331快慢指针负责判断是否有环;相遇后双指针同速走负责找入口。
332
333接下来看一下另一个题目:19 删除链表的倒数第 N 个结点
3341. 前提条件
335题目给一个链表和整数 n:
336
3371 -> 2 -> 3 -> 4 -> 5
338n = 2
339
340要求删除倒数第 n 个节点。
341
342倒数第 2 个是:4
343
344删除后:
345
3461 -> 2 -> 3 -> 5
347
3482. 核心概念
349用两个指针:
350fast
351slow
352
353让 fast 先走 n 步。
354
355然后:
356
357fast 和 slow 一起走。
358
359当 fast 到达链表末尾时:
360
361slow 正好在要删除节点的前一个位置。
362
3633. 为什么要用 dummy 虚拟头结点?
364
365如果要删除的是头节点,比如:
366
3671 -> 2 -> 3
368n = 3
369
370倒数第 3 个就是:1
371
372如果没有虚拟头结点,删除头节点比较麻烦。
373
374所以创建:
375
376dummy -> 1 -> 2 -> 3
377
378最后返回:
379
380dummy->next
381这样所有情况统一处理。
382
3834. C代码:
384#include <stdlib.h>
385
386struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
387 struct ListNode* dummy =
388 malloc(sizeof(struct ListNode));
389
390 dummy->next = head;
391
392 struct ListNode* fast = dummy;
393 struct ListNode* slow = dummy;
394
395 for (int i = 0; i < n; i++) {
396 fast = fast->next;
397 }
398//先让fast走n步,之后两者一块走
399 while (fast->next != NULL) {
400 fast = fast->next;
401 slow = slow->next;
402 }
403//走到尽头之后,slow的下一个位置就是需要的,然后把slow的下一个位置的next扔给slow的next连接就好
404 slow->next=slow->next->next;
405
406 struct ListNode* ans = dummy->next;
407 free(dummy);
408
409 return ans;
410}
4115. 例题流程
412
413链表:1 -> 2 -> 3 -> 4 -> 5
414n = 2
415
416加 dummy:
417
418dummy -> 1 -> 2 -> 3 -> 4 -> 5
419
420初始:
421
422fast = dummy
423slow = dummy
424fast 先走 2 步
425
426第一步:
427
428fast = 1
429
430第二步:
431
432fast = 2
433
434此时:
435
436fast 和 slow 相隔 2 个节点。
437两个一起走
438
439当前:
440
441slow = dummy
442fast = 2
443
444循环条件:
445
446while (fast->next != NULL)
447
448第一轮:
449
450slow = 1
451fast = 3
452
453第二轮:
454
455slow = 2
456fast = 4
457
458第三轮:
459
460slow = 3
461fast = 5
462
463此时:
464
465fast->next == NULL
466
467停止。
468
469现在:
470
471slow = 3
472
473要删除的节点是:
474
475slow->next = 4
476
477执行:
478
479slow->next = slow->next->next;
480
481变成:
482
4831 -> 2 -> 3 -> 5
4846. 为什么 slow 会停在删除节点前面?
485
486因为:
487
488fast 比 slow 领先 n 个节点。
489
490当 fast 到达最后一个节点时:
491
492slow->next
493
494正好是倒数第 n 个节点。
495
496所以删除:slow->next即可。
497
4987. 删除头节点例子
499
500链表:
501
5021 -> 2 -> 3
503n = 3
504
505加 dummy:
506
507dummy -> 1 -> 2 -> 3
508
509fast 先走 3 步:
510
511fast = 3
512slow = dummy
513
514此时:
515
516fast->next == NULL
517
518不进入 while。
519
520所以:
521
522slow = dummy
523
524删除:
525
526slow->next = 1
527
528执行:
529
530slow->next = slow->next->next;
531
532结果:
533
5342 -> 3
535
536最后返回:
537
538dummy->next
539
540正好是新头节点 2。
541
5428. 易错点
543易错点 1:不用 dummy 会难处理删除头节点
544
545例如:
546
5471 -> 2 -> 3
548n = 3
549
550要删头节点。
551
552用 dummy 最稳。
553
554易错点 2:fast 先走 n 步,不是 n+1 步
555
556在这个写法里:
557
558fast 和 slow 都从 dummy 开始
559fast 先走 n 步
560while(fast->next != NULL)
561
562这样 slow 最后停在删除节点前一个。
563
564易错点 3:删除节点前要保存
565struct ListNode* deleteNode = slow->next;
566slow->next = deleteNode->next;
567free(deleteNode);
568
569如果是 LeetCode,有些人不 free 也能过,但 C 语言最好写完整。
570
571易错点 4:最后返回 dummy->next
572
573不能返回原来的 head。
574
575因为如果删的是头节点,head 已经被删掉了。
576
5779. 总结
57819 删除链表倒数第 N 个节点
579
580核心:让 fast 先走 n 步。
581
582然后 fast 和 slow 同时走。
583
584当 fast 到尾部时,
585
586slow 在待删除节点前一个位置。
587
588--------------------------------
589
590为什么用 dummy:
591
592统一处理删除头节点的情况。
593
594--------------------------------
595
596返回:
597
598dummy->next
599
600快慢指针通过保持 n 个节点的距离,把“倒数第 n 个”转化成“slow 的下一个节点”。
快慢指针:876 链表的中间结点
- 前提条件
题目给你一个单链表:
1 -> 2 -> 3 -> 4 -> 5
要求返回:中间结点
如果有两个中间结点,返回第二个。
例如:1 -> 2 -> 3 -> 4 -> 5
返回:3
如果:1 -> 2 -> 3 -> 4 -> 5 -> 6
中间有:3 和 4
题目要求返回第二个:4
- 核心概念:快慢指针
定义两个指针:
slow = head;
fast = head;
每次:
slow = slow->next;
fast = fast->next->next;
也就是:
slow 每次走一步
fast 每次走两步
当 fast 到达末尾时:slow 正好在中间。
- 为什么 slow 会到中间?
因为:fast 的速度是 slow 的 2 倍。
如果 fast 走完整条链表,slow 就走了一半。
-
代码模板
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
-
奇数长度例子
1 -> 2 -> 3 -> 4 -> 5
初始:slow = 1 fast = 1
第一轮:slow = 2 fast = 3
第二轮:slow = 3 fast = 5
再下一轮:fast->next == NULL
停止。
返回:slow = 3
6. 偶数长度例子
1 -> 2 -> 3 -> 4 -> 5 -> 6
初始:slow = 1 fast = 1
第一轮:slow = 2 fast = 3
第二轮:slow = 3 fast = 5
第三轮:slow = 4 fast = NULL
停止。
返回:slow = 4
这就是第二个中间结点。
- while 条件
while (fast != NULL && fast->next != NULL)
因为循环里有:
fast = fast->next->next;
所以必须保证:
fast 不为空
fast->next 不为空
否则访问:fast->next->next
可能出错。
- 快慢指针题型总结
快慢指针:slow 每次走一步,fast 每次走两步
用途:
- 找链表中点
- 判断链表是否有环
- 找环入口
- 回文链表
- 链表归并排序找中点
刚刚看的是最简单的问题,接下来我们看一下回文链表和链表回退k步的问题:
快慢双指针多用于解决无法统计次数的循环问题,否则一旦有总的循环次数n,直接对应就能找到位置
一、142 环形链表 II
- 前提条件
题目给一个链表:
struct ListNode {
int val;
struct ListNode *next;
};
要求:
如果链表有环,返回环的入口节点;
如果没有环,返回 NULL。
例如:
3 -> 2 -> 0 -> -4
↑ ↓
← ← ← ← ← ←
环入口是:节点 2
- 核心概念
快慢指针:
slow 每次走 1 步
fast 每次走 2 步
如果链表有环:fast 一定会在环内追上 slow。
相遇后:
一个指针回到 head;
另一个指针留在相遇点;
两个指针每次都走 1 步;
再次相遇的位置就是环入口。
- 为什么能找到入口?
设:
head 到环入口距离 = a
环入口到相遇点距离 = b
相遇点再回到入口距离 = c
图:
head ---- a ---- entry ---- b ---- meet
↑ |
|---- c ----|
slow 走的距离:
a + b
fast 走的距离:
a + b + k(b + c)
因为 fast 速度是 slow 的 2 倍:
fast 距离 = 2 * slow 距离
所以:
a + b + k(b + c) = 2(a + b)
化简可得到:
a = k(b + c) - b
也就是:
a = (k - 1)(b + c) + c
意思是:
从 head 到入口的距离 a
等价于
从相遇点走 c 再绕若干圈到入口。
所以:
一个从 head 走;
一个从 meet 走;
每次都走一步;
它们会在入口相遇。
相遇后,一个回 head,一个留 meet,同速走,相遇点就是入口。
-
固定模板
struct ListNode detectCycle(struct ListNode head) {
struct ListNode slow = head;
struct ListNode fast = head;
//先都定位到head头
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
//之后分配步调,slow走一步,fast走两步
if (slow == fast) {
struct ListNode* p1 = head;
struct ListNode* p2 = slow;
//一旦相遇之后把p1再定位到head,然后p2定位到meet的地点,循环往后走直到相遇就是入口的位置
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
}
return NULL;
}
-
例题流程
链表:
3 -> 2 -> 0 -> -4
↑ ↓
← ← ← ← ←
也就是:
3 -> 2 -> 0 -> -4
^ |
|_________|
初始:
slow = 3
fast = 3
第一轮:
slow = 2
fast = 0
第二轮:
slow = 0
fast = 2
第三轮:
slow = -4
fast = -4
相遇。
然后:
p1 = head = 3
p2 = meet = -4
一起走。
第一步:
p1 = 2
p2 = 2
相遇在节点 2。
所以返回:2是环入口。
- 易错点
易错点 1:while 条件必须写完整
while (fast != NULL && fast->next != NULL)
因为循环里有:
fast = fast->next->next;
所以必须保证:
fast 不为空;
fast->next 不为空。
易错点 2:判断相遇要比较指针,不是比较值
正确:if (slow == fast)
错误:if (slow->val == fast->val)
因为链表里不同节点可能值一样。
易错点 3:相遇点不一定是入口
第一次相遇的位置通常不是入口。
必须再做:
p1 = head
p2 = meet
一起走
才能找入口。
-
总结
142 环形链表 II
-
slow、fast 从 head 出发。
-
slow 每次 1 步,fast 每次 2 步。
-
如果 fast 或 fast->next 为 NULL,说明无环。
-
如果 slow == fast,说明有环。
-
一个指针回 head,另一个留在相遇点。
-
两个指针每次走 1 步。
-
再次相遇处就是环入口。
快慢指针负责判断是否有环;相遇后双指针同速走负责找入口。
接下来看一下另一个题目:19 删除链表的倒数第 N 个结点
- 前提条件
题目给一个链表和整数 n:
1 -> 2 -> 3 -> 4 -> 5
n = 2
要求删除倒数第 n 个节点。
倒数第 2 个是:4
删除后:
1 -> 2 -> 3 -> 5
- 核心概念
用两个指针:
fast
slow
让 fast 先走 n 步。
然后:
fast 和 slow 一起走。
当 fast 到达链表末尾时:
slow 正好在要删除节点的前一个位置。
- 为什么要用 dummy 虚拟头结点?
如果要删除的是头节点,比如:
1 -> 2 -> 3
n = 3
倒数第 3 个就是:1
如果没有虚拟头结点,删除头节点比较麻烦。
所以创建:
dummy -> 1 -> 2 -> 3
最后返回:
dummy->next
这样所有情况统一处理。
- C代码:
#include <stdlib.h>
struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
struct ListNode* dummy =
malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode* fast = dummy;
struct ListNode* slow = dummy;
for (int i = 0; i < n; i++) {
fast = fast->next;
}
//先让fast走n步,之后两者一块走
while (fast->next != NULL) {
fast = fast->next;
slow = slow->next;
}
//走到尽头之后,slow的下一个位置就是需要的,然后把slow的下一个位置的next扔给slow的next连接就好
slow->next=slow->next->next;
struct ListNode* ans = dummy->next;
free(dummy);
return ans;
}
5. 例题流程
链表:1 -> 2 -> 3 -> 4 -> 5
n = 2
加 dummy:
dummy -> 1 -> 2 -> 3 -> 4 -> 5
初始:
fast = dummy
slow = dummy
fast 先走 2 步
第一步:
fast = 1
第二步:
fast = 2
此时:
fast 和 slow 相隔 2 个节点。
两个一起走
当前:
slow = dummy
fast = 2
循环条件:
while (fast->next != NULL)
第一轮:
slow = 1
fast = 3
第二轮:
slow = 2
fast = 4
第三轮:
slow = 3
fast = 5
此时:
fast->next == NULL
停止。
现在:
slow = 3
要删除的节点是:
slow->next = 4
执行:
slow->next = slow->next->next;
变成:
1 -> 2 -> 3 -> 5
6. 为什么 slow 会停在删除节点前面?
因为:
fast 比 slow 领先 n 个节点。
当 fast 到达最后一个节点时:
slow->next
正好是倒数第 n 个节点。
所以删除:slow->next即可。
- 删除头节点例子
链表:
1 -> 2 -> 3
n = 3
加 dummy:
dummy -> 1 -> 2 -> 3
fast 先走 3 步:
fast = 3
slow = dummy
此时:
fast->next == NULL
不进入 while。
所以:
slow = dummy
删除:
slow->next = 1
执行:
slow->next = slow->next->next;
结果:
2 -> 3
最后返回:
dummy->next
正好是新头节点 2。
- 易错点
易错点 1:不用 dummy 会难处理删除头节点
例如:
1 -> 2 -> 3
n = 3
要删头节点。
用 dummy 最稳。
易错点 2:fast 先走 n 步,不是 n+1 步
在这个写法里:
fast 和 slow 都从 dummy 开始
fast 先走 n 步
while(fast->next != NULL)
这样 slow 最后停在删除节点前一个。
易错点 3:删除节点前要保存
struct ListNode* deleteNode = slow->next;
slow->next = deleteNode->next;
free(deleteNode);
如果是 LeetCode,有些人不 free 也能过,但 C 语言最好写完整。
易错点 4:最后返回 dummy->next
不能返回原来的 head。
因为如果删的是头节点,head 已经被删掉了。
- 总结
19 删除链表倒数第 N 个节点
核心:让 fast 先走 n 步。
然后 fast 和 slow 同时走。
当 fast 到尾部时,
slow 在待删除节点前一个位置。
为什么用 dummy:
统一处理删除头节点的情况。
返回:
dummy->next
快慢指针通过保持 n 个节点的距离,把“倒数第 n 个”转化成“slow 的下一个节点”。