快速排序
基本思想
快速排序是基于分治法的,其思路如下:
- 在待排序序列中,选取一个数值作为 基准
pivot
,通过一趟排序,将序列以pivot
划分为了 左右两个子序列 - 左子序列元素值都比
pivot
小,右子序列元素值都比pivot
大,相当于一趟排序后,确定了pivot
在序列中的最终位置 - 分别递归对左右子序列重复操作,直至确定所有元素的最终位置
❓ 其难点主要在于,在某一趟排序(实则为某次递归中),如何划分左右两个子序列
思路一
在之前学习javascript
的时候,看了阮一峰大神的博客,他给出一种浅显易懂的思路:
快速排序(Quicksort)的Javascript实现 - 阮一峰的网络日志 (ruanyifeng.com)
- 直接开辟两个辅助空间,用来存储左右两个子序列
- 遍历待排列表,与基准
pivot
比较,或小或大,分别存储在两个辅助空间中 - 然后分别对这两个辅助列表元素重复上述操作
虽然方法易理解,易实现,但是每轮都要开辟额外的存储空间,空间复杂度是大于 O(n)
的
思路二
在 严蔚敏 老师的教材《数据结构》上,划分操作如下:
- 假设每次以待排表中第一个元素为基准
pivot
,则将比pivot
小的元素向左移动,比pivot
大的元素向右移动 - 其移动 通过 交换 实现,可以理解为因为
pivot
记录了表头(即第一个元素),所以表中无论怎么交换,总有一个位置为 “空”
思路三
在刷题时看到的一种思路,只利用一次循环,操作如下:
- 以序列末元素 为基准
pivot
,从首到尾遍历序列,以i
记录首部下标 - 对于比基准大或相等的元素,不做处理
- 对于比基准小的元素,与
i
位置 做交换,i++
后移一位 - 遍历完成全部交换后,最后将 基准
pivot
与i
位置 交换,返回i
,确定i
的最终位置
代码CPP实现
参考思路二:
划分操作(思路二)
// 划分操作,low为数组头部,high为尾部,按升序排列
int partition(int arr[], int low, int high) {
int pivot = arr[low]; // 以数组第一个数为基准
while (low < high) { // 当low = high时,该躺排序(该次递归)完成
while (low < high && arr[high] >= pivot)
--high; // 因为表头为“空”,所以从右到左比较,一直到出现比 pivot 小的数
arr[low] = arr[high]; // 将该数存入“空”的位置(即low),high变为新的“空”的位置
while (low < high && arr[low] <= pivot)
++low; // 因为空的位置已经更改在右侧,所以从左到右比较,一直到出现比 pivot 大的数
arr[high] = arr[low]; // 将该数存入“空”的位置(即high),low 变为新的“空”的位置
} // 当结束遍历时,已经划分左右子表
arr[low] = pivot; // 确定 pivot 的最终位置 low(此时low = high)
return low; // 返回 pivot 的位置下标
}
操作步骤可以跟随注释过一遍,这也解释了 两个子表的划分以及元素的移动 是通过 交换 实现的。
快速排序并不产生有序子序列,每趟排序后会确定 pivot 的最终位置
划分操作(思路三)
// 划分操作,low为数组头部,high为尾部,按升序排列
int partition(vector<int> &arr, int low, int high) {
int pivot = arr[high]; // 以数组末元素为基准
int i = low; // i 为交换位置
for(int j = low; j < high; j++){
if(arr[j] < pivot)
swap(arr[i++],arr[j]); // 先交换位置,i再后移一位
}
swap(arr[i],arr[high]); // 确定 pivot 的最终位置
return i;
}
递归的程序结构
void quickSort(int arr[], int low, int high) {
if (low < high) { // 递归跳出条件
int pivotpos = partition(arr, low, high); // 先划分子表,后递归,由上向下确定每个基准的最终位置
quickSort(arr, low, pivotpos - 1);
//quickSort(arr, pivotpos, high); //wrong,基准元素位置已经确定为 pivotpos,不必再参与排序
quickSort(arr, pivotpos + 1, high);
}
}
性能分析与改进
-
平均空间复杂度
O(logn)
,借助了递归工作栈,平均情况下的递归栈深度为O(logn)
-
平均时间复杂度
O(nlogn)
-
稳定性:不稳定,元素值相同的元素相对位置会发生改变
-
提高效率:尽量选择贴近中值的元素作为基准,那么形成的递归树就会贴近 完全二叉树,从而递归深度最小
更多排序算法以及动画演示可以参考: