序
快速排序(QuickSort)也是一种排序算法,对包含n个数组的输入数组,最坏情况运行时间为O(n^2)。虽然这个最坏情况运行时间比较差,但是快速排序通常是用于排序的最佳实用选择,这是因为其平均性能相当好,期望的运行时间为O(nlgn),且O(nlgn)中隐含的常数因子很小,另外它还能够进行就地排序在虚拟环境中也能很好的工作。
原理
快速排序也和合并排序一样,基于分治法,分为分解、解决、合并三个步骤;
分解:数组array[low…high]被分为两个(可能空)子数组array[low…temp-1]和array[temp+1…high],使得array[low…temp-1]中的每一个元素都小于等于array[temp],而array[temp+1…high]中的每一个元素都大于array[temp],下标temp也是在这个过程中被计算出来;
解决:通过递归的调用快速排序,对子数组array[low…temp-1],array[temp+1…high]进行排序;
合并:因为两个子数组是就地排序的,将他们的合并不需要操作,整个数组array[low…high]是已经排好序的。
本章介绍了快速排序算法的原理、程序实现(包括随机化版本)及其性能分析。
快排算法实现
#include <iostream>
#include <ctime>
#include <cstdlib>
#define N 10
using namespace std;
//快速排序的递归算法
void quickSort(int * array, int low, int high);
//求分割点
int partition(int * array, int low, int high);
//交换两个变量的值
void exchange(int &a, int &b);
int main()
{
//声明一个待排序数组
int array[N];
//设置随机化种子,避免每次产生相同的随机数
srand(time(0));
for (int i = 0; i<N; i++)
{
array[i] = rand() % 101;//数组赋值使用随机函数产生1-100之间的随机数
}
cout << "排序前:" << endl;
for (int j = 0; j<N; j++)
{
cout << array[j] << " ";
}
cout << endl << "排序后:" << endl;
//调用快速排序函数对该数组进行排序
quickSort(array, 0, N - 1);
for (int k = 0; k<N; k++)
{
cout << array[k] << " ";
}
cout << endl;
return 0;
}//main
void quickSort(int * array, int low, int high)
{
if (low < high)
{
int temp = partition(array, low, high);
quickSort(array, low, temp - 1);
quickSort(array, temp + 1, high);
}
}
int partition(int * array, int low, int high)
{
int i = low - 1;
//默认将划分段的最后一个元素为主元
int x = array[high];
for (int j = low; j<high; j++)
{
if (array[j] <= x)//在array[i]左边都是小于x即array[high]的数,右边均是大于它的数
{
i += 1;
exchange(array[i], array[j]);
}
}
exchange(array[i + 1], array[high]);
return i + 1;//所以循环完毕后,i+1就是该数组的分割点
}
void exchange(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
快速排序的随机化版本
在上面介绍的快速排序算法实现中,Partition(A , p , r)总是默认A[r]为主元,作为比较标准。如果可以采用随机取样的随机化技术的话,将会使得分析更加简单。下面是随机化版本的快速排序算法实现:
#include <iostream>
#include <ctime>
#include <cstdlib>
#define N 10
using namespace std;
//快速排序的递归算法
void quickSort(int * array, int low, int high);
//求分割点
int partition(int * array, int low, int high);
//以low ~ high 之间的一个随机元素作为主元 , 求分割点
int randomPartition(int *array, int low, int high);
//交换两个变量的值
void exchange(int &a, int &b);
int main()
{
//声明一个待排序数组
int array[N];
//设置随机化种子,避免每次产生相同的随机数
srand(time(0));
for (int i = 0; i<N; i++)
{
array[i] = rand() % 101;//数组赋值使用随机函数产生1-100之间的随机数
}
cout << "排序前:" << endl;
for (int j = 0; j<N; j++)
{
cout << array[j] << " ";
}
cout << endl << "排序后:" << endl;
//调用快速排序函数对该数组进行排序
quickSort(array, 0, N - 1);
for (int k = 0; k<N; k++)
{
cout << array[k] << " ";
}
cout << endl;
system("pause");
return 0;
}//main
void quickSort(int * array, int low, int high)
{
if (low < high)
{
int temp = randomPartition(array, low, high);
quickSort(array, low, temp - 1);
quickSort(array, temp + 1, high);
}
}
int partition(int * array, int low, int high)
{
int i = low - 1;
//默认将划分段的最后一个元素为主元
int x = array[high];
for (int j = low; j<high; j++)
{
if (array[j] <= x)//在array[i]左边都是小于x即array[high]的数,右边均是大于它的数
{
i += 1;
exchange(array[i], array[j]);
}
}
exchange(array[i + 1], array[high]);
return i + 1;//所以循环完毕后,i+1就是该数组的分割点
}
int randomPartition(int *array, int low, int high)
{
//找到low ~ high 之间的一个随机位置
int i = rand() % (high - low + 1) + low;
//交换该随机主元至尾部,
exchange(array[i], array[high]);
return partition(array, low, high);
}
void exchange(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
随机版本的快排与普通快排区别并不是很大,改动的仅仅是求分割点步骤中的主元选取,也就是增加了randomPartition函数,选定好主元元素下标i后,将该元素交换至段尾,依然调用partition函数求分割点。
快速排序性能分析
快速排序的运行时间与划分是否对称有关,而后者又与选择了哪一个元素进行划分有关。如果划分是对称的,那么本算法在渐近意义上与合并排序一样快,如果划分是不对称的那么本算法在渐进意义上与插入排序一样慢。下面分别讨论快速排序的最坏情况划分、最佳情况划分、平衡的划分。
最坏情况划分:快速排序的最坏情况划分行为发生在划分过程中产生的两个区域分别包含n-1个元素和0个元素的时候。假设算法每次递归调用都出现了这种不对称划分,划分的时间代价为O(n),因为对一个大小为0的数组进行递归调用后,返回了T(n)=O(1),故算法的运行时间可递归的表示为:
T(n) = T(n-1) + T(0) + O(n) = T(n-1) + O(n)
从直观上来看,如果将每一层递归的代价加起来,就可以得到一个算术级数(等式(array,2)其和值的量极为O(n^2))利用代换法可以比较直接的证明递归式 T(n) = T(n-1) + O(n)的解为 T(n) = O(n^2)。
因此如果在算法的每一层递归上,划分都是最大程度不对称的,那么算法的运行时间为O(n^2),亦即快速排序算法的最坏情况运行时间不如插入排序的好。此外当输入数组完全排好序时,快速排序的运行时间是O(n^2),而插入排序的运行时间为O(n)。
最佳情况划分:在Partition可能做的最平衡划分中,得到的两个子问题的大小都不可能大于[n/2],因为若其中一个子问题的大小为[n/2],则另外一个子问题的大小必然为[n/2]-1。在这种情况下,快速排序的运行速度要快得多,这时表达其运行时间的递归式为:
T(n) <= 2T(n/2) + O(n)
解该递归式可得T(n) = O(nlgn)。由于在每一层递归划分的两边都是对称的,因此从渐进意义上来看,算法运行的就更快了。
平衡的划分: 快速排序的平均情况运行时间与其最佳情况运行时间很接近,而不是非常接近与其最坏情况运行时间(证明原因详细参考《算法导论》原书第二版P88),因为任何一种按常数比例进行划分都会产生深度为O(lgn)的递归树,其中每一层的代价都是O(n),因而每当按照常数比例进行划分时,总的运行时间都是O(nlgn)。
练习
思考题
版权声明:本文为博主原创文章,未经博主允许不得转载。