1. 程式人生 > >程式設計師程式設計藝術-----第九章-----閒話連結串列追趕問題

程式設計師程式設計藝術-----第九章-----閒話連結串列追趕問題


前奏
    有這樣一個問題:在一條左右水平放置的直線軌道上任選兩個點,放置兩個機器人,請用如下指令系統為機器人設計控制程式,使這兩個機器人能夠在直線軌道上相遇。(注意兩個機器人用你寫的同一個程式來控制)。
    指令系統:只包含4條指令,向左、向右、條件判定、無條件跳轉。其中向左(右)指令每次能控制機器人向左(右)移動一步;條件判定指令能對機器人所在的位置進行條件測試,測試結果是如果對方機器人曾經到過這裡就返回true,否則返回false;無條件跳轉,類似彙編裡面的跳轉,可以跳轉到任何地方。

    ok,這道很有意思的趣味題是去年微軟工程院的題,文末將給出解答(如果急切想知道此問題的答案,可以直接跳到本文第三節)。同時,我們看到其實這個題是一個典型的追趕問題,那麼追趕問題在哪種面試題中比較常見?對了,連結串列追趕。本章就來闡述這個問題。有不正之處,望不吝指正。


第一節、求連結串列倒數第k個結點
第13題、題目描述:
輸入一個單向連結串列,輸出該連結串列中倒數第k個結點,
連結串列的倒數第0個結點為連結串列的尾指標。

分析:此題一出,相信,稍微有點 經驗的同志,都會說到:設定兩個指標p1,p2,首先p1和p2都指向head,然後p2向前走k步,這樣p1和p2之間就間隔k個節點,最後p1和p2同時向前移動,直至p2走到連結串列末尾。

    前幾日有朋友提醒我說,讓我講一下此種求連結串列倒數第k個結點的問題。我想,這種問題,有點經驗的人恐怕都已瞭解過,無非是利用兩個指標一前一後逐步前移。但他提醒我說,如果參加面試的人沒有這個意識,它怎麼也想不到那裡去。

    那在平時準備面試的過程中如何加強這一方面的意識呢?我想,除了平時遇到一道面試題,儘可能用多種思路解決,以延伸自己的視野之外,便是平時有意注意觀察生活。因為,相信,你很容易瞭解到,其實這種連結串列追趕的問題來源於生活中長跑比賽,如果平時注意多多思考,多多積累,多多發現並體味生活,相信也會對面試有所幫助。

    ok,扯多了,下面給出這個題目的主體程式碼,如下:

struct ListNode
{
    char data;
    ListNode* next;
};
ListNode* head,*p,*q;
ListNode *pone,*ptwo;

//@heyaming, 第一節,求連結串列倒數第k個結點應該考慮k大於連結串列長度的case。
ListNode* fun(ListNode *head,int k)
{
 assert(k >= 0);
 pone = ptwo = head;
 for( ; k > 0 && ptwo != NULL; k--)
  ptwo=ptwo->next;
 if (k > 0) return NULL;
 
 while(ptwo!=NULL)
 {
  pone=pone->next;
  ptwo=ptwo->next;
 }
 return pone;

擴充套件:
這是針對連結串列單項鍊表查詢其中倒數第k個結點。試問,如果連結串列是雙向的,且可能存在環呢?請看第二節、程式設計判斷兩個連結串列是否相交。


第二節、程式設計判斷兩個連結串列是否相交
題目描述:給出兩個單向連結串列的頭指標(如下圖所示)

比如h1、h2,判斷這兩個連結串列是否相交。這裡為了簡化問題,我們假設兩個連結串列均不帶環。

分析:這是來自程式設計之美上的微軟亞院的一道面試題目。請跟著我的思路步步深入(部分文字引自程式設計之美):

  1. 直接迴圈判斷第一個連結串列的每個節點是否在第二個連結串列中。但,這種方法的時間複雜度為O(Length(h1) * Length(h2))。顯然,我們得找到一種更為有效的方法,至少不能是O(N^2)的複雜度。
  2. 針對第一個連結串列直接構造hash表,然後查詢hash表,判斷第二個連結串列的每個結點是否在hash表出現,如果所有的第二個連結串列的結點都能在hash表中找到,即說明第二個連結串列與第一個連結串列有相同的結點。時間複雜度為為線性:O(Length(h1) + Length(h2)),同時為了儲存第一個連結串列的所有節點,空間複雜度為O(Length(h1))。是否還有更好的方法呢,既能夠以線性時間複雜度解決問題,又能減少儲存空間?
  3. 進一步考慮“如果兩個沒有環的連結串列相交於某一節點,那麼在這個節點之後的所有節點都是兩個連結串列共有的”這個特點,我們可以知道,如果它們相交,則最後一個節點一定是共有的。而我們很容易能得到連結串列的最後一個節點,所以這成了我們簡化解法的一個主要突破口。那麼,我們只要判斷倆個連結串列的尾指標是否相等。相等,則連結串列相交;否則,連結串列不相交。
    所以,先遍歷第一個連結串列,記住最後一個節點。然後遍歷第二個連結串列,到最後一個節點時和第一個連結串列的最後一個節點做比較,如果相同,則相交,否則,不相交。這樣我們就得到了一個時間複雜度,它為O((Length(h1) + Length(h2)),而且只用了一個額外的指標來儲存最後一個節點。這個方法時間複雜度為線性O(N),空間複雜度為O(1),顯然比解法三更勝一籌。
  4. 上面的問題都是針對連結串列無環的,那麼如果現在,連結串列是有環的呢?還能找到最後一個結點進行判斷麼?上面的方法還同樣有效麼?顯然,這個問題的本質已經轉化為判斷連結串列是否有環。那麼,如何來判斷連結串列是否有環呢?

總結:
所以,事實上,這個判斷兩個連結串列是否相交的問題就轉化成了:
1.先判斷帶不帶環
2.如果都不帶環,就判斷尾節點是否相等
3.如果都帶環,判斷一連結串列上倆指標相遇的那個節點,在不在另一條連結串列上。
如果在,則相交,如果不在,則不相交。

    1、那麼,如何編寫程式碼來判斷連結串列是否有環呢?因為很多的時候,你給出了問題的思路後,面試官可能還要追加你的程式碼,ok,如下(設定兩個指標(p1, p2),初始值都指向頭,p1每次前進一步,p2每次前進二步,如果連結串列存在環,則p2先進入環,p1後進入環,兩個指標在環中走動,必定相遇):

  1. //[email protected] KurtWang  
  2. //July、2011.05.27。  
  3. struct Node  
  4. {  
  5.     int value;  
  6.     Node * next;  
  7. };  
  8. //1.先判斷帶不帶環  
  9. //判斷是否有環,返回bool,如果有環,返回環裡的節點  
  10. //思路:用兩個指標,一個指標步長為1,一個指標步長為2,判斷連結串列是否有環  
  11. bool isCircle(Node * head, Node *& circleNode, Node *& lastNode)  
  12. {  
  13.     Node * fast = head->next;  
  14.     Node * slow = head;  
  15.     while(fast != slow && fast && slow)  
  16.     {  
  17.         if(fast->next != NULL)  
  18.             fast = fast->next;  
  19.         if(fast->next == NULL)  
  20.             lastNode = fast;  
  21.         if(slow->next == NULL)  
  22.             lastNode = slow;  
  23.         fast = fast->next;  
  24.         slow = slow->next;  
  25.     }  
  26.     if(fast == slow && fast && slow)  
  27.     {  
  28.         circleNode = fast;  
  29.         return true;  
  30.     }  
  31.     else  
  32.         return false;  
  33. }  

    2&3、如果都不帶環,就判斷尾節點是否相等,如果都帶環,判斷一連結串列上倆指標相遇的那個節點,在不在另一條連結串列上。下面是綜合解決這個問題的程式碼:

  1. //判斷帶環不帶環時連結串列是否相交  
  2. //2.如果都不帶環,就判斷尾節點是否相等  
  3. //3.如果都帶環,判斷一連結串列上倆指標相遇的那個節點,在不在另一條連結串列上。  
  4. bool detect(Node * head1, Node * head2)  
  5. {  
  6.     Node * circleNode1;  
  7.     Node * circleNode2;  
  8.     Node * lastNode1;  
  9.     Node * lastNode2;  
  10.     bool isCircle1 = isCircle(head1,circleNode1, lastNode1);  
  11.     bool isCircle2 = isCircle(head2,circleNode2, lastNode2);  
  12.     //一個有環,一個無環  
  13.     if(isCircle1 != isCircle2)  
  14.         return false;  
  15.     //兩個都無環,判斷最後一個節點是否相等  
  16.     else if(!isCircle1 && !isCircle2)  
  17.     {  
  18.         return lastNode1 == lastNode2;  
  19.     }  
  20.     //兩個都有環,判斷環裡的節點是否能到達另一個連結串列環裡的節點  
  21.     else  
  22.     {  
  23.         Node * temp = circleNode1->next;  //updated,多謝蒼狼 and hyy。  
  24.         while(temp != circleNode1)    
  25.         {  
  26.             if(temp == circleNode2)  
  27.                 return true;  
  28.             temp = temp->next;  
  29.         }  
  30.         return false;  
  31.     }  
  32.     return false;  
  33. }  

擴充套件2:求兩個連結串列相交的第一個節點
思路:在判斷是否相交的過程中要分別遍歷兩個連結串列,同時記錄下各自長度。

    @Joshua:這個演算法需要處理一種特殊情況,即:其中一個連結串列的頭結點在另一個連結串列的環中,且不是環入口結點。這種情況有兩種意思:1)如果其中一個連結串列是迴圈連結串列,則另一個連結串列必為迴圈連結串列,即兩個連結串列重合但頭結點不同;2)如果其中一個連結串列存在環(除去迴圈連結串列這種情況),則另一個連結串列必在此環中與此環重合,其頭結點為環中的一個結點,但不是入口結點。在這種情況下我們約定,如果連結串列B的頭結點在連結串列A的環中,且不是環入口結點,那麼連結串列B的頭結點即作為A和B的第一個相交結點;如果A和B重合(定義方法時形參A在B之前),則取B的頭結點作為A和B的第一個相交結點。 

    @風過無痕:讀《程式設計師程式設計藝術》,補充程式碼2012年7月18日 週三下午10:15
    發件人: "風過無痕" <[email protected]>將發件人新增到聯絡人
    收件人: "zhoulei0907" <[email protected]>
你好
    看到你在csdn上部落格,學習了很多,看到下面一章,有個擴充套件問題沒有程式碼,發現自己有個,發給你吧,思路和別人提出來的一樣,感覺有程式碼更加完善一些,呵呵

擴充套件2:求兩個連結串列相交的第一個節點
    思路:如果兩個尾結點是一樣的,說明它們有重合;否則兩個連結串列沒有公共的結點。
    在上面的思路中,順序遍歷兩個連結串列到尾結點的時候,我們不能保證在兩個連結串列上同時到達尾結點。這是因為兩個連結串列不一定長度一樣。但如果假設一個連結串列比另一個長L個結點,我們先在長的連結串列上遍歷L個結點,之後再同步遍歷,這個時候我們就能保證同時到達最後一個結點了。由於兩個連結串列從第一個公共結點開始到連結串列的尾結點,這一部分是重合的。因此,它們肯定也是同時到達第一公共結點的。於是在遍歷中,第一個相同的結點就是第一個公共的結點。
    在這個思路中,我們先要分別遍歷兩個連結串列得到它們的長度,並求出兩個長度之差。在長的連結串列上先遍歷若干次之後,再同步遍歷兩個連結串列,直到找到相同的結點,或者一直到連結串列結束。PS:沒有處理一種特殊情況:就是一個是迴圈連結串列,而另一個也是,只是頭結點所在位置不一樣。 

    程式碼如下:

  1. ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2)  
  2. {  
  3.       // Get the length of two lists  
  4.       unsigned int nLength1 = ListLength(pHead1);  
  5.       unsigned int nLength2 = ListLength(pHead2);  
  6.       int nLengthDif = nLength1 - nLength2;  
  7.       // Get the longer list  
  8.       ListNode *pListHeadLong = pHead1;  
  9.       ListNode *pListHeadShort = pHead2;  
  10.       if(nLength2 > nLength1)  
  11.       {  
  12.             pListHeadLong = pHead2;  
  13.             pListHeadShort = pHead1;  
  14.             nLengthDif = nLength2 - nLength1;  
  15.       }  
  16.       // Move on the longer list  
  17.       for(int i = 0; i < nLengthDif; ++ i)  
  18.             pListHeadLong = pListHeadLong->m_pNext;  
  19.       // Move on both lists  
  20.       while((pListHeadLong != NULL) && (pListHeadShort != NULL) && (pListHeadLong != pListHeadShort))  
  21.       {  
  22.             pListHeadLong = pListHeadLong->m_pNext;  
  23.             pListHeadShort = pListHeadShort->m_pNext;  
  24.       }  
  25.       // Get the first common node in two lists  
  26.       ListNode *pFisrtCommonNode = NULL;  
  27.       if(pListHeadLong == pListHeadShort)  
  28.             pFisrtCommonNode = pListHeadLong;  
  29.       return pFisrtCommonNode;  
  30. }  
  31. unsigned int ListLength(ListNode* pHead)  
  32. {  
  33.       unsigned int nLength = 0;  
  34.       ListNode* pNode = pHead;  
  35.       while(pNode != NULL)  
  36.       {  
  37.             ++ nLength;  
  38.             pNode = pNode->m_pNext;  
  39.       }  
  40.       return nLength;  
  41. }  

第三節、微軟工程院面試智力題
題目描述:
    在一條左右水平放置的直線軌道上任選兩個點,放置兩個機器人,請用如下指令系統為機器人設計控制程式,使這兩個機器人能夠在直線軌道上相遇。(注意兩個機器人用你寫的同一個程式來控制)
    指令系統:只包含4條指令,向左、向右、條件判定、無條件跳轉。其中向左(右)指令每次能控制機器人向左(右)移動一步;條件判定指令能對機器人所在的位置進行條件測試,測試結果是如果對方機器人曾經到過這裡就返回true,否則返回false;無條件跳轉,類似彙編裡面的跳轉,可以跳轉到任何地方。

分析:我儘量以最清晰的方式來說明這個問題(大部分內容來自ivan,big等人的討論):
      1、首先題目要求很簡單,就是要你想辦法讓A最終能趕上B,A在後,B在前,都向右移動,如果它們的速度永遠一致,那A是永遠無法追趕上B的。但題目給出了一個條件判斷指令,即如果A或B某個機器人向前移動時,若是某個機器人經過的點是第二個機器人曾經經過的點,那麼程式返回true。對的,就是抓住這一點,A到達曾經B經過的點後,發現此後的路是B此前經過的,那麼A開始提速兩倍,B一直保持原來的一倍速度不變,那樣的話,A勢必會在|AB|/move_right個單位時間內,追上B。ok,簡單虛擬碼如下:

start:
if(at the position other robots have not reached)
    move_right
if(at the position other robots have reached)
    move_right
    move_right
goto start

再簡單解釋下上面的虛擬碼(@big):
A------------B
|                  |
在A到達B點前,兩者都只有第一條if為真,即以相同的速度向右移動,在A到達B後,A只滿足第二個if,即以兩倍的速度向右移動,B依然只滿足第一個if,則速度保持不變,經過|AB|/move_right個單位時間,A就可以追上B。

     2、有個細節又出現了,正如ivan所說,

if(at the position other robots have reached)
    move_right
    move_right

上面這個分支不一定能提速的。why?因為如果if條件花的時間很少,而move指令發的時間很大(實際很可能是這樣),那麼兩個機器人的速度還是基本是一樣的。

那作如何修改呢?:

start:
if(at the position other robots have not reached)
    move_right
    move_left
    move_right
if(at the position other robots have reached)
    move_right
goto start

-------

這樣改後,A的速度應該比B快了。

      3、然要是說每個指令處理速度都很快,AB豈不是一直以相同的速度右移了?那到底該作何修改呢?請看:

go_step()
{
   向右
   向左
   向右
}
--------
三個時間單位才向右一步

go_2step()
{
   向右
}
------

    一個時間單向右一步向左和向右花的時間是同樣的,並且會佔用一定時間。 如果條件判定指令時間比移令花的時間較少的話,應該上面兩種步法,後者比前者快。至此,咱們的問題已經得到解決。