在单链表中查找循环

Posted

技术标签:

【中文标题】在单链表中查找循环【英文标题】:Finding loop in a singly linked-list 【发布时间】:2012-05-03 18:31:42 【问题描述】:

如何检测单链表是否有循环? 如果它有循环,那么如何找到循环的起点,即循环开始的节点。

【问题讨论】:

在链表中查找循环在 Elements of Programming 中进行了讨论,毫无疑问在许多其他地方。 另一种算法解释,也可以找到第一个循环元素:marcin-chwedczuk.github.io/… 可能重复***.com/questions/2936213/… How to detect a loop in a linked list?的可能重复 我的一个朋友问我这个问题,他允许我用 O(1) 复杂度实现它,但我仍然坚持这一点。谁能解决我的问题?谢谢 【参考方案1】:

你可以通过简单地在列表中运行两个指针来检测它,这个过程在同名寓言中被称为龟兔算法:

首先,检查列表是否为空(headnull)。如果是这样,则不存在循环,因此请立即停止。 否则,在第一个节点head上启动第一个指针tortoise,在第二个节点head.next上启动第二个指针hare。 然后连续循环直到harenull(这在单元素列表中可能已经是正确的),在每次迭代中将tortoise 提前1 和hare 2。保证兔子首先到达终点(如果有 终点),因为它开始领先并且跑得更快。 如果没有结束(即,如果有一个循环),它们最终将指向同一个节点,您可以停止,因为您知道您在循环内某处找到了一个节点。

考虑以下从3 开始的循环:

head -> 1 -> 2 -> 3 -> 4 -> 5
                  ^         |
                  |         V
                  8 <- 7 <- 6

tortoise 从 1 开始,hare 从 2 开始,它们具有以下值:

(tortoise,hare) = (1,2) (2,4) (3,6) (4,8) (5,4) (6,6)

因为它们在(6,6) 处相等,并且由于hare 应该总是在非循环列表中超出tortoise,这意味着您发现了一个循环。

伪代码如下所示:

def hasLoop (head):
  return false if head = null           # Empty list has no loop.

  tortoise = head                       # tortoise initially first element.
  hare = tortoise.next                  # Set hare to second element.

  while hare != null:                   # Go until hare reaches end.
    return false if hare.next = null    # Check enough left for hare move.
    hare = hare.next.next               # Move hare forward two.

    tortoise = tortoise.next            # Move tortoise forward one.

    return true if hare = tortoise      # Same means loop found.
  endwhile

  return false                          # Loop exit means no loop.
enddef

该算法的时间复杂度为O(n),因为访问的节点数(龟兔赛跑)与节点数成正比。


一旦您知道循环中的节点,还有一个O(n) 保证方法可以找到循环的开始

当你在循环的某处找到一个元素但你不确定循环的开始在哪里之后,让我们回到原来的位置。

head -> 1 -> 2 -> 3 -> 4 -> 5
                  ^         |
                  |         V
                  8 <- 7 <- 6
                             \
                              x (where hare and tortoise met).

这是要遵循的过程:

提前hare 并将size 设置为1。 然后,只要haretortoise不同,继续前进hare,每次增加size。这最终给出了循环的大小,在本例中为 6。 此时,如果size1,这意味着您必须已经处于循环的开始(在大小为 1 的循环中,只有一个可能的节点可以在循环中,所以它必须是第一个)。在这种情况下,您只需返回 hare 作为开始,然后跳过下面的其余步骤。 否则,将haretortoise 设置为列表的第一个 元素,并将hare 精确地推进size 次(在本例中为7)。这给出了两个指针,它们恰好循环的大小不同。 然后,只要 haretortoise 不同,就将它们一起推进(兔子以更平稳的速度奔跑,与乌龟的速度相同 - 我猜它从第一次奔跑就累了)。由于它们将始终完全保持 size 元素彼此分开,tortoise到达循环的开始恰好hare 返回循环开始。

您可以通过以下演练看到这一点:

size  tortoise  hare  comment
----  --------  ----  -------
   6         1     1  initial state
                   7  advance hare by six
             2     8  1/7 different, so advance both together
             3     3  2/8 different, so advance both together
                      3/3 same, so exit loop

因此3 是循环的起点,并且由于这两个操作(循环检测和循环开始发现)都是O(n) 并按顺序执行的,所以整个事情加在一起也是O(n)


如果您想要一个更正式的证明,可以查看以下资源:

question 在我们的姊妹网站上; Wikipedia cycle detection 页面;或 Peter Gammie 的“龟兔赛跑算法”,2016 年 4 月 17 日。

如果您只是在支持该方法(不是正式证明),您可以运行以下 Python 3 程序,该程序评估其在大量尺寸(循环中有多少元素)和导入(循环开始之前的元素)。

你会发现它总是找到两个指针相交的点:

def nextp(p, ld, sz):
    if p == ld + sz:
        return ld
    return p + 1

for size in range(1,1001):
    for lead in range(1001):
        p1 = 0
        p2 = 0
        while True:
            p1 = nextp(p1, lead, size)
            p2 = nextp(nextp(p2, lead, size), lead, size)
            if p1 == p2:
                print("sz = %d, ld = %d, found = %d" % (size, lead, p1))
                break

【讨论】:

我们能比 O(n^2) 更好地找到循环的开始吗? 当您在循环中找不到 C 时,我理解将 C 前进一个。但是,将 B 提前一位真的有必要吗?我们知道 B 在循环内。只要它在循环内,它在什么位置都应该是对的?它要么与 C 相遇(在循环开始时),要么再次与自己相遇。是为了一些运行时优化? @Jonathan,在每个周期开始时将B 提前一个是为了确保它不会开始等于A。那是因为A == BC 还没有在循环中的信号(B 已经运行了整个循环而没有找到C)。如果我们以A == B开头,循环将立即退出。 @user3740387,您可能想看看 math.stackexchange.com/questions/913499/…、en.wikipedia.org/wiki/Cycle_detection 或 Peter Gammie 于 2016 年 4 月 17 日撰写的“龟兔赛跑算法”。理解它(比我目前准备做的工作更多),但他们在这件事上似乎非常确定。 @Sisir,这是 O(n),因为您最多检查列表中的每个元素一次。我会将其添加到答案中。【参考方案2】:

下面的代码会判断SLL中是否存在循环,如果存在则返回起始节点。

int find_loop(Node *head)

    Node * slow = head;
    Node * fast =  head;
    Node * ptr1;
    Node * ptr2;
    int k =1, loop_found =0, i;

    if(!head) return -1;

    while(slow && fast && fast->next)
            slow = slow->next;
        /*Moving fast pointer two steps at a time */
            fast = fast->next->next;
            if(slow == fast)
                    loop_found = 1;
                    break;
            

    

    if(loop_found)
    /* We have detected a loop */
    /*Let's count the number of nodes in this loop node */

            ptr1  = fast;
            while(ptr1 && ptr1->next != slow)
                    ptr1 = ptr1->next;
                    k++;
            
    /* Now move the other pointer by K nodes */
            ptr2 = head;

            ptr1  = head;
            for(i=0; i<k; i++)
                    ptr2 = ptr2->next;
            

    /* Now if we move ptr1 and ptr2 with same speed they will meet at start of loop */

            while(ptr1 != ptr2)
                    ptr1  = ptr1->next;
                    ptr2 =  ptr2->next;
            

    return ptr1->data;


【讨论】:

【参考方案3】:

选择的答案给出了一个 O(n*n) 解决方案来找到循环的开始节点。这是一个 O(n) 的解决方案:

一旦我们发现慢 A 和快 B 在循环中相遇,让它们中的一个静止,而另一个每次继续走一步,以确定循环的周长,例如,P。

然后我们把一个节点放在头部,让它走P步,然后把另一个节点放在头部。我们将这两个节点每次都推进一步,当它们第一次相遇时,它就是循环的起点。

【讨论】:

这其实很聪明。计算出循环的长度(周长),然后同步推进两个指针,精确地分开该距离直到它们相等,这是一个比我最初给出的更好的解决方案。 +1。我已将其合并到接受的答案中,在此过程中删除了效率较低的 O(n^2) 方法。 那是著名的龟兔算法:) en.wikipedia.org/wiki/Cycle_detection 一位面试官问我“为什么有必要 - 当他们第一次见面时,这是循环的起点。”如何从逻辑上证明这种说法? @Bhavuk - 这是有道理的,因为通过以相等的速度运行这些指针,您始终将距离保持为 loopsoze 常数。所以一旦他们再次相遇,你可以肯定地说循环开始了,这是循环的起点。 为了更直观的例子,想想模拟时钟中的时针和分针,它们以不同的速度运行,但它们彼此相遇【参考方案4】:

你也可以使用hash map来判断一个链接列表是否有循环下面的函数使用hash map来判断链接列表是否有循环

    static bool isListHaveALoopUsingHashMap(Link *headLink) 

        map<Link*, int> tempMap;
        Link * temp;
        temp = headLink;
        while (temp->next != NULL) 
            if (tempMap.find(temp) == tempMap.end()) 
                tempMap[temp] = 1;
             else 
                return 0;
            
            temp = temp->next;
        
        return 1;
    

双指针方法是最好的方法,因为时间复杂度是 O(n) Hash Map 需要额外的 O(n) 空间复杂度。

【讨论】:

【参考方案5】:
boolean hasLoop(Node *head)
    
      Node *current = head;
      Node *check = null;
      int firstPtr = 0;
      int secondPtr = 2;
      do 
        if (check == current) return true;
        if (firstPtr >= secondPtr)
            check = current;
            firstPtr = 0;
            secondPtr= 2*secondPtr;
        
        firstPtr ++;
       while (current = current->next());
      return false;
    

另一个 O(n) 解决方案。

【讨论】:

【参考方案6】:

一种完全不同的方法:- 反转链表。 倒车时,如果您再次到达头部,则列表中有一个循环, 如果你得到 NULL 那么没有循环。 总时间复杂度为O(n)

【讨论】:

如果有循环可以反转吗?它不会无限运行,因为您永远不会到达终点开始倒车吗? 当您尝试反向列表添加时,检查是否重新访问头部的条件。所以对于 a->b->c->d->b 将终止为 a 能不能礼貌点,举个例子【参考方案7】:

在查看所选答案时,我尝试了几个示例并发现: if (A1,B1), (A2,B2) ... (AN, BN) 是指针 A 和 B 的遍历 其中A步1个元素,B步2个元素,并且,Ai和Bj是A和B经过的节点,AN=BN。 那么,循环开始的节点是Ak,其中k = floor(N/2)。

【讨论】:

【参考方案8】:

好的 - 我昨天在一次采访中遇到了这个问题 - 没有可用的参考资料,我想出了一个非常不同的答案(当然是在开车回家的时候......)因为链接列表是正常分配的(我并不总是承认)使用 malloc 逻辑,我们知道分配的粒度是已知的。在大多数系统上,这是 8 个字节 - 这意味着底部 3 位始终为零。考虑 - 如果我们将链表放在一个类中以控制访问并使用 0x0E 的掩码 ored 到下一个地址,那么我们可以使用低 3 位来存储中断面包屑因此我们可以编写一个方法来存储我们的最后一个面包屑- 说 1 或 2 - 并交替使用它们。然后,我们检查循环的方法可以逐步遍历每个节点(使用我们的下一个方法)并检查下一个地址是否包含当前的面包屑 - 如果有,我们有一个循环 - 如果没有,那么我们将屏蔽低 3 位并添加我们当前的面包屑。面包屑检查算法必须是单线程的,因为您不能一次运行其中两个,但它会允许其他线程异步访问列表 - 关于添加/删除节点的常见警告。你怎么看?如果其他人认为这是一个有效的解决方案,我可以编写示例类......只是想想有时一种新的方法是好的,并且总是愿意被告知我刚刚错过了重点......谢谢所有标记

【讨论】:

【参考方案9】:

我在 Narasimha Karamanchi 的数据结构书中读到了这个答案。

我们可以使用弗洛伊德循环寻找算法,也称为龟兔算法。在这里,使用了两个指针;一个(比如slowPtr)由一个节点推进,另一个(比如fastPtr)由两个节点推进。如果单链表中存在任何循环,它们肯定会在某个时候相遇。

struct Node
int data;
struct Node *next;



 // program to find the begin of the loop

 int detectLoopandFindBegin(struct Node *head)
      struct Node *slowPtr = head, *fastPtr = head;
      int loopExists = 0;
      // this  while loop will find if  there exists a loop or not.
      while(slowPtr && fastPtr && fastPtr->next)                                                  
        slowPtr = slowPtr->next;                      
        fastPtr = fastPtr->next->next;
        if(slowPtr == fastPtr)
        loopExists = 1;
        break;
      

如果存在任何循环,那么我们将其中一个指针指向头部,现在将它们都推进一个节点。他们将相遇的节点将是单链表中循环的开始节点。

        if(loopExists)      
             slowPtr = head;
             while(slowPtr != fastPtr)
               fastPtr = fastPtr->next;
               slowPtr = slowPtr->next;
             
             return slowPtr;
          
         return NULL;
        

【讨论】:

【参考方案10】:

另一种解决方案

检测循环:

    创建一个列表 循环遍历链表并继续将节点添加到链表中。 如果节点已经存在于列表中,我们就有了一个循环。

去除循环:

    在上面的步骤#2 中,while 循环遍历链表,我们还跟踪前一个节点。

    一旦我们在第 3 步中检测到循环,将前一个节点的下一个值设置为 NULL

    #代码

    def detect_remove_loop(head)

        cur_node = head
        node_list = []
    
        while cur_node.next is not None:
            prev_node = cur_node
            cur_node = cur_node.next
            if cur_node not in node_list:
                node_list.append(cur_node)
            else:
                print('Loop Detected')
                prev_node.next = None
                return
    
        print('No Loop detected')
    

【讨论】:

【参考方案11】:

在大多数情况下,以前的所有答案都是正确的,但这里是带有可视化和代码的逻辑的简化版本(适用于 Python 3.7)

正如其他人解释的那样,逻辑非常简单。我要创建乌龟/慢和野兔/快。如果我们以不同的速度移动两个指针,那么最终快会遇到慢!!您也可以将其视为大头针圆形场地中的两个跑步者。如果跑得快的人继续绕圈,那么它会遇到/超过慢跑者。

因此,我们将在每次迭代中以 1 的速度移动 Tortoise/slow 指针,同时以 2 的速度继续递增或移动 Hare/fast 指针。一旦它们相遇,我们就知道有一个循环。这也称为Floyd's cycle-finding algorithm

这是执行此操作的 Python 代码(注意 has_cycle 方法是主要部分):

#!/usr/bin/env python3
class Node:
    def __init__(self, data = None):
        self.data = data
        self.next = None
    def strnode (self):
        print(self.data)


class LinkedList:
    def __init__(self):
        self.numnodes = 0
        self.head = None


    def insertLast(self, data):
        newnode = Node(data)
        newnode.next = None
        if self.head == None:
            self.head = newnode
            return

        lnode = self.head
        while lnode.next != None :
            lnode = lnode.next
        lnode.next = newnode # new node is now the last node
        self.numnodes += 1

    def has_cycle(self):    
        slow, fast = self.head ,self.head  
        while fast != None:       
            if fast.next != None:
                 fast = fast.next.next
            else:
                 return False
            slow = slow.next  
            if slow == fast:
                print("--slow",slow.data, "fast",fast.data) 
                return True    
        return False


linkedList = LinkedList()
linkedList.insertLast("1")
linkedList.insertLast("2")
linkedList.insertLast("3")


# Create a loop for testing 
linkedList.head.next.next.next = linkedList.head; 
#let's check and see !
print(linkedList.has_cycle())

【讨论】:

【参考方案12】:

首先,创建一个节点

struct Node  
    int data; 
    struct Node* next; 
; 

全局初始化头指针

Struct Node* head = NULL;

在链表中插入一些数据

void insert(int newdata)

    Node* newNode = new Node();
    newNode->data = newdata;
    newNode->next = head;
    head = newNode;

创建一个函数detectLoop()

void detectLoop()
    if (head == NULL || head->next == NULL)
        cout<< "\nNo Lopp Found in Linked List";
    
    else
        Node* slow = head;
        Node* fast = head->next;
        while((fast && fast->next) && fast != NULL)
            if(fast == slow)
                cout<<"Loop Found";
                break;
            
            fast = fast->next->next;
            slow = slow->next;
        
        if(fast->next == NULL)
            cout<<"Not Found";
        
    

从 main() 调用函数

int main() 
 
    insert(4);
    insert(3);
    insert(2);
    insert(1);

    //Created a Loop for Testing, Comment the next line to check the unloop linkedlist
    head->next->next->next->next = head->next;

    detectLoop();
    //If you uncomment the display function and make a loop in linked list and then run the code you will find infinite loop 
    //display();
 

【讨论】:

完整程序:github.com/iamrahman/DataStructure/blob/master/…【参考方案13】:
                bool FindLoop(struct node *head)
                
                    struct node *current1,*current2;

                    current1=head;
                    current2=head;

                    while(current1!=NULL && current2!= NULL && current2->next!= NULL)
                     
                          current1=current1->next;
                          current2=current2->next->next;

                          if(current1==current2)
                          
                                return true;
                          
                    

                    return false;
                

【讨论】:

以上是关于在单链表中查找循环的主要内容,如果未能解决你的问题,请参考以下文章

检测单链表中循环的开始?

单链表中查找倒数第K个节点

[数据结构与算法] 链表的其他类型

第二十三课 顺序表和单链表的对比分析

数据结构不带头结点非循环的单链表

单链表的基础操作