常见排序算法有很多,比如冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序等。本系列文章分几部分分别对常见排序算法进行归纳总结。
本文为算法系列一之:冒泡排序、插入排序、选择排序
相关概念
- 原地排序(Sorted in place)
原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。我们今天讲的三种排序算法,都是原地排序算法。
- 稳定性
如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
冒泡排序
一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作
代码实现
/**
* 冒泡排序
* 关键词:比较、交换,每一轮冒泡出一个最大的
* 优化:当没有交换时说明已经达到完全有序
* @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 次冒泡操作。
插入排序
一般我们往数组中插入一个元素,都采用下图的方式,即先遍历数组,然后找到对应位置插入元素。
插入排序也是基于这种思想进行插入的。
首先将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
代码实现
/**
* 插入排序
* 关键词:已排序和未排序、比较、移动
* 步骤: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)
选择排序
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
代码实现
/**
* 选择排序
* 关键词:已排序和未排序、比较、移动、最小元素
* 步骤: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 顺序就变了,所以就不稳定了。
性能对比
分析评价一个排序算法,一般从执行效率、内存消耗和稳定性三个方面来看。
相对于冒泡排序和插入排序,选择排序本身是不稳定的排序算法,所以相对逊色。
那么针对冒泡排序和插入排序,虽然冒泡排序和插入排序在时间复杂度上是一样的,但从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,因为冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。所以在实际开发中应用较多的还是插入排序。
这三种排序算法都是基于数组实现的,但是这三种排序方式时间复杂度都是O(n^2),相对较高,实际使用时还是倾向于使用接下来分析的时间复杂度为O(nlogn) 的排序算法。敬请期待后续文章~