快速排序
快速排序的主要思想基于分治。
我们先规定:待排序数组为q,第一个数组元素下标是L,最后一个数组元素下标是R
快速排序的原理:
- 确定分界点。分界点可以是q[L]、q[(L+R)/2]、q[R]或者一个随机的数组元素
- 调整范围,挑选出x(x是个值),使得第一个区间里的所有数都小于等于x,第二个区间里的所有数都大于等于x
- 递归处理左右两个区间
我们赋予递归函数的意义就是将一个数组进行排序后变为有序数组,所以经历步骤3后,左区间和右区间经过递归后就都是有序的了。
- 因为有步骤2,所以直接将递归处理后的左右两个区间拼接到一起,此时整个区间就排序完成了
步骤2的一种暴力思路是:开辟额外的数组空间a[]和b[]。随后遍历q数组,将所有小于q[x]的元素全部放入a[]中,将所有大于q[x]的元素全部放入b[]中,再把a[]和b[]里的每个元素重新赋值给数组q
下面介绍一种不需要开辟额外空间的优美的方法。
声明两个指针i、j。i最开始指向第一个元素,j最开始指向最后一个元素。
先从i开始,如果 q[i] < q[x],那么i向后移动一位(i++),直到 q[i] >= q[x]。此时我们转换到q[j],如果q[j] > q[x],那么j向前移动一位(j–),直到 q[j] <= q[x],此时交换(swap)q[i]和q[j]。
在任意时刻,i前面的所有数都是小于等于x 的,j右边的所有数都是大于等于x的。
两个指针相遇或者穿过时,结束循环。
快排模板循环后i和j的相对位置:1.i==j 2.i=j+1
快排模板
AcWing的测试数据如果写x=q[l]就会超时
AcWing 785.快速排序
#include <iostream>
using namespace std;
const int N=1e6+10;
int n;
int q[N];
void quick_sort(int q[],int l,int r){
if(l>=r) return;
int x=q[(l+r)/2],i=l-1,j=r+1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j){
swap(q[i],q[j]);
}
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&q[i]);
quick_sort(q,0,n-1);
for(int i=0;i<n;i++) printf("%d ",q[i]);
return 0;
}
归并排序
归并排序也用到了分治的思想。
我们先规定:待排序数组为q,第一个数组元素下标是L,最后一个数组元素下标是R
归并排序的原理:
- 确定分界点:mid=(l+r)/2
- 递归处理左右两边
这里和快速排序的情况类似。我们赋予递归函数的意义就是将一个数组进行排序后变为有序数组,所以经历步骤2后,左区间和右区间经过递归后就都是有序的了。
- 此时左右两个区间都成了有序的,我们再把两个有序的数组合并成一个有序数组,这个步骤称为归并 ,时间复杂度为O(n)
使用双指针法来把两个有序的数组合并成一个有序数组
归并模板
AcWing 787. 归并排序
#include <iostream>
using namespace std;
const int N=100000;
int n;
int q[N],temp[N];
void merge_sort(int q[],int l,int r){
if(l>=r) return;
int mid=(l+r)>>1,k=0,i=l,j=mid+1;
merge_sort(q,l,mid);
merge_sort(q,mid+1,r);
//用双指针法将左右两个有序区间合并
while((i<=mid)&&(j<=r)){
if(q[i]<=q[j]){
temp[k++]=q[i++];
}else{
temp[k++]=q[j++];
}
}
//处理左区间或者右区间的剩余部分
while(i<=mid) temp[k++]=q[i++];
while(j<=r) temp[k++]=q[j++];
//将temp数组中的有序的元素一一赋值给q数组
for(i=l,j=0;i<=r;i++,j++) q[i]=temp[j];
return;
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
merge_sort(q,0,n-1);
for(int i=0;i<n;i++){
printf("%d ",q[i]);
}
return 0;
}
归并排序是稳定的。
一个排序算法是稳定的:如果原序列里两个数的值是相同的,排序后位置如果不发生变化,那么这个排序就是稳定的。
整数二分
二分的本质并不是单调性。如果有单调性,就一定可以二分;没有单调性,未必不能二分。
假设有一个已经存在的性质,在左半边区间不满足这个性质,在右半边区间满足这个性质,那么就可以将整个区间一分为二。
那么二分就可以寻找性质的边界,既可以找到红色点也可以找到黄色点。
二分寻找红色边界点
-
mid=(l+r+1)/2
-
if(check(mid)) 来判断mid是否满足性质
如果true,mid的左边都满足红色性质,说明答案(红色边界点)在[mid,r](闭区间 包含mid) 更新方式为l=mid,r不变这里我们是看到更新方式为l=mid时才确定mid=(l+r+1)/2的,具体原因稍后解释。
如果false,mid的右边都不满足红色性质,说明答案(红色边界点)在[l,mid-1] 更新方式为r=mid-1,l不变
二分寻找黄色边界点
- mid=(l+r)/2
- if(check(mid))
如果true,mid的右边都满足黄色性质,说明答案(黄色边界点)在[l,mid],更新方式为r=mid,l不变
如果false,mid的左边都不满足黄色性质,说明答案(黄色边界点)在[mid+1,r],更新方式为l=mid+1,r不变
有一个二分问题时如何去考虑?如何选择用哪个模板?
先确定check函数,然后想一下如何去更新区间,看更新方式再决定要不要在(l+r)上补上+1
为什么更新方式为l=mid时要补上+1?
当l=r-1时,mid=(l+r)/2=2*l+1/2=l。
这时如果check(mid)为true,更新方式为l=mid,就相当于没更新!也就很不巧的死循环了!因此这里需要补上+1
整数二分模板
AcWing 789. 数的范围
#include <iostream>
using namespace std;
const int N=100010;
int n,m;
int q[N];
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
while(m--){
int x;
scanf("%d",&x);
int l=0,r=n-1;
//二分起始位置 性质定义成>=X
while(l<r){
int mid=(l+r)>>1;
if(q[mid]>=x){//如果q[mid]>=x 说明mid右边都>=x
r=mid;
}else{
l=mid+1;
}
}
//此时退出循环了 l==r 如果序列中不存在X 我们二分出来的数肯定不等于X
if(q[l]!=x){
cout<<"-1 -1"<<endl;
}else{
cout<<l<<" ";
//二分结束位置 性质定义成<=X
l=0,r=n-1;
while(l<r){
int mid=(l+r+1)>>1;
if(q[mid]<=x){
l=mid;
}else{
r=mid-1;
}
}
cout<<l<<endl;
}
}
return 0;
}
我们每一次二分的时候,都是保证区间里一定有答案的。
二分的其实是是问题的规模,每次通过二分都能缩小问题规模。
我们定义了一个性质,这个性质一定是有边界的,二分算法是一定能把这个边界找出来的。
浮点数二分
因为浮点数除法的特性,浮点数二分的边界处理很简单。
当浮点数二分的区间长度足够小时,就可以用l或者r当作答案。
比如r-l<=10的-6次方时,我们就可以认为找到答案了。
求平方根
#include <iostream>
using namespace std;
//求平方根
int main(){
int x;
cin>>x;
double l=0,r=max(1,x);
while(r-l>1e-8){//另一种写法是无论如何循环100次
double mid=(l+r)/2;
if(mid*mid>=x){
//说明mid>=根号x
r=mid;
}else{
l=mid;
}
}
printf("%lf\n",l);
return 0;
}
AcWing 790. 数的三次方根
#include <iostream>
using namespace std;
int main(){
double x;
cin>>x;
int flag=0;
if(x<0){
x=-x;
flag=1;
}
double l=-10000,r=10000;
while(r-l>1e-8){
double mid=(l+r)/2;
if(mid*mid*mid>=x) r=mid;
else l=mid;
}
if(flag==0)
printf("%lf\n",l);
else
printf("%lf\n",-l);
return 0;
}