常用排序算法(3) - 快速排序
快速排序
算法描述
快速排序,是历史上实践中最快的泛型排序算法,虽然其平均运行时间为O(n log n),而最坏情形下性能为O(n^2),但由于高度优化的内部循环,一般经过优化的快速排序不会出现这种最坏情形。快速排序也是一种分治的递归算法,它的基本步骤如下:
- 从无序数组中选出一个值作为基准值
- 把大于基准值的数放在数组右边,小于基准值的数放在数组左边
- 递归的将左边和右边的数组递归进行快速排序
- 由此就可得到一个有序数组
可以看出,上面算法描述中有一些点,是没有固定做法的。
- 如何选取基准值
- 如何将数组分割为大于、等于、小于的三部分
所以,算法中这些没有固定做法的点正是快速排序的可以优化的关注点。
实现
-
首先,是最简单的快速排序实现,算法中直接选用第0个元素作为基准值。
但是在这样的情况下,如果输入是预排序或者反序的,那么每次都依然会把所有元素放到同一个分组里面,而无法产生实质的分治策略。//快速排序(效率一般的实现) template <typename Comparable> void quickSort1(vector<Comparable>& a, int left, int right) { if (left >= right) return; auto tmp = std::move(a[left]); int i = left; int j = right; while (i < j) { while (i < j && a[j] >= tmp) --j; if (i < j) { //i是空位,可以把后面的元素放到a[i]位置,移动后,j位置变成了空位 a[i] = std::move(a[j]); ++i; } while (i < j && a[i] < tmp) ++i; if (i < j) { //j是空位,可以把前面的元素放到a[j]位置,移动后,i位置重新变成空位 a[j] = std::move(a[i]); --j; } } a[i] = std::move(tmp); //递归排序前半部分和后半部分 quickSort1(a, left, i - 1); quickSort1(a, i + 1, right); }
-
根据上面的分析,先对如何选取基准值做一些优化,这里使用常见的三数中值分割法。选取左边,右边和中间位置的值,用他们的中值作为基准值。
//三数中值选取,并将三个数排序,最左边是三数中最小的值,最右边是三数中最大的值,然后把中值放到right - 1的位置上,那么整个比较就只需要从 left 到right - 1。 template <typename Comparable> int median3Num(vector<Comparable>& a, int left, int right) { int mid = (left + right) / 2; if (a[left] > a[mid]) std::swap(a[left], a[mid]); if (a[mid] > a[right]) std::swap(a[mid], a[right]); if (a[left] > a[mid]) std::swap(a[left], a[mid]); std::swap(a[mid], a[right - 1]); return a[right - 1]; }
而对于如何将数组分割,跟上面的普通做法相近,但是并不需要有空出一个位置来存放数字的概念,而只需要两边 i 和 j 找到各自不符合的元素后,交换位置即可同时完成两个数字的定位。
//快速排序 template <typename Comparable> void quickSort(vector<Comparable>& a) { if (a.size() < 2) return; quickSort(a, 0, a.size() - 1); } template <typename Comparable> void quickSort(vector<Comparable>& a, int left, int right) { if (left + 10 <= right) { auto& pivot = median3Num(a, left, right); int i = left; int j = right - 1; while (i < j) { while (a[++i] < pivot) {} while (a[--j] > pivot) {} if (i < j) std::swap(a[i], a[j]); } //恢复基准值位置 std::swap(a[i], a[right - 1]); quickSort(a, left, i - 1); //排序小于基准值的元素 quickSort(a, i + 1, right); //排序大于基准值的元素 } else InsertionSort(a, left, right); //当数组内数字较少时直接使用插入排序 } //插入排序(带左右边界) template <typename Comparable> void InsertionSort(vector<Comparable>& a, int left, int right) { for (int i = left + 1; i <= right; ++i) { auto tmp = std::move(a[i]); int j = i; for (; j > left && tmp < a[j - 1]; --j) { a[j] = std::move(a[j - 1]); } a[j] = std::move(tmp); } }
可以看到上面快速排序最后在元素较少时直接使用了插入排序,这是因为较少时,如果数组接近有序,插入排序的复杂度下限可以到O(n),而不再需要递归进行快速排序。这也是一个优化,而许多相关文章的说法是数组元素在10~20左右取值都是合理的。
后记
除了在元素较少时使用插入排序,同样的还有当递归层次较深时转而使用堆排序,堆排序的时间复杂度同样可以到O(n log n),所以整体的空间复杂度和时间复杂度都是可控的。STL里面的排序算法也是综合了3种排序的实现。