一、写这篇文章的初衷
Hi~ 我是小雨。
最近在刷知乎的时候,无意间看到这样一个提问 —— 《阮一峰版快速排序完全是错的》一文是否存在事实错误?
有个同学在某平台上的留言被 winter 老师截图挂在回答里了,其中有句话让我印象很深刻:
前端的天花板真的是实在是太低了。
我个人对阮一峰老师还是有一点点小崇拜的,记得大学初学前端那会儿,经常翻看阮老师的 JS 教程,即使后来参加工作了,也偶尔去查阅他写的 Flex 布局教程。
这样一个充满技术热情的师长形象,和自己所从事的领域,被人贬低得一文不值,这成功地燃起了我的胜负欲。
于是气势汹汹地打开了我的 VSCode。
二、快速排序很难吗
我呢,一直坚信一个观点 —— 所有技术名词都是纸老虎。
要想实现快速排序,首先要弄清楚它是什么?
下面是维基百科对快速排序的定义:
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的 2个子序列,然后递归地排序两个子序列。
这么一看,是不是有点眉目了?那就乘胜追击,开始写代码吧!
首先,声明一个函数 sortArray
,它接收一个数组 nums
作为参数。
先提前判断一下:如果这个数组 nums
的长度小于或等于 1,根本不用排序,直接返回原数组即可。
function sortArray(nums) {
if (nums.length <= 1) {
return nums;
}
}
复制代码
在数组 nums
中选择其中一个作为参照物(我偏不叫它基准),为后续分割大小两个子数组作参考,这里我就选择中间那一个吧,有意见先保留。
function sortArray(nums) {
if (nums.length <= 1) {
return nums;
}
var pivotIndex = Math.floor(nums.length / 2);
var pivotValue = nums[pivotIndex];
}
复制代码
声明两个临时数组 left
和 right
,然后遍历数组 nums
:
- 比参考物小的放进
left
; - 比参考物大的或相等的放进
right
; - 遇到参考物自身,跳过当前循环即可。
function sortArray(nums) {
if (nums.length <= 1) {
return nums;
}
var pivotIndex = Math.floor(nums.length / 2);
var pivotValue = nums[pivotIndex];
var left = [];
var right = [];
for (var i = 0; i < nums.length; i += 1) {
if (i === pivotIndex) {
continue;
}
if (nums[i] < pivotValue) {
left.push(nums[i]);
} else {
right.push(nums[i]);
}
}
}
复制代码
最后就是我们非常熟悉的递归操作了,反反复复地对两个子数组 left
和 right
进行上面的操作。
function sortArray(nums) {
if (nums.length <= 1) {
return nums;
}
var pivotIndex = Math.floor(nums.length / 2);
var pivotValue = nums[pivotIndex];
var left = [];
var right = [];
for (var i = 0; i < nums.length; i += 1) {
if (i === pivotIndex) {
continue;
}
if (nums[i] < pivotValue) {
left.push(nums[i]);
} else {
right.push(nums[i]);
}
}
return sortArray(left).concat([pivotValue], sortArray(right));
}
复制代码
每次递归结束时,返回 sortArray(left)
、[pivotValue]
和 sortArray(right)
拼接成的数组,给到上一次递归,供上一次递归进行拼接。
递归到最后,整个数组也就被排好序了。
这类似 上梁不正下梁歪 的道理,下梁都被扶正了,这上梁自然也就是正的了(吐槽:什么烂比喻......)。
诶?这快速排序,就这么轻而易举被我打败了吗?其实不然。
上面这种写法,我想大多数人第一反应都会这么去写。
优点:
- 非常容易想到,间接地保护了我们的大脑,让我们短时间内很有成就感;
- 代码足够简洁明了,即使是初学算法的小白也能看懂每一行代码都在做什么。
缺点:
- 容易让人变得自大,间接地阻碍了你代码水平的提升;
- 这种写法最大的问题在于,每次递归都需要创建两个临时数组,浪费内存,空间复杂度很糟糕。
它在 leetcode 跑出的成绩如下:
- 执行用时:152 ms,在所有 JavaScript 提交中击败了 71.95% 的用户;
- 内存消耗:60.5 MB,在所有 JavaScript 提交中击败了 6.37% 的用户。
果然,浪费是最大的可耻!时间复杂度有待改善,空间复杂度更需要拯救。不然又会被嘲讽了:这就是前端同学写出来的快速排序吗?
三、我就在原地快速排序
对,没错,这次我就在原地进行快速排序,不创建临时数组 left
和 right
了。
代码我先贴为敬,后面再详细解释。
function swap(nums, left, right) {
var template = nums[left];
nums[left] = nums[right];
nums[right] = template;
}
function partition(nums, left, right) {
var pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left;
var pivotValue = nums[pivotIndex];
swap(nums, pivotIndex, right);
for (var i = left; i < right; i += 1) {
if (nums[i] <= pivotValue) {
swap(nums, left, i);
left += 1;
}
}
swap(nums, left, right);
return left;
}
function quickSort(nums, left, right) {
if (left >= right) {
return;
}
var nextPivot = partition(nums, left, right);
quickSort(nums, left, nextPivot - 1);
quickSort(nums, nextPivot + 1, right);
}
function sortArray(nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
复制代码
不好意思,我可能要食言了,懒得解释了,给你们一个视频自己去悟可以吗?
视频在这里:1秒记住快速排序
虽然也是个标题党,不过视频里的动画还是很生动的,我就是看着这个动画写出来的这么棒的快速排序。
有多棒?看看 leetcode 跑出来的成绩:
- 执行用时:104 ms,在所有 JavaScript 提交中击败了 99.38% 的用户;
- 内存消耗:49.3 MB,在所有 JavaScript 提交中击败了 75.22% 的用户。
我是挺满意的了,你们呢?
四、结尾要说的
一句话:我为前端 XDJM 们正名了!
江湖路远,有缘再会吧!