排序问题之快速排序
排序问题
算法问题的基础问题之一,便是排序问题:
输入:n个数的一个序列,<a1, a2,..., an>。
输出:一个排列<a1',a2', ... , an'>,满足a1' ≤ a2' ≤... ≤ an' 。(输出亦可为降序,左边给出的例子为升序)
一.算法描述
(1)分治法
快速排序使用到了分治方法(Divide and Conquer)。
Divide:将原问题分解为若干子问题,其中这些子问题的规模小于原问题的规模。
Conquer:递归地求解子问题,当子问题规模足够小时直接求解。
Merge:将子问题的解合并得到原问题的解。
(2)快速排序的分治思想
分解:分解待排序的n个元素序列A[p, ... q]为A[p, ... , r-1],A[r],A[r+1, ... , q],使得A[p, ... , r-1]中的所有元素都小于A[r],A[r+1, ... , q]中的所有元素都大于A[r]
解决:递归地调用快速排序,对两个子序列A[p, ... , r-1]、A[r+1, ... , q]进行快速排序
合并:因为子序列都是按原址排序的,不需要进行合并操作
(3)快速排序
Partition(A, p, q):假设我们要对一个输入规模为n的序列A[p,p+1, ... , q]进行排序,我们可以选择头部A[p]为主元pivot,把小于pivot的元素都交换到pivot左边,大于它的都交换到它右边,最后返回pivot的下标r。
QuickSort(A, p, q):先调用Partition(A, p, q)进行划分,然后对pivot划分出的两个子序列继续快速排序QuickSort(A, p, r-1)、QuickSort(A, r+1, q)。
下面我们给出一个对序列[5, 2, 4, 6, 1, 3]使用归并排序得到递增序列的过程。在图中我们给出了第一次划分的过程,首先选取第一个元素作为主元Pivot,红色剪头指向小于Pivot分区的末尾,初始位置为Pivot。往后遍历过程中每次遇到了比Pivot小的元素时,红色箭头就后移一位,并将找到的元素与该位置调换。在循环过程中保持红色部分的元素都比Pivot小,黄色部分都比Pivot大,最后将红色箭头所指元素与Pivot调换,就得到了分区结果。
如下图整个排序过程我们需要进行三次划分,每次都选出要划分序列的队首(蓝色框)作为pivot,然后在递归结束后便能直接得到排序后的结果,不需要合并操作。
二.代码实现
下面是插入排序的C++实现:
#include<iostream> #include<cmath> using namespace std; /** * @brief 对arr[start, end]区间进行随机化快速排序 * @param arr 待排序的数组 * @param start 序列的开始下标 * @param end 序列的终止下标 */ void QuickSort(int* arr, int start, int end) { //起始下标不小于终止下标,就不需要再分区 if(start >= end){ return; } else{ //开始分区 int pivot = arr[start]; int little_end = start; for(int i = start+1; i <= end; i++){ if(arr[i] < pivot){ little_end++; swap(arr[i],arr[little_end]); } } //unstable way //swap(arr[start], arr[little_end]); //stable way for(int i = start; i < little_end; i++){ arr[i] = arr[i + 1]; } arr[little_end] = pivot; //分区完成后对子问题快排 QuickSort(arr, start, little_end - 1); QuickSort(arr, little_end + 1, end); } } int main() { int arr[] = {5,2,4,7,10,9,8,1,6,3}; QuickSort(arr,0,sizeof(arr)/sizeof(int) - 1); for(int i = 0; i < sizeof(arr)/sizeof(int); i++){ cout << arr[i] <<' '; } cout << endl; }
三.算法分析
(1)时间复杂度
快速排序的性能依赖于划分是否平衡。
Best-case:每次划分出的子序列长度都不大于划分前的一半。有递归式T(n) = 2T(n/2) + θ(n),推出T(n) = θ(nlgn),即时间复杂度为o(nlgn)。
Worst-case:每次划分都不平衡,得到的子序列长度分别为0和n-1。有递归式T(n) = T(n-1) + θ(n)。这样的快排近似于插入排序,时间复杂度为o(n2)。
Average-case:划分后两子序列的比例为常数,例如2:8。经过画递归树得到这种平衡划分的情况与1:1划分的情况时间复杂度相同,为o(nlgn)。
(2)稳定性
快速排序的稳定性取决于划分算法,第二节代码实现中给出的划分算法是稳定的。而在1962年Hoare在《QuickSort》论文中首次提出这个算法时,给出的Hoare_Partition划分是非稳定的。
(3)适合范围
被认为是最佳的比较排序算法,平均时间最短,对于随机分布的序列来说一般都不会是worst-case。
(4)算法改进
随机化快速排序。对于已排序的序列或者几乎排序好的序列,快速排序的运行接近于最差情况,所以我们可以采用随机化的方法来避免。一种简单的随机化策略是每次分区之间随机的选取pivot,而非选取第一个的值。所以针对我们已有的算法,我们只需要将数组首位与随机一位的值交换后再进行相同的操作即可。
使用Hoare划分的随机化快速排序代码如下:
#include<iostream> #include<vector> using namespace std; /** * @brief 生成[start, end]区间内的随机整数 */ int GenRandom(int start, int end){ srand((unsigned)time(NULL)); return rand()%(end - start + 1) + start; } /** * @brief 对v[start, end]区间进行随机划分 * @return 主元的索引 */ int HoareRandomPartition(vector<int> &v, int start, int end){ //生成一个区间内随机数,将该位置元素与第一位元素交换 int random = GenRandom(start, end); swap(v[start], v[random]); //Hoare划分 int pivot = v[start]; int i = start; int j = end; while(true){ while(v[j] > pivot){ j--; } while(v[i] <= pivot && i < j){ i++; } if(i<j) swap(v[i], v[j]); else break; } swap(v[start],v[j]); return j; } /** * @brief 对v[start, end]区间进行随机化快速排序 */ void RandomizedQuickSort(vector<int> &v, int start, int end){ //随机划分后,再对主元左右两侧的序列进行快排 if(start < end){ int SplitIndex = HoareRandomPartition(v, start, end); RandomizedQuickSort(v, start, SplitIndex-1); RandomizedQuickSort(v, SplitIndex + 1, end); } } int main() { //arr为要排序的数组 //int arr[] = {5,2,4,7,10,9,8,1,6,3}; int arr[] = {5,2,4,7,10,9,8,1,6,3,12,34,54,765,2}; //把数组放入向量中 vector<int> v(arr, arr + sizeof(arr)/sizeof(int)); //随机化快速排序 RandomizedQuickSort(v, 0, sizeof(arr)/sizeof(int)-1); //按顺序打印排序后向量中的所有元素 copy (v.begin(), v.end(), ostream_iterator<int> (cout, " ")); cout << endl; }