您的位置:首页 > 其它

《算法之美》の链表问题の判断链表循环与否

2010-05-23 11:40 197 查看
问题:
一个链表要么以NULL结尾(非循环的),要么以循环结尾(循环的),请编写一个函数,接受链表的头指针作为参数,确定该链表是循环的还是非循环的。如果链表是循环的,函数返回true,如果是非循环的,函数返回false。注意,不能以任何方式修改链表。
 
解答:
这两种链表的区别在与它们的末尾。在非循环链表中,末尾节点是以NULL结束的,因此只要遍历链表,直到找到一个以NULL结尾的节点就行;但在非循环链表中,仅仅遍历链表,就会陷入死循环中。
 
所以我们先研究一下末尾节点。对于循环链表中末尾节点指向的节点,还有另一个指针(头指针)指向它。这意味着有两个指针指向了同一个节点,这个节点是唯一一个有两个元素指向的节点。根据这一特征,我们可以遍历该链表,检查每个节点,确定是否有两个节点指向它,如果有,则该链表肯定是循环链表,否则,该链表是非循环的,则最终会遇到一个NULL指针。不幸的是,我们很难检查指向每一个元素的节点数。
 
除了检查是否有两个指针指向同一节点之外,我们还可以检查是否曾经遇到过某个节点。如果发现一个曾经遇到过的节点,就表明这是一个循环的链表;如果遇到NULL指针,就表明这是一个非循环链表。现在关键是怎样判断是否遇到过某个节点呢,最简单的就是遍历每个元素时作标记,但题目不允许我们修改该链表。
 
那么我们可以利用链表已有的东西,因为我们知道链表的当前节点和链表的头指针,因此可以将当前节点的next指针与前面所有的节点直接进行比较。如果对于第i个节点,比较它的next指针,看是否指向了1到i-1号节点之中的某一个。如果出现相等的情况,说明这是一个循环链表。但这个算法的时间复杂度是O(n2)。
 
我们有一种使用两个指针的更好的算法,交替移动两个指针,且两个指针的移动速度不一样。在非循环链表中,较快的指针会先到达末尾;在循环链表中,两个指针会陷入无限循环,较快的指针最终会赶上并超过较慢的指针。如果较快的指针最终超过了较慢的指针,说明这是一个循环链表。如果它遇到一个NULL指针,说明这是一个非循环链表。
 
算法的实现代码如下:

#include <iostream>

 
struct ListNode
{
    int m_nKey;
    ListNode* m_pNext;      
};
 
void InitList(ListNode** pList)
{
    *pList = (ListNode*)malloc(sizeof(ListNode));
    (*pList)->m_pNext = NULL;
}
 
void InsertList(ListNode* pList, int data)
{
     ListNode* pNewNode = (ListNode*)malloc(sizeof(ListNode));
     pNewNode->m_nKey = data;
     pNewNode->m_pNext = pList->m_pNext;
     pList->m_pNext = pNewNode;
}
 
bool determinTermination(ListNode *head)
{
     if(head == NULL)
         return false;
     ListNode *fast, *slow;
     slow = fast = head;
    
     while(true)//排除fast->m_pNext->m_pNext不存在的情况
     {
         if(fast == NULL || fast->m_pNext == NULL)
             return false;  //非循环的
          
         slow = slow->m_pNext;
         fast = fast->m_pNext->m_pNext;
        
         if(fast == slow || fast->m_pNext == slow)
             return true;     //循环的
     }
}
 
int main()
{
    ListNode* pListHead = NULL;
    InitList(&pListHead);
    for(int i=9; i>=0; i--)
    {
        InsertList(pListHead, i);           
    }
   
    if(determinTermination(pListHead) == true)
        std::cout<<"1)单向链表是循环的"<<std::endl;
    else
        std::cout<<"1)单向链表是非循环的"<<std::endl;
       
    ListNode* pTmp = pListHead;
    while(pTmp->m_pNext != NULL)
    {
        pTmp = pTmp->m_pNext;                   
    }
    pTmp->m_pNext = pListHead->m_pNext; //连接成循环链表
   
    if(determinTermination(pListHead) == true)
        std::cout<<"2)单向链表是循环的"<<std::endl;
    else
        std::cout<<"2)单向链表是非循环的"<<std::endl;
   
    system("pause");
    return 0;
}
 
 
复杂度分析:
如果链表是非循环的,较快的指针会在检查了n个节点之后到达链表的末尾,而较慢的指针只遍历了1/2的节点。因此总共遍历了3/2的节点,时间复杂度是O(n)。
 
如果链表是循环的,较慢的指针永远也不会遍历超过一次。当较慢的指针检查了n个节点时,较快的指针已经检查了2n个节点,并超过了较慢的指针。最坏情况下,需要检查3n个节点,时间复杂度是O(n)。
 
因此,不论链表是循环的还是非循环的,这种两个指针的方法都比一个指针的方法好很多。

 
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 null system n2