二分力扣题
一:搜索二维矩阵
74. 搜索二维矩阵
按照题意:直接利用二维数组转换成一维数组进行求解
方法一:普通等于的二分查找
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
this->m = matrix.size();
this->n = matrix[0].size();
int l=0;
int r=m*n-1; //(m-1)*n+(n-1) r坐标:(m-1,n-1)
int mid;
while(l<=r){
mid=(l+r)>>1;
auto [i,j]=getIndex(matrix,mid);
//三种分支
if(matrix[i][j]<target) l=mid+1;
else if(matrix[i][j]>target) r=mid-1;
else return true;//==
}
//没找到
return false;
}
private:
int m, n;
// 一维转换二维
pair<int, int> getIndex(vector<vector<int>>& matrix, int index) {
return {index / n, index % n};
}
};
方法二:按照求最后一个等于
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
this->m = matrix.size();
this->n = matrix[0].size();
int l=0;
int r=m*n-1; //(m-1)*n+(n-1) r坐标:(m-1,n-1)
int mid;
//最后一个等于(扩大l)
while(l<=r){
mid=(l+r)>>1;
auto [i,j]=getIndex(matrix,mid);
if(matrix[i][j]<=target){//最后一个<=
l=mid+1;
}else{
r=mid-1;
}
}
//r要求>=0
if(r<0) return false;
auto [i,j]=getIndex(matrix,r);
//下标已经合法了
return matrix[i][j]==target?true:false;
}
private:
int m, n;
// 一维转换二维
pair<int, int> getIndex(vector<vector<int>>& matrix, int index) {
return {index / n, index % n};
}
};
也可以按照第一个等于,注意if条件是“>=“
//第一个等于(缩小r) while(l<=r){ mid=(l+r)>>1; auto [i,j]=getIndex(matrix,mid); //合法就缩小r if(matrix[i][j]>=target){ r=mid-1; }else{ l=mid+1; } } //l要求<= ---(m*n-1) if(l>m*n-1) return false; auto [i,j]=getIndex(matrix,l);//注意是l //下标已经合法了 return matrix[i][j]==target?true:false;
二:不重复元素找最小值
153. 寻找旋转排序数组中的最小值
不是只有单调序列才能二分查找,只要题目属于分段单调的,而且能够找到分界点就可以二分。
本题就是一个不含重复元素的单调序列,把它的后半部分移到了数组开头位置。
比如:[1,3,5,7,9]->[7,9 || 1,3,5]
序列具备:两段都是
单调函数
,而且前一段序列值一定大于后一半想要找的最小值满足:
- 它一定比最右端元素小
- 而且是比最右端元素小的最小值
而且:比最右端元素大的值一定不是最小值
相当于:
找第一个<=最右端元素的值
//6789
// 1234
//找到第一个<=最右端的数
int findMin(vector<int>& nums) {
int l=0;
int r=nums.size()-1;
while(l<r){
int mid=(l+r)>>1;
if(nums[mid]<=nums[r]){
r=mid;//注意保留mid
}else{
l=mid+1;
}
}
return nums[l];//注意返回的是下标对应的值
}
也可以比较第一个值nums[0]
,但是要注意,如果[l,r]不存在转折,也就是满足递增提前判断
如果mid对应值>=
nums[0]
,最低谷一定在mid右端,l=mid+1如果mid对应值值<
nums[0]
,最低谷可能是mid或mid左边,r=mid
int findMin(vector<int>& nums) {
int l=0;
int r=nums.size()-1;
while(l<r){
//如果[l,r]是一段递增
if(nums[l]<=nums[r]){
return nums[l];
}
int mid=(l+r)/2;
//存在低谷
if(nums[mid]>=nums[0]){
l=mid+1;
}else{
r=mid;
}
}
return nums[l];
}
三:搜索旋转数组值
33. 搜索旋转排序数组
先确定mid再寻找值,确定mid与target无关,只跟两侧有关
本题的数组序列是递增数列的一部分移动到了前面
比如:1 3 5 7 9 -> 7 9 || 1 3 5 满足:两部分都递增,而且前一部分比后一部分大
解题时:
首先判断mid在哪一个递增分段上,有两种可能:
- 如果mid落在左分支,即:
nums[mid]>=nums[l]
此时:只需要判断target在红色还是不在红色
nums[l]<=target<nums[mid]
- 在红色,r=mid-1
- 不在红色,l=mid+1
- 如果mid落在右分支,即:
nums[mid]<nums[l]
同上面,判断target是否落在红色区间
nums[mid]<=target<nums[r]
- 在红色,l=mid+1
- 不在红色,r=mid-1
代码:
int search(vector<int>& nums, int target) {
int l = 0;
int r = nums.size() - 1;
while (l <= r) {
int mid = (l + r) >> 1;
// 返回结果
if (nums[mid] == target) {
return mid;
}
if (nums[mid] >= nums[l]) {
// 判断taget所在位置
if (target >= nums[l] && target < nums[mid]) {
r = mid - 1;
} else {
l = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[r]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
}
// 没找到
return -1;
}
还可以判断<=
while(l<=r){ int mid=(l+r)>>1; if(nums[mid]==target) return mid; else if(nums[mid]<=nums[r]){//mid if(nums[mid]<target && target<=nums[r]){ l=mid+1; }else{ r=mid-1; } }else{ if(target>=nums[l]&&nums[mid]>target){ r=mid-1; }else{ l=mid+1; } } }
也就是判断mid位置就两种
- 大于等于nums[l]
- 小于等于nums[r]
都只判断一个就可以确定mid
四:选举人
911. 在线选举
思路:
-
既然是统计大小,一定会用到查找表(set或map)
-
题目给出的是某一时刻投给了哪个人,那么
TopVotedCandidate()
计算:某一个时刻哪个人票数最多 -
然后
q(t)
计算是任意t时间点的获胜人数,只有某一时间点获胜人数的信息,所以二分查找:—最后一个<=t的时间点,然后直接返回对应获胜者即可
map统计频次,mp[人]=票数,用winners存储当前时间点获胜的人
class TopVotedCandidate {
public:
TopVotedCandidate(vector<int>& persons, vector<int>& times) {
this->times=times;
int maxWinner=0;//当前最大票数
for(auto& val:persons){
mp[val]++;//对应人票数+1
if(mp[val]>=mp[maxWinner]){
//题目要求相等取最近获胜者,需要更新
maxWinner=val;
}
winners.push_back(maxWinner);
}
}
int q(int t) {
//查找最后一个<=t的时刻
int l=0;
int r=times.size()-1;
while(l<r){
int mid=(l+r+1)>>1;//别忘了+1
if(times[mid]<=t){
l=mid;//扩大l
}else{
r=mid-1;
}
}
return winners[l];
}
private:
unordered_map<int,int> mp;//mp[选举人]=票数
vector<int> winners;//当前t时刻的获胜者
vector<int> times;//为q(t)传参
};
或者也可以定义最大值记录当前获胜者的最大票数和得到最大票数的优胜者
int maxVotes=0;//当前最大票数 int top=0;//要存入的获胜者 for(auto& val:persons){ mp[val]++;//对应人票数+1 if(mp[val]>=maxVotes){ //题目要求相等取最近获胜者,需要更新 maxVotes=mp[val]; top=val;//当前人 } winners.push_back(top); }
方法二:
直接计算:mp[当前时间点]=获胜者
,只是需要开辟新数组来记录最大值
class TopVotedCandidate {
public:
TopVotedCandidate(vector<int>& persons, vector<int>& times) {
int maxVotes=0;
votes=vector<int>(times.size());
for(int i=0;i<times.size();i++){
votes[persons[i]]++;//persons[i]这个人的票数+1
//因为出现i-1
if(i==0){
maxVotes=votes[persons[i]];
mp[times[i]]=persons[i];
continue;
}
if(votes[persons[i]]>=maxVotes){
maxVotes=votes[persons[i]];
//当前时刻的的票最多人
mp[times[i]]=persons[i];
}else{
mp[times[i]]=mp[times[i-1]];
}
}
}
int q(int t) {
while(!mp.count(t)){
t--;
}
return mp[t];
}
private:
//mp[time]=persons;
unordered_map<int,int> mp;
vector<int> votes;
};
/**
* Your TopVotedCandidate object will be instantiated and called as such:
* TopVotedCandidate* obj = new TopVotedCandidate(persons, times);
* int param_1 = obj->q(t);
*/
三分查找
问题:
已知函数 𝑓(𝑥) 在区间 [l,r] 上单峰且连续,求 𝑓(𝑥) 在 [l,r]上的极值
也就是说:三分查找适用于存在局部极大值或局部极小值的函数
来找到极值点
如图:在定义域[L,R]内部取两点
lmid
和rmid
,整个函数被这两个点分成了三块区域,通过这两个值的大小,来舍弃某一些区间来缩小查找范围
查找过程
有高峰的函数,只要满足:nums[lmid]<nums[rmid]
,那么极大值一定在lmid
的右边,也就是让l=lmid+1
;
同理:如果nums[rmid]<nums[lmid]
,极大值一定在rmid
左边,也就是:r=rmid-1
寻找峰值
162. 寻找峰值
为了能保证每次取舍一半区间,让[l,r]内部取的lmid和rmid取值为:
- lmid=(l+r)>>1,也就是中点
- rmid=lmid+偏移量,可以是lmid+1
当然lmid和rmid也可以是三等分点
int findPeakElement(vector<int>& nums) {
//这里题目一定会有解,l和r直接取两端即可
int l=0;
int r=nums.size()-1;
while(l<r){
//在[l,r]内部取lmid和rmid
int lmid=(l+r)>>1;
int rmid=lmid+1;
//进行区间取舍
if(nums[lmid]<=nums[rmid]){
l=lmid+1;
}else{
r=rmid-1;
}
}
return l;//r
}
这里的等号取不取,对本题而言没有影响
二分判定枚举
二分法还有一个重要应用是 枚举答案
,尤其是值域上的二分枚举。
注意:要先确定存在二段性,才可以用二分枚举答案
引例:猜数字
374. 猜数字大小
使用相加除二可能会溢出
mid = (l + r) / 2;
如果 l 和 r 足够大,mid值会导致int 数据类型越界,如果超出int的最大范围 那么结果变成负数
防溢出写法:
使用减法r - l不会超出最大的整型范畴
mid = l + (r - l)/2;
int guessNumber(int n) {
//数字一定在1~n中
int l=1;
int r=n;
//二分猜值
while(l<=r){
int mid=l+(r-l)/2;//防止溢出
int ans=guess(mid);//调用接口
//三种
if(ans==-1){//要猜的更小
r=mid-1;
}else if(ans==1){
l=mid+1;
}else{//==
return mid;
}
}
return -1;
}
可以选用第一个大于等于或最后一个小于等于,注意mid的向上和向下取值
int guessNumber(int n) { //数字一定在1~n中 int l=1; int r=n; //二分猜值 while(l<r){ int mid=l+(r-l)/2;//防止溢出 int ans=guess(mid);//调用接口 //找第一个== if(ans<=0){ r=mid; }else{ l=mid+1; } } return l; }
int guessNumber(int n) { //数字一定在1~n中 int l=1; int r=n; //二分猜值 while(l<r){ //向上取整 int mid=l+1+(r-l)/2;//防止溢出 int ans=guess(mid);//调用接口 //找第一个== if(ans>=0){ l=mid; }else{ r=mid-1; } } return l; }
对于一个最优化
问题:
- 求解:求出这个最优解
- 判定:判断这个解是否合法
p问题
就是能够多项式时间求出问题的解
np问题
就是能够多项式时间验证问题的解
二分答案法
如果解空间具有单调性,那么利用二分加上判定就相当于枚举整个问题的解,这种求出最优解的方法叫二分答案法
也就是通过
判定算法
来枚举
解空间,一样可以求出最优解比如:
猜数问题,在解空间每次选择中间值通过判定猜的数是大了还是小了,一样可以得到最终结果
一:运送包裹能力
1011. 在 D 天内送达包裹的能力
也就是想找到一个值W,这个值能满足在days天顺序运送完货物,而且W是最小的
这道题满足单调性,W值越大,那么越能在days天运送完货物
思路:首先写出判定函数isValid()
,该函数可以判定解的合法性,然后用二分查找结合该函数就能找到最小值
注意:
- W最小值就是所有货物取最大,也就是一下只运送一个货物,但是days会很长
- W最大值就是一次运送完所有货物,但是W值一定不是想要的最小值
- 判定函数:就是给定的W值能否满足:在days天内运送完所有货物。
说白了就是枚举W的所有可能,但是由于具备单调性,W能在days天运送完,那么W+1一定也能在days天内运完。所以可以利用二分枚举所有可能,然后来利用判定函数来取舍区间
解:
class Solution {
public:
int shipWithinDays(vector<int>& weights, int days) {
int l=0;
int r=0;
//初始化值,l取最小,r取最大
for(auto val:weights){
l=max(l,val);
r+=val;
}
//二分给定W值
while(l<r){
int mid=(l+r)>>1;
if(isValid(weights,days,mid)){
r=mid;//缩小mid值
}else{
l=mid+1;
}
}
return l;
}
private:
//给定值W,能否在days天内,完成所有物品运输
bool isValid(vector<int>& weights,int days,int W){
//尽量把货物塞满W,多了就下一天再搬(贪心)
int sum=0;
int time=1;//第一天
for(int i=0;i<weights.size();i++){
if(sum+weights[i]<=W){
sum+=weights[i];
}else{//这天超过了,开始下一天的搬运
time+=1;
sum=weights[i];
}
}
return time<=days;//只要time比规定时间小就完成
}
};
二:分割数组最大值
410. 分割数组的最大值
这道题其实和运送包裹完全一样。
思路:
- 给定一个用函数来判定:值不超过V的数字和划分一组,看组数是否小于等于k
- 然后利用二分来进行求解,取满足判定函数的最小mid值(缩小l)
class Solution {
public:
int splitArray(vector<int>& nums, int k) {
int l=0;
int r=0;
//最小l:每一个单组成组;最大r:只划分一组
for(auto& val:nums){
l=max(l,val);
r+=val;
}
//二分
while(l<r){
int mid=(l+r)>>1;
if(isValid(nums,k,mid)){
r=mid;
}else{
l=mid+1;
}
}
return l;
}
private:
//将nums以<=V划分组,看划分的个数是否<=k
bool isValid(vector<int>& nums,int k,int V){
int sum=0;
int time=1;//从第一组开始分
for(int i=0;i<nums.size();i++){
if(sum+nums[i]<=V){
sum+=nums[i];
}else{//重新划分新组
time++;
sum=nums[i];
}
}
return time<=k;
}
};
这里可以二分查找的原因:
解具备单调性
,因为假设划分k组后某组最大值是V,那么V+1,V+2,V+N也一定满足题目的要求。也就是说:可以用二分枚举V然后通过判定得到能符合要求的最小值。
假设划分k组后,最大值为V。那么划分k-n组一定满足最大值小于V。
题目要求:最大值最小,所以可以找到恰好符合的临界条件
三:制作花最少天数
1482. 制作 m 束花所需的最少天数
题目的解满足单调性:
假定T天就能完成k组m束花的要求,那么T+1,T+2,…也一定可以
假设T天可以采摘超过m束花,那么最优解可能在T-1,T-2,…里面取得
class Solution {
public:
int minDays(vector<int>& bloomDay, int m, int k) {
int MAX=(1e+9)+1;
int l=1;//第一天就全采摘了
int r=MAX;//题目给定最大+1
while(l<r){
int mid=(l+r)>>1;
if(isValid(bloomDay,m,k,mid)){
r=mid;//缩小r,因为求最短T
}else{
l=mid+1;
}
}
return l==MAX?-1:l;
}
private:
//给定时间T,问:能否在T天内采摘m束花,每束花必须满足连续相邻k朵
bool isValid(vector<int>& bloomDay,int m,int k,int T){
int continuous=0;//计算连续
int time=0;//初始化不能采摘
for(int i=0;i<bloomDay.size();i++){
if(bloomDay[i]<=T){//开花了
continuous++;
//满足条件更新结果
if(continuous==k){
time++;
continuous=0;
}
}else{
continuous=0;//断了,重新计数
}
}
return time>=m;//至少要采摘m
}
};
四:能吃完香蕉的最少天数
875. 爱吃香蕉的珂珂
也就是定义好速度V,然后二分枚举求出整个最小V
题目中:
如果
pile[i]<V
,也就是1h能吃V根数量多于该堆数量,那么取时间一小时所以相当于
pile[i]/V
向上取整可以利用余数来选择是否加一,也可以:
class Solution {
public:
int minEatingSpeed(vector<int>& piles, int h) {
int l=1;
int r=1e9;
while(l<r){
int mid=(l+r)>>1;
if(isValid(piles,h,mid)){
r=mid;//缩小r
}else{
l=mid+1;
}
}
return l;
}
private:
//给定速度V:在h小时内能否吃完所有香蕉
bool isValid(vector<int>& piles,int h,int V){
int hour=0;//千万别忘了初始值0
for(int i=0;i<piles.size();i++){
if(piles[i]%V==0){
hour+=piles[i]/V;
}else{
hour+=piles[i]/V+1;
}
}
return hour<=h;
}
};
计算hour还可以:
for(auto pile:piles){ hour+=pile/V+(pile%V==0?0:1); }
也可以:
for(int i=0;i<piles.size();i++){ hour+=(piles[i]+V-1)/V;// hour+=(piles[i]-1)/V+1; }
注意:
因为题目限制了珂珂一个小时之内只能选择一堆香蕉吃
,因此速度最大值就是这几堆香蕉中,数量最多的那一堆。
int r=1;
for(auto val:piles) r=max(r,val);
排序
基于比较的排序算法,时间复杂度:不能突破
O(NlogN)
简单证明:
- 比较排序
- 非比较排序
选插冒三种初级排序
选择排序
// 选择排序
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums[i], nums[minIndex]);
}
return nums;
}
插入排序
- 直接交换
//插入排序
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
for(int i=1;i<n;i++){//i从1开始,默认第一个有序
//结束条件是:j>0,因为访问j-1
for(int j=i;j>0 && nums[j]<nums[j-1];j--){
swap(nums[j],nums[j-1]);
}
}
return nums;
}
- 优化
可以不直接交换,记住值,然后后一个值等于前一个值,停止时,值改成最先记录的
//插入排序
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
for(int i=1;i<n;i++){//i从1开始,默认第一个有序
int e=nums[i];
int j;
for(j=i;j>0 && nums[j-1]>e;j--){
nums[j]=nums[j-1];
}
nums[j]=e;
}
return nums;
冒泡排序
把大的依次放到后面,每一次冒泡都会得到一个最大值,冒泡次数=数组长度-1
// 冒泡排序
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n - 1; i++) {//冒泡长度-1次
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) { // 大的放后面
swap(nums[j], nums[j + 1]);
}
}
}
return nums;
}
当然也可以从n-1开始,小的放前面
如果在一次遍历中没有进行任何交换,可以提前结束排序,因为这意味着数列已经排序完成。
优化:引入是否交换过的标志
// 冒泡排序
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
bool flag=false;//是否交换过
for(int i=0;i<n-1;i++){
for(int j=0;j<n-1-i;j++){
//大的后放
if(nums[j]>nums[j+1]){
swap(nums[j],nums[j+1]);
flag=true;
}
}
if(!flag) break;
}
return nums;
}
归并排序
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
mergeSort(nums,0,nums.size()-1);
return nums;
}
private:
void merge(vector<int>& nums,int l,int mid,int r){
int i=l;
int j=mid+1;
int k=0;
//创建aux存储合并结果
int* aux=new int[r-l+1];
while( i<=mid && j<=r){
if(nums[i]<nums[j]){
aux[k++]=nums[i++];
}else{
aux[k++]=nums[j++];
}
}
//判断while中是哪个条件不满足
while(i<=mid){
aux[k++]=nums[i++];
}
while(j<=r){
aux[k++]=nums[j++];
}
//把aux结果赋值给nums
//注意:aux:[0,r-l+1) nums:[l,r]
/*for(int i=0;i<r-l+1;i++){
nums[i+l]=aux[i];
}*/
for(int i=l;i<=r;i++){
nums[i]=aux[i-l];
}
}
void mergeSort(vector<int>& nums,int l,int r){
if(l==r) return;
int mid=(l+r)>>1;
//分
mergeSort(nums,l,mid);
mergeSort(nums,mid+1,r);
//治:合并两个结果
merge(nums,l,mid,r);
}
};
也可以改写merge
条件1:i<=mid
条件2:j<=r
if(条件1不满足)
else if(条件2不满足)
else if (此时判断)//此时条件1和条件2一定满足了
void merge(vector<int>& nums,int l,int mid,int r){ int i=l; int j=mid+1; //创建aux存储合并结果 int* aux=new int[r-l+1]; for(int k=0;k<r-l+1;k++){ if(i>mid){ aux[k]=nums[j++]; }else if(j>r){ aux[k]=nums[i++]; } //i<=mid && j<=r else if(nums[i]<nums[j]){ aux[k]=nums[i++]; }else{ aux[k]=nums[j++]; } } //还原nums //for(int i=l;i<=r;i++){ // nums[i]=aux[i-l]; //} for(int i=0;i<r-l+1;i++){ nums[i+l]=aux[i]; } }
for(int k=0;k<r-l+1;k++){ if(i<=mid && j<=r){ if(nums[i]<nums[j]){ aux[k]=nums[i++]; }else{ aux[k]=nums[j++]; } } else if(i<=mid){//j>r aux[k]=nums[i++]; }else{ aux[k]=nums[j++]; } }
初级排序的优化排序
堆排序
简洁实现,直接利用stl
由于是从小到大,可以存值的负数的大根堆,然后赋值取负号
vector<int> sortArray(vector<int>& nums) {
priority_queue<int> pq;
//首先建堆
for(int i=0;i<nums.size();i++){
pq.push(-nums[i]);
}
//出堆
for(int i=0;i<nums.size();i++){
nums[i]=-pq.top();//负号
pq.pop();
}
return nums;
}
当然直接建立小根堆也行: priority_queue<int,vector<int>,greater<int>> pq;
//大的沉底
定义采用向下调整函数
思路:
首先对数组原地调整成大顶堆
- 注意,因为数组从0开始,k的左孩子
k*2+1
右孩子k *2+2
- k的父节点:(k-1)/2
- 从最后一个父节点不断向上调整至根节点就可以得到堆
接下来要从小到大输出:
- 每次把堆顶元素往后放,然后从0开始重新调整堆
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
//从第一个非叶子结点向下调整,直至到根
for(int i=(n-2)/2;i>=0;i--){
shiftDown(nums,n,i);
}
//从小到大,把根顶后移
for(int i=n-1;i>0;i--){
swap(nums[0],nums[i]);
shiftDown(nums,i,0);//注意:[0,n-2],所以传n-1
}
return nums;
}
private:
//向下调整---数组长度,从k开始向下调整
void shiftDown(vector<int>& nums,int n,int k){
//注意:k*2+1是左孩子,k*2+2是右孩子
while(k*2+1<n){
int j=k*2+1;
if(j+1<n && nums[j+1]>nums[j]) j++;
//比较父节点和儿子最大结点j
if(nums[k]<nums[j]){
swap(nums[k],nums[j]);
}else{
break;
}
k=j;//k指向交换后的位置
}
}
};
优化:记录值,赋值代替交换
void shiftDown(vector<int>& nums,int n,int k){
//注意:k*2+1是左孩子,k*2+2是右孩子
int e=nums[k];
while(k*2+1<n){
int j=k*2+1;
if(j+1<n && nums[j+1]>nums[j]) j++;
//比较父节点和儿子最大结点j
if(nums[j]>e){
nums[k]=nums[j];//父=儿子
}else{
break;
}
k=j;//k指向交换后的位置
}
nums[k]=e;
}
希尔排序(不常用)
- 利用增量分组对插入排序进行优化
这里最后的0如果选用插入排序会跨越整个数组,影响算法性能
希尔排序
的做法:
先将元素进行分组,每次先在组内进行排序,尽量让元素可以在早期尽量多地移动。
选择分组的跨度是5,跨度是数组长度的一半。
相同颜色的元素为一组
在组内进行插入排序
接着将跨度缩小一半,从5变成2,接着重复上述逻辑
- 最后,跨度设为1,总体进行插入排序,得到结果。
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
int h=n/2;//假设以一半为增量
while(h>0){
for(int i=h;i<n;i++){//从增量往前插入
for(int j=i;j>=h && nums[j]<nums[j-h];j-=h){
swap(nums[j],nums[j-h]);
}
}
h=h>>1;//更新h
}
return nums;
}
这里增量先选取数组长度一半,然后每次缩小一半,直至为1
快速排序
- 快速排序也是一个基于分治的算法
从数组中选取一个
中轴元素
pivot
- 把小元素放在pivot左边
- 把大元素放在pivot右边
然后继续对左边和右边进行快速排序
归并排序:每次对半排序数组,排序好左右数组后,再合并成有序数组
快速排序:先把左右数组调配出,然后对左右数组进行排序
快速排序的分治:
也就是说:先把元素分成左右两部分后,然后再接着对左右两部分继续使用快排
对于:[L,R]的数组而言,当 L>=R停止递归
方法一:双指针法
这种方法:先移动i或者先移动j都可以
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
quickSort(nums,0,nums.size()-1);
return nums;
}
private:
//数组分成: (比基准值小的序列 基准 比基准值大的序列)
//返回int,基准值不参与比较
int quickDetail(vector<int>& nums,int l,int r){
//随机选择基准元素,并放在nums[l]位置
swap(nums[l],nums[rand()%(r-l+1)+l]);
int e=nums[l];
//双指针
int i=l+1;
int j=r;
while(1){
while(i<=j && nums[i]<=e) i++;
while(i<=j && nums[j]>=e) j--;
if(i>j) break;
swap(nums[i++],nums[j--]);
}
//j停在小的位置
swap(nums[l],nums[j]);
return j;
}
//分治
void quickSort(vector<int>& nums,int l,int r){
if(l>=r) return;//注意是大于等于
//先划分数组
int p=quickDetail(nums,l,r);
//再对左右两部分进行分治
quickSort(nums,l,p-1);
quickSort(nums,p+1,r);
}
};
也可以按照符合要求的判断
int quickDetail(vector<int>& nums,int l,int r){ //随机选择基准元素,并放在nums[l]位置 swap(nums[l],nums[rand()%(r-l+1)+l]); int e=nums[l]; //双指针 int i=l+1; int j=r; while(i<=j){ //先j后i也行 while(i<=j && nums[j]>=e) j--; while(i<=j && nums[i]<=e) i++; if(i<=j) swap(nums[i++],nums[j--]); } //j停在小的位置 swap(nums[l],nums[j]); return j; }
方法二:大于跳过
思路:i指向小于等于初始值的位置,j不断右移
- 遇到>基准值的右移,i不动,j右移
- 遇到<=基准值的,把
j指向值
和第一个>基准值的位置
交换,也就是i后面的第一个元素
----所以:i的位置指向的是最后一个<=基准值
//数组:[l,r]
int partition(vector<int>& nums,int l,int r){
swap(nums[l],nums[rand()%(r-l+1)+l]);
int e=nums[l];
int i=l;
for(int j=i+1;j<=r;j++){
if(nums[j]<=e){//这里可以不带等号
swap(nums[j],nums[++i]);
}
}
swap(nums[l],nums[i]);
return i;
}
注意等号无所谓,可以
大于等于
跳过,也可以只大于
就跳过这种方法存在致命缺陷,就是等于元素过多,会让左右两部分极度不平衡
方法三:三路快排
[l+1,lt]维护的是<10的部分
,[gt,r]维护的>10的部分
,从而[lt+1,gt-1]维护等于
i==gt
程序停止
1:这是i值>10情况:
交换i值和gt-1位置的值,i不变,gt-1
2:这是i值<10的情况:
交换该值和lt+1的位置的值,lt+1,i右移
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
threeWayQuick(nums,0,nums.size()-1);
return nums;
}
private:
void threeWayQuick(vector<int>& nums,int l,int r){
if(l>=r) return;
//初始化lt,gt使得:[l+1,lt],[gt,r]为空
int lt=l;
int gt=r+1;
int e=nums[l];
int i=l+1;
while(i<gt){
if(nums[i]>e){
swap(nums[i],nums[--gt]);
}else if(nums[i]<e){
swap(nums[i],nums[++lt]);
i++;
}else{//==e,直接右移
i++;
}
}
swap(nums[l],nums[lt]);
//分治
threeWayQuick(nums,l,lt-1);
threeWayQuick(nums,gt,r);
}
};
这里等于不需要传值,所以需要两个值,把分治和划分情况写成一个函数
也可以传入pair
pair<int,int> partition(vector<int>& nums,int l,int r){
int lt=l;
int gt=r+1;
int e=nums[l];
int i=l+1;
while(i<gt){
if(nums[i]>e){
swap(nums[i],nums[--gt]);
}else if(nums[i]<e){
swap(nums[i],nums[++lt]);
i++;
}else{//==e,直接右移
i++;
}
}
swap(nums[l],nums[lt]);
return {lt-1,gt};
}
void threeWayQuick(vector<int>& nums,int l,int r){
if(l>=r) return;
//初始化lt,gt使得:[l+1,lt],[gt,r]为空
auto [k,v]=partition(nums,l,r);
//分治
threeWayQuick(nums,l,k);
threeWayQuick(nums,v,r);
}
算法的稳定性
若经过排序,这些相同元素
的相对次序
保持不变,则称这种排序算法是稳定的;否则称为不稳定的。
在原序列中,A1=A2,且A1在A2之前,经过排序后,A1仍在A2之前。
稳定性意义:
要排序的内容是一个具备多个数字属性
的复杂对象,且原本的初始顺序存在意义,需要在二次排序的基础上保持原有排序的意义。
各排序算法的稳定性:
1、堆排序、快速排序、希尔排序、选择排序是不稳定的排序算法;
2、基数排序、冒泡排序、插入排序、归并排序是稳定的排序算法
非比较类排序
计数排序
-
简单版—不考虑稳定性
把数组值大小作为count的下标,统计频次
然后赋值回给原数组,注意i值是大小
void simpleCountSort(vector<int>& nums){
int n = nums.size();
int sz = 0;
//数组元素会作为下标,计算出数组长度
for(auto val:nums){
sz = max(sz, val);
}
vector<int> count(sz + 1);//数组标号从0开始
//遍历数组,统计次数
for(auto val:nums){
count[val]++;
}
//最后赋回给nums
int j = 0;
for (int i = 0; i <= sz;i++){//遍历count
if(count[i]!=0){
while(count[i]-->0){
nums[j++] = i;
}
}
}
}
- 考虑稳定性
计算前缀和,这样:元素为nums[i],则count[nums[i]]-1对应的位置就是存放的正确位置
数组赋值后,把对应频次-1
vector<int> countSort(vector<int>& nums){
/*初始化count频次数组*/
//1:计算大小
int sz = 0;
for(auto val:nums){
sz = max(sz, val);
}
vector<int> count(sz + 1);
/*计算count前缀和*/
//1:遍历数组
for (auto val:nums){
count[val]++;
}
//2:更新前缀
for (int i = 0; i < sz;i++){
count[i + 1] += count[i];
}
/*存放结果*/
//倒着赋值
vector<int> res(nums.size());
for (int i = nums.size() - 1;i>=0; i--) {
res[count[nums[i]] - 1] = nums[i];
count[nums[i]]--;//别忘了频次-1
}
return res;
}
桶排序(了解)—BucketSort
- 在桶排序里的“桶”是指一种
数据结构
,代表一个区间范围,用于临时存储排序元素 - 桶排序是一种分布式排序算法,将待排序数组元素分到有限数量的桶里,每个桶里的数据分别进行排序, 按照桶的顺序将元素合并成有序数组。
桶排序的工作原理可以拆分为 3 步:
- 初始化 m 个桶,将 n 个元素分配到 m 个桶中
- 对每个桶内的数据分别进行排序,这里可以借助任意排序算法
- 按照桶的从大到小的顺序,将所有元素合并成有序数组
例子:
- 假设使用的待排序元素整除规则是
nums[i] / 10
,分别将待排序数组的每个元素分配到每个桶中。
- 对每个桶内的数据分别进行排序,桶内的排序可以借助任意排序算法,比如插入排序、快速排序等。
- 按照桶的从大到小的顺序,将所有元素合并成有序数组。
习题:颜色分类
75. 颜色分类
采用不稳定的计数排序就行
void countSort(vector<int>& nums){
//只有0,1,2,所以key最大2
vector<int> count(3);
//频次统计
for(auto val:nums){
count[val]++;
}
//赋值回去
int j=0;
for(int i=0;i<=2;i++){
if(count[i]!=0){
while(count[i]-->0){
nums[j++]=i;
}
}
}
}
排序力扣题
一:数组的相对排序
1122. 数组的相对排序
方法一:计数排序
用count数组统计arr1的频次,然后遍历arr2输出元素,最后遍历count输出余下元素
//计数排序
vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
//先统计arr1的元素频次
vector<int> count(1001);
for(auto &val:arr1){
count[val]++;
}
//先把arr2的元素输出
int j=0;
for(auto &val:arr2){
while(count[val]-->0){
arr1[j++]=val;
}
}
//再把剩余元素输出
for(int i=0;i<count.size();i++){
while(count[i]-->0){
arr1[j++]=i;
}
}
return arr1;
}
方法二:直接比较
其实就是优先输出arr2顺序的元素,然后然后比较顺序输出
----先用map记录arr2的次序,map[arr2[i]]=i,然后进行比较:
- 如果两个元素都在arr2中,比较数组次序
- 如果一个元素在arr2中,输出这个元素
- 如果都不在arr2中,那么正常比较大小
vector<int> relativeSortArray(vector<int>& arr1, vector<int>& arr2) {
//用map记录arr2相对次序:mp[arr2[i]]=i
unordered_map<int,int> mp;
for(int i=0;i<arr2.size();i++){
mp[arr2[i]]=i;
}
//对arr1用自定义比较
sort(arr1.begin(),arr1.end(),[&](int x,int y)->bool{
if(mp.count(x) && mp.count(y)){
return mp[x]<mp[y];
}else if(mp.count(x)){
return true;//要x<y的x
}else if(mp.count(y)){
return false;//x<y
}else{
return x<y;
}
});
return arr1;
}
当然也可以这样写sort
//对arr1用自定义比较 sort(arr1.begin(),arr1.end(),[&](int x,int y)->bool{ if(mp.count(x)){ return mp.count(y)?mp[x]<mp[y]:true; }else{//x不再 return mp.count(y)?false:x<y; } });
二:数组中第k大的元素
215. 数组中的第K个最大元素
方法一:堆排序
注意:最大值是逆序的,第一个最大n-1,…
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int n=nums.size();
for(int i=(n-2)/2;i>=0;i--){
shiftDown(nums,n,i);
}
for(int j=n-1;j>=0;j--){
swap(nums[0],nums[j]);
shiftDown(nums,j,0);
}
return nums[n-k];//n-1 n-2
}
private:
void shiftDown(vector<int>& nums,int n,int m){
int e=nums[m];
while(m*2+1<n){
int j=m*2+1;
//注意if和while别用错了
if(j+1<n && nums[j+1]>nums[j]) j+=1;
if(nums[j]>e){
nums[m]=nums[j];
}else{
break;
}
m=j;
}
nums[m]=e;
}
};
方法二:快速排序