系列文章链接
1.冒泡排序
1.1 原理
冒泡排序是最简单的排序之一,用一句话描述它
在
无序区通过交换找出最大的元素,放到有序区的前面 以无序数组[3,2,4,1]举例,如果将其中的最大值排到末尾,得到[3,2,1,4],[3,2,1]就是无序区,[4]就是有序区。
这个交换怎么理解?我们来看一张图
还是以
[3,2,4,1]这个数组为例,
- 比较
3和2,3较大,交换两者位置,得到[2,3,4,1] - 继续比较
3和4,4较大,不做处理,得到[2,3,4,1] - 再比较
4和1,4较大,交换两者位置,得到[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,总的执行次数之和为
不难看出是等差数列求和,等差数列的求和公式为
套入公式可得到
因此可以得到时间复杂度为。
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中位置i和j的值。对这部分不感兴趣的同学,直接阅读本章小结。
与异或相对的是同或。
因为不常用,因此百分之九十的人都会记不住这两者的区别。笔者提供一个比较容易记忆的口诀,异或是不进位的二进制相加。
异或有以下数学规律:
与
0异或等于自身: 0 ^ n => n
相同的数异或为0: n ^ n => 0
符合结合律: (a ^ b) ^ c = a ^ (b ^ c) (1)推导
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[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 本章小结
- 冒泡排序可以理解为,在
无序区通过交换找出最大的元素,放到有序区的前面。 - 冒泡排序的时间复杂度为
- 冒泡排序的额外空间复杂度为
- 冒泡排序不具有稳定性
- 异或可以理解为不进位的二进制相加,可用于实现数组交换等“骚操作”
2.选择排序
2.1 原理
选择排序,是一种简单直观的排序算法,可以理解为
从
无序区里找一个最小的数放在有序区的后面 以无序数组[3,2,4,1]举例,如果将其中的最小值排到前端,得到[1,3,2,4],[1]就是有序区,[3,2,4]就是无序区。假设对
[3,2,4,1]开始第一轮遍历
- 创建索引
i指向数组开始的0位置,用于指向无序区的第一个数 - 遍历
[3,2,4,1],得到最小值为1,将它和0位置的数值3交换 - 此时左边的
有序区新增数值1,索引i位置右移一位,无序区为[3,2,4]重复上面过程直到无序区为空。完成这个过程大约总共需要4轮遍历,分别是对[2,3,4,1]、[3,2,4]、[3,4]、[4]的遍历。
2.2 复杂度和稳定性
2.2.1 时间复杂度
选择排序的时间复杂度推导方式和冒泡排序相似,在例子中的4轮遍历分别进行了3、2、1和0次对比。如果数组的长度为N,时间复杂度为
通过等差数列求和公式
可以得到时间复杂度为
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 本章小结
- 选择排序是从
无序区里找一个最小的数放在有序区的后面 - 选择排序的时间复杂度为 O(N ^ 2)
- 选择排序的额外空间复杂度为O(1)
- 选择排序不稳定
3. 插入排序
3.1 原理
有的时候我们容易把插入排序和选择排序记混。插入排序其实和我们玩扑克牌时整理牌的方式一样,笔者建议用这个方法来记忆这个排序算法。用一句话描述就是
把
无序区的每一个元素,插入到有序区的合适位置
仍然以无序数组[3,2,4,1]举例
在一开始[3]就是有序区,[2,4,1]是无序区。
随后无序区最左边的2在有序数组中排序,2与3比较后,得到有序区为[2,3]无序区为[4,1]。
以此类推,直到无序区为空。完成这个过程大约总共需要3轮遍历,分别是
- 无序数组
[2,4,1]中的2在有序数组[3]中寻找合适位置,得到有序区为[2, 3] - 无序数组
[4,1]中的4在有序数组[2,3]中寻找合适位置,得到有序区为[2,3,4] - 无序数组
[1]中的1在有序数组[2,3,4]中寻找合适位置,得到有序区为[1,2,3,4]
3.2 复杂度和稳定性
3.2.1 时间复杂度
如果数组为倒序排列,并且长度为N,那么无序数组中的每一个数,在有序数组中寻找合适位置的时候,需要进行1 ~N-1次对比。
最大时间复杂度为
通过等差数列求和公式可以得到结果为
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 本章小结
- 插入排序和扑克牌类似,是把
无序区的每一个元素,插入到有序区的合适位置 - 插入排序的时间复杂度为
- 插入排序的额外空间复杂度为
- 插入排序具有稳定性