【算法杂货铺】分治


目录

🌈前言🌈

📁 快速排序

 📂75. 颜色分类 - 力扣(LeetCode)

 📂 912. 排序数组 - 力扣(LeetCode)

📂 215. 数组中的第K个最大元素 - 力扣(LeetCode)

  📂 面试题 17.14. 最小K个数 - 力扣(LeetCode)

📁 归并排序

 📂 912. 排序数组 - 力扣(LeetCode)

📂 LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

  📂 315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

   📂 493. 翻转对 - 力扣(LeetCode)

📁 总结


🌈前言🌈

        欢迎收看本期【算法杂货铺】,本期内容将讲解分治思想,通过讲解快速排序和归并排序这两个排序来理解学习分治。

        本文旨在零基础学习,轻松搞懂,所以会不会讲解算法时间复杂度等验证,而是给出结论。当然,习题是会给出题解和思路的。

        此外,本文中所有代码都是C++实现,其他语言可以参考题解思路。

        为了照顾没有学习过快速排序的同学,本文会在讲解习题之前,讲解排序思路。

📁 快速排序

        快速排序的思想是选择一个基准元素key,以key为中间值,将数组分为两个区间,左区间的元素是严格小于key值,右区间元素严格大于key值。

        再将左区间和右区间排序,也是同样的思路,选出key值,划分左右区间,再进行排序。

        由上图可知,我们每次将数组排序,都将数组划分成两个区间,一共会执行logN层,每一层都会遍历n次,所以时间复杂度是O(NlogN)。

        快速排序的实现 : 三指针(数组分三块) +  随机选择基准元素。 

 📂75. 颜色分类 - 力扣(LeetCode)

        在讲解快速排序之前,我们首先来学习一下,怎么将每一层的数组排序,即三指针(数组分三块)。

        这里我们使用三个指针(下标)来讲区间划分。

left : 表示0元素的末尾。

cur : 用来扫描数组。

right : 表示2的开头。

        扫描期间,cur要保证以下区间的稳定:

[0,left] : 都是0元素;

[left + 1 , cur - 1] : 都是1元素;

[cur , right - 1] : 是未扫描元素。

[right , size()-1] : 是2元素。

算法流程:

i. 初始化left = -1 , cur =0 , right = size() + 1;

ii. 循环扫描数组,cur < right时,cur就继续往后扫描数组。

        a. nums[cur] == 0时,就交换nums[left + 1] 和 nums[cur],再让left++,cur++。cur为什么可以++,因为left+1位置的元素,一定为0,或者1的。1的话,我们直接++即可;如果是0,就1种情况,cur == left + 1,所以是自己与自己交换,也可以直接++。

        b. nums[cur] == 1时,cur直接++。

        c. nums[cur] == 2时,交换nums[right - 1] 和 nums[cur],right--, 此时cur不能++,因为不能保证right-1位置的值一定是0 或者 1,所以需要接着判断交换后,i位置的值。

iii. cur >= right 表示扫描结束,此时总共有三个区间,分别代表0,1,2的区间。

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left = -1 , cur = 0 , right = nums.size();
        while(cur < right)
        {
            if(nums[cur] == 0)
                swap(nums[++left] , nums[cur++]);
            else if(nums[cur] == 1)
                cur++;
            else    
                swap(nums[--right],nums[cur]);
        }
    }
};

 📂 912. 排序数组 - 力扣(LeetCode)

        如果你理解颜色分类这道题,那么你以及掌握快速排序的大部分内容了,剩下的就是理解如果将区间划分,以及选择基准元素了。

        选择基准元素,最佳的策略是随机选择[left,right]范围内的任意元素,对应的是rand()函数,这样时间复杂度会达到最优解。

        将一层排完序后,再递归到左右区间进行排序。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand(time(NULL));
        qsort(nums , 0 , nums.size()-1);
        return nums;
    }

    void qsort(vector<int>& nums,int l ,int r)
    {
        if(l >= r)
            return;
        
        int key = GetRandom(nums,l,r);

        int left = l - 1 , right = r + 1;
        int cur = l;

        while(cur < right)
        {
            if(nums[cur] < key)
                swap(nums[++left], nums[cur++]);
            else if(nums[cur] == key)
                cur++;
            else    
                swap(nums[--right],nums[cur]);
        }

        qsort(nums,l,left);
        qsort(nums,right,r);
    }

    int GetRandom(vector<int>& nums,int left,int right)
    {
        return nums[rand()%(right - left + 1) + left];
    }
};

📂 215. 数组中的第K个最大元素 - 力扣(LeetCode)

        通过快速排序,我们将区间划分成三块[0,left]  [left + 1 , right - 1]  [right , size()-1]。计算出每个区间元素个数,再到相应区间去找即可。

        算法就是 快速选择算法。

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        srand(time(NULL));
        return qsort(nums , 0 , nums.size() - 1 , k);
    }

    int qsort(vector<int>& nums,int l , int r , int k)
    {
        if(l == r)  
            return nums[l];
    
        int key = GetRandom(nums,l,r);

        int left = l - 1 , right = r + 1;
        int cur = l;
        while(cur < right)
        {
            if(nums[cur] < key)
                swap(nums[++left],nums[cur++]);
            else if(nums[cur] == key)
                cur++;
            else
                swap(nums[--right],nums[cur]);
        }

        int c = r - right + 1;
        int b = right - left - 1;
        if(c >= k )
            return qsort(nums,right,r,k);
        else if(b + c >= k)
            return key;
        else 
            return qsort(nums,l,left,k-b-c);
    }

    int GetRandom(vector<int>& nums, int left , int right)
    {
        return nums[rand()%(right - left + 1) + left];
    }
};

  📂 面试题 17.14. 最小K个数 - 力扣(LeetCode)

        依旧,是一道快速选择算法的题目,不过是从前面开始,并且是选k个数,那我们就将排好序的前k个数返回即可。顺序没有要求。

        顺序没有要求,意味着当k落在[left + 1, right - 1]这个区间的时候,直接返回即可。因为左边两个区间所有元素都是小于左区间的。

class Solution {
public:
    vector<int> smallestK(vector<int>& arr, int k) {
        srand(time(NULL));

        qsort(arr, 0 , arr.size()-1 , k);
        
        return {arr.begin() , arr.begin() + k};
    }

    void qsort(vector<int>& nums,int l,int r,int k)
    {
        if(l >= r)
            return;
    
        int key = GetRandom(nums,l,r);

        int left = l - 1, right = r + 1;
        int cur = l;
        while(cur < right)
        {
            if(nums[cur] < key)
                swap(nums[++left],nums[cur++]);
            else if(nums[cur] == key)
                cur++;
            else    
                swap(nums[--right] , nums[cur]);
        }

        int a = left - l + 1;
        int b = right - left - 1;
        if(a >= k)
            qsort(nums,l,left,k);
        else if(a + b >= k)
            return ;
        else    
            qsort(nums,right,r,k-a-b);
    
    }

    int GetRandom(vector<int>& nums,int left , int right)
    {
        return nums[rand() % (right - left + 1) + left];
    }
};

📁 归并排序

 📂 912. 排序数组 - 力扣(LeetCode)

        归并排序,非常充分的体现了分治思想,根据元素个数将数组划分成两个区间,直到只有1个元素,使整个数组的排序分为【左半部分排序】和【右半部分排序】。

        归并排序和快速排序有什么区别呢?归并排序是先将左区间排序,再将右区间排序,最后整体排序,是一种后序遍历的思路。快速排序是先将元素放在合适位置后,即先整体排序,再将左右区间排序,是一种前序遍历的思路。

        当然,还有就是归并排序是要有辅助数组的。

class Solution {
public:
    vector<int> temp;
    vector<int> sortArray(vector<int>& nums) {
        temp.resize(nums.size());
        mergesort(nums,0,nums.size()-1);
        return nums;
    }

    void mergesort(vector<int>& nums,int left,int right)
    {
        if(left >= right)
            return ;
        
        int mid = (left + right) >>1;

        mergesort(nums,left,mid);
        mergesort(nums,mid+1 ,right);

        int cur1 = left,cur2 = mid+1;
        int i = left;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2])
            {
                temp[i++] = nums[cur1++];
            }
            else
            {
                temp[i++] = nums[cur2++];
            }
        }

        while(cur1 <= mid)
        {
           temp[i++] = nums[cur1++];
        }
        while(cur2 <= right)
        {
            temp[i++] = nums[cur2++];
        }
        for(int i= left ; i <= right ;i++)
            nums[i] = temp[i];
    }
};

📂 LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

        逆序对的概念就是有两个元素,后面的元素小于前一个元素。本期就是统计逆序对的个数,如果我们采用暴力解法,即从当前位置开始,往后遍历,找到比它小的就+1,肯定是不行的,时间复杂度为O(N^2)。

        用归并排序求逆序数是很经典的方法,主要是在归并排序的合并过程中统计处逆序的数量,也就是在合并两个有序序列的过程中,能快速求出逆序对的数量。

1. 为什么可以利用归并排序:

        将数组划分成两个,可以将逆序对产生的方式划分为3组:1. 全部从左数组来;2. 全部从右数组来;3. 左右数组各一个。根据排列组合的分类相加原理,三种情况下产生的逆序对总和,正好是总的逆序对的个数。

        这个思路正好匹配归并排序:1. 先排序做数组;2. 再排序有数组;3. 左右数组合并。

        因此,我们可以利用归并排序的过程,先求出左数组中逆序对的数量,再求出右数组中逆序对的个数,最后求出左右数组各一个逆序对的数量,三者相加即可。

2. 为什么要用归并排序

        在归并排序过程中,我们得到的是两个有序数组,可以利用数组的有序性,快速统计出逆序对的数量,而不是将所有情况枚举出来。

        有两个策略:

        1. 快速统计出某个数字前面有多少个数比它大;(升序)

        2. 快速统计出某个数组后面有多少个数比它小;(降序)

        这里的顺序是不能改变的,升序不能改为降序,可以理解为如果升序改为降序,可能会有重复的计算。

class Solution {
public:
    int temp[50010];
    int reversePairs(vector<int>& record) {
        return mergesort(record , 0 , record.size()-1);
    }

    int mergesort(vector<int>& nums, int left , int right)
    {
        if(left >= right)
            return 0;
        
        int ret = 0;
        int mid = (left + right) >> 1;

        ret += mergesort(nums,left,mid);
        ret += mergesort(nums,mid+1,right);

        int cur1 = left , cur2 = mid+1;
        int i = left;
        while(cur1 <= mid &&cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2])
            {
                temp[i++] = nums[cur1++];
            }
            else
            {
                ret += mid - cur1 + 1;
                temp[i++] = nums[cur2++];
            }
        }

        while(cur1 <= mid)
        {
            temp[i++] = nums[cur1++];
        }
        while(cur2 <= right)
        {
            temp[i++] = nums[cur2++];
        }

        for(int i= left ; i <= right ;i++)
        {
            nums[i] = temp[i];
        }
        return ret;
    }
};

  📂 315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

各个数组和函数功能

 1. 创建两个全局数组:

vector<int> index : 记录下标

vector<int> ret : 记录结果

inde用来与原数组与对应位置的元素绑定,ret用来记录每个位置统计出来的逆序对的个数。

并创建index和ret的辅助数组。

2. countsamller()主函数:

a. 初始化两个全局数组,index初始化为数组的下标,ret初始化为0。

b. 为两个数组开辟大小为n的空间。

c. 调用mergesort()函数,并返回ret结果数组。

3. mergesort()函数

a. 定义递归出口:left>=right,直接返回。

b. 划分区间 [left , mid] 和 [mid + 1 , right]。

c. 统计左右区间逆序对数量

d. 统计左右区间逆序对数量。

class Solution {
public:
    vector<int> ret;
    vector<int> index;
    vector<int> indexTemp;
    vector<int> retTemp;
    vector<int> countSmaller(vector<int>& nums) {

        ret.resize(nums.size());
        index.resize(nums.size());
        indexTemp.resize(nums.size());
        retTemp.resize(nums.size());

        for(int i=0;i<nums.size();i++)
        {
            ret[i] = 0;
            index[i] = i;
        }

        mergesort(nums, 0 , nums.size()-1 , ret);

        return ret;
    }

    void mergesort(vector<int>& nums,int left,int right,vector<int>& ret)
    {
        if(left >= right)
            return;

        int mid = (left + right) >> 1;

        //处理左区间逆序对
        mergesort(nums,left,mid,ret);
        //处理右区间逆序对
        mergesort(nums,mid+1,right,ret);
        //处理一左一右
        int cur1 = left,cur2 = mid+1;
        int i = left;
        while(cur1 <= mid && cur2 <= right)
        {
            if(nums[cur1] <= nums[cur2])                
            {
                retTemp[i] = nums[cur2];
                indexTemp[i++] = index[cur2++];
            }

            else
            {
                ret[index[cur1]] += right - cur2 +1;
                retTemp[i] = nums[cur1];
                indexTemp[i++] = index[cur1++];
            } 
        }

        //处理剩余排序问题
        while(cur1 <= mid)
        {
            retTemp[i] = nums[cur1];
            indexTemp[i++] = index[cur1++];
        }
        while(cur2 <= right)
        {
            retTemp[i] = nums[cur2];
            indexTemp[i++] = index[cur2++];           
        }

        for(int i=left;i<=right;i++)
        {
            nums[i] = retTemp[i];
            index[i] = indexTemp[i];
        }
    }
};

   📂 493. 翻转对 - 力扣(LeetCode)

        翻转对和逆序对的定义大同小异,你对许是前面的数要大于后面的数。而翻转数是前面的一个数要大于后面某个数的两倍。因此,我们可以采用归并排序来解决问题。

        与逆序对最大的不同在于,求逆序对时,可以在归并的时候统计逆序对的数量。而翻转对则需要提前统计。

class Solution {
public:
    int temp[50010];

    int reversePairs(vector<int>& nums) {
        return mergesort(nums , 0 , nums.size()-1);
    }

    int mergesort(vector<int>& nums,int left,int right)
    {
        if(left >= right)
            return 0;
        int ret = 0;

        int mid =(left + right) >> 1;

        ret += mergesort(nums,left,mid);
        ret += mergesort(nums,mid+1,right);

        //计算翻转对
        int cur1 = left,cur2 = mid+1;
        while(cur1 <= mid)
        {
            //while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0) cur2++;
            while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
            {
                cur2++;
            }
            if(cur2 > right)
            {
                break;
            }
            ret += right - cur2 + 1;
            cur1++;
        }

        //合并有序数组
        cur1 = left,cur2 = mid+1;
        int i = left;
        while(cur1 <= mid && cur2 <= right)
        {
            temp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++]:nums[cur1++];
        }

        while(cur1 <= mid)
        {
            temp[i++] = nums[cur1++];
        }   
        while(cur2 <= right)
        {
            temp[i++] = nums[cur2++];
        }

        for(int i= left ;i <= right ; i++)
        {
            nums[i] = temp[i];
        }
        return ret;
    }
};

📁 总结

        以上,就是本期【算法杂货铺】的主要内容了,主要通过讲解快速排序和归并排序来学习分治思想。

        如果本期内容有帮助到你,欢迎点赞,关注,收藏。Thanks♪(・ω・)ノ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/478927.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

以RISC-V架构的CLIC中断机制讲解:中断咬尾、中断抢占、中断晚到

1、中断的相关属性 中断所属特权模式&#xff08;M模式 > S模式 > U模式&#xff09;中断等级&#xff1a;决定是否能够抢占当前的中断中断优先级&#xff1a;影响中断的仲裁&#xff0c;优先级高时优先被响应中断编号&#xff1a;区分中断&#xff0c;影响中断的仲裁 …

操作系统面经-什么是操作系统?

通过以下四点可以概括操作系统到底是什么&#xff1a; 操作系统&#xff08;Operating System&#xff0c;简称 OS&#xff09;是管理计算机硬件与软件资源的程序&#xff0c;是计算机的基石。操作系统本质上是一个运行在计算机上的软件程序 &#xff0c;主要用于管理计算机硬…

视频素材库哪家好?我给大家来分享

视频素材库哪家好&#xff1f;这是很多短视频创作者都会遇到的问题。别着急&#xff0c;今天我就来给大家介绍几个视频素材库哪家好的推荐&#xff0c;让你的视频创作更加轻松有趣&#xff01; 视频素材库哪家好的首选当然是蛙学网啦&#xff01;这里有大量的高质量视频素材&am…

成都百洲文化传媒有限公司电商新浪潮的领航者

在当今电商行业风起云涌的时代&#xff0c;成都百洲文化传媒有限公司以其独特的视角和专业的服务&#xff0c;成为了众多商家争相合作的伙伴。今天&#xff0c;就让我们一起走进百洲文化的世界&#xff0c;探索其背后的成功密码。 一、百洲文化的崛起之路 成都百洲文化传媒有限…

python共享单车信息系统的设计与实现flask-django-php-nodejs

课题主要分为二大模块&#xff1a;即管理员模块和用户模块&#xff0c;主要功能包括&#xff1a;用户、区域、共享单车、单车租赁、租赁归还、报修信息、检修信息等&#xff1b; 语言&#xff1a;Python 框架&#xff1a;django/flask 软件版本&#xff1a;python3.7.7 数据库…

从内存巷弄到指针大道(一)

文章目录 1.内存和地址1.1理解内存地址酒店大堂&#xff1a;内存的入口房间号&#xff1a;内存地址的意义酒店的楼层划分&#xff1a;内存的结构酒店的房间单位&#xff1a;计算机中的常见单位 1.2如何理解编址 2.指针变量和地址2.1取地址操作符&#xff08;&)2.2 指针变量…

windows系统下python进程管理系统

两年来&#xff0c;我们项目的爬虫代码大部分都是放在公司的windows机器上运行的&#xff0c;原因是服务器太贵&#xff0c;没有那么多资源&#xff0c;而windows主机却有很多用不上。为了合理利用公司资源&#xff0c;降低数据采集成本&#xff0c;我在所以任务机器上使用anac…

力扣热门算法题 59. 螺旋矩阵 II,60. 排列序列,61. 旋转链表

59. 螺旋矩阵 II&#xff0c;60. 排列序列&#xff0c;61. 旋转链表&#xff0c;每题做详细思路梳理&#xff0c;配套Python&Java双语代码&#xff0c; 2024.03.21 可通过leetcode所有测试用例。 目录 59. 螺旋矩阵 II 解题思路 完整代码 Java Python 60. 排列序列 …

Linux基础命令[20]-useradd

文章目录 1. useradd 命令说明2. useradd 命令语法3. useradd 命令示例3.1 不加参数3.2 -d&#xff08;指定家目录&#xff09;3.3 -g&#xff08;指定用户组&#xff09;3.4 -G&#xff08;指定附属组&#xff09;3.5 -p&#xff08;加密密码&#xff09;3.6 -e&#xff08;指…

东方博宜 1449. 求满足条件的数的和

东方博宜 1449. 求满足条件的数的和 这道题我苦想了很久&#xff0c;觉得2个及2个以上很难解决&#xff0c;但是后面发现&#xff0c;可以用一个变量记录次数&#xff0c;次数大于等于2就好了。 #include<iostream> using namespace std; int main() {int n ;cin >…

JetPack之DataBinding基础使用

目录 一、简介二、使用2.1 使用环境2.2 xml文件绑定数据2.3 数据绑定的对象2.3.1 object2.3.2 ObseravbleField2.3.3 ObseravbleCollection 2.4 绑定数据 三、应用场景 一、简介 DataBinding是谷歌15年推出的library,DataBinding支持双向绑定&#xff0c;能大大减少绑定app逻辑…

防火墙在解决方案及典型项目中的应用

防火墙在解决方案及典型项目中的应用 防火墙作为基础安全防护产品&#xff0c;在各种解决方案、业务场景中配套应用&#xff0c;本节给出各类方案资料链接方便查阅。 防火墙在华为网络解决方案中的应用 解决方案 文档 主要应用 CloudFabric云数据中心网解决方案 资料专区…

游戏引擎开发公司 Unity 调查:超六成游戏工作室采纳AI助力开发,效率与质量双提升

Unity是一家专注于游戏引擎开发的公司&#xff0c;其开发的Unity引擎被广泛应用于游戏开发领域&#xff0c;为开发者提供了强大的工具来创建高质量的游戏。Unity引擎不仅支持多种平台&#xff0c;而且具有易用性和灵活性&#xff0c;使得开发者能够高效地进行游戏开发。近年来&…

一文速通自监督学习(Self-supervised Learning):教机器自我探索的艺术

一文速通自监督学习&#xff08;Self-supervised Learning&#xff09;&#xff1a;教机器自我探索的艺术 前言自监督学习是什么&#xff1f;自监督学习的魔力常见的自监督学习方法1. 对比学习2. 预测缺失部分3. 旋转识别4. 时间顺序预测 结语 &#x1f308;你好呀&#xff01;…

Springboot 博客_002 项目环境配置

引入相关依赖 mysqlmybatis <dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-…

数据库关系运算理论:专门的关系运算概念解析

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

德邦物流、京东重货、跨越速运、百世快运同台比价,寄哪个物流最便宜?一目了然

快递物流上门取件综合版的上线确实为许多用户提供了极大的便利。 德邦物流、京东重货、跨越速运、百世快运等作为业内知名的物流公司&#xff0c;其服务质量和运输效率都得到了广大用户的认可。 一键下单的功能更是简化了操作流程&#xff0c;提高了用户体验。 德邦物流&…

【半导体存储】关于NANDFlash的一些小知识

前言 作为一名电子专业的学生&#xff0c;半导体存储显然是绕不过去的一个坎&#xff0c;今天聊一聊关于NandFlash的一些小知识。 这里十分感谢深圳雷龙发展有限公司为博主提供的两片SD NAND的存储芯片&#xff0c;同时也给大家推荐该品牌的相关产品。 一、定义 存储芯…

漫谈微服务网关

一、什么是服务网关 服务网关 路由转发 过滤器 1、路由转发&#xff1a;接收一切外界请求&#xff0c;转发到后端的微服务上去&#xff1b; 2、过滤器&#xff1a;在服务网关中可以完成一系列的横切功能&#xff0c;例如权限校验、限流以及监控等&#xff0c;这些都可以通过…

(Linux 学习十二)文件查找和文件压缩

一.文件查找 which 命令查找 也可以用 whereis find 文件查找&#xff0c;针对文件名 locate 文件查找&#xff0c;依赖数据库alias 别名 alias yyy ls --colorauto -l yyy //相当于别名 查看文件which ls //查找ls 命令位置 whereis vim //也是查找命令locate …