快速排序算法原理及实现(单轴快速排序、三向切分快速排序、双轴快速排序)

欢迎探讨,如有错误敬请指正

如需转载,请注明出处http://www.cnblogs.com/nullzx/

1. 单轴快速排序的基本原理

快速排序的基本思想就是从一个数组中任意挑选一个元素(通常来说会选择最左边的元素)作为中轴元素,将剩下的元素以中轴元素作为比较的标准,将小于等于中轴元素的放到中轴元素的左边,将大于中轴元素的放到中轴元素的右边,然后以当前中轴元素的位置为界,将左半部分子数组和右半部分子数组看成两个新的数组,重复上述操作,直到子数组的元素个数小于等于1(因为一个元素的数组必定是有序的)。

以下的代码中会常常使用交换数组中两个元素值的Swap方法,其代码如下

public static void Swap(int[] A, int i, int j){
    int tmp;
    tmp = A[i];
    A[i] = A[j];
    A[j] = tmp;
}

2. 快速排序中元素切分的方式

快速排序中最重要的就是步骤就是将小于等于中轴元素的放到中轴元素的左边,将大于中轴元素的放到中轴元素的右边,我们暂时把这个步骤定义为切分。而剩下的步骤就是进行递归而已,递归的边界条件为数组的元素个数小于等于1。以首元素作为中轴,看看常见的切分方式。

2.1 从两端扫描交换的方式

基本思想,使用两个变量i和j,i指向首元素的元素下一个元素(最左边的首元素为中轴元素),j指向最后一个元素,我们从前往后找,直到找到一个比中轴元素大的,然后从后往前找,直到找到一个比中轴元素小的,然后交换这两个元素,直到这两个变量交错(i > j)(注意不是相遇 i == j,因为相遇的元素还未和中轴元素比较)。最后对左半数组和右半数组重复上述操作。

public static void QuickSort1(int[] A, int L, int R){
    if(L < R){//递归的边界条件,当 L == R时数组的元素个数为1个
        int pivot = A[L];//最左边的元素作为中轴,L表示left, R表示right
        int i = L+1, j = R;
        //当i == j时,i和j同时指向的元素还没有与中轴元素判断,
        //小于等于中轴元素,i++,大于中轴元素j--,
        //当循环结束时,一定有i = j+1, 且i指向的元素大于中轴,j指向的元素小于等于中轴
        while(i <= j){
            while(i <= j && A[i] <= pivot){
                i++;
            }
            while(i <= j && A[j] > pivot){
                j--;
            }
            //当 i > j 时整个切分过程就应该停止了,不能进行交换操作
            //这个可以改成 i < j, 这里 i 永远不会等于j, 因为有上述两个循环的作用
            if(i <= j){
                Swap(A, i, j);
                i++;
                j--;
            }
        }
        //当循环结束时,j指向的元素是最后一个(从左边算起)小于等于中轴的元素
        Swap(A, L, j);//将中轴元素和j所指的元素互换
        QuickSort1(A, L, j-1);//递归左半部分
        QuickSort1(A, j+1, R);//递归右半部分
    }
}

2.2 两端扫描,一端挖坑,另一端填补

基本思想,使用两个变量i和j,i指向最左边的元素,j指向最右边的元素,我们将首元素作为中轴,将首元素复制到变量pivot中,这时我们可以将首元素i所在的位置看成一个坑,我们从j的位置从右向左扫描,找一个小于等于中轴的元素A[j],来填补A[i]这个坑,填补完成后,拿去填坑的元素所在的位置j又可以看做一个坑,这时我们在以i的位置从前往后找一个大于中轴的元素来填补A[j]这个新的坑,如此往复,直到i和j相遇(i == j,此时i和j指向同一个坑)。最后我们将中轴元素放到这个坑中。最后对左半数组和右半数组重复上述操作。

public static void QuickSort2(int[] A, int L, int R){
  if(L < R){
      //最左边的元素作为中轴复制到pivot,这时最左边的元素可以看做一个坑
      int pivot = A[L];
      //注意这里 i = L,而不是 i = L+1, 因为i代表坑的位置,当前坑的位置位于最左边
      int i = L, j = R;
      while(i < j){
          //下面面两个循环的位置不能颠倒,因为第一次坑的位置在最左边
          while(i < j && A[j] > pivot){
              j--;
          }
          //填A[i]这个坑,填完后A[j]是个坑
          //注意不能是A[i++] = A[j],当因i==j时跳出上面的循环时
          //坑为i和j共同指向的位置,执行A[i++] = A[j],会导致i比j大1,
          //但此时i并不能表示坑的位置
          A[i] = A[j];
           
          while(i < j && A[i] <= pivot){
              i++;
          }
          //填A[j]这个坑,填完后A[i]是个坑,
          //同理不能是A[j--] = A[i]                
          A[j] = A[i];
      }
      //循环结束后i和j相等,都指向坑的位置,将中轴填入到这个位置
      A[i] = pivot;
       
      QuickSort2(A, L, i-1);//递归左边的数组
      QuickSort2(A, i+1, R);//递归右边的数组
  }
}

2.3 单端扫描方式

j从左向右扫描,A[1,i]表示小于等于pivot的部分,A[i+1,j-1]表示大于pivot的部分,A[j, R]表示未知元素

初始化时,选取最左边的元素作为中轴元素,A[1,i]表示小于等于pivot的部分,i指向中轴元素(i < 1),表示小于等于pivot的元素个数为0,j以后的都是未知元素(即不知道比pivot大,还是比中轴元素小),j初始化指向第一个未知元素。

当A[j]大于pivot时,j继续向前,此时大于pivot的部分就增加一个元素

上图中假设对A[j]与pivot比较后发现A[j]大于pivot时,j的变化

当A[j]小于等于pivot时,我们注意注意i的位置,i的下一个就是大于pivot的元素,我们将i增加1然后交换A[i]和A[j],交换后小于等于pivot的部分增加1,j增加1,继续扫描下一个。而i的下一个元素仍然大于pivot,又回到了先前的状态。

上图中假设对A[j]与pivot比较后发现A[j] <= pivot时,i,j的变化

public static void QuickSort3(int[] A, int L, int R){
    if(L < R){
        int pivot = A[L];//最左边的元素作为中轴元素
        //初始化时小于等于pivot的部分,元素个数为0
        //大于pivot的部分,元素个数也为0
        int i = L, j = L+1;
        while(j <= R){
            if(A[j] <= pivot){
                i++;
                Swap(A, i, j);
                j++;//j继续向前,扫描下一个
            }else{
                j++;//大于pivot的元素增加一个
            }
        }
        //A[i]及A[i]以前的都小于等于pivot
        //循环结束后A[i+1]及它以后的都大于pivot
        //所以交换A[L]和A[i],这样我们就将中轴元素放到了适当的位置
        Swap(A, L, i);
        QuickSort3(A, L, i-1);
        QuickSort3(A, i+1, R);
    }
}

3. 三向切分的快速排序

三向切分快速排序的基本思想,用i,j,k三个将数组切分成四部分,a[L, i-1]表示小于pivot的部分,a[i, k-1]表示等于pivot的部分,a[j+1]表示大于pivot的部分,而a[k, j]表示未判定的元素(即不知道比pivot大,还是比中轴元素小)。我们要注意a[i]始终位于等于pivot部分的第一个元素,a[i]的左边是小于pivot的部分。

我们选取最左边的元素作为中轴元素,初始化时,i = L,k = L+1,j=R(L表示最左边元素的索引,R表示最右边元素的索引)

通过上一段的表述可知,初始化时<pivot部分的元素个数为0,等于pivot部分元素的个数为1,大于pivot部分的元素个数为0,这显然符合目前我们对所掌握的情况。k自左向右扫描直到k与j错过为止(k > j)。我们扫描的目的就是逐个减少未知元素,并将每个元素按照和pivot的大小关系放到不同的区间上去。

在k的扫描过程中我们可以对a[k]分为三种情况讨论

(1)a[k] < pivot 交换a[i]和a[k],然后i和k都自增1,k继续扫描

(2)a[k] = pivot k自增1,k接着继续扫描

(3)a[k] > pivot 这个时候显然a[k]应该放到最右端,大于pivot的部分。但是我们不能直接将a[k]与a[j]交换,因为目前a[j]和pivot的关系未知,所以我们这个时候应该从j的位置自右向左扫描。而a[j]与pivot的关系可以继续分为三种情况讨论

3.1)a[j] > pivot j自减1,j接着继续扫描

3.2)a[j] == pivot 交换a[k]和a[j],k自增1,j自减1,k继续扫描(注意此时j的扫描就结束了)

3.3)a[j] < pivot: 此时我们注意到a[j] < pivot, a[k] > pivot, a[i] == pivot,那么我们只需要将a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。然后i和k自增1,j自减1,k继续扫描(注意此时j的扫描就结束了)

注意,当扫描结束时,i和j的表示了=等于pivot部分的起始位置和结束位置。我们只需要对小于pivot的部分以及大于pivot的部分重复上述操作即可。

public static void QuickSort3Way(int[] A, int L, int R){
    if(L >= R){//递归终止条件,少于等于一个元素的数组已有序
        return;
    }
     
    int i,j,k,pivot;
    pivot = A[L]; //首元素作为中轴
    i = L;
    k = L+1;
    j = R;
     
    OUT_LOOP:
    while(k <= j){
        if(A[k] < pivot){
            Swap(A, i, k);
            i++;
            k++;
        }else
        if(A[k] == pivot){
            k++;
        }else{// 遇到A[k]>pivot的情况,j从右向左扫描
            while(A[j] > pivot){//A[j]>pivot的情况,j继续向左扫描
                j--;
                if(j < k){
                    break OUT_LOOP;
                }
            }
            if(A[j] == pivot){//A[j]==pivot的情况
                Swap(A, k, j);
                k++;
                j--;
            }else{//A[j]<pivot的情况
                Swap(A, i, j);
                Swap(A, j, k);
                i++;
                k++;
                j--;
            }
        }
    }
    //A[i, j] 等于 pivot 且位置固定,不需要参与排序
    QuickSort3Way(A, L, i-1); // 对小于pivot的部分进行递归
    QuickSort3Way(A, j+1, R); // 对大于pivot的部分进行递归
}

4. 双轴快速排序

双轴快速排序算法思路和三向切分快速排序算法的思路基本一致,双轴快速排序算法使用两个轴,通常选取最左边的元素作为pivot1和最右边的元素作pivot2。首先要比较这两个轴的大小,如果pivot1 > pivot2,则交换最左边的元素和最右边的元素,已保证pivot1 <= pivot2。双轴快速排序同样使用i,j,k三个变量将数组分成四部分

A[L+1, i]是小于pivot1的部分,A[i+1, k-1]是大于等于pivot1且小于等于pivot2的部分,A[j, R]是大于pivot2的部分,而A[k, j-1]是未知部分。和三向切分的快速排序算法一样,初始化i = L,k = L+1,j=R,k自左向右扫描直到k与j相交为止(k == j)。我们扫描的目的就是逐个减少未知元素,并将每个元素按照和pivot1和pivot2的大小关系放到不同的区间上去。

在k的扫描过程中我们可以对a[k]分为三种情况讨论(注意我们始终保持最左边和最右边的元素,即双轴,不发生交换)

(1)a[k] < pivot1 i先自增,交换a[i]和a[k],k自增1,k接着继续扫描

(2)a[k] >= pivot1 && a[k] <= pivot2 k自增1,k接着继续扫描

(3)a[k] > pivot2: 这个时候显然a[k]应该放到最右端大于pivot2的部分。但此时,我们不能直接将a[k]与j的下一个位置a[--j]交换(可以认为A[j]与pivot1和pivot2的大小关系在上一次j自右向左的扫描过程中就已经确定了,这样做主要是j首次扫描时避免pivot2参与其中),因为目前a[--j]和pivot1以及pivot2的关系未知,所以我们这个时候应该从j的下一个位置(--j)自右向左扫描。而a[--j]与pivot1和pivot2的关系可以继续分为三种情况讨论

   3.1)a[--j] > pivot2 j接着继续扫描

   3.2)a[--j] >= pivot1且a[j] <= pivot2 交换a[k]和a[j],k自增1,k继续扫描(注意此时j的扫描就结束了)

   3.3) a[--j] < pivot1 先将i自增1,此时我们注意到a[j] < pivot1,  a[k] > pivot2,  pivot1 <= a[i] <=pivot2,那么我们只需要将a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。k自增1,然后k继续扫描(此时j的扫描就结束了)

注意

1. pivot1和pivot2在始终不参与k,j扫描过程。

2. 扫描结束时,A[i]表示了小于pivot1部分的最后一个元素,A[j]表示了大于pivot2的第一个元素,这时我们只需要交换pivot1(即A[L])和A[i],交换pivot2(即A[R])与A[j],同时我们可以确定A[i]和A[j]所在的位置在后续的排序过程中不会发生变化(这一步非常重要,否则可能引起无限递归导致的栈溢出),最后我们只需要对A[L, i-1],A[i+1, j-1],A[j+1, R]这三个部分继续递归上述操作即可。

public static void QuickSortDualPivot(int[] A, int L, int R){
    if(L >= R){
        return;
    }
     
    if(A[L] > A[R]){
        Swap(A, L, R); //保证pivot1 小于等于pivot2
    }
     
    int pivot1 = A[L];
    int pivot2 = A[R];
     
    //如果这样初始化 i = L+1, k = L+1, j = R-1,也可以
    //但代码中边界条件, i,j先增减,循环截止条件,递归区间的边界都要发生相应的改变
    int i = L;
    int k = L+1;
    int j = R;
 
    OUT_LOOP:
    while(k < j){
        if(A[k] < pivot1){
            i++;//i先增加,k扫描中pivot1就不参与其中
            Swap(A, i, k);
            k++;
        }else
        if(A[k] 大于等于pivot1 && A[k] 小于等于pivot2){
            k++;
        }else{
            while(A[--j] > pivot2){//j先增减,j首次扫描pivot2就不参与其中
                if(j <= k){//当i和j相遇
                    break OUT_LOOP;
                }
            }
            if(A[j] 大于等于pivot1 && A[j] 小于等于pivot2){
                Swap(A, k, j);
                k++;
            }else{
                i++;
                Swap(A, i, j);
                Swap(A, j, k);
                k++;
            }
        }
    }
    Swap(A, L, i);//将pivot1交换到适当位置
    Swap(A, R, j);//将pivot2交换到适当位置
     
    //一次双轴切分至少确定两个元素的位置,这两个元素将整个数组区间分成三份
    QuickSortDualPivot(A, L, i-1);
    QuickSortDualPivot(A, i+1, j-1);
    QuickSortDualPivot(A, j+1, R);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容

  • quicksort可以说是应用最广泛的排序算法之一,它的基本思想是分治法,选择一个pivot(中轴点),将小于pi...
    黎景阳阅读 434评论 0 1
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,149评论 0 52
  • JavaScript 参考手册这个链接要参考,多使用 JavaScript 中的所有事物都是对象:字符串、数值、数...
    勇往直前888阅读 394评论 0 1
  • 嘻呵阅读 180评论 0 1
  • 有小空闲的时候常浏览《今日头条》里的搞笑动图,然后把笑出来的图存下来,留着回去后慢慢看。 不知不觉我已经下载了七百...
    十大恶人阅读 177评论 1 0