之前一篇写了如何反转链表以及定位链表的中点,现在来一个稍微复杂点的问题。
这个面试题是我第一次参加实习生笔试时遇到的,当时并没有解出来。题目要求是检测单向链表是否存在环,并且只用常数的额外存储空间。
简单直接的办法自然是将每一个指针记录下来,并且和新出现的节点的next指针比较,如果next指向之前出现过的节点,说明有环。但是这种方法需要至少线性的空间,不符合题目要求。而且每次都需要查找比较,性能很低。或者使用hash表,则空间消耗较list更大。
出来之后我就开始搜索了,解法很好懂,但是如果没有学习过相关算法,要靠自己想出来还是挺有难度的。
解法的奥秘全在下面这张图里了。
算法解析
解法相当简单:两个指针同时从头部出发,normal
以每步1节点的速度向前移动,fast
以两倍速移动,如果二者相遇,则说明存在环。
设计得相当巧妙。有人问有没有可能第一次刚好越过而没有相遇呢?请自己简单分析一下,是不存在这种可能。
定位环起点
这已经不是面试题的要求了,属于扩展内容,需要用到上图标出的等式及其变形。
解释下上图中字母的含义,a是起点到环起点的距离,x为环起点到相遇点的距离,L为环周长,n为fast在环上移动的圈数。
等式如何来的,normal走了a+x
, fast多走了nL
,总里程是2倍的关系,所以a+x = nL
;
变换一下是a = nL - x
, a就是我们要定位的点的位置。
如何使用这个式子,也需要比较巧妙的设计:
- normal
从head
往前走
- fast继续
从相遇点
前移,但这次降为1倍速
- 它们将在
环起始点
相遇
至于为什么,请再仔细参照上面的变形式。
相关代码
使用C实现,顺便熟悉下C语言。
节点定义
1 2 3 4
| typedef struct Node { char *name; struct Node *next; } Node;
|
检测与定位函数
无环返回0,有环返回环起始点指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Node *detectLoop(Node *head) { Node *fast = head, *normal = head; while (fast && fast->next) { normal = normal->next; fast = fast->next->next; if (fast == normal) { break; } } if (!fast) { return 0; } else { //locate the beginning node of the loop normal = head; while (normal != fast) { normal = normal->next; fast = fast->next; } return normal; } }
|
测试主函数
这里使用了一个如下结构的环:
以下是main()函数代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int main(int argc, char *argv[]) { Node node9 = {"9", 0}, node8 = {"8", &node9}, node7 = {"7", &node8}, node6 = {"6", &node7}, node5 = {"5", &node6}, node4 = {"4", &node5}, node3 = {"3", &node4}, node2 = {"2", &node3}, node1 = {"1", &node2}, head = {"0", &node1}; node9.next = &node4; Node *p = &head; int i = 0; while (p && i++ < 12) { printf("%s ", p->name); p = p->next; } printf("\n"); Node *pNode = detectLoop(&head); if (pNode) { printf("The loop starts at: %s", pNode->name); } else { printf("There is no loop"); } return 0; }
|
运行结果 :
1 2
| 0 1 2 3 4 5 6 7 8 9 4 5 The loop starts at: 4
|
好的,测试完成。实质上,返回链表尾(9号结点)更有意义一些,稍作修改即可。