- 理解排序的稳定性:
- 定义:序列[1,4\,3,6,5,4,2]排序后为[1,2,3,4,4\,5,6]则为不稳定,因为*4***与 4 的相对位置变了,*4*…4 变成了,4…***4*
- 判断要点:主要看算法是否有“间隔置换”,比如选择排序,4第一次与 2 置换后来到了 4 的后面(如果是相邻的交换就不会),最终也没能换回到 4 的前面,所以选择排序是不稳定的;希尔排序/快排也是,某一次增量/基准是可能有“间隔置换”,把原来的相对位置打乱了,所以是不稳定的;如冒泡没有间隔置换,相邻的操作可以保持相对位置不变,所以是稳定的(唯一例外,插入排序,虽然也是间隔交换,但是它保持了相对位置)
冒泡算法
- 两两比较,相邻两个元素比较,n 个元素需要 n 轮(每轮有 n 次);数据都像泡泡流(冒……)向合适的地方
- 时间复杂度 O(n^2),空间复杂度 O(1),稳定
1 | let arr = [1,5,9,7,5,6,7,4,6,57,65,48,4,65,4,987,999]; |
选择排序
- 从数组第一个位置开始,遍历剩余(未排序的,应该都在这个位置后)数组元素选择一个最合适的数放在这个位置。
- 时间复杂度 O(n^2),空间复杂度 O(1),不稳定(因为会置换)
1 | let arr = [1,5,9,7,5,6,7,4,6,57,65,48,4,65,4,987,999]; |
插入排序
- (排升序,以下算法是)从第二个开始,认为前面的已经是有序的了,当前位置的在排好序的前面的元素比较,小于的就插入,不然继续往前;不断插入到前面,插入——代表相对位置已经确定。这里的和选择排序的区别是,插入排序排的每次排是相对位置,选择排序是每次都是排最终位置。
- 时间复杂度是 O(n^2),空间复杂度为 O(1),稳定
1 | let arr = [1,5,9,7,5,6,7,4,6,57,65,48,4,65,4,987,999]; |
希尔排序
其实是升级版的插入排序,对插入排序做了优化,插入排序对有序度高的数组排序快,那么优化点就可以是序列的有序程度。希尔排序从“数组个数”和“数组有序度”去优化了插入排序。
整个数组的个数除于 2 得到一个分组数(增量一),然后在每个分组里面进行插入排序,排序后,这样一轮即完成了;然后第二轮,用第一轮(上一轮)的增量(增量一)除于 2,然后每个分组里面在进行插入排序,排序后结束本轮;重复步骤,知道增量变成<1,排序完成;
比如数组 1-8,一共 8 个元素,8/2=4,记增量为 4,所以分成四组,也即每组只有两个元素;同组元素时要分(因为分开才会整个数组都……怎么描述),所以这样分【1,5】、【2,6】、【3,7】、【4,8】,所以第
i
个元素的同组元素是i+增量
;编程时考虑不拆分原数组,所以为了“每个分组里排序”,那么需要利用
i+增量
是同组这个属性时间复杂度是 O(n^1.3),空间复杂度为 O(1),不稳定(因为会置换)
1 | let testArr = [1, 5, 9, 7, 5, 6, 7, 4, 6, 57, 65, 48, 4, 65, 4, 987, 999]; |
归并排序
- 分而治之思想,大序列划分为小序列,小序列再划分为更小的序列,直到最后只有一个的时候就很简单两个比较;这样的比较对排序
- 时间复杂度 O(n ㏒2 n),空间复杂度为 O(n),稳定
1 | let testArr = [1, 5, 9, 7, 5, 6, 7, 4, 6, 57, 65, 48, 4, 65, 4, 987, 999]; |
快速排序
- 也是分而治之思想,与归并排序的区别是,划分问题的方法:归并排序的划分子问题一般方法是从数组中间划分两端,然后递归划分直到一个;快速排序的划分是以一个元素作为一个基准,大于它和小于它分两半,然后递归划分直到一个。同时快排的主要操作是交换数组元素,使用的空间相对归并排序更少
- 实现:选择第一个元素作为基准,先从
数组尾
往回遍历,找到比基准小的与基准互换,互换后再数组头+1
开始(第一个已经确定比基准小了,应该放基准左变),找到比基准大的与基准互换,互换后又从右边刚刚基准所在-1
找小于的,互换位置,从左边找大于再刚刚基准所在+
1`。。。不断重复左右跳,直到没有元素可找,一轮结束;然后分别在基准两侧再进行刚刚的操作(更小粒度)。【比较乱,直接看代码吧】 - 平均时间复杂为 O(nlog₂n),最差可以达到 O(n^2),n 表示元素个数;空间复杂度为 O(1),一个空间记录基准+双指针用了 2 个空间 = 3 =》O(1);若用了递归可能性能还差点;不稳定(因为会置换)
1 | //测试数组 |
堆排序
前置知识:
完全二叉树:所有节点都先有左子树再有右子树的二叉树
堆,用数组来存储一颗有序的完全二叉树,分大根堆和小根堆
大根堆:根比左右子节点都大,但左右节点谁大没有规定
小根堆:根比左右子节点都小,但左右节点谁小没有规定
怎么用数组存储一个完全二叉树?
完全二叉树有个性质,如果按先序遍历给每个节点一个从 0 开始的序号,那么第 i 号节点的左右子节点分别是 2i+1、2i+2;所以数组构造堆,就是遍历数组,让当前元素 i 和 2i+1、2i+2 比较大小,最大的/最小的换到当前位即可
思想大概是利用堆这种结构排序,以大根堆为例,把数组构造成一个大根堆,取根节点,也即最大值,剩下的 n-1 个再构造一个大根堆,再取根节点,不断循环这样的操作就可以得到一个大到小的排序/小到大的排序
构造大根堆:
- 找 arr.length/2-1 的节点,即非叶子节点的从左到右的第一个节点,构造左右子节点都小于当前的节点,若是由子节点大于当前节点则交换,若交换后破坏了子节点的大根堆构造那么也要进行替换,直到最后
- 然后找第二个非叶子节点 arr.length/2-2,重复上面操作,直到多有非叶子节点都完成
堆排序是一种选择排序,堆其实就是帮助选择的数据结构而已
时间复杂度:O(nlog₂n)
空间复杂度:O(1)
是否稳定:不稳定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45//测试数组
let testArr = [1, 55, 68, 21, 598, 66, 221, 1554, 3879, 5216, 100, 8, 9, 5, 7, 6, 22, 21]
function heapSort(arr){
// 数组arr交换i,j的值
const 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]
}
// 堆调整
const heapify = (arr,i,len) => {
let left = i*2+1 //左节点
let right = i*2+2 //右节点
let largest = i //最大值序号
if(left < len && arr[left] > arr[largest]){
largest = left
}
if(right < len && arr[right] > arr[largest]){
largest = right
}
if(largest !== i){
swap(arr,i,largest)
heapify(arr,largest,len)
}
}
// 构造大根堆
for(let i = Math.floor(arr.length/2);i>=0;i--){
heapify(arr,i,arr.length)
}
// 取最大值
for(let i = arr.length-1;i>0;i--){
swap(arr,i,0)
heapify(arr,0,i)
}
return arr
}
console.log(heapSort(testArr))
桶排序
- 主要就是利用数组下标排序,把原数组放到另一个按下对应值的下标的数组(称桶数组)中,在按序取得这个桶数组的非空元素即可。为了节约空间,原数组的值可以经过一个“提炼”(一定的换算)得到一个下标,压缩桶数组的长度,这样有可能多个元素命中一个桶,那么在放进桶里排下序即可;思想有点类似 hash 表
- 时间复杂度 O(n),空间 O(n),稳定,是线性排序,没有 O(nlogn)限制,大数据很快,但是空间消耗也大
- 实现:
- 计划桶数组长度和原数组一样,可以取出原数组最大值和最小值(O(n)),求出它们的间隔,再除于原数组长度求每个桶的值间隔
- 遍历原数组,按照换算取得下标:下标 = 取整[ (元素 - 最小值) / 桶间隔 ] ; 看这个下标的桶是否有元素,有元素插入时稍微做个排序即可 O(m),没有的直接作为第一个元素
- 遍历桶数组,从下标小到大获取,桶内元素也同样获取即可
1 | //测试数组 |
参考
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
最后更新: 2021年07月15日 23:43
原始链接: https://idkhts.github.io/2021/01/19/%E6%8E%92%E5%BA%8F/