用合适的数据结构,事倍功半——leetcode 21 & 23
先看23题:
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
题目是很简单的,它也的确就是个easy题。直接上代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution
{
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
/*if (!l1) { return l2; }
if (!l2) { return l1; }
ListNode* l = NULL;
if (l1->val < l2->val)
{
l = l1;
l1 = l1->next;
}
else
{
l = l2;
l2 = l2->next;
}*/
ListNode* l = new ListNode(INT_MIN);
ListNode* head = l;
while (l1 && l2)
{
if (l1->val < l2->val)
{
l->next = l1;
l1 = l1->next;
}
else
{
l->next = l2;
l2 = l2->next;
}
l = l->next;
}
l->next = l1 ? l1 : l2;
//return head;
return head->next;
}
};
没什么好说的,唯一值得注意的地方就是,对于头结点的处理,怎么样可以使代码更简洁一点。想象一下,新的链表要求升序,那么第一个结点必须是最小的,但是现在有两个原链表,我们还要比较一下这两个链表的头结点,选那个小的作为新链表的头结点。这一段代码,逻辑上不难理解,但还是要几行的。
一种简单的实现方法是,把新链表地头结点设为INT_MAX,然后我们就可以放心大胆地往新链表中添加结点了。这样操作在时间复杂度上没有任何改进,但是因为在循环外少了个判断,因此代码更简洁了。如果这样做的话,记得返回的结点是头结点的后继结点。
再看23题:
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
21题的解法居然也可以过
这个题在leetcode上的难度是hard,但我觉得它是不配这个难度的。我们完全可以按照21题的做法去做这个题,代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
int min_index = findMinIndex(lists);
if (min_index == -1) { return NULL; }
ListNode* l = lists[min_index], *head = l;
lists[min_index] = lists[min_index]->next;
while(1)
{
min_index = findMinIndex(lists);
if (min_index == -1) { break; }
l->next = lists[min_index];
lists[min_index] = lists[min_index]->next;
l = l->next;
}
return head;
}
int findMinIndex(vector<ListNode*>& lists)
{
int min_val = INT_MAX, n = lists.size(), min_index = -1;
for (int i = 0; i < n; ++i)
{
if (lists[i])
{
if (lists[i]->val < min_val)
{
min_index = i;
min_val = lists[i]->val;
}
}
}
return min_index;
}
};
这样做,是能过的。那么它的时间复杂度是多少呢?我们算一下,假设总共有n个数,分别在k个链表中,每次我们都要比较k个链表的当前结点,找到最小的那个结点。因此时间复杂度是O(kn)。
最小堆
有没有更简单的解法呢?要是我们能把所有数都拿出来,然后再排序,时间复杂度会不会低一点?我们分析一下时间复杂度。现在的问题就是,我们把拿出来的数放到什么数据结构呢?
最容易想到的自然就是vector了,我们算一下这种做法的时间复杂度是多少。把所有数都拿出来是O(n),排序的时间复杂度是O(nlgn)。因此整个解法的时间复杂度是O(nlgn)。这个解法不见得时间复杂度更低的,需要比较k和lgn的值。但是在k比较大的情况下,这个解法显然是比较好的。
但是,说到排序,其实还有一种数据结构有这个作用。之前我们用得不算特别多,但还是应该会用的。它就是优先队列(实质上是一个最大堆或者最小堆,默认是最大堆)。事实上我们只能访问到堆顶元素,但是堆的优势在于它会自动维护,也就是说,以最小堆为例,无论我们向堆中插入或者删除多少数据,堆顶的元素始终是堆中元素最小的那个。所以,我们把所有数据扔进堆中,再一个一个拿出来,那么肯定是从小到大的。
然后,我们算一下用最小堆做这个题的时间复杂度是多少。堆的性质当中有很重要的一条,就是向堆中插入元素的时间复杂度是O(lgn),n为堆的大小。因此向堆中插入n个元素,时间复杂度就是O(lgn)(事实上时间复杂度是 lg1 + lg2 + … + lgn = O(nlgn))。而将这些元素依次取出也是这个时间复杂度。所以整个解题过程的时间复杂度就是O(nlgn),和用vector是一样的。实现代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
int n = lists.size();
priority_queue<int, vector<int>, greater<int> > que;
//默认最大堆,所以要定义为最小堆
ListNode* l = new ListNode(INT_MIN), *head = l;
for (int i = 0; i < n; ++i)
{
while(lists[i])
{
que.push(lists[i]->val);
lists[i] = lists[i]->next;
}
}
while (!que.empty())
{
l->next = new ListNode(que.top());
que.pop();
l = l->next;
}
return head->next;
}
};
真的需要这么大的最小堆吗?
还能再简单点吗?我们现在有k个链表,最小堆的大小是n,因为我们把链表的所有结点都扔进去了。事实上有没有这个必要呢?我们能不能在向堆中放入数据的同时,取出一部分元素先进行排序呢?我们推演一下,现在k个链表的头结点入堆,然后我们想找出最小结点的后驱结点,并且把它放入堆。我们发现,当这个最小结点的后驱结点入堆之后,我们就不需要在堆中保存这个最小结点了,因为这个最小结点所在链表的所有元素,可以通过后驱结点找到。因此,我们可以放心大胆地把这个最小结点弹出堆。
想明白这一点,就很容易知道,堆的大小为k就够用了。我们看一下这个时间复杂度,堆的大小是k,有n个结点要ru’dui出堆,因此时间复杂度是O(nlgk),这个时间复杂度显然是比之前的O(nlgn)要小的,因此这种改进是有意义的。实现的代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
struct cmp1
{
ListNode* l;
cmp1(ListNode *ll) { l = ll; }
cmp1() {}
bool operator<(const cmp1& ob) const
//优先队列默认的是大顶堆,改成小顶堆
{
return l->val > ob.l->val;
}
};
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
if (lists.empty()) { return NULL; }
int n = lists.size();
priority_queue<cmp1> que;
cmp1 ob;
for (auto it: lists)
{
if (it) { que.push(it); } //有些链表为空,没必要头结点入堆
}
if (que.empty()) { return NULL; } //所有链表都为空
ListNode* head = new ListNode(INT_MIN), *cur = head;
while (!que.empty())
{
ob = que.top();
que.pop();
if (ob.l->next) //若if不满足,说明该链表已经到末端
{
cmp1 ob2 = cmp1(ob.l->next);
que.push(ob2);
}
cur->next = ob.l;
cur = cur->next;
}
return head->next;
}
};
这两道题到这里就结束了,我之前做到这里的时候也以为事情告一段落了,但过了几天才发现这才刚开始,后面又有几道题,和堆这个数据结构脱不开干系。这就留到下一篇再说了。