概述
js提供的排序api为
arr.sort([compareFunction])
如果不提供排序函数,则会将元素转化为字符串并按从前到后依次对比各字符的charcode升序排列,比如
[12,2].sort()//[12,2]
比较函数的两个参数a,b是待比较的两个元素,如果函数返回值小于0,则a会排在b之前,如果返回值大于0,则a排在b之后,否则不变。
其他排序
其他是和语言无关的排序算法,这里会介绍三种简单的和三种时间复杂度低的,即
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序
- 堆排序
其中前三种时间复杂度o(n^2),后三种o(nlogn),相关源码这里 。
另外的这里不展开,比如
- 桶排序 先确定有限个桶,然后将待排序元素按照特定属性放入各自桶中,再根据需要确定是否对每个桶进行排序。比如在前 K 个高频元素中的特定属性指的是元素出现的频率,这道题的具体思路为
- 统计每个元素出现的频率,并记录最大频率数n
- 准备n个桶,并根据频率数将各自元素放入各个桶内
- 然后从高频到低频的方向统计k个元素即可
- 拓扑排序 是对有向无环图顶点的广度优先遍历
冒泡排序
双层遍历,外层确定遍历范围,内层比较。
以递增排序为例,外层i从最后一个元素开始递减直至第一个,内层j从第0个开始和下一个比较,如果前面的大于后面的就交换,直到i-1个。
module.exports = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}
选择排序
选择排序和冒泡很类似,但内层比较遇到需要调整时不会立即交换,而是在内层比较之前假定第0个为最大值(记序号为max),然后遍历一遍待排序元素,直到第i个,然后将第i个和第max个交换。
虽然不需要每次都交换,但时间复杂度没差别。
module.exports = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
let max = 0
for (let j = 1; j <= i; j++) {
if (arr[max] < arr[j]) {
max = j
}
}
;[arr[max], arr[i]] = [arr[i], arr[max]]
}
return arr
}
插入排序
插入排序是假定第0个有序,i从第1个开始依次插入前面(i-1)有序数组中,使前i个成有序数组。
插入时,虽然插入位置可以通过二分法寻找,但实际的插入时间复杂度仍然没变化,而且书写更容易出错,因此这里选择交换插入。
module.exports = (arr) => {
for (let i = 1; i < arr.length; i++) {
// binaryInsert(arr, i)
compareInsert(arr, i)
}
return arr
}
//比较插入
function compareInsert(arr, i) {
while (arr[i - 1] > arr[i] && i > 0) {
;[arr[i - 1], arr[i]] = [arr[i], arr[i - 1]]
i--
}
}
归并排序
归并排序是分治法的一类典型问题,即将原问题分为子问题(分),然后解决子问题进而解决大问题,并最终解决原问题(治)。
归并排序即将大数组分为两个小数组,然后递归分解每个小数组,最终得到长度为1的子数组,然后将已经排好序的小数组(长度为1的数组已经有序)分别合并成大数组,并最终合并成排好序的原数组。
module.exports = (arr) => {
function sort(arr1) {
if(arr.length<2) return arr
let mid=Math.floor((arr.length-1)/2)
return merge(sort(arr.slice(0,mid+1)),sort(arr.slice(mid+1,arr.length)))
}
function merge(arr1, arr2) {
let arr=[]
while(arr1.length&&arr2.length){
arr.push(arr1[0]>arr2[0]?arr2.shift():arr1.shift())
}
arr.push(...(arr1.length?arr1:arr2))
return arr
}
return sort(arr)
}
快速排序
快速排序也是一种分治,和归并排序不同的是,快速排序是原地排序,而且只需要将大问题分解为小问题并解决即可,不需要后面的治。
具体的说,就是每次取一个参考值,比如第0个,将大于参考值的放到参考值后面,小于参考值的放到前面,这样就保证了参考值的有序,且原数组被分成两部分,在每个区间递归快速排序直到全部有序。
module.exports = (arr) => {
function sort(l, r) {
//由于l和r的来源,这里l可能大于r
if (l >= r) {
return
}
let pivot = arr[l], //挖坑
left = l,
right = r
while (left < right) {
//此时第0个位置为空,则遇到右边小于pivot的就移动过来
//后面需要等于号,不然如果有重复数据永远不会走出循环
while (left < right && arr[right] >= pivot) {
right--
}
arr[left] = arr[right]
//此时right的位子为空
while (left < right && arr[left] <= pivot) {
left++
}
arr[right] = arr[left]
}
//此时left===right
arr[left] = pivot
sort(l, left - 1)
sort(left + 1, r)
}
sort(0, arr.length - 1)
return arr
}
堆排序
堆数据结构
堆是一种数据结构,在逻辑上是一种完全二叉树,在物理上是数组。
这一节前置了一些二叉树的知识,后面会在对应章节具体讨论。
因为数组下标是从0开始的,二叉树从1开始的,因此处理前在数组前添加一个元素,比如0。二叉树在数组中按照广度优先遍历的顺序保存。
如果所有父节点都大于等于其子结点,这样的堆被称为大根堆,按照数组的形式即arr[i]>=arr[2*i]且arr[i]>=arr[2*i+1],最后一个非叶子节点序号是Math.floor((arr.length - 1) / 2)
堆排序
堆排序就是利用堆这种数据结构进行排序,即利用堆顶(比如大根堆)是最值的特点可以依次选出前n(大)元素,乃至将整体排序,堆排序适合不需要完整排序的情况,比如数组中的第K个最大元素。
在排序之前我们得到的是一个普通数组,因此首先要建堆和排序两部分
module.exports = (arr) => {
//当把数组当作树看待时,应在前面补充一个元素,使得下标一致,这样数组的最下下标为1
arr.unshift(0)
//首先将原序列调整为大根堆
buildMaxHeap(arr)
//从大根堆最后一个开始和第一个元素交换,即将最大元素调整至序列最后
for (let i = arr.length - 1; i > 1; i--) {
;[arr[i], arr[1]] = [arr[1], arr[i]]
// 然后把除了最后一个元素的序列调整成大根堆,重复以上过程直到子序列还剩最后一个元素,排序完成
heapAjust(arr, 1, i - 1)
}
arr.shift()
return arr
}
//调整以第k个元素为根的子树为大根堆
const heapAjust = (arr, k, end) => {
//arr为原数组,k为指定待调整子树的根节点,len为调整范围,创建大根堆时不是必须的,主要是排序时使用
//将根节点调整为该子树的最大值
arr[0] = arr[k] //先把当前子树根节点暂存,随后就可以将该位置用更大的值覆盖
for (let i = 2 * k; i <= end; i *= 2) {
if (i < end && arr[i] < arr[i + 1]) {
//如果有右子树且右子树大于左子树,则用右子树和当前父节点比较
i += 1
}
if (arr[0] >= arr[i]) {
break
} else {
arr[k] = arr[i] //较大的子结点成为根节点
k = i //继续以新的k为根节点继续比较
}
}
//最后k的位置是最终应该放的值
arr[k] = arr[0]
}
//建立大根堆,大根堆中的父节点大于等于子结点
const buildMaxHeap = (arr) => {
//将所有的非终端节点,从后往前依次遍历,使以各个节点为根的子树变为大根堆,
//其中最后一个非终端节点为:不大于序列长度n/2的整数,此时n=arr.length-1
//注意条件没用等号,因为0位置是后来补的
for (let i = Math.floor((arr.length - 1) / 2); i > 0; i--) {
heapAjust(arr, i, arr.length - 1)
}
}