概述
排序算法 | 思路 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
冒泡排序 | 两两比较,将较大(或较小)的元素换到后面,每轮比较后数组后面都是排序好的 | O(n^2) | O(1) | 稳定 |
插入排序 | 将待插入的元素依次与前面已排序的元素对比,插入到正确的位置 | O(n^2) | O(1) | 稳定 |
归并排序 | 分治,排序,合并 | O(nlogn) | O(n) | 稳定 |
快速排序 | 基准元素,小于基准元素放左边,大于基准元素放右边,递归 | 最好O(nlogn)、最坏O(n^2) | O(logn) | 不稳定 |
拓扑排序 | 有向图、无环、理清依赖关系、广度优先搜索或深度优先搜索 |
1.基本的排序算法
- 冒泡排序(BubbleSort)
- 插入排序(InsertionSort)
2.常考的排序算法
- 归并排序(MergeSort)
- 快速排序(QuickSort)
- 拓扑排序(Topological Sort)
3.其他排序算法
- 堆排序(Heap Sort)
- 桶排序(Bucket Sort)
注意:
- 冒泡排序和插入排序是最基础的,面试官有时候喜欢拿它们来考察你的基础知识,并且看看你能不能快速地写出没有bug的代码。
- 归并排序、快速排序和拓扑排序的思想是解决绝大部分涉及排序问题的关键。
- 堆排序和桶排序,本节课不作深入研究,但有时间的话一定要看看,尤其是桶排序,在一定的场合中(例如知道所有元素出现的范围时),能在线性的时间复杂度里解决战斗,掌握好它的解题思想能开阔解题思路。
冒泡排序
- 基本思想
给定一个数组,我们把数组里的元素通通倒入到水池中,这些元素将通过相互之间的比较,按照大小顺序一个一个地像气泡一样浮出水面。
- 实现
每一轮,从杂乱无章的数组头部开始,每两个元素比较大小并进行交换,直到这一轮当中最大或最小的元素被放置在数组的尾部,然后不断地重复这个过程,直到所有元素都排好位置。其中,核心操作就是元素相互比较。
- 代码实现
var arr = [2, 1, 7, 9, 5, 8];
// 冒泡排序 O(n^2) 稳定算法
// 在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的
// 每一轮,从数组头部开始,比较两个元素,将大的换到后面,则这一轮结束后就将最大的元素换到了数组尾部
// 如果hasChange===flase,说明上一轮未发生位置交换,已经排序好了,就不需要下一轮了
bubbleSort = function (arr) {
var hasChange = true;
for (var i = 0; i < arr.length - 1 && hasChange; i++) {
hasChange = false;
for (var j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
hasChange = true;
}
}
}
return arr;
};
bubbleSort(arr); // [1,2,5,7,8,9]
- 空间复杂度: 由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,所以空间复杂度是 O(1)。
- 时间复杂度:冒泡排序的时间复杂度是 O(n^2)。它是一种稳定的排序算法。(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。)
插入排序
- 基本思想
不断地将尚未排好序的数插入到已经排好序的部分。
- 特点
在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来说,经过每一轮的排序处理后,数组前端的数都是排好序的。
- 代码实现
function InsertionSort(arr) {
var len = arr.length;
// 将数组的第一个元素当作是已经排序好的,从第二个元素,即 i 从 1 开始遍历数组
for (var i = 1; i < len; i++) {
var temp = arr[i]; // 存 待插入的元素
for (var j = i; j > 0; j--) {
// 将待插入的元素与他前面的进行比较,如果比前面的小,就将前面的后移
if (temp >= arr[j - 1]) {
break; // 当前考察的数大于前一个数,证明有序,退出循环
} else {
arr[j] = arr[j - 1]; // 将前一个数复制到后一个数上
}
}
// 内循环结束,j所指向的位置就是temp值插入的位置
arr[j] = temp; // 找到考察的数应处于的位置
}
console.log(arr);
return arr;
}
InsertionSort(arr); // [1, 2, 5, 7, 8, 9]
- 时间复杂度:
和冒泡排序一样,插入排序的时间复杂度是 O(n2),并且它也是一种稳定的排序算法。
- 空间复杂度
假设数组的元素个数是 n,由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。
归并排序( Merge Sort)
- 基本思想
核心是分治,就是把一个复杂的问题分成两个或多个相同或相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致。
- 实现
一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,才开始排序。
排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。
- 代码实现
// 归并排序
function mergeSort(arr) {
debugger
var len = arr.length;
if (len < 2) {
return arr;
}
// 首先将无序数组划分为两个数组
var mid = Math.floor(len / 2);
var left = arr.slice(0, mid);
var right = arr.slice(mid, len);
return merge(mergeSort(left), mergeSort(right));
}
// 合并,依次将小的放进新数组
function merge(left, right) {
var result = [];
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
result.push(left.shift(0));
} else {
result.push(right.shift(0));
}
}
while (left.length > 0) {
result.push(left.shift(0));
}
while (right.length > 0) {
result.push(right.shift(0));
}
return result;
}
console.log(mergeSort(arr)); // [1, 2, 5, 7, 8, 9]
- 空间复杂度:由于合并n个元素需要分配一个大小为n的额外数组,合并完成之后,这个数组的空间就会被释放,所以算法的空间复杂度就是O(n)。归并排序也是稳定的排序算法。
- 时间复杂度:归并算法是一个不断递归的过程。
举例:数组的元素个数是n,时间复杂度是T(n)的函数。
解法:把这个规模为n的问题分成两个规模分别为η/2的子问题,每个子问题的时间复杂度就是T(n/2),那么两个子问题的复杂度就是2×T(n/2)。当两个子问题都得到了解决,即两个子数组都排好了序,需要将它们合并,一共有n个元素,每次都要迸行最多n-1次的比较,所以合并的复杂度是o(n)。由此我们得到了递归复杂度公式:T(n)=2×T(n/2)+O(n)。
对于公式求解,不断地把一个规模为η的问题分解成规模为η2的问题,一直分解到规模大小为1。
如果n等于2,只需要分一次;如果η等于4,需要分2次。这里的次数是按照规模大小的变化分类的以此类推,对于规模为η的问题,一共要进行log(η)层的大小切分。在每一层里,我们都要进行合并,所涉及到的元素其实就是数组里的所有元素,因此,每一层的合并复杂度都是O(n),所以整体的复杂度就是 o(nlogn)
建议:归并算法的思想很重要,其中对两个有序数组合并的操作,在很多面试题里都有用到,建议大家一定要把这个算法练熟。
快速排序( Quick Sort)
- 基本思想:快速排序也采用了分治的思想。
- 实现:取一个基准元素,比这个元素小的放到左边数组,比这个元素大的放到右边数组,然后递归地排序两个子数组,然后把排序好的左数组、基准元素、排序好的右数组合并。
- 代码实现
// 快速排序
function quickSort(arr) {
if (arr.length < 2) {
return arr;
}
var p = arr[0]; // 用第一个元素作为基准值,比它小的放到左边数组,比它大的放到右边数组
var left = [];
var right = [];
for (var i = 1; i < arr.length; i++) {
if (arr[i] <= p) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), p, ...quickSort(right)];
}
console.log(quickSort(arr)); // [1, 2, 5, 7, 8, 9]
- 时间复杂度: 最好情况O(nlogn),最坏情况O(n^2)
拓扑排序( Topological Sort)
- 基本思想和前面介绍的几种排序不同,拓扑排序应用的场合不再是一个简单的数组,而是研究图论里面顶点和顶点连线之间的性质。拓扑排序就是要将这些顶点按照相连的性质进行排序。
要能实现拓扑排序,得有几个前提
1.图必须是有向图
2.图里面没有环拓扑排序一般用来理清具有依赖关系的任务。
举例:假设有三门课程A、B、C,如果想要学习课程C就必须先把课程B学完,要学习课程B,还得先学习课程A,所以得出课程的学习顺序应该是A->B->C
实现
1.将问题用一个有向无环图(DAG, Directed Acyclic Graph)进行抽象表达,定义出哪些是图的顶点,顶点之间如何互相关联。
2.可以利用广度优先搜索或深度优先搜索来进行拓扑排序。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。