前言
上个月我贡献了掘金中的第一篇文章,小白也能看懂的五个排序算法解析, 给大家讲解了几个常用排序算法的白话文,然后有小伙伴向我提出建议,如果有代码附带一起说感觉效果会更好,于是我又来了。让每篇文章专注做一件事,总结文是让大家明白每个排序算法的思路和区别,单篇文章负责带领大家入门某个排序算法并进行实战。
正题
首先,跟大家说明白,快速排序的原理: 就是通过建立一个基准值(可以直接用数组第一个元素),把大于基准值的元素放到左半部分,小于基准值的元素放到右半部分。然后再对左右两个部分,分别进行快速排序,直到左右部分里的元素小于或者等于1个的时候终止。
暴力写法
思路
开辟左右两个部分的数组,把小于基准值的元素放到左边数组,大于的元素放到右边数组,然后如果左右数组的长度大于1,则递归执行。
实现
function quickSort(arr) {
// left存放比arr[0]小的, right存放比arr[0]大的
let left = [], right = [];
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[0]) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 如果长度大于1,递归进行快排
if (left.length > 1) {
left = quickSort(left);
}
if (right.length > 1) {
right = quickSort(right);
}
return [ ...left, arr[0], ...right ];
};
这样子一个简单的快速排序就实现了,不过这里用到了额外的空间,每次需要开辟左右两个数组去存储元素,这样子数据量大的时候,特别耗空间。
优雅写法
思路
于是我们对上面的代码进行一次优化,我们每次不开辟空间了,只记录当前的基准值的索引,然后把比基准值小的放到前面去,比基准值大的放到后面。但是这样子每次我们操作的时候,就需要执行三个动作:
- 删除节点, 从原本位置删掉;
- 插入节点, 插入最前面或者最后面;
- 变更索引, 如果前面插入了节点,基准值所在位置要加一,后面插入则减一。
这样子代码写完心智负担挺重的,因为索引不断被打乱,导致我们循环过程中本来该进行到的某个元素,会被意外变成另一个元素,那么如何在不变更原来的元素时进行呢?
首先我们把数据分成三部分:
基准值、 [比基准值小的]、 [比基准值大的]
我们把第一个元素作为基准值,然后只要去维护比基准值小的部分,剩余的自然是比基准值大的了。最终我们只需要把基准值调个位置,换到中间去就实现了一轮快速排序,然后递归进行即可。
[比基准值小的]、基准值、 [比基准值大的]
我们要做的是统计当前一共有几个比基准值小的元素,把他们放在前半段即可。最终数量就是我们基准值所在的索引。
实现
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
let index = partition(arr, left, right);
quickSort(arr, left, index - 1);
quickSort(arr, index + 1, right);
}
return arr;
};
// 寻找基准值的位置
// 通过left和right去记录从哪排到哪
function partition(arr, left, right) {
// 记录有多少个比基准值小的元素, 记得索引值 + left
let index = left;
// 记得是从left开始,不是从0开始
for (let i = left + 1; i <= right; i++) {
if (arr[i] < arr[left]) {
index++;
// 交换位置,把小于的都放前面来
swap(arr, i, index);
}
}
// 最终把基准值放到两部分数据中间
swap(arr, left, index);
return index;
}
// 交换节点的值
function swap(arr, i, j) {
if (i !== j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
实战
215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
提示:
1 <= k <= nums.length <= 104-104 <= nums[i] <= 104
思路
先给整个数组做个快速排序,然后返回索引值为k - 1的元素即可。刚刚我们一直是从小到达排序的,现在要改成从大到小排序了,其实只需要在我们刚刚的代码改一个符号即可。
实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
let reuslt = quickSort(nums);
return reuslt[k - 1];
};
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
let index = partition(arr, left, right);
quickSort(arr, left, index - 1);
quickSort(arr, index + 1, right);
}
return arr;
};
function partition(arr, left, right) {
let index = left;
for (let i = left + 1; i <= right; i++) {
// 从大到小和从小到大改这个符号即可
if (arr[i] > arr[left]) {
index++;
swap(arr, i, index);
}
}
swap(arr, left, index);
return index;
}
function swap(arr, i, j) {
if (i !== j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
优化
这道题目人家只要求出第k位,所以我们只需要求出相应的值即可,至于前半部分和后半部分的顺序我们并不关心。于是这道题目可以做如下优化:
- 确定我们要找的值所在区间,超出区间的我们不去排序;
- 每一轮找到基准值的位置,判断一下我们要找的元素是再基准值的左边还是右边,每次只需要对两者其一进行排序即可。
优化代码
/**
* @param {number[]} nums
* @param {number} k
* @return {number}
*/
var findKthLargest = function(nums, k) {
return quickSort(nums, k);
};
function quickSort(arr, k, left = 0, right = arr.length - 1) {
if (left < right) {
let index = partition(arr, left, right);
// 确定我们要找的值所在区间,超出区间的我们不去排序
if (index === k - 1) {
return arr[index];
} else if (index > k - 1) {
return quickSort(arr, k, left, index - 1);
} else {
return quickSort(arr, k, index + 1, right);
}
}
return arr[left];
};
function partition(arr, left, right) {
let index = left;
for (let i = left + 1; i <= right; i++) {
// 从大到小和从小到大改这个符号即可
if (arr[i] > arr[left]) {
index++;
swap(arr, i, index);
}
}
swap(arr, left, index);
return index;
}
function swap(arr, i, j) {
if (i !== j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
总结
学习排序主要学的是排序的思路,解决问题的思想。而不单单只是为了学习这固定的十几行代码。只有当你武器库足够丰富,你才可以选择用什么武器去杀敌,而不是有什么就拿什么。