快速排序
快速排序:基本思想是通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的
直接从代码来分析:
void QuickSort(vector<int>& vec)
{
QSort(vec, 0, vec.size() - 1);
}
void QSort(vector<int>& vec, int low, int high)
{
int pivot; //记录枢轴值pivot
if (low < high)
{
pivot = Partition(vec, low, high); //将序列分割为符合要求的两部分
//算出枢轴值pivot
QSort(vec, low, pivot - 1); //对低子表递归排序
QSort(vec, pivot + 1, high); //对高子表递归排序
}
}
int Partition(vector<int>& vec, int low, int high)
{
int pivotkey;
pivotkey = vec[low]; //对子表的第一个作枢轴记录
while (low < high) //从表的两端进行扫描
{
while (low < high && vec[high] >= pivotkey) //从后找到第一个比枢轴小的值
high--;
swap(vec[low], vec[high]); //交换该值与枢轴值
while (low < high && vec[low] <= pivotkey) //从前找到第一个比枢轴大的值
low++;
swap(vec[low], vec[high]); //交换该值与枢轴值
}
return low; //返回枢轴下标
}
从代码来看,因未考虑诸多因素,所以实现比较简单,但不管其如何变化,中心思想仍然是将整个序列进行一个 p a r t i t i o n partition partition,然后再递归对每个部分进行排序,明白了这一点代码实现就比较简单了
简单的测试
测试序列:50,10,90,30,70,40,80,60,20
从结果可以看出每次 p a r t i t i o n partition partition所找的枢轴值
复杂度分析
快速排序的时间性能取决于快速排序递归的深度。
- 最优情况下, P a r t i t i o n Partition Partition每次都划分得很均匀,如果排序n个关键字,其递归树得深度就为[ log 2 n \log_2{n} log2n + 1],即仅需递归 l o g 2 n log_2{n} log2n次,需要时间为 T ( n ) T(n) T(n)的话,第一次 P a r t i t i o n Partition Partition应该是需要对整个数组扫描一遍,做 n n n次比较。然后,获得的枢轴将数组一分为二,那么还需要 T n / 2 ) Tn/2) Tn/2)的时间,于是不断进行划分,得出下面的不等式推断:
T(n) ≤ \leq ≤ 2T(n/2) + n, T(1) = 0
T(n) ≤ \leq ≤ 2(2T(n/4) + n/2) + n = 4T(n/4) + 2n
T(n) ≤ \leq ≤ 4(2T(n/8) + n/4) + 2n = 8T(n/8) + 3n
…
T(n) ≤ \leq ≤ nT(1) + ( log 2 n \log_2{n} log2n) ·n = O(n l o g n log{n} logn)
- 最坏情况下,待排序的序为支正序或倒序,每次划分只得到一个比上一次划分少一个记录的子序列,注意另一个为空。如果递归数画出来,他就是一颗协树,此时需要执行一个 n − 1 n-1 n−1次递归调用, 且第 i i i次划分还需经过 n − 1 n-1 n−1次关键字的转换,也就是枢轴的位置,因此比较次数为:
∑ i = 1 n − 1 \sum_{i=1}^{n-1} ∑i=1n−1 = n-1 + n-2 +… + 1 = n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)
最终其时间复杂度为O( n 2 n^2 n2)
平均情况下,设枢轴的关键字应该在第 k k k的位置,那么:
T(n) = 1 n \frac{1}{n} n1 ∑ k = 1 n T ( k − 1 ) + T ( n − k ) ) + n \sum_{k=1}^nT(k - 1) + T(n-k)) + n ∑k=1nT(k−1)+T(n−k))+n = 2 n \frac{2}{n} n2 ∑ k = 1 n T ( k ) + n \sum_{k=1}^nT(k) + n ∑k=1nT(k)+n
由数学归纳法可以证明其数量级为O(n log n \log n logn)
就空间复杂度而言,主要递归造成的栈空间的使用,最好情况下,递归树的深度为 log 2 n \log_2{n} log2n,其空间复杂度也为O[ log n \log{n} logn],最坏情况,需要进行 n − 1 n - 1 n−1次递归调用,其空间复杂度为 O ( n ) O(n) O(n),空间复杂度也为O[ log n \log{n} logn]
由于关键字的比较与交换是跳跃的,因此,快速排序也是一种不稳定的排序算法
快速排序的优化
1. 优化选取枢轴
采用名为三数取中的方法进行优化:
int pivotkey;
int m = low + (high - low)/2;
if (vec[low] > vec[high])
swap(vec[low], vec[high]); //使左端值较小
if (vec[m] > vec[high])
swap(vec[m], vec[high]); //使中间值较小
if (vec[m] > vec[low])
swap(vec[m], vec[low]; //使左端值较小
pivotkey = vec[low];
2. 优化不必要的交换
将交换改为替换:
int Partition(vector<int>& vec, int low, int high)
{
int pivotkey;
pivotkey = vec[low]; //对子表的第一个作枢轴记录
int tmp = pivotkey;
while (low < high) //从表的两端进行扫描
{
while (low < high && vec[high] >= pivotkey) //从后找到第一个比枢轴小的值
high--;
vec[low] = vec[high]; //交换该值与枢轴值
while (low < high && vec[low] <= pivotkey) //从前找到第一个比枢轴大的值
low++;
vec[high] = vec[low]; //交换该值与枢轴值
}
vec[low] = tmp;
return low; //返回枢轴下标
}
3. 优化小数组时的排序方案
即在小数组时采用不同的排序方法:
#define MAX_LENGTH_INSERT_SORT 7
void QSort(vector<int>& vec, int low, int high)
{
int pivot;
if ((high - row) > MAX_LENGTH_INSERT_SORT) //当high - low大于常数采用快速排序
{
pivot = Partition(vec, low, high);
QSort(vec, low, pivot - 1);
QSort(vec, pivot + 1, high);
}
else
InsertSort(vec); //小于常数采用插入排序
}
4. 优化递归操作
对 Q S o r t QSort QSort实施尾递归优化:
void QSort(vector<int>& vec, int low, int high)
{
int pivot;
if ((high - row) > MAX_LENGTH_INSERT_SORT) //当high - low大于常数采用快速排序
{
while (low < high)
{
pivot = Partition(vec, low, high);
QSort(vec, low, pivot - 1); //对低子表进行递归排序
low = pivot + 1; //尾递归
}
}
else
InsertSort(vec);
}
从实现来说,将 p i v o t + 1 pivot + 1 pivot+1赋值给 l o w low low,然后将 i f if if改为了 w h i l e while while,这样在进行下一次循环时,得到的 P a r t i t i o n ( v e c , l o w , h i g h ) Partition(vec, low, high) Partition(vec,low,high)实际上就相当于 Q S o r t ( v e c , p i v o t + 1 , h i g h ) QSort(vec, pivot + 1, high) QSort(vec,pivot+1,high),与之前的并无差别,但这样修改实际将递归修改为了迭代,采用迭代而非递归的方法可以缩减堆栈深度,从而提高了整体性能
补充:
- S G I S T L SGI STL SGISTL中采用的sort在早期版本里使用的是完全的 Q u i c k S o r t QuickSort QuickSort,但后来考虑到不当的枢轴选择导致的不当分割造成算法性能下降,如今, S G I S T L SGI STL SGISTL采用的是一种名为 i n t r o s o r t introsort introsort(内省式排序)的算法,其行为在大部分情况下与 Q u i c k S o r t QuickSort QuickSort完全相同,但当分割行为有恶化二次行为的倾向时,能够自我侦测,转而改用 H e a p S o r t Heap Sort HeapSort,使其效率能维持在 H e a p S o r t Heap Sort HeapSort的O( N log N N\log{N} NlogN)
测试代码:
#include <iostream>
#include <vector>
using namespace std;
void QuickSort(vector<int>& vec);
void QSort(vector<int>& vec, int low, int high);
int Partition(vector<int>& vec, int low, int high);
void PrintStep(vector<int> vec, int n, int i);
void PrintResult(vector<int> vec, int n);
int count = 1;
void QuickSort(vector<int>& vec)
{
cout << "--------------快速排序--------------" << endl;
QSort(vec, 0, vec.size() - 1);
}
void QSort(vector<int>& vec, int low, int high)
{
int pivot;
if (low < high)
{
pivot = Partition(vec, low, high);
QSort(vec, low, pivot - 1);
QSort(vec, pivot + 1, high);
}
}
int Partition(vector<int>& vec, int low, int high)
{
int pivotkey;
pivotkey = vec[low];
while (low < high)
{
while (low < high && vec[high] >= pivotkey)
high--;
swap(vec[low], vec[high]);
while (low < high && vec[low] <= pivotkey)
low++;
swap(vec[low], vec[high]);
}
PrintStep(vec, vec.size(), count);
++count;
return low;
}
void PrintStep(vector<int> vec, int n, int i)
{
cout << "第" << i << "轮排序结果: ";
for (int j = 0; j < n; ++j)
cout << vec[j] << ' ';
cout << endl;
}
void PrintResult(vector<int> vec, int n)
{
for (int j = 0; j < n; ++j)
cout << vec[j] << ' ';
cout << endl;
}
int main(int argc, char **argv)
{
int a[] = {50,10,90,30,70,40,80,60,20};
vector<int> vec(a, a+9);
QuickSort(vec);
cout << "最终排序结果为:";
PrintResult(vec, vec.size());
return 0;
}