寻找链表的中点
我们可以设置快慢指针,快指针(fast)一次走两步,慢指针(slow)一次走一步。fast 走完整条链表时,slow 指向的结点就是链表的中点。
该方法通过速度差来寻得链表中点。同理也可求得链表 2 / 3 处的结点。故假设slow一次走 s 步,fast 一次走 f 步,则当 fast 走完整条链表时,slow 指向链表 s / f 处的结点。
寻找链表的倒数第k个结点
相关习题:LeetCode 19. 删除链表的倒数第 N 个结点
设置p1
、p2
两个指针,让p2
指针先走k-1步,然后两个指针每次都走一步,p2
链表走完链表时,p1
指针指向的结点就是倒数第k个结点。
该方法通过相对距离来寻得链表倒数第 k 个结点。需注意,这与上一方法不互通,如果用速度差来求倒数第 k 个结点,设 链表长度为 n ,当 fast 走完链表时,时间相同,有 n − k V s l o w = n V f a s t \frac {n-k}{V_{slow}} = \frac n {V_{fast}} Vslown−k=Vfastn ,在该式中,k 是已知量, V s l o w V f a s t \frac {V_{slow}} {V_{fast}} VfastVslow 是我们要求的量,可 n 是未知量,故无法求得。
如果用相对距离来求中点,同样不可行。因为 n 是未知数,而中点为倒数第 n − n 2 n - \frac n 2 n−2n 个结点,即 k = n − n 2 k=n - \frac n 2 k=n−2n ,无法求得 k 的值。
关键要点
- 速度差与相对距离的适用场景
- 速度差法(快慢指针)适用于求解链表的比例位置,像中点、1/3 处等节点。
- 相对距离法(间隔指针)适用于求解链表的倒数位置,例如倒数第 k 个节点。
- 注意事项
- 在使用间隔指针法时,要留意链表长度是否大于等于 k。
- 当链表长度为偶数时,快慢指针法得到的中点是中间两个节点中的第二个。若想获取第一个中间节点,可对终止条件进行调整。
双指针策略通过巧妙设定指针的移动速度或者间隔距离,能够在一次遍历链表的过程中完成目标节点的定位,时间复杂度为 O (n),空间复杂度为 O (1),实现了效率的优化。
判断回文
相关习题:LeetCode 206. 反转链表;LeetCode 234. 回文链表
快慢指针 + 反转链表,该思路通过 O(n) 时间复杂度和 O(1) 空间复杂度解决问题,是判断单链表回文的最优解法之一。唯一需要注意的是边界条件处理(如空链表、单节点链表)和链表反转时的指针操作细节。
思路:
- 找中点:用快慢指针(快指针一次走两步,慢指针一次走一步)找到链表中点。
- 反转后半段:将后半段链表反转,使后半段顺序与前半段对称。用头插法将后半段链表结点插入新链表。
- 双指针比较:前半段从头开始,反转后的后半段从新头节点开始,逐一比较节点值。
- 恢复链表(可选):若需保持原链表结构,可再次反转后半段恢复。用尾插法。
#include <iostream>
using namespace std;
// 定义链表节点结构
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
bool isPalindrome(ListNode* head) {
// 处理空链表或单节点链表
if (head == nullptr || head->next == nullptr) return true;
// 1. 快慢指针找中点(偶数长度时慢指针停在左半段最后节点,奇数长度时停在中间节点)
ListNode* slow = head;
ListNode* fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
// 2. 反转后半段链表(从slow->next开始)
ListNode* secondHalf = slow->next;
slow->next = nullptr; // 断开前半段与后半段
ListNode* reversedHead = nullptr;
while (secondHalf != nullptr) {
ListNode* nextNode = secondHalf->next;
secondHalf->next = reversedHead;
reversedHead = secondHalf;
secondHalf = nextNode;
}
// 3. 双指针比较两段链表
ListNode* p1 = head;
ListNode* p2 = reversedHead;
bool result = true;
while (p2 != nullptr) { // 仅比较后半段长度次(自动适配奇偶长度)
if (p1->val != p2->val) {
result = false;
break;
}
p1 = p1->next;
p2 = p2->next;
}
// 4. 恢复链表(可选)
secondHalf = reversedHead;
reversedHead = nullptr;
while (secondHalf != nullptr) {
ListNode* nextNode = secondHalf->next;
secondHalf->next = reversedHead;
reversedHead = secondHalf;
secondHalf = nextNode;
}
slow->next = reversedHead; // 重新连接前半段和恢复后的后半段
return result;
}
// 辅助函数:创建无头结点链表
ListNode* createLinkedList(int arr[], int n) {
if (n == 0) return nullptr;
ListNode* head = new ListNode(arr[0]);
ListNode* current = head;
for (int i = 1; i < n; i++) {
current->next = new ListNode(arr[i]);
current = current->next;
}
return head;
}
// 辅助函数:打印链表
void printLinkedList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
cout << current->val;
if (current->next != nullptr) {
cout << " -> ";
}
current = current->next;
}
cout << endl;
}
// 辅助函数:释放链表内存
void freeLinkedList(ListNode* head) {
while (head != nullptr) {
ListNode* temp = head;
head = head->next;
delete temp;
}
}
int main() {
// 测试用例1: 偶数长度回文链表
int arr1[] = {1, 2, 2, 1};
ListNode* head1 = createLinkedList(arr1, 4);
cout << "链表1: ";
printLinkedList(head1);
cout << "是否回文: " << (isPalindrome(head1) ? "是" : "否") << endl;
cout << "恢复后的链表1: ";
printLinkedList(head1);
freeLinkedList(head1);
// 测试用例2: 偶数长度非回文链表
int arr2[] = {1, 2, 3, 4};
ListNode* head2 = createLinkedList(arr2, 4);
cout << "\n链表2: ";
printLinkedList(head2);
cout << "是否回文: " << (isPalindrome(head2) ? "是" : "否") << endl;
cout << "恢复后的链表2: ";
printLinkedList(head2);
freeLinkedList(head2);
// 测试用例3: 奇数长度回文链表
int arr3[] = {1, 2, 3, 2, 1};
ListNode* head3 = createLinkedList(arr3, 5);
cout << "\n链表3: ";
printLinkedList(head3);
cout << "是否回文: " << (isPalindrome(head3) ? "是" : "否") << endl;
cout << "恢复后的链表3: ";
printLinkedList(head3);
freeLinkedList(head3);
// 测试用例4: 奇数长度非回文链表
int arr4[] = {1, 2, 3, 4, 5};
ListNode* head4 = createLinkedList(arr4, 5);
cout << "\n链表4: ";
printLinkedList(head4);
cout << "是否回文: " << (isPalindrome(head4) ? "是" : "否") << endl;
cout << "恢复后的链表4: ";
printLinkedList(head4);
freeLinkedList(head4);
// 测试用例5: 单节点链表
ListNode* head5 = new ListNode(5);
cout << "\n链表5: ";
printLinkedList(head5);
cout << "是否回文: " << (isPalindrome(head5) ? "是" : "否") << endl;
delete head5;
// 测试用例6: 双节点回文链表
ListNode* head6 = new ListNode(6);
head6->next = new ListNode(6);
cout << "\n链表6: ";
printLinkedList(head6);
cout << "是否回文: " << (isPalindrome(head6) ? "是" : "否") << endl;
freeLinkedList(head6);
return 0;
}
判断链表是否有环
相关习题:LeetCode 141. 环形链表
在不改变链表物理节点数(即长度)的前提下,单链表的环必须包含尾节点,且只能通过尾节点的指针指向链表中某个前驱节点形成。因为单链表中唯一允许修改指针的节点是尾节点(其指针原本为null
)。若中间节点的指针被修改为指向前驱节点,会导致链表断裂(后续节点丢失)。
Floyd判圈算法(Floyd Cycle Detection Algorithm),又称龟兔赛跑算法(Tortoise and Hare Algorithm)。可用于判定链表、迭代函数、有限状态机中是否有环。如果有环,可以找出环的起点,求出环的长度。两个人在赛跑,A速度快,B速度慢,若是存在环(勺状图),A和B总是会相遇的,相遇时A所经过的路径的长度要比B多若干个环的长度。
快指针一次走两步,慢指针一次走一步。如果快慢指针能相遇,说明链表有环。无论落后几步,比如下方的落后三步、两步、一步,fast
与slow
指针终会相遇。
当慢指针slow刚进入环时,快指针fast早已进入环中。如下图所示,设头结点到环的入口点的距离为a,环的入口点沿着环的方向到相遇点的距离为m,环长为 r ,相遇时slow绕过了 s 圈,fast绕过了 f 圈。
“slow行走距离的两倍=fast行走的距离”,
即
2
(
a
+
s
r
+
m
)
=
a
+
f
r
+
m
2(a+sr +m)=a+fr+m
2(a+sr+m)=a+fr+m,
算得
a
+
m
=
(
f
−
2
s
)
r
a+m=(f-2s)r
a+m=(f−2s)r,令
n
=
f
−
2
s
n=f-2s
n=f−2s,
得
a
+
m
=
n
r
a+m=nr
a+m=nr,又
a
,
m
,
r
a,m,r
a,m,r 均为非负整数,故
n
n
n 为非负整数。
由上式可得
m
=
n
r
−
a
m=nr-a
m=nr−a,无论
a
,
r
a,r
a,r 有多长,总存在非负整数
n
n
n ,使得点
m
≥
0
m\geq0
m≥0,即相遇点存在。
由
m
≥
0
m\geq 0
m≥0 得
n
≥
a
r
n\geq\frac ar
n≥ra,故第一次相遇时
n
=
⌈
a
c
⌉
n=\lceil \frac a c \rceil
n=⌈ca⌉。
代入数据就方便理解了,假设
a
=
2
,
r
=
2
a=2,r=2
a=2,r=2 那么
n
≥
1
n\geq1
n≥1,故
n
=
1
,
m
=
0
n=1,m=0
n=1,m=0 。
快慢指针相遇时,慢指针绕环的圈数
s
<
1
s < 1
s<1,即慢指针必然在第一圈内被追上,
s
=
0
s=0
s=0,故
n
=
f
−
2
s
=
f
n=f-2s=f
n=f−2s=f,即
n
n
n 为相遇时fast
绕过了的圈数,因为fast
要绕后追slow
,所以
n
≥
1
n \geq 1
n≥1。
快指针与慢指针的相对速度为 1 步 / 次,保证了两者的距离每次至少减少 1,因此快指针必然在相遇前逐步逼近慢指针,而不会跳过。这一性质是 Floyd 判环算法的核心逻辑之一,确保了算法在 O (n) 时间内正确检测到环。
寻找入环点
快慢指针相遇之后,让快指针重新回到头结点,然后让两指针每次都走一步,两指针再次相遇的结点就是入环点。通过 a + m = n r a+m=nr a+m=nr 就能理解,因为它们的相对距离是 r r r 的整数倍,所以它们会相遇在入环点。如果不好理解,就将它变为 a = ( n − 1 ) r + ( r − m ) a = (n-1)r+(r-m) a=(n−1)r+(r−m) ,环外结点的长度就等于相遇点指针转 n-1 圈后( n ≥ 1 n\geq1 n≥1),回到 m 点再走 r - m 步。
知道了入环点就能知道环外结点数和环内结点数,也就能知道链表总长度。
#include <iostream>
struct LNode {
int data;
LNode *next;
explicit LNode(int val) : data(val), next(nullptr) {}
};
int findLoopStartAndLength(LNode *head, LNode **loopStart) {
// 步骤1:检测环并找到相遇点
LNode *slow = head, *fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (fast == nullptr || fast->next == nullptr) {
*loopStart = nullptr;
return 0; // 无环,长度为0(或根据需求返回非环部分长度)
}
// 步骤2:找到环起点
LNode *p1 = head, *p2 = slow;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
*loopStart = p1; // 保存环起点
// 步骤3:计算非环部分长度 a
int a = 0;
LNode *temp = head;
while (temp != p1) {
temp = temp->next;
a++;
}
// 步骤4:计算环长度 b
int b = 1; // 至少有一个节点(环起点自身)
temp = p1->next;
while (temp != p1) {
temp = temp->next;
b++;
}
// 总长度 = a + b
return a + b;
}
int main() {
// 创建带环链表:1->2->3->4->5->3(环起点为3)
auto *head = new LNode(1);
head->next = new LNode(2);
head->next->next = new LNode(3);
head->next->next->next = new LNode(4);
head->next->next->next->next = new LNode(5);
head->next->next->next->next->next = head->next->next; // 5->3
LNode *loopStart = nullptr;
int length = findLoopStartAndLength(head, &loopStart);
std::cout << "总长度: " << length << std::endl;
if (loopStart) {
std::cout << "环起点值: " << loopStart->data << std::endl;
}
// 注意:带环链表的内存释放需特殊处理,避免无限循环
return 0;
}
除了该方法外,还可通过给每个结点增加一个变量表示访问频度int freq=0
,第一个freq=2
的结点就是环的入口点。这与哈希表有异曲同工之处,如果发现某个结点在表中已经存在了,那么这个结点就是入环结点。
快指针一次移动多步是否相遇
假设快指针一次走三步,慢指针一次走一步,那么慢指针入环时在这两种情况下两指针永远不会相遇。
但是无论环外结点的数量是多少,只要初始状态时,快慢指针均指向头结点,在经过n次移动之后,都无法构造出这种相对位置。证明如下:
假设
- 环外有 a 个节点,环内有 b 个节点 ( b ≥ 1 ) (b\geq1) (b≥1)。
- 快指针速度为 k ≥ 1 k\geq1 k≥1(慢指针速度为1),初始时两者均指向头结点。
相遇条件分析
1. 慢指针入环时刻
慢指针走 a 步到达入环点(节点 0),
此时快指针已走 k a 步,其在环内的位置为:
c = [ k a − a ] % b = [ a ( k − 1 ) ] % b c = \left[ka - a\right] \% b = \left[a(k-1)\right] \% b c=[ka−a]%b=[a(k−1)]%b
2. 相遇方程推导
设慢指针入环后再走 x 步相遇,则相遇条件为:
x ≡ c + k x ( mod b ) ⟹ ( k − 1 ) x ≡ − c ( mod b ) x \equiv c + kx \ (\text{mod} \ b) \implies (k-1)x \equiv -c \ (\text{mod} \ b) x≡c+kx (mod b)⟹(k−1)x≡−c (mod b)
代入 c = [ a ( k − 1 ) ] % b c = \left[a(k-1)\right] \% b c=[a(k−1)]%b,即 c = a ( k − 1 ) − m b c = a(k-1) - mb c=a(k−1)−mb(m 为整数),
得: ( k − 1 ) ( x + a ) ≡ 0 ( mod b ) (k-1)(x + a) \equiv 0 \ (\text{mod} \ b) (k−1)(x+a)≡0 (mod b)
3. 同余方程求解
设 d = gcd ( k − 1 , b ) d = \gcd(k-1, b) d=gcd(k−1,b),则方程可约简为:
k − 1 d ( x + a ) ≡ 0 ( mod b d ) \frac{k-1}{d}(x + a) \equiv 0 \ (\text{mod} \ \frac{b}{d}) dk−1(x+a)≡0 (mod db)
由于 k − 1 d \frac{k-1}{d} dk−1 与 b d \frac{b}{d} db 互质(数论中 “约简最大公约数后两数互质”),
方程等价于: x + a ≡ 0 ( mod b d ) ⟹ x ≡ − a ( mod b d ) x + a \equiv 0 \ (\text{mod} \ \frac{b}{d}) \implies x \equiv -a \ (\text{mod} \ \frac{b}{d}) x+a≡0 (mod db)⟹x≡−a (mod db)
其最小非负整数解为: x = ( b d − a m o d b d ) x = \left(\frac{b}{d} - a \mod \frac{b}{d}\right) x=(db−amoddb)
即 x = b d ⋅ t − a x = \frac{b}{d} \cdot t - a x=db⋅t−a(t 为使 x ≥ 0 x \geq 0 x≥0 的最小正整数)。
结论
- 当 k > 1 k > 1 k>1 时:
- 无论 a 和 b 取何值,方程必有解,快慢指针必定相遇。
- 当 k = 1 k = 1 k=1 时:
- 快慢指针速度相同,轨迹完全重合。
- 若 a = 0 a = 0 a=0(头结点即入环点):初始即相遇;
- 若 a ≥ 1 a \geq 1 a≥1:慢指针入环时(时刻 a),快指针也同步到达入环点,此后持续相遇。
- 因此, k = 1 k = 1 k=1 时两者仍必定相遇。
- 唯一例外:
- 若链表无环( b = 0 b = 0 b=0),则快慢指针永远不会相遇。但此情形与前提 “环内有 b 个节点” 矛盾。
最终结论
- 对于存在环的链表,无论 k ≥ 1 k \geq 1 k≥1 取何值,快慢指针初始均指向头结点时必定相遇。
- 注意:相遇不代表不会有跳过,可能跳过慢指针几次才与它相遇。
公共结点问题
相关题目:LeetCode 160. 相交链表
两个无环单链表有公共结点,即两个链表从某一结点开始,它们的next
都指向同一结点。每个单链表结点只有一个next
域,因此从第一个公共结点开始,之后的所有结点都是重合的,不可能再出现分叉。所以两个有公共结点而部分重合的单链表,拓扑形状看起来像 >--
,而不可能像 >-<
。同一地址不可能有不同的值,但同一值可以放在不同的地址中。
链表1:head1 → node1 → node2 → ↘
node5 → node6 → node7 → null
链表2:head2 → node3 → node4 → ↗
顺序遍历两个链表到尾结点时,并不能保证两个链表同时到达尾结点。这是因为两个链表的长度不同。假设一个链表比另一个链表长 k 个结点,我们先在长链表上遍历 k 个结点,之后同步遍历两个链表,这样就能够保证它们同时到达最后一个结点。因为两个链表从第一个公共结点到链表的尾结点都是重合的,所以它们肯定同时到达第一个公共结点。
公共节点必须是地址相同的节点,而非仅值相等。若链表有环,尾节点不存在,长度无法计算(需要用寻找入环点的方法来求),此方法仅适用于两无环单链表。
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (!headA || !headB) return nullptr;
// 1. 计算两链表长度
int lenA = 0, lenB = 0;
ListNode *pA = headA, *pB = headB;
while (pA) { lenA++; pA = pA->next; }
while (pB) { lenB++; pB = pB->next; }
// 2. 长链表指针先移动差值步
pA = headA;
pB = headB;
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++) {
pA = pA->next;
}
} else {
for (int i = 0; i < lenB - lenA; i++) {
pB = pB->next;
}
}
// 3. 同步移动找交点
while (pA && pB && pA != pB) {
pA = pA->next;
pB = pB->next;
}
return pA; // 相遇时为公共节点,或nullptr
}
参考资料:
《2026年数据结构考研复习指导》王道论坛组编
知乎【leetcode 算法汇总 (三)快慢指针】
知乎【Floyd判圈算法与Brent判圈算法】
CSDN【Floyd判圈法(Floyd Cycle Detection Algorithm)】
【如何判断单链表是否有环?链表中"快慢指针"的妙用】
知乎回答
环形链表:快慢指针来帮忙