一、简介
- 时间复杂度: O(nlogn)
- 空间复杂度:O(logn)
- 是否稳定:否
- 优点:快,数据移动较少
- 缺点:不稳定
二、核心思想
- 分治,选基准值,分成比基准值小的区域,比基准值大的区域治之。
- 递归,对分区再选基准值,分成更小的两个区域,直到不能再分。
三、排序过程图
其实大致过程就是选基准值,分区, 重复上面所说。有点像二叉树,二叉树的深度为log2n。
四、上代码
暂时先写三种写法吧,这三种都没有优化基准值的选择,基准值都是直接选的最左侧。
- 第一种非原地排序,写法很简单,能应付面试,但是面试官一般会问空间复杂度方面可以优化下吗?
- 第二种优化了空间复杂度,原地排序
- 第三种是第二种的优化,优化了交换次数
第一种、非原地排序
这个代码不用过多解释,看我注释即可。
const quickSort = function (arr) {
// 递归结束条件
if(arr.length < 2) return arr;
// 基准
const pivot = arr.splice(0, 1);
// 左区
const left = [];
// 右区
const right = [];
// 将剩余元素按照一定规则,分配到左区、右区。
for(let i = 0; i < arr.length; i++) {
// 大于基准值的分配到右区,小于基准值的分配到左区
if(arr[i] > pivot[0]) {
right.push(arr[i])
} else {
left.push(arr[i])
}
}
// 返回 左区 拼 基准 拼 右区, 再对左区、右区分别重选基准分区
return quickSort(left).concat(pivot).concat(quickSort(right));
}
第二种、原地排序
代码先看quickSort,再看partition,代码几乎每行都有注释了。看代码前带着几个问题来看。
问题:
- 左区和右区是怎么分的?
- 基准值与谁交换,它该在左区还是右区?
- 如何记录要和基准值交换的元素的下标?
const partition = function(arr, left, right) {
// 基准值
let pivotValue = arr[left];
// 左区末尾下标
let leftLast = left;
// 遍历数组,除最左侧,因为其被设置成了基准
for(let i = left + 1; i <= right; i++) {
// 小于基准值的应被放到左区,这里注意,左区末尾下标需要先加1
if(arr[i] <= pivotValue) {
leftLast++;
[arr[i], arr[leftLast]] = [arr[leftLast], arr[i]];
}
}
// 循环结束后的数组的顺序应该已经是 基准+左区+右区,但是我们想要得到的顺序是 左区+基准+右区,故将基准和左区末尾元素交换
[arr[left], arr[leftLast]] = [arr[leftLast], arr[left]];
// 最后返回新的基准下标即可
return leftLast;
}
const quickSort = function (arr, left = 0, right = arr.length - 1) {
// 递归结束条件
if (left >= right) return;
// 基准下标
let pivotIndex = partition(arr, left, right)
// 对左区再选基准分区
quickSort(arr, left, pivotIndex - 1);
// 对右区再选基准分区
quickSort(arr, pivotIndex + 1, right);
// 返回拍好序的数组
return arr;
}
第三种、原地排序II
第二种的升级版,第二种是找到小的就交换一次,这种是同时找到大的和小的交换这两者。从右往左记录第一个比基准值小的元素下标,从左往右记录第一个比基准值大的元素下标,将他们交换。一直到两者记录下标相交结束。
还是直接上代码吧。还是那句话,注释很详细,看不懂上面说的,看代码,代码看懂后再看上面的那段话你就懂了。
const partition = function(arr, left, right) {
// 基准值
const pivotValue = arr[left];
// 从左往右找大值的下标记录
let i = left;
// 做右往左找小值的下标记录
let j = right;
while(i < j) {
// 从右往左记录小于基准的小值
while(i < j && arr[j] >= pivotValue) {
j--
}
// 从左往右记录大于基准的大值
while(i < j && arr[i] <= pivotValue) {
i++
}
if(i < j) {
// 交换找到的大值和小值
[arr[i], arr[j]] = [arr[j], arr[i]]
// 交换后继续查找
i++;
j--;
}
}
// 这里其实i === j, 想不明白的自己拿一个数组按这个步骤走。
[arr[i], arr[left]] = [arr[left], arr[i]];
// 返回基准下标
return i;
}
const quickSort = function (arr, left = 0, right = arr.length - 1) {
// 递归结束条件
if (left >= right) return;
// 基准下标
let pivotIndex = partition(arr, left, right)
// 对左区再选基准分区
quickSort(arr, left, pivotIndex - 1);
// 对右区再选基准分区
quickSort(arr, pivotIndex + 1, right);
// 返回拍好序的数组
return arr;
}