基础很好?真的了解冒泡、选择、插入排序吗

710 阅读7分钟

系列文章链接

1.冒泡排序

1.1 原理

冒泡排序是最简单的排序之一,用一句话描述它

无序区通过交换找出最大的元素,放到有序区的前面 以无序数组[3,2,4,1]举例,如果将其中的最大值排到末尾,得到[3,2,1,4][3,2,1]就是无序区[4]就是有序区

这个交换怎么理解?我们来看一张图
bubbleSort.gif 还是以[3,2,4,1]这个数组为例,

  1. 比较323较大,交换两者位置,得到[2,3,4,1]
  2. 继续比较344较大,不做处理,得到[2,3,4,1]
  3. 再比较414较大,交换两者位置,得到[2,3,1,4]。 完成这轮遍历后不难发现,已经将最大值4移动到最右端。

接下来可以对无序区中的[2,3,1]重复上面过程,慢慢扩充有序区,直到无序区为空。 完成这个过程大约总共需要4轮遍历,分别是对[2,3,4,1][2,3,1][1,2][1]的遍历。

1.2 复杂度和稳定性

复杂度可以分为时间复杂度额外空间复杂度, 如果想了解更多可以参考算法的时间与空间复杂度(一看就懂)

接下来我们推导一下冒泡排序的时间复杂度:

1.2.1 时间复杂度

在刚才的例子,我们在第一轮遍历中,对[2,3,4,1]进行了3次比较。在其余三轮遍历中,对[2,3,1][1,2][1]分别进行了2次、1次和0次比较。如果数组的长度为N,总的执行次数之和为

O=(N1)+(N2)+(N3)+...+1O = (N - 1) + (N - 2) + (N - 3) + ... + 1

不难看出是等差数列求和,等差数列的求和公式为

S(n)=(a0+an)n2S(n) = \frac{(a_0 + a_n) n}{2}

套入公式可得到

O=(1+N1)N2=N22O = \frac{(1 + N - 1)N}{2} = \frac{N^2}{2}

因此可以得到时间复杂度为O(N2)O(N^2)

1.2.2 额外空间复杂度

冒泡排序所需要的额外空间不会随着某个变量n的大小而变化,因此它的空间复杂度是O(1)。

1.2.3 稳定性

稳定性指的是在数组排序过程中,值相同的两个元素前后位置有没有发生变化,更多可以参考排序算法的稳定性

在冒泡排序中过程中,当比较相同的两个值时可以不变更两者位置,所以是稳定的,可以结合上述例子理解这个结论。

1.3 代码实现

fuction bubleSort(arr) {
  var len = arr.length;
  for (var i = len - 1; i >= 0; i -= 1) {
    for (var j = 0; j < i; j += 1) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1)
      }
    }
  }
  return arr;
}

function swap(arr, i, j) {
    if (i === j) return;
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

这里交换函数swap可以看作是一个“彩蛋”,它的作用是:通过异或(^) 交换数组arr中位置ij的值。对这部分不感兴趣的同学,直接阅读本章小结。

与异或相对的是同或

因为不常用,因此百分之九十的人都会记不住这两者的区别。笔者提供一个比较容易记忆的口诀,异或是不进位的二进制相加

异或有以下数学规律:

0异或等于自身: 0 ^ n => n
相同的数异或为0: n ^ n => 0
符合结合律: (a ^ b) ^ c = a ^ (b ^ c) (1)推导 arr[j]=arr[i]arr[j] = arr[i]

arr[i] = arr[i] ^ arr[j]
arr[j] = arr[i] ^ arr[j]

将第一个式子代入到第二个式子,可得到

arr[j] = (arr[i] ^ arr[j]) ^ arr[j]
       = arr[i] ^ (arr[j] ^ arr[j])
       = arr[i] ^ 0
       = arr[i]

(2)推导 arr[i]=arr[j]arr[i] = arr[j]

arr[i] = arr[i] ^ arr[j]
arr[j] = arr[i]
arr[i] = arr[i] ^ arr[j]

将前两个式子代入到第三个式子,得到

arr[i] = (arr[i] ^ arr[j]) ^ arr[i]
       = (arr[i] ^ arr[i]) ^ arr[j]
       = 0 ^ arr[j]
       = a[j]

综上可以实现数组中值的交换
注意:不要对数组中同一个位置的值,执行上面的逻辑。

1.4 本章小结

  1. 冒泡排序可以理解为,在无序区通过交换找出最大的元素,放到有序区的前面。
  2. 冒泡排序的时间复杂度为O(N2)O(N ^ 2)
  3. 冒泡排序的额外空间复杂度为O(1)O(1)
  4. 冒泡排序不具有稳定性
  5. 异或可以理解为不进位的二进制相加,可用于实现数组交换等“骚操作”

2.选择排序

2.1 原理

选择排序,是一种简单直观的排序算法,可以理解为

无序区里找一个最小的数放在有序区的后面 以无序数组[3,2,4,1]举例,如果将其中的最小值排到前端,得到[1,3,2,4][1]就是有序区[3,2,4]就是无序区selectionSort.gif 假设对[3,2,4,1]开始第一轮遍历

  1. 创建索引i指向数组开始的0位置,用于指向无序区的第一个数
  2. 遍历[3,2,4,1],得到最小值为1,将它和0位置的数值3交换
  3. 此时左边的有序区新增数值1,索引i位置右移一位,无序区[3,2,4] 重复上面过程直到无序区为空。完成这个过程大约总共需要4轮遍历,分别是对[2,3,4,1][3,2,4][3,4][4]的遍历。

2.2 复杂度和稳定性

2.2.1 时间复杂度

选择排序的时间复杂度推导方式和冒泡排序相似,在例子中的4轮遍历分别进行了3210次对比。如果数组的长度为N,时间复杂度为

O=(N1)+(N2)+(N3)+...+1O = (N - 1) + (N - 2) + (N - 3) + ... + 1

通过等差数列求和公式

S(n)=(a0+an)n2S(n) = \frac{(a_0 + a_n) n}{2}

可以得到时间复杂度为O(N2)O(N ^ 2)

2.2.2 额外空间复杂度

选择排序所需要的额外空间不会随着某个变量n的大小而变化,因此它的空间复杂度是O(1)。

2.2.3 稳定性

在上面例子中讲到,当在无序区获得到最小值后,会和无序区最左边的值交换,这个过程会让原本在后面的值放到前面。

因此,数组中相同的值就有可能交换位置,选择排序不具有稳定性

2.3 代码实现

function selectSort(arr) {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    minIndex = i;
    for (let j = minIndex; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      swap(arr, minIndex, i)
    }
  }
  return arr;
}

function swap(arr, i, j) {
    if (i === j) return;
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

2.4 本章小结

  1. 选择排序是从无序区里找一个最小的数放在有序区的后面
  2. 选择排序的时间复杂度为 O(N ^ 2)
  3. 选择排序的额外空间复杂度为O(1)
  4. 选择排序不稳定

3. 插入排序

3.1 原理

有的时候我们容易把插入排序和选择排序记混。插入排序其实和我们玩扑克牌时整理牌的方式一样,笔者建议用这个方法来记忆这个排序算法。用一句话描述就是

无序区的每一个元素,插入到有序区的合适位置

insertionSort.gif

仍然以无序数组[3,2,4,1]举例

在一开始[3]就是有序区,[2,4,1]无序区
随后无序区最左边的2在有序数组中排序,23比较后,得到有序区为[2,3]无序区为[4,1]
以此类推,直到无序区为空。完成这个过程大约总共需要3轮遍历,分别是

  1. 无序数组[2,4,1]中的2在有序数组[3]中寻找合适位置,得到有序区[2, 3]
  2. 无序数组[4,1]中的4在有序数组[2,3]中寻找合适位置,得到有序区[2,3,4]
  3. 无序数组[1]中的1在有序数组[2,3,4]中寻找合适位置,得到有序区[1,2,3,4]

3.2 复杂度和稳定性

3.2.1 时间复杂度

如果数组为倒序排列,并且长度为N,那么无序数组中的每一个数,在有序数组中寻找合适位置的时候,需要进行1 ~N-1次对比。
最大时间复杂度为

O=1+2+...+(N2)+(N1)O = 1 + 2 + ... + (N - 2) + (N - 1)

通过等差数列求和公式可以得到结果为O(N2)O(N ^ 2)

3.2.2 额外空间复杂度

插入排序所需要的额外空间不会随着某个变量n的大小而变化,因此它的空间复杂度是O(1)。

3.2.3 稳定性

在上图可以知道,将无序区的元素插入到有序区时,是从右往左遍历。假如有两个相同的值,先排序的值在有序区中,而后排序的值会排在它的右边,位置没有变化,因此插入排序也是稳定的。

3.3 代码实现

function insertSort(arr) {
  for (var i = 0; i < arr.length - 1; i++) {
    var minIndex = i;
    for (var j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) {
        minIndex = j;
      }
    }
    swap(arr, i, minIndex);
  }
  return arr;
}

function swap(arr, i, j) {
  if (i === j) {
    return;
  }
  arr[i] = arr[i] ^ arr[j];
  arr[j] = arr[i] ^ arr[j];
  arr[i] = arr[i] ^ arr[j];
}

3.4 本章小结

  1. 插入排序和扑克牌类似,是把无序区的每一个元素,插入到有序区的合适位置
  2. 插入排序的时间复杂度为O(N2)O(N ^ 2)
  3. 插入排序的额外空间复杂度为O(1)O(1)
  4. 插入排序具有稳定性