題目:
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
題意:
將k個已排好序的鏈表合並為一個非下降排序的鏈表。
思路:
將每個鏈表的表頭元素取出來,建立一個小頂堆,因為k個鏈表中都排好序了,因此每次取堆頂的元素就是k個鏈表中的最小值,可以將其合並到合並鏈表中,再將這個元素的指針指向的下一個元素也加入到堆中,再調整堆,取出堆頂,合並鏈表。。。。以此類推,直到堆為空時,鏈表合並完畢。
因為想練習建堆的過程,所以我沒有用STL里的make_heap等方法,而是自己寫的建堆函數。若想看用STL的建堆方法的,可以參考網上答案。
建堆的時間復雜度是k/2logk, 每次取出堆頂再加入元素的復雜度是logk,假設每條鏈表平均有n個元素,則一共有nk-k次。因此總的時間復雜度為O(nklogk)。
還有一種思路是取出兩條,用merge2Lists的方法合並為一條,再將這條和下一條用merge2Lists來合並為一條,以此類推。假設每條鏈表平均有n個元素,此種時間復雜度是O(2n+3n+…+kn), 為O(nk²),因此若用此法會超時。
我的代碼如下:
class Solution { public: ListNode *mergeKLists(vector<ListNode *> &lists) { // 使用堆排序, // 1. 選出每個鏈表的頭來插入小頂堆中, // 2. 再把堆頂接入合並鏈表中, // 3. 被選出的指針后移再加入小頂堆中,回到2 // 4. 最后所有鏈表都為空時,返回合並鏈表的頭指針 if(lists.empty()) return nullptr; vector<ListNode* > heap; // 1. 選出每個鏈表的頭來插入小頂堆中, for(int i = 0; i != lists.size(); i ++){ if(lists[i]) heap.push_back(lists[i]); } makeHeap(heap); // 2. 再把堆頂接入合並鏈表中, ListNode head(-1); // 合並鏈表的表頭 ListNode* p = &head; while(!heap.empty()){ auto minNode = popHeap(heap); p->next = minNode; // 接入鏈表 p = p->next; // 3. 被選出的指針后移再加入小頂堆中,回到2 auto next = minNode->next; if(next) pushHeap(heap, next); } // 4. 最后所有鏈表都為空時,返回合並鏈表的頭指針 return head.next; } // 建立小頂堆 // 自低向上 void makeHeap(vector<ListNode*> &heap){ // 從最后一個元素的父節點開始建立小頂堆 for(int i = heap.size()/2; i >0 ; i --){ minHeap(heap, i); } } // 調整小頂堆,以第i個元素為根建立小頂堆 //位置從1開始,取元素時記得-1 // 自頂向下 void minHeap(vector<ListNode*> &heap, int i){ int l = i*2; int r = l+1; int least(i); // 算出最小元素的位置 if((l< heap.size()+1) && heap[l-1]->val<heap[i-1]->val ){ // 如果沒有超過邊界並且左孩子比父親小,則換 least = l; } if(r<heap.size()+1 && heap[r-1]->val<heap[least-1]->val){ // 如果沒有超過邊界並且右孩子最小,則換 least = r; } if(least != i){ swap(heap[i-1], heap[least-1]); minHeap(heap, least); } } // 在小頂堆中插入一個元素 // 自低向上 void pushHeap(vector<ListNode*> &heap, ListNode* p){ heap.push_back(p); int child = heap.size(); int parent = child/2; for(int child = heap.size(),parent = child/2; parent; child--, parent = child/2){ if(heap[child-1]->val < heap[parent-1]->val){ swap(heap[child-1], heap[parent-1]); } } } // 彈出堆頂 ListNode* popHeap(vector<ListNode*> &heap){ swap(heap[0], heap[heap.size()-1]); auto p = heap.back(); heap.pop_back(); minHeap(heap, 1); return p; } };
優化代碼:
后來想到既然堆每次加入一個元素的時候都要調整堆頂,那么每次把要添加的元素換到堆頂再調整就不用寫pushHeap的函數了,當要添加的元素為空時,相當於執行popHeap函數,因此可以簡化代碼:
class Solution { public: ListNode *mergeKLists(vector<ListNode *> &lists) { // 使用堆排序, // 1. 選出每個鏈表的頭來插入小頂堆中, // 2. 再把堆頂接入合並鏈表中, // 3. 被選出的指針后移再加入小頂堆中,回到2 // 4. 最后所有鏈表都為空時,返回合並鏈表的頭指針 if(lists.empty()) return nullptr; vector<ListNode* > heap; // 1. 選出每個鏈表的頭來插入小頂堆中, for(int i = 0; i != lists.size(); i ++){ if(lists[i]) heap.push_back(lists[i]); } makeHeap(heap); // 2. 再把堆頂接入合並鏈表中, ListNode head(-1); // 合並鏈表的表頭 ListNode* p = &head; while(!heap.empty()){ auto minNode = heap[0]; p->next = minNode; // 接入鏈表 p = p->next; // 3. 被選出的指針后移再加入小頂堆中,回到2 auto next = minNode->next; if(next) { heap[0] = next; }else{ swap(heap[0], heap[heap.size()-1]); heap.pop_back(); } minHeap(heap, 1); } // 4. 最后所有鏈表都為空時,返回合並鏈表的頭指針 return head.next; } // 建立小頂堆 // 自低向上 void makeHeap(vector<ListNode*> &heap){ // 從最后一個元素的父節點開始建立小頂堆 for(int i = heap.size()/2; i >0 ; i --){ minHeap(heap, i); } } // 小頂堆,以第i個元素為根建立小頂堆 //位置從1開始,取元素時記得-1 // 自頂向下 void minHeap(vector<ListNode*> &heap, int i){ int l = i*2; int r = l+1; int least(i); // 算出最小元素的位置 if((l< heap.size()+1) && heap[l-1]->val<heap[i-1]->val ){ // 如果沒有超過邊界並且左孩子比父親小,則換 least = l; } if(r<heap.size()+1 && heap[r-1]->val<heap[least-1]->val){ // 如果沒有超過邊界並且右孩子最小,則換 least = r; } if(least != i){ swap(heap[i-1], heap[least-1]); minHeap(heap, least); } } };