题目
解答1:两个两个合并
思路
最简单直接的方法,不解释了。
代码
/*
* @lc app=leetcode.cn id=23 lang=cpp
*
* [23] 合并K个升序链表
*/
// @lc code=start
/**
* 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) {}
* };
*/
#include <vector>
using namespace std;
class Solution
{
public:
ListNode *mergeKLists(vector<ListNode *> &lists)
{
int listslength = lists.size();
ListNode *reslist = nullptr;
for (int i = 0; i < listslength; i++)
{
reslist = mergeTwoLists(reslist, lists[i]);
}
return reslist;
}
//这个方法可以照搬 21.合成两个有序链表 来使用
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
{
//一旦有哪个链表元素遍历完了,直接返回另外一个链表即可
if (l1 == nullptr || l2 == nullptr)
{
return l1 ? l1 : l2;
}
//l1比l2小,所以下一个元素应该从 "l1->next" 和 "l2" 这两个结点开头的两个链表里选
//意思很明显了,就是l1要顺位指向下一个结点
//else同理
else if (l1->val < l2->val)
{
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else
{
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
//最后不需要再写return,上面两端if的判断说明:如果l1->val < l2->val,最后一定是执行return l1
//否则,最后一定是执行return l2,均符合题目要求(小的结点当头头)
}
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
- 时间复杂度: O(k2n) 。假设每个链表的最长长度均为 n。
在第一次合并后,reslist的长度为 n
第二次合并后,reslist的长度为2n
第 i 次合并后,reslist的长度为 in。
由此,可以推出:第 i 次合并的时间复杂度(第i-1个reslist和第i个链表合并):O(n+(i-1)*n) 。
最后,合并k个链表意味着需要进行k-1次两两合并:O( ∑ i = 1 k − 1 i × n \sum_{i=1}^{k-1}i\times n ∑i=1k−1i×n) = O( k ( k − 1 ) 2 × n \frac{k(k-1)}{2}\times n 2k(k−1)×n) = O(k2n)
由于实际中每个链表最长长度均不超过n,所以可以归纳出以上的时间复杂度。 - 空间复杂度:O(1) 。
解答2:分治法
思路
分治法是对解法1的优化,使用了归并排序的思想,思考一下是不是很像一颗倒置二叉树。
代码
/*
* @lc app=leetcode.cn id=23 lang=cpp
*
* [23] 合并K个升序链表
*/
// @lc code=start
/**
* 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) {}
* };
*/
#include <vector>
using namespace std;
class Solution
{
public:
ListNode *mergeKLists(vector<ListNode *> &lists)
{
int listslength = lists.size();
return mergeDAC(lists, 0, listslength - 1);
}
ListNode *mergeDAC(vector<ListNode *> &lists, int left, int right)
{
if (left == right)
{
return lists[left];
}
if (left > right)
{
return nullptr;
}
//注意>>1和/2的区别
int mid = (left + right) >> 1;
return mergeTwoLists(mergeDAC(lists, left, mid), mergeDAC(lists, mid + 1, right));
}
//这个方法可以照搬21.合成两个有序链表来使用
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
{
//一旦有哪个链表元素遍历完了,直接返回另外一个链表即可
if (l1 == nullptr || l2 == nullptr)
{
return l1 ? l1 : l2;
}
//l1比l2小,所以下一个元素应该从 "l1->next" 和 "l2" 这两个结点开头的两个链表里选
//意思很明显了,就是l1要顺位指向下一个结点
//else同理
else if (l1->val < l2->val)
{
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else
{
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
//最后不需要再写return,上面两端if的判断说明:如果l1->val < l2->val,最后一定是执行return l1
//否则,最后一定是执行return l2,均符合题目要求(小的结点当头头)
}
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
- 时间复杂度: O(
k
n
log
k
kn\log k
knlogk) 。假设每个链表的最长长度均为 n。
第一次合并 k 2 \frac{k}{2} 2k组链表,每组链表合并时间复杂度为 O(2n) ;
第二次合并 k 4 \frac{k}{4} 4k组链表,每组链表合并时间复杂度为 O(4n) ;
以此类推,总的时间复杂度:O(n+(i-1)*n) 。
最后,合并k个链表意味着需要进行k-1次两两合并:O( ∑ i = 1 l o g k k 2 i × 2 i n \sum_{i=1}^{\\logk }\frac{k}{2^{i}}\times 2^{i}n ∑i=1logk2ik×2in) = O( k n log k kn\log k knlogk)
由于实际中每个链表最长长度均不超过n,所以可以归纳出以上的时间复杂度。 - 空间复杂度: O(
log
k
\log k
logk) 。 递归次数产生的空间。
注:为什么上面的时间复杂度和空间复杂度存在 log k?想一想二叉树高度和叶子结点的关系。
解答3:队列法
思路
假如某天免费抢购iPhone,来了16个人,商店分了4条队列,但是又要按照先来后到的顺序,那我们应该怎么办呢?
| 1 5 9 13
柜 | 2 6 10 14
台 | 3 7 11 15
| 4 8 12 16
假定这16个人排成了以上的顺序。我们肯定要先选每列的第一个人,看看谁的更小,就轮到谁。可以很容易地看出来,第一个人可以先拿到iPhone,然后走人。
| 5 9 13
柜 | 2 6 10 14
台 | 3 7 11 15
| 4 8 12 16
5号很开心,以为轮到自己了,但是店员告诉他,你前面还有三位顾客正在等待哦,不能插队。2号点了个赞,开开心心地拿到iPhone走了。
| 5 9 13
柜 | 6 10 14
台 | 3 7 11 15
| 4 8 12 16
接着,3号和4号都拿到了iPhone,这回终于轮到5号了。
| 5 9 13
柜 | 6 10 14
台 | 7 11 15
| 8 12 16
之后就是一样的道理,大家看懂了吗?
代码
/*
* @lc app=leetcode.cn id=23 lang=cpp
*
* [23] 合并K个升序链表
*/
// @lc code=start
/**
* 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) {}
* };
*/
#include <vector>
#include <queue>
using namespace std;
//如果在自定义类内重载运算符,那么这个函数作为成员函数,有一个隐式的参数this指针,无法实现双目运算符重载
//一个双目运算符重载应该类外定义,作为全局重载使用
class Solution
{
public:
//注意这个是队列的struct,而链表本身的struct仍然是listnode
struct reset_heap_seq
{
int val;
ListNode *queueNext;
//重载<变为>,const reset_heap_seq &rhs指任意一个结点
//函数加上const后缀的作用是表明函数本身不会修改类成员变量,同时使得该函数可以被 const 对象所调用
bool operator<(const reset_heap_seq &node) const
{
return val > node.val;
}
};
priority_queue<reset_heap_seq> firstQueue;
ListNode *mergeKLists(vector<ListNode *> &lists)
{
//将目前list中非空的每个链表的第一个node push到优先队列中
//自动调整为小根堆,即已经自动排序
for (ListNode *node : lists)
{
if (node)
firstQueue.push({node->val, node});
}
//定义返回的链表数组和头结点
ListNode *resList = new ListNode();
ListNode *head = resList;
//如果队列非空,则pop第一个数(最小值)
//注意分清楚queue的每一个元素是一个链表,而resList是一个链表指针
//多想想就不难理解 first.queueNext->next->val 是什么意思
while (!firstQueue.empty())
{
//定义first为队列top元素,避免编写过于复杂
auto first = firstQueue.top();
//弹出队列top元素
firstQueue.pop();
//接上队列元素下一个
resList->next = first.queueNext;
resList = resList->next;
//查找下一个队列里的元素,如果非空,将当前元素所在链表的下一个元素push进队列
if (first.queueNext->next)
firstQueue.push({first.queueNext->next->val, first.queueNext->next});
}
return head->next;
}
};
// @lc code=end
时间复杂度和空间复杂度(官方答案,说的很好)
- 时间复杂度: O( k n log k kn\log k knlogk) 。
- 空间复杂度: O(k) 。 优先队列中的元素不超过 k 个,用于存放每一条链表当前的第一个元素。
注:为什么上面的时间复杂度还是和 log k有关?想一想,优先队列基于什么?(大顶堆/小顶堆)
反思与总结
- 想一想>>1和/2的不同之处。
- 碰到最大最小问题,可以思考优先队列的方法是否合适。