冒泡排序
function bubbleSort(arr){
const len = arr.length;
for(let i = 0;i<len-1;i++){
for(let j = 0;j<len-1-i;j++){
if(arr[j+1]<arr[j]) [arr[j],arr[j+1]] = [arr[j+1],arr[j]]
}
}
return arr;
}
时间复杂度为 (O(n^2)) 每次两两对比数组中的两个值,如果前一个大于后一个,交换位置,这样每次内层循环完成后,都会把未排序完成的部分的最大值推到未排序部分的最后位置 如此进行len-1次,整个数组就会变得有序了 需要注意的是内层循环过程中,因为每次循环都会把最大的值推到待排序区间的末尾,所以内层循环不是每一次都需要走到整个数组的末尾
选择排序
function selectSort(arr){
for(let i = 0;i<arr.length-1;i++){
let ind = i
for(let j = i+1;j<arr.length;j++){
if(arr[j]<arr[ind]) ind = j;
}
if(ind!==i){
const tmp = arr[ind]
arr[ind] = arr[i]
arr[i] = tmp
}
}
return arr;
}
间复杂度为 (O(n^2)) 每次找出未排序区间的最小值,如果未排序区间最小值的下标不是本次循环下标,交换两个位置的值 如此每次循环都会确定未排序区间的最小值到本次循环的下标位置 循环len-1次,整个数组就变得有序了
插入排序
function insertSort(arr){
for(let i = 1;i<arr.length;i++){
for(let j = i;0<j;j--){
if(arr[j]<arr[j-1]) [arr[j-1],arr[j]] = [arr[j],arr[j-1]]
}
}
return arr;
}
时间复杂度为 (O(n^2)) 就像码牌一样,每次将新的值放到有序数组的合适位置 如此每次循环,都会保证当前下标的之前区间是有序的,直到数组的末尾
希尔排序
function shellSort(arr){
const len = arr.length;
let step = len >> 1;
// 处理步长
for(step;0<step;step = step >> 1){
// 分组插入排序
for(let i = step;i<len;i++){
for(let j = i;j>=step&&arr[j]<arr[j-step];j-=step){
[arr[j-step],arr[j]] = [arr[j],arr[j-step]]
}
}
}
return arr
}
希尔排序是对插入排序的优化算法 通过步长,从后向前比较间隔为步长的两个元素的大小,如果增序排列且后边的值小于前面的值,则互换位置 这样每次完成后每一组步长内的元素是有序的 步长每次/2 直到步长为1,整个数组有序 这样可以让交换的过程尽量变短
归并排序
function mergeSort(arr){
if(arr.length===1) return arr;
function merge(left,right){
const res = [];
while(left.length&&right.length){
if(left[0]<right[0])
res.push(left.shift())
else
res.push(right.shift())
}
return [...res,...left,...right]
}
const mid = arr.length >> 1;
return merge(
mergeSort(arr.slice(0,mid)),
mergeSort(arr.slice(mid))
)
}
时间复杂度为 (O(nlogn) 通过递归每次两分的拆分数组,直到数组中只有一个元素为止 在递归回溯的过程中,组合左右两部分数组 组合过程中,每次比较两个数组的第一位,将更小的值取出放入结果数组,直到两个数组中某一个清空,组合结果返回
快速排序
快排1
function quickSort(arr){
if(arr.length<2) return arr;
const base = arr.pop(),left = [],right = [];
for(let i = 0;i<arr.length;i++){
if(arr[i]<base)
left.push(arr[i])
else
right.push(arr[i])
}
return [...quickSort(left),base,...quickSort(right)]
}
取数组的某一项为基准 base,将小于该基准的数组放到left,反之放到right 不停递归拆分数组直到数组长度为1 递归回溯过程中组合left,base,right
快排2
function handle(arr, left, right) {
if(left>=right) return left;
const base = arr[left]
let l = left + 1;
let r = right;
while (l <= r) {
if(arr[l] < base) {
l++;
} else if(arr[r] >= base) {
r--;
} else {
// 存在 l<r && arr[l]>arr[r] 的情况 进行交换
[arr[l],arr[r]] = [arr[r],arr[l]]
l++,r--
}
}
// 将起始元素与最后一个小于它的值交换
[arr[left],arr[l-1]] = [arr[l-1],arr[left]]
// 返回基准值下标
return l - 1;
}
function quickSort(arr, l, r) {
if (l < r) {
const ind = handle(arr, l, r);
// 递归处理基准值左侧区间
quickSort(arr, l, ind - 1);
// 递归处理基准值右侧区间
quickSort(arr, ind + 1, r);
}
return arr;
}
通过双指针在每个区间中查看是否存在 l<r && arr[l]>arr[r] 的情况,如果存在将两个元素的值互换
最后将基准值与最后一个小于它的元素进行互换,返回最后一个小于基准值的元素的下标位置
因为双指针移动过程中将左指针元素大于右指针元素的情况进行了处理,又因为基准值是和最后一个小于它的元素的值进行互换,所以此时基准值前面的元素肯定都小于它,后面的元素一定都大于等于它
通过基准值将数组进行二分,递归处理二分后的两个区间即可
时间复杂度为均为 (O(nlogn)
不同的是
快排1 空间复杂度为更高,个人测试为 O(5n~10(n-1))
快排2 的空间复杂度是小于等于 O(n-1) 的
计数排序
计数排序是通过
function countSort(arr){
countArr = [],
res = [];
for(let i = 0;i<arr.length;i++){
if(countArr[arr[i]] === undefined) countArr[arr[i]] = 0
countArr[arr[i]]++
}
for(let i = 0;i<countArr.length;i++){
while(countArr[i]){
res.push(i)
countArr[i]--
}
}
return res;
}
计数排序过程是
1.通过将待排序数组的值放到计数数组的对应下标位置并记录数量的排序算法,同一个值出现几次,则在计数数组中对应下标位置存在对应次数数值
2.遍历计数数组,如果计数数组当前下标值不为0,则将当前下标push进结果数组,当前下标值-1,直到为0
但是计数排序存在很大局限,只能对非负整数进行排序,因为数组的下标是从0开始的整数
那如果数组中存在负整数怎么办呢,我们采用下面这种改良版
function countSort(arr){
const min = Math.min(...arr),
countArr = [],
res = [];
for(let i = 0;i<arr.length;i++){
if(countArr[arr[i]-min] === undefined) countArr[arr[i]-min] = 0
countArr[arr[i]-min]++
}
for(let i = 0;i<countArr.length;i++){
while(countArr[i]){
res.push(i+min)
countArr[i]--
}
}
return res;
}
在存储到计数数组中的时候,存放在当前值减去待排序数组中的最小值的位置,这样,可以保证负整数的值也可以存在大于等于0的下标位置
在遍历计数数组并取出值的时候,再加上min即可
那如果要排序的是非整数,甚至是商品列表怎么办呢?
首先如果是这种待排序数组,就要考虑是否选择其他排序方式更适合呢?
如果还想通过计数排序进行排序,这里我想到的就是可以通过一个哈希表维护排序条件和对应下标的关系,如果您有不同的方法,欢迎评论区留言讨论。
Array.sort
Array.sort 查看 V8 源码可以看到当排序区间小于等于10的时候,会使用插入排序,反之会使用快速排序,而且它的快速排序肯定要比我文章中的快排进行了更多的优化,所以达到了如此高效的排序效率
v8 array.js InnerArraySort 710行
拓扑排序
拓扑排序通常用来对有向无环图进行排序,排序后的结果,可以表示图中的连接关系或者先后关系。
那什么是有向无环图呢?下面的示例其实就是一种有向无环图。
那它有什么含义呢?其实图通常是实际问题抽象出来的一种关系。
比如说上图可以表示上下级关系,以上下级关系为例:
此时 a 是 b 和 c 的领导;
b 是 a 的领导;
c 是基层员工,没有下属。
上图还可以用来表示线路图关系:
此时从 a 城市去往 b 城市有直达列车,也可以先去 b 城市,再换成去 c 城市。
接下来我们以下面的图为例演示拓扑排序的过程。
这里我们要了解一些前置知识:
这里我们需要知道一个概念叫 入度,这里我们可以简单理解为有几个节点指向当前节点,那么当前节点的入度就是几,以上图为例,a 的入度为 0,b 的入度为 1,e 的入度为 2。
拓扑排序中要借助额外的存储空间存储中间节点,通常是一个队列,在 JavaScript 中我们可以用数组模拟,还需要一个结果数组存储排序后的结果。
接下来我们看一下拓扑排序的过程:
因为 JavaScript 没有图这种数据结构,所以这里我们不进行实际的代码演示,我会在 leetcode题解 的文章中应用到拓扑排序进行解题,感兴趣的小伙伴可以去我的拓扑排序题解专栏查看。
基数排序
假设我们有这样的一个初始数组
首先我们讲一下大的逻辑:
- 基数排序首先会对数组中的值按个位大小进行排序,得到结果
21,11,31,21,32,22,13 - 然后对数组中的值按十位大小进行排序,得到结果
11,13,21,21,22,31,32也就是说基数排序会对数字按照每一位的值分别进行一次排序,最后得到排序完成的结果。
具体每一次的过程是如何做的呢?
首先按个位数排序的时候,会首先统计每个个位上的数字的出现的次数,示例中1出现了4次,2出现了2次,3出现了1次。
然后对这个结果求前缀和,得到4,6,7,这一步是为了得到归位数字的区间,也就是个位为1的4个数字放到0~3的区间,个位为2的2个数字放到4~5的区间,个位为3的1个数字放到下标6的位置。
而归位的过程是从后向前遍历原数组,然后从区间的末尾往前放置。
所以此时首先找到21,放到下标3的位置, 然后找到22放到下标5的位置
然后找到31放到下标2的位置
然后找到32放到下标4的位置
然后找到11放到下标1的位置
然后找到21放到下标0的位置
然后找到13放到下标6的位置
所以得到结果21,11,31,21,32,22,13
那为什么遍历数组的顺序和放置的顺序要保持一致呢?
这样做是为了保证数据的稳定性。
这里的稳定性是指排序后元素之间的相对位置是不变的,比如初始的时候,31在21的前面,按个位排序后,31依然在21的前面。
而这样的一个特性,是基数排序按位排序有效的一个基础。
这里大家要理解一下,比如我们个位排序后,21在22的前面,则按十位排序后,21依然在22的前面。
接下来我们看一下按十位排序的过程。
同样首先统计每个十位上的数字出现的次数,1 出现了 2 次,2 出现了 3 次,3 出现了 2 次。
所以求前缀和得到 2,5,7,得到归位数字的区间后如下:
从后向前遍历原数组,然后从区间的末尾往前放置。
所以此时先找到 13,放到下标 1 的位置。
然后找到 22 放到下标 4 的位置
然后找到 32 放到下标 6 的位置
然后找到 21 放到下标 3 的位置
然后找到 31 放到下标 5 的位置
然后找到 11 放到下标 0 的位置
然后找到 21 放到下标 2 的位置
所以得到结果 11,13,21,21,22,31,32
至此就完成了示例数组的基数排序。
接下来我们看一下动画演示过程:
在做代码实现之前要讲几个前置知识,计算机采用 32 位二进制存储整型数字,所以我们可以通过 低 16 位 和 高 16 位 进行两次排序,这里可以简单理解 低 16 位 对应上述排序个位,高 16 位 对应上述排序十位。
num & 0xffff 可以取整数的 低 16 位,num & 0xffff >> 16 可以取整数的 高 16 位。
所以我们首先对 低 16 位 进行一次排序,然后对 高 16 位 进行一次排序,就可以完成整个基数排序的过程。代码如下:
// 求低16位
function low16(num) {
return num & 0xffff
}
// 求高16位
function __high16(num) {
return (num & 0xffff0000) >> 16
}
function high16(num) {
const h = __high16(num)
return h > 32767 ? h - 32768 : h + 32768
}
function radixSort(arr) {
// 获取输入数组长度
const len = arr.length,
// 创建计数数组
count = Array(65536).fill(0),
// 创建 temp 数组存储低 16 位排序后结果
temp = Array(len)
// 获取低十六位出现次数
for (let i = 0; i < len; i++) {
count[low16(arr[i])]++
}
// 计算前缀和
for (let i = 1; i < 65536; i++) {
count[i] += count[i - 1]
}
// 归位
for (let i = len - 1; i >= 0; i--) {
temp[--count[low16(arr[i])]] = arr[i]
}
// console.log(temp)
// return temp
// 清空 count
for (let i = 0; i < 65536; i++) count[i] = 0
// 获取高十六位出现次数
for (let i = 0; i < len; i++) {
count[high16(arr[i])]++
}
// 计算前缀和
for (let i = 1; i < 65536; i++) {
count[i] += count[i - 1]
}
// 归位
for (let i = len - 1; i >= 0; i--) {
arr[--count[high16(temp[i])]] = temp[i]
}
return arr
}
基数排序只能对整数进行排序,时间复杂度为 O(n)。
至此,我们就讲完了 10 种排序算法!
本文会随个人学习进行更新,喜欢的话给个赞吧!😁
如有任何问题或建议,欢迎留言讨论!👏🏻👏🏻👏🏻