✅✅排序算法(上)

150 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情🚀🚀

前言

这几天卡哥那边的题太难了,然后又想到自己排序算法这一块还很欠缺,干脆搞一下排序算法~

后面的内容都以修言老师的小册内容为主

概念

我们在面试的时候,排序算法也是其中热门的考点。为什么面试官总是对排序算法情有独钟呢?

因为可以通过这种方式在较短的时间里试探出候选人算法能力的扎实程度和知识链路的完整性。

所以我们有必要把这一块的知识打扎实!!!

以面试为导向来看,需要大家着重掌握的排序算法,主要是以下5种:

  • 基础排序算法:

    • 冒泡排序
    • 插入排序
    • 选择排序
  • 进阶排序算法

    • 归并排序
    • 快速排序

后面我也按自己的语言来理解一下修言老师的内容吧~

冒泡排序

基本思路分析

冒泡排序的过程,就是从第一个元素开始,重复比较相邻的两个项,若第一项比第二项更大,则交换两者的位置;反之不动。
每一轮操作,都会将这一轮中最大的元素放置到数组的末尾。假如数组的长度是 n,那么当我们重复完 n 轮的时候,整个数组就有序了。

排序过程模拟

下面就拿一个数组举例

[5, 3, 2, 4, 1]

首先,将第一个元素 5 和它相邻的元素 3 作比较,发现5 比 3 大,故将 5 和 3 交换:

[3, 5, 2, 4, 1]
 ↑  ↑

将第二个元素 5 和第三个元素 2 作比较,发现 5 比 2大,故将 5 和 2 交换:

[3, 2, 5, 4, 1]
    ↑  ↑

将第三个元素 5 和第四个元素 4 作比较,发现 5 比 4 大,故将 5 和 4 交换:

[3, 2, 4, 5, 1]
       ↑  ↑

将第四个元素 5 和第五个元素 1 作比较,发现 5 比 1 大,故将 5 和 1 交换:

[3, 2, 4, 1, 5]
          ↑ ↑

到这里第一轮的排序就结束了

我们可以很清楚的看到,最大数仿佛就像气泡一样浮出了水面。

这就是冒泡排序的由来

后面的排序也以此类推,这里就不做过多赘述了~

代码实现

function betterBubbleSort(arr) {
    // 缓存数组长度
    const len = arr.length  
    // 外层循环用于控制从头到尾的比较+交换到底有多少轮
    for(let i=0;i<len;i++) {
        // 内层循环用于完成每一轮遍历过程中的重复比较+交换
        for(let j=0;j<len-1-i;j++) {
        // 若相邻元素前面的数比后面的大
            if(arr[j] > arr[j+1]) {
                // 交换两者
                [arr[j], arr[j+1]] = [arr[j+1], arr[j]]
            }
        }
    }
    return arr
}

这里内层循环为什么是j < len - 1 - i呢?

随着外层循环的进行,数组尾部的元素会渐渐变得有序——当我们走完第1轮循环的时候,最大的元素被排到了数组末尾;走完第2轮循环的时候,第2大的元素被排到了数组倒数第2位;走完第3轮循环的时候,第3大的元素被排到了数组倒数第3位......以此类推,走完第 n 轮循环的时候,数组的后 n 个元素就已经是有序的。

为了规避对尾部有序数组的遍历(它都有序了,还遍历啥呀~),直接写成j < len - 1 - i

选择排序

基本思路分析

选择排序的关键字是最小值:循环遍历数组,每次都找出当前范围内的最小值,把它放在当前范围的头部;然后缩小排序范围,继续重复以上操作,直至数组完全有序为止。

排序过程模拟

下面我们尝试基于选择排序的思路,对如下数组进行排序:

[5, 3, 2, 4, 1]

首先,索引范围为 [0, n-1] 也即 [0,4] 之间的元素进行的遍历(两个箭头分别对应当前范围的起点和终点):

[5, 3, 2, 4, 1]
 ↑           ↑

得出整个数组的最小值为 1。因此把1锁定在当前范围的头部,也就是和 5 进行交换:

[1, 3, 2, 4, 5]

交换后,数组的第一个元素值就明确了。接下来需要排序的是 [1, 4] 这个索引区间:

[1, 3, 2, 4, 5]
    ↑        ↑

遍历这个区间,找出区间内最小值为 2。因此区间头部的元素锁定为 2,也就是把 2 和 3 交换。相应地,将需要排序的区间范围的起点再次后移一位,此时区间为 [2, 4]:

[1, 2, 3, 4, 5]
       ↑     ↑

遍历 [2,4] 区间,得到最小值为 33 本来就在当前区间的头部,因此不需要做额外的交换。
以此类推,4会被定位为索引区间 [3,4] 上的最小值,仍然是不需要额外交换的。

代码实现

function selectSort(arr)  {
  // 缓存数组长度
  const len = arr.length 
  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex  
  // i 是当前排序区间的起点
  for(let i = 0; i < len - 1; i++) { 
    // 初始化 minIndex 为当前区间第一个元素
    minIndex = i  
    // i、j分别定义当前区间的上下界,i是左边界,j是右边界
    for(let j = i; j < len; j++) {  
      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
      if(arr[j] < arr[minIndex]) {  
        minIndex = j
      }
    }
    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
    if(minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
    }
  }
  return arr
}

插入排序

基本思路分析

插入排序的核心思想是“找到元素在它前面那个序列中的正确位置”。
具体来说,插入排序所有的操作都基于一个这样的前提:当前元素前面的序列是有序的。基于这个前提,从后往前去寻找当前元素在前面那个序列里的正确位置。

暂时不明白也没关系,后面有更详细的过程

排序过程模拟

下面我们尝试基于插入排序的思路,对如下数组进行排序:

[5, 3, 2, 4, 1]

首先,单个数字一定有序,因此数组首位的这个 5 可以看做是一个有序序列。在这样的前提下, 我们就可以选中第二个元素 3 作为当前元素,思考它和前面那个序列 [5] 之间的关系。很明显, 3 比 5 小,注意这里按照插入排序的原则,靠前的较大数字要为靠后的较小数字腾出位置:

[暂时空出, 5, 2, 4, 1]
当前元素 3

再往前看,发现没有更小的元素可以作比较了。那么现在空出的这个位置就是当前元素 3 应该待的地方:

[3, 5, 2, 4, 1]

以上我们就完成了一轮插入。 这一轮插入结束后,大家会发现,有序数组 [5] 现在变成了有序数组 [3, 5] ——这正是插入排序的用意所在,通过正确地定位当前元素在有序序列里的位置、不断扩大有序数组的范围,最终达到完全排序的目的

后面的排序也以此类推,这里就不做过多赘述了~

其实这里的过程也可以通俗理解为我们打扑克, 开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。

代码实现

function insertSort(arr) {
  // 缓存数组长度
  const len = arr.length
  // temp 用来保存当前需要插入的元素
  let temp  
  // i用于标识每次被插入的元素的索引
  for(let i = 1;i < len; i++) {
    // j用于帮助 temp 寻找自己应该有的定位
    let j = i
    temp = arr[i]  
    // 判断 j 前面一个元素是否比 temp 大
    while(j > 0 && arr[j-1] > temp) {
      // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
      arr[j] = arr[j-1]   
      j--
    }
    // 循环让位,最后得到的 j 就是 temp 的正确索引
    arr[j] = temp
  }
  return arr
}

不得不说,这里的代码还是有难度的。后面自己还得多看看。

小结

  • 冒泡排序:每一轮将最大的数通过气泡浮出水面的方式排序出来~
  • 选择排序:每一轮将最小的数放置在数组的最前面~
  • 插入排序:像整理扑克牌一样,先找到一定的有序数组,然后与有序数组进行比较,找到合适的位置。