排序与检索(归并/快排/二分)

说到排序,学过STL的应该知道sort / stable_sort(后者可以使相同值的元素位置不改变,所以更稳定)。如果使用现成的排序结构,当然可以用选择排序 / 冒泡排序,但复杂度极高(O(n^2)),相对高效的排序算法有归并排序 / 快速排序二分排序

目录

归并排序

快速排序

二分查找


归并排序

归并排序是分治思想在排序方面的应用,吸收了分治中“从小到大”递归运算的思想,递归函数分为以下三个部分:

划分问题:把序列分成元素个数尽量相等的两半

递归求解:递归中就把两半元素分别排序

合并问题:把左右两个有序表结合起来

 其中“合并问题”中要把左右排号的两个序列重新排列。由于两个序列是排好的,所以可以用一个循环,让它每次取两个序列的最小元素并进行比较,比较出来的较小元素放到新序列中,并在原序列中删除这个元素。循环的终止条件是两个序列都空,其中两个序列原来的元素个数可以相等、也可以不等(但是因为是使用整除/2,序列元素个数最多相差一个),在循环末尾只要出现一个序列空另一个序列不空的情况,可以直接把非空序列仅剩的一个元素加入新序列并退出循环(无需特判,具体看代码)。可以用一张图来表示这个整个过程:

以下是完整的代码👇:

void merge_sort(int* A,int x,int y,int* T){
    if(y-x>1){                   //如果序列中只有一个元素,就没必要进入
        int m=x+(y-x)/2;         //求出两半的中点值
        int p=x,q=m,i=x;
        merge_sort(A,x,m,T);     //左半边递归
        merge_sort(A,m,y,T);     //右半边递归
        while(p<m||q<y){         //只要有一个非空,循环就可以继续
            /*if:如果另一个序列为空,或者本序列非空且末端小*/
            if(q>=y||(p<m && A[p]<=A[q])) 
                T[i++]=A[p++];   //左半部分的末端放进临时空间
            else
                T[i++]=A[q++];   //右半部分...
        }
        for(i=x;i<y;i++)  A[i]=T[i];  //从临时空间将数据重新传回A
    }
}

 这里有几个问题(易出错/难理解)需要注意。

第一个问题是,在第三步“合并问题”之前要求把左右两半的数组进行排序,但是代码中好像没有明显的排序语句?这是因为这个非直观的“排序”,在递归中完成。假设深入到递归的中深层去看,序列中只有两个元素(y-x==2),那么“放进临时数组比较并存储”的过程就是个排序过程,将这两个元素小的放在前,大的放在后;来到倒数第二层递归,有两个双元素的序列要进行合并,由于最深层递归中已经实现了让这两个双元素数组都是小的在前,所以运算结果也是从小到大的合并序列。可以说某一层“合并”前的排序都是这一层往下所有递归的劳动成果,它们从最深层就开始让元素保持从小到大的顺序并逐层往上合并。

第二个问题是T这段“临时空间”的作用。或许有人觉得在左右两个序列中可以直接把取出的较小元素放回A数组(原数据数组),但是要注意这里“取元素”的元素来源也是A,也就是说如果从A中取出元素再放回A,那么本次的“放回”可能会影响下一次取出的元素值,所以引入临时数组将这一段排序后的元素全部存储,再集体一次放进A中。这里T可以不作为参数传递,但是由于每一次所需要的临时空间的大小由xy决定,且直接传整个数组再赋值、传回值时可以做到下标对应,减小出错概率。

第三个问题(易错点)是短路运算符的使用及合并继续的判断。前面说过合并循环的执行条件是两个序列都不为空,换言之可以用短路运算符 || 表示“只要有一个非空,就可以执行”;同样在取出最小值时,由于可能出现某个序列为空的情况,所以取小元素的判断条件是“另一个串为空 || 本串不空且元素较小”。

使用归并排序可以解决经典的 逆序对问题 :给一列数a1,a2,...,an,求逆序对数(逆序对指i<j但是ai>aj的对)。直接枚举ij复杂度达到n^2必定超时,所以可以用归并优化:

step1划分问题把序列分成元素个数尽量相等的两半
step2递归求解统计i和j均在左边/右边的逆序对个数
step3  合并问题统计i在左边j在右边的逆序对

其中最难实现的依然是step3“合并问题”,解决思路是对于右边的每一个j元素,统计左边比它大的元素。但是这不需要手动实现——归并排序中合并操作数是由小到大完成的,所以右边的任意一个A[j]放进T中时,左边序列中残存的就是所有比A[j]大的数字。

所以在代码上,可以直接把 else T[i++]=A[q++] 改成 { T[i++]=A[q++]; cnt+=m-p } 即可。

不需要考虑“排序”会不会影响下一次取逆序,因为递归的顺序是从小到大,本次递归排序到下一次中只是“某半边”内部的排序。同理,本次递归取出的跨两边区域的逆序对,进入下一个递归中就是某一半边内部的逆序对(所以上面表格中,递归求解的作用就是统计均在某侧的逆序对)。

归并排序的时间复杂度为O(nlogn),显然比直接枚举好得多。

快速排序

快速排序和归并排序一样使用了分治的思想:(注意观察不同)

step1划分问题数组各个元素先简单重排再分为左右两个部分,左边任意元素小于等于右边
step2递归求解左右两部分分别排序
step3  合并问题无需合并,因为此时数组已经有序

关于不同的题目,“划分问题”中的简单重排有多种方法(因题而异)。

最典型的例题是 快速选择问题:输入n个整数和一个正整数k,输出这些整数从小到大排列的第k个(n≤10^7)。

由于数据规模过大,直接排序(O(nlogn))还是会造成TLE,所以选择使用(最快的通用内部排序算法)快速排序。大致思路是在第一步“划分”结束后,将数组分为两部分,只需要将前一部分的个数判断结果在哪一个部分。期望情况下复杂度为O(n)。

由于快速排序对题意的依赖性较大,最坏情况复杂度为O(n^2),平均情况O(nlogn),理想情况仅为O(n)。

二分查找

二分查找其实也是吸收了分治法的思想,另外结合了“不断缩小查找范围”的思路,所以可以称为“折半查找”。二分可以用递归实现,但是为了代码方便一般只用循环:

int bsearch(int *A,int y,int v)
{
    int m;
    while(x<y){
        m=x+(y-x)/2;
        if(A[m]==v) return m;
        else if(A[m]>v) y=m;
        else  x=m+1;
    }
    return -1;
}

除了以上写法以外,二分还有一种更通用的写法(不需要特判等于情况):

/*该思路来源于 同济大学CPCLab 2022暑期集训 */
while(l<=r){  //l为左边界,r为右边界,都可以取到
    int mid=(l+r)/2;
    if(Big(mid))      //mid>n,猜大了
        r=mid-1;
    else{
        ans=mid;
        l=mid+1;
    }
}
//结果是ans

如果想特定找出序列中“第一个出现”或“最后一个出现”的位置,可以用STL中的lower_bound和upper_bound。

最后,如果想处理小数,可以用以下两种小数的二分方法:

/*该思路来源于 同济大学CPCLab 2022暑期集训 */
/*方法1*/
for(int i=1;i<60;i++){  //60是来控制精确位数的
    double mid=(l+r)/2;
    if(check(mid))
        ans=r=mid;      //因为是连续的,所以这样写
    else
        l=mid;
}
/*方法2*/
const double eps=1e-7;
while(fabs(r-l)>eps){
    double mid=(l+r)/2;
    if(check(mid))
        r=mid;
    else
        l=mid;
}                       //最后结果是l或r(都可以🙆‍)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zhqi HUA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值