1. 程式人生 > >LeetCode 23. Merge k Sorted Lists 合併k個已排序的連結串列為一個排序連結串列

LeetCode 23. Merge k Sorted Lists 合併k個已排序的連結串列為一個排序連結串列

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

方法一: Brute Force

Intuition & Algorithm

  • Traverse all the linked lists and collect the values of the nodes into an array.
  • Sort and iterate over this array to get the proper value of nodes.
  • Create a new sorted linked list and extend it with the new nodes

Complexity Analysis

  • Time complexity : O(NlogN) where N is the total number of nodes.

    • Collecting all the values costs O(N) time.
    • A stable sorting algorithm costs O(NlogN) time.
    • Iterating for creating the linked list costs O(N) time.
  • Space complexity : O(N).

    • Sorting cost O(N) space (depends on the algorithm you choose).
    • Creating a new linked list costs O(N) space. 

方法二:Compare one by one

Algorithm

  • Compare every k nodes (head of every linked list) and get the node with the smallest value.
  • Extend the final sorted linked list with the selected nodes.

Complexity Analysis

  • Time complexity : O(kN) where k is the number of linked lists.

    • Almost every selection of node in final linked costs O(k) (k-1 times comparison).
    • There are N nodes in the final linked list.
  • Space complexity :

    • O(n) Creating a new linked list costs O(n) space.
    • O(1) It's not hard to apply in-place method - connect selected nodes instead of creating new nodes to fill the new linked list. 

  自己想的一個比較直觀的方法是,建立一個vector<bool>用來記錄lists中的相應位置的連結串列是否都已經被連結進結果連結串列中,如果是則設為true,否則為false。然後每一次while迴圈均將當前還未連結完的所有連結串列中最小的那個頭結點連結入結果連結串列中。

class Solution {
public:
	ListNode * mergeKLists(vector<ListNode*>& lists) {
		auto list_size = lists.size();
		vector<bool> record(list_size, false); //當相應連結串列還未被完全連結到結果連結串列時,其對應的值為false。否則為true
		for (int i = 0; i < list_size; ++i) {
			if (lists[i] == nullptr) {
				record[i] = true;
			}
		}
		ListNode *head = nullptr, *now = nullptr;
		while ( needContinue(record) ) {
			int List_number = FindMinNumber(lists, record);
			if (head == nullptr) {	
				head = lists.at(List_number);
				now = head;
				if (lists.at(List_number)->next == nullptr) {
					//該連結串列已被全部鏈入
					record[List_number] = true;
				}
				else {
					lists.at(List_number) = lists.at(List_number)->next;
				}
			}
			else {
				now->next = lists.at(List_number);
				now = now->next;
				if (lists.at(List_number)->next == nullptr) {
					//該連結串列已被全部鏈入
					record[List_number] = true;
				}
				else {
					lists.at(List_number) = lists.at(List_number)->next;
				}
			}
		}
		return head;
	}

	//是否還有連結串列未完全處理完
	bool needContinue(vector<bool> &record) {
		for (auto temp : record) {
			if (temp == false) {
				//還有連結串列未全部處理
				return true;
			}	
		}
		return false;
	}

	//找到lists中所有連結串列中首節點最小的那個連結串列在vector中的序號
	int FindMinNumber(vector<ListNode*>& lists,vector<bool>& record) {
		int res = -1;
		for (int i = 0; i < lists.size(); ++i) {
			if (record.at(i) == true)
				continue;
			if (res == -1) {
				res = i;
			}
			else {
				if (lists.at(i)->val < lists.at(res)->val) {
					res = i;
				}
			}
		}
		return res;
	}
};

方法三:Optimize Approach 2 by Priority Queue

Algorithm

Almost the same as the one above but optimize the comparison process by priority queue

參考:

struct compare {
    bool operator()(const ListNode* l, const ListNode* r) {
        return l->val > r->val;
        //表示式comp(a,b),如果a被認為是在函式定義的嚴格弱排序中的b之前(即b的優先順序更高),則返回true。
    }
};
ListNode *mergeKLists(vector<ListNode *> &lists) { //priority_queue
    priority_queue<ListNode *, vector<ListNode *>, compare> q;
    for(auto l : lists) {
        if(l)  q.push(l);
    }
    if(q.empty())  return NULL;

    ListNode* result = q.top();
    q.pop();
    if(result->next) q.push(result->next);
    ListNode* tail = result;            
    while(!q.empty()) {
        tail->next = q.top();
        q.pop();
        tail = tail->next;
        if(tail->next) q.push(tail->next);
    }
    return result;
}

Complexity Analysis

  • Time complexity : O(Nlogk) where k is the number of linked lists.

    • The comparison cost will be reduced to O(logk) for every pop and insertion to priority queue. But finding the node with the smallest value just costs O(1) time.
    • There are N nodes in the final linked list.
  • Space complexity :

    • O(n) Creating a new linked list costs O(n) space.
    • O(k) The code above present applies in-place method which cost O(1) space. And the priority queue (often implemented with heaps) costs O(k) space (it's far less than N in most situations). 

Difference between Priority-Queue and Heap

Concept:

1.Heap is a kind of data structure. It is a name for a particular way of storing data that makes certain operations very efficient. We can use a tree or array to describe it.

   18
  /	\
 10	 16
/ \   / \
9  5  8  12

18, 10, 16, 9, 5, 8, 12

2.Priority queue is an abstract datatype. It is a shorthand way of describing a particular interface and behavior, and says nothing about the underlying implementation.

A heap is a very good data structure to implement a priority queue. The operations which are made efficient by the heap data structure are the operations that the priority queue interface needs.

Implementation: c++

1.priority_queue: we can only get the top element (具體實現即方法三的程式碼)

2.make_heap: we can access all the elements(具體實現如下)

static bool heapComp(ListNode* a, ListNode* b) {
        return a->val > b->val;
}
ListNode* mergeKLists(vector<ListNode*>& lists) { //make_heap
    ListNode head(0);
    ListNode *curNode = &head;
    vector<ListNode*> v;   
    for(int i =0; i<lists.size(); i++){
        if(lists[i]) v.push_back(lists[i]);
    }
    make_heap(v.begin(), v.end(), heapComp); //vector -> heap data strcture

    while(v.size()>0){
        curNode->next=v.front();
        pop_heap(v.begin(), v.end(), heapComp); 
        v.pop_back(); 
        curNode = curNode->next;
        if(curNode->next) {
            v.push_back(curNode->next); 
            push_heap(v.begin(), v.end(), heapComp);
        }
    }
    return head.next;
}

方法四: Merge lists one by one

Algorithm

Convert merge k lists problem to merge 2 lists (k-1) times. Here is the merge 2 lists problem page.

ListNode *mergeKLists(vector<ListNode *> &lists) {
    if(lists.empty()){
        return nullptr;
    }
    while(lists.size() > 1){
        lists.push_back(mergeTwoLists(lists[0], lists[1]));
        lists.erase(lists.begin());
        lists.erase(lists.begin());
    }
    return lists.front();
}

/*
*第一次進入if(l1->val < l2->val) 判斷的時候的return即為最終返回的頭結點。這個函式的目的是返回
*當前兩個引數中val值較小的那個節點指標,並令這個指標指向下一個val值最小的節點。當兩個引數中有一
*個為nul時,直接將另一個引數剩餘的所有連結串列直接連線在要返回的連結串列後面。
*/
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) {
    if(l1 == nullptr){
        return l2;
    }
    if(l2 == nullptr){
        return l1;
    }
    if(l1->val <= l2->val){
        l1->next = mergeTwoLists(l1->next, l2);
        return l1;
    }
    else{
        l2->next = mergeTwoLists(l1, l2->next);
        return l2;
    }
}

Complexity Analysis

  • Time complexity : O(kN) where k is the number of linked lists.

    • We can merge two sorted linked list in O(n) time where n is the total number of nodes in two lists.
    • Sum up the merge process and we can get: 
  • Space complexity : O(1)

    • We can merge two sorted linked list in O(1) space.

方法五:分治法

Intuition & Algorithm

This approach walks alongside the one above but is improved a lot. We don't need to traverse most nodes many times repeatedly

  • Pair up k lists and merge each pair.

  • After the first pairing, k lists are merged into k/2 lists with average 2N/k length, then k/4, k/8 and so on.

  • Repeat this procedure until we get the final sorted linked list.

Thus, we'll traverse almost NN nodes per pairing and merging, and repeat this procedure about log​2​​k times.

Divide_and_Conquer

for each level, the total comparison is N, there are log K levels. so the runtime is O(N log K).
for the first level, as you said, we merge K/2 lists. but every list only has N/K length. for second level, we merge K/4 lists with 2N/K length. so on and so forth.

ListNode* mergeKLists(vector<ListNode*>& lists) {
    int k = (int)lists.size();
    if(k==0) return NULL;
    if(k==1) return lists[0];
    return doMerge(lists, 0, (int)lists.size()-1);
}


ListNode* doMerge(vector<ListNode*>& lists, int left, int right) {
    if(left==right) return lists[left];
    else if(left+1==right) return merge2Lists(lists[left], lists[right]);
    ListNode* l1 = doMerge(lists, left, (left+right)/2);
    ListNode* l2 = doMerge(lists, (left+right)/2+1, right);
    return merge2Lists(l1, l2);
}


/*
*第一次進入if(l1->val < l2->val) 判斷的時候的return即為最終返回的頭結點。這個函式的目的是返回
*當前兩個引數中val值較小的那個節點指標,並令這個指標指向下一個val值最小的節點。當兩個引數中有一
*個為nul時,直接將另一個引數剩餘的所有連結串列直接連線在要返回的連結串列後面。
*/
ListNode *merge2Lists(ListNode *l1, ListNode *l2) {
    if(l1 == nullptr){
        return l2;
    }
    if(l2 == nullptr){
        return l1;
    }
    if(l1->val <= l2->val){
        l1->next = merge2Lists(l1->next, l2);
        return l1;
    }
    else{
        l2->next = merge2Lists(l1, l2->next);
        return l2;
    }
}