快速排序是20世纪最伟大的10大算法之一,由C. R. A. Hoare 于 1960 年提出的一种排序算法
1. 快速排序的原理
快速排序(英译为Quicksort,简称快排),快速排序的基本思想是在待排序的n个记录中任取一个记录(通常取第一个记录)作为基准,把该记录放入适当位置后,数据序列被此记录划分成两部分,分别是比基准小和比基准大的记录;然后再对基准两边的序列用同样的策略,分别进行快速排序。
图1-快速排序
也就是说,快速排序算法是采用的分治法策略把一个序列分为两个序列。
2. 快速排序过程
图2-快速排序过程
例如有这么一个待排序列R为{4,1,3,2,6,5,7},以该序列中的第一个元素4为基准
,i和j两个下标用于划分两个子序列,j记录比基准大的子序列,而i记录比基准小的序列。
图3-快速排序过程
j从后往前遍历每个元素,跟基准temp进行比较,如果R[j] > temp,j则往前移动。当j下标移动到元素2的位置时,此时R[j] < temp,于是把2赋值给下标i的位置,即R[i] = R[j]。
图4-快速排序过程
然后i从前往后遍历每个元素,跟基准temp进行比较,如果R[i] < temp,i往后移动;如果R[i] > temp,那么把R[j] 的值赋值给下标j的位置,即R[j]= R[i]。如果此时下标i和下标j相等(i=j),那么R[i] = temp。
一次划分,一趟快速排序后,序列R为:{2,1,3,4,6,5,7},其中{2,1,3}是比基准4小的记录的无序子序列,而{6,5,7}是比基准4大的记录的无序子序列。
然后分别对子序列{2,1,3}和子序列{6,5,7}分别进行快速排序,直到子序列中的记录个数为0或1,整个序列R有序为止
。
3. 快速排序算法
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
typedef int KeyType; //定义关键字类型
typedef int InfoType;
typedef struct
{
KeyType key; //关键字项
InfoType data; //其他数据项的类型InfoType
} RecType; //排序的记录类型定义
/*
快速排序
R:要排序的数组
s:数组R的起始下标
t:数组R的终点下标
*/
void QuickSort(RecType R[] , int s , int t)
{
int i = s;
int j = t;
RecType temp;
if(s < t) //数组R中元素至少有2个
{
temp.key = R[s].key; //去数组R中第一个元素作为基准
//从数组R左右两端开始往中间扫描,直到i==j为止,停止扫描
while (i != j)
{
//从右往左扫描比基准temp大的R[j]
while (j > i && R[j].key >= temp.key)
{
j--;
}
//如果比基准temp小,则交换R[i]和R[j]
R[i].key = R[j].key;
//从左向右扫描比基准temp小的R[i]
while (j > i && R[i].key <= temp.key)
{
i++;
}
//如果扫描到的R[i]这样的数比基准temp大的话,则交换R[i]和R[j]
R[j].key = R[i].key;
}
//如果跳出循环,说明i==j,那么基准回归
R[i].key = temp.key;
/*
此时已经划分成了两个子序列,继续递归对这两个子序列进行排序
*/
//扫描比基准temp小的记录的子序列,进行排序
QuickSort(R , s , i-1);
//比基准temp大的记录的子序列,进行排序
QuickSort(R , i+1 , t);
}
}
int main(void)
{
RecType R[7] = {0};
int arr[]= {4,1,3,2,6,5,7};
int n = sizeof(arr) / sizeof(int);
int i;
printf("--------排序前--------\n");
for(i = 0; i < n; i++)
{
R[i].key = arr[i];
printf("R[%d].key = %d\n" , i , R[i].key);
}
//快速排序
QuickSort(R, 0 , n-1);
printf("--------排序后--------\n");
for(i = 0; i < n; i++)
{
printf("R[%d].key = %d\n" , i , R[i].key);
}
return 0;
}
测试结果:
4. 快速排序过程演示
假设有这样一个待排序列{6,8,7,9,0,1,3,2,4,5},该序列的快速排序过程如下图所示:
图5-快速排序过程演示
从图中我们可以看到这个序列在进行快速排序过程中是以第一个元素6为基准
,一趟快速排序划分后,比基准小的子序列为{5,4,3,0,1},比基准大的子序列为{9,7,8},同理,再继续对这两个子序列进行递归排序。
其中{5,4,3,0,1}子序列在进行划分后,会出现这样一种极端的情况:以选取子序列中第一个元素5为基准,最后划分时只有比基准小的子序列{4,3,2,0,1},其实这种情况对于快速排序来说是非常不利的,因为还要继续递归排序。
最后当子序列中的记录个数为0或1时,则排序结束,此时整个序列已经是有序的了。
如果我们将这个序列的递归排序过程看成一个3叉树的话,每个分支节点对应一次递归调用。该序列在进行排序过程中发生了7次递归调用,而递归调用的次数无论是先从左分区开始处理,还是先从右分区开始处理,总的递归调用次数是不变的,也就是说递归调用次数与左右分区的先后处理顺序无关
。
5. 基准的选择
前面在介绍快速排序算法时提到了,快速排序算法会在待排序的n个记录中任取一个记录作为基准,而基准又称为轴值,在有些书籍上又称为枢纽元(pivot)。
从快速排序过程演示中,我们知道基准的选择对于快速排序算法来说是非常重要的
,虽然选择任何一个元素作为基准都可以完成排序,但是好的基准的选择能最大发挥快速排序算法的性能,而不好的基准选择则会让快速排序的性能大打折扣,甚至变成”慢速”排序。因此在选择基准上,最好的情况就是尽量选择能均匀划分子序列的基准(即尽量使子序列相等)。
比如在图5中,序列{5,4,3,0,1}在选择基准的策略上是选择序列中第一个元素5作为基准
,在划分子序列就出现了极端的情况,划分后使左右的子序列极端不相等。因此在基准的选择上要根据实际的情况来选择,一般选择的策略有以下几种:
1.选择最左边记录
2.随机选择
3.选择平均值
5.1 选择最左边记录
对于选择最左边记录来说,如果给定的待排序列是一个随机,无序的序列,那么这种选择策略是可以接受的。但如果这个序列是一个正序或逆序的,还是以选择最左边记录作为基准的策略的话,这将会产生一个非常极端,糟糕的情况:
图6-逆序的序列
如果是一个逆序的序列,所有的记录将被划分到左子序列,而右子序列为空。采用这种选择基准策略的话,后面所有的递归排序过程中都会受此影响
,将又会重复之前的极端情况,从{0,4,3,2,1}这个子序列来看,在划分后,所有的记录将被划分到右子序列中……
图7-正序的序列
如果是一个正序的序列,所有的记录将被划分到右子序列,而左子序列为空。可是在排完序后,我们发现序列还是和原来一样,并没有干什么事情,那么在排序过程中花费的时间就显得有些浪费了。
因此,在快速排序过程中,对于基准的选择还是尽量不要采用这种选取最左边记录策略。
5.2 随机选择
一般来说,随机选择是一种非常安全的策略, 除非生成随机数本身就有问题,使用生成的随机数作为基准时,不会总是出现非常极端或糟糕的情况,但从另一方面来说,每次随机数的生成一般也需要很大开销的,对于快速排序算法的性能并没有太大的帮助。
5.3 取三数中值
三数中值是取三个关键字中的中间数作为基准,也可以是随机选取三个关键字中的中间数作为基准,但实际上随机选取并没有太大的帮助,通常的做法是从一个序列中选取最左端,最右端,中间三个数的中间值作为基准,例如现在有这样一个序列:{8,1,4,9,6,3,5,2,7,0},它的左边元素是8,右边元素是0,中间位置(left + right)/2上的元素是6,于是从这三个关键字中{8,6,0}中选取中间值6作为基准。
下面我们来看一下三数中值的实现代码:
//交换位置
void Swap(RecType *r1 , RecType *r2)
{
RecType temp;
temp.key = r1->key;
r1->key = r2->key;
r2->key = temp.key;
}
//三数中值实现代码
RecType Median3(RecType R[] , int left , int right)
{
int Center = (left + right) / 2;
//保证左端较小
if(R[left].key > R[right].key)
{
Swap(&R[left] , &R[right]);
}
//保证左端最小
if(R[Center].key < R[left].key)
{
Swap(&R[Center] , &R[left]);
}
//保证右端最大
if(R[Center].key > R[right].key)
{
Swap(&R[Center] , &R[right]);
}
//最后,保证最左端最小,最右端最大,中间较小
//返回中间值
return R[Center];
}
6. 快速排序改进版实现代码
void Swap(RecType *r1 , RecType *r2)
{
RecType temp;
temp.key = r1->key;
r1->key = r2->key;
r2->key = temp.key;
}
//取三数中值法
RecType Median3(RecType R[] , int left , int right)
{
int Center = (left + right) / 2;
//保证左端较小
if(R[left].key > R[right].key)
{
Swap(&R[left] , &R[right]);
}
//保证左端最小
if(R[Center].key < R[left].key)
{
Swap(&R[Center] , &R[left]);
}
//保证右端最大
if(R[Center].key > R[right].key)
{
Swap(&R[Center] , &R[right]);
}
//最后,保证最左端最小,最右端最大,中间较小
//返回中间值
return R[Center];
}
/*
快速排序
R:要排序的数组
s:数组R的起始下标
t:数组R的终点下标
*/
void QuickSort(RecType R[] , int s , int t)
{
int i = s;
int j = t;
RecType pivot;
//取三数中值法选择基准
pivot = Median3(R, s , t);
if(s < t) //数组R中元素至少有2个
{
//从数组R左右两端开始往中间扫描,直到i==j为止,停止扫描
while (i != j)
{
//从右往左扫描比基准temp大的R[j]
while (j > i && R[j].key > pivot.key)
{
j--;
}
//从左向右扫描比基准temp小的R[i]
while (j > i && R[i].key < pivot.key)
{
i++;
}
//开始交换R[i]和R[j]的位置
if(i < j)
{
Swap(&R[i] , &R[j]);
}
}
/*
此时已经划分成了两个子序列,继续递归对这两个子序列进行排序
*/
//对左子序列进行排序
QuickSort(R , s , i-1);
//对右子序列进行排序
QuickSort(R , j+1 , t);
}
}
7. 快速排序性能分析
1.最坏的情况:每次划分的基准,是当前无序区中关键字最大(或最小的元素)。
图8-最坏情况
这是极端不平衡的一种情况,就如同采用选取最左边记录中所讲的情况一样,当出现这种最坏的情况,在排序过程中需要进行n-1次划分,当进行到第i次划分的长度为n-i+1,对于n-i+1这样长度的序列需要比较的次数为n-i,那么花费的时间为:
2. 平均情况下的时间复杂度比较复杂,这里我们直接记住结论,平均情况下的时间复杂度为:
O(nlog2n)
O
(
n
l
o
g
2
n
)
,由于快速排序算法是递归调用的,那么空间复杂度为
O(log2n)
O
(
l
o
g
2
n
)
。
3. 因为快速排序算法是跳跃式比较,排序的,所以快速排序是一种不稳定排序算法。