快速排序之所以特别快,主要是由于非常精炼和高度优化的内部循环。
像归并排序一样,快速排序也是一种分治的递归算法。数组S排序的基本算法由下列简单的四部组成:
1.如果S中元素个数是0或1,则返回
2.取S中任一元素v,称之为pivot(枢纽元,主元,基准)
3.将S-{v}分成两个不想交的集合:S1={x∈S-{v} | x <= v} 和 S2={x∈S-{v} | x > v}
4.返回{QuickSort(S1)后,继随v,继而QuickSort(S2)
为了完成上述的步骤3,有两种常用方法,可以分别简称为:一前一后两个指针,一左一右两个指针。这两种方法只是实现上的差异,效率是一样的。
其实主要影响快速排序性能的是pivot的选取。
- 我们常用的一种选取方式(选取第一个或最后一个为pivot)是错误的,因为它有很大的可能会影响快速排序的性能。
- 一种安全的做法是,随机选取pivot。
- 最好的一种方法称为,三数中值分割法(Median-of-Three Partitioning)。就是利用数组的中值作为pivot,显然这很难算出,而且明显减慢排序的速度。所以常用的方法是,选取最左端、最右端、中心位置上的三个元素的中值作为pivot。
代码如下:
#include <iostream> #include <random> #include <utility> #include <ctime> #include <algorithm> using namespace std; void QuickSort(int* arr, size_t first, size_t last); size_t Partition(int* arr, size_t first, size_t last); // 一前一后两个指针 size_t Partition_2(int* arr, size_t first, size_t last); // 一左一右两个指针 int main() { const int ArrSize = 10; int* arr = new int[ArrSize]; static default_random_engine e(time(0) % 100); static uniform_int_distribution<int> u(0, 100); // 随机数范围[0, 100] for (size_t i = 0; i < ArrSize; ++i) { arr[i] = u(e); } QuickSort(arr, 0, ArrSize - 1); cout << boolalpha; cout << is_sorted(arr, arr + ArrSize) << endl; return 0; } void QuickSort(int* arr, size_t first, size_t last) { if (first < last) { size_t pivotIndex = Partition(arr, first, last); QuickSort(arr, first, pivotIndex - 1); QuickSort(arr, pivotIndex + 1, last); } } size_t Partition(int* arr, size_t first, size_t last) { int value = arr[first]; // 以第一个元素为主元 size_t i = first; // 后指针 size_t j = first + 1; // 前指针 while (j <= last) { if (arr[j] <= value) { ++i; swap(arr[i], arr[j]); } ++j; } swap(arr[first], arr[i]); return i; } size_t Partition_2(int* arr, size_t first, size_t last) { int value = arr[first]; size_t i = first; //左指针 // **1 size_t j = last; // 右指针 while (i < j) { while (arr[i] <= value) { ++i; } while (arr[j] > value) { --j; } if (i < j) { swap(arr[i], arr[j]); } } swap(arr[first], arr[j]); // **2 return j; }
这里,为了减少复杂度,我选取第一个元素为pivot。
两种划分方式中,一前一后两个指针方式,较好理解,i为后指针,j为前指针。i所指元素以及之前元素都是<=pivot的,j每次+1,然后判断arr[j]和pivot的关系。
一左一右两个指针方式中,我认为有两处难以理解的地方(都已在代码中标注)。
**1,选取当前范围内第一个元素为pivot,为什么还把i(左指针)从first开始?
answer: 快速排序是以递归方式进行的,递归的终止条件时,!(first < last),所以只有当前范围的元素个数>=2时,排序算法才会一直执行下去。
只有两个元素时,这两个元素之间的大小关系只有三种情况。当第一个元素大于第二个元素时,不妨设第一个元素为10,在数组中索引为0,第二个元素为0,在数组中索引为1。
如果说,左指针为first+1(这里为1),则后面的while循环不会被执行,所以步骤3没有得到满足,排序会失败。
**2,为什么是{ swap(arr[first], arr[j]); return j; },而不是{ swap(arr[first], arr[i]); return i; }
answer: while 循环的结束条件为!(i < j),循环结束后,i在j之后,i所指的元素为,第一个大于pivot的元素,j所指的元素为,最后一个小于等于pivot的元素。所以执行的是,{ swap(arr[first], arr[j]); return j; }。
参考:《数据结构与算法分析--C语言描述》