鏈表是否存在環及環入口點、兩個鏈表是否相交、相交鏈表的第一個公共結點
1.求鏈表倒數第k個結點
題目描述:
輸入一個單向鏈表,輸出該鏈表中倒數第k個結點,鏈表的倒數第0個結點爲鏈表的尾指針。
分析:設置兩個指針p1,p2,首先p1和p2都指向鏈表頭結點,然後p2向前走k步,這樣p1和p2之間就間隔k個節點,最後p1和p2同時向前移動,直至p2走到鏈表末尾。需要注意的是,要考慮可能的非法輸入參數,也就是說要做參數檢查,防止程序出現異常。
struct ListNode
{
int value;
ListNode* next;
};
ListNode* leastKNode(ListNode *pHead, intk)
{
if(head == NULL|| k < 0)
{
throw std::new exeception("Invalid parameters!");
}
ListNode* pFirst =pHead;
ListNode* pSec = pHead;
for(; k>0 &&pSec != NULL; --k)
pSec = pSec->next();
//鏈表長度小於k
if (k > 0)
returnNULL;
//pSec走到鏈表尾時,pFirst指向倒數第k個結點
while(pSec != NULL)
{
pFirst =pFirst->next;
pSec =pSec->next;
}
return pFirst;
}
2.判斷兩個鏈表是否相交
題目描述:給出兩個單鏈表的頭指針(如下圖所示)h1和h2,判斷這兩個鏈表是否相交。(爲了簡化問題,假設兩個鏈表均不帶環)
分析:這是來自編程之美上的微軟亞研院的一道面試題目,其思路如下:
1>. 最直接的方法:
循環判斷第一個鏈表的每個節點是否在第二個鏈表中。這種方法的時間複雜度爲O(Length(h1) * Length(h2))。
2>. hash遍歷:
針對第一個鏈表構造hash表,然後判斷第二個鏈表的每個結點是否在出現在該hash表中,如果第二個鏈表的所有結點都能在hash表中找到,即說明第二個鏈表與第一個鏈表有相同的結點。時間複雜度爲:O(Length(h1) + Length(h2));同時爲了存儲第一個鏈表所有節點,空間複雜度爲O(Length(h1))。
3>. 如果兩個沒有環的鏈表相交於某一節點,那麼在這個節點之後的所有節點都將重合。如果兩鏈表相交,則最後一個節點一定重合。很容易能得到鏈表的最後一個節點,所以一種簡潔的方法是隻要判斷兩個鏈表的尾指針是否相等。相等,則鏈表相交;否則,鏈表不相交。這種方法的時間複雜度爲O((Length(h1) + Length(h2)),空間複雜度爲O(1)。
上面的問題前提假設鏈表無環,如果鏈表是有環呢?
在沒有前提假設的情況下,判斷兩個鏈表是否相交的問題主要步驟爲:
判斷帶不帶環
如果都不帶環,就判斷尾節點是否相等
如果都帶環,判斷一鏈表上倆指針相遇的那個節點,在不在另一條鏈表上。在,則相交;不在,則不相交。
如果一個帶環一個不帶環,則肯定不相交。
3.判斷鏈表是否帶環
同樣設置兩個指針p1和p2,初始時都指向鏈表頭。p1每次前進一步,p2每次前進二步,如果鏈表存在環,則p2先進入環,p1後進入環,兩個指針在環中走動,必定相遇。
//判斷鏈表是否有環,如果有環,返回環裏的節點
bool hasCircle(ListNode * pHead,ListNode *& pCircleNode, ListNode *& pLastNode)
{
ListNode* pFast =pHead->next;
ListNode* pSlow =pHead;
while(pFast !=pSlow && pFast != NULL && pSlow != NULL)
{
if(pFast->next != NULL)
pFast = pFast->next;
else
pLastNode = pFast;
if(pSlow->next == NULL)
pLastNode = pSlow;
pFast =pFast->next;
pSlow =pSlow->next;
}
if(pFast ==pSlow && pFast != NULL && pSlow != NULL)
{
pCircleNode = pFast;
return true;
}
else
return false;
}
單鏈表有環,則怎麼找到環入口點呢?
當快慢指針相遇時,慢指針肯定沒有遍歷完鏈表,而快指針可能已經在環內循環了n圈(1<=n)。假設慢指針走了s步,則快指針走了2s步,同時快指針步數等於s 加上在環上多轉的n圈,設環長爲r,則:
2s = s + nr s = nr
設整個鏈表長L,入口環與相遇點距離爲x,起點到環入口點的距離爲a。
a + x = nra + x = (n – 1)r +r = (n-1)r + L – a a = (n-1)r + (L – a – x)
(L – a – x)爲相遇點到環入口點的距離,由此可知,從鏈表頭到環入口點等於(n-1)循環內環+相遇點到環入口點,於是從鏈表頭、與相遇點分別設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點爲環入口點。
ListNode* findLoopEntrance(ListNode*pHead)
{
if (pHead == NULL)
throw std::new exception("Invalid Parameter!");
ListNode* pSlow = pHead;
ListNode*pFast = pHead;
while (pFast != NULL && pFast->next !=NULL)
{
pSlow =pSlow->next;
pFast =pFast->next->next;
if (pSlow== pFast )
break ;
}
if (pFast== NULL || pFast->next == NULL)
return NULL;
pSlow = pHead;
while (pSlow!= pFast)
{
pSlow = pSlow->next;
pFast = pFast->next;
}
return pSlow;
}
從網是找到的一種易於理解的解釋:
一種O(n)的辦法就是(兩個指針,一個每次遞增一步,一個每次遞增兩步,如果有環的話兩者必然重合,反之亦然):
關於這個解法最形象的比喻就是在操場當中跑步,速度快的會把速度慢的扣圈
可以證明,p2追趕上p1的時候,p1一定還沒有走完一遍環路,p2也不會跨越p1多圈才追上。我們可以從p2和p1的位置差距來證明,p2一定會趕上p1但是不會跳過p1的。因爲p2每次走2步,而p1走一步,所以他們之間的差距是一步一步的縮小,4,3,2,1,0 到0的時候就重合了。根據這個方式,可以證明,p2每次走三步以上,並不總能加快檢測的速度,反而有可能判別不出有環。既然能夠判斷出是否是有環路,那改如何找到這個環路的入口?
解法如下:當p2按照每次2步,p1每次一步的方式走,發現p2和p1重合,確定了單向鏈表有環路了。接下來,讓p2回到鏈表的頭部,重新走,每次步長不是走2了,而是走1,那麼當p1和p2再次相遇的時候,就是環路的入口了。
證明:
在p2和p1第一次相遇的時候,假定p1走了n步驟,環路的入口是在p步的時候經過的,那麼有
p1走的路徑: p+c = n; c爲p1和p2相交點距離環路入口的距離
p2走的路徑: p+c+k*L = 2*n; L爲環路的周長,k是整數
n+K*L = 2*n ==> n=K*L
顯然,如果從p+c點開始,p1再走n步驟的話,還可以回到p+c這個點。
同時p2從頭開始走的話,經過n步,也會達到p+c這點。
顯然在這個步驟當中p1和p2只有前p步驟走的路徑不同,所以當p1和p2再次重合的時候,必然是在鏈表的環路入口點上。
綜合上面的2、3兩部分,判斷兩個鏈表是否相交:
//如果都不帶環,就判斷尾節點是否相等
//如果都帶環,判斷一鏈表上兩指針相遇的節點是否出現在另一條鏈表上
bool isListIntersect(ListNode* pHead1, ListNode* pHead2)
{
ListNode* pCircleNode1;
ListNode* pCircleNode2;
ListNode* pLastNode1;
ListNode* pLastNode2;
bool isCircle1 = hasCircle(pHead1,pCircleNode1, pLastNode1);
bool isCircle2 = hasCircle(pHead2,pCircleNode2, pLastNode2);
//一個有環,一個無環
if(isCircle1 != isCircle2)
return false;
//兩個都無環,判斷最後一個節點是否相等
else if(!isCircle1 && !isCircle2)
return pLastNode1 == pLastNode2;
//兩個都有環,判斷環裏的節點是否能到達另一個鏈表環裏的節點
else
{
ListNode * temp = pCircleNode1->next;
while(temp != pCircleNode1){
if(temp == pCircleNode2)
return true;
temp = temp->next;
}
return false;
}
return false;
}
4. 兩個鏈表相交的第一個節點
分析:
如果兩個尾結點是一樣的,說明它們有重合;否則兩個鏈表沒有公共的結點。
當兩個鏈表長度不一樣時,假設一個鏈表比另一個長L個結點,先在長的鏈表上遍歷L個結點,之後再同步遍歷兩個鏈表。這樣就能保證同時到達最後一個結點了。由於兩個鏈表從第一個公共結點開始到鏈表的尾結點之間的所有結點都是重合的。因此,它們肯定也是同時到達第一公共結點的。於是在遍歷中,第一個相同的結點就是第一個公共的結點。分別遍歷兩個鏈表得到它們的長度,並求出兩個長度之差。在長的鏈表上先遍歷若干次之後,再同步遍歷兩個鏈表,直到找到相同的結點或其中一個鏈表結束。(這裏沒有考慮循環鏈表的情況)
ListNode* findFirstCommonNode(ListNode*pHead1, ListNode* pHead2)
{
//獲取兩個鏈表的長度
unsigned int nLength1 = ListLength(pHead1);
unsigned int nLength2 = ListLength(pHead2);
int nLengthDif = nLength1 - nLength2;
ListNode *pListHeadLong = pHead1;
ListNode *pListHeadShort = pHead2;
if(nLength2 > nLength1)
{
pListHeadLong = pHead2;
pListHeadShort = pHead1;
nLengthDif = nLength2 - nLength1;
}
//在長的鏈表上先遍歷nLengthDif步
for(int i = 0; i < nLengthDif; ++ i)
pListHeadLong = pListHeadLong->m_pNext;
//同步遍歷兩個鏈表
while((pListHeadLong != NULL)
&& (pListHeadShort != NULL)
&& (pListHeadLong != pListHeadShort))
{
pListHeadLong = pListHeadLong->m_pNext;
pListHeadShort = pListHeadShort->m_pNext;
}
//找到第一個公共結點
ListNode *pFisrtCommonNode = NULL;
if(pListHeadLong == pListHeadShort)
pFisrtCommonNode = pListHeadLong;
return pFisrtCommonNode;
}
unsigned int ListLength(ListNode* pHead)
{
unsigned int nLength = 0;
ListNode* pNode = pHead;
while(pNode != NULL)
{
++ nLength;
pNode = pNode->m_pNext;
}
return nLength;
}