今天看了一篇关于带环单链表精讲的文章,在这里给大家做一个总结。之前看过很多有关单链表带环的文章,但是有些文章讲的太文章化,不容易理解,理论性太强。接下来我会用最简单通俗易懂的语言解析这个问题。
当你拿到一个单链表的数据信息时,我相信大部分的人都会选择去判断一下这个单链表是否有环?这也是最基本的反应。下面我给出单链表的数据结构:
typedef int Elemtype;
//头节点中的data域存放是链表的长度
typedef struct ListNode
{
Elemtype data;//值域
ListNode *m_pNext;//指针域
}ListNode,*PListNode;
链表的具体操作这里就不详细列出了,在我的前几篇博客中都有详细的代码。
一:那么我们接下来分析,如何判断一个链表是否有环呢?
我们采用假设的方法,我们先假设该链表就是一个带环的单链表,那么我们来分析:
有的同学会想到说,既然这个链表是带环的,那么从头开始遍历链表,直至找到某两个节点的地址一样,或者遍历到NULL。这是脑子直的人的想法也是最最简单易懂的方法 。
在单链表的很多处理的过程中我们都会采用一种很有效的处理方式:辅助指针(快慢指针),就像古人说的三个臭皮匠顶过一个诸葛亮。我们让快指针每次走两个单位,慢指针每次走一个单位,我们可以发现,当fast != NULL&&fast->next !=NULL,时只要fast = slow那么这个链表就是带环的,反之不满足上述条件,就是不带环的链表。
//函数名:Isloop
//函数功能:判断是不是带环的链表
//函数参数:链表的头节点指针
bool Isloop(PListNode plist)//plist 为头结点
{
if(plist == NULL) return true;//首先判断传入的指针是否存在
PListNode pf = plist;//快指针
PListNode ql = plist;//慢指针
while(pf != NULL && pf->m_pNext!= NULL)//说明没有走到尾
{
pf = pf->m_pNext->m_pNext;//当出现二级指向时,我们要判断pf->my_pNext不为空
ql = ql->m_pNext;
if(pf == ql)
{
break;
}
}//存在两种情况,①pf或者pf->m_pNext为空②pf=ql
//然后下面的代码就是对这两种情况的解释
if(pf == NULL || pf->m_pNext == NULL)
{
return false;
}
else
{
return true;
}
}
我相信大部分的同学,在判断完成后,肯定心里窃窃欢喜,便把这道题目抛掷脑后了。当时对于初学者的作者来说也是这样,但是后来作者又想了几个值得思考的问题
二:既然我们确定这是个带环的链表那么这个链表的入口点在哪里呢?怎样确定这个入口点呢?
当初我查阅了很多资料,一直以为这是一个很深奥的数学公式推导问题,并且很多资料也就是朝这方面解释的。但是我问了自己的C++老师他给出了最简单易懂的算法:
首先我们来看最简单的一种情况:
首先我们可以很确定的一件事:那就是fast和slow走的步数一样,但是由于fast一次走两步,所以fast走过的长度是slow的两倍。
同时我们模拟出最简单的情况:我们假设不在环的直线部分是很短的(因为这里有一个临界值)那样的话fast只在环中走了一圈,蓝色就是slow所走的路径,绿色就是fast所走的路径,同时呢在下面那张图中红色部分就是fast比slow多走 的那部分。
同时呢由于len fast = 2 * len slow,所以呢红色部分和slow的蓝色路径一样,
同时呢我们发现他们有共同的部分,去掉共同部分。我们得到一个很重要的信息:
从起点到入口点 和 相遇点到入口点 的长度一样,这就是我们的算法。
还有一种普通的情况那就是,当直线部分很长,超过了那个临界点。此时fast比slow在环内多走了n圈,但是上面的理论对于这种普通的情况仍然使用,只不过是fast在环内走了很多圈。
代码实现;
//环的入口点
//函数名:FindEtrance
//函数功能:找到环的入口点并且返回该节点的指针
//函数返回值:节点地址
PListNode FindEtrance(PListNode plist)
{
PListNode fast = plist;//快指针
PListNode slow = plist;//慢指针
while(fast != NULL && fast->m_pNext != NULL)
{
fast = fast->m_pNext->m_pNext;
slow = slow->m_pNext;
if(fast == slow)
{
break;
}
}
//分为两种情况
if(fast == NULL && fast->m_pNext == NULL)
{
return NULL;
}
//是环的前提下,此时快指针已经追上慢指针
slow = plist;//从此时开始快慢各走一步下次相遇即是环的入口点
while(slow != fast)
{
fast = fast->m_pNext;
slow = slow->m_pNext;
}
return slow;
}
三:既然我们现在也知道了环的入口点,那么还有什么可以探寻的呢?
我们想要知道环的长度:
算法分析:当fast和slow第一次在相遇点相遇时,此后他们人生的道路只能在那个环里度过了,同时我们还可以得到一个结论
那就是,相对慢指针来说,他从相遇点,到下一次相遇时刚好慢指针走了环的长度,
这就是解决此问题的算法:
代码如下:
//计算环的长度,第一次相遇开始计数,第二次停止计数
//函数名:Length
//函数功能:计算链表环的长度
//函数返回值:环的长度
int length(PListNode plist)
{
PListNode fast = plist;//快
PListNode slow = plist;//慢
int length = 0;
bool begin = false;
bool again = false;
while(fast != NULL && fast->m_pNext != NULL)
{
fast = fast->m_pNext->m_pNext;
slow = slow->m_pNext;
if(fast == slow && again == true)
{
break;
}
if(fast == slow && again == false)
{
begin = true;
again = true;
}
//第一次到第二次相遇,通过慢指针来计数
if(begin == true)
{
++length;
}
}
return length;
}
到现在,我整理的单链表的内容已经基本完整了。
如果有什么错误或者意见希望大家多多提出。