題目:兩個單向鏈表,找出它們的第一個公共結點。
鏈表的結點定義為:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
分析:這是一道微軟的面試題。微軟非常喜歡與鏈表相關的題目,因此在微軟的面試題中,鏈表出現的概率相當高。
如果兩個單向鏈表有公共的結點,也就是說兩個鏈表從某一結點開始,它們的m_pNext都指向同一個結點。但由於是單向鏈表的結點,每個結點只有一個m_pNext,因此從第一個公共結點開始,之后它們所有結點都是重合的,不可能再出現分叉。所以,兩個有公共結點而部分重合的鏈表,拓撲形狀看起來像一個Y,而不可能像X。
看到這個題目,第一反應就是蠻力法:在第一鏈表上順序遍歷每個結點。每遍歷一個結點的時候,在第二個鏈表上順序遍歷每個結點。如果此時兩個鏈表上的結點是一樣的,說明此時兩個鏈表重合,於是找到了它們的公共結點。如果第一個鏈表的長度為m,第二個鏈表的長度為n,顯然,該方法的時間復雜度為O(mn)。
接 下來我們試着去尋找一個線性時間復雜度的算法。我們先把問題簡化:如何判斷兩個單向鏈表有沒有公共結點?前面已經提到,如果兩個鏈表有一個公共結點,那么 該公共結點之后的所有結點都是重合的。那么,它們的最后一個結點必然是重合的。因此,我們判斷兩個鏈表是不是有重合的部分,只要分別遍歷兩個鏈表到最后一 個結點。如果兩個尾結點是一樣的,說明它們用重合;否則兩個鏈表沒有公共的結點。
在上面的思路中,順序遍歷兩個鏈表到尾結點的時候,我們不能保證在兩個鏈表上同時到達尾結點。這是因為兩個鏈表不一定長度一樣。但如果假設一個鏈表比另一個長l個結點,我們先在長的鏈表上遍歷l個結點,之后再同步遍歷,這個時候我們就能保證同時到達最后一個結點了。由於兩個鏈表從第一個公共結點考試到鏈表的尾結點,這一部分是重合的。因此,它們肯定也是同時到達第一公共結點的。於是在遍歷中,第一個相同的結點就是第一個公共的結點。
在這個思路中,我們先要分別遍歷兩個鏈表得到它們的長度,並求出兩個長度之差。在長的鏈表上先遍歷若干次之后,再同步遍歷兩個鏈表,知道找到相同的結點,或者一直到鏈表結束。此時,如果第一個鏈表的長度為m,第二個鏈表的長度為n,該方法的時間復雜度為O(m+n)。
基於這個思路,我們不難寫出如下的代碼:
- template<typename T>
- struct ListNode
- {
- T data;
- ListNode* pNext;
- };
- template<typename T> int GetListLength(ListNode<T>* pHead)
- {
- int nLength = 0;
- while(pHead)
- {
- ++nLength;
- pHead = pHead->pNext;
- }
- return nLength;
- }
- template<typename T>
- ListNode<T>* FindFirstCommonNode(ListNode<T>* pHead1, ListNode<T>* pHead2)
- {
- int nLength1 = GetListLength(pHead1);
- int nLength2 = GetListLength(pHead2);
- // 交換兩鏈表,是1為較長的鏈表
- if(nLength1 < nLength2)
- {
- ListNode<T>* pTemp = pHead1;
- pHead1 = pHead2;
- pHead2 = pTemp;
- }
- for(int i = 0; i < nLength1 - nLength2; ++i)
- {
- pHead1 = pHead1->pNext;
- }
- while(pHead1 && pHead1 != pHead2)
- {
- pHead1 = pHead1->pNext;
- pHead2 = pHead2->pNext;
- }
- return pHead1;
- }
PS:兩個有公共結點而部分重合的鏈表,拓撲形狀看起來像一個Y,而不可能像X。即從公共節點開始之后的所有都相同!求出鏈表長度差,遍歷差數目長的鏈表,之后同步遍歷兩鏈表,第一個相同的節點即是結果。
PS:另外一種方法是把在一個鏈表尾部插入另一個鏈表,然后判斷合成的新鏈表是否有環。環入口即為第一個公共點。
上題是基於兩個鏈表無環考慮的。
1)判斷一個鏈表是否有環。
設置兩個指針(fast, slow),初始值都指向頭,slow每次前進一步,fast每次前進二步,如果鏈表存在環,則fast必定先進入環,而slow后進入環,兩個指針必定相遇。(當然,fast先行頭到尾部為NULL,則為無環鏈表)
- // 判斷鏈表是否有環
- template<typename T>
- bool HasRing(ListNode<T>* pHead)
- {
- ListNode<T>* pFast = pHead;
- ListNode<T>* pSlow = pHead;
- while(pFast && pFast->pNext)
- {
- pSlow = pSlow->pNext;
- pFast = pFast->pNext->pNext;
- if(pSlow == pFast) break;
- }
- return !pFast && !pFast->pNext;
- }
2)確定環入口:
當fast若與slow相遇時,slow肯定沒有走遍歷完鏈表,而fast已經在環內循環了n圈(1<=n)。
假設slow走了s步,則fast走了2s步(fast步數還等於s 加上在環上多轉的n圈),設環長為r,則:
2s = s + nr
s= nr
設入口環與相遇點距離為x,x<r,起點到環入口點的距離為a.
a = s - x = (n-1)r+ (r-x), 而r-x即為fast再次到環入口點時的步數
由此可知,從鏈表頭到環入口點等於(n-1)循環內環+相遇點到環入口點,於是我們從鏈表頭、與相遇點分別設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點為環入口點。
- // 判斷鏈表環入口,如果沒有環,返回NULL
- template<typename T>
- ListNode* FindRingEntrance(ListNode<T>* pHead)
- {
- ListNode<T>* pFast = pHead;
- ListNode<T>* pSlow = pHead;
- // 先找到相遇點
- while(pFast && pFast->pNext)
- {
- pSlow = pSlow->pNext;
- pFast = pFast->pNext->pNext;
- if(pSlow == pFast) break;
- }
- // 一個從起點,一個從相遇點重新遍歷,步長相同,當再次相遇時,就是環入口
- pSlow = pHead;
- while(pFast && pFast != pSlow)
- {
- pSlow = pSlow->pNext;
- pFast = pFast->pNext;
- }
- return pFast;
- }
3)如過兩個鏈表可能有環,如何判斷兩個鏈表是否相交?以及找到兩個鏈表的第一個公共點?
如果兩個有環的鏈表相交,那么它們的環必定為公共環。如果交點不在環上,第一個公共點就是第一個交點。但是如果交點在環上,環的入口點不同。那么就以任一環的入口點為第一公共點。
有可能兩個鏈表均有環,但是它們並不相交。
算法:
找到兩個鏈表的環入口。如果有一個為NULL,一個非NULL,一定無交點,返回NULL。
如果均為NULL,變成兩個無環鏈表求交點。
如果均非NULL,則是判斷兩個有環的鏈表是否有交點:如果環入口相同,則它們實在分支上相交。否則判斷是否能從一個環中找到另一個的入口節點。
- // 通用的鏈表求第一公共交點的函數
- template<typename T>
- ListNode<T>* FindFirstCommonNode_2(ListNode<T>* pHead1, ListNode<T>* pHead2)
- {
- ListNode<T>* pEntrance1 = FindRingEntrance(pHead1);
- ListNode<T>* pEntrance2 = FindRingEntrance(pHead2);
- ListNode<T>* pResult = NULL;
- if(pEntrance1 == pEntrance2)
- {
- ListNode<T>* pTemp1 = NULL, *pTemp2 = NULL;
- if(pEntrance1)
- {
- // 環入口點已經相交,尋找第一交點。
- pTemp1 = pEntrance1->pNext;
- pTemp2 = pEntrance2->pNext;
- pEntrance1->pNext = NULL;
- pEntrance2->pNext = NULL;
- pResult = FindFirstCommonNode(pHead1, pHead2);
- pEntrance1->pNext = pTemp1;
- pEntrance2->pNext = pTemp2;
- }
- else
- {
- // 無環單鏈表求第一交點
- pResult = FindFirstCommonNode(pHead1, pHead2);
- }
- return pResult;
- }
- else if(pEntrance1 == NULL || pEntrance2 == NULL)
- {
- // 一有環,另一個無環,無交點
- return NULL;
- }
- else
- {
- // 入口點不同的兩環求交點
- pResult = pEntrance1->pNext;
- while(pResult != pEntrance2 && pResult != pEntrance1)
- {
- pResult = pResult->pNext;
- }
- return pResult == pEntrance2 ? pResult : NULL;
- }
- }
參考鏈接:https://blog.csdn.net/wcyoot/article/details/6426436