算法沉淀——分治算法(leetcode真题剖析)

在这里插入图片描述

算法沉淀——分治算法

  • 快排思想
    • 01.颜色分类
    • 02.排序数组
    • 03.数组中的第K个最大元素
    • 04.库存管理 III
  • 归并思想
    • 01.排序数组
    • 02.交易逆序对的总数
    • 03.计算右侧小于当前元素的个数
    • 04.翻转对

分治算法是一种解决问题的算法范式,其核心思想是将一个大问题分解成若干个小问题,递归地解决这些小问题,最后将它们的解合并起来得到原问题的解。分治算法的一般步骤包括分解(Divide)、解决(Conquer)、合并(Combine)。

具体来说,分治算法包含以下几个步骤:

  1. 分解(Divide): 将原问题分解成若干个规模较小、相互独立的子问题。这一步通常是问题规模的减小或者数据规模的缩小。
  2. 解决(Conquer): 递归地解决这些子问题。对于规模较小的子问题,可以直接求解。
  3. 合并(Combine): 将子问题的解合并起来,得到原问题的解。

分治算法通常适用于能够被划分成相互独立子问题的问题,并且这些子问题的结构和原问题一样。经典的分治算法有许多,如归并排序、快速排序、二分搜索等。

经典例子:归并排序

  1. 分解(Divide): 将待排序的数组分成两半。
  2. 解决(Conquer): 对每个子数组进行归并排序,递归地进行排序。
  3. 合并(Combine): 合并已排序的子数组,得到最终的排序结果。

分治算法的优点包括:

  • 模块化设计: 将问题分解成小问题,使得算法结构清晰,易于理解和实现。
  • 可并行性: 分治算法通常适用于并行计算,因为子问题可以独立地求解。
  • 适用范围广: 适用于一类问题,如排序、查找等。

快排思想

01.颜色分类

题目链接:https://leetcode.cn/problems/sort-colors/

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i]012

思路

具体的思路可以分为以下三个部分:

  1. 红色部分(0): 通过交换,保证红色元素的右边界 left 的左侧都是红色元素。初始时,left 设置为-1。
  2. 白色部分(1): 遍历过程中,遇到白色元素(1)时,直接将指针 i 向右移动,不进行交换。白色元素已经排列在红色元素的右侧,所以不需要额外操作。
  3. 蓝色部分(2): 通过交换,保证蓝色元素的左边界 right 的右侧都是蓝色元素。初始时,right 设置为数组的长度。

整个过程在遍历指针 i 小于右边界 right 的情况下进行。当 iright 相遇时,排序完成。

代码

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

02.排序数组

题目链接:https://leetcode.cn/problems/sort-an-array/

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

思路

普通快排在这里是通过不了的,所以我们可以使用上面颜色分类的思想进行三路划分的优化

三路划分是对传统快速排序算法的一种改进,通过将数组划分为三个部分:小于、等于、大于基准值,从而在存在大量相同元素的情况下,提高了性能。

传统快速排序在处理有大量相同元素的数组时可能会导致不均匀的划分,使得递归树不平衡,进而影响性能。三路划分通过在划分过程中将数组分为小于、等于、大于基准值的三个部分,有效地解决了这一问题,具有以下优势:

  1. 减少重复元素的递归处理: 在存在大量相同元素的情况下,传统快速排序可能导致递归深度较大,而三路划分能够将相同元素聚集在一起,从而减少递归深度。
  2. 避免不必要的交换: 在传统快速排序中,可能会进行多次相同元素的交换,而三路划分通过将相同元素聚集在一起,避免了不必要的交换操作,提高了性能。
  3. 适用于含有大量重复元素的场景: 当数组中存在大量相同元素时,三路划分能够更好地利用重复元素的信息,提高排序效率。

三路划分的核心思想是通过一个循环,将数组划分为小于、等于、大于基准值的三个部分。这样,相同元素被聚集在等于基准值的部分,从而在递归过程中能够更高效地处理重复元素。这一优化使得算法在处理包含大量相同元素的数组时,性能更为稳定。

代码

class Solution {
public:int getRandom(vector<int>& nums,int left, int right){return nums[rand()%(right-left+1)+left];}void qsort(vector<int>& nums,int l, int r){if(l>=r) return;int key=getRandom(nums,l,r);int i=l,left=l-1,right=r+1;while(i<right){if(nums[i]<key) swap(nums[++left],nums[i++]);else if(nums[i]==key) i++;else swap(nums[--right],nums[i]);}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;}
};

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

题目链接:https://leetcode.cn/problems/kth-largest-element-in-an-array/

给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

提示:

  • 1 <= k <= nums.length <= 105
  • -104 <= nums[i] <= 104

思路

这里最常规的写法应该是使用堆排,但是这样达不到O(n)的时间复杂度,所以这里我们结合快排中的三路划分思想

代码

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];// 1. 随机选择基准元素int key = getRandom(nums, l, r);// 2. 根据基准元素将数组分为三块int left = l - 1, right = r + 1, i = l;while (i < right) {if (nums[i] < key) {swap(nums[++left], nums[i++]);} else if (nums[i] == key) {i++;} else {swap(nums[--right], nums[i]);}}// 3. 分情况讨论int c = r - right + 1, b = right - left - 1;if (c >= k) {// 第 k 大元素在右侧部分return qsort(nums, right, r, k);} else if (b + c >= k) {// 第 k 大元素等于基准元素return key;} else {// 第 k 大元素在左侧部分return qsort(nums, l, left, k - b - c);}}int getRandom(vector<int>& nums, int left, int right) {return nums[rand() % (right - left + 1) + left];}
};
  1. 计算左、右和基准三个部分的元素个数:
    • c 表示右侧部分元素的个数,即大于基准元素的个数。
    • b 表示基准元素左侧部分元素的个数,即等于基准元素的个数。
  2. 判断第 k 大元素的位置:
    • 如果右侧部分元素个数 c 大于等于 k,说明第 k 大元素在右侧部分。因此,递归地在右侧部分中继续寻找第 k 大元素。
    • 如果 b + c 大于等于 k,说明第 k 大元素等于基准元素。此时,基准元素即为所求的第 k 大元素,直接返回基准元素的值。
    • 如果以上两个条件都不满足,说明第 k 大元素在左侧部分。因此,递归地在左侧部分中继续寻找第 k 大元素,同时将 k 减去右侧和基准元素的个数。

这样的划分和递归过程保证了在不同情况下都能正确地找到第 k 大元素,从而完成整个算法。这是随机化快速排序在选择第 k 大元素时的一种处理策略,通过考虑基准元素左右两侧的元素个数,提高了算法在寻找第 k 大元素时的效率。

04.库存管理 III

题目链接:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/

仓库管理员以数组 stock 形式记录商品库存表,其中 stock[i] 表示对应商品库存余量。请返回库存余量最少的 cnt 个商品余量,返回 顺序不限

示例 1:

输入:stock = [2,5,7,4], cnt = 1
输出:[2]

示例 2:

输入:stock = [0,2,3,6], cnt = 2
输出:[0,2] 或 [2,0]

提示:

  • 0 <= cnt <= stock.length <= 10000 0 <= stock[i] <= 10000

思路

这一题和上一题的思路基本一致,同样我们使用快速选择的算法,可以使时间复杂度达到O(n),只不过需要简单做一些调整

代码

class Solution {
public:void qsort(vector<int>& nums, int l, int r, int k) {if (l >= r) return;// 随机选择基准元素int key = nums[rand() % (r - l + 1) + l];int left = l - 1, right = r + 1, i = l;// 划分过程while (i < right) {if (nums[i] < key) {swap(nums[++left], nums[i++]);} else if (nums[i] == key) {i++;} else {swap(nums[--right], nums[i]);}}int a = left - l + 1, b = right - left - 1;// 根据划分情况递归处理if (a > k) {// 第 k 小元素在左侧部分qsort(nums, l, left, k);} else if (a + b >= k) {// 第 k 小元素在基准元素右侧,且可能包含部分基准元素return;} else {// 第 k 小元素在右侧部分qsort(nums, right, r, k - a - b);}}vector<int> inventoryManagement(vector<int>& stock, int cnt) {srand(time(NULL));// 调用随机化快速排序qsort(stock, 0, stock.size() - 1, cnt);// 返回前 cnt 小的商品return {stock.begin(), stock.begin() + cnt};}
};

归并思想

01.排序数组

题目链接:https://leetcode.cn/problems/sort-an-array/

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

提示:

  • 1 <= nums.length <= 5 * 104
  • -5 * 104 <= nums[i] <= 5 * 104

思路

要理解分治中的归并思想,首先我们从归并排序入手,这里我直接编写代码,想看更清晰的排序剖析,可以翻看博主之前关于八大排序的博客

代码

class Solution {vector<int> tmp;
public:vector<int> sortArray(vector<int>& nums) {tmp.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 = (right + left) >> 1;// 递归对左右两部分进行归并排序mergeSort(nums, left, mid);mergeSort(nums, mid + 1, right);// 归并合并两个有序部分int cur1 = left, cur2 = mid + 1, i = 0;while (cur1 <= mid && cur2 <= right)tmp[i++] = (nums[cur1] <= nums[cur2]) ? nums[cur1++] : 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];}
};

02.交易逆序对的总数

题目链接:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/

在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。

示例 1:

输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。 

限制:

0 <= record.length <= 50000

思路

这里我们使用归并的思想可以对数组边排序边进行逆序对的计算,我们在进行归并排序划分时,左边和右边都是相对有序的,我们在归并时,找到了左边相对右边大的那个数,就可以进行一次逆序对的组合,即此时左边被遍历的数及其之后的数都能和此时右边的数进行逆序匹配,此时我们累加逆序对的值,直到我们把整个数组归并完毕,逆序对的总数也就计算完毕了

代码

class Solution {int tmp[50000];
public: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, 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;}
};

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

题目链接:https://leetcode.cn/problems/count-of-smaller-numbers-after-self/

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

示例 1:

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

示例 2:

输入:nums = [-1]
输出:[0]

示例 3:

输入:nums = [-1,-1]
输出:[0,0]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

思路

我们可以继续利用上面的逆序对思想,只不过我们需要使用额外的数组来记录相对下标。

代码

class Solution {vector<int> ret;vector<int> index;int tmp[500000];int tindex[500000];
public:vector<int> countSmaller(vector<int>& nums) {int n=nums.size();ret.resize(n);index.resize(n);for(int i=0;i<n;i++)  index[i]=i;mergeSort(nums,0,n-1);return ret;}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,i=0;while(cur1<=mid&&cur2<=right){if(nums[cur1]<=nums[cur2]){tmp[i]=nums[cur2];tindex[i++]=index[cur2++];}else{ret[index[cur1]]+=right-cur2+1;tmp[i]=nums[cur1];tindex[i++]=index[cur1++];}}while(cur1<=mid){tmp[i]=nums[cur1];tindex[i++]=index[cur1++];}while(cur2<=right){tmp[i]=nums[cur2];tindex[i++]=index[cur2++];}for(int j=left;j<=right;j++){nums[j]=tmp[j-left];index[j]=tindex[j-left];}}
};

04.翻转对

题目链接:https://leetcode.cn/problems/reverse-pairs/

给定一个数组 nums ,如果 i < jnums[i] > 2*nums[j] 我们就将 (i, j) 称作一个*重要翻转对*

你需要返回给定数组中的重要翻转对的数量。

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

思路

总体思路依旧是使用归并,我们在每次排序前,找到当前的左边某个数大于右边的两倍,即可一次性计算该数后面的翻转对个数,数组排序完成,即可计算全部的翻转对

代码

class Solution {int tmp[50000];
public: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,i=left;while(cur1<=mid){while(cur2<=right&&nums[cur2]>=nums[cur1]/2.0) cur2++;if(cur2>right) break;ret+=right-cur2+1;cur1++;}cur1=left,cur2=mid+1;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];return ret;}
};

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

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

相关文章

去除vue自带的边距

使用vue时发现总有去不掉的外边距&#xff0c;在index.vue里面怎样设置样式都不管用 查阅资料后发现要在vue项目自带的index.html文件内添加下面的样式代码才行 <style>*{margin: 0;padding: 0;}body,html{margin: 0;padding: 0;} </style>

揭秘外观模式:简化复杂系统的关键设计策略

前言 外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它隐藏了系统的复杂性&#xff0c;并向客户端提供了一个可以访问系统的接口。这种类型的设计模式向现有的系统添加一个接口&#xff0c;来隐藏系统的复杂性。这种模式涉及到一个单一的类…

全国计算机技术与软件专业技术资格(水平)考试软考中级高级报名步骤

第一步&#xff1a; 1、登陆全国的计算机网上报名平台 http://bm.ruankao.org.cn/sign/welcome 根据自己所在地区&#xff0c;选择报考城市入口&#xff08;例如&#xff1a;北京考区考生&#xff0c;直接选择北京就 可以&#xff09; 第二步&#xff1a;用户登录 1、已有…

数据湖的整体思路

湖本质上是一个集中化&#xff0c;中心化的&#xff0c;一体化的存储技术&#xff0c;并且在其之上追求技术架构的统一化&#xff0c;如流批一体&#xff0c;服务分析一体化。 当数据湖成为中心&#xff0c;那么就可以围湖而建“数据服务环”&#xff0c;环上的服务包括了数仓、…

微服务多级缓存

多级缓存 1.什么是多级缓存 传统的缓存策略一般是请求到达Tomcat后&#xff0c;先查询Redis&#xff0c;如果未命中则查询数据库&#xff0c;如图&#xff1a; 存在下面的问题&#xff1a; •请求要经过Tomcat处理&#xff0c;Tomcat的性能成为整个系统的瓶颈 •Redis缓存…

【Docker】私有仓库

目录 1.搭建 2. 上传镜像 3.拉取镜像 1.搭建 1.拉取私有仓库的镜像 docker pull registry 2.创建私有仓库容器 docker run -id --nameregistry -p 5000:5000 registry 3.打开浏览器,输入地址&#xff08;http:私有仓库服务器ip:5000/v2/_catalog&#xff09; 出现如图表示私…

【程序设计竞赛】C++与Java的细节优化

必须强调下&#xff0c;以下的任意一种优化&#xff0c;都应该是在本身采用的算法没有任何问题情况下的“锦上添花”&#xff0c;而不是“雪中送炭”。 如果下面的说法存在误导&#xff0c;请专业大佬评论指正 读写优化 C读写优化——解除流绑定 在ACM里&#xff0c;经常出现…

Spring Boot 笔记 008 创建接口_获取用户信息

1.1.1 编写userinfo接口 1.1.2 User实体类中增加转json忽略password注释 package com.geji.pojo;import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data;import java.time.LocalDateTime;//lombok 在…

使用matplotlib库在Python中绘制折线图

一.使用matplotlib库在Python中绘制一个简单的折线图 # codingutf-8 from matplotlib import pyplot as plt # 导入matplotlib库中的pyplot模块&#xff0c;并重命名为plt# 设置图片大小&#xff0c;dpi是每英寸点数&#xff0c;figsize是图像的宽和高 plt.figure(figsize(20…

网络协议与攻击模拟_17HTTPS 协议

HTTPShttpssl/tls 1、加密算法 2、PKI&#xff08;公钥基础设施&#xff09; 3、证书 4、部署HTTPS服务器 部署CA证书服务器 5、分析HTTPS流量 分析TLS的交互过程 一、HTTPS协议 在http的通道上增加了安全性&#xff0c;传输过程通过加密和身份认证来确保传输安全性 1、TLS …

vue3 中使用pinia 数据状态管理(在Taro 京东移动端框架中的使用)

1.pinia 介绍 pinia 是 Vue 的存储库&#xff0c;它允许您跨组件/页面共享状态。就是和vuex一样的实现数据共享。 依据Pinia官方文档&#xff0c;Pinia是2019年由vue.js官方成员重新设计的新一代状态管理器&#xff0c;更替Vuex4成为Vuex5。 Pinia 目前也已经是 vue 官方正式的…

单页404源码

<!doctype html> <html> <head> <meta charset"utf-8"> <title>简约 404错误页</title><link rel"shortcut icon" href"./favicon.png"><style> import url("https://fonts.googleapis.co…