排序算法之冒泡、插入、选择

374 阅读5分钟

常见排序算法有很多,比如冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序等。本系列文章分几部分分别对常见排序算法进行归纳总结。

img

本文为算法系列一之:冒泡排序、插入排序、选择排序


相关概念

  • 原地排序(Sorted in place)

原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。我们今天讲的三种排序算法,都是原地排序算法。

  • 稳定性

如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

冒泡排序

一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作

img img

代码实现

  /**
   * 冒泡排序
   * 关键词:比较、交换,每一轮冒泡出一个最大的
   * 优化:当没有交换时说明已经达到完全有序
   * @param arr 
   */
  bubbleSort = (arr) => {
    if (arr.length <= 1) return arr;
    
    for (let i = 0; i < arr.length; i++) {
      let hasChange = false // 标志位,当没有交换时说明已经达到完全有序
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          const temp = arr[j]
          arr[j] = arr[j + 1]
          arr[j + 1] = temp
          hasChange = true
        }
      }
      if (!hasChange) break
    }
    
    return arr
  }js

说明

  • 冒泡排序是原地排序算法

冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。

  • 冒泡排序是稳定的排序算法

当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。

  • 冒泡排序的时间复杂度 O(n^2)

最好情况的时间复杂度是O(n),只需一次冒泡操作,就知道已经是有序的了;

最坏情况的时间复杂度是O(n^2),要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作。

插入排序

一般我们往数组中插入一个元素,都采用下图的方式,即先遍历数组,然后找到对应位置插入元素。

img

插入排序也是基于这种思想进行插入的。

首先将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

img

代码实现

  /**
   * 插入排序
   * 关键词:已排序和未排序、比较、移动
   * 步骤:1. 区分已排序[0]和未排序[1,length-1] 2. 遍历已排序部分,依次j--将较小值往后挪一位,记录当前位置 3. 将未排序的当前值插入至记录的位置。
   * 遍历的是已排序部分
   * @param arr 
   */
  const insertionSort = (arr) => {
    if (arr.length <= 1) return arr;
    
    for (let i = 1; i < arr.length; i++) {
      let pointer = arr[i] // i=1是未排序,i=0已排序
      let j; // 单独定义j,因为循环结束后要用到j下标
      for (j = i - 1; j >= 0; j--) {
        // 若 arr[j] > arr[i],依次向后移位arr[j],直到找到合适的位置j跳出循环,并在j位置插入arr[i]。
        if (arr[j] > pointer) {
          arr[j + 1] = arr[j] // 移动数据
        } else {
          break
        }
      }
      arr[j + 1] = pointer // 插入数据。j+1是因为j=0时j--=-1
    }
    
    return arr
  }

说明

  • 插入排序是原地排序算法
  • 插入排序是稳定的排序算法
  • 插入排序的时间复杂度是O(n^2)
  • 扩展:相当于在数组中插入一个元素的时间复杂度就是O(n)

选择排序

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

img

代码实现

/**
   * 选择排序
   * 关键词:已排序和未排序、比较、移动、最小元素
   * 步骤:1. 区分已排序[0]和未排序[1,length-1] 2. 遍历未排序部分找到最小值 3. 交换最小值和已排序的最后一位
   * 遍历的是未排序部分
   * @param arr 
   */
  const selectionSort = (arr) => {
    if (arr.length <= 1) return arr;

    for (let i = 0; i < arr.length - 1; i++) {
      let min = i; // 假设最小值是 arr[0]
      // 未排序从 i+1 开始寻找,确定 "最小值"
      for (let j = i + 1; j < arr.length; j++) {
        if (arr[j] < arr[min]) {
          min = j // 注意此处是找到整个数组的最小值。不要交换。
        }
      }
      // 找到最小值后,进行交换
      let temp = arr[i];
      arr[i] = arr[min];
      arr[min] = temp;
    }
    return arr
  }

说明

  • 选择排序是原地排序算法
  • 选择排序时间复杂度为 O(n^2)
  • 但选择排序是一种不稳定的排序算法。

比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。

性能对比

分析评价一个排序算法,一般从执行效率、内存消耗和稳定性三个方面来看。

img

相对于冒泡排序和插入排序,选择排序本身是不稳定的排序算法,所以相对逊色。

那么针对冒泡排序和插入排序,虽然冒泡排序和插入排序在时间复杂度上是一样的,但从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,因为冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。所以在实际开发中应用较多的还是插入排序。

这三种排序算法都是基于数组实现的,但是这三种排序方式时间复杂度都是O(n^2),相对较高,实际使用时还是倾向于使用接下来分析的时间复杂度为O(nlogn) 的排序算法。敬请期待后续文章~

参考

极客文章 排序(上):为什么插入排序比冒泡排序更受欢迎?

代码参考 github.com/wangzheng08…