#高考加油#每一天的学习就是对自己的投资

javascript系列--十大排序算法的总结(冒泡,选择,插入,希尔,归并,快排,堆排序,计数排序,桶排序,基数排序)

一、前言

传统的计算机算法和数据结构领域,都是基于java或者c/c++等。没用javascript实现过或是没仔细看过相关算法的原理,导致写起来浪费很多时间。对于一个前端来说,尤其是笔试面试的时候,算法方面考的其实不难(十大排序算法或是和十大排序算法同等难度的)。

算法由来:9世纪波斯数学家提出的算法“al-Khowarizmi”,阿拉伯人对数学史的贡献很大,让人钦佩。


二、说明

1、排序的定义:对序列的对象根据某一个关键字进行排序。

比如:需要对数字进行排序,1,7,10,2,6,40,33,99,使得1<2<6<7<10<33<40<99,就是类似于站队,矮的在前面,高的在后边。

2、算法的评述短发优劣术语的说明

稳定:a原本在b的前面,而a=b,排序后a仍然在b的前面;不稳定:a原本在前面,而a=b,排序后a可能出现在b的后边;

内排序:所有排序操作都在内存中完成;外排序:由于数据很大,因此把数据放在磁盘中,排序通过磁盘和内存的数据传输才能进行。

时间复杂度:一个算法执行需要耗费的时间;空间复杂度:运行这一个算法需要内存的大小。可以参考:javascript系列--时间复杂度和空间复杂度


3、排序算法图片总结


解释:n表示数据规模,k表示桶的个数,in-place表示占用常数内存,out-place表示占用额外内存。

排序分类



三、冒泡排序(Bubble Sort

最常用的排序算法之一,稳定排序。

思路:重复的访问每一个元素,一次比较两个元素,如果顺序不对就交换位置。

冒泡的由来:越大的元素会经过交换,浮动到元素的最后边。

描述:(1)比较相邻的元素,如果第一个比第二个大,就交换位置。(2)对每一对相邻元素做同样的工作,从开始的第一对到最后一对,这样最后的元素就是最大的;(3)针对所有元素,重复上述步骤,除了最后一个元素。

// 冒泡算法
function bubbleSort(arr){
    var len = arr.length;
    for(var i = 0;i < len; i++){
        for(var j =0;j < len-1-i;j++){
            if(arr[j]>arr[j+1]){
                var temp = arr[j+1];
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort(arr));

分析:一共进行14趟,每一趟比较15-i次,因为那i次在上一趟中已经排好序了。


改进的冒泡1

思路:设置一个标志性变量flag,用于记录每一趟排序中最后一次进行交换的位置。由于flag的位置之后的元素均已经交换到位,多以下一趟排序只需要扫到flag位置。

// 改进冒泡算法,加入标志位
function bubbleSort2(arr){
    var i = arr.length - 1;
    while(i > 0){
        var flag = 0;
        for(var j = 0;j < i;j++){
            if(arr[j]>arr[j+1]){
                flag = j;
                console.log('交换前数组顺序:',arr);
                var temp = arr[j+1];arr[j+1] = arr[j]; arr[j] = temp;
                console.log( '比较的元素:',arr[j], arr[j+1],'交换的位置:',flag,'交换后数组顺序:',arr)
            }
        }
        i = flag;
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort2(arr));


改进的冒泡2

思路:传统的冒泡排序每一趟值呢周到一个最大值或者最小值,我们可以每一趟排序中进行正向和反向两遍冒泡,一次可以得到两个最终值(最大和最小),从而是排序趟数减少了一半。

// 改进冒泡算法3,一趟找出最大值和最小值
function bubbleSort3(arr){
    var low = 0;
    var high = arr.length - 1;
    var temp,j;
    while(low < high){
        for(j = low;j < high;++j){   //正向冒泡,找最大值
            if(arr[j] > arr[j+1]){
                temp = arr[j+1];arr[j+1] = arr[j]; arr[j] = temp;
            }
        }
        --high;   //修改high,前移一位
        for(j = high; j > low; --j){
            if (arr[j]<arr[j-1]) {
                temp = arr[j]; arr[j]=arr[j-1];arr[j-1]=temp;
            }
        }
        ++ low;
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort3(arr));


总结

最佳情况:O(n)(已经是正序的情况)

最坏情况:O(n*2)(已经是倒叙的情况)

平均情况:O(n*2)

pubbleSort


四、选择排序(selection sort)

最常用的排序算法之一,但是是不稳定排序。

无论是什么数据进去,时间复杂度都是O(n*2)。数据规模越小越好,唯一的好处就是不占用额外的空间。

思路:在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。然后再从剩余未排序的数组汇总继续寻找最小(大)元素,依次类推。

描述:(1)初始状态:无序区为R[1...n],有序区为空;(2)第i趟排序(i = 1,2,3,...,n-1)开始的时候,当前有序区R[1,...,i-1]和无序区R[i,...,n]。从该趟排序从当前无序区中选出关键字最小的记录R[k],将R[k]与无序区的第一个记录R[1]交换,使无序区R[1,...,i]减少1个,和有序区R[i+1,...,n]增加1个。

// 选择排序
function selectionSort(arr){
    var length = arr.length;
    var minIndex,temp;
    for(var i = 0;i < length - 1;i++){
        minIndex = i;
        for(var j = i + 1;j<length;j++){
            if(arr[j] < arr[minIndex]){
                minIndex = j;
            }
        }
        temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp;
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(selectionSort(arr));


总结

最佳情况:O(n*2)

最坏情况:O(n*2)

平均情况:O(n*2)

selectionSort



五、插入排序(insertion sort)

插入排序也比较常见,稳定排序。

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。

思路:通过构建有序序列,对于未排序的元素在已排序的序列中从后向前扫描,并找到相应的位置并插入。插入排序的实现上,通常采用in-place排序(需要用到O(1)的额外空间排序),因此从后向前扫描的时候,需要反复把已排序的元素逐步向后移动,为最新元素提供插入空间。

描述:(1)从第一个元素开始,该元素被认为是已经被排序;(2)取出下一个元素,在已经排序的元素序列中从后往前扫描;(3)如果已排序的元素大于新元素,将该元素移动到下一个位置;(4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;(5)将新元素插入到该位置;(6)重复2-5的步骤。

// 插入排序 insertion sort
function insertionSort(arr){
    var length = arr.length;
    for(var i = 1;i < length;i++){
        var current = arr[i];
        var j = i - 1;
        while(j>=0 && arr[j]>current){
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = current;
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertionSort(arr));


改进的插入排序

思路:查找插入位置时使用二分查找的方式。

// 改进的插入排序 二分查找 insertion sort
function insertionSort(arr){
    var length = arr.length;
    for(var i = 1;i < length;i++){
        var current = arr[i];
        var left = 0;
        var right = i - 1;
        while(left <= right){
            var middle = parseInt((left+right)/2);
            if(current < arr[middle]){
                right = middle - 1;
            }else{
                left = middle + 1;
            }
        }
        for(var j = i - 1;j >= left;j--){
            arr[j+1] = arr[j];
        }
        arr[left] = current;
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertionSort(arr));


总结

最佳情况:O(n)

最坏情况:O(n*2)

平均情况:O(n*2)

insertionSort


六、希尔排序(shellsort)

突破排序的时间复杂度O(n*2),达到O(nlogn),不稳定的排序。

是一种简单的插入排序的改进版,与插入排序的区别是:希尔排序会优先比较距离较远的元素。希尔排序也被称为缩小增量排序

思路:核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。

描述:将整个待排序的记录序列分割成若干个子序列分贝进行直接插入排序。(1)选择一个增量序列t1,t2,t3,...,tk,其中ti>tj,tk = 1;(2)按照增量序列个数k,对序列进行k趟排序;(2)每趟排序,根据对应的增量ti,将待排序分隔成若干长度为m的子序列,分别对各子表进行直接插入排序。增量因子为1的时候,整个序列作为一个表来处理,表长度即为整个序列的长度。

// 希尔排序 shell sort
function shellSort(arr){
    var length = arr.length,temp,gap = 1;
    while(gap < length/5){
        gap = gap*5 + 1;
    }
    for(gap;gap>0;gap = Math.floor(gap/5)){
        for(var i = gap;i<length;i++){
            temp = arr[i];
            var j = i - gap;
            while(j>=0 && arr[j]>temp){
                arr[j+gap] = arr[j];
                j = j-gap;
            }
            arr[j+gap] = temp;
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(shellSort(arr));

希尔排序图示(图片来源网络):



总结

最佳情况:O(nlog2n)

最坏情况:O(nlog2n)

平均情况:O(nlogn)


六、归并排序(merge sort)

突破排序的时间复杂度O(n*2),达到O(nlogn),稳定的排序。

归并排序跟选择排序一样,不受数据的影响,但表现比选择排序好。始终是O(nlogn)的时间复杂度。代价是额外的内存空间。

思路:算法采用的是分治法的一个典型应用,是一种稳定的排序,将已经有序的子序列合并,得到完全有序的序列。先让每一个子序列都有序,使子序列里面有序,将两个有序表合并,称为二路归并。

描述:(1)把长度为n的输入序列分为两个长度为n/2的子序列;(2)对这两个子序列分别采用归并排序;(3)将两个排序好的子序列合并成一个最终的排序序列。

 // 归并排序 merge sort

function mergeSort(arr){
    var length = arr.length;
    if(length<2){
        return arr;
    }
    var middle = Math.floor(length/2);
    var left = arr.slice(0, middle);
    var right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right){
    var result = [];
    while(left.length && right.length){
        if(left[0] <= right[0]){
            result.push(left.shift());
        }else{
            result.push(right.shift());
        }
    }
    while(left.length){
        result.push(left.shift());
    }
    while(right.length){
        result.push(right.shift());
    }
    return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));


总结

最佳情况:O(n)

最坏情况:O(nlogn)

平均情况:O(nlogn)

mergeSort


七、快速排序(quick sort)

突破排序的时间复杂度O(n*2),达到O(nlogn),空间复杂度为O(logn),不稳定的排序。

顾名思义,快排,速度就是快,效率高,处理大数据的最快的排序算法之一。

思路:也是分治法的思想,通过一趟排序将待排序的元素分为独立的两个部分,其中一部分记录的关键字均比另外一部分的关键字小,然后分别对这两部分继续进行排序,已达到整个序列有序。

描述:分治法吧一个串list分为两个子串sub-list。(1)从串中挑一个元素,作为基准;(2)从新排序串,所有元素比基准值小的摆放在基准的前面,所有元素比基准值大的摆在基准的后边,相同的数可以到任意一边。这个分区退出后,基准就位于中间位置,这个称为分区操作;(3)递归的把小于基准值的元素的子串和大于基准值的子串进行排序。

觉得还是不是很懂,那我们在看看具体实现:

具体实现

(1)一般是取序列的第一个作为基准数;

(2)设置 i,j 两个指针分别指向最左端和最右端;

(3)每次比较都从右端 j 指针开始项左移动,寻找比基准点小的数,然后停止移动;

(4)然后左侧指针 i 向右移动寻找比基准数大的数,然后停止移动;

(5)此时交换 i 和 j 所指向的内容,这算一趟中一次交换完成,直到 i , j 指针相遇位置k,这时候将基准数和k位置的数字交换,这算完成了一趟排序。

// 快排 quicksort
function quickSort(arr,left,right){
    if(left<right){
        var x = arr[left];   //基准数,一般取第一个值
        var i = left;    //左边的指针
        var j = right;   //右边的指针
        var temp;       //临时变量
        while(i != j){  //每一趟进行的多次比较和交换最终找到位置k
            while(arr[j]>=x && i<j){   //大于基准数,j指针向左移动,直到小于基准数
                j--;
            }
            while(arr[i]<=x && i<j){   //小于基准数,j指针向左移动,直到大于基准数
                i++;
            }
            if(i<j){
                temp = arr[i];arr[i] = arr[j];arr[j] = temp;
            }
        }
        arr[left] = arr[i];  //交换基准数和k位置的数
        arr[i] = x;
        quickSort(arr, left, i - 1);   //递归
        quickSort(arr, i + 1,right);   //递归
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr,0,arr.length-1));


还有一种方法实现

// 快排 quicksort 方法二
function quickSort2(arr){
    if(arr.length <= 1){return arr}
    var xIndex = Math.floor(arr.length/2);
    var x = arr.splice(xIndex, 1)[0];
    var left = [];
    var right = [];
    for(var i=0;i<arr.length;i++){
        if(arr[i] < x){
            left.push(arr[i]);
        }else{
            right.push(arr[i]);
        }
    }
    return quickSort2(left).concat([x], quickSort2(right));
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort2(arr,0,arr.length-1));

这种方法更容易理解,缺点肯定是空间复杂度升高了。


总结

这个是自己实现的,但是javascript中已经有了原生的sort原生。原生的实现还是比自己写的快。

最佳情况:O(nlogn)

最坏情况:O(n*2)

平均情况:O(nlogn)

quickSort



八、堆排序(heap sort)

一种利用堆概念来排序的选择排序,是一种不稳定的排序。

思路:利用堆这种数据结构设计的排序算法。堆积是一个近似完全的二叉树的结构,并且拥有堆积的性质:子节点的键值或者索引总小于(大于)它的父节点。

描述:(1)将初始待排序序列(r1,r2,r3,...,rn)构建成一个大顶堆,此堆为初始的无序区;(2)将堆顶元素r[1]与最后一个元素r[n]互换,此时得到新的无序区(r1,r2,r3,...,rn-1)和显得有序区(rn),且满足r[1,2,3,...,n-1]<=r[n];(3)由于交换后的新的堆顶r[1]可能违反堆的性质,因此需要对当前无序区(r1,r2,r3,...,rn-1)调整为新堆,然后再次将r[1]与无序区最后一个元素互换,得到新的无序区(r1,r2,r3,...,rn-2)和新的有序区(rn-1,rn),不断重复这个过程,直到有序区的元素个数为n-1,这样整个排序过程完成。

// 堆排序
function heapAdjust(arr, i, length){
    var left = 2*i+1;
    var right = 2*i+2;
    var largest = i;
    var temp;
    if(left<length && arr[largest]<arr[left]){
        largest = left;
    }
    if(right<length && arr[largest] < arr[right]){
        largest = right;
    }
    if(largest != i){
        temp = arr[i]; arr[i] = arr[largest]; arr[largest] = temp;
        heapAdjust(arr, largest, length);
    }
}
function heapSort(arr){
    // 建立大顶堆
    var heapSize = arr.length;
    var temp;
    for(var i=Math.floor(heapSize/2)-1;i>=0;i--){
        heapAdjust(arr,i,heapSize);
    }
    // 堆排序
    for(var j = heapSize-1;j>=1;j--){
        temp = arr[0]; arr[0] = arr[j]; arr[j] = temp;
        heapAdjust(arr,0,--heapSize);
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(heapSort(arr));


总结

最佳情况:O(nlogn)

最坏情况:O(nlogn)

平均情况:O(nlogn)

heapSort


九、计数排序(counting sort)

将输入的数据值转化为键存储在额外开辟的数组空间,一种线性的时间复杂度,一种稳定的排序。

注意:计数排序要求输入的数据必须是有确定范围的整数。

思路:计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中的值等于i的元素的个数,然后根据数组C来讲A中的元素排到正确的位置。只能对整数排序。

描述:(1)找出待排序数组中最大和最小的元素;(2)统计数组中每一个值为i的元素出现的次数,存入到数组c的第i项;(3)对所有的计数累加(从c的第一个元素开始,每一项和前一项相加);(4)反向填充到目标数组:将每一个元素 i 放在新数组的第C(i)项,每放一个元素就将c(i)减1。

// 计数排序 
function countingSort(arr){
    var length = arr.length;
    var B =[];
    var C = [];
    var min = max = arr[0];
    for(var i=0;i<length;i++){
        min = min <= arr[i]?min:arr[i];
        max = max >= arr[i]?max:arr[i];
        C[arr[i]] = C[arr[i]]?C[arr[i]] + 1 : 1;
    }
    for(var j = min;j < max;j++){
        C[j+1] = (C[j+1] || 0) + (C[j] || 0);
    }
    for(var k=length-1;k>=0;k--){
        B[C[arr[k]]-1] = arr[k];
        C[arr[k]]--;
    }
    return B;
}
var arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log(countingSort(arr));


总结

最佳情况:O(n+k)

最坏情况:O(n+k)

平均情况:O(n+k)

countingSort


十、桶排序(bucket sort)

桶排序其实是计数排序的升级。利用的是函数的映射关系,高效在于映射函数,一种稳定的排序。

思路:假设输入数据服从均匀分布,将数据分到有限的数量的桶里,每一个桶再分别排序(这时候可能会用到其他排序算法,也可以使用桶排序进行递归)

描述:(1)设置一个定量的数组作为空桶;(2)遍历输入数据,并且将数据一个一个方法哦对应的桶里去;(3)对每个不是空的桶进行排序;(4)从不是空桶里把排序好的数据拼接起来。

// 桶排序 
function bucketSort(array, num) {
    if (array.length <= 1) {
        return array;
    }
    var len = array.length, buckets = [], result = [], min = max = array[0], regex = '/^[1-9]+[0-9]*$/', space, n = 0;
    num = num || ((num > 1 && regex.test(num)) ? num : 10);
    console.time('桶排序耗时');
    for (var i = 1; i < len; i++) {
        min = min <= array[i] ? min : array[i];
        max = max >= array[i] ? max : array[i];
    }
    space = (max - min + 1) / num;
    for (var j = 0; j < len; j++) {
        var index = Math.floor((array[j] - min) / space);
        if (buckets[index]) {   //  非空桶,插入排序
            var k = buckets[index].length - 1;
            while (k >= 0 && buckets[index][k] > array[j]) {
                buckets[index][k + 1] = buckets[index][k];
                k--;
            }
            buckets[index][k + 1] = array[j];
        } else {    //空桶,初始化
            buckets[index] = [];
            buckets[index].push(array[j]);
        }
    }
    while (n < num) {
        result = result.concat(buckets[n]);
        n++;
    }
    console.timeEnd('桶排序耗时');
    return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bucketSort(arr,4));


总结:

最佳情况:O(n+k)

最坏情况:O(n+k)

平均情况:O(n+k)



十一、基数排序(radix sort)

基数排序也是非比较的排序算法,对每一位进行排序,从低位开始排序。一种稳定排序算法。

思路:基数排序是按照低位先排序,然后收集,再按照高位排序,然后再收集;依次类推,直到最高位;有时候有些属性是有优先级顺序的,先按照低优先级排序,再按照高优先级排序。最后顺序就是高优先级咋前,高优先级相同的低优先级高的在前。基数培训基于分别排序,分别手机,所以是稳定的。

描述:(1)取得数组中最大数,并取得位数;(2)arr为原数组,从最低位开始取每个位组成radix数组;(3)对radix进行计数排序(利用计数排序适用于小范围数的特点)。

// 基数排序 
function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    console.time('基数排序耗时');
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]== null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    console.timeEnd('基数排序耗时');
    return arr;
}
var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log(radixSort(arr,2)); 


总结

最佳情况:O(n*k)

最坏情况:O(n*k)

平均情况:O(n*k)

radioSort


基数排序和计数排序和桶排序

都利用了桶的概念,但是对桶的使用上有明显差异。

1、基数排序:根据键值的每位数来分配桶;

2、计数排序:每个桶只存储单一键值;

3、桶排序:每个通存储一定范围的数值。


十二、总结

排序算法其实还是很有研究的必要,虽然很多不咋用,但是有时候掌握算法的思路对于你在遇到其他问题时候可以触类旁通。作为一个算法的弱鸡,有问题的地方希望各位看官指导一下。参考资源来源于网络和自己觉得写的不好的地方。

感谢你的阅读,本文由 sau交流学习社区 版权所有。
如若转载,请注明出处:sau交流学习社区-power by saucxs(程新松)(/page/734.html)
交流咨询
    官方QQ群
    群号663940201,欢迎加入!
    sau交流学习社区交流群

图文推荐

saucxs聊天机器人
saucxs
hi ,欢迎来到sau交流学习社区,欢迎与我聊天,问我问题哦!