排序算法(快排,归并)详解

458 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

1.分成子问题

2.递归处理子问题

3.子问题合并

基于AcWing,y总的模板进行讲解

1.快速排

例题:785. 快速排序 - AcWing题库

1.模板介绍

void quick_sort(int q[], int l, int r)
{
    //递归终止
    if (l >= r) return;
    //l-1,要包含边界,r+1也是
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        //关于为什么采用 do..while
        //防止死循环
        //如果 q[i]q[j]都为x,那么交换后 i,j不会进行任何处理,就会进行死循环
        //如 1,1,1,1,1 相同数进行while 循环,得到的结果是i,j不会进行移动,使得一致死循环
        //如果取q[i]<=x [1,1] 就会导致 数组越界 不会报错,此时q[i]显示成了12784544等大数,并且死循环
        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);
    
    //如果要使用i的话
     quick_sort(q, l, i-1), quick_sort(q, i, r);
    否则 [1,2]会进行死循环,因为左边界因为不满足条件,i就不会进行更新,就会导致死循环
     //同样如果x取到了x[r]就会导致右边界出现问题,陷入死循环
    并且x=q[l+r+1>>1]
}

2.模板详解

1)参数确定

分治算法 对数组进行一个左右拆分,所以首先需要数组参数,其次是左右区间

void quick_sort(int q[], int l, int r)

2)递归终止

其实这里写==也是可以通过,此函数返回值为void,所以直接return即可

if(l>=r)return;

3)循环条件

问题1:为什么是l-1,r+1

//因为后续使用do...while循环,如果不将左右边界进行向外扩张,那么会导致一开始,左右边界取不到
int i = l - 1, j = r + 1, x = q[l + r >> 1];

问题2:为什么是do...while

while (i < j)
{
    //关于为什么采用 do..while
    //防止死循环
    //如果 q[i]q[j]都为x,那么交换后 i,j不会进行任何处理,就会进行死循环
    //如 1,1,1,1,1 相同数进行while 循环,得到的结果是i,j不会进行移动,那么就导致while一直进行形成死循环
    do i ++ ; while (q[i]< x);
    do j -- ; while (q[j] > x);
    if (i < j) swap(q[i], q[j]);
}

问题3:q[i]<x可以取等号吗

do i ++ ; while (q[i]<=x);
do j -- ; while (q[j]>= x);
同上也可以拿相同数组进行测试,11111
此时上面两个while循环都成功,会一直进行
就会导致i移到了r+1,j移到了-1,就导致数组越界,但是并不会报错,读者可以自己尝试
此时你可以在while循环进行输出q[i],q[j]就会得到一些较大的数 q[i]显示成了12784544,形成死循环,应该是开辟的新空间,具体待解决

4)递归

//递归 进行分段排序quick_sort(q, l, j), quick_sort(q, j + 1, r);
​
​
//如果要使用i的话,那么x取的位置就要进行更改
x=q[l+r+1>>1]
解释:如果是向下取整那么就会遇到x=左边界的情况,就导致i不进行更新
quick_sort(q, l, i-1), quick_sort(q, i, r);
eg:1,3,2,4 如果取到了左边界,i不会更新,j会一直更新到为0
就导致了quick_sort(q,0,-1),quick_sort(q,0,3)//一直循环了
 
 
//同样如果以j为分界的话,如果x取到了x[r]就会导致右边界出现问题,陷入死循环
 注意事项

循环到最后j<=i,q[j]<=x

do i++; while(q[i] < x)和do j--; while(q[j] > x)不能用q[i] <= xq[j] >= x
​
假设q[l..r]全相等
​
则执行完do i++; while(q[i] <= x);之后,i会自增到r+1
​
然后继续执行q[i] <= x 判断条件,造成数组下标越界(但这貌似不会报错)
​
并且如果之后的q[i] <= x (此时i > r) 条件也不幸成立,
​
就会造成一直循环下去(亲身实验),造成内存超限(Memory Limit Exceeded)

2.归并排序

例题:787. 归并排序 - AcWing题库

1.模板介绍

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;
​
    int mid = l + r >> 1;
    //归并是先分,再进行排序
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);
​
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r) 
        //将小的先排在前面
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];
    //然后再判断是否还有剩余的元素,进行填充,因为分到最后小数在前面,所以先进行i<=mid的填充
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];
        
    //注意这里的i<=r,是要等于r的,r就是右边界
    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

2.模板详解

1)参数确定

2)递归中止

与快排一致,不多赘述

3)递归

//先进行数组的划分,然后再进行汇总
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);

4)循环

int k = 0, i = l, j = mid + 1;
//将数组划分为了左右两部分,左边是较小数组,右边是较大数组
while (i <= mid && j <= r) 
  //判断较小数组的值是否小于较大数组的值,如果是那么就存入较小数
 if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
​
else tmp[k ++ ] = q[j ++ ];
//然后再判断是否还有剩余的元素,进行填充,因为分到最后小数在前面,所以先进行i<=mid的填充
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];

5)结束

//注意这里的i<=r,是要等于r的,r就是右边界
//该函数是数组原地进行,没有生成新的数组,所以最后将排序好的数组传回原数组
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];

总结

快排和归并都是同一性质的排序,都属于分治算法,只不过前者是先排后分,后者是先分后排再归总,所以读者将分治问题的思想掌握,那么这两种算法就轻而易举