快速排序算法(以int型数组为例)

快速排序的本质思想是分而治之

  一个待排序列,怎么 让它变得有序呢?我们先来看看一个有序的序列所具有的特征:当前指向的位置上的元素,一定不大于它右边位置的元素,也一定不小于它左边的元素,并且它的下标(秩),正是比他小的元素的数量

  就如箭头指向所指的已排序的数组其中的某个位置,该位置的值为5,不大于任何它右边位置的值,也不小于任何它左边的值。
  再考虑一下极端情况:如果数组的长度只有1,那这个数组本身就是有序的。

  那么如果我们随机选取了某一个元素,我们便可以将数组分为三个部分:

  最开始W区域就是未排序的整个数组,L和H区域是空的,紧接着我们在W区域中任意选取某一个元素,再将比它大的扔到右边,比他小的扔到左边,最后W区域退化成只有那个选定的元素存在的区域,长度为1,并且不难证明,此时该元素所在的位置,其实就是最后完全排序好后它的位置,利用反证法粗略证明如下:
  1.假设该元素做过上述变换以后所在的位置不是它最终排好序后的位置。
  2.比该位置小的元素都在它的L区域内,假设数量为N。
  3.该数组已排好序后,选取的那个元素具有的一个性质就是它左边的元素不大于它,其数量也就是N。
  4.那么就说明应该存在有两个位置,使得那两个位置左边的元素数量为N,显然不存在,产生矛盾,前提不成立。

我们来举个例子:
未排序的数组a

  这是一个数组的初始状态,我们的操作步骤是:
    1.取出a[0]中的元素作为我们选中的元素(称作轴点),用一个变量保存(可以形象地认为那个地方被挖空了)。
    2.我们从数组最后开始,即a[7],判断该位置元素大小,如果比轴点大,说明该元素本来就应该是H区域的元素,不需要调换,继续往左遍历,等到了a[6]时,值2 比轴点值 4小,应该是属于L区域的元素,此时a[0]是空的(想象),我们就可以将这个值赋值到a[0]中,就相当于一个填坑动作,经过这个动作以后,a[6]这个位置就相当于是空的。
    3.经历过一次填坑动作以后,我们遍历的顺序就需要改变一下了,就要从a[0]开始往右遍历了(为了代码的简约易读),a[0]刚换过来的肯定是不大于轴点的,然后到a[1],发现比轴点小,继续往右,到a[2],比轴点大,那么就应该进行一次填坑操作,将值a[6]赋值为5,此时a[2]变成了坑,遍历再次反转,注意!我们这次回到的位置是刚被填完坑的地方,即从a[6]开始往左遍历,循环往复。
    4.这一系例操作的终止标志应该是当左遍历撞上了右遍历,更精确的描述应该是:有两个指针 Lo , Hi ,分别指向数组最左端a[0],最右端a[7],并跟随着遍历移动,当 Lo < Hi 时,就一直反复执行上述步骤,直到这个条件被打破, 此时Lo 和 Hi 指向了相同的一个位置,这个位置非常特殊,因为这个位置是一个坑,我们用轴点的值来填补它,此时这个位置便就是我们选取的元素的最终位置。

首次排序后的数组

  最终我们得到了这个结果,虽然它不是有序的,但是显然这距离完全有序迈进了一步,我们上面的一顿操作使这个数组产生了一些原来不一定存在的特性:

我们所选取的轴点已经到达了它最终的位置,L区域的所有元素不必轴点元素大,H区域的所有元素都不比轴点元素小。

  这是非常有用的信息,这意味着我们可以将产生的L区域和H区域当作一个新的更小规模的待排序数组,再次继续重复迭代地操作,每一次迭代,我们选取的那个区域的轴点元素,都可以找到它最终的位置,直到这个数组退化到本身就有序的单个元素的数组,将大问题转化成一个个子问题,并逐一解决,至此排序完成。

注意

  现在我们需要考虑这个轴点的选取问题,理想状态下我们当然是希望每次选取的这个轴点都是这组待排序列的中值(中位数),这样便可以均等划分L、H区间,这是最优的“”策略,然而寻找中值所花费的代价是我们所不能忍受的,于是便退而求其次,采用一些较为好实现的方法。通常的办法有首、末选元法、随机选元法、三数中值分割法

代码

  首先是快速排序的基本算法框架QuickSort()

typedef int Rank;

void QuickSort(int *t , Rank lo =0 , Rank hi = 10){
    if(hi - lo < 2) return; //递归基 如果hi和lo差距仅有1,即仅有一个元素的比对,直接返回
    Rank mid = partition(t,lo,hi-1); //在[lo,hi-1)之间构造轴点
    QuickSort(t,lo,mid);
    QuickSort(t,mid+1 ,hi);
}

  由此可以发现,快速排序算法耗费的时间绝大部分是来自分割函数partition(),除此之外的消耗仅占 o(1)的时间。

void swap(int *a ,int *b){
    int temp = *a ;
    *a = *b;
    *b = temp;
}


Rank partition(int *t, Rank lo , Rank hi){
    swap(&t[lo] , &t[lo+rand()%(hi-lo+1)]); //任选一个元素与首元素互换,保证选区的轴点不极端偏移
    int pivot = t[lo]; //选取转换后首元素为轴点
    while(lo < hi){
        //在每一次都要保证lo < hi的前提下,将比pivot大的元素放在它的右边,同理小的放左边 
        while((lo < hi) & (pivot <= t[hi])) {hi--;} t[lo] = t[hi];
        while((lo < hi) & (t[lo] <= pivot)) {lo++;} t[hi] = t[lo];  
    }
    t[lo] = pivot;
    return lo;
}

  上述寻找轴点的方式用的是随机选元法,还要注意一个小细节就是遍历时每一次头尾两个指针的移动都可能会造成 Lo < Hi 这个条件不成立,所以每一次遍历成功的前提就是需要满足这个条件。

进一步优化

 while(lo < hi){
        //在每一次都要保证lo < hi的前提下,将比pivot大的元素放在它的右边,同理小的放左边 
        while((lo < hi) & (pivot <= t[hi])) {hi--;} t[lo] = t[hi];
        while((lo < hi) & (t[lo] <= pivot)) {lo++;} t[hi] = t[lo];  
    }

  对于上述核心步骤,我们发现,如果数组是一个元素均重复的退化情况,就会造成最后分配的L区域和H区域的元素数量相差巨大(假设总数是N,第一次L区域有N-1个元素,H区域居然是空!!),更可怕的是,在后续迭代时,这种情况会一直存在,导致算法等效退化为线性递归,总体运行时间将高达o(n^2),这是我们所不能接受的。

Rank partition(int *t, Rank lo , Rank hi){
    //优化部分
    swap(&t[lo] , &t[lo+rand()%(hi-lo+1)]); //任选一个元素与首元素互换,保证选区的轴点不极端偏移
    int pivot = t[lo]; //选取转换后首元素为轴点
    while(lo < hi){
        while(lo < hi){
            if(pivot < a[hi])
                hi--;
            else
                {a[lo++] = a[hi]; break;}
        }

        while(lo < hi){
            if(a[lo] < pivot)
                lo++;
            else
                {a[hi--] = a[lo]; break;}
        }
    }
    t[lo] = pivot;
    return lo;
}

  优化后的核心步骤中加了两个关键的break语句,使得就算是元素重复的完全退化数组,找到的轴点也必定在数组居中部位,并且每一次迭代都是如此。

复杂度

最坏情况下

假设虽然采用了一些寻找轴点的策略,但不走运的是每次找到的轴点均是当前待排数组的边界,则算法就如同未优化的partition函数遇到均是重复元素的退化数组一般,退化成线性递归,时间复杂度高达o(n^2)!!!

平均复杂度

当然,通常情况下我们可没这么倒霉,遇到上述情况的次数可以说是寥寥无几,而相应的,快速排序的平均复杂度是相当喜人的,可以达到o(nlogn),并且其时间复杂度中的常系数更小,例如,采用随机采元法的快速排序算法的平均效率是o(2ln2logn),大约是o(1.386*logn)!! (具体数学推导方法参照清华大学邓俊辉著的《数据结构》第三版p339页)

完整代码加测试
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
typedef int Rank;


void swap(int *a ,int *b){
    int temp = *a ;
    *a = *b;
    *b = temp;
}


// Rank partition(int *t, Rank lo , Rank hi){
//  swap(&t[lo] , &t[lo+rand()%(hi-lo+1)]); //任选一个元素与首元素互换,保证选区的轴点不极端偏移
//  int pivot = t[lo]; //选取转换后首元素为轴点
//  while(lo < hi){
//      //在每一次都要保证lo < hi的前提下,将比pivot大的元素放在它的右边,同理小的放左边 
//      while((lo < hi) & (pivot <= t[hi])) {hi--;} t[lo] = t[hi];
//      while((lo < hi) & (t[lo] <= pivot)) {lo++;} t[hi] = t[lo];  
//  }
//  t[lo] = pivot;
//  return lo;
// }    


Rank partition(int *t, Rank lo , Rank hi){
    //优化部分
    swap(&t[lo] , &t[lo+rand()%(hi-lo+1)]); //任选一个元素与首元素互换,保证选区的轴点不极端偏移
    int pivot = t[lo]; //选取转换后首元素为轴点
    while(lo < hi){
        while(lo < hi){
            if(pivot < t[hi])
                hi--;
            else
                {t[lo++] = t[hi]; break;}
        }

        while(lo < hi){
            if(t[lo] < pivot)
                lo++;
            else
                {t[hi--] = t[lo]; break;}
        }
    }
    t[lo] = pivot;
    return lo;
}   

 
void QuickSort(int *t , Rank lo =0 , Rank hi = 10){
    if(hi - lo < 2) return; //递归基 如果hi和lo差距仅有1,即仅有一个元素的比对
    Rank mid = partition(t,lo,hi-1); //在[lo,hi-1)之间构造轴点
    QuickSort(t,lo,mid);
    QuickSort(t,mid+1 ,hi);
}


void  getArray(int *a){
    srand((unsigned)time(NULL));
    for(int i = 0 ;  i < 10 ; i++){
        a[i] = rand()%100 + 1; 
    }
}

void printArray(int *a){
    int i = 0 ;
    while(i < 10){
        printf("%d ",a[i]);
        i++;
    }
    printf("\n");
}

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

推荐阅读更多精彩内容