头图

小五的算法系列 - 排序算法

黄刀小五
English

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五算法系列起航篇 - 排序算法.

前言

此系列文章以《算法图解》和《学习JavaScript算法》两书为核心, 其余资料为辅助, 并佐以笔者愚见所成. 力求以简单、趣味的语言带大家领略这算法世界的奇妙. 此系列文章由浅入深, 逐一击破, 接下来我们用排序算法来开启这伟大航路的第一篇章.

本文讲解以下五种常见排序, 分别为三种简单排序: “冒泡排序”, “选择排序”, “插入排序” 和 两种复杂排序: “快速排序”, “归并排序”.

每种排序都通过 基本思想过程图解算法分析时间及空间复杂度代码实现 五部分进行详细阐述.

<补充说明> 建议优先弄懂程序栈的执行顺序, 可阅读《算法图解 - 3.3 栈》, 会让学习快排和归并事半功倍; 此章主要用于理解递归的执行顺序, 笔者认为, 如果快排和归并理解困难, 归根结底是递归没有理解透彻; 此章节可为接下来的算法学习打好基石;【以上不影响本文阅读, 大家因人而异, 自行选择】

冒泡排序

👺 基本思想: <相邻元素 两两对比找最大>

👺 过程图解:

👺 算法分析:

  1. 外层循环: length - 1
  2. 内层循环: length - 1 - 已确定元素个数(等同于外层循环的index)
  3. 比对相邻元素大小, 若 arr[j] > arr[j + 1], 则互换元素

👺 复杂度:

根据上述分析, 外层循环 $n-1$ 次, 内层循环 $(n-1)/2$ 次, 故总执行次数为 $(n-1)^2 / 2$

tips: 时间复杂度及空间复杂度均不关心低阶变量、系数及常数

时间复杂度: 上述最高项为 $n^2$, 故时间复杂度为 $O(n^2)$

空间复杂度: 只有temp变量需要内存空间, 故空间复杂度为 $O(1)$

👺 代码实现:

const bubbleSort = (arr: number[]) => {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}

选择排序

👺 基本思想: <依次选出最小值>

👺 过程图解:

👺 算法分析:

  1. 外层循环: length - 1
  2. 内层循环: 从 i + 1 处开始循环, 循环 length - (i + 1)
  3. 记录外层循环 indexminIndex, 内层循环时, 若发现更小值, 更新 minIndex
  4. 互换 arr[index]arr[minIndex] 的位置

👺 复杂度:

根据上述分析, 外层循环 $n-1$ 次, 内层循环 $(n-1)/2$ 次, 故总执行次数为 $(n-1)^2 / 2$

时间复杂度: $O(n^2)$

空间复杂度: tempminIndex变量需要内存空间, 因其不关心系数, 故为 $O(1)$

👺 代码实现:

const selectSort = (arr: number[]) => {
  let minIndex: number;

  for (let i = 0; i < arr.length; i++) {
    minIndex = i;

    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[minIndex]) minIndex = j;
    }
    
    let temp = arr[minIndex];
    arr[minIndex] = arr[i];
    arr[i] = temp;
  }

  return arr;
}

插入排序

👺 基本思想: <从左至右 依次确认元素项位置>

👺 过程图解:

👺 算法分析:

  1. 外层循环: 从1开始循环, 循环次数 length - 1
  2. 内层循环: 从 j = i 开始循环, 逐一与前项比对, 小于前项则互换位置, 直到插入到正确的位置

👺 复杂度:

根据上述分析, 外层循环 $n-1$ 次, 内层循环 $n/2$ 次, 故总执行次数为 $n * (n - 1) / 2$

时间复杂度: $O(n^2)$

空间复杂度: 只有temp变量需要内存空间, 故为 $O(1)$

👺 代码实现:

const insertSort = (arr: number[]) => {
  for (let i = 1; i < arr.length; i++) {
    for (let j = i; j > 0; j--) {
      if (arr[j - 1] > arr[j]) {
        let temp = arr[j - 1];
        arr[j - 1] = arr[j];
        arr[j] = temp;
      }
    }
  }
  return arr;
}

快速排序

👺 基本思想: <按基准值 左右分组后合并> (我们选取数组中间项为基准值)

👺 过程图解:

👺 算法分析:

  1. 快速排序的思想是以一个值为基准, 小于基准值的推入左侧数组, 大于基准值的推入右侧数组; 递归调用, 直至数组为空; 将左右数组与基准值进行拼接, 全部拼接后形成所需数组;
  2. 通过 spliceMath.floor(arr.length / 2) 来获取 middle 的值;
  3. 理解程序栈的调用顺序, 可很好的理解快速排序;

👺 复杂度:

以基准值分半的递归过程复杂度为 $logn$; for循环的复杂度为 $n$

时间复杂度: $O(nlogn)$

空间复杂度: 空间复杂度即递归层数 -> $O(logn)$

👺 代码实现:

const quickSort = (arr: number[]): number[] => {
  if (!arr.length) return [];

  let leftArr: number[] = [];
  let rightArr: number[] = [];
  let middle = arr.splice(Math.floor(arr.length / 2), 1)[0];
  
  arr.forEach(item => {
    item < middle ? leftArr.push(item) : rightArr.push(item);
  })

  return quickSort(leftArr).concat(middle, quickSort(rightArr));
}

归并排序

👺 基本思想: <将有序数组合并为更大的有序数组>

👺 过程图解:

👺 算法分析:

  1. 归并排序用到的是分治思想, 即将原问题分解为子问题, 求解子问题后将子问题合并; 故将大数组分解为小数组, 直至数组中只有一个元素, 此时数组已全部有序, 将有序数组进行合并并排序, 形成新的有序数组, 全部合并后排序完成;
  2. 通过 Math.floor(arr.length / 2) 来获取中间项索引, 通过索引及 slice 将数组一分为二;
  3. 合并时, 比对左右数组的第一项, 将较小值推入新数组, 通过 shift 更新左右数组;
  4. 理解程序栈的调用顺序, 可很好的理解归并排序;

👺 复杂度:

【分】二分操作 -> 复杂度为$logn$; 【合】while循环 -> 复杂度为$n$;

时间复杂度: $O(nlogn)$

空间复杂度: 空间复杂度为递归层数和临时数组所占空间 -> $logn+n$, 故空间复杂度为 $O(n)$

👺 代码实现:

const mergeSort = (arr: number[]): number[] => { // 拆分
  if (arr.length <= 1) return arr;

  let middle = Math.floor(arr.length / 2);
  let leftArr = arr.slice(0, middle);
  let rightArr = arr.slice(middle);

  return merge(mergeSort(leftArr), mergeSort(rightArr));
}

const merge = (leftArr: number[], rightArr: number[]) => { // 合并
  let result: number[] = [];

  while (leftArr.length || rightArr.length) {
    if (leftArr.length && rightArr.length) {
      leftArr[0] < rightArr[0]
        ? result.push(leftArr.shift() as number)
        : result.push(rightArr.shift() as number);
    } else {
      if (leftArr.length) result.push(leftArr.shift() as number);
      if (rightArr.length) result.push(rightArr.shift() as number);
    }
  }

  return result;
}

阅读 480

小五的算法之路
此专栏为笔者在毕业后首次重新 “研读” 数据结构与算法后所著,意在记录学习中的磕磕绊绊与心得体会,并...

前端开发攻城狮, 擅长搬砖, 精通ctrl + c & ctrl + v, 日常bug缔造者.

277 声望
17 粉丝
0 条评论
你知道吗?

前端开发攻城狮, 擅长搬砖, 精通ctrl + c & ctrl + v, 日常bug缔造者.

277 声望
17 粉丝
宣传栏