快速排序&归并排序

快速排序&归并排序

by 葛鑫


因为快速排序和归并排序都能很好的考察一个人对分治策略的理解程度和编码的能力,所以经常出现在面试场合中,在纸上手写bugfree的快排和归并排序算法可以说是基本要求,所以很有必要认真的学习一下这两种排序。



1.归并排序

先写归并排序的原因有2个,一是熟悉递归,二是理解分治思想。如果你认为自己对递归不熟悉,可以去谷歌“递归”,而分治简而言之就是分而治之,把一个大问题分解成若干个小问题,解决掉小问题以后再把答案合并起来。那么归并排序是怎样运用递归与分治的呢?简单来说,归并排序分为三步。

1.把待排序数组分成两半

2.分别将左半边和右半边的数组排好序

3.合并排序好的两个数组


当然,这三步递归地进行。不过有几个问题要解释清楚。

1.为什么我们要把数组分成两半?

2.第二步中的“排好序”到底如何实现?

3.如何合并排好序的两个数组?


1.众所周知,简单的排序算法,比如冒泡排序,插入排序,选择排序时间复杂度都是O(n^2),而归并排序是O(nlogn),它快就快在把一个n变成了logn,这个巨大的飞跃如何实现?就是依靠的“二分”,也就是binary search。如果你不知道为什么二分把n变成了logn,请多去做一些考察二分查找的题目。实际上,数据结构里到处都体现着二分,二叉查找树(BST)本质上是二分,线段树(Segment Tree)本质也是二分,快速傅立叶变换(FFT)也是二分。可以这样说,分治中,我们需要把大问题分解成若干小问题,这个分法,很多时候就是依靠二分。


2.这里的关键是要理解递归,我们来看一眼代码。

void merge_sort(int A[],int l,int r){
    if(l == r)
        return ;
    
    int  p =(l+r)/2;
    merge_sort(A, l, p);
    merge_sort(A, p+1, r);
    merge(A,l,r,p);
}


递归一定要有尽头,不然出现无限递归就很尴尬了。这个尽头就被叫做base case。在这里就是 l == r 的时候,因为这个时候,元素只有1个,1个元素必定是有序的。我们可以发现,如果 l != r 会怎样呢? 我们一半一半一半一半地分,直到l == r,接着两个merge_sort返回以后,我们才调用了merge。第一次调用merge的时候,r一定比l大1,想想为什么?也就是说,我们要合并的只有2个元素,对于2个元素的合并,且要求合并后有序,这一点在第三步中给出解答。如果你想更好更快的理解,就在IDE里进行单步调试。


3.合并最关键的一点,我们需要新的内存空间来存放合并后的结果。基本思想是初始化 i = l, j = p+1。然后我们比较A[i]和A[j],小的那一个放进新的空间,然后指针(在这里就是i或者j)自增1,直到所有元素进入新空间为止,然后拷贝回原数组。代码如下。

void merge(int A[],int l,int r,int p){
    int* copy = (int*)malloc((r-l+1)*sizeof(int));
    int i = l, j = p+1, cnt = 0;
   
    while(i<=p && j<=r){
        if(A[i]<=A[j])
            copy[cnt++] = A[i++];
        else
            copy[cnt++] = A[j++];
    }
    while(i<=p) copy[cnt++] = A[i++];
    while(j<=r) copy[cnt++] = A[j++];
    
    for(int k=l,cnt=0;k<=r;k++,cnt++)
        A[k] = copy[cnt];

    free(copy);
}



如果你还不懂,推荐哈佛cs50的材料(https://study.cs50.net/merge_sort)


2.快速排序

快速排序应该是内置算法中用的最多的一种,顾名思义,快速排序最大的特点就是速度快,并且与归并排序相比最大的一个优点是不需要额外空间存储中间结果。但缺点在于这是一种不稳定的排序算法(http://blog.csdn.net/wusuopubupt/article/details/22743315),并且最坏情况下,这种算法时间复杂度可能会达到O(n^2)


快速排序的基本思想是,在无序的数组中,随机选择一个主元,然后将除这个主元以外的元素与之相比,小的放在它的左边,大的放在它的右边,然后递归的进行下去。这就是一种分治策略,然而却不需要合并,减少很多麻烦。真正麻烦的地方就在 “小的放在它的左边,大的放在它的右边”这一步上。


现在来看一下代码实现(C++)

void quick_sort(int A[],int l,int r){
    if(l<r){
        int mid = getmid(A,l,r);
        quick_sort(A, l, mid-1);
        quick_sort(A, mid+1, r);
    }
}

递归的一个base case就是 l == r 这个时候只有一个元素,不需要排序了。 “主元的选择”,“小的放在它的左边,大的放在它的右边”这关键的两步,其实都由getmid函数实现,然后来看一下getmid()函数(算法导论里把这个函数称作partition,不过为防止和函数库冲突,我换了名字)

我们总是选择A[r]作为主元,然后从函数第二行到最后一行在干的事情都是“小的放在它的左边,大的放在它的右边”。返回的是[这次选择的主元在最后排序好的数组中应在的位置],这里要借助算法导论里面的图好好体会一下。这是一个相当聪明的编程trick,优雅无比。 


int getmid(int A[],int l,int r){
    int x = A[r];
    int i = l-1;
    for(int j = l;j<r;j++){
        if(A[j]<=x){
            swap(A[++i],A[j]);
        }
    }
    swap(A[++i],A[r]);
    return i;
}





如果说归并排序是一种自底向上的排序方法,快排便是自顶向下。


除此之外,主元的选择会影响到排序的效率,若我们每次选择的主元,最后都落在了[l,r]的中间位置,那就是最完美的二分了,但实际上这不现实。所以出现了随机化快速排序,这种随机体现在,对主元选择的随机,在一些极端情况种,比如A[] = {2,1,1,1,1,1,1,1,1,1} 排序效率会比总是选择最右元素作为主元好。


实现也超简单,在 [l, r]中随机选择一个元素与A[r]交换即可。


最后安利一下哈佛cs50,这是门计算机基础入门,内容包括C语言基础,查找与排序,内存管理,数据结构,Web编程,深入浅出极其易懂,业界良心。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值