javascript排序算法
以下是JavaScript实现各种排序算法,并根据references:十大排序算法JavaScript实现总结对各种排序进行简单比较。
文章目录
应用总结
数据基本有序且数据量较小
- 冒泡排序
- 插入排序
- 希尔排序
- 选择排序
数据量大
- 堆排序 :适用于数据量大的情况,当数据呈流式输入时堆排序也很方便。
- 归并排序:适用于数据量大且需要要求排序稳定时使用。
- 快速排序:适用于数据量大的情况。
简单排序
- 冒泡排序
- 选择排序
- 插入排序
高级排序
- 希尔排序
- 快速排序
- 归并排序
- 堆排序
冒泡排序:
特点是嵌套循环。每次都是比较相邻的两个元素然后如果满足条件立即交换位置。所以每次一轮循环结束后最后两个元素总是已经排好序的。每轮排序后最后一位总是当前轮次最大的值
function bubbleSort(arr){
// 这个地方有一点需要注意:i是从arr.length开始还是arr.length-1开始呢?
// 其实从运行上看都是可以的,因为undefined-10==NaN,但是在做一些冒泡排序的变种题
// 的时候,这个地方就需要格外区分。(参考leetcode 164题)
for (var i = arr.length; i >0 ; i--) {
// 一定注意是j<i
for (var j = 0; j < i; j++) {
if (arr[j]>arr[j+1]) {
var temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
return arr;
}
console.log(bubbleSort(test));
选择排序:
特点是嵌套循环。每次都是固定一个位置让该位置跟所有其他位置元素对比,也就是说每次把最小的元素放在本次循环的第一位,总是在内层循环结束后进行交换位置,实现排序。一次遍历结束,最左边的一定是已经排序好的元素。
var test1=[6,8,0,6,7,4,3,1,5,10];
function selectionSort(arr){
for (var i = 0; i < arr.length-1; i++) {
var min=i;
for (var j = i+1; j < arr.length; j++) {
if (arr[min]>arr[j]) {
min=j;
}
}
var temp=arr[min];
arr[min]=arr[i];
arr[i]=temp;
console.log(arr);
}
return arr;
}
console.log(selectionSort(test1));
插入排序:
也是使用嵌套循环实现。通俗的讲插入排序就是首先拿出来第一个数。然后从第二个数开始和第一个数对比如果小于第一个数就把第二个数移到第一个数前面。以此类推,第三个数和前两个数对比一直到j–等于0或者说它大于它前面某个数跳出循环,插入到当前j的位置。
const insertSort=arr=>{
let len=arr.length;
/**
* 插入排序的精髓是在一个已经有序的小序列上进行比较插入。
总是跟以前的元素相比然后直接插入到比该元素a大的元素b前面,且比元素a小的元素c后面
* 插入排序是稳定的,排序过程中不会让两个相同的元素位置发生改变
**/
for(let i=1;i<len;i++){
let temp=arr[i];
// key1:j要从i开始移动,这样才能保证移动到位
let j=i;
// key2:不断的拿这个值和其他值做比较,j且应该大于0
while(j>0&&temp<arr[j-1]){
// 向后错一位留️位置
arr[j]=arr[j-1];
// console.info('while===>',arr);
j--;
}
// 就是插入到这个位置
arr[j]=temp;
// console.info('change==>',arr);
}
return arr;
};
希尔排序:
在插入排序的基础上做了很大改进。具体原理是通过定义一个间隔序列表示排序过程中进行比较的元素之间有多远的间隔。然后对按间隔分好的数进行插入排序,最后的间隔总是1,实现对排好序的这些分组进行排序。
var test3=[6,0,2,9,3,5,8,0,5,4];
var gaps=[5,3,1];
function shellSort(gaps,arr){
for (var g = 0; g < gaps.length; g++) {
for (var i = gaps[g]; i < arr.length; i++) {
var temp=arr[i];
var j=i;
while(j>=gaps[g]&&temp<=arr[j-gaps[g]]){
arr[j]=arr[j-gaps[g]];
j-=gaps[g];
}
arr[j]=temp;
}
console.log(arr);
}
return arr;
}
console.log(shellSort(gaps,test3));
快速排序:
原理比较简单,其是处理大数据集最快的排序算法之一。
纠正:看了阮一峰之前的博客中关于快速排序的 JavaScript 实现,如下的第二段代码片,私以为这种实现方式可以说是最简单最偷懒的一种方式,而且有悖于快排空间复杂度只有 O(logN) 的本质。
核心是: ① 原地移动,只需要一个新的空间来暂存基准值 ② 递归,对移动后的基准值左边的数组和右边的数组依次进行排序即可
function quickSort (arr, low, high){
if(low>=high){
return arr
}
function partition(arr, low, high){
// 选择开始或者结束作为基准值
// 选择开始值作为基准值的方式可以参考 https://github.com/LynnWonder/javascript_prac/tree/leetcode/sort
const pivot = arr[high]
let j = low-1
// 不断的移动 i,把比基准值大的值也一并移动到后面去
for(let i=low;i<high;i++){
// 如果想要逆序,这里可以改成大于
if (arr[i]<=pivot){
j++
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
// 交换基准值到它真正改在的位置
[arr[j+1], arr[high]] = [arr[high], arr[j+1]]
return j+1
}
pivot = partition(arr, low, high)
quickSort(arr,low, pivot-1)
quickSort(arr,pivot+1, high)
return arr
}
比较简单的实现:
// 快速排序
var test4=[7,4,6,3,1,5,8];
function quickSort(arr){
if (arr.length<=1) {
return arr;
}
var idx=Math.floor(arr.length/2);
var pivot=arr[idx];
arr.splice(idx,1);
console.log(arr);
var left=[],right=[];
for (var i = 0; i < arr.length; i++) {
if (arr[i]<pivot) {
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot],quickSort(right));
}
console.log(quickSort(test4));
时间复杂度计算方法:
T(N)=2*T(N/2)+N ===>O(nlogn)
归并排序
references:
概念
归并排序的思想就是把一系列排好序的子序列合并成一个大的完整有序序列。
归并排序时建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,成为2-路归并。
实现
/**
* 合并函数,也是归并排序的关键
* @param left
* @param right
* @returns {[]}
*/
const merge=(left,right)=>{
let res=[];
while(left.length&&right.length){
if(left[0]<=right[0]){
res.push(left.shift());
}else{
res.push(right.shift());
}
}
while(left.length){
res.push(left.shift());
}
while(right.length){
res.push(right.shift());
}
return res;
};
/**
* 把长度为n的输入序列分成两个长度为n/2的子序列;
* 对这两个子序列分别采用归并排序;
* 将两个排序好的子序列合并成一个最终的排序序列;
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n)
* 经检测是稳定排序
* @param arr
* @returns {*[]|*}
*/
const mergeSort=arr=>{
if(arr.length<2) return arr;
let mid=Math.floor(arr.length/2);
let left=arr.slice(0,mid),right=arr.slice(mid);
return merge(mergeSort(left),mergeSort(right));
};
summary
时间复杂度分析:
从这个递归树可以看出,第一层时间代价为cn,第二层时间代价为cn/2+cn/2=cn.....
每一层代价都是cn,总共有logn+1
层。所以总的时间代价为cn*(logn+1)
.时间复杂度是o(nlogn)
.
btw,也可以用master theorem得到
空间复杂度: O(n)
堆排序
概念扫盲:
- 堆:可以被看做一棵树的数组对象。一棵完全二叉树,且堆的某个节点的值总是不大于或不小于其父节点的值
- 大根堆:根节点最大的堆
- 小根堆:根节点最小的堆
- 堆有序:一棵二叉树的每个结点都大于等于它的两个子节点
- 二叉堆:一组能够用堆有序的完全二叉树排序的元素
- 优先队列:普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。
优先队列可以分为最大优先队列和最小优先队列,最大优先队列主要支持两种操作:插入元素和删除最大元素,最小优先队列则支持插入元素和删除最小元素。
- 优先队列适用场景:不需要对数据集完全有序,我们只需要获取数据集最大的一个或几个元素。
- 堆有序化:当我们向二叉堆中添加一个元素或从二叉堆中删除一个元素后,导致二叉堆的有序性被打破,这时我们要通过某种过程来恢复二叉堆的有序性,这个过程就是堆的有序化。
- 堆有序化-上浮swim:让一个结点向上移动到满足堆有序的位置
- 堆有序化-下沉sink:让一个结点向下移动到满足堆有序的位置
简述堆排序的全过程:
首先将数组看成一个数据结构:堆,核心的两个程序分别是构建大顶堆(arr,start,size)和交换位置(arr,i,j),构建大顶堆是一个递归的过程,一次结束后能找到所有数据中最大的那个数,然后让它跟最后一个结点交换并将其剔除,再次递归建立大顶堆(arr,0,size-1),为什么从0开始的呢,因为第一个结点和最后一个结点交换位置后破坏了堆的结构,此时要从第一个开始重新构建。
具体过程:
-
初始化建堆:找到最后一个根结点(不是叶子结点)从它开始构建大顶堆,会发现并没有递归,因为再往下就不会有根节点了,但此时有for循环控制着,能够将所有数据都遍历一遍。
-
排序重建堆:开始交换第一个和最后一个结点,同时构建最大堆的size要开始递减,最后得到排序后的数组
const HeapSort=(arr)=>{
let len=arr.length;
if(len<=1) return arr;
// 首先构建最大堆
let lst=Math.floor(len/2)-1;
/**
* 思路错误:我们应该从倒数第一个根节点往前遍历构建最大堆
* 直到构建到根节点
**/
for(let i=lst;i>=0;i--){
// console.info('first',i,arr);
buildMaxHeap(arr,i,len);
}
/**
* 那么此时一轮最大堆才构建完毕,我们要做的是交换根节点和最后一个叶子结点
* 此处应该是一个循环,循环构建最大堆,同时交换位置
**/
for(let i=0;i<len;i++){
// console.info('second',i,arr);
swap(arr,0,len-1-i);
/**
* 此时的构建最大堆不是从最后一个根节点开始了
* 因为此时我们就从根节点开始,往里面递归,注意buildMaxHeap做了这种操作
**/
buildMaxHeap(arr,0,len-i-1);
}
return arr;
};
const buildMaxHeap=(arr,i,size)=>{
// 找出最后一个父节点
let left=i*2+1;
let right=i*2+2;
let temp=i;
if (left>=size||right>=size) return;
if(arr[left]>arr[temp]){
temp=left;
}
if(arr[right]>arr[temp]){
temp=right;
}
// 递归的位置不对,构建完最大堆之后,应该从交换前的那个位置进行再次构建
// 以避免由于交换破坏了子树的最大堆结构
if (temp!==i){
swap(arr,temp,i);
buildMaxHeap(arr,temp,size);
}
};
/**
* change position
* @param arr
* @param i
* @param j
*/
const swap=(arr,i,j)=>{
let temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
};
/**
下面是一版自己实现的代码,和上面在处理上有稍许不同
*/
/**
* 首先按自己的印象实现一个堆排序,
* 堆排序的关键是构建大根堆
*/
const buildHeap=(arr,i)=>{
if (i>arr.length) return;
let left=i*2+1;
let right=i*2+2;
let maxIdx=i;
if (arr[left]>arr[maxIdx]){
maxIdx=left;
}
if (arr[right]>arr[maxIdx]){
maxIdx=right;
}
if (maxIdx!==i){
swap(i,maxIdx,arr);
}
buildHeap(arr,left);
buildHeap(arr,right);
};
/**
* 实现交换两个元素的辅助数组
* @param i
* @param j
* @param arr
*/
const swap=(i,j,arr)=>{
let temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
};
/**
* 实现堆排序,如何找出最后一个拥有叶子节点的根节点
* 易出错点1:寻找最后一个拥有叶子结点的根节点这一步拥有困难,思路出现错误,**不应该是求对数
* 易出错点2:构建完一次大根堆之后怎么处理,这部分不是用的递归,而是从新从头开始构建大根堆
*/
const heapSort=arr=>{
let res = [];
if (arr.length<1) return res;
//应该是从最后一个拥有叶子节点
for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
buildHeap(arr, i);
}
for (let i=0;i<arr.length;i++){
swap(0, arr.length - 1, arr);
res.push(arr.pop());
buildHeap(arr,0);
i--;
}
return res;
};
let arr0=[7,4,6,5,1,3,9];
let arr1=[1,2,3,4,5,6,2,2];
console.info(heapSort(arr0));
console.info(heapSort(arr1));
思考堆排序的时间复杂度和空间复杂度:
堆排序的时间复杂度分析
堆排序的时间复杂度分析
初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度是O(nlogn),因而总的时间复杂度是O(nlogn)
思考堆排序的稳定性
稳定排序和不稳定排序
不稳定。举例子说明:[7,4,6s,5,3,6e,1];经过排序后就会两个6的位置交换
非比较排序
并不是所有的排序 都是基于比较的,计数排序和基数排序就不是。基于比较排序的排序方法,其复杂度无法突破
𝑛log
𝑛 的下限,但是 计数排序 桶排序 和基数排序是分布排序,他们是可以突破这个下限达到O(n)的的复杂度的。
计数排序
references:
基本介绍
计数排序是一种稳定的线性时间排序算法。计数排序使用一个额外的数组C
,使用 C[i]
来计算i
出现的次数。然后根据数C
来将原数组A
中的元素排到正确的位置。
适用条件
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加1),这使得对于数组中数据范围很大的数组,需要大量的时间和内存。(简言之,不适于大范围数组)
基本实现
const countingSort=(arr)=>{
let min=max=arr[0];
for(let i=0;i<arr.length;i++){
if(arr[i]<min){
min=arr[i];
}else if(arr[i]>max){
max=arr[i];
}
}
let temp=new Array(max+1).fill(0);
while(arr.length>0){
let val=arr.shift();
temp[val]++;
}
// console.info(temp);
for(let i=0;i<temp.length;i++){
while(temp[i]>0){
arr.push(i);
temp[i]--;
}
}
return arr;
};
summary
计数排序的最坏时间复杂度、最好时间复杂度、平均时间复杂度、最坏空间复杂度都是O(n+k)
。n
为元素个数,k
为待排序数的最大值。
桶排序
references:
基本介绍
桶排序,顾名思义,将排序的数据放到桶里,初始时设置桶的数量,即排序的范围,如若要对100范围内的某10个数排序,即设置桶的数量为100,然后分别编号1到100,将要排序的10个数,放到与桶编号匹配的桶中。并将该编号的桶设置一个标志位,标志桶内有数据,输出时只要遍历所有桶,选择有数据的桶,并按编号输出即可。
桶排序利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
适用条件
桶排序的适用范围是,待排序的元素能够均匀分布在某一个范围[MIN, MAX]之间。
实现
const insertSort=arr=>{
for(let i=1;i<arr.length;i++){
let j=i,temp=arr[i];
while(j>0&&temp<arr[j-1]){
arr[j]=arr[j-1];
j=j-1;
}
arr[j]=temp;
}
return arr;
};
const bucketSort=(arr,bucketSize)=>{
if(arr.length===0) return;
let minValue=arr[0],maxValue=arr[0],DEFAULT_BUCKET_SIZE=5;
for(let i=0;i<arr.length;i++){
if(arr[i]<minValue){
minValue=arr[i];
}else if(arr[i]>maxValue){
maxValue=arr[i];
}
}
// 设置桶的默认容量为5
bucketSize=bucketSize||DEFAULT_BUCKET_SIZE;
// 确定桶的数量
let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
let buckets=new Array(bucketCount);
for(let i=0;i<bucketCount;i++){
buckets[i]=[];
}
while(arr.length>0){
let temp=arr.shift();
buckets[Math.floor((temp- minValue) / bucketSize)].push(temp);
}
for(let i=0;i<buckets.length;i++){
insertSort(buckets[i]);
for(let j=0;j<buckets[i].length;j++){
arr.push(buckets[i][j]);
}
}
console.info(buckets);
return arr;
};
summary
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
时间复杂度分析:
循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)
;
利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为∑ O(Ni*logNi)
。其中Ni
为第i个桶的数据量;
很显然,上述两个部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:
映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
为了使桶排序更加高效,我们需要做到这两点:
1、在额外空间充足的情况下,尽量增大桶的数量
2、使用的映射函数能够将输入的N个数据均匀的分配到K个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
什么时候最快
当输入的数据可以均匀的分配到每一个桶中
什么时候最慢
当输入的数据被分配到了同一个桶中
综上所述,对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
空间复杂度为:
O(N+M)
直接插入是稳定排序,因此桶排序也是稳定排序。
基数排序
references:
基本介绍
基数排序时按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
实现
const radixSort=(arr,maxDigit)=>{
let first=10,sec=1,count=[];
for(let i=0;i<maxDigit;i++,first*=10,sec*=10){
while(arr.length>0){
let tempArr=arr.shift();
let temp=Math.floor((tempArr%first)/sec);
if(!count[temp]){
count[temp]=[tempArr];
}else{
count[temp].push(tempArr);
}
}
console.info('count===>',count);
for(let k=0;k<count.length;k++){
if(count[k]){
while(count[k].length>0){
arr.push(count[k].shift());
}
}
}
console.info('arr===>',arr);
}
return arr;
};
/**
* MSD区别于LSD,它的目标是让分组后的每个数组元素都只有一个元素,如果多于1个就进行递归
* 递归的过程中让当前比较位-1
* @param arr
* @param r
* @returns {*}
*/
const radixSort1=(arr,r)=>{
let radix=Math.pow(10,r-1),count=[];
while(arr.length>0){
let tempArr=arr.shift();
let temp=Math.floor(tempArr%Math.pow(10,r)/radix);
if(!count[temp]){
count[temp]=[tempArr];
}else{
count[temp].push(tempArr);
}
}
for(let j=0;j<count.length;j++){
if(count[j]&&count[j].length>1){
radixSort1(count[j],r-1);
}
}
// collect items
for(i=0;i<count.length;i++){
while(count[i]&&count[i].length>0){
arr.push(count[i].shift());
}
}
return arr;
};
summary
-
时间复杂度:
-
最佳情况:T(n) = O(n * k)
-
最差情况:T(n) = O(n * k)
-
平均情况:T(n) = O(n * k)
-
其中:k为数组中的数的最大的位数
-
基数排序有两种方法:
-
MSD(Most Significant Digit First)从高位开始进行排序,即从左到右
-
LSD(Least Significant Digit First)从低位开始进行排序,即从右向左(以上代码是LSD)
-
基数排序 vs 计数排序 vs 桶排序
这三种方法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶
- 计数排序:每个桶只存储单一键值
- 桶排序:每个桶存储一定范围的数值
什么是稳定排序?
通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前
其中直接插入,冒泡,归并排序等都是稳定排序。
刷题记录: