合并K个排序链表是我在刷LeetCode的时候遇到的题目,描述大致如下:
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
一开始的思路是类似于合并两个有序链表的思路,就是每次在这k个链表中取出一个最小的节点,以此类推,不过这里还存在一个问题,那就是对于两个链表来说,只需要一个简单的判断就可以得到它们之中的最值了,而对于k个链表则不行,所以,我们还需要一个类似于缓冲区一样的东西。步骤大致如下:
- 从这k个链表中遍历取出第一个不为nullptr的节点并放入缓冲区中
- 从缓冲区中拿出一个最小值,取名为min
- 判断min->next是否指向了nullptr,如果不为空的话,则以min->next节点代替之前的min节点。
重复2-3步骤直至缓冲区为空即可。
根据描述可以推断出此法可行,不过需要对缓冲区进行一定的优化,因为每次取的都是最值,所以我们可以对这个缓冲区进行排序。
1.插入排序
我第一个想到的就是插入排序,原因无它,简单。
/*倒序*/
void insertOne(vector<ListNode*>& list, ListNode* node) {
if (node == nullptr)
return;
auto it = list.begin();
//找到合适的插入位置
for (; it != list.end(); it++)
{
auto temp = *it;
if (node->val > temp->val)
break;
}
//插入
list.insert(it, node);
}
insertOne就是不完整插入排序,该函数需要保证list在这之前已经完全有序。
这里采用的是倒序,即最小值在最后的位置,这样的好处就是在取出来的时候不需要把所有的元素都向前移位。
ListNode* mergeKLists(vector<ListNode*>& lists) {
//队列
vector<ListNode*> queue;
//保存结果
ListNode* result = new ListNode(0);
ListNode* p = result;
//目前需要保证不是空数组 插入头
for (int i = 0; i < lists.size(); i++)
insertOne(queue, lists[i]);
while (!queue.empty()) {
//从队列中取出最小值
auto min = queue.back();
p->next = min;
p = p->next;
queue.pop_back();
//判断是否存在后续
if (min->next != nullptr)
insertOne(queue, min->next);
}
return result->next;
}
mergeKList就是合并这k个链表的函数。
result指针保存着插入的结果,它用到了dummy head(头节点),使用它可以避免对特殊情况进行判断。
queue数组则是之前谈到的缓冲区。
//从队列中取出最小值
auto min = queue.back();
p->next = min;
p = p->next;
queue.pop_back();
从缓冲区queue中取出最小值,然后把这个值链到了result头节点链表之中。
//判断是否存在后续
if (min->next != nullptr)
insertOne(queue, min->next);
由于在一开始存放的是各个非空链表的头指针,所以可以凭借着它的next的值可以遍历这个链表。
假设k=3.数据为{{1, 4, 5, }, {1, 3, 3}, {2, 6}}。
按照之前的思路,缓冲区如下:
此时我们取出的最小值min则是{1, 4, 5}链表中的值为1的节点,接着我们判断该节点的next不为空,则去除掉min节点。
接着把{4,5}插入到缓冲区中。
这时弹出的则是缓冲区最右边的值为1的节点,以此类推,直至缓冲区为空为止。
接着在leetcode中运行代码,结果如下:
嗯,运行速度比较慢。
需要注意的是,上述的缓冲区可以使用链表,但是注意不要覆盖掉ListNode的next属性,否则会破坏原有的链表结构。可以使用c++提供的单向链表slist 或双向链表list,也可以自己封装。运行速度应该会有所提升。
2.最小堆
既然可以使用排序算法,那么堆排序也值得一试。
void makeHeap(vector<ListNode*>& list, int len) {
int i;
for (i = len / 2; i > 0; i--)
heapAdjustDown(list, i, len);
}
把list数组调整成最小堆。
/*向下调整*/
void heapAdjustDown(vector<ListNode*>& list, int s, int length) {
//暂存
ListNode* temp = list[s];
int j = 0;
for (j = 2 * s; j <= length; j *= 2) {
//s节点的子孩子中取较小的那个
if (j < length && list[j]->val > list[j + 1]->val)
j++;
//root的值要小于子节点,则不必调整
if (temp->val <= list[j]->val)
break;
list[s] = list[j];
s = j;
}
//回写
list[s] = temp;
}
当栈顶元素被替换后,堆被破坏,将栈顶元素向下调整使其继续保持最小堆的性质,再输出栈顶元素。
而向上调整则是在将新节点放在了堆的末端,再执行的向上调整,由于本次的替换局限于堆顶,所以用不到向上调整的函数。
ListNode* mergeKLists(vector<ListNode*>& lists) {
//堆
vector<ListNode*> heap;
//保存结果 引入了头节点
ListNode* head = new ListNode(0);
ListNode* p = head;
//插入空指针,以满足完全二叉树的特性
heap.push_back(nullptr);
for (int i = 0; i < lists.size(); i++) {
if (lists[i] != nullptr)
heap.push_back(lists[i]);
}
//调整为最小堆
makeHeap(heap, heap.size() - 1);
while (heap.size() > 1) {
//从最小堆中取出值
auto min = heap[1];
p->next = min;
p = p->next;
//判断是否存在后续
if (min->next != nullptr)
heap[1] = min->next;
//把最后一个元素放到首部
else
{
heap[1] = heap.back();
heap.pop_back();
}
if (heap.size() != 1)
heapAdjustDown(heap, 1, heap.size() - 1);
}
return head->next;
}
由于为了使用到完全二叉树的特性,所以堆的有效节点是从1开始存放的。
此时的循环条件改为了heap.size() > 1,和之前的queue.empty()作用是相同的。
if (heap.size() != 1)
heapAdjustDown(heap, 1, heap.size() - 1);
需要注意的是,当堆里面不存在有效数据的时候,则不再对堆调整。
接着在leetcode上运行:
几乎相同的代码,第一次跑了30ms,第二次跑了50多ms。不管是哪一个,都要比之前的插入排序快了10倍多一点。
3.最小堆(c++ algorithm)
当然,在笔试或者是面试的时候,手撸最小堆或最大堆算法还是有一定难度的,因此,除了上面的做法之外,还有一种做法就是使用c++的<algorithm>库中所提供的方法,其代码大致如下:
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
//最小堆
vector<ListNode*> heap(lists.size());
for (int i = 0;i < lists.size(); i++){
if (lists[i] != nullptr)
heap[i] = lists[i];
}
//避免链表头节点判断
ListNode* pHead = new ListNode(0);
ListNode* p = pHead;
//构建堆
make_heap(heap.begin(), heap.end(), compare);
while (!heap.empty()){
ListNode* min_node = heap[0];
p->next = min_node;
p = p->next;
//从最小堆中删除
pop_heap(heap.begin(), heap.end(), compare);
heap.pop_back();
//插入堆中
if (min_node->next != nullptr){
heap.push_back(min_node->next);
push_heap(heap.begin(), heap.end(), compare);
}
}
auto result = pHead->next;
delete pHead;
return result;
}
private:
static bool compare(ListNode* node1, ListNode* node2){
return node1->val > node2->val;
}
};
主要用到的就是make_heap、push_heap和pop_heap,经过leetcode测试,消耗时间如下: