排序算法的分类
我最开始接触和使用最多的排序算法就是冒泡排序了(可能很多人和我一样)但是把它好好的和其他各类排序算法进行比较。很多时候我们有更好的选择。
排序算法基本分为三类:
排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|
冒泡 插入 选择 | O(n2) | 是 |
快排 归并 | O(nlogn) | 是 |
桶 计数 基数 | O(n) | 不是 |
排序算法的优异
几个基本概念:
原地排序:空间复杂度为O(1)的排序。
分析是否是原地排序其实就是分析空间复杂度。
稳定排序:带排序的序列中含有值相同的元素,经过排序之后,相等元素的前后顺序没有改变过。
也就是说分析一个排序算法除了要分析时间复杂度、空间复杂度之外还要分析稳定性。
冒泡排序
冒泡排序
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
很好理解,使用JavaScript实现如下:
1 | const bubbleSort = arr => { |
需要注意的是,上面做了一个小优化,当上一次比较没有发生位置交换说明当前序列已经完全有序,不需要再进行下一轮冒泡。
简单思考一下:冒泡排序的空间复杂度是O(1)是原地排序;只是相邻位置的元素之间会比较交换,所以是稳定排序;时间复杂度的分析就比较复杂了
最好情况时间复杂度是O(n),待排序的序列直接就是有序的;最坏时间复杂度是O(n2),待排序的序列是倒序的。
平均时间复杂度的分析很复杂,可以从“有序度”和“逆序度”的概念分析,也可以使用概率论的知识分析,最终的平均时间复杂度为)O(n2)。这也是预料之中的,最好和最坏情况都是极端的例子,在所有排列的情况(n!)中概率很低。多数情况下时间复杂度都是O(n2)
插入排序
插入排序
具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
代码实现:
1 | const insertionSort = arr => { |
插入排序的空间复杂度是O(1) 是原地排序;是稳定的排序算法;最好时间复杂度是O(n),最坏时间复杂度是O(n2),平均时间复杂度同样是O(n2),循环 n 次,每次插入操作都像是在数组中插入一个数据,不过这里的数组是有序的,所以插入排序是有优化空间的,在一个有序的数组中插入数据的时间复杂度可以做到(logn),所有插入时间排序可以做到O(nlogn),不过这属于插入排序的变种,称为二分查找插入排序。
选择排序
选择排序
算法描述:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const selectionSort = arr => {
const len = arr.length;
for (let i = 0; i < len - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
const temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
};
选择排序的空间复杂度是O(1)是原地排序算法;但是选择排序并不是稳定的排序算法,交换位置让值相同的元素可能发生前后位置变化;时间复杂度和冒泡排序、插入排序一样都是O(n2)
选择排序的不稳定是我们很少用它的主要原因。当然数据规模不大的情况下,这几种排序其实差异不大,当数据规模大到一定程度,首选当然是插入排序,因为插入排序的优化空间很大。当然在实际开发中我们会有更多的选择,也就是快排、归并等。