常见排序算法及其JS实现

Posted 桥本环奈粤港澳分奈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见排序算法及其JS实现相关的知识,希望对你有一定的参考价值。

一、常见排序算法

常见排序算法有冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、基数排序、计数排序、桶排序

二、JS实现

1. 冒泡排序

主要思想: 对相邻的元素进行两两比较,顺序相反则进行交换,每一趟会将最小/大的元素浮到顶端,最终有序

function bubbleSort(array)  //冒泡排序
    if (!Array.isArray(array) || array.length <= 1) return;
    let lastIndex = array.length - 1;
    while (lastIndex > 0) 
        let flag = true;
        let k = lastIndex;
        for (let j = 0; j < k; j++) 
            if (array[j] > array[j + 1]) 
                flag = false;
                lastIndex = j;
                [array[j], array[j + 1]] = [array[j + 1], array[j]];
            
        
        if (flag) break;
    

优化后的冒泡排序,当排序序列为已排序序列时,为最好的时间复杂度为 O(n)。

冒泡排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

2. 选择排序

主要思想: 每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止。

function selectSort(array)  //选择排序
    let arr = array.slice(0)  // 复制数组
    if (!Array.isArray(arr) || arr.length <= 1) return;
    for (let i = 0; i < arr.length - 1; i++) 
        let minIndex = i;
        for (let j = i + 1; j < arr.length; j++)  // 每轮选出低i小的值
            if (arr[minIndex] > arr[j]) 
                minIndex = j
            
        
        [arr[minIndex], arr[i]] = [arr[i], arr[minIndex]] // 放在位置i 
    
    return arr

let array = [21, 45, 316, 3, 265, 266, 9];
console.log(selectSort(array)); // [3,   9,  21, 45, 265, 266, 316]

选择排序不管初始序列是否有序,时间复杂度都为 O(n²)。

选择排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,不是稳定排序。

3. 插入排序

主要思想: 直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

function insertSort(array)  //插入排序
    for (let i = 1; i < array.length; i++)  // 循环从 1 开始,0 位置为默认的已排序的序列
        temp = array[i] // 保存当前需要排序的元素
        j = i
            // 在当前已排序序列中比较,如果比需要排序的元素大,就依次往后移动位置
        while (j - 1 >= 0 && array[j - 1] > temp) 
            array[j] = array[j - 1]
            j--
        
        array[j] = temp // 将找到的位置插入元素
    

let array = [21, 45, 316, 3, 265, 266, 9];
insertSort(array);
console.log(array); // [3,   9,  21, 45, 265, 266, 316]

当排序序列为已排序序列时,为最好的时间复杂度 O(n)。

插入排序的平均时间复杂度为 O(n²) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(1) ,是稳定排序。

4. 希尔排序

主要思想: 把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。

function hillSort(array)  //希尔排序
    let length = array.length
    for (let gap = parseInt(length >> 1); gap >= 1; gap = parseInt(gap >> 1))  //gap为增量,每次增量大小减半
        for (let i = gap; i < length; i++) 
            temp = array[i]  // 保存当前需要排序的元素
            j = i
            while (j - gap >= 0 && array[j - gap] > temp)   // 在当前已排序的序列中比较,如果比需要排序的元素大,则后移位置
                array[j] = array[j - gap]
                j = j - gap
            
            array[j] = temp  // 插入元素
        
    


平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n^s) ,空间复杂度为 O(1) ,不是稳定排序。

5. 归并排序

主要思想: 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。递归的将数组两两分开直到只包含一个元素,然后将数组排序合并,最终合并为排序好的数组。

function merge(arr1, arr2)  //归并
    let i1 = 0
    let i2 = 0
    let res = []
        // 左右两个数组的元素依次比较,将较小的元素加入结果数组中,直到其中一个数组的元素全部加入完则停止
    while (i1 < arr1.length && i2 < arr2.length) 
        if (arr1[i1] <= arr2[i2]) 
            res.push(arr1[i1++])
         else 
            res.push(arr2[i2++])
        
    

    while (i1 < arr1.length)  // 如果是左边数组还有剩余,则把剩余的元素全部加入到结果数组中。
        res.push(arr1[i1++])
    
    while (i2 < arr2.length)  // 如果是右边数组还有剩余,则把剩余的元素全部加入到结果数组中。
        res.push(arr2[i2++])
    
    return res


function mergeSort(array)  //归并排序
    if (!Array.isArray(array) || array.length === 0) return
        // 将数组两两分开直到只包含一个元素
    if (array.length === 1) return array
    let mid = parseInt(array.length >> 1) // 找到中间索引值
    let left = array.slice(0, mid) // 截取左半部分
    let right = array.slice(mid, array.length) // 截取右半部分
    return merge(mergeSort(left), mergeSort(right)) //递归分解后,将数组排序合并


归并排序将整个排序序列看成一个二叉树进行分解,首先将树分解到每一个子节点,树的每一层都是一个归并排序的过程,每一层归并的时间复杂度为 O(n),因为整个树的高度为 lgn,所以归并排序的时间复杂度不管在什么情况下都为O(nlogn)。

归并排序的空间复杂度取决于递归的深度和用于归并时的临时数组,所以递归的深度为 logn,临时数组的大小为 n,所以归并排序的空间复杂度为 O(n)。

归并排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(nlogn) ,空间复杂度为 O(n) ,是稳定排序。

6. 快速排序

主要思想: 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

function quickSort(array, start, end)  //快速排序
    // 如果不是数组或者数组长度小于等于1,直接返回,不需要排序 
    if (!Array.isArray(array) || length <= 1 || start >= end) return
    let x = array[start] // 取第一个值为枢纽值,获取枢纽值的大小
    let i = start
    let j = end
    while (i < j) 
        while (i < j && array[j] >= x)  // 从右往左找到第一个比枢纽值小的值array[j]
            j--
        
        if (i < j)  // 将array[j]放到位置i, i右移
            array[i++] = array[j]
        
        while (i < j && array[i] <= x)  // 从左往右找到第一个比枢纽值大的值
            i++
        
        if (i < j) 
            array[j] = array[i] //将array[i]放到位置j
        
    
    array[i] = x // 将枢纽值x放在位置i,此时i往左的值全部比x小,i往右的值全部比x大
    quickSort(array, start, i - 1) // 快速排序左部分
    quickSort(array, i + 1, end) // 快速排序右部分


首先将第一个位置的数作为枢纽值,然后 end 指针向前移动,当遇到比枢纽值小的值或者 end 值等于 start 值的时候停止,然后将这个值填入 start 的位置,然后 start 指针向后移动,当遇到比枢纽值大的值或者start 值等于 end 值的时候停止,然后将这个值填入 end 的位置。反复循环这个过程,直到 start 的值等于 end 的值为止。将一开始保留的枢纽值填入这个位置,此时枢纽值左边的值都比枢纽值小,枢纽值右边的值都比枢纽值大。然后在递归左右两边的的序列。

当每次换分的结果为含 ⌊n/2⌋和 ⌈n/2⌉−1 个元素时,最好情况发生,此时递归的次数为 logn,然后每次划分的时间复杂度为 O(n),所以最优的时间复杂度为 O(nlogn)。一般来说只要每次换分都是常比例的划分,时间复杂度都为 O(nlogn)。

当每次换分的结果为 n-1 和 0 个元素时,最坏情况发生。划分操作的时间复杂度为 O(n),递归的次数为 n-1,所以最坏的时间复杂度为 O(n²)。所以当排序序列有序的时候,快速排序有可能被转换为冒泡排序。

快速排序的空间复杂度取决于递归的深度,所以最好的时候为 O(logn),最坏的时候为 O(n)。

快速排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(n²) ,空间复杂度为 O(logn) ,不是稳定排序。

7. 堆排序

主要思想:
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。

function heapSort(array)  //堆排序
    buildMaxHeap(array) //构建堆
    for (let i = array.length - 1; i >= 0; i--) 
        [array[0], array[i]] = [array[i], array[0]]
        heapify(array, 0, i) //每次拿到堆顶的最大值放到后面,再调整堆
    



function buildMaxHeap(array) 
    let length = array.length
    let iparent = parseInt(length >> 1) - 1 //找到最后一个非叶子节点
    for (let i = iparent; i >= 0; i--) 
        heapify(array, i, length) //循环调整子树
    


function heapify(array, index, length) 
    let imax = index
    let li = index * 2 + 1
    let ri = index * 2 + 2
    while (1) 
        imax = index
        li = index * 2 + 1
        ri = index * 2 + 2
        if (li < length && array[li] > array[imax]) 
            imax = li
        
        if (ri < length && array[ri] > array[imax]) 
            imax = ri
        
        if (imax !== index) 
            [array[imax], array[index]] = [array[index], array[imax]]
            index = imax
         else 
            break
        
    

建立堆的时间复杂度为 O(n),排序循环的次数为 n-1,每次调整堆的时间复杂度为 O(logn),因此堆排序的时间复杂度在不管什么情况下都是 O(nlogn)。

堆排序的平均时间复杂度为 O(nlogn) ,最坏时间复杂度为 O(nlogn) ,空间复杂度为 O(1) ,不是稳定排序。

8. 基数排序

主要思想: 将整数按位数切割成不同的数字,然后按每个位数分别比较。排序过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

function radixSort(array) 
    let length = array.length
    if (!Array.isArray(array) || length <= 1) return;
    let bucket = []
    let max = array[0]
    for (let i = 1; i < length; i++) 
        if (max < array[i]) max = array[i] // 先找到最大值
    

    let loop = (max + "").length // 确定位数 

    for (let i = 0; i < loop; i++) 
        for (let j = 0; j < length; j++) 
            let str = (array[j] + "").length
            if (str.length >= i + 1) 
                let k =
                    bucket[k]
            
        
    


基数排序的平均时间复杂度为 O(nk),k 为最大元素的长度,最坏时间复杂度为 O(nk),空间复杂度为 O(n) ,是稳定排序。


详细资料可以参考:
《常见排序算法 - 基数排序》
《排序算法之 基数排序 及其时间复杂度和空间复杂度》

算法总结可以参考:
《算法的时间复杂度和空间复杂度-总结》
《十大经典排序算法(动图演示)》
《各类排序算法的对比及实现》

以上是关于常见排序算法及其JS实现的主要内容,如果未能解决你的问题,请参考以下文章

选择排序的具体实现及其原理

快速排序(C语言)

归并排序算法及其JS实现

数据结构之八大排序算法(C语言实现)

快速排序算法原理及其js实现

堆排序原理及其js实现