前言的前言:
如果看不懂这些干巴巴的理论文字,那就先不用看了,下面代码中有详细的注释,大家可以先跟着代码走几遍,回过头来再看这些文字描述,总之:纸上得来终觉浅,绝知此事要躬行。前言:
排序算法哪家强,从实际应用的角度上将,快排表现很好。很自然地,人们会觉得短数组比长数组更好处理,因此可能会想到将原始数组分为若干各子部分然后分别进行排序。快速排序就是基于分治法的排序算法,这不过它更多地侧重于“分”,没有明显的“合”过程。
快速排序的要点:
1.快排算法的主要思想:
(1)从待排序序列S中“任意”选择一个记录k作为轴值(Pivot)。
(2)将剩余的记录分割(partition)成左子序列L和右子序列R。
(3)L中所有记录都小于或等于k,R中记录都大于等于k,因此k正好位于正确的位置。
(4)对子序列L和R递归进行快速排序,直到子序列中只含有0或1个元素,退出递归。
2.如何选择轴值pivot,这对快排的时间性能影响很大,轴值的选择应尽量使得序列可以据此划分为均匀的两半。
3.如何实现最关键的分割的过程:最简单的分割方法就是左指针l和右指针r(下标)分别从序列的左端、右端向序列中间扫描;左边越过那些小于等于pivot的值,停在第一个大于pivot的值Kl;右边越过那些大于等于pivot的值,停在第一个小于pivot的值Kr;交换逆置记录Kl和Kr;从交换后的位置,继续从左右向中间扫描,发现并交换逆置记录对,直到l、r交叉而整个序列扫描完毕。这种方法适用于需要处理l、r边界,轴值最后定位等情况。
4. 由于递归开销过大,所以当子序列足够短时,我们采用插入排序来完成对子序列的排序。
5. 快速排序之所以快是因为:它每次选定轴值pivot并进行划分子集(分割交换)后该轴值被一次性的放到了他最终该放到的位置。
下面以一趟分割交换为例:
下标 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
初始 |
25 |
34 |
45 |
32 |
34` |
12 |
29 |
64 |
选基准 |
25 |
34 |
45 |
29 |
34` |
12 |
32 |
64 |
分割 |
25 |
12 |
45 |
29 |
34` |
34 |
32 |
64 |
分割 |
25 |
12 |
29 |
45 |
34` |
34 |
32 |
64 |
基准定位 |
25 |
12 |
29 |
32 最终位置 |
34` |
34 |
45 |
64 |
完整代码如下:(pivot采用取头、中、尾中位数、分割采用上述介绍的方法)
//#pragma once //QuickSort.h #include <iostream> using namespace std; const int Cutoff = 28; //阈值,当子序列元素个数小于Cutoff时,采用简单排序 template <class T> class Quick { private: T * A; //用来存储待排序列的数组 int N; //待排序列元素个数 public: Quick(int size); //构造函数 ~Quick() { delete[] A; } //析构函数 void QuickSort(int Left, int Right); //递归进行快排 T Median3(int Left, int Right); //选主原(轴值)- 取头、中、尾的中位数 friend void Swap(T* a, T* b); //交换两个数 - 友元函数 void InsertSort(T* B, int Nb); //当子序列元素个数小于阈值时调用插入排序 void Print(); //输出结果 }; //Quick类的实现 //构造函数初始化 template <class T> Quick<T>::Quick(int size) { N = size; A = new T[N]; for (int i = 0; i < N; i++) cin >> A[i]; } //交换两个数 template <class T> Swap(T* a, T* b) { int tmp; tmp = *a; *a = *b; *b = tmp; } //输出结果 template <class T> void Quick<T>::Print() { for (int i = 0; i < N; ++i) { cout << A[i] << " "; } } //插入排序 template <class T> void Quick<T>::InsertSort(T* B, int Nb) { int tmp, p, i; for (p = 1; p < Nb; ++p) //总共摸取Nb-1张牌 { tmp = B[p]; //当前摸到手中的一张牌 for (i = p; i > 0 && A[i - 1] > tmp; i--) //i很多情况下都是用来控制循环次数的 { A[i] = A[i - 1]; } A[i] = tmp; //新牌归位 } } //选轴值pivot template <class T> T Quick<T>::Median3(int Left, int Right) { int Center = (Left + Right) / 2; if (A[Left] > A[Center]) Swap(&A[Left], &A[Center]); if (A[Left] > A[Right]) Swap(&A[Left], &A[Right]); if (A[Center] > A[Right]) Swap(&A[Center], &A[Right]); //此时A[Left] <= A[Center] <= A[Right],为了接下来的分割过程,将轴值藏到右边 //将轴值藏到子序列的右端只是为了不影响分割过程 //分割时只需考虑A[Left+1] - A[Right-2]的区间 Swap(&A[Center], &A[Right - 1]); //返回轴值 return A[Right - 1]; } //递归分割 template <class T> void Quick<T>::QuickSort(int Left, int Right) { int Pivot, Low, High; //基准、左、右指针 if (Cutoff < Right - Left) //如果序列元素充分多,则进入快排 { Pivot = Median3(Left, Right); Low = Left; High = Right - 1; //分割过程 while (true) { while (A[++Low] < Pivot); while (A[--High] > Pivot); if (Low < High) Swap(&A[Low], &A[High]); else break; } //将当前子序列的轴值一次性的放到他最终所在的位置上 - Low Swap(&A[Low], &A[Right - 1]); //当前子序列分割完成,递归进入更小一层子序列的分割 QuickSort(Left, Low - 1); //递归解决左边子序列 QuickSort(Low + 1, Right); //递归解决右边子序列 } else InsertSort(A + Left, Right - Left + 1); //当子序列足够短时,采用插入排序 } int main() { int N; //待排元素个数 cin >> N; //定义Quick对象QuickA Quick<int> QuickA(N); //快速排序 QuickA.QuickSort(0, N - 1); //输出结果 QuickA.Print(); return 0; }