算法工程师的升级之路-数据结构与算法篇(3)快速排序
引言
快速排序与之前提到过的归并排序法相似,都是基于分治思想的排序算法。快速排序算法的核心要点就是基于切分操作,理解了切分操作就理解了80%的快速排序算法了。
算法原理
快速排序的算法原理为下图
由图可见,通过切分操作数组被分割为三部分:小于切分元素的子数组;切分元素;不小于切分元素的子数组。
回忆一下有序数组的概念:a0≤a1≤...an-1≤an,只需要满足前面的元素小于或等于后面的元素,数组就是有序的。那么我们来看一下切分操作的结果,切分元素左边的子数组的所有元素小于切分元素,切分元素右边的子数组的所有元素不小于(大于或等于)切分元素。我们可以得到一个结论就是切分操作令切分元素放置在了正确的位置上。
接下来的操作就是对无序的子数组递归地执行切分操作,得到了熟悉的二叉树结构。对于第n层的递归切分,可以将2n-1个元素放置在正确的有序位置上。通过简单的等比数列求和,对于第n层的递归,2n-1个元素都已经有序。所以对于长度为L的数组,需要的递归二叉树的理想高度是log2L,而切分操作是线性的。所以直观地,快速排序的时间复杂度是O(n logn),是一个对数线性的排序算法。
代码实现
下面以C++代码为例实现一个快速排序算法。
#include<string>
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
void _swap(vector<int>&a, int lo, int hi) { int v = a[lo]; a[lo] = a[hi]; a[hi] = v; } int partition(vector<int>&a, int lo, int hi) { int i = lo, j = hi + 1; int v = a[lo];//切分元素 while (true) { while (a[++i] < v)if (i >= hi)break; while (a[--j] > v)if (j <= lo)break; if (i >= j)break;//避免越界 _swap(a, i, j); } _swap(a, lo, j); return j; } void quicksort(vector<int>&a, int lo, int hi) { if (lo >= hi)return; int j = partition(a, lo, hi); quicksort(a, lo, j - 1); quicksort(a, j + 1, hi); } void sort(vector<int>&a) { int n = a.size(); if (n == 0 || n == 1)return; quicksort(a, 0, n - 1); } void main() { vector<int> a = { 1,3,5,5,2,3,1,5,10,0,1,2,4 }; sort(a); for (int i : a)cout << i << ','; cout << endl; }
性能分析
由于切分元素是随机选择的,而快速排序是基于切分操作进行的。所以我们可以想到,算法的效率与切分元素的选择有很大的关系,当切分元素恰好为数组的中位数的时候(恰好将数组对半分),排序的效率是最高的(保证了树的左右是对称生长的,树的高度最小)。所以在使用快速排序之前,都会将数组进行随机打乱,避免出现近似有序的数据导致性能恶化。
快速排序和归并排序虽然都是O(n logn)的算法,但是实际上快速排序会比归并排序算法要快。观察快速排序的代码可以发现,内部的循环使用一个递增的索引将元素和一个定值进行比较,这么做高效地利用了内存的cache结构(将一定字节的数据同时加载到内存中),所以速度快。同时快速排序的数据移动比归并排序要少(归并排序从辅助数组移动回排序数组将涉及大量的数据移动)。所以快速排序的正确实现速度很快。
快速排序与归并排序不同,归并排序是稳定的排序(在归并操作中,可以保证相同大小元素的相对位置不变);快速排序算法是不稳定的排序算法。如对数组【 2,5,1,1,2,2,3,1'】(为了区分前后两个1,给第二个1加了'),以第一个2为切分元素,交换的结果为【1,1',1,2,2,2,3,5】可以看到,最后一个1被交换到了第二的位置。
性能优化
对于快速排序,性能能够继续进行优化和改进。有以下的几点:
1.子数组规模小的时候,切换到插入排序。(一般5~15的数组切换到插入排序的效果好)
2. 使用三向切分快速排序,将数组切分为小于、等于、大于三段。(改善了有大量相同元素的排序效率)