问题:

一个链表要么以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)。

 

因此,不论链表是循环的还是非循环的,这种两个指针的方法都比一个指针的方法好很多。