数据结构与算法——分治的思想(C++,leetcode例题讲解)

数据结构与算法——分治的思想(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=1k(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=12ik×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个升序链表,递归结束的条件是链表数组的左端索引等于右端索引,即被分割成只有一个升序链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值