快速排序
思路
①确定分界点pivot
,可以取数组最左边的元素arr[l],也可以取数组最右边的元素arr[r],取中间的元素arr[mid],或者随机取值
②调整区间
确定两个指针i,j,注意i=left-1,j = right+1(这样的目的是和之后每次交换后两个指针都移动一次做一个统一 ,方便do while写法),左指针i从数组左边界left开始,往右扫描,右指针j从数组右边界right开始,往左扫描。
1.当a[i] < pivot时,i右移,当a[i]>=pivot时,i停下;
2.当a[j] > x时,j左移;当去a[j]<=pivot时,j停下;
3.交换a[i]和a[j];
4.将i右移一位,j左移一位。
重复上述4个操作,直到i>=j,跳出while循环,此时
q[l…i] <= x q[j…r] >= x
③递归处理左右两段
以j为分界点,递归处理a[l…j] 和 a[j+1…r]
算法模板:
#include <iostream>
using namespace std;
const int N = 1e5+10;
int a[N];
void quick_sort(int a[],int left,int right)
{
if(left>=right) return; //当数组个数只有一个或空的时候返回
int pivot = a[(left+right)>>1], i = left-1 ,j = right+1; //因中间值作为分界点 ,i,j的定义方便接下来的do while
while(i<j)
{
do i++; while(a[i]<pivot); //i遇到比pivot大的数停下
do j--; while(a[j]>pivot); //j遇到比pivot小的数停下
if(i<j) swap(a[i],a[j]); //i,j交换
}
// 跳出while循环后, a[j]<=pivot a[i]>=pivot
// 这时a[l...j]<=pivot a[i...r]>=pivot
//以j来划分的话就是如下代码
quick_sort(a,left,j);
quick_sort(a,j+1,right);
//以i划分就是如下代码,并且修改pivot = a[(left+right+1)>>1]
//quick_sort(a,left,i-1);
//quick_sort(a,i,right);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d",&a[i]);
quick_sort(a,0,n-1);
for (int i = 0; i < n; i ++ ) printf("%d ",a[i]);
return 0;
}
关于快速排序的证明与边界分析,详见大佬文章
快速排序算法的证明与边界分析
练习题
Acwing 786 第k个数
思路:
快速选择是对快速排序的改造,快速选择在递归处理前和快速排序一致。
那么while循环结束后,q[l..j] <= x
,q[j+1..r] >= x
,求第k个数的话,当分割好的左数组(个数为j-l+1
)个数大于
k,说明第k个数在左数组中,递归处理左数组,否则说明第k个数在右数组,递归处理右数组,但是针对右数组不是查第k个数,而是查看第k - (左数组元素数目)
个数
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N];
int n,k;
int quick_sort(int l,int r,int k)
{
if(l>=r) return a[l];
int x = a[(l+r)>>1],i = l - 1 , j = r + 1;
while(i<j)
{
do i++; while(a[i]<x);
do j--; while(a[j]>x);
if(i<j) swap(a[i],a[j]);
}
//和快速排序一样,处理完后 q[l..j] <= x,q[j+1..r] >= x
int left_cnt = j - l + 1; //左数组元素个数
if(k<=left_cnt) return quick_sort(l,j,k);//当分割好的左数组个数大于k,说明第k个数在左数组中,递归处理左数组,
return quick_sort(j+1,r,k-left_cnt); //否则,递归处理右数组,查看第k - (左数组元素数目)个数
}
int main()
{
cin >> n >> k;
for (int i = 0; i < n; i ++ ) cin >> a[i];
cout << quick_sort(0,n-1,k);
return 0;
}
归并排序
思路
①确定数组中间为分界点mid,将数组一分为二
②分别递归处理左半部分数组和右半部分数组
,使得左半部分数组和右半部分数组排好序
③将排好顺序的左半部分数组和右半部分数组,合并成一整个有序的数组
1.开辟一个额外的数组和一个指针k指向额外数组,i指针指向左半部分数组的开头,即i=l
,j指针指向右半部分数组的开头,即j=mid+1
2.当a[i]<=a[j],将a[i]加入到额外数组末尾中,i++,k++,当a[i]>a[j],则将a[j]加入到额外数组末尾中,j++,k++,直到i<=mid && j<=r
,说明左半部分数组,右半部分数组至少有一个全部加入到额外数组中了,则跳出循环
3.若左半部分数组剩余,将左半部分数组直接加入到额外数组后面,若右半部分数组剩余,则将左半部分数组直接加入到额外数组后面
4.将额外数组的内容全部复制到a数组中
算法模板:
#include <iostream>
using namespace std;
const int N = 1e5+10;
int a[N],tmp[N],n;
void merge_sort(int a[],int l,int r)
{
if(l==r) return;
int mid = (l+r) >> 1;
merge_sort(a,l,mid),merge_sort(a,mid+1,r);
int k = 0,i = l,j = mid + 1;
while(i<=mid && j<=r)
{
if(a[i]<=a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while(i<=mid) tmp[k++] = a[i++];
while(j<=r) tmp[k++] = a[j++];
for (int i = l, j= 0 ; i <= r; i ++,j++) a[i] = tmp[j];
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ ) scanf("%d",&a[i]);
merge_sort(a,0,n-1);
for (int i = 0; i < n; i ++ ) printf("%d ",a[i]);
return 0;
}
关于归并排序的证明与边界分析,详见大佬文章
归并排序算法的证明与边界分析
练习题
Acwing 788 逆序对的数量
首先给出逆序对的定义,对于数列的第 i 个和第 j 个元素,如果满足 i < j 且 a[i] > a[j],则称其为一个逆序对。
那么求逆序对的数量,我们将数组平均分为左右两部分,那么数组的逆序对一定在以下三种情况:
①构成逆序对的两个数全部都在左边(绿色)
②构成逆序对的两个数全部都在右边(蓝色)
③构成逆序对的两个数一个在左边,一个在右边(红色)
那么整个数组的逆序对数目 = 左半部分数组的逆序对数目
+ 右半部分数组的逆序对数目
+ 位于两端情况的逆序对数目
对于左半部分数组和右半部分数组可以递归处理,处理两端情况的逆序对数目可以借鉴归并排序中合并数组的思想,假设我们现在已经得到左半部分,和右半部分数组的逆序对数目,并且左半部分,和右半部分数组已经排好序了。
我们用i指针指向左半部分数组的开头,j指针指向右半部分数组的开头,在进行合并数组时,a[i]应当比a[j]小,但当a[i]>a[j]
的时候,说明左半部分数组中a[i]及其以后的元素都与右半部分数组a[j]构成逆序对, 数目为 mid - i + 1
合并数组将得到两端情况的逆序对数目加上之前左半部分数组的逆序对数目和右半部分数组的逆序对数目,得到总的数目
代码框架大致如下:
- 递归计算左边的和右边的;
- 算左右两端的;
- 把他们加到到一起。
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int a[N],tmp[N];
LL merge_sort(int a[],int l, int r)
{
if(l>=r) return 0;
int mid = (l+r) >> 1;
LL res = merge_sort(a,l,mid)+merge_sort(a,mid+1,r); //递归计算左边的和右边的;
int i = l, j = mid+1, k = 0;
/* 合并数组 */
while(i<=mid && j<=r)
{
if(a[i]<=a[j]) tmp[k++] = a[i++];
else
{
tmp[k++] = a[j++];
res += mid - i + 1; // 出现逆序对,数目为mid-i+1
}
}
while(i<=mid) tmp[k++] = a[i++];
while(j<=r) tmp[k++] = a[j++];
/* 合并数组 */
for(int i=l,j=0;i<=r;i++,j++) a[i] = tmp[j]; // 将排好序的复制回原数组
return res;
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i ++ ) scanf("%d",&a[i]);
cout << merge_sort(a,0,n-1) << endl;
return 0;
}
整数二分
思路
二分的本质是边界性
。一个区间[l...r]
根据某个条件可以分为左右两部分,则利用二分来查找左右两部分的边界点。
以绿色区间满足条件m,红色区间满足条件n为例
(一)划分出绿色区间的边界点
①取中间值,即mid = (l+r+1)>>1
②判断中间值mid是否满足条件m,如果中间值mid满足条件m,说明绿色区间的边界点在[mid...r]
,则更新左边界l=mid
,否则说明绿色区间的边界点在[l..mid-1]
,则更新右边界r = mid-1
算法模板:
while(l < r)
{
int mid = l + r + 1 >> 1;
if(check(mid)) l = mid;
else r = mid - 1;
}
return l;
(二)划分出红色区间的边界点
①取中间值,即mid = (l+r)>>1
②判断中间值mid是否满足条件n,如果中间值mid满足条件n,说明
说明红色区间的边界点在[l..mid]
,则更新右边界r = mid
,否则说明红色区间的边界点在[mid+1...r]
,则更新左边界l = mid + 1
算法模板:
while(l < r)
{
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
return l;
浮点数二分
思路
类似于整数二分
整数二分是在一组离散的值找到一个分界点,而浮点数二分是在一组连续的值找一个分界点。整数二分的结束条件时,区间内只有一个数,而浮点数二分的结束条件时,当二分的区间小于给定误差时,就可以停止二分。
或者直接迭代一定的次数,比如循环100次后停止二分。
(浮点数二分,相对于整数二分不用过度考虑边界问题, 直接更新边界为mid就行,即l = mid
,r = mid
)
算法模板:
Acwing 790 数的三次方根
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
double x;
cin >> x;
double l = -10000, r = 10000;
while(r-l>=1e-9) //小于一个误差就行
{
double mid = (l+r)/2;
if(mid * mid * mid <= x) l = mid;
else r = mid;
}
printf("%.6lf",l);
return 0;
}