排序算法

Mr.ZhaoAbout 13 min

1. 冒泡排序

1.1 基本思路

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

每一轮操作,都会将这一轮中最大的元素放置到数组的末尾。假如数组的长度是 n,那么当我们重复完 n 轮的时候,整个数组就有序了

1.2 过程演示

// 对以下数组进行排序
[5, 3, 2, 4, 1]

每一次从头到尾的遍历都只能定位到一个元素的位置,因此元素有多少个,总的循环就要执行多少轮

1.3 编码实现

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.4 改进

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

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
}

1.5 进一步改进

最好的情况就是数组本来就是有序的

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),最坏时时间复杂度为O(n2n^2)

2. 选择排序

2.1 基本思路

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

2.2 过程演示

// 对以下数组进行排序
[5, 3, 2, 4, 1]

2.3 编码实现

function selectSort(arr)  {
  // 缓存数组长度
  const len = arr.length 
  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex  
  // i 是当前排序区间的起点
  for(let i = 0; i < len - 1; i++) { 
    // 这里len-1是因为遍历到最后一次的时候已经有序了
    // 初始化 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
}

时间复杂度为:O(n2n^2)

3. 插入排序

3.1 基本思路

插入排序的核心思想是“找到元素在它前面那个序列中的正确位置”

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

3.2 过程演示

// 对以下数组进行排序
[5, 3, 2, 4, 1]

几个关键点:

  • 当前元素前面的那个序列是有序的
  • “正确的位置”如何定义——所有在当前元素前面的数都不大于它,所有在当前元素后面的数都不小于它
  • 在有序序列里定位元素位置的时候,是从后往前定位的。只要发现一个比当前元素大的值,就需要为当前元素腾出一个新的坑位

3.3 编码实现

function insertSort(arr) {
  // 缓存数组长度
  const len = arr.length
  // temp 用来保存当前需要插入的元素
  let temp  
  // i用于标识每次被插入的元素的索引
  for(let i = 1;i < len; i++) {
    temp = arr[i]  
    // j用于帮助 temp 寻找自己应该有的定位
    let j = 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
}
  • 最好时间复杂度:它对应的数组本身就有序这种情况。此时内层循环只走一次,整体复杂度取决于外层循环,时间复杂度就是一层循环对应的 O(n)
  • 最坏时间复杂度:它对应的是数组完全逆序这种情况。此时内层循环每次都要移动有序序列里的所有元素,因此时间复杂度对应的就是两层循环的 O(n2n^2)
  • 平均时间复杂度:O(n2n^2)