除了sort()排序,你还会其他的排序方法吗

108 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

大家在日常的开发过程中,如果遇到要对一个数组进行某个字段的排序,我想肯定会写出下面这段代码

array.sort((a,b) => a.key - b.key)

很快啊,就实现了一个数组按照某个字段来排序。但是当数组很大时,如一个数组长度成千上万,这样遍历会不会造成性能浪费呢?

答案是肯定的。那我们有其他的方法进行排序吗?一定有,下面介绍一下几个常规的排序方法。

一、 冒泡排序

平均时间复杂度: O(n*n) 稳定性: 稳定 相信冒泡排序对于大家来说肯定是很熟悉的了。通常的冒泡排序就是用两个for循环来遍历数组。

// 冒泡排序
function bubbleSort(arr) {
  const len = arr.length
  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        let temp = arr[j]
        arr[j] = arr[j + 1]
        arr[j + 1] = temp
      }
    }
  }
  return arr
}

我们可以从代码中看到,每遍历一次就拿当前的元素与下一个元素进行对比,根据要倒序还是顺序来决定是否要进行交换。比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

bubbleSort.gif

二、 选择排序

平均时间复杂度: O(n*n) 稳定性: 不稳定

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。
function selectSort(arr) {
  const len = arr.length
  let minIndex = 0
  for (let i = 0; i < len - 1; i++) {
    // 当前最小值得索引为i
    minIndex = i
    for (let j = i + 1; j < len; j++) {
      // 遍历i后的元素
      if (arr[j] < arr[minIndex]) {
        // 找出最小的下标
        minIndex = j
      }
    }

    // 找到最小的下标后,与i的下标元素进行替换
    let temp = arr[i]
    arr[i] = arr[minIndex]
    arr[minIndex] = temp
  }

  return arr
}

selectionSort.gif

三、 插入排序

平均时间复杂度: O(n*n) 稳定性: 稳定

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
function insertSort(arr) {
  const len = arr.length
  let current = null
  for (let i = 0; i < len; i++) {
    // 当前需要插入的元素
    current = arr[i]

    for (let j = i - 1; j >= 0; j--) {
      // 这里为啥不能直接用arr[i]来比较?
      // 因为如果前一个比arr[i]大,那就会把arr[i]替换成arr[i-1]
      if (arr[j] > current) {
        // 交换
        arr[j + 1] = arr[j]
        arr[j] = current
      } else {
        arr[j + 1] = current
        break
      }
    }
  }
  return arr
}

insertionSort.gif

看到这里,感觉各位大哥们已经有点迷糊了,这里我说一下选择排序和插入排序的区别:

选择排序和插入排序区别:

  1. 选择排序是每一次从后面没有排序的数组里找最小的往前面放。
  2. 插入排序是每一次遍历拿当前元素与已经排序的序列进行比较,并放到正确顺序位置

四、 希尔排序

平均时间复杂度: O(nlogn) 稳定性: 不稳定

受不了了,开始整名词了。希尔是啥?啥英雄?哈哈哈,其实希尔排序是插入排序的一个优化版本,其主要思想就是

  1. 首先定义一个增量
  2. 以增量为间隔进行分组
  3. 组内进行排序
  4. 增量变为原来的二分之一,重复执行
  5. 直到增量小于一
function shellSort(arr) {
  const len = arr.length
  let gap = Math.floor(len / 2)
  let current = null
  while (gap > 0) {
    // 进行分组插入排序
    for (let i = gap; i < len; i++) {
      current = arr[i]

      // for (var j = i - gap; j >= 0 && arr[j] > current; j = j - gap) {
      //   arr[j + gap] = arr[j]
      // }
      // arr[j + gap] = current

      for (let j = i - gap; j >= 0; j = j - gap) {
        if (arr[j] > current) {
          arr[j + gap] = arr[j]
          arr[j] = current
        } else {
          arr[j + gap] = current
          break
        }
      }
    }

    // 开始下一个循环
    gap = Math.floor(gap / 2)
  }

  return arr
}

四、 快速排序

平均时间复杂度: O(nlogn) 稳定性: 稳定

听名字就知道很厉害,确实。快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。

  1. 从数列中挑出一个元素,称为 "基准"(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
// 交换
function swap(arr, left, right) {
  let temp = arr[left]
  arr[left] = arr[right]
  arr[right] = temp
}

function quickSort(arr, left, right) {
  if(left >= right ){
    return
  }
  // 取第一个头节点
  let point = arr[left]
  let i = left
  let j = right
  // 当左索引小于右索引,退出
  while (i !=  j) {
    // 从右边找第一个小于point的元素
    while (i <  j && arr[j] > point) {
      j--
    }
    // 从左边开始,找第一个大于point的元素
    while (i < j && arr[i] <= point) {
      i++
    }

    // 交换左右节点
    if(i < j){
      swap(arr, i, j)
    }
  }
  // i j 重合. 交换重叠索引和point
  swap(arr, left, i)

  // 数组左边递归
  quickSort(arr,left, j)
  // 数组右边递归
  quickSort(arr, j + 1, arr.length - 1)
}