1. 程式人生 > >Leetcode Week3 Merge Two(k) Sorted Lists

Leetcode Week3 Merge Two(k) Sorted Lists

ron 後者 效率 好的 margin 返回 如果 很多 頭指針

Question

Q1.Merge two sorted linked lists and return it as a new list. The new list should be made by splicing together the nodes of the first two lists.

Example:

Input: 1->2->4, 1->3->4
Output: 1->1->2->3->4->4

Q2.Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

Example:

Input:
[
  1->4->5,
  1->3->4,
  2->6
]
Output: 1->1->2->3->4->4->5->6

  在解決將k個已排序(從小到大)好的鏈表合並成一個已排序(從小到大)的鏈表前,我們先從一個特例開始,即將兩個鏈表合並成一個。

Answer

1.合並兩個排列好的鏈表

  問題就是將兩個已排序的鏈表合並成一個,這是歸並排序裏的一個基本操作。

現在描述以下解決這個問題的算法,用數組來描述。

  我們輸入兩個數組,設為A,B,輸出輸出C,加入三個計數器Aptr,Bptr,Cptr,初始化為數組開頭。

  取A[Aptr]和B[Bptr]的較小值拷貝到C的下一個位置,相應的計數器加一,重復該操作,直到某個計數器指向的索引值大於數組的邊界。

  這時候代表一個數組的元素已經全部拷貝到C的相應位置了,我們將另一個數組剩下的元素拷貝到C裏。

  回到Leetccode的這個問題,從鏈表的角度,計數器和鏈表元素可以用指針來代替,輸入和輸出數組也可以通過鏈表頭指針訪問,下面通過不完整的代碼實現這個算法的思想。

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
// first_Node用來輸出鏈表, last_Node用作上面算法描述的計數器(即Cptr),也用作插入元素,代表輸出C的最後一個節點 ListNode
*first_Node = NULL, *last_Node = NULL;
// 在兩個數組都沒被拷貝到C時的操作。
while (l1 != NULL && l2 != NULL) {
// 判斷A[Aptr](與l1->val一致),B[Bptr](與l2->val一致)哪個較小,將較小的插入到鏈表C的最後,Aptr和Bptr相應加一(相當於l1,l2)
if (l1->val > l2->val) { last_Node->next = l2; last_Node = last_Node->next; l2 = l2->next; } else { last_Node->next = l1; last_Node = last_Node->next; l1 = l1->next; } }
// 將剩下的元素插入到C的最後
if (l1 == NULL && l2 != NULL) { last_Node->next = l2; } else if (l1 != NULL && l2 == NULL) { last_Node->next = l1; }
// 返回鏈表C
return first_Node; }

  上面的代碼已經很能體現代碼的具體思想了,剩下的工作就是將代碼完善,將邊界的情況考慮周全,實際呢,這又是特別麻煩的,一個不小心就會出現問題,比如說訪問空指針。我們仔細考慮就會知道,上面代碼沒有對first_Node進行初始化,所以我們需要對first_Node判斷是否為空,空就要初始化為對應的節點。那麽在哪裏判斷呢,很容易想到,我們需要在判斷A[Aptr]和B[Bptr]哪個較小後加入,然而這還沒結束,將剩下的元素插入到C的最後 後的代碼也沒有對first_Node(last_Node)是否為空作判斷,可又有對last_Node進行操作,這是十分危險的,所以我們也要在這裏加入判斷。最後完整的代碼如下:

 ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode  *first_Node = NULL, *last_Node = NULL;
        while (l1 != NULL && l2 != NULL) {
         
            if (l1->val > l2->val) {
                if (first_Node == NULL) {
                    first_Node = l2;
                    last_Node = first_Node;
                 
               } else {
                last_Node->next = l2;
                last_Node = last_Node->next;
                }
                l2 = l2->next;
            }
            else {
                if (first_Node == NULL) {
                    first_Node = l1;
                    last_Node = first_Node;
                 
               } else {
                last_Node->next = l1;
                last_Node = last_Node->next;
                }
                l1 = l1->next;
            }

        }
        if (l1 == NULL && l2 != NULL) {
             if (first_Node == NULL)
                first_Node = l2;
             else
                last_Node->next = l2;
        }
        else if (l1 != NULL && l2 == NULL) {
             if (first_Node == NULL)
                 first_Node = l1;
           else
                 last_Node->next = l1;
        }
        return first_Node;
    }

2.合並k個排列好的鏈表

  好了,有了上面的例子,我們合並k的鏈表也會有思路了,模仿上面算法的思路,我們可以每次將k個鏈表的最小值比較一遍,取最小值,將其拷貝到輸出鏈表C中,當然這是可行的,時間復雜度為O(kN),N為最後剩下鏈表的長度,想了想感覺這有點麻煩,所以我們不妨考慮另一種方法。

  按照上一題的思路,其實我們可以兩兩鏈表合並,最後合並成一個鏈表,這樣我們就可以重用上面的代碼了。兩兩合並可以是鏈表1和鏈表2合並後的鏈表 再和鏈表3合並,以此類推,也可以是鏈表1和鏈表2合並為鏈表①,鏈表3和鏈表4合並為鏈表②,依次類推,①又和②合並……,如下圖所示。聰明的你一比較就知道後者的效率會高很多,因為合並的次數明顯變少了。因此,我們根據後者編寫代碼:

技術分享圖片

  

   ListNode* mergeKLists(vector<ListNode*>& lists) {
// 遞歸終止的條件
if (lists.size() == 0) return NULL; if (lists.size() == 1) return lists[0]; // 返回鏈表頭節點指針
vector
<ListNode*> tmpVec;
// 每兩個鏈表合並
for (int i = 0; i < lists.size()/2*2; i += 2) tmpVec.push_back(mergeTwoLists(lists[i], lists[i+1])); // 如果k為奇數,將剩下的最後一個加入到tmpVec中 if (lists.size()%2 != 0) tmpVec.push_back(lists[lists.size()-1]);
// 繼續合並
return mergeKLists(tmpVec); }

  實際上,這樣的解決辦法稱為分治(divide and conquer),將一個大問題分為幾個子問題來解決,在編程裏經常用遞歸實現,上面就有所體現。

Leetcode Week3 Merge Two(k) Sorted Lists