1. 程式人生 > >如何判斷單鏈表是否有環、環的入口、環的長度和總長

如何判斷單鏈表是否有環、環的入口、環的長度和總長

問題描述
1.如何判斷單鏈表是否有環?
2.如果有環,求出環的入口
3.求環長
4.求總長

探討
要想判斷有環,我們可以聯絡實際生活中的例子,很容易就想到操場上跑圈,因為是環形,所以快的肯定會追上慢的,所以我們可以應用到連結串列上,用一個快指標和一個慢指標,但是細想一下發現,我們在跑操的時候相遇時座標位置不一定是整數啊(這裡相比連結串列節點而言的),而連結串列是一個節點連線起來,我們怎麼做,能讓他們在節點上相遇呢,這裡就要為2個指標找到合適的速度,使之能夠恰巧在某一結點上相遇。

原理:如果快的指標走到NULL,說明無環;而fast==slow相遇,則證明肯定存在環。

公式推導
為什麼存在環的情況下,兩個指標會相遇呢?以下推到n都是指 環長!

問題一
這裡寫圖片描述
1.假定2個指標同一個起點
我們讓兩個指標全部指向頭節點,然後給slow指標的速度為一步,而fast指標的速度為M步,則在第i次迭代的時候,slow指標走到i mod n,而fast指向Mi mod n,要想slow和fast相遇,則i mod n=Mi mod n,(M-1)i mod n,則我們可以令M=2(最小的可以取得值),i mod n = 0,則 i=n時,相遇,所以我們可以給fast 2倍的速度,這樣它們會在 最後一個節點相遇。
2.假定不在同一個起點,並且fast提前K位置
其實這個類似連結串列中含有個小環的情況,即不是所有點在環中的情況,這樣當slow即將進入環狀的時候,fast已經在環中k mod n位置了,所以問題轉化為假定不在同一個起點,並且fast提前K位置,是否會在一點相遇?
這裡寫圖片描述
fast的速度仍設定為2倍,假定第i次迭代時,slow指向i mod n,fast指向k+2i mod n,其k大於0小於你,那麼i ≡ (2i+k)(mod n) -> (i+k) mod n = 0 -> 當i=n-k時,p與q相遇。
這裡寫圖片描述
變相理解,如何同一個起點出發,他們會在整圈(也就是最後一個節點)相遇,現在fast在提前K位置出發,這樣就會使相遇點本來是最後節點,現在fast少走k步,即可與slow相遇,所以在n-K位置相遇。類似問題求倒數第K個節點:

http://blog.csdn.net/dawn_after_dark/article/details/73611115
所以不管是圖1的連結串列,還是圖2的連結串列,只要有環,快指標跟慢指標相遇,逆命題也成立;所有當快指標跟慢指標相遇,就一定存在環。
程式碼:

bool LinkList::isContainCirle() {
Node* slow = head;
Node* fast = head; //都指向頭節點
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
return !(fast==NULL || fast->next==NULL); //只要有一個為空,就說明無環
}

問題二
方法一
我們已經在上面的討論中,已經得知slow與fast會在環中n-k位置相遇,我們先靠主觀方面來探討這個問題,兩個指標同時從頭節點開始走,當慢指標即將進入環中的時候,快指標位於k mod n,說明慢指標走的這段路程也能對應k mod n, 因為快指標是慢指標速度的2倍,所以快指標在環中走的距離與慢指標走的距離一樣。而我們發現相遇點位於n-k,再走k步就可以到達環的入口,並且慢指標走的路程也能對應k mod n,所以我們再令取2指標,一個指向頭節點,另一個指向碰撞點,都以1步的速度前進,這兩個指標相遇點就是環的入口,這個結論適用於全環的連結串列,因為這時k=0,頭節點走一步就到了環的入口了。
以上只是我們主觀的理解方式,如果採用推導呢,slow走過的路程為s,環長為n,所以,2s=s+k+(m-1)n,化簡為s=k+(m-1)n,所以slow在環外相當於走了k+(m-1)n。
而碰撞點位於n-k的位置,所以要想走到環入點,則需要走k+mn步,這時你就會發現只要讓這兩個指標從頭節點與碰撞點以一步的速度過來,就一定會在環入點相遇,從而求出環入點!
程式碼:

Node* LinkList::findEnterCircle() {
Node* slow = head;
Node* fast = head;
Node* enterCircle;
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (fast == NULL || fast->next == NULL) { //如果沒有環,返回0
enterCircle = NULL;
}
else {
for (enterCircle = head;enterCircle != slow;enterCircle = enterCircle->next, slow = slow->next); //頭節點、相遇點同時起步,適用於全環情況,可以自行檢驗
return enterCircle;
}
}

方法二:
利用方法三求出的環長做,思路很簡單,思路就是既然我已經知道環長了,我完全可以使2個指標同時指向頭節點,然後令一個指標走一個環長的距離,再讓另一個指標開始走,這樣兩個指標距離始終隔一個環長,相遇的時候,就是環入口點。
思路來自這篇部落格:http://blog.csdn.net/elicococoo/article/details/51173166
程式碼:

Node* LinkList::findEnterCircle() {
Node* enterCircle = head,*q=head;
int length = getCircleLength();
for (int i = 0;i < length;i++) {
q = q->next;
}
while (enterCircle != q) {
enterCircle = enterCircle->next;
q = q->next;
}
return enterCircle;
}

方法三
該方法需要先確定是否有環,否則結果無效。思路是用2個指標,其中一個是另一個的前驅,每次都把前驅的next指向NULL,即斷開,然後把後繼的指標賦值給前驅,後繼指標繼續後移,這樣當後繼指標為空時,前驅指標指的就是環入口點。因為環的入後的點在第一次進入環的時候斷開了,所以再次迴圈到這的時候,指標停住的地方就是環的入口。

此方法破壞了原有連結串列的結構,不提倡這樣做,但是思路很好,我們在連結串列定義的時候加入輔助標記變數,達到偽斷開的目的。

問題三
求環長就比較簡單了,儲存相遇點,然後令一指標從這出發向前,記錄走過的節點數,直到指標與相遇點相遇,即可求出環長。
程式碼:

int LinkList::getCircleLength() { //環長,在連結串列中一般指的是連結串列長度
Node* slow = head;
Node* fast = head;
int circleLength;
while (fast&&fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (fast == NULL || fast->next == NULL) { //如果沒有環,返回0
circleLength = 0;
}
else{
circleLength = 1; //如果有環,首先肯定包括尾節點
for (Node* p = slow->next;slow != p;p = p->next) { //每走一步加一
circleLength++;
}
}
return circleLength;
}

問題四
總長度等於環外長度+環長度,環長度我們已求出,求環外長度,只需一個指標從頭節點開始到環入口結束就行,記錄走過的節點數目。
程式碼:

int LinkList::getCircleLinklistLength() {
int totalLength;
totalLength = getCircleLength();
Node* enterCircle = findEnterCircle();
if (enterCircle) {
for (Node* p = head;p->next != enterCircle;p = p->next) {
totalLength++;
}
}
return totalLength;
}