快速排序(Quick Sort)称划分交换排序。其基本思想是:在当前无序区R[1]到R[h]到中任取一个记录作为比较的“基准”(不妨记为temp),用此基准将当前无序区划分为左右两个较小的无序子区:R[1]到R[i-1]和R[i+1]到R[h],且左边的无序子区中记录的关键字均小于或等于基准temp的关键字,右边的无序子区中记录的关键字均大于或等于基准temp的关键字,而基准temp则位于最终排序的位置上
R[1]到R[i-1]中关键字≤temp.key=R[i+1]到R[h]的关键字(1≤i≤h)
当R[1]到R[I-1]和R[I+1]到R[h]均非空时,分别对它们进行上述的划分过程,直至所有无序子区中记录均已排好序为止。
要完成对当前无序区R[1]到R[h]的划分,具体做法是:设置两个指针i和j,它们的初值分别为i=1和j=h。不妨取基准为无序区的第1个记录R[i](即R[1]),并将它保存在变量temp中。令j自h起向左扫描,直到找到第1个关键字小于temp.key的记录R[j],将R[j]移至i所指的位置上(这相当于交换了R[j]和基准R[i](即temp)的位置,使关键字小于基准关键字的记录移到了基准的左边);然后,令i自i+1起向右扫描,直至找到第1个关键字大于temp.key的记录R[i],将R[i]移至 j指的位置上(这相当于交换了R[j]和基准R[i](即temp)的位置,使关键字大于基准关键字的记录移到了基准的右边);接着,令j自j+1起向右扫描,如此交替改变扫描方向,从两端各自往中间靠拢,直至i=j时,i便是基准x的最终位置,将x放在此位置 上就完成了一次划分。
综合上面的叙述,下面分别给出一次划分及其排序的算法。
int partition(r,1,h) /*返回划分后被定们的基准记录的位置*/
rectype R[ ]; /*对无序区R[1]到R[h]做划分*/
int 1,h;
{int i,j;
rectype temp;
i=1;j=h temp=R[i]; /*初始化,temp为基准*/
Do{
While((R[j].key> =temp.key) && (i <j))
j--; /*从右向左扫描,查找第1个关键字小于temp.key的记录*/
if(i <j) R[i++]=R[j]; /*交换R[i]和R[j]*/
while((R[i].key <=temp.key) && (i <j))
i++; /*从左向左扫描,查找第1个关键字大于temp.key的记录*/
if(i <j) R[j--]=R[i]; /*交换R[i]和R[j]*/
}
quicksort(R,s1,t1) /*对R[s1]到R[t1]*/
rectype R[ ];
int s1,t1;
{int i;
if (s1 <t1) /* 只有一个记录或无记录须排序*/
{i= partition (R,s1,t1); /*对R[s1]到R[t1]做划分*/
quicksort (R,s1,i-1); /*递归处理左区间*/
quicksort (R,i+1,t1); /*递归处理右区间*/
}
}
图9-7展示了一次划分的过程及整个快速排序的过程。图中方括号表示无序区,方框表示基准temp的关键字,它未参加真正的交换,只是在划分完成时才将它放入正确的位置上。
初始关键字 [[49 ] 38 65 97 76 13 27 49`]
i j
j向左扫描 [[49] 38 65 97 76 13 27 49 `]
i j
第一次交换后 [27 38 65 97 76 13 [ ] 49`]
i j
i向右扫描 [27 38 65 97 76 13 [ ] 49 `]
i j
第二次交换后 [27 38 [ ] 97 76 13 65 49 `]
i j
j向左扫描,位置不变
第三次交换后 [27 38 13 97 76 [ ] 65 49]`]
i向左扫描,位置不变, i j
第四次交换后 [27 38 13 [ ] 76 97 65 49 `]
i j
j 向左扫描 [27 38 13 [49] 76 97 65 49 `]
i j
初始关键字: [49 38 65 97 76 13 27 49`]
一趟排序之后: [27 38 13] 49 [76 97 65 49`]
二趟排序之后: [13] 27 [38] 49 [49` 65] 76 [97]
三趟排序之后: 13 27 38 49 49` [65] 76 97
最后的排序结果: 13 27 38 79 79` 65 76 97
(b) 各趟排序之后的状态
最坏情况是第次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的基准左边的无序子区为空(或右边的无序子区为空),而划分所得的另一个非空的无序子区中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做n-1趟,每一趟中需进行n-i次比较,故总手工艺比数次数达到最大值:
Cmax=∑ (n-i)=n(n-1)/2=O(n2)
显然,如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。
在最好情况下,每次划分所取的基准都是当前无序区的“中值”记录,划分的结果是基准的左、右两个无序子区的长度大致相等地。设C(n)表示对长度为n的文件进行快速排序所需的比较次数,显然,它应该等于对长度为n的无序区进行划分所需的比较次数n-1。加上递归地对划分所得的左、右两个无序子区(长度≤n/2)进行快速排序所需的比较总人
数。
假设文件长度n=2k,那么总的比较次数为:
C(n) ≤n+2C(n/2)
≤n+2[n/2+2C(n/22)]=2n+4C(n/22)
≤2n+4[n/4+2C(n/23)]=3n+8C(n/23)
≤……
≤kn+2kC(n/2k)=nlog2n+nC(1)
=O(nlog2n)
注意:式中C(1)为一常数,k=log2n。 ,
因为快速排序的记录移动次数不大于比较的次数,所以,快速排序的最坏时间复杂度应为O(n2),最好时间复杂雅兴O(log2n)。为了改善最坏情况下的时间性能,可采用三者取中的规则,即在每一趟划分开始前,首先比较R[1].key,R[h].key和R[[(1+h)/2]].key,令三者中取中值的记录和R[1]交换之。
可以证明:快速排序的平均时间复杂度也是O(nlog2n),它是目前基于比较的内部排序方法 中速度最快的,快速排序亦因此而得名。
快速排序需要一个栈空间来实现递归。若每次划分均能将文件均匀分割为两部分,则栈的最大深度为[log2n]+1,所需栈空间为O(log2n)。最坏情况下,递归深度为n,所需栈空间为O(n)。
快速排序是不稳定的,请读者自行检验。
R[1]到R[i-1]中关键字≤temp.key=R[i+1]到R[h]的关键字(1≤i≤h)
当R[1]到R[I-1]和R[I+1]到R[h]均非空时,分别对它们进行上述的划分过程,直至所有无序子区中记录均已排好序为止。
要完成对当前无序区R[1]到R[h]的划分,具体做法是:设置两个指针i和j,它们的初值分别为i=1和j=h。不妨取基准为无序区的第1个记录R[i](即R[1]),并将它保存在变量temp中。令j自h起向左扫描,直到找到第1个关键字小于temp.key的记录R[j],将R[j]移至i所指的位置上(这相当于交换了R[j]和基准R[i](即temp)的位置,使关键字小于基准关键字的记录移到了基准的左边);然后,令i自i+1起向右扫描,直至找到第1个关键字大于temp.key的记录R[i],将R[i]移至 j指的位置上(这相当于交换了R[j]和基准R[i](即temp)的位置,使关键字大于基准关键字的记录移到了基准的右边);接着,令j自j+1起向右扫描,如此交替改变扫描方向,从两端各自往中间靠拢,直至i=j时,i便是基准x的最终位置,将x放在此位置 上就完成了一次划分。
综合上面的叙述,下面分别给出一次划分及其排序的算法。
int partition(r,1,h) /*返回划分后被定们的基准记录的位置*/
rectype R[ ]; /*对无序区R[1]到R[h]做划分*/
int 1,h;
{int i,j;
rectype temp;
i=1;j=h temp=R[i]; /*初始化,temp为基准*/
Do{
While((R[j].key> =temp.key) && (i <j))
j--; /*从右向左扫描,查找第1个关键字小于temp.key的记录*/
if(i <j) R[i++]=R[j]; /*交换R[i]和R[j]*/
while((R[i].key <=temp.key) && (i <j))
i++; /*从左向左扫描,查找第1个关键字大于temp.key的记录*/
if(i <j) R[j--]=R[i]; /*交换R[i]和R[j]*/
}
quicksort(R,s1,t1) /*对R[s1]到R[t1]*/
rectype R[ ];
int s1,t1;
{int i;
if (s1 <t1) /* 只有一个记录或无记录须排序*/
{i= partition (R,s1,t1); /*对R[s1]到R[t1]做划分*/
quicksort (R,s1,i-1); /*递归处理左区间*/
quicksort (R,i+1,t1); /*递归处理右区间*/
}
}
图9-7展示了一次划分的过程及整个快速排序的过程。图中方括号表示无序区,方框表示基准temp的关键字,它未参加真正的交换,只是在划分完成时才将它放入正确的位置上。
初始关键字 [[49 ] 38 65 97 76 13 27 49`]
i j
j向左扫描 [[49] 38 65 97 76 13 27 49 `]
i j
第一次交换后 [27 38 65 97 76 13 [ ] 49`]
i j
i向右扫描 [27 38 65 97 76 13 [ ] 49 `]
i j
第二次交换后 [27 38 [ ] 97 76 13 65 49 `]
i j
j向左扫描,位置不变
第三次交换后 [27 38 13 97 76 [ ] 65 49]`]
i向左扫描,位置不变, i j
第四次交换后 [27 38 13 [ ] 76 97 65 49 `]
i j
j 向左扫描 [27 38 13 [49] 76 97 65 49 `]
i j
初始关键字: [49 38 65 97 76 13 27 49`]
一趟排序之后: [27 38 13] 49 [76 97 65 49`]
二趟排序之后: [13] 27 [38] 49 [49` 65] 76 [97]
三趟排序之后: 13 27 38 49 49` [65] 76 97
最后的排序结果: 13 27 38 79 79` 65 76 97
(b) 各趟排序之后的状态
最坏情况是第次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的基准左边的无序子区为空(或右边的无序子区为空),而划分所得的另一个非空的无序子区中记录数目,仅仅比划分前的无序区中记录个数减少一个。因此,快速排序必须做n-1趟,每一趟中需进行n-i次比较,故总手工艺比数次数达到最大值:
Cmax=∑ (n-i)=n(n-1)/2=O(n2)
显然,如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。
在最好情况下,每次划分所取的基准都是当前无序区的“中值”记录,划分的结果是基准的左、右两个无序子区的长度大致相等地。设C(n)表示对长度为n的文件进行快速排序所需的比较次数,显然,它应该等于对长度为n的无序区进行划分所需的比较次数n-1。加上递归地对划分所得的左、右两个无序子区(长度≤n/2)进行快速排序所需的比较总人
数。
假设文件长度n=2k,那么总的比较次数为:
C(n) ≤n+2C(n/2)
≤n+2[n/2+2C(n/22)]=2n+4C(n/22)
≤2n+4[n/4+2C(n/23)]=3n+8C(n/23)
≤……
≤kn+2kC(n/2k)=nlog2n+nC(1)
=O(nlog2n)
注意:式中C(1)为一常数,k=log2n。 ,
因为快速排序的记录移动次数不大于比较的次数,所以,快速排序的最坏时间复杂度应为O(n2),最好时间复杂雅兴O(log2n)。为了改善最坏情况下的时间性能,可采用三者取中的规则,即在每一趟划分开始前,首先比较R[1].key,R[h].key和R[[(1+h)/2]].key,令三者中取中值的记录和R[1]交换之。
可以证明:快速排序的平均时间复杂度也是O(nlog2n),它是目前基于比较的内部排序方法 中速度最快的,快速排序亦因此而得名。
快速排序需要一个栈空间来实现递归。若每次划分均能将文件均匀分割为两部分,则栈的最大深度为[log2n]+1,所需栈空间为O(log2n)。最坏情况下,递归深度为n,所需栈空间为O(n)。
快速排序是不稳定的,请读者自行检验。