[AcWing算法基础课] Chapter1 基础算法(一)

快速排序

快速排序的主要思想基于分治

我们先规定:待排序数组为q,第一个数组元素下标是L,最后一个数组元素下标是R

快速排序的原理:

  1. 确定分界点。分界点可以是q[L]、q[(L+R)/2]、q[R]或者一个随机的数组元素
  2. 调整范围,挑选出x(x是个值),使得第一个区间里的所有数都小于等于x,第二个区间里的所有数都大于等于x
  3. 递归处理左右两个区间

我们赋予递归函数的意义就是将一个数组进行排序后变为有序数组,所以经历步骤3后,左区间和右区间经过递归后就都是有序的了。

  1. 因为有步骤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

归并排序的原理:

  1. 确定分界点:mid=(l+r)/2
  2. 递归处理左右两边

这里和快速排序的情况类似。我们赋予递归函数的意义就是将一个数组进行排序后变为有序数组,所以经历步骤2后,左区间和右区间经过递归后就都是有序的了。

  1. 此时左右两个区间都成了有序的,我们再把两个有序的数组合并成一个有序数组,这个步骤称为归并 ,时间复杂度为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;
}

归并排序是稳定的。

一个排序算法是稳定的:如果原序列里两个数的值是相同的,排序后位置如果不发生变化,那么这个排序就是稳定的。

整数二分

二分的本质并不是单调性。如果有单调性,就一定可以二分;没有单调性,未必不能二分。

假设有一个已经存在的性质,在左半边区间不满足这个性质,在右半边区间满足这个性质,那么就可以将整个区间一分为二。

在这里插入图片描述

那么二分就可以寻找性质的边界,既可以找到红色点也可以找到黄色点。

二分寻找红色边界点

  1. mid=(l+r+1)/2

  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不变

二分寻找黄色边界点

  1. mid=(l+r)/2
  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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值