Cracking the Coding Interview:: 寻找有环链表的环路起始节点

简介:

给定一个有环链表,实现一个算法返回环路的开头节点。 这个问题是由经典面试题-检测链表是否存在环路演变而来。这个问题也是编程之美的判断两个链表是否相交的扩展问题。

首先回顾一下编程之美的问题。 由于如果两个链表如果相交,那么交点之后node都是共享(地址相同)的,因此最简单暴力的方法就是两个for循环,判断该链表的node是否属于另外一个链表。但是这个算法复杂度是O(length1 * length2)。如果链表较长,这个复杂度有点高了。

当然也可以遍历其中某个链表,将node的地址存储hash table中;然后接下来遍历另外一个链表,查找node是否在这个hash table中。这样的话时间复杂度就是O(length1 + length2)。但是需要额外的O(length1)的空间。在C++ STL Java等主流语言中,hash table都有很好的实现,因此编程也比较简单。

注意我们这个方法是认定如果两个链表相交,那么肯定交点以后的点都是共享的,包括最后一个点。那么找到链表的最后一个节点,如果节点相同,那么就相交。当然了,以上算法需要处理有环存在的情况。

本文的问题是寻找有环链表的开头节点,那么如何判断一个链表是否有环?

我们可以用快慢游标的方法。具体来说,就是使用两个游标遍历链表,其中快游标(快指针,fastRunner)每次经过两个node,慢游标(慢指针,slowRunner)每次经过一个node。那么如果有环,那么这两个游标肯定会相遇。使用反证法可以推理这个推论是正确的,假如fastRunner正好经过了slowRunner,而没有相遇,那么上一部,就是fastRunner后退两步,slowRunner后退一步,两个runner必定相遇。如果现在slowRunner正好领先一步fastRunner,那么下一步二者相遇。

bool checkLinkRing(const Node *head)
{
  if( head == nullptr )
    return false;

  auto fastRunner = head;
  auto slowRunner = head;
  while( fastRunner->next != nullptr && fastRunner->next->next != nullptr )
  {
    fastRunner = fastRunner->next->next;
    slowRunner = slowRunner->next;
    if( fastRunner == slowRunner )
      return true;
  }

  return false;
}
接下来的问题,如何找到环的起始点?看下图,假设交点在K处

那么在slowRunner走了K步到达交点,那么此时fastRunner走了2K步到达黄圈处。那么fastRunner距离追上slowRunner还有RingSize - K步。 注意,只能顺时针前进。fastRunner相对slowRunner的速度是每步经过一个node(fastRunner虽然每次经过2个node,但是slowRunner也向前走了一个node,因此每步二者的距离仅仅减少一个node),因此在走了RingSize - K时相遇。也就是slowRunner在进入环后,再走RingSize - K 步后二者相遇于绿处。此时,绿点顺时针距离交点有RingSize - ( RingSize - K) = K。注意括号内的RingSize - K是slowRunner走的。

关键问题出来了,在二者相遇的点,距离交点也是K: 与head到交点的距离相同。因此,我们可以调整两个游标,使slowRunnder从头开始,fastRunnder从相遇点开始,二者以相同的速度前行,必然在相交点再次相遇!

auto findRingCrossPoint(const Node *head) ->decltype(head)
{
  if( head == nullptr )
    return nullptr;

  auto fastRunner = head;// C++11, fastRunner is const Node *
  auto slowRunner = head;
  while( fastRunner->next != nullptr && fastRunner->next->next != nullptr )
  {
    fastRunner = fastRunner->next->next;
    slowRunner = slowRunner->next;
    if( fastRunner == slowRunner )
      break;
  }
  
  // in case that there is no ring.
  if( fastRunner != slowRunner )
    return nullptr;

  slowRunner = head;
  while( true )
  {
    fastRunner = fastRunner->next;
    slowRunner = slowRunner->next;
    if( fastRunner == slowRunner )
    {
      break;
    }
  }

  return fastRunner;
}


目录
相关文章
|
6月前
|
存储 Python
链表中插入节点
链表中插入节点
|
30天前
LeetCode第二十四题(两两交换链表中的节点)
这篇文章介绍了LeetCode第24题的解法,即如何通过使用三个指针(preNode, curNode, curNextNode)来两两交换链表中的节点,并提供了详细的代码实现。
16 0
LeetCode第二十四题(两两交换链表中的节点)
|
30天前
Leetcode第十九题(删除链表的倒数第N个节点)
LeetCode第19题要求删除链表的倒数第N个节点,可以通过快慢指针法在一次遍历中实现。
38 0
Leetcode第十九题(删除链表的倒数第N个节点)
|
23天前
(剑指offer)18、删除链表的节点—22、链表中倒数第K个节点—25、合并两个排序的链表—52、两个链表的第一个公共节点(2021.12.07)
(剑指offer)18、删除链表的节点—22、链表中倒数第K个节点—25、合并两个排序的链表—52、两个链表的第一个公共节点(2021.12.07)
40 0
|
3月前
|
算法
LeetCode第24题两两交换链表中的节点
这篇文章介绍了LeetCode第24题"两两交换链表中的节点"的解题方法,通过使用虚拟节点和前驱节点技巧,实现了链表中相邻节点的交换。
LeetCode第24题两两交换链表中的节点
05_删除链表的倒数第N个节点
05_删除链表的倒数第N个节点
04_两两交换链表中的节点
04_两两交换链表中的节点
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 22. 链表中倒数第k个节点
Leetcode题目"剑指 Offer 22. 链表中倒数第k个节点"的Python解决方案,使用双指针法找到并返回链表中倒数第k个节点。
51 5
|
3月前
|
Python
【Leetcode刷题Python】剑指 Offer 18. 删除链表的节点
Leetcode题目"剑指 Offer 18. 删除链表的节点"的Python解决方案,通过使用双指针法找到并删除链表中值为特定数值的节点,然后返回更新后的链表头节点。
40 4
|
4月前
|
安全 云计算
云计算自旋锁问题之在线程安全地删除链表节点时,需要频繁加锁会影响性能如何解决
云计算自旋锁问题之在线程安全地删除链表节点时,需要频繁加锁会影响性能如何解决
49 2