一、如何确定环的第1个节点
快慢指针(F-S)都从链表头出发,最终在环形轨道上相遇,从而确定有环。此时,慢指针S原地不动,快指针F回到链表头,然后快指针降低速度(跟慢指针一样每次走1步),快慢指针同时往前走,当它们再次相遇的时候,正好在环的第1个节点。
二、为什么再次相遇在环的第1个节点?
为什么F和S再次相遇的地方是环的第1个节点?需要推导。二者相遇在环上,这个问题本质上是模运算的问题。模运算的特点是,溢出重复,但本质上又有所不同。比如,这周的星期一和下周的星期一,就不一样;凌晨1点和中午1点在时钟上位于一个位置,但它们不一样。所以F和S相遇了,但是它们走过的距离是不一样的。
我们可以把环无限展开,变成数轴。先举一个例子,环形链表为0->1->2->[3->4->5->6->7->8->3],无限展开为0->1->2->[3->4->5->6->7->8]->[9/3->10/4->11/5->12/6->13/7->14/8]->[15/3/9->...]->...;即0,1,2在环外,3是环的起点,环的长度为6,所以3/9/15/...是同一点,从3开始,所有相差为6的倍数的那些数都是同一个点。
归纳一下,把链表看成数轴,第1个节点为数轴的原点,在环之前有x个节点,坐标为0,1,...,x-1;环上有r个点,坐标依次为[x,x+1,...,x+r-1],[x+r,x+r+1,...,x+2r-1],...。当快慢指针在环上相遇时,它们的坐标是不同的,因为快指针走得快,所以它的坐标比慢指针要大。另一方面,它们在环上的位置重叠(相遇),那么,它们的坐标差值为r的整倍数。另外,对于任意两个点m和n,当m≥x且n≥x(即m和n都在环上)时,如果|m-n|=kr,那么m和n在环上相遇。所以,从这里可以看出,相遇的等价条件是,坐标差值为r的整倍数。
回到算法上来,F和S都从坐标0开始往前移动,前面讨论过,它们一定会在环上相遇。而相遇的含义是,它们的坐标差值为r的整倍数。假设S走了n步与F在环上相遇,那么,S的坐标为n,F的坐标为2n,它们的差值为2n-n=n=kr(因为是r整倍数,假设为k倍)。假设在环的前面有x个节点,即坐标x是环上的第1个节点,在环之前的坐标为0,1,...,x-1,那么必然有n≥x(即S一定走到了环上然后与F相遇)。而且,如果给出具体的x和r的数字,这里可以计算出F和S第1次相遇在哪个坐标,根据条件n=kr≥x,找到满足该条件的最小整数k,那么kr就是第1次相遇的坐标。
第1次相遇之后,F回到坐标0点,S保持在坐标n点,然后它们以相同的速度1前进,F前进x步,来到环的第1个点坐标为x的位置,而S前进x步来到坐标n+x位置,它们的坐标差为(n+x)-x=n=kr,所以它们在环上相遇,因为x是环上的第1个点,所以它们相遇的位置为环上的第1个点。
整理一下:快慢指针在环上必然相遇,相遇即坐标相差r,首次相遇时n=kr;然后快指针回到原点,二者以相同速度1各自前进,当快指针(注意速度已经恢复正常)到达环的第1个点(坐标为x)时,走过x步,而慢指针也走过x步,来到n+x,此时快慢指针相差为n,正好是r的整倍数,所以它们相遇在环的第1个节点(坐标x位置)。
typedef struct node {
struct node *next;
int value;
} node_t;
node_t*
get_ring_head(node_t *list)
{
node_t *fast = list;
node_t *slow = list;
while (fast && fast->next) {
// 快慢指针都是从第1个节点开始, 开始是相等的, 所以先移动后判断
fast = fast->next->next;
slow = slow->next;
// 首次相遇
if (fast == slow) {
fast = list;
// 当再次相遇时循环退出
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return (fast);
}
}
return (NULL);
}