快速排序算法的基本思想
快速排序是一种二叉树结构的交换排序方法。
设数组a中存放了n个数据元素,low为数组的低端下标,high为数组的高端下标,从数组a中任取一个元素(通常取a[low])作为标准,调整数组a中各个元素的位置,使排在标准元素前面元素的关键字均小于标准元素的关键字,排在标准元素后面元素的关键字均大于或等于标准元素的关键字。这样一次过程结束后,一方面将标准元素放在未来排好序的数组中该标准元素应在的位置,另一方面将数组中的元素以标准元素为中心分成了两个子数组,位于标准元素左边子数组中元素的关键字均小于标准元素的关键字,位于标准元素右边子数组中元素的关键字均大于或等于标准元素的关键字。对这两个子数组中的元素分别再进行方法类同的递归快速排序。递归算法的结束条件是high≤low,即上界下标小于或等于下界下标。
算法如下:
void QuickSort(DataType a[], int low, int high){//对数组a[]进行快速排序
int i = low, j = high;
DataType temp = a[low];//取第一个元素为标准数据元素
while(i<j){
while(i<j&&temp.key<=a[j].key){
j--;//在数组的右端扫描
}
if(i<j){
a[i] = a[j];
i++;
}
while(i<j&&a[i].key<temp.key){
i++;//在数组左端扫描
}
if(i<j){
a[j] = a[i];
j--;
}
}
a[i] = temp;
if(low<i){
QuickSort(a,low,i-1);//对左端,子集合进行递归
QuickSort(a,j+1,high);//对右端,子集合进行递归
}
}
算法分析
快速排序算法过程是递归的过程,我们首先看第一次递归调用的执行过程。把a[low]作为标准元素,标准元素存放在临时变量temp中。把标准元素的定位过程分成两个子过程:在数组的右端扫描定位和在数组的左端扫描定位。
在数组的右端扫描定位时,从数组的右端(数组右端下标设为j)开始,比较标准元素的关键字和数组右端元素的关键字,若标准元素的关键字小于或等于数组右端元素的关键字,则数组右端下标j减1后继续比较;否则,a[j]赋值给a[i]并且下标i加1后,转到在数组的左端扫描定位。
在数组的左端扫描定位时,从数组的左端(数组左端下标设为i)开始,比较标准元素的关键字和数组左端元素的关键字,若标准元素的关键字大于数组左端元素的关键字,则数组左端下标i加1后继续比较;否则,a[i]赋值给a[j]并且下标j减1后,转到在数组的右端扫描定位。上述在数组的右端扫描定位和在数组的左端扫描定位反复进行,直到数组左端下标i大于或等于数组右端下标j时为止。此时,把标准元素(即存放在临时变量temp中的元素)赋值给a[i]。数组的位置a[i]即为标准元素最终应在的位置。这时,处于标准元素左边元素的关键字均小于标准元素的关键字,处于标准元素右边元素的关键字均大于或等于标准元素的关键字。这样的一次过程称为一次快速排序。
算法之所以要右端扫描定位和左端扫描定位轮换进行,是因为这样可以有效利用左端和右端空出来的一个数组元素空间。初始时,令temp=a[low],则a[low]数组元素空间即可用于存放右端比较时,比标准元素小的数组元素。随后,数组右端空出来一个数组元素空间,则可用于存放左端比较时,比标准元素大的数组元素。
一次快速排序过程
如图所示是快速排序算法一次快速排序过程的一个示例。初始时,标准元素60存放在临时变量temp中,空白方框表示其中存放的数据元素已复制到别处,此数组位置已空出。先从数组右端下标j开始,因为36小于标准元素60,所以a[7]=36赋值给a[0]并且下标i加1后,转到在数组的左端扫描;此时再从数组左端下标i开始,55小于标准元素60,数组左端下标i加1后继续比较。48小于标准元素60,数组左端下标i加1后继续比较。37小于标准元素60,数组左端下标i加1后继续比较。10小于标准元素60,数组左端下标i加1后继续比较。90大于标准元素60,a[5]=90赋值给a[7]并且下标j减1后,转到在数组的右端扫描;此时再从数组右端下标j开始,84大于标准元素60,数组右端下标j减1后继续比较;此时i=j,循环过程结束,把临时变量temp中的标准元素60赋值给a[5]。这样,一次快速排序过程就结束了。
标准元素60把原数组中的元素分成了两部分,位于标准元素60左端的元素均小于标准元素60,位于标准元素60右端的元素均大或等于标准元素60。然后再分别对这两个子序列进行算法相同、但区间不同的递归快速排序。对每个子序列区间进行的快速排序算法,都是把该子序列区间内的第一个数据元素作为自己的标准元素。递归算法的出口条件是low≥high。当某个子序列区间递归调用的参数low和high满足出口条件low≥high时,该次递归调用结束;当所有子序列区间递归调用的参数low和high都满足出口条件low≥high时,整个快速排序过程结束。
测试代码
#include <stdio.h>
#include <stdlib.h>
typedef int KeyType;
typedef struct{
KeyType key;
}DataType;
void QuickSort(DataType a[], int low, int high){//对数组a[]进行快速排序
int i = low, j = high;
DataType temp = a[low];//取第一个元素为标准数据元素
while(i<j){
while(i<j&&temp.key<=a[j].key){
j--;//在数组的右端扫描
}
if(i<j){
a[i] = a[j];
i++;
}
while(i<j&&a[i].key<temp.key){
i++;//在数组左端扫描
}
if(i<j){
a[j] = a[i];
j--;
}
}
a[i] = temp;
if(low<i){
QuickSort(a,low,i-1);//对左端,子集合进行递归
QuickSort(a,j+1,high);//对右端,子集合进行递归
}
}
void main(void){
int i=0;
DataType array[8]={60,55,48,37,10,90,84,36};
DataType array2[8];
printf("原数组顺序:");
for(i=0;i<8;i++){
printf("%d ",array[i]);
}
printf("\n\n");
QuickSort(array,0,7);
printf("快速排序之后结果:");
for(i=0;i<8;i++){
printf("%d ",array[i]);
}
getch(0);
}
快速排序的过程
复杂度分析
快速排序算法的时间复杂度和各次标准数据元素的值关系很大。如果每次选取的标准元素都能均分两个子数组区间长度,这样的快速排序过程是一个完全二叉树结构。即每个结点都把当前数组分成两个大小相等的数组结点,n 个元素数组的根结点的分解次数就构成一棵完全二叉树。这时分解次数等于完全二叉树的深度。每次快速排序过程无论怎样划分数组,全部的比较次数都接近于n-1次,所以最好情况下快速排序算法的时间复杂度为O(nlbn);
快速排序算法的最坏情况是,数据元素已全部有序,此时数组根结点的分解次数构成一棵二叉退化树(即单分支二叉树),一棵二叉退化树的深度是n,所以最坏情况下快速排序算法的时间复杂度为O(n2)。
在一般情况下,标准元素值的分布是随机的,数组的分解次数构成一棵二叉树,这样的二叉树的深度接近于lbn,所以快速排序算法的平均(或称期望)时间复杂度为O(nlbn)。
快速排序算法需要堆栈空间临时保存递归调用参数,堆栈空间的使用个数和递归调用的次数(也即二叉树的深度)有关,由于二叉树有可能是单支二叉树,而单支二叉树的深度为n-1,所以,最坏情况下快速排序算法的空间复杂度为O(n)。
分析上述例子即可发现,快速排序算法是一种不稳定的排序方法。