【算法】利用分治思想解算法题:快排、归并、快速选择实战(C++)

1. 分治思想 介绍

分治法将问题划分成多个相互独立且相同或类似的子问题,然后递归地解决每个子问题,并将结果合并以得到原始问题的解。

分治思想通常包含以下三个步骤:

  1. 分解:将原始问题划分成多个规模较小、相互独立且类似的子问题。这个步骤可以通过递归方法实现。
  2. 解决:递归地解决每个子问题。当子问题足够小而可以直接求解时,使用简单的方法解决。
  3. 合并:将各个子问题的解合并,得到原始问题的解。

核心思想

是将一个复杂的问题分解成多个简单的子问题,通过递归地解决子问题,最终将子问题的解合并成原始问题的解

我们也遇到过一些,例如:归并排序、快速排序、二叉树的遍历等。

优缺点

优点在于能够有效地降低问题的复杂度,提高算法的效率。缺点在于需要额外的空间和时间来处理子问题的合并过程。


2. 分治思想 引入

75.颜色分类

在这里插入图片描述
思路

  • 题意分析:数组中只有三种元素0、1、2,要求按照012的顺序对数组进行原地排序
  • 解法一:排序
    • 我们想当然的可以想到用排序解决,这个解法也不用多说
  • 解法二:三指针划分数组
    • 数组中只有三种元素,我们利用三个指针
      在这里插入图片描述
  • 如上图所示

代码

void sortColors(vector<int>& nums) {// 三个指针划分区域// [0, left-1] : 0// [left, i] : 1// [i, right-1] : 未检测// [right, n-1] : 2int left = -1, i = 0, right = nums.size();while(i < right) // i,right相遇,全部检索完成{if(nums[i] == 0){swap(nums[++left], nums[i++]);}else if(nums[i] == 1){++i;}else{swap(nums[--right], nums[i]);}}
}

3. 分治 - 快排思想

912.排序数组-快排

思路

  • 解法:快速排序
    在这里插入图片描述
    • 如上图所示,快排本质就是通过一个key值将数组不断的分类排序,当所有子数组(左右区间)排序完毕后,向上返回,完成还原的操作
    • 细节注意:我们使用区间范围内的随机值作为key值,可以避免最坏情况,并均匀分割。

代码

class Solution {
public:// 获取随机数int getRandom(vector<int>& nums, int left, int right){int r = rand();return nums[r % (right - left) + left];}// 快排void qsort(vector<int>& nums, int l, int r){if(l >= r)  return;int left = l - 1, i = l, right = r + 1;int key = getRandom(nums, l, r);while(i < right){if(nums[i] < key)swap(nums[++left], nums[i++]);else if(nums[i] == key)i++;elseswap(nums[--right], nums[i]);}// [l, left] [left+1, right-1] [right,r]qsort(nums, l, left);qsort(nums, right, r);} vector<int> sortArray(vector<int>& nums) {// 快排 + 三数划分 + 随机数优化srand(time(NULL)); // 设置随机数种子qsort(nums, 0, nums.size() - 1);return nums;}
};

215.数组中的第K个最大元素

在这里插入图片描述

4. 分治 - 快速选择

215.数组中的第K个最大元素

思路

我们知道堆排序可以用于解决topk问题,时间复杂度为O(nlogn),这里在引入快速选择算法,一样可以用于解决topK问题,时间复杂度为O(n)

  • 解法:快速选择算法
    在这里插入图片描述
    • 如图所示,依然利用三数划分数组的思想:
    • 我们将数组划分为三部分,求出每一部分的长度
    • 根据k与数组长度关系,找到正确的区间,直到找到正确值

代码

class Solution {
public:// 获取随机数int getRandom(vector<int>& nums, int left, int right){ return nums[rand() % (right - left + 1) + left];}// 快排int qsort(vector<int>& nums, int l, int r, int k){if(l == r)  return nums[l];int left = l - 1, i = l, right = r + 1;int key = getRandom(nums, l, r);while(i < right){if(nums[i] < key) swap(nums[++left], nums[i++]);else if(nums[i] > key) swap(nums[--right], nums[i]);else i++;}// 找第k大的数// [l, left] [left + 1, right - 1] [right, r]// 区间元素个数: a b c// int a = left - l + 1;int b = (right - 1) - (left + 1) + 1, c = r - right + 1;if(c >= k) return qsort(nums, right, r, k); // 右区间 第k位else if(b + c >= k) return key;// else if(a >= k) qsort(nums, l, left, k-b-c);else return qsort(nums, l, left, k-b-c);}int findKthLargest(vector<int>& nums, int k) {// 快速选择算法srand(time(NULL));return qsort(nums, 0, nums.size() - 1, k);}
};

面试题17.14.最小K个数

在这里插入图片描述

思路

  • 题意分析:题目要求设计算法来返回数组中最小的k个数,我们可以使用堆排序,或者快速选择算法
  • 解法:排序 / 堆 / 快速选择
    • 对于解决类似的题,都可以有上述三种做法,但由于上一题题目要求O(n)的时间复杂度,所以选择快速选择算法
    • 这里我们依然以此思想解题
      在这里插入图片描述

代码

class Solution {
public:int getRandom(vector<int> &arr, int left, int right){return arr[rand() % (right - left + 1) + left];}void qsort(vector<int>& arr, int l, int r, int k){if(l >= r) return;int left = l - 1, i = l , right = r + 1;int key = getRandom(arr, l, r);while(i < right){if(arr[i] < key) swap(arr[++left], arr[i++]);else if(arr[i] > key) swap(arr[--right], arr[i]);else i++;}// [l, left] [left+1, right-1] [right, r]// 左区间长度: a | 中间区间长度: bint a = left - l + 1, b = right - left - 1;if(a > k) qsort(arr, l, left, k);else if(a + b >= k) return;else qsort(arr, right, r, k - a - b);}vector<int> smallestK(vector<int>& arr, int k) {// 三数划分 + 随机数作key + 快速选择srand(time(NULL)); // 设置随机数种子qsort(arr, 0, arr.size()-1, k);return vector<int>(arr.begin(), arr.begin() + k);}
};

5. 分治 - 归并思想

912.排序数组_归并

在这里插入图片描述

思路

在这里插入图片描述

在这里插入图片描述

  • 根据上图对比,我们知道归并与快排的区别,但两者都运用了分治的思想
  • 解法:归并
    1. 首先根据mid值递归划分数组
    2. 后向上返回,需要将两有序数组合并:
      • 通过两指针遍历数组,依次比较合并数组
      • 循环结束后将两数组中未合并的数添加
    3. 最后将合并后的数组添加到num中
    • 细节注意:我们使用全局数组tmp,用于节省递归多次创建的时间开胸啊

代码

class Solution {
public:vector<int> tmp; // 临时数组 当递归创建多次时,全局遍历节省时间开销void mergeSort(vector<int>& nums, int left, int right){if(left >= right) return;int mid = left + (right - left) / 2;// int mid = (left + right) >> 1;// 1. 数组划分mergeSort(nums, left, mid);mergeSort(nums, mid + 1, right);// 2. 合并数组int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right)tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];// 2.5 处理未遍历完的数while(cur1 <= mid) tmp[i++] = nums[cur1++];while(cur2 <= right) tmp[i++] = nums[cur2++];// 3. 将元素写入到nums中for(int i = left; i <= right; ++i)nums[i] = tmp[i - left];}vector<int> sortArray(vector<int>& nums) {// 归并排序tmp.resize(nums.size());mergeSort(nums, 0, nums.size() - 1);return nums;}
};

LCR170.交易逆序对的总数

在这里插入图片描述

思路

  • 解法一:暴力枚举

    • 首先想到的当然是暴力解法,用两层for循环
    • 外层for循环遍历数组每次固定一个数,内层for循环遍历数组寻找比其小的数。
  • 解法二:归并
    在这里插入图片描述

  • 归并

    • 我们有两种策略,即排序时选择1. 升序 2. 降序
      在这里插入图片描述

    在这里插入图片描述

    • 我们选用策略1,即升序排序解题:
      1. 首先根据mid划分数组,递归左右部分并将逆序对个数加到ret中
      2. 对左右部分执行排序操作,当nums[cur1] > nums[cur2] 时,更新ret
      3. 将未合并的元素添加
      4. 最后合并数组

代码

class Solution {
public:vector<int> tmp;// 归并排序 + 计算逆序对个数int mergeSort(vector<int>& nums, int left, int right){if(left >= right) return 0; // 划分数组int mid = left + (right - left) / 2;int ret = 0; // 结果ret += mergeSort(nums, left, mid);ret += mergeSort(nums, mid + 1, right);// 左边部分 排序 右边部分 排序int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2])tmp[i++] = nums[cur1++]; // 排序else{ret += mid - cur1 + 1; // 更新结果 + 排序tmp[i++] = nums[cur2++];}}// 将未合并元素加上while(cur1 <= mid)  tmp[i++] = nums[cur1++];while(cur2 <= right)  tmp[i++] = nums[cur2++];// 还原数组for(int i = left; i <= right; ++i)nums[i] = tmp[i - left];return ret;}   int reversePairs(vector<int>& record) {tmp.resize(record.size());return mergeSort(record, 0, record.size()-1);}
};

315.计算右侧小于当前元素的个数

在这里插入图片描述

思路

  • 题意分析:题目要求找到数组nums中 “每一位其右侧小于该位的个数”,且将该个数存放到新数组count中

  • 解法一:暴力枚举

    • 很干脆,两层for循环,前固定数,后遍历从该数到数组末尾的元素,寻找小于自己的个数,统计到count中
  • 解法二:归并
    在这里插入图片描述

    • 根据题意,我们首先创建两个全局数组分别存放临时下标与临时元素,并用index数组保存nums中所有元素下标。
    1. 根据中点划分数组 + 向左右部分递归(找左右部分 满足条件的数)
    2. 通过两指针cur1、cur2遍历数组,进行比较(一左一右)
      • 随后执行上图操作
      • 当cur1元素 > cur2元素,此时cur2后所有元素都是小于自己的,更新结果,并记录cur1元素的下标与值
      • 当cur1元素 <= cur2元素,cur2继续向右移,找符合条件的值

代码

class Solution {
public:vector<int> ret; // 结果数组vector<int> index; // 记录元素移动前原始下标int tmpIndex[100001]; // 临时存放下标int tmpNums[100001]; // 临时存放元素void mergeSort(vector<int>& nums, int left, int right){if(left >= right) return;// 根据中点划分数组int mid = left + (right-left) / 2;// 递归排序+找数mergeSort(nums, left, mid);mergeSort(nums, mid + 1, right);// 具体排序找数过程int cur1 = left, cur2 = mid + 1, i = 0;while(cur1 <= mid && cur2 <= right){if(nums[cur1] <= nums[cur2]){tmpNums[i] = nums[cur2];tmpIndex[i++] = index[cur2++];}else{ret[index[cur1]] += right - cur2 + 1;tmpNums[i] = nums[cur1];tmpIndex[i++] = index[cur1++];}}// 排序剩余元素while(cur1 <= mid){tmpNums[i] = nums[cur1];tmpIndex[i++] = index[cur1++];}while(cur2 <= right){tmpNums[i] = nums[cur2];tmpIndex[i++] = index[cur2++];}// 还原数组for(int j = left; j <= right; ++j){nums[j] = tmpNums[j-left]; // tmpNums是从0开始的,所以这里还原从0-leftindex[j] = tmpIndex[j-left];}}vector<int> countSmaller(vector<int>& nums) {// 哈希思想int n = nums.size();ret.resize(n);index.resize(n);// 初始化index数组 保存原始下标for(int i = 0; i < n-1; ++i)index[i] = i;mergeSort(nums, 0, n-1);return ret;}
};

493.翻转对

在这里插入图片描述

思路

  • 解法一:暴力枚举

    • 两层for循环,外层循环每次固定一位数,内层循环遍历数组判断是否nums[i] > nums[j] *
  • 解法二:归并

    1. 首先关于计算翻转对的操作:
      • 利用单调性,使用同向双指针
    2. 同前面的题一样,这里有两种策略:
      • 策略一:升序
        • 计算在当前元素前,有多少元素的两倍小于自己
      • 策略二:降序
        • 计算在当前元素前,有多少元素的一半大于自己

代码

class Solution {
public:vector<int> tmp;int mergeSort(vector<int>& nums, int left, int right) {if (left >= right) return 0;int ret = 0;// 先计算左右两侧的翻转对int mid = left + (right - left) / 2;ret += mergeSort(nums, left, mid);ret += mergeSort(nums, mid + 1, right);// 计算翻转对操作int cur1 = left, cur2 = mid + 1;while(cur1 <= mid){while(cur2 <= right && (nums[cur1] / 2.0 <= nums[cur2])) cur2++; // 找到符合条件的cur2位置,用*可能溢出if(cur2 > right) break;ret += right - cur2 + 1;++cur1;}// 具体排序// 降序判断cur1 = left, cur2 = mid + 1;int i = 0;while(cur1 <= mid && cur2 <= right)tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];// 排序剩余元素while(cur1 <= mid) tmp[i++] = nums[cur1++];while(cur2 <= right) tmp[i++] = nums[cur2++];// 还原数组for(int j = left; j <= right; ++j)nums[j] = tmp[j - left];return ret;}int reversePairs(vector<int>& nums) {tmp.resize(nums.size());return mergeSort(nums, 0, nums.size() - 1);}
};

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Gradle系列之大锅菜汇总

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。 &#x1f60a; 座右铭&#xff1a;不…

【程序员的自我修养08】精华!!!动态库的由来及其实现原理

绪论 大家好&#xff0c;欢迎来到【程序员的自我修养】专栏。正如其专栏名&#xff0c;本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度&#xff0c;我会尽自己最大的努力&#xff0c;争…

opencv003图像裁剪(应用NumPy矩阵的切片)

这一部分相对于马上要学习的二值化是要更更更简单一些的&#xff0c;只需三行&#xff0c;便能在opencv上裁剪图像啦&#xff08;顺便云吸猫&#xff0c;太可爱了&#xff01;&#xff09; 出处见水印&#xff01; 1、复习图像的显示 前几天期末考试&#xff0c;太久没有看…

QML 中自定义虚拟键盘

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 我们知道 Qt 中虚拟键盘模块遵循的是 GPL 协议&#xff0c;是不可用于商业发布的。如果项目中使用了 Qt 自带的虚拟键盘&#xff…

maven、springboot项目编译打包本地jar、第三方jar包

0. 引言 一般我们在maven项目中都是通过引入pom坐标的形式来引入第三方jar包&#xff0c;但某些场景下&#xff0c;第三方是直接提供的jar包文件&#xff0c;这就需要我们从本地引入第三方包并进行打包。所以我们今天来看下如何进行本地引入第三方包操作 1. 步骤 1、在项目下…

JVM是如何基于虚拟机栈运行的

众所周知&#xff1a;JVM执行Java代码是靠执行引擎实现的。执行引擎有两套解释器&#xff1a;字节码解释器、模板解释器。字节码解释器比较简单&#xff0c;不多说&#xff0c;看图。本篇文章咱们讨论模板解释器执行Java代码的底层原理。 早些年研究模板解释器看到R大用汇编写的…

你知道vue中key的原理吗?说说你对它的理解

一、Key是什么 开始之前&#xff0c;我们先还原两个实际工作场景 当我们在使用v-for时&#xff0c;需要给单元加上key <ul><li v-for"item in items" :key"item.id">...</li> </ul>用new Date()生成的时间戳作为key&#xff0c…

数据中心网络架构

参考&#xff1a; 一文读懂胖树 数据中心网络架构VL2详解 数据中心网络拓扑设计目标 总体目标 业务可以部署在任意的服务器上可以根据需要动态扩展或者缩小服务器规模 网络角度 均衡负载且高性能&#xff1a;服务器之间的性能仅受限于服务器网卡&#xff0c;而不是链路性能…

《Ensemble deep learning: A review》阅读笔记

论文标题 《Ensemble deep learning: A review》 集成深度学习&#xff1a; 综述 作者 M.A. Ganaie 和 Minghui Hu 来自印度理工学院印多尔分校数学系和南洋理工大学电气与电子工程学院 本文写的大而全。 初读 摘要 集成学习思想&#xff1a; 结合几个单独的模型以获得…

【GitHub】ssh: connect to host github.com port 22: Connection refused

本地使用git上传GitHub仓库时发现的一个报错&#xff0c;以为是本机连不上github了&#xff0c;ping过后发现能够正常访问&#xff0c;于是上网找到了一个很完美的解决方案 原因&#xff1a;22端口被占用或被防火墙屏蔽 解决方法&#xff1a;切换GitHub的443端口 1.首先找到…

【数据结构】手撕排序(排序的概念及意义、直接插入和希尔排序的实现及分析)

目录 一、排序的概念及其运用 1.1排序的概念 1.2排序运用 1.3 常见的排序算法 二、插入排序 2.1基本思想&#xff1a; 2.2直接插入排序&#xff1a; 2.3步骤&#xff1a; 2.4直接插入排序的实现 三、希尔排序( 缩小增量排序 ) 3.1希尔排序的发展历史 3.2 希尔…