返回 LeetCode 刷题

Markdown File

快慢双指针

快慢双指针.md

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 的下一个节点”。
Rendered Preview

快慢指针:876 链表的中间结点

  1. 前提条件
    题目给你一个单链表:
    1 -> 2 -> 3 -> 4 -> 5
    要求返回:中间结点

如果有两个中间结点,返回第二个。

例如:1 -> 2 -> 3 -> 4 -> 5

返回:3

如果:1 -> 2 -> 3 -> 4 -> 5 -> 6

中间有:3 和 4

题目要求返回第二个:4

  1. 核心概念:快慢指针

定义两个指针:

slow = head;
fast = head;

每次:

slow = slow->next;
fast = fast->next->next;

也就是:

slow 每次走一步
fast 每次走两步

当 fast 到达末尾时:slow 正好在中间。

  1. 为什么 slow 会到中间?

因为:fast 的速度是 slow 的 2 倍。

如果 fast 走完整条链表,slow 就走了一半。

  1. 代码模板
    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;
    }

  2. 奇数长度例子
    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

这就是第二个中间结点。

  1. while 条件
    while (fast != NULL && fast->next != NULL)

因为循环里有:

fast = fast->next->next;

所以必须保证:

fast 不为空
fast->next 不为空

否则访问:fast->next->next

可能出错。

  1. 快慢指针题型总结

快慢指针:slow 每次走一步,fast 每次走两步

用途:

  1. 找链表中点
  2. 判断链表是否有环
  3. 找环入口
  4. 回文链表
  5. 链表归并排序找中点
    刚刚看的是最简单的问题,接下来我们看一下回文链表和链表回退k步的问题:
    快慢双指针多用于解决无法统计次数的循环问题,否则一旦有总的循环次数n,直接对应就能找到位置

一、142 环形链表 II

  1. 前提条件

题目给一个链表:

struct ListNode {
int val;
struct ListNode *next;
};

要求:

如果链表有环,返回环的入口节点;
如果没有环,返回 NULL。

例如:

3 -> 2 -> 0 -> -4
↑ ↓
← ← ← ← ← ←

环入口是:节点 2

  1. 核心概念

快慢指针:

slow 每次走 1 步
fast 每次走 2 步

如果链表有环:fast 一定会在环内追上 slow。

相遇后:

一个指针回到 head;
另一个指针留在相遇点;
两个指针每次都走 1 步;
再次相遇的位置就是环入口。

  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,同速走,相遇点就是入口。

  1. 固定模板
    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;
    }

  2. 例题流程

链表:

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. 易错点
    易错点 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
一起走

才能找入口。

  1. 总结
    142 环形链表 II

  2. slow、fast 从 head 出发。

  3. slow 每次 1 步,fast 每次 2 步。

  4. 如果 fast 或 fast->next 为 NULL,说明无环。

  5. 如果 slow == fast,说明有环。

  6. 一个指针回 head,另一个留在相遇点。

  7. 两个指针每次走 1 步。

  8. 再次相遇处就是环入口。

快慢指针负责判断是否有环;相遇后双指针同速走负责找入口。

接下来看一下另一个题目:19 删除链表的倒数第 N 个结点

  1. 前提条件
    题目给一个链表和整数 n:

1 -> 2 -> 3 -> 4 -> 5
n = 2

要求删除倒数第 n 个节点。

倒数第 2 个是:4

删除后:

1 -> 2 -> 3 -> 5

  1. 核心概念
    用两个指针:
    fast
    slow

让 fast 先走 n 步。

然后:

fast 和 slow 一起走。

当 fast 到达链表末尾时:

slow 正好在要删除节点的前一个位置。

  1. 为什么要用 dummy 虚拟头结点?

如果要删除的是头节点,比如:

1 -> 2 -> 3
n = 3

倒数第 3 个就是:1

如果没有虚拟头结点,删除头节点比较麻烦。

所以创建:

dummy -> 1 -> 2 -> 3

最后返回:

dummy->next
这样所有情况统一处理。

  1. 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. 删除头节点例子

链表:

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. 易错点
    易错点 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 已经被删掉了。

  1. 总结
    19 删除链表倒数第 N 个节点

核心:让 fast 先走 n 步。

然后 fast 和 slow 同时走。

当 fast 到尾部时,

slow 在待删除节点前一个位置。


为什么用 dummy:

统一处理删除头节点的情况。


返回:

dummy->next

快慢指针通过保持 n 个节点的距离,把“倒数第 n 个”转化成“slow 的下一个节点”。