快速排序(内部排序)
快速排序也是交换排序的一种,但和冒泡排序不同的是,冒泡排序只比较相邻两个记录的顺序,
而快速排序的原理是:将未排序元素根据一个作为基准的“主元"(Pivot)分为两个子
序列,其中一个子序列的记录均大于主元,而另一个子序列均小于主元,然后递归地对这两个
子序列用类似的方法进行排序,本质上,快速排序使用分治法,将问题的规模减小,然后再分别进行处理。
对未排序元素,选择一个主元分为两个子序列,其中一个子序列所有元素均大于主元,另一个均小于主元,然后递归的
对这两个子序列进行类似的处理。本质上快速排序采用分治法将问题规模减小,然后再分别进行处理。
1)选择一个主元,并与最后一个元素进行交换;
2)设置两个指针Low和Hight,初始值分别指向第一个和倒数第二个元素;
3)Low从左向右扫描,其位置左侧为已遍历或交换过的比主元小的元素;
Hight从右向左扫描,其位置右侧为已遍历或交换过的比主元大的元素;
首先从Low指向的位置向右扫描,若遇到比主元大的元素,则停止。
然后从High指向的位置向左扫描,若遇到比主元小的元素,则停止。
4)若Low和Hight没有错位(即Low<High),则Low和High指向的元素互换位置。
5)重复3、4直至High和Low错位,将基准与A[Low]对换位置。
这就完成了一次划分,以主元为边界分别划分成大于和小于主元的两个子序列。
6)递归地对两个子序列用同样的方法进行快排。
为了避免最坏结果,在确定主元时需要有一定技巧。一种比较好的方
法是,将A[low] 、A[ high]、A[(low+high)/2]三者关键字的中值作为主元,
这样有可能避免在基本有序的序列中进行快速排序时时间复杂度出现最坏
情况的问题。
另外一个问题是,由于快速排序一般是用递归实现的,如果待排序列的
规模比较小,递归的副作用就会凸显出来,效果甚至还不如简单的插人排
序。所以更专业一些的处理,是在递归过程中检查当前子问题的规模,
当其小于某个阈值时就不继续递归,而是直接调用插入排序解决问题。
算法实现(C#):
// 快速排序
public static void MyQuickSort(int[] arr)
{
QSort(arr, 0, arr.Length - 1);
}
private static void QSort(int[] arr, int left, int right)
{
// 主元
int pivot;
// 扫描指针low和high
int low, high;
// 简单插入排序阈值,阈值必须大于0否则会出现越界错误
int qSortMinNum = 1000;
// 达到阈值进行快速排序
if (right - left > qSortMinNum)
{
// 查找主元 left <= center <= right 主元为center
pivot = MeDian3(arr, left, right);
low = left;
high = right - 1;
// 开启一轮扫描
while (true)
{
// 从左侧扫描比主元大的元素/或相等元素
while (arr[++low] < pivot) ;
// 从右侧扫描比主元小的元素/或相等元素
while (arr[--high] > pivot) ;
// 扫描指针合法(low < high)则交换
if (low < high)
Swap(ref arr[low], ref arr[high]);
else // 否则说明整个扫描已经完成,退出扫描,并将主元归位
break;
}
// 主元归位,即放置到low位置,low为大于主元元素
Swap(ref arr[low], ref arr[right - 1]);
// 递归排序左边元素
QSort(arr, left, low - 1);
// 递归排序右边元素
QSort(arr, low + 1, right);
}
else// 否则简单插入排序
InsertionSort(arr, left, right);
}
private static int MeDian3(int[] arr, int left, int right)
{
// 计算中位位序
int center = (left + right) / 2;
// 确保 left <= center <= right
if (arr[left] > arr[center])
Swap(ref arr[left], ref arr[center]);
if (arr[left] > arr[right])
Swap(ref arr[left], ref arr[right]);
if (arr[center] > arr[right])
Swap(ref arr[center], ref arr[right]);
// 将主元放置到right-1位置
Swap(ref arr[center], ref arr[right - 1]);
// 返回主元
return arr[right - 1];
}
(注意:Swap函数见排序序列第一篇文章,InsertionSort见本系列第三篇文章)
快速排序的时间复杂度分析略显复杂。最好的情况下,每一次划分都将原序列分成两个基
本等长的子序列,随着递归层次的加深子序列的数量翻倍,但在每一递归层次上比较总次数都是
0(N)次,而递归层次(深度)是log2N,由此可见,快速排序的最好时间复杂度应为0(Nlog2N)。
更复杂一些的证明显示,快速排序的平均时间复杂度也是0(Nlog2N)。相对于其他的内部排序,
快速排序的平均时间效率是最高的。
空间复杂度上,由于快速排序需要进行至少log2N层的递归,因此需要至少0(log2N)深度的栈空间。
若每次划分的子组大小不够平均,则栈空间的深度更大,在最坏的情况下将导致接近0(N)的栈空间深度。
此外,快速排序是不稳定的。因为在和主元进行比较时,可能导致一个元素交换到和它等值的另一个元素位置以前,导致两者的位置发生相对变换,因此快速排序是不稳定的。