数据结构与算法——分治的思想(C++,leetcode例题讲解)
算法是程序的核心,是我们抽象思维的实现。博主认为,我们用程序来解决问题时,应该结合生活当中的经验和程序本身的特性,创造出性能更加优越的算法。分治的思想是我们写程序时的常用的思想,它恰好反映了我们在生活解决问题时经常把一个问题分成多个问题。具体的说,简单的有我们熟悉的归并排序,稍微复杂的有快速傅里叶变换等。这些算法的本质就是分治的思想。博主今天就采用leetcode的一道题来讲解一下分治的思想,觉得有用的小伙伴可以点个赞,爱学习的你们真棒!
1、题目描述
LCR 078. 合并 K 个升序链表(https://leetcode.cn/problems/vvXgSW/description/)
给定一个链表数组,每个链表都已经按升序排列。
请将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i]
按 升序 排列lists[i].length
的总和不超过10^4
2、什么是分治?
分治的意思是把一个问题分解成多个子问题来解决,以提高解决的效率。以下图为例(来源于本题的力扣官方题解),当我们合并k个升序链表时,相当于合并前
k
2
\frac{k}{2}
2k项合并后的结果和后
k
2
\frac{k}{2}
2k项合并后的结果。并且,当我们合并前
k
2
\frac{k}{2}
2k项合并时,同样可以一分为二来合并。这样,当分到只有两个升序链表时,只需要合并这两个升序链表就可以。如果只有一个升序链表,则返回其本身。
仔细思考,我们会发现,这种不断拆分的过程可以看成一个二叉树,而树的问题的解决大多使用递归,这也是我们之后代码的逻辑。
3、为什么要用分支解决(时间复杂度分析)?
那么,我们为什么要使用分治来解决问题?答案是要提高算法的效率。我们来分析一下使用分治和不使用分治的时间复杂度。
这里我们就搬用力扣官方题解的原话。
我们就假设每个链表长度为n,那么合并两个链表的时间复杂度为O(n)。
首先考虑顺序合并(用一个变量ans来维护以及合并的链表,第i次循环把第i个链表和ans合并,答案保存到ans中):
在第一次合并后,ans的长度为n;第二次合并后,ans的长度为2xn,第i次合并后,ans的长度为ixn。第i次合并的时间代价是O(n+(i−1)×n)=O(i×n),那么总的时间代价为
O
(
∑
i
=
1
k
(
i
×
n
)
)
=
O
(
(
1
+
k
)
⋅
k
2
×
n
)
=
O
(
k
2
n
)
O(\sum_{i = 1}^{k} (i \times n)) = O(\frac{(1 + k)\cdot k}{2} \times n) = O(k^2 n)
O(i=1∑k(i×n))=O(2(1+k)⋅k×n)=O(k2n)
然后考虑分治合并:
考虑递归**「向上回升」**的过程——第一轮合并
k
2
\frac{k}{2}
2k组链表,每一组的时间代价是 O(2n);第二轮合并
k
4
\frac{k}{4}
4k组链表,每一组的时间代价是 O(4n)…所以总的时间代价是
O
(
∑
i
=
1
∞
k
2
i
×
2
i
n
)
=
O
(
k
n
×
l
o
g
k
)
O(\sum_{i=1}^{∞}\frac{k}{2^i}×2^in)=O(kn×logk)
O(i=1∑∞2ik×2in)=O(kn×logk)
可见,分治的方法拥有更低的时间复杂度,并且当k越大时效果越好。
4、代码分析
我们现在开始代码分析,首先给出代码。
/**
* 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:
//合并两个升序链表
ListNode* merge_tow_lists(ListNode* L1, ListNode* L2){
ListNode* head = new(ListNode);
ListNode* p1 = head;
while(L1&&L2){
if(L1->val <= L2->val){
p1->next = L1;
L1 = L1->next;
}
else{
p1->next = L2;
L2 = L2->next;
}
p1 = p1->next;
}
p1->next = (L1? L1:L2);
return head->next;
}
//合并升序链表
ListNode* merge_lists(vector<ListNode*>& lists, int l, int r){
if(r==l) return lists[l];
else{
int mid = (r+l)>>1;
ListNode* L1 = merge_lists(lists, l, mid);
ListNode* L2 = merge_lists(lists, mid+1, r);
return merge_tow_lists(L1, L2);
}
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
int n = lists.size();
if(n == 0) return nullptr;
return merge_lists(lists, 0, n-1);
}
};
我们要合并k个升序链表,要先写出合并两个升序链表的代码。我们只需要使用两个指针分别遍历每个链表,比较并插入到新建链表的尾部。最后,当任意指针遍历到空时,将另一指针接到新链表的尾部即可完成两个升序链表的合并。
然后,我们要使用递归的方法分治合并k个升序链表,递归结束的条件是链表数组的左端索引等于右端索引,即被分割成只有一个升序链表。