在讲解 快速排序 之前,我们提前引入了两个问题:
如果你还未研读上面的两个题目,建议先研习一下,因为我们这章要讲解的快速排序原理就是基于上面两个问题的解决方式来做的
快速排序可以分为 3 个版本:
1.0 版本:荷兰国旗问题
固定选择最末元素为 num 值,保证 num 前的数组内容分为两部分
完成后 num 与 >num 区域的第一个元素交换,这样 num 确定好了位置
剩余就是递归新的两部分
代码部分:
int partition1(int *arr, int l, int r) { int less = l - 1; int more = r; while(l < more) { if(arr[l] <= arr[r]) { swap(arr, ++less, l++); } else { l++; } } swap(arr, less+1, r); return less+1; } void doquickSort1(int *arr, int l, int r) { if(l < r) { int pIdx = partition1(arr, l, r); doquickSort1(arr, l, pIdx-1); doquickSort1(arr, pIdx+1, r); } }
2.0 版本:荷兰国旗问题II
固定选择最末元素为 num 值,保证 num 前的数组内容分为三部分
完成后 num 与 >num 区域的第一个元素交换,这样 num 确定好了位置一定在 == num 区域
剩余就是递归新的两部分
相比较 1.0 因为一次搞定了一批 == num 的数字,所以效率有提升
代码部分:
int* partition2(int *arr, int l, int r) { int less = l - 1; int more = r; while(l < more) { if(arr[l] < arr[r]) { swap(arr, ++less, l++); } else if(arr[l] > arr[r]) { swap(arr, --more, l); } else { l++; } } swap(arr, more, r); int *pIdx = (int *)malloc(sizeof(int) * 2); pIdx[0] = less+1; pIdx[1] = more; return pIdx; } void doquickSort2(int *arr, int l, int r) { if(l < r) { int* pIdx = partition2(arr, l, r); // 数组指代 == num 的区间坐标 int newl = pIdx[0]; int newr = pIdx[1]; free(pIdx); doquickSort2(arr, l, newl-1); doquickSort2(arr, newr+1, r); } }
3.0 版本:荷兰国旗问题II
面对1.0 或者 2.0 版本,面对最差情况,都是 O(N^2)
比如 1,2,3,4,5,6,7,8,9 (num 选定的很偏)
当然最好的情况就是选定的 num 在中间位置左右,这样左右的区间规模几乎相等
按照 master 公式(忘记的参考这里:时间复杂度之 Master 公式)
T(N) = 2 * T(N / 2) + O(N) (左右规模都是 N /2,其余消耗在做partition过程)
这样计算出来的时间复杂度 = O(N* logN)
num 逐渐打偏就会退化成 O(N^2)的算法
采用范围内的随机选择一个数,当做num放到末尾
因为随机选的数,好情况与坏情况在概率上来讲都是一样的,每一个num造成的T(N)值都是等概率事件,都是 1 / N 的概率
把所有的数进行概率累加 + 数学上的长期期望得到时间复杂度 = O(N* logN)
数学的推理不在此讨论范围,记住就行
代码:
相对于2.0 版本仅有一个改动部分,就是不选用固定元素作为num
int randIdx = l + rand() % (r - l + 1); swap(arr, randIdx, r); int* pIdx = partition2(arr, l, r); // 依旧用 3 个区域