C++ 经典排序方法之一——归并排序思路解析+例题——逆序运用的两种方法

9 篇文章 0 订阅
1 篇文章 0 订阅

归并排序

思路

归并排序主要运用了分治的思想,将一组长而复杂的数组,不断分为短小而简单处理的问题,这些个问题都相互独立,但与原问题之间具有的性质相同。得出子问题的解后进行合并,从而深入浅出的解决问题。

一般体现了:分解 -> 求解 -> 合并 的过程。

我们先从最为基础的步骤:把两个已排序的数组归并排序为一个数组了。

就是从头到尾的比较两数组的元素大小,的数先放入到归并到的数组中,直至其中一个数组的元素已全部遍历放入到归并数组中,另一个数组也全部放入。(如下图所示)

实现代码如下:

void merge_sort_two_vec(vector<int> &sub_vec1,
                        vector<int> &sub_vec2,
                        vector<int> &vec){
        int i = 0;
        int j = 0;
        while(i < sub_vec1.size() && j < sub_vec2.size()){
            if(sub_vec1[i] <= sub_vec2[j]){ 
                vec.push_back(sub_vec1[i]);
                i++;
            }
            else{
                vec.push_back(sub_vec2[j]);
                j++;
            }
        }
        for(; i < sub_vec1.size(); i++){
            vec.push_back(sub_vec1[i]);
        }
        for(; j < sub_vec2.size(); j++){
            vec.push_back(sub_vec2[i]);
        }
    }

现在已经能够完成两个有序数组的排序,那么如果我把一整个无序数组不断地二分,直到一个数组中的元素个数为1时,我们就可以把这样的数组视作为有序的数组,将两个这样的数组归并排序为一个有序的数组后,再继续与其它这样有序的数组进行归并排序,直至排序为原本数组个数的有序归并数组,即完成了归并排序。(如下图所示)

实现代码如下:

void merge_sort(vector<pair<int, int>> &vec,
                    vector<int> &count){
        if(vec.size() < 2){ //递归结束条件
            return;
        }
        int mid = vec.size() / 2;
        vector<int> sub_vec1; //将数组分为两个
        vector<int> sub_vec2;
        for(int i = 0; i < mid; i++){
            sub_vec1.push_back(vec[i]);
        }
        for(int i = mid; i < vec.size(); i++){
            sub_vec2.push_back(vec[i]);
        }
        merge_sort(sub_vec1); //拆解为两个子问题
        merge_sort(sub_vec2);
        vec.clear();
        merge_sort_two_vec(sub_vec1, sub_vec2, vec); //归并数组
    }

思考——这样的排序法方式怎么快了?

假设有n个元素,这n个元素完成归并排序的时间T(n) = 分解时间 + 解决子问题时间 + 合并时间
分解时间:将原问题拆解为两个子问题的时间 复杂度为O(n)
解决子问题时间:解决两个子问题的时间 2*T(n/2)
合并时间:对两个已排序的数组归并的时间 复杂度O(n)
如图所示:

列式如下计算:
T(n) = 2T(n/2) + 2O(n)
= 2T(n/2) + O(n)
= O(n + 2
n/2 + 4n/4 + … + n1)
= O(nlogn)

逆序数计算

题目[leetcode315]

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

示例:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

提示:
0 <= nums.length() <= 105
-104 <= nums[i] <= 104

思路

方法一:
在归并排序的基础上进行修改加工,从而达到计算逆序数的目的。

依然运用前面归并两个有序数组的思路,因为两个数组都是有序的,所以归并时,其中前一个数组可以计算其逆序数,而后一个数组由于已排序,逆序数对于其为0,先不考虑。(如下图所示)

为什么后一个有序数组先不考虑逆序数的大小呢?
我们先从最为极端的方式去思考,如果按照前面的将一开始无序的数组不断二分,只有当分至数组个数为1时,才能肯定其必然有序,那此时二分至最后一个数(即无序数组的最后一个数)的后面已经没有数字,所以其逆序数必定为0,而不管其如何的与其他的数进行归并,其依然在后一个数组中,仍然不考虑其逆序数的大小,满足题目要求,这样去理解之后,是不是稍微能理解前面思路可取之处了?

所以,这样归并排序时,这两个数组计算逆序数的大小也只是先考虑两个有序数组之间的逆序数大小,归并完后,在与其他有序数组计算出逆序数后加上

而这里依然如前面归并时使用的是双指针遍历法,只是当前面数组的数小于后面数组中的数时,其对应的逆序数要加上后面数的下标值,从而达到计算逆序数的目的。
过程展示如下:


实现代码如下:

class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        vector<pair<int, int>> vec;
        vector<int> count;
        for(int i = 0; i < nums.size(); i++){
            vec.push_back(make_pair(nums[i], i));
            count.push_back(0);
        }
        merge_sort(vec, count);
        return count;
    }
private:
    void merge_sort_two_vec(vector<pair<int, int>> &sub_vec1,
                            vector<pair<int, int>> &sub_vec2,
                            vector<pair<int, int>> &vec,
                            vector<int> &count){
        int i = 0;
        int j = 0;
        while(i < sub_vec1.size() && j < sub_vec2.size()){
            if(sub_vec1[i].first <= sub_vec2[j].first){
                count[sub_vec1[i].second] += j; //前面数组计算逆序数
                vec.push_back(sub_vec1[i]);
                i++;
            }
            else{
                vec.push_back(sub_vec2[j]);
                j++;
            }
        }
        for(; i < sub_vec1.size(); i++){
            count[sub_vec1[i].second] += j;  //前面的数组计算逆序数
            vec.push_back(sub_vec1[i]);
        }
        for(; j < sub_vec2.size(); j++){
            vec.push_back(sub_vec2[j]);
        }
    }
    void merge_sort(vector<pair<int, int>> &vec){
        if(vec.size() < 2){
            return;
        }
        int mid = vec.size() / 2;
        vector<pair<int, int>> sub_vec1;
        vector<pair<int, int>> sub_vec2;
        for(int i = 0; i < mid; i++){
            sub_vec1.push_back(vec[i]);
        }
        for(int i = mid; i < vec.size(); i++){
            sub_vec2.push_back(vec[i]);
        }
        merge_sort(sub_vec1, count);
        merge_sort(sub_vec2, count);
        vec.clear();
        merge_sort_two_vec(sub_vec1, sub_vec2, vec, count);
    }
};

方法二:
基础知识点击:二叉查找树了解

使用二叉查找树的形式完成逆序数的计算。
逆序数的计算还是运用了计算第n个数比后面的数大的个数的方式

所以将元素按照原数组逆序的形式,将其插入到二叉查找树中,再计算插入后又多少个元素比其小即可。

当插入的数小于等于根节点的数时,递归插入到左子树中,并且该节点的count++。(如下图所示)

当插入的数大于根节点的数时,count_small = node->count+1,即该节点的当时左子树个数+1,递归插入到当前节点的右子树。

代码实现如下:

struct BSTNode {
	int val;
	int count;
	BSTNode *left;
	BSTNode *right;
	BSTNode(int x) : val(x), left(NULL), right(NULL), count(0) {}
};

class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        std::vector<int> result;
    	std::vector<BSTNode *> node_vec;
    	std::vector<int> count;
    	//按照原数组逆序新建节点
    	for (int i = nums.size() - 1; i >= 0; i--){
    		node_vec.push_back(new BSTNode(nums[i]));
	    } 
	    count.push_back(0);
	    //用节点构建二叉树
	    for (int i = 1; i < node_vec.size(); i++){ 
	    	int count_small = 0;
    		BST_insert(node_vec[0], node_vec[i], count_small);
    		count.push_back(count_small);
    	}
        for (int i = node_vec.size() - 1; i >= 0; i--){
        	delete node_vec[i];
        	result.push_back(count[i]); //逆序插入到结果中,因为原来计算逆序数时,为逆序(逆逆得正)
        }
        return result;
    }
private:
    void BST_insert(BSTNode *node, BSTNode *insert_node, int &count_small){
        if (insert_node->val <= node->val){ //小于等于时,统计当前的根节点的左子树节点个数
            node->count++;
            if (node->left){
                BST_insert(node->left, insert_node, count_small);
            }
            else{
                node->left = insert_node;
            }
        }
        else{
            count_small += node->count + 1; //大于根节点时,才计算逆序数
            if (node->right){
                BST_insert(node->right, insert_node, count_small);
            }
            else{
                node->right = insert_node;
            }
        }
    }
};

致谢

本章知识点和思路由小象学院相关视频提供,由本人学习并梳理得出,希望自己加深记忆的同时,也能给大家提供更多有关于一些算法的知识点。
你的点赞评论收藏就是对我最大的支持与鼓励,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值