`
喻红叶
  • 浏览: 41779 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

快速排序算法

 
阅读更多

算法思想

快速排序(quicksort)是在实践中最快的已知排序算法,快速排序算法是一种分治的递归算法,对数组S进行快速排序的步骤:

(1)如果S中的元素个数是0或者1,则返回;

(2)取S中任一元素v,称之为枢纽元(pivot);

(3)将S - { v }分成一个不相交的集合,S1 = { x属于S - { v } | x <= v },S2 ={ x属于S - { v } | x >= v };

(4)quicksort( S1 ),v,quicksort( S2 )。

简单的说:在数组中选择一个枢纽元,然后将数组划分成两部分,左边的部分元素全部小于等于枢纽元,右边的部分全部大于等于枢纽元,在左右两边递归的进行这种分割。在快速排序中如何选择枢纽元是一个重要的决策,理想的情况是分割出来的两个集合大致相等,约等于原集合的一半。

算法过程

首先给出算法导论中的快排算法:

quicksort(A,p,r)
 if p < r
   then q = PARTITION(A,p,r)
        quicksort(A,p,q - 1)
        quicksort(A,q + 1,r)

其中A是待排序数组,p和r分别是本次处理的左边界和右边界。算法的重点是PARTITION(A,p,r),它将A分割成S1和S2,返回的q是枢纽元保存的位置。算法导论中给出的PARTITION(A,p,r)是单边移动的,也就是说每次只从左边移动分割数组。

PARTITION(A,p,r)
 x = A[r]
 i = p -  1
 for j = p to  r -1
   if A[j] <= x
      then i = i + 1;
           exchange A[i] <--> A[j]
exchange A[i + 1] <--> A[r]
return i + 1
上面的算法中,总是选择最右边元素作为枢纽元,j从左边界开始遍历数组,A[j]小于等于枢纽元,就将A[j]与A[i + 1]交换,因为A[i + 1]大于枢纽元或者就是当前调整的元素(A[j])。i的下一个元素是大于枢纽元或者是A[j]。下面以序列:2 8 7 1 3 5 6 4为例,演示上述过程:


最右边的元素4被选为枢纽元,i初始为p -1,i的下一个元素大于枢纽元或者是正在处理的元素,图中j的位置表示下一趟要处理的元素。红色矩形里的元素表示已经分割过的小于枢纽元的元素。

描述一下图中的过程:初始时i = -1,j = 0

(1)第一次,2 < 4,将i加1,交换A[i] <--> A[j],其实就是2和它自己交换;++j

(2)第二次,8 > 4,i不变,++j

(3)第三次,7 > 4,i不变,++j

(4)第四次,1 < 5,++i,A[i] <--> A[j],也就是1和8交换;++j

(5)第五次,3 < 4,++i,A[i] <--> A[j],也就是3和7交换;++j

(6)第六次,5 > 4,i不变,++j

(7)第七次,6 > 4,i不变,++j

一次分割完成,A[i]是最后一个小于枢纽元的元素,A[i + 1]则大于枢纽元,所以将A[i + 1]与A[r]交换,就将数组A分割成三部分:S1,枢纽元,S2,可以看到,枢纽元已经在正确的位置上了。代码实现如下,这个代码将PARITION合并到了quicksort里了:

void quicksort(int* a,int p,int r) {
    //如果是0或者1个元素,就返回
    if(p >= r)
        return ;
    int i = p - 1;
    int j;
    int pivot = a[r];
    //<=i指向的是小于pivot的元素
    for(j = p; j < r; j++) {
        if(a[j] <= pivot) {
            ++i;
            swap(&a[i],&a[j]);
        }
    }
    //没有必要使用q,是为了跟上述的算法一致,q保存的是枢纽元
    int q = i + 1;
    swap(&a[q],&a[right]);
    //递归调用
    quicksort(a,p,q - 1);
    quicksort(a,q + 1,r);
}

选取枢纽元

快速排序算法的性能依赖于枢纽元的选取,我们希望每次分割都能得到两个大小相等的集合,可是如果枢纽元选取不当的话,最坏的情形是S1和S2中有一个为空,那么快速排序的时间花费是O(n^2)。选取第一个元素或者最后一个元素绝对是愚蠢的,如果序列是预排序的,那么最坏的情况就会发生;选取前两个互异关键字中较大者作为枢纽元,和只选取第一个作为枢纽元具有同样的害处。一种安全的策略是随机选择枢纽元,但这并不是非常好的做法。

枢纽元最好是选择数组的中值,但是这很困难,这样的中值可以通过随机选取三个元素,并用它们的中值作为枢纽元,由此得到三数中值分割法。

三数中值分割法

随机选择三个数并没有太大的帮助性,一般的做法是选择左端、右端和中心点三个数的中值作为枢纽元,显然三数中值分割法可以消除预排序输入的最坏情形。前面讲的分割过程是单边的,下面将一个双边分割的过程,双边分割的过程如下:

(1)将枢纽元与最后一个元素交换,这样枢纽元就能离开待分割的数据段;

(2)i从第一个元素开始,j从倒数第二个元素开始,因为最后一个元素师枢纽元。

(3)当i在j的左边时,i右移,移过那些小元素,一旦i碰到一个大元素立即停止;j左移,移过那些大元素,一旦j碰到一个小元素立即停止;

(4)此时i指向一个大元素,j指向一个小元素;如果i在j的左边,交换两个元素,效果相当于将一个大元素移到右边,同时将一个小元素移到左边。

还是需要具体的过程来演示,使用序列:2 8 7 1 6 3 9 5 4,使用三数中值策略选择枢纽元6,将6与最后一个元素交换,得到如下序列及i,j的初始位置

i2 8 7 1 4 3 9 j5 | 6

第一次:i右移,直到遇到元素大于枢纽元,j左移,直到遇到元素小于枢纽元 2 i8 7 1 4 3 9 j5 | 6

交换i和j指向的元素,第一次交换后得到的序列(和上一行公用i和j): 2 i5 7 1 4 3 9 j8 | 6

第二次:i右移,j左移:2 5 i7 1 4 j3 9 8 | 6

交换i和j指向的元素, 2 5 i3 1 4 j7 9 8 | 6

第三次:i右移,j左移:2 5 31 j4 i79 8 | 6

可以看到i和j已经交错了,分割结束。可以看出,<i位置上都是小于枢纽元的元素,i位置是第一个大于枢纽元的元素,所以将i与右端枢纽元交换,一次分割完成

2 5 31 469 8 7

等于枢纽元的处理

前面考虑的所有元素跟枢纽元都是互异的,那么如果i、j遇到等于枢纽元的元素应该怎么办?有一点是确定的,i和j应该做相同的动作,否则分割将偏向一方。考虑一种极端的情况:所有元素都相等。

如果i和j都停止,那么i和j将进行很多次的交换,虽然这没有意义,但是i和j将在中间交错,其效果是划分了两个相等的子数组,运行时间是O(N logN);

如果i和j都不停止,那么i将移动到倒数第二个位置,交换后,枢纽元就在倒数第二个位置上,这样划分了两个极不平衡的子数组。运行时间是O(N^2);

所以,我们发现进行不必要的交换得到两个均衡的子数组要比蛮干冒险得到两个不均衡的数组要好,因此,如果i和j遇到等于枢纽元的元素,那么就让它们都停止。

选取中值

选取三个数的中值,比较容易的做法是对三个数进行排序,这样A[mid]自然就是想要的枢纽元。同时,这样做还有一个好处,左端的元素和右端的元素都已经处在了正确的位置上。然后交换A[mid]和A[right - 1],这样使得枢纽元离开待分割序列。分割时,i从left + 1开始,j从right - 2开始。

int median3(int A[],int left,int right) {
    int mid = (left + right) / 2;
    if(A[left] > A[mid])
        swap(&A[left],&A[mid]);
    if(A[left] > A[right])
        swap(&A[left],&A[right]);
    if(A[mid] > A[right])
        swap(&A[mid],&A[right]);
    //枢纽元和A[right - 1]交换
    swap(&A[mid],&A[right - 1]);
    return A[right - 1];
}

void quicksort(int *A,int left,int right) {
    if(left >= right)
        return ;

    int pivot = Median3(A,left,right);
    //如果小于等于三个元素的话,选中值时已经排序完成
    if((right - left) < 3)
        return;

    int i = left;
    int j = right-1;

    for(;;) {
        while(A[++i] < pivot);
        while(A[--j] > pivot);
        if(j < j) 
            swap(&A[i],&A[j]);
        else
            break;

    }
    swap(&A[i],&A[right - 1]);
    quicksort(p,left,i - 1);
    quicksort(p,i + 1,right);
}

由于A[left] <= pivot,所以j如果移动到了left就会停止,同样,i如果移动到了right - 1(枢纽元的位置)也会停止,防止了越界的产生。有一点需要注意的是,对于小数组(N<=20),快速排序不如插入排序好。快速排序是递归的,递归到最后几层的时候,基本上都是小数组。因此,一种好的策略是,当N=10时,不再采用快速排序,而使用插入排序。

时间复杂度分析

快速排序是递归的,每次将数组分成一个长度为i的子数组和长度为N-i-1的子数组,所以快速排序的时间等于两个递调用的时间加上线性分割的时间:

T(N) = T(i) + T(N - i -1) + cN

最坏情形分析

最快的情形就是每次分割,其中一个子数组为空,另一个为N-1,此时

T(N) = T(N - 1) + cN (1)

T(N - 1) = T(N -2) + c(N - 1)

T(N - 2) = T(N - 3) + c(N - 2)

......

T(2) = T(1) + c(2)

将上面式子相加得到:T(N) + T(N - 1) + T(N - 2) + ... + T(2) = T(N - 1) + T(N - 2) + ... + T(1) + c((N - 1) + (N -2) +... + 3 + 2)

T(N) = T(1) + c(2 + 3 +...+ N -1) = O( N^2)

最好情形分析

最好的情形就是每次分割成两个大小相等的子数组,约等于N/2

T(N) = 2T(N / 2) + cN (*)

等式两边同时除以N,得到:

T(N) / N = T(N / 2) / (N / 2) + c (**)

重复上面的步骤:

T(N / 2) / (N / 2) = T(N / 4) / (N / 4) + c (***)

T(N / 4) / (N / 4 ) =T(N / 8) / (N / 8) + c (****)

.....

T(2) / 2 = T(1) / 1 + c

将上述所有式子相加:

T(N) / N +T(N / 2) / (N / 2) +T(N / 4) / (N / 4)...+ T(2) / 2 =T(N / 2) / (N / 2) +T(N / 4) / (N / 4) + ... T(1) + clogN(递归logN次,所以上面有logN个式子)

将相同的项消去,得到:T(N) / N = T(1) + clogN

T(N) = cNlogN + N = O(NlogN)

平均情形分析

假设每次分割总产生9:1的分割,虽然这看上去很不均衡,其分割如下图所示


只有一点需要说明,那就是每次层最多需要cn时间,所以平均数回见复杂度是:O(NlogN)。

转载请注明出处:喻红叶《快速排序算法》

分享到:
评论

相关推荐

    快速排序算法相关分析

    快速排序算法相关分析 快速排序算法是一种有效的排序算法,虽然算法在最坏的情况下运行时间为 O(n^2),但由于平均运行时间为 O(nlogn),并且在内存使用、程序实现复杂性上表现优秀,尤其是对快速排序算法进行随机化...

    FPGA并行快速排序算法-位宽可设

    在本文中,我们将深入探讨基于FPGA的并行快速排序算法,特别关注“位宽可设”的特性。这种算法能够高效地处理大量数据,并且在硬件实现上具有很高的灵活性。我们将从以下几个方面来阐述这个主题: 一、快速排序算法...

    快速排序算法以及归并算法

    ### 快速排序算法 快速排序是一种高效的排序算法,采用分而治之的策略,其核心思想是选择一个基准元素,通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再...

    快速排序算法matlab

    ### 快速排序算法在MATLAB中的应用与理解 #### 一、快速排序算法简介 快速排序是一种非常高效的排序算法,由英国计算机科学家托尼·霍尔(Tony Hoare)于1960年提出。它采用分治策略来把一个序列分为较小的两个子...

    C语言实现快速排序算法

    快速排序算法,C语言 快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有...

    c语言 数据结构 快速排序算法

    c语言版本的数据结构的快速排序算法,适用于新手学习

    快速排序算法实现

    快速排序算法C语言实现,快排序算法QuickSort.cpp

    快速排序算法实现 c#代码

    ### 快速排序算法在C#中的实现 #### 一、快速排序算法简介 快速排序是一种高效的排序算法,由英国计算机科学家托尼·霍尔(Tony Hoare)于1960年提出。该算法采用分治策略来把一个序列分为较小的两个子序列,再对这...

    快速排序算法java代码

    "快速排序算法java代码" 快速排序算法是由Tony Hoare在1960年提出的一种排序算法,它的平均时间复杂度为O(n log n),是目前最快的排序算法之一。下面我们将详细地讲解快速排序算法的java代码实现。 快速排序算法的...

    严蔚敏快速排序算法的伪代码。。。。。

    根据提供的标题、描述、标签及部分内容,我们可以梳理出与快速排序算法相关的关键知识点。 ### 快速排序算法概述 快速排序是一种高效的排序算法,在实际应用中,它的平均性能通常优于其他时间复杂度为 \(O(n\log n...

    快速排序算法快速排序算法PDF

    快速排序算法是一种高效的排序方法,由英国计算机科学家托尼·霍尔(C.A.R.Hoare)在1962年提出。该算法的基本思想是选择一个基准值(pivot),通过一趟排序将待排序的记录分割成独立的两部分,其中一部分的所有记录...

    分别使用Java和Python实现快速排序算法.zip

    快速排序:分别使用Java和Python实现快速排序算法.zip 快速排序:分别使用Java和Python实现快速排序算法.zip 快速排序:分别使用Java和Python实现快速排序算法.zip 快速排序:分别使用Java和Python实现快速排序算法....

    C++快速排序算法程序

    C++快速排序算法程序,用于处理大量数据, 并对这些数据进行快速的排序

    改进的快速排序算法

    快速排序算法的改进思路 1.选取好的基准,是两边的数据个数尽量的均匀 取数组的第一个,中间,最后一个数据,取三个数中第二大的数作为基准 2. 不递归 3.与插入结合,当段内的数组个数小于等于16的时候,使用...

    快速排序算法 C语言实现

    快速排序算法 C语言实现 快速排序算法是一种高效的排序算法,通过递归的方式将记录分区排序。下面将详细介绍快速排序算法的实现细节。 首先,我们需要了解快速排序算法的基本思想。快速排序算法的核心思想是选择一...

    Java实现快速排序算法+编程知识+技术开发

    Java实现快速排序算法+编程知识+技术开发; Java实现快速排序算法+编程知识+技术开发; Java实现快速排序算法+编程知识+技术开发; Java实现快速排序算法+编程知识+技术开发; Java实现快速排序算法+编程知识+技术...

    快速排序算法和冒泡排序效率对比

    快速排序和冒泡排序是两种常见的排序算法,它们在计算机科学中扮演着重要的角色,特别是在数据处理和优化程序性能方面。本篇文章将深入探讨这两种排序算法的原理、效率以及它们在C#编程语言中的实现。 首先,让我们...

    matlab 实现快速排序算法_一共有主程序,分类,插入和选择四个程序

    资源名:matlab 实现快速排序算法_一共有主程序,分类,插入和选择四个程序 资源类型:matlab项目全套源码 源码说明: 全部项目源码都是经过测试校正后百分百成功运行的,如果您下载后不能运行可联系我进行指导或者...

Global site tag (gtag.js) - Google Analytics