算法学习笔记 - 1 (四种经典排序算法)

527 阅读7分钟

前言

学习与分享,两者是一个相辅相成的关系。写下笔记,记录下当前的学习记录,待下次需要复习的时候,能有个很好的借鉴,也希望借此加深自己对已学知识的理解。我将尽量以通俗易懂的方式来描述,如果有什么描述不当的地方,还请各位大大指出,谢谢。

正文

1. 选择排序 (将数组内元素从小到大进行排序,下同), eg: [3, 5, 6, 2, 1]

> 核心思想是,从左往右,找出最小的元素值对应的索引`min`,与需要替换的值进行替换

代码实现:

    const selectionSort = (array) => {
        // 从左往右进行排序,先排第一个
        let index = 0
        while (index < array.length - 1) {
            // 假定第一个值是最小的
            let min = index
            // 遍历该数组
            for (let i = min + 1; i < array.length; i++) {
                // 寻找比它小的值,如果能找到比它小的,就记录最小值对应索引,以便循环完毕后进行替换
                if (array[i] < array[min]) {
                    min = i
                }
            }
            // 替换元素值,并让index++,进行下一轮循环找最小值, 直到倒数第二个值为止
            swap(array, index++, min)
          }
    }

2. 插入排序

>   核心思想是, 从左往右,如果下一个元素比前一个元素小,则进行互换,否则跳出循环进入下一轮比对

插入排序配图

插入排序配图2
代码实现:

    const insertSort = (array) => {
      let index = 0
      while (index < array.length) {
        // 向后加一位,然后对这个数组进行值对比
        for (let i = ++index; i > 0; i--) {
          if (array[i] < array[i - 1]) {
            swap(array, i - 1, i)
          } else {
            break
          }
        }
      }
    }

3. 归并排序

> 核心思想是,将数组递归分割为各个小数组,并给各个小数组各自进行排序,最后合并回原数组中,
  • 以四个元素的数组为例 arr = [5, 6, 2, 1]
  • 获取中间值作为递归分割点 middle = Math.floor((0 + 3) / 2) => middle = 1
  • 第一个小数组为索引 0 - middle 对应的元素, 第二个小数组为索引 middle - arr.length -1 对应的元素
  • 也就是将arr分为两个小数组,分别为 [5, 6], [2, 1]
  • 先对小数组进行排序,这里直接看右边的小数组(因为第一个小数组相当于已经排好序了)
  • 循环小数组[2, 1](在 arr数组中对应的位置为索引23)
  • 获取中间值作为递归分割点middle = Math.floor((0 + 1) / 2) => middle = 0
  • 由于再往后进行的时候,两个元素的数组会分割成两个只有一个元素的数组,那个时候已经没必要进行单个元素自己对比自己。因此,可以对小数组内元素进行排序
  • 先建立辅助数组 aux,并且该数组是原数组的浅拷贝 aux = [...arr]
  • middle为分割点,将小数组模拟划分为两个小数组,左数组入口i为元素2对应的索引值2,右数组入口jmiddle + 1,也就是元素1对应的索引值3
  • 循环aux对应的索引23

  • 这样就将一个小数组排好序了(可能没有讲的很明白,如果有问题,评论区里问一下,我在空闲的时候回复你,另附可视化跳链)
  • 具体代码实现。 PS: 这里的代码只是排序的代码,并没有包括分组的代码(看下去)
  // array => 原数组
  // aux => 辅助数组
  // low => 需要排序的索引最小值
  // mid => 分割点
  // high => 需要排序的索引最大值
  const _merge = (array, aux, low, mid, high) => {
    // 重新给辅助数组赋值,防止辅助数组已被污染
    for (let k = low; k <= high; k++) {
      aux[k] = array[k]
    }

    let i = low // 设置左入口
    let j = mid + 1 // 设置右入口
    for (let k = low; k <= high; k++) {
      if (i > mid) { // 如果左边的入口索引值已经大于mid,则证明左边的值都已经排序完成
        array[k] = aux[j++]
      } else if (j > high) { // 同理,如果右边的入口索引值已经大于最大长度,则证明右边的值都已经排序完成
        array[k] = aux[i++]
      } else {
        // 如果左边当前索引值小于右边的索引值,则将左边索引对应值赋值给数组
        aux[i] < aux[j] ? array[k] = aux[i++] : array[k] = aux[j++]
      }
    }
  }
  • 归并算法详尽代码
let array = [5, 6, 2, 1]

const mergeSort = (array) => {
  const _sort = (array, aux, low, high) => {
    if (low >= high) {
      return
    }
    const mid = Math.floor((low + high) / 2)
    // 以 mid 为分割点,分割数组至更小的数组然后再进行 _merge 排序
    _sort(array, aux, low, mid)
    _sort(array, aux, mid + 1, high)
    _merge(array, aux, low, mid, high)
  }
  
  const _merge = (array, aux, low, mid, high) => {
    for (let k = low; k <= high; k++) {
      aux[k] = array[k]
    }

    let i = low // 设置左入口
    let j = mid + 1 // 设置右入口
    for (let k = low; k <= high; k++) {
      if (i > mid) { // 如果左边的入口索引值已经大于mid,则证明左边的值都已经排序完成
        array[k] = aux[j++]
      } else if (j > high) { // 同理,如果右边的入口索引值已经大于最大长度,则证明右边的值都已经排序完成
        array[k] = aux[i++]
      } else {
        // 如果左边当前索引值小于右边的索引值,则将左边索引对应值赋值给数组
        aux[i] < aux[j] ? array[k] = aux[i++] : array[k] = aux[j++]
      }
    }
  }
  // 设置辅助数组
  let aux = [...array]
  _sort(array, aux, 0, array.length - 1)
}
mergeSort(array)
console.log(array)
// [1, 2, 5, 6]

4. 快速排序

核心思想是,选定一个值作为支点,然后根据该支点对应的值将比它小的值划分到左边,将比它大的值划分到右边,此时该支点对应的值已经排好,然后再对支点左边的值进行排序,对右边的值进行排序。(不分先后,都可以)

  • 以首位index = 0为支点pivat,也可以认为是分区点
  • 设定一个让索引往右移的指针,为支点将要去往的分支点的右边, storeIndex = index + 1
  • 循环数组,只要找到比支点小的值,就让它与storeIndex指针对应的值替换,并且让storeIndex往右移(++)
  • 直到数组已经循环完毕后,storeIndex对应的值还是分支点的右边,此时只需要将storeIndex - 1的值与pivat进行替换,pivat的值就已经排好了。

  • 对应代码实现
  const _quickSort = (array, low, high) => {
    let pivat = low // 设定支点值
    let storeIndex = low + 1  // 以支点后第一个值作为划分比支点值小以及比支点值大的索引值,称为阈值
    let index = storeIndex // 指针从分区值之后第一个值开始
    while (index <= high) {
      // 遇到比支点值小的,就和阈值进行交换,并让阈值索引后移一位
      if (array[index] < array[pivat]) {
        swap(array, storeIndex++, index)
      }
      index++
    }
    // 此时的阈值是处于划分两侧值的阈值右方,因此需要 - 1 拿到真正的支点分区值,与设定的支点进行替换,此时左侧的值都是比分区值小的,右侧都是比分区值大的
    swap(array, pivat, storeIndex - 1)
  }
  • 快速排序详细代码
// 快速排序
let array = [3, 5, 6, 2, 1]

const quickSort = (array) => {
  const _sort = (array, low, high) => {
    if (low >= high) {
      return
    }
    let j = _quickSort(array, low, high)
    _sort(array, low, j - 1)
    _sort(array, j + 1, high)
  }
  const swap = (array, i, j) => {
    let temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  const _quickSort = (array, low, high) => {
    let pivat = low // 设定支点值
    let storeIndex = low + 1  // 以支点后第一个值作为划分比支点值小以及比支点值大的索引值,称为阈值
    let index = storeIndex // 指针从分区值之后第一个值开始
    while (index <= high) {
      // 遇到比支点值小的,就和阈值进行交换,并让阈值索引后移一位
      if (array[index] < array[pivat]) {
        swap(array, storeIndex++, index)
      }
      index++
    }
    // 此时的阈值是处于划分两侧值的阈值右方,因此需要 - 1 拿到真正的支点分区值,与设定的支点进行替换,此时左侧的值都是比分区值小的,右侧都是比分区值大的
    swap(array, pivat, storeIndex - 1)
    return storeIndex - 1
  }
  _sort(array, 0, array.length - 1)
}

quickSort(array)
console.log(array)

写在最后

写着写着就6.1了,5月的随后一天,在写这篇文章的前5分钟,我都没有想过要写下来,不过后来在学习的过程中,总会遇到一些当时难以理解的问题,如果能把它记录下来,那当我下次在使用到,但那一时半会没法想起具体的思想的时候,可以回来看看自己当时的想法,如果有什么理解错误的地方,还请指出,谢谢。看到这里的同行者,谢谢你,能让你读到这里,就表示我还是有进步的了,后续继续加油!当然,如果我的这篇花了不少心思的文章能够刚好帮到你,我也很荣幸与开心哈哈。