链表中“快慢指针”的妙用

寻找链表的中点

相关习题:LeetCode 876. 链表的中间结点

我们可以设置快慢指针,快指针(fast)一次走两步,慢指针(slow)一次走一步。fast 走完整条链表时,slow 指向的结点就是链表的中点。

该方法通过速度差来寻得链表中点。同理也可求得链表 2 / 3 处的结点。故假设slow一次走 s 步,fast 一次走 f 步,则当 fast 走完整条链表时,slow 指向链表 s / f 处的结点。

寻找链表的倒数第k个结点

相关习题:LeetCode 19. 删除链表的倒数第 N 个结点

设置p1p2两个指针,让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}} Vslownk=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 n2n 个结点,即 k = n − n 2 k=n - \frac n 2 k=n2n ,无法求得 k 的值。

关键要点

  1. 速度差与相对距离的适用场景
  • 速度差法(快慢指针)适用于求解链表的比例位置,像中点、1/3 处等节点。
  • 相对距离法(间隔指针)适用于求解链表的倒数位置,例如倒数第 k 个节点。
  1. 注意事项
  • 在使用间隔指针法时,要留意链表长度是否大于等于 k。
  • 当链表长度为偶数时,快慢指针法得到的中点是中间两个节点中的第二个。若想获取第一个中间节点,可对终止条件进行调整。

双指针策略通过巧妙设定指针的移动速度或者间隔距离,能够在一次遍历链表的过程中完成目标节点的定位,时间复杂度为 O (n),空间复杂度为 O (1),实现了效率的优化。

判断回文

相关习题:LeetCode 206. 反转链表LeetCode 234. 回文链表

快慢指针 + 反转链表,该思路通过 O(n) 时间复杂度和 O(1) 空间复杂度解决问题,是判断单链表回文的最优解法之一。唯一需要注意的是边界条件处理(如空链表、单节点链表)和链表反转时的指针操作细节。

思路

  1. 找中点:用快慢指针(快指针一次走两步,慢指针一次走一步)找到链表中点。
  2. 反转后半段:将后半段链表反转,使后半段顺序与前半段对称。用头插法将后半段链表结点插入新链表。
  3. 双指针比较:前半段从头开始,反转后的后半段从新头节点开始,逐一比较节点值。
  4. 恢复链表(可选):若需保持原链表结构,可再次反转后半段恢复。用尾插法。
#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多若干个环的长度。

快指针一次走两步,慢指针一次走一步。如果快慢指针能相遇,说明链表有环。无论落后几步,比如下方的落后三步、两步、一步,fastslow指针终会相遇。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

当慢指针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=(f2s)r,令 n = f − 2 s n=f-2s n=f2s
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=nra,无论 a , r a,r a,r 有多长,总存在非负整数 n n n ,使得点 m ≥ 0 m\geq0 m0,即相遇点存在。
m ≥ 0 m\geq 0 m0 n ≥ a r n\geq\frac ar nra,故第一次相遇时 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 n1,故 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=f2s=f,即 n n n 为相遇时fast绕过了的圈数,因为fast要绕后追slow,所以 n ≥ 1 n \geq 1 n1

快指针与慢指针的相对速度为 1 步 / 次,保证了两者的距离每次至少减少 1,因此快指针必然在相遇前逐步逼近慢指针,而不会跳过。这一性质是 Floyd 判环算法的核心逻辑之一,确保了算法在 O (n) 时间内正确检测到环。

寻找入环点

相关习题:LeetCode 142. 环形链表 II

快慢指针相遇之后,让快指针重新回到头结点,然后让两指针每次都走一步,两指针再次相遇的结点就是入环点。通过 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=(n1)r+(rm) ,环外结点的长度就等于相遇点指针转 n-1 圈后( n ≥ 1 n\geq1 n1),回到 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) (b1)
  • 快指针速度为 k ≥ 1 k\geq1 k1(慢指针速度为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=[kaa]%b=[a(k1)]%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) xc+kx (mod b)(k1)xc (mod b)

代入 c = [ a ( k − 1 ) ] % b c = \left[a(k-1)\right] \% b c=[a(k1)]%b,即 c = a ( k − 1 ) − m b c = a(k-1) - mb c=a(k1)mb(m 为整数),

得: ( k − 1 ) ( x + a ) ≡ 0   ( mod  b ) (k-1)(x + a) \equiv 0 \ (\text{mod} \ b) (k1)(x+a)0 (mod b)

3. 同余方程求解

d = gcd ⁡ ( k − 1 , b ) d = \gcd(k-1, b) d=gcd(k1,b),则方程可约简为:

k − 1 d ( x + a ) ≡ 0   ( mod  b d ) \frac{k-1}{d}(x + a) \equiv 0 \ (\text{mod} \ \frac{b}{d}) dk1(x+a)0 (mod db)

由于 k − 1 d \frac{k-1}{d} dk1 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+a0 (mod db)xa (mod db)

其最小非负整数解为: x = ( b d − a m o d    b d ) x = \left(\frac{b}{d} - a \mod \frac{b}{d}\right) x=(dbamoddb)

x = b d ⋅ t − a x = \frac{b}{d} \cdot t - a x=dbta(t 为使 x ≥ 0 x \geq 0 x0 的最小正整数)。

结论

  1. k > 1 k > 1 k>1
  • 无论 a 和 b 取何值,方程必有解,快慢指针必定相遇。
  1. k = 1 k = 1 k=1
  • 快慢指针速度相同,轨迹完全重合。
  • a = 0 a = 0 a=0(头结点即入环点):初始即相遇;
  • a ≥ 1 a \geq 1 a1:慢指针入环时(时刻 a),快指针也同步到达入环点,此后持续相遇。
  • 因此, k = 1 k = 1 k=1 时两者仍必定相遇
  1. 唯一例外
  • 若链表无环( b = 0 b = 0 b=0),则快慢指针永远不会相遇。但此情形与前提 “环内有 b 个节点” 矛盾。

最终结论

  • 对于存在环的链表,无论 k ≥ 1 k \geq 1 k1 取何值,快慢指针初始均指向头结点时必定相遇。
  • 注意:相遇不代表不会有跳过,可能跳过慢指针几次才与它相遇。

公共结点问题

相关题目: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)】
【如何判断单链表是否有环?链表中"快慢指针"的妙用】
知乎回答
环形链表:快慢指针来帮忙

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你要飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值