排序算法

122 阅读4分钟

排序算法和思路

复杂度

时间复杂度

一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。

没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。

常见的时间复杂度有:

  • O(1): Constant Complexity: Constant 常数复杂度
  • O(log n): Logarithmic Complexity: 对数复杂度
  • O(n): Linear Complexity: 线性时间复杂度
  • O(n^2): N square Complexity 平⽅方
  • O(n^3): N square Complexity ⽴立⽅方
  • O(2^n): Exponential Growth 指数
  • O(n!): Factorial 阶乘

空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。

一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。

1.冒泡排序

思想

循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。

这样一次循环之后最后一个数就是本数组最大的数。

下一次循环继续上面的操作,不循环已经排序好的数。

优化:当一次循环没有发生冒泡,说明已经排序完成,停止循环。

代码

    function bubbleSort(array) {
      for (let j = 0; j < array.length; j++) {
        let complete = true;
        for (let i = 0; i < array.length - 1 - j; i++) {
          // 比较相邻数
          if (array[i] > array[i + 1]) {
            [array[i], array[i + 1]] = [array[i + 1], array[i]];
            complete = false;
          }
        }
        // 没有冒泡结束循环
        if (complete) {
          break;
        }
      }
      return array;
    }

复杂度

时间复杂度:O(n2)

空间复杂度:O(1)

稳定性

稳定

2.选择排序

思想

  • 从数组下标为0位置的元素开始直到数组的末尾
  • 找出这些元素中最小的值在哪个位置上
  • 将最小的值交换到0位置上
  • 重复前面的操作,从下标为1位置的元素开始
  • 依此类推

每次循环选取一个最小的数字放到前面的有序序列中。

代码

    function selectionSort(array) {
      for (let i = 0; i < array.length - 1; i++) {
        let minIndex = i;
        for (let j = i + 1; j < array.length; j++) {
          if (array[j] < array[minIndex]) {
            minIndex = j;
          }
        }
        [array[minIndex], array[i]] = [array[i], array[minIndex]];
      }
    }

复杂度

时间复杂度:O(n2)

空间复杂度:O(1)

稳定性

不稳定

3.插入排序

思想

假设数组成员的左边(前面)是已经排好队的序列,这时只需逐个将数组的成员插入到序列的合适位置即可。

  • 从数组的第二个元素开始,此时因为左边只有一个元素,可以认为左边是一个已经排好队的序列
  • 将第二个元素与第一个比较,如果第一个元素大,则把第一个元素向右移一位,第二个元素放到第一个元素的位置;否则保持位置不变
  • 取出第三个元素,与左边已经排好的序列(第一、二个元素)从右到左依次比较,如果序列的元素大于第三个元素,则将该元素向右移一位,直到序列的元素不大于第三个元素或者已经比较到了最左边,将第三个元素插入到最后一次被移动元素的位置
  • 取出第四个元素,重复上面一步
  • 以此类推

将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。

插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。

代码

    function insertSort(array) {
      for (let i = 1; i < array.length; i++) {
        let target = i;
        for (let j = i - 1; j >= 0; j--) {
          if (array[target] < array[j]) {
            [array[target], array[j]] = [array[j], array[target]]
            target = j;
          } else {
            break;
          }
        }
      }
      return array;
    }

复杂度

时间复杂度:O(n2)

空间复杂度:O(1)

稳定性

稳定

4.快速排序

思想

快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。

实现步骤:

  • 选择一个基准元素target(一般选择第一个数)
  • 将比target小的元素移动到数组左边,比target大的元素移动到数组右边
  • 分别对target左侧和右侧的元素进行快速排序

快速排序也利用了分治的思想(将问题分解成一些小问题递归求解)

下面是对序列6、1、2、7、9、3、4、5、10、8排序的过程:

代码

写法1

单独开辟两个存储空间leftright来存储每次递归比target小和大的序列

每次递归直接返回left、target、right拼接后的数组

浪费大量存储空间,写法简单

    function quickSort(array) {
      if (array.length < 2) {
        return array;
      }
      const target = array[0];
      const left = [];
      const right = [];
      for (let i = 1; i < array.length; i++) {
        if (array[i] < target) {
          left.push(array[i]);
        } else {
          right.push(array[i]);
        }
      }
      return quickSort(left).concat([target], quickSort(right));
    }

写法2

记录一个索引l从数组最左侧开始,记录一个索引r从数组右侧开始

l<r的条件下,找到右侧小于target的值array[r],并将其赋值到array[l]

l<r的条件下,找到左侧大于target的值array[l],并将其赋值到array[r]

这样让l=r时,左侧的值全部小于target,右侧的值全部小于target,将target放到该位置

不需要额外存储空间

    function quickSort(array, start, end) {
      if (end - start < 1) {
        return;
      }
      const target = array[start];
      let l = start;
      let r = end;
      while (l < r) {
        while (l < r && array[r] >= target) {
          r--;
        }
        array[l] = array[r];
        while (l < r && array[l] < target) {
          l++;
        }
        array[r] = array[l];
      }
      array[l] = target;
      quickSort(array, start, l - 1);
      quickSort(array, l + 1, end);
      return array;
    }

复杂度

时间复杂度:平均O(nlogn),最坏O(n2),实际上大多数情况下小于O(nlogn)

空间复杂度:O(logn)(递归调用消耗)

稳定性

不稳定

5.效率分析比较

详细的效率分析非常复杂,在此仅作简单分析(比较次数和交换次数),或者给出结论。

比较次数:进行数据比较的次数。 交换次数:数据交换的次数。

  • 如冒泡排序中,假设数组长度为7
  • 则第一次循环6次比较, 第二次5次比较, 第三次4次比较....直到最后一趟进行了一次比较
  • 7个数据项比较次数: 6 + 5 + 4 + 3 + 2 + 1
  • 则N个数据项的比较次数:N * (N - 1) / 2

冒泡排序 VS 选择排序

冒泡排序和选择排序的比较次数都是N * (N - 1) / 2。冒泡排序假设两次比较需要交换一次,则交换次数为N * (N - 1) / 4;而选择排序的仅有N-1次,所以可知选择排序的效率大于冒泡排序。

选择排序 VS 插入排序

在插入排序中,因为在比较时只要找到了一个不大于的值,就不再进行后面的比较,所以比较次数最多为N * (N - 1) / 2。交换次数最多也为N * (N - 1) / 2。 对于基本有序的情况,插入排序的效率大于选择排序。

快速排序

多数情况下,快速排序的效率高于前面4种。

(v8引擎中array.sort在数组长度小于等于10的时候采用插入排序,大于10的时候采用快速排序和插入排序组合的方式)

参考链接:www.jianshu.com/p/f1f2dc978…
参考链接:www.jianshu.com/p/3c2184320…