快速排序算法

引言

快速排序是应用最广泛的排序算法(C++中的sort函数内部就是快排),优点是实现简单,速度快(时间复杂度NlogN),原地排序(只需要很小的辅助栈),缺点就是写的不好性能就会比较差。

基本思想是分治算法:选择一个元素,将数组分割成左右两个数组,左边的数组都比这个数小,右边的数组都比这个数大,再分别递归的对左右数组进行相同的操作,最后达到排序的目的。

基础版快排

思路

快速排序_pic1.png

假设有如上图所示的数组,选择第一个元素4,用某种办法将其放在正确的位置(左边的元素小于4,右边的元素都大于4),如下图所示:

快速排序_pic2.png

接下来对左边的数组和右边的数组分别按照这个思路递归。

partition

难点是,如何将这个数字4放在正确的位置,这个将数组分为两个部分的操作叫做切分(partition)。通常选择数组的第一个元素v作为数组的分界点,将第一个元素的索引称为l:

快速排序_pic3.png

l的右边开始遍历并整理数组,使得前面一部分小于v,后面一部分大于v

快速排序_pic4.png

并用j记录下大于v和小于v的分界点的索引,i记录当前位置的索引。

快速排序_pic5.png

此时,满足条件 arr[l+1...j] < v arr[j+1...i-1]>v

接下来分为两种情况(暂时未考虑等于):

  1. e>v

    快速排序_pic6.png

此时e>v,所以可以直接将e放在>v的数组的后面,也就是不用动,下一步就是i++,继续判断新的位置的情况。

  1. e<v

    快速排序_pic7.png

这种情况可以将e与>v的数组的第一个元素交换,即将ij+1位置的元素交换。得到:

快速排序_pic8.png

最终,所有元素都遍历完,此时该把v放在正确的位置:

快速排序_pic9.png

正确的位置就是在<v数组的最后一个位置,也就是索引为j的位置,将lj位置的元素进行交换就可以得到最终结果:

快速排序_pic10.png

接下来就是对左右数组分别进行同样的操作。

代码

// 对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
template <typename T>
int __partition(T arr[], int l, int r){
    T v = arr[l];
    int j = l; // arr[l+1...j] < v ; arr[j+1...i) > v
    for( int i = l + 1 ; i <= r ; i ++ )
        if( arr[i] < v ){
            j ++;
            swap( arr[j] , arr[i] );
        }
    swap( arr[l] , arr[j]);
    return j;
}

// 对arr[l...r]部分进行快速排序
template <typename T>
void __quickSort(T arr[], int l, int r){
    if( l >= r )
        return;
    int p = __partition(arr, l, r);
    __quickSort(arr, l, p-1 );
    __quickSort(arr, p+1, r);
}

template <typename T>
void quickSort(T arr[], int n){
    __quickSort(arr, 0, n-1);
}

缺陷及优化

缺陷

对于近乎有序的数组,上面的快速排序思路会很慢。快速排序之所以快的一个原因是:将一个数组分为两个部分,分别排序,最好的情况是每次都平分,最后logN次就能完成排序。但是在近乎有序的数组进行排序时,每次选取第一个元素为标定点,最后的切分是极不合理的。如图:

快速排序_pic11.png

最差的情况下,每次选取最左边的元素为标定点,每次都没有元素比他小:

快速排序_pic12.png

改进

不选择第一个元素为标定点,而是从数组长度范围内随机选择一个索引作为标定点。这样的话,出现上面那种情况的概率很小(虽然说不是不可能),因为每次都选中最小元素几乎不可能。

template <typename T>
int _partition(T arr[], int l, int r){
    //rand()%(r-l+1)+l 在l和r之间产生一个随机数
    swap( arr[l] , arr[rand()%(r-l+1)+l] );
    T v = arr[l];
    int j = l;
    for( int i = l + 1 ; i <= r ; i ++ )
        if( arr[i] < v ){
            j ++;
            swap( arr[j] , arr[i] );
        }
    swap( arr[l] , arr[j]);
    return j;
}

template <typename T>
void _quickSort(T arr[], int l, int r){
//    if( l >= r )
//        return;
    //小优化:在数组长度比较小的时候,使用插入排序更快。
    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }
    int p = _partition(arr, l, r);
    _quickSort(arr, l, p-1 );
    _quickSort(arr, p+1, r);
}

template <typename T>
void quickSort(T arr[], int n){
    srand(time(NULL)); //用时间当随机种子
    _quickSort(arr, 0, n-1);
}

双路快排

上面的简单排序在切分时,没有考虑到e==v的时候该怎么办,不同的做法,性能差异其实很大。所以在对有大量重复元素的数组进行排序的时候,上面的算法性能很差。

上面的代码实际编写中,隐含着在e==v时,将e保留在原处不动,如图:

快速排序_pic13.png

不管是将e放在<v的数组还是>v的数组,在有大量重复元素的时候,最后两个数组都是极不平均的。

快速排序_pic14.png

思路

将上面切分的思路修改一下,现在将<v的元素和>v的元素分别放在数组的两端:

快速排序_pic15.png

此时需要两个索引iji从左边开始扫描,j从右边开始反向扫描。先对i进行扫描,如果遇到了<v的元素,就继续扫描直到找到 >=v的元素就停下来。如下图:

快速排序_pic16.png

此时,j开始反向扫描,直到遇到<=v的元素就停下来。如下图:

快速排序_pic17.png

此时可以交换ij索引位置的元素,

快速排序_pic18.png
快速排序_pic19.png

此时,将=v的元素分配给了两边的数组,不会发生聚集在某一侧的情况。结束条件就是ij相遇。

代码

template <typename T>
int _partition2(T arr[], int l, int r){
    swap( arr[l] , arr[rand()%(r-l+1)+l] );
    T v = arr[l];
    // arr[l+1...i) <= v; arr(j...r] >= v
    int i = l+1, j = r;
    while( true ){
        while( i <= r && arr[i] < v )
            i ++; //搜索arr[i] >= v
        while( j >= l+1 && arr[j] > v )
            j --; //搜索arr[j] <= v
        if( i > j )
            break;
        swap( arr[i] , arr[j] );
        i ++;
        j --;
    }
    swap( arr[l] , arr[j]);
    return j;
}

template <typename T>
void _quickSort(T arr[], int l, int r){
//    if( l >= r )
//        return;
    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }
    int p = _partition2(arr, l, r);
    _quickSort(arr, l, p-1 );
    _quickSort(arr, p+1, r);
}

template <typename T>
void quickSort(T arr[], int n){
    srand(time(NULL));
    _quickSort(arr, 0, n-1);
}

三路快排

没有最优,只有更优,上面双路快排的思想已经很优秀了,但是在解决大量重复元素的情况下,还有一个更加经典的实现方式,也就是这节的主角——三路快排。

思路

不同于上面的快排思想,三路快排直接考虑将数组分为三个部分: >v,=v和<v。

快速排序_pic20.png

上面将数组分为了三个部分,现在考虑i索引位置的e元素,分三种情况:

  1. e==v

    快速排序_pic21.png

此时元素e不用进行任何操作。i++

  1. e<v

    快速排序_pic22.png
快速排序_pic23.png

此时将ilt+1的元素进行交换,lt++; i++

  1. e>v

    快速排序_pic24.png
快速排序_pic25.png

此时,将igt-1的元素交换,i++; gt--

结束条件:igt相遇。

快速排序_pic26.png
快速排序_pic27.png

此时将llt位置的元素交换,即可完成切分操作,接下来对<v和>v的部分分别切分。优势:可以看到很多元素放在了中间,下次切分的时候少了很多元素!!!

代码

template <typename T>
void __quickSort3Ways(T arr[], int l, int r){
    if( r - l <= 15 ){
        insertionSort(arr,l,r);
        return;
    }
    swap( arr[l], arr[rand()%(r-l+1)+l ] );
    T v = arr[l];
    int lt = l;     // arr[l+1...lt] < v
    int gt = r + 1; // arr[gt...r] > v
    int i = l+1;    // arr[lt+1...i) == v
    while( i < gt ){
        if( arr[i] < v ){
            swap( arr[i], arr[lt+1]);
            i ++;
            lt ++;
        }
        else if( arr[i] > v ){
            swap( arr[i], arr[gt-1]);
            gt --;
        }
        else{ // arr[i] == v
            i ++;
        }
    }
    swap( arr[l] , arr[lt] );
    __quickSort3Ways(arr, l, lt-1);
    __quickSort3Ways(arr, gt, r);
}

template <typename T>
void quickSort3Ways(T arr[], int n){
    srand(time(NULL));
    __quickSort3Ways( arr, 0, n-1);
}

衍生问题

问题

用快排思想求数组中的第n大的元素(O(n)复杂度)。

思路

快速排序_pic28.png

在快速排序的过程中,一次切分之后,将某个标定元素放在了合适的位置,此时根据该位置的索引p就可以直到这个元素是数组中排名第p的元素,如果n>p,只需要对后面的元素进行一次切分,如果n<p,则需要对前面的元素进行切分,如果n=p,则说明找到了!

总结

从最简单的快速排序算法开始,可以看到算法的一步一步的优化,重要的不是学习快排这个算法,而是学习这个优化思想和思路。要善于分析各种场景下算法的性能,分析性能差的原因,想出解决办法。

参考

程序猿的内功修炼,学好算法与数据结构

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容