写在前面:
今天我们来看的题目是 Merge K sorted Lists。看名字好像是一道很简单的题,但其实蕴藏的一些优秀解法值得我们一起讨论和学习。
注意,这道题目的 lists 都是烦人的 Linked List,意味着虽然这些 list 都是 sorted 的,但是他们不能像 Merge K sorted Array 一样利用 动态数组 的方便特性轻易取得结果,那我们可以怎么思考呢?
题目介绍:
题目信息:
- 题目链接:https://leetcode.com/problems/merge-k-sorted-lists/description/https://leetcode.com/problems/merge-k-sorted-lists/description/
- 题目所属类型:Array,Priority Queue
- 题目难度:Hard(但其实我个人觉得在新题里的难度最多 medium, Leetcode 越来越难了~)
- 题目来源:Google 面试高频真题
题目问题:
- 给定 K 个有序链表,将他们按序合并成一个单一链表
- k <= 10^4
- 链表长度小于 500
- 例子:
- [1-> 2 -> 3]
- [1-> 4 -> 5]
- [2-> 6]
- 需要被合并成 [1->1->2->2->3->4->5->6] 顺序无所谓
题目想法 MindSet:
从合并双链表开始:
- 参考类似的合并有序数组的想法,既然链表也是有序的,那对于两个链表来说,只需要使用两个指针进行比较,取小的那一个填入,然后将被填入的指针往前移动,再进行比较,直到其中一个 list 被使用完,合并结束
- 对于两个链表的合并可以参考 https://leetcode.com/problems/merge-two-sorted-lists/
- 我们可以举一反三,遍历 k 个链表对每组相邻的链表都做类似合并
- Runtime O(KN),K 是所有的链表总数
- 缺点:是一个可行的解决方案,但是会随着 K 的变大而有些缓慢
- 优点:不需要额外的存储空间,Space:O(1)
能否更好的利用“有序”的关系:
- 既然合并两个链表可以依靠双指针的想法,那对于 k 个链表,可不可以考虑 k 指针的方法?
- 即,在每一步,都把当前 k 个数组指针上的数字进行比较,take 最小的那个,然后将那一个指针向前移动,直到所有合并结束?
- 在每一次比较的时候我们需要遍历 K 个指针上的大小来决定极值
- Runtime 依旧是 O(KN)
在比较时做的更好?
- 我们目前对于每一个 step 上的比较是线性的,有没有一种方法可以将比较做的更好?
- Priority_Queue / Min-heap !!!!!
题目解法:
- 将所有 链表 的第一个元素存入 Minheap 中
- 循环直到 Minheap 中不再有任何元素:
- 将 Minheap 中最小元素代表的 Node 填入结果序列中
- Minheap 中删除掉这个已使用最小元素
- 如果 Node -> next 不为 nullptr,填入 Minheap 中
- Tip: 在只有 Maxheap 的语言中(如 C++),如果要使用 minheap,可以将所有元素值 * (-1)
题目代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
// set as min-heap based on val
priority_queue<pair<int, ListNode*>> compare;
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* res = new ListNode(0); //dummy head
ListNode* begin = res;
//push every first point into the min heap since it is sorted:
for(int i = 0; i < lists.size(); i++){
if(lists[i] != nullptr){
compare.push({lists[i]->val * (-1), lists[i]}); //trick to do the minheap
}
}
//for every iteration, get the top priority for the next and push it next to the list:
while(!compare.empty()){
//let the current min to be append to the list, and proceed the pointer
ListNode* curr_min = compare.top().second;
res->next = curr_min;
res = res->next;
//push the next one from its original list to the heap
compare.pop();
if(curr_min->next){
compare.push({curr_min->next->val * (-1), curr_min->next});
}
}
//one step forward cuz we set up a dummy start
return begin->next;
}
};
- Runtime:O(N*logK)
- Space: O(K) - for Minheap storage
- 我们用更大的空间优化了他的执行时间
写在后面:
其实我使用的方法并不是最优的解法(时间 + 空间),但我认为这是最好理解且也最具代表性的一种思路。他同时包括了 Minheap 的特性使用与合并有序链表的思维,比较有代表性,也能通过一道题为大家带来更多收获。
如果使用 Divide and Conquer 方法对于 one by one 合并进行优化,可以在 Runtime 做到与我的方法相同的情况下,Space 做到 O(1),因为他不需要使用额外的存储空间,感兴趣的同学可以去自行阅读解法,我认为是过于难了不如我的方法解决易懂😁,大家在 OA 面试的时候也不一定就能想的出来那么难的,反正我做不到,大牛轻喷~~
Leetcode 答案地址:https://leetcode.com/problems/merge-k-sorted-lists/editorial/