数组排序上(冒泡、插入、选择)

88 阅读3分钟

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

数组排序的总结之冒泡、插入、选择、归并、快排

力扣

912. 排序数组

对于前端来说,数组排序并不是什么难题,一个sort走天下

  1. 从小到大
arr.sort((a,b) => { return a - b })

2.从大到小

arr.sort((a,b) => { return b - a })

但对于面试来说,这个sort可能不太够用了,哈哈,卷起来呀~

下面总结一下常用的几种排序算法(掘金修言小册学习记录),主要分五种

  • 基础排序算法:
    • 冒泡排序
    • 插入排序
    • 选择排序
  • 进阶排序算法
    • 归并排序
    • 快速排序

冒泡排序

冒泡排序的过程,就是从第一个元素开始,重复比较相邻的两个项,若第一项比第二项更大,则交换两者的位置;反之不动,如下

冒泡排序.gif

这个过程中,循环了两次:第一次循环找出需要比较+交换多少轮,第二次循环找出每一轮有多少回的比较+交换,好像有点绕,结合动图和代码理解一下

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

在冒泡排序的过程中,有一些”动作“是不太必要的。比如,随着外层循环的进行,数组尾部的元素会渐渐变得有序——当我们走完第1轮循环的时候,最大的元素被排到了数组末尾;走完第2轮循环的时候,第2大的元素被排到了数组倒数第2位;走完第3轮循环的时候,第3大的元素被排到了数组倒数第3位......以此类推,走完第 n 轮循环的时候,数组的后 n 个元素就已经是有序的。

为了避免这些冗余的比较动作,我们需要规避掉数组中的后 n 个元素,对应的代码可以这样写

function bubbleSort(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
}

冒泡排序的时间复杂度是O(n^2),但有种最好情况,数组本身就是有序的,在最好情况下复杂度是 O(n),改进一下代码

function betterBubbleSort(arr) {
    const len = arr.length  
    
    for(let i=0;i<len;i++) {
        // 区别在这里,我们加了一个标志位
        let flag = false
        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]]
                // 只要发生了一次交换,就修改标志位
                flag = true
            }
        }
        
        // 若一次交换也没发生,则说明数组有序,直接放过
        if(flag == false)  return arr;
    }
    return arr
}

选择排序

重点是选择,选择出最小值

找当前范围内的最小值,循环数组,把最小值放到当前范围的头部,然后缩小范围(减去头部),重复上面操作,时间复杂度O(n^2)

function selectSort(arr) {
  // 缓存数组长度
  const len = arr.length
  // 定义minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex
  // i 是当前排序区间的起点
  for(let i =0;i<len-1;i++) {
    // 初始化minIndex = i
    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]

  1. 首先5作为一个有序数组,3作为当前元素,和5比较,3<5,因此5给3挪出一个位置变成 [空位置,5],然后看位置前面还有没有元素,这里没有了,把3放到空位置上,有序数组变成 [3,5]
  2. 然后2作为当前元素,首先和5比较,2<5,5挪出一个位置变成[3,空位置,5],然后继续往前,还有3,2<3,所以3放到空位置上变成,[空位置,3,5],再往前没有元素了,所以把2放到空位置变成[2,3,5]
  3. 继续下去,把4作为当前元素.....

代码如下

function insertSort(nums) {
  // 缓存数组长度
  const len = nums.length
  // temp 每次遍历到的元素,当前需要插入的元素,
  // i 用来标识每次被插入的元素的索引,从1开始(第二个)
  for(let i = 1;i<len;i++) {
    // j 帮助temp寻找自己应该有的定位,应该去哪
    let j = i // 先初始为当前遍历的位置
    temp = nums[i] // temp 就是当前遍历的元素,需要操作的元素
    // 判断j前面的元素是否比temp大(是否比当前元素大)
    while(j>0 && nums[j-1] > temp) {
      // 前面的元素比temp(当前元素)大的话,继续往前(将j前面的一个元素后移一位,为temp让出位置
      nums[j] = nums[j-1]
      j--
    }
    // 循环让位结束后,得到的j 就是temp的正确索引
    nums[j] = temp
  }
  return nums
}