排序算法

90 阅读4分钟

冒泡排序

  • 就是从第一个元素开始,重复比较相邻的两个项。若第一项比第二项更大,则交换两者的位置;反之不动
    • 时间复杂度:最好O(n)数组默认就是有序的;最坏O(n^2);平均O(n^2)
// flag 优化思想
// 因为可能 arr 默认就是有序,或者经过最少一次比较之后就有序了
// 所以只要在内循环一次,发现都没有进行换位。就证明 arr 已经有序了
function bubbleSort(arr) {
  let len = arr.length
  // 外层确保每个元素都和其他进行了一次比较
  for (let i = 0; i < len; i++) {
    // flag 要在循环内,因为 arr 可能要最少进行一次比较后才有序。在循环内每次重置一下
    let flag = false
    // 两两比较,完成一次后找出一个最大值
    // len - 1 - i 因为每循环一次,就有一个值是确定了大小的,所以下一次就可以忽略它
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换两者;es6
        ;[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
  // 存储的是 索引
  let minIndex = null
  for (let i = 0; i < len; i++) {
    // 初始化 minIndex
    minIndex = i
    // i,j 指针定义当前的区间
    // 所以 j 其实是动态的,结束位置不变
    for (let j = i; j < len; j++) {
      // 查找还是一个遍历比较的过程
      if (arr[minIndex] > arr[j]) {
        minIndex = j
      }
    }

    // 当内循环结束,发现 minIndex 更新了
    // 就替换两者位置,这样始终让头部保持最小
    if (minIndex !== i) {
      ;[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
    }
  }
  return arr
}

插入排序

  • 当前元素前面的序列是有序的。基于这个前提,从后往前去寻找当前元素在前面那个序列里的正确位置
    • 时间复杂度:最好O(n)数组默认就是有序的;最坏O(n^2);平均O(n^2)
// 取一个数,每次都和他之前内容的进行比较,如果比该值大就往后腾位置
function innerSort(arr) {
  const len = arr.length
  let temp = null
  // i 从 1 开始,因为第一个值本身是有序的 [5]
  for (let i = 1; i < len; i++) {
    // j用于帮助 temp 寻找自己应该有的定位
    let j = i
    // 需要 temp 先保存 arr[i],因为后面 arr[j] = arr[j - 1] 会更改 arr[i]
    // temp 是需要比较的值,需要找到比它大的值
    temp = arr[i]
    // 循环比较之前的有序数组,直到找到位置为止
    // temp->arr[i]->arr[j]
    while (j > 0 && arr[j - 1] > temp) {
      // 满足条件就 往后 腾位置
      // 前面的值赋值给现在的位置,就是前面的值后移了
      arr[j] = arr[j - 1]
      // 先移位再改动指针
      j--
    }

    // 找到了位置
    arr[j] = temp
  }

  return arr
}

归并排序

  • 将需要被排序的数组从中间分割为二,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。之后从最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。当合并至原有的规模时,就得到了一个完全排序的数组
    • 时间复杂度O(nlog(n))
// 整个函数是嵌套的主要有两个返回动作。正好对应着思想的 分割 和 合并
// 从外向内先把 arr 分成单个 [1](程序上半层)
// 然后再 mergeArr 两两合并(程序下半层)
function mergeSort(arr) {
  const len = arr.length
  // 靠这里来结束下面 l/r 两个 二分法 的操作
  // 然后返回内容开始 mergeArr 合并
  if (len <= 1) {
    return arr
  }

  const mid = Math.floor(len / 2)
  // 返回的有两种情况
  // 1.len=1 返回单个 [1]
  // 2.[1]、[2] 合并的结果 [1,2]
  const leftArr = mergeSort(arr.slice(0, mid))
  const rightArr = mergeSort(arr.slice(mid, len))

  // 最里层 len<=1 时不走这的。只有在外层合并时
  return mergeArr(leftArr, rightArr)
}
// 两个数组同时循环不合适用for
function mergeArr(arr1, arr2) {
  let i = 0,
    j = 0
  const len1 = arr1.length
  const len2 = arr2.length
  let res = []
  // 比较大小,填入数组
  while (i < len1 && j < len2) {
    if (arr1[i] < arr2[j]) {
      res.push(arr1[i])
      i++
    } else {
      res.push(arr2[j])
      j++
    }
  }

  // 上面结束表示其中一个先走完了,需要把剩下的加入
  if (i < len1) {
    // arr1 有剩余
    // 重要:res.concat() 返回是需要接收的
    res = res.concat(arr1.slice(i))
  } else {
    // arr2 有剩余
    res = res.concat(arr2.slice(j))
  }

  return res
}

mergeSort([4, 3, 2, 1])
// 执行流程
// 输入 4, 3, 2, 1
  // -> leftArr [4, 3]
    // 返回 [4], [3]
  // <- l=[4]、r=[3]
  // 合并 [3, 4]
  // -> rightArr [2, 1]
    // 返回 [2], [1]
  // 合并 [1, 2]
// <- l=[3, 4]、r=[1, 2]
// 合并 [1, 2, 3, 4]

快速排序

  • 会将原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。具体是先找个一个基准值(二分法),然后遍历它的左右并做处理确保它左边的比它小,右边的比它大。然后按照上面的思路进行分组重复直到子分组为单个时结束
    • 时间复杂度:最好O(nlog(n))数组默认就是有序的;最坏O(n^2);平均O(nlog(n))
// 快速排序入口
function quickSort(arr, left = 0, right = arr.length - 1) {
  // 定义递归边界,若数组只有一个元素,则没有排序必要
  if(arr.length > 1) {
    // lineIndex表示下一次划分左右子数组的索引位
    const lineIndex = partition(arr, left, right)
    // 如果左边子数组的长度不小于1,则递归快排这个子数组
    if(left < lineIndex - 1) {
      // 左子数组以 lineIndex-1 为右边界
      quickSort(arr, left, lineIndex - 1)
    }
    // 如果右边子数组的长度不小于1,则递归快排这个子数组
    if(lineIndex < right) {
      // 右子数组以 lineIndex 为左边界
      quickSort(arr, lineIndex, right)
    }
  }
  return arr
}
// 以基准值为轴心,划分左右子数组的过程
// 最好 基准值 左边得比它小,右边的比它大
function partition(arr, left, right) {
  // 基准值默认取中间位置的元素
  let pivotValue = arr[Math.floor(left + (right-left)/2)]
  // 初始化左右指针
  let i = left
  let j = right
  // 当左右指针不越界时,循环执行以下逻辑
  while(i<=j) {
    // 目的是找出 >= 基准值的数
    // 左指针所指元素若小于基准值,则右移左指针
    while(arr[i] < pivotValue) {
      i++
    }
    // 目的是找出 <= 基准值的数
    // 右指针所指元素大于基准值,则左移右指针
    while(arr[j] > pivotValue) {
      j--
    }
    // 进到这一步表示找个了那两个 相近基准值 的值
    // 在内部交换而不是外部,因为左右两侧可能不止一个 相近基准值 的值
    // 若i<=j,则意味着基准值左边存在较大元素或右边存在较小元素,交换两个元素确保左右两侧有序
    if(i<=j) {
      swap(arr, i, j)
      i++
      j--
    }

  }
  // 返回左指针索引作为下一次划分左右子数组的依据
  return i
}

// 快速排序中使用 swap 的地方比较多,我们提取成一个独立的函数
function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]]
}