我是如何被快速排序的暴风雨所洗礼的?

一、写这篇文章的初衷

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];
}
复制代码

声明两个临时数组 leftright,然后遍历数组 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]);
    }
  }
}
复制代码

最后就是我们非常熟悉的递归操作了,反反复复地对两个子数组 leftright 进行上面的操作。

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) 拼接成的数组,给到上一次递归,供上一次递归进行拼接。

递归到最后,整个数组也就被排好序了。

这类似 上梁不正下梁歪 的道理,下梁都被扶正了,这上梁自然也就是正的了(吐槽:什么烂比喻......)。

诶?这快速排序,就这么轻而易举被我打败了吗?其实不然。

上面这种写法,我想大多数人第一反应都会这么去写。

优点:

  1. 非常容易想到,间接地保护了我们的大脑,让我们短时间内很有成就感;
  2. 代码足够简洁明了,即使是初学算法的小白也能看懂每一行代码都在做什么。

缺点:

  1. 容易让人变得自大,间接地阻碍了你代码水平的提升;
  2. 这种写法最大的问题在于,每次递归都需要创建两个临时数组,浪费内存,空间复杂度很糟糕。

它在 leetcode 跑出的成绩如下:

  • 执行用时:152 ms,在所有 JavaScript 提交中击败了 71.95% 的用户;
  • 内存消耗:60.5 MB,在所有 JavaScript 提交中击败了 6.37% 的用户。

果然,浪费是最大的可耻!时间复杂度有待改善,空间复杂度更需要拯救。不然又会被嘲讽了:这就是前端同学写出来的快速排序吗?

三、我就在原地快速排序

对,没错,这次我就在原地进行快速排序,不创建临时数组 leftright 了。

代码我先贴为敬,后面再详细解释。

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 们正名了!

江湖路远,有缘再会吧!

おすすめ

転載: juejin.im/post/7068969129692102692