1. 程式人生 > >演算法設計與分析(六)(上週第五週的寫錯標題,這才是真正的第六週)

演算法設計與分析(六)(上週第五週的寫錯標題,這才是真正的第六週)

Merge k Sorted Lists

題目

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個已經有序的數組合併成一個有序陣列,這些陣列是用連結串列的方式表示的。很明顯,這是歸併排序和分治演算法結合的題目。

基礎知識

  • 歸併排序 歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。 將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。一般我們接觸的歸併排序是二路歸併排序,具體的過程如下: 在這裡插入圖片描述 這個過程不斷將一個數組分割,分割到兩個或者一個元素,然後又將有序的小的陣列兩兩合併成一個有序的陣列。這個排序演算法是穩定的。

解答及演算法步驟

這裡產生了兩種基本的想法,由於最後要合成一個數組,假設有A,B,C,D,E,F,G,H六個有序的陣列

  • 第一種想法 先合併A和B為A,A和C合併結果為A,A合併D作為A,再合併E為A,再合併F為A,再合併G為A,再合併H為A,最後A即為結果。

    • 演算法步驟
      1. 新建一個連結串列儲存答案。
      2. 合併vector的前兩個連結串列,並將合併的連結串列刪除,將結果儲存回vector首部。
      3. 繼續2的操作,知道vector只有一個元素。
  • 第二種想法 這可以說是對第一種想法的改進,時間複雜度更低,先合併A和B為A1,再合併C和D為A2,再合併E和F為A3,再合併G和H為A4,再合併A1和A2為B1,再合併A3和A4為B2,最後合併B1和B2。這剛剛好是歸併排序的逆過程。

    • 演算法步驟
      1. 將vector分成兩個部分。
      2. 將這兩個部分的連結串列兩兩合併。
      3. 遞迴過2,知道只有一個元素。

原始碼

  • 做法一
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if(lists.size() == 0) {
            return NULL;
        }
        //every two merge
        while(lists.size() > 1) {
        	ListNode *temp = merge(lists[0], lists[1]);
            lists.erase(lists.begin());
        	lists.erase(lists.begin());
        	lists.insert(lists.begin(),temp);
        	
        }
        return lists[0];
    }
    ListNode* merge(ListNode *l1, ListNode *l2) {
    	if(l1 == NULL) {
    		return l2;
    	}
    	if(l2 == NULL) {
    		return l1;
    	}
    	if(l1->val < l2->val) {
    		l1->next = merge(l1->next, l2);
    		return l1;
    	} else {
    		l2->next = merge(l2->next, l1);
    		return l2;
    	}
    }
};
  • 做法二
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        //every two merge
        if(lists.empty()) {
            return NULL;
        }
        return merge(lists, 0, lists.size()-1);
    }


    ListNode* merge_two(ListNode *l1, ListNode *l2) {
    	if(l1 == NULL&&l2 == NULL) {
    		return NULL;
    	}
    	if(l1 == NULL) {
    		return l2;
    	}
    	if(l2 == NULL) {
    		return l1;
    	}
    	if(l1->val < l2->val) {
    		l1->next = merge_two(l1->next, l2);
    		return l1;
    	} else {
    		l2->next = merge_two(l2->next, l1);
    		return l2;
    	}
    }

    ListNode* merge(vector<ListNode*>& lists, int begin, int end) {
        //condition to end
        if(begin == end) {
            return lists[begin];
        }

        int middle = (begin + end)/2;

        ListNode *l1 = merge(lists, begin, middle);
        ListNode *l2 = merge(lists, middle+1, end);

        return merge_two(l1, l2);
    }
};

複雜度分析

表面看來這兩種做法的時間複雜度是一樣的,但我們執行後發現法一解決問題平均時間為216ms,而法二需要20ms,下面我們分別分析一下這兩種做法的時間複雜度。

  • 法一 法一的事件複雜度為O(k2k^2n) 第一個和第二個需要時間為O(n+n) 前面合併的2n個元素的與下一個合併時間為O(2n+n) … 最後一步的時間為O((k-1)n+n)

最後所有時間的和為O(k2k^2n)

  • 法二 法二的時間複雜度為O(nklognk) 根據大師定理 有T(k,n)=T([k2\frac{k}{2}],n)+T([k2\frac{k}{2}],n)+O(kn) 和T(1,n)=O(1) 可以推出T(k,n)=O(klogkn)