[路飞]前端算法——算法篇(一、排序算法): 初识十大排序算法

473 阅读3分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

前言

前端算法系列是我对算法学习的一个记录, 主要从常见算法数据结构算法思维常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流点赞收藏, 让我们共同进步, daydayup👊

目录地址:目录篇

相关代码地址: Github

相关视频地址: 哔哩哔哩-百日算法系列

一、算法分类

  • 比较类: 通过元素之间的比较来实现, 时间复杂度不能突破O(nlogn)
  • 非比较类: 不需要比较元素的大小

排序算法.png

二、算法复杂度

相关知识补充

  • 时间复杂度: 执行代码所需时间数据量之间的函数关系
  • 空间复杂度: 执行代码所需空间数据量之间的函数关系
  • 稳定性: 相等的两个数据排序之后是否顺序不变

849589-20180402133438219-1946132192.png

三、十大排序算法详解

在线排序算法演示

前言: 代码的实现方式可能略有不同、但主体的思想不变、为了使代码更加简洁抽离出部分功能代码

3.0 冒泡排序(Bubble Sort)

描述

依次比较前后两个元素、将较小的值移动到前面、同时较大的元素会冒泡到末尾

图解

849589-20171015223238449-2146169197.gif

代码

function bubbleSort(nums) {
    let len = nums.length
    for(let i=len; i>0; i--) {
        for(let j=0; j<i-1; j++) {
            if (nums[j] > nums[j+1]) temp(nums, j, j + 1)
        }
    }
}

// 数据解构版位置互换
function temp(nums, i, j) {
    [nums[i], nums[j]] = [nums[j], nums[i]]
}

3.1 快速排序(Quick Sort)

描述

选定一个基数(pivot)、将大于等于基数的放右边、小于基数的放左边、然后递归处理

图解

849589-20171015230936371-1413523412.gif

代码

// 本函数以数组的首个元素为基准
function quickSort(nums) {
    if (nums.length < 2) return nums
    let left = [], right = []
    while(nums.length > 1) {
        let num = nums.pop()
        if (num >= nums[0]) {
            right.push(num)
        } else {
            left.push(num)
        }
    }
    return [...quickSort(left), nums[0], ...quickSort(right)]
}

3.2 选择排序(Selection Sort)

描述

每次从数组中出最小的元素

图解

849589-20171015224719590-1433219824.gif

代码

function selectionSort(nums) {
    let i = 0, 
        len = nums.length
    while(i < len) {
        let min = getMinIdx(nums, i)
        temp(nums, i, min)
        i++
    }
    return nums
}

// 获取数组最小(注意范围)
function getMinIdx(nums, i) {
    let min = 0
    for(let j=i; j < nums.length; j++) {
        if (nums[j] < nums[min]) min = j
    }
    return min
}

3.3 堆排序(Heap Sort)

描述

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

图解

849589-20171015231308699-356134237.gif

代码

// TODO
var len;    // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
 
function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}
 
function heapify(arr, i) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;
 
    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }
 
    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }
 
    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
 
function heapSort(arr) {
    buildMaxHeap(arr);
 
    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

3.4 插入排序(Insertion Sort)

描述

从未排序数组中拿出来一个插入到已排序数组中、类比打扑克的起牌过程

图解

849589-20171015225645277-1151100000.gif

代码

/*
function insertionSort(nums) {
    for(let i=1; i<nums.length; i++) {
        let j = i - 1
        let num = nums[i]
        while(j > 0 && num < nums[j]) j--
        nums.splice(i, 1)
        nums.splice(j + 1, 0, num)
    }
    return nums
}
*/
for(let i=1; i<nums.length; i++) {
    let j = i - 1
    let num = nums[i]
    while(j >= 0 && num < nums[j]) j--
    nums.splice(i, 1)
    nums.splice(j + 1, 0, num)
}
return nums

3.5 希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

描述

插入排序的升级版、先对间隔为 n/2 的数据集比较、再对间隔为 n/2/2 的比较、直到最后比较整个数组

图解

849589-20180331170017421-364506073.gif

代码

function shellSort(nums) {
    let len = nums.length, gap = len
    while(gap = getGap(gap)) {
        for(let i=0; i<nums.length; i++) {
            let k = i, num = nums[i]
            while(k - gap >= 0 && num < nums[k - gap]) {
                nums[k] = nums[k - gap] // * 复习点
                k = k - gap
            }
            nums[k] = num
        }
    }
}

// 获取增量
function getGap(len) {
    return Math.floor(len/2)
}

3.6 归并排序(Merge Sort)

描述

经典分治算法、先分成两堆分别排序、然后再合并

图解

849589-20171015230557043-37375010.gif

代码

function mergeSort(nums) {
    if (nums.length < 2) return nums
    let i = Math.floor(nums.length / 2)
    return merge(
        mergeSort(nums.slice(0, i)),
        mergeSort(nums.slice(i))
    )
}

// 合并两个数组
function merge(l, r) {
    let res = []
    while(l.length || r.length) {
        if (!l.length || l[0] > r[0]) {
            res.push(r.shift())
        } else if (!r.length || l[0] <= r[0]) {
            res.push(l.shift())
        } else {
            //
        }
    }
    return res
}

3.7 计数排序(Counting Sort)

描述

将数据的值作为下标存储在新数组中并计数、然后再取出、计数排序要求输入的数据必须是有确定范围的整数

图解

849589-20171015231740840-6968181.gif

代码

function countingSort(nums, max) {
    let ans = []
    let res = new Array(max + 1)
    for(let i=0; i<nums.length; i++) {
        let k = nums[i]
        res[k] = res[k] ? (res[k] + 1) : 1 
    }
    for(let i=0; i<res.length; i++) {
        while (res[i]) {
            ans.push(i)
            res[i] -= 1
        }
    }
    return ans
}

3.8 桶排序(Bucket Sort)

描述

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

图解

代码

function bucketSort(nums) {
    const size = 10
    const [buckets, getHashMap] = HashMap(nums, size)

    for(let i=0; i<nums.length; i++) {
        let idx = getHashMap(nums[i])
        buckets[idx].push(nums[i])
    }

    nums.length = 0
    for(let i=0; i<buckets.length; i++) {
        if (buckets[i].length === 0) continue

        // 对桶内的数据排序 这里采用插入排序
        insertionSort(buckets[i])

        for(let j=0; j<buckets[i].length; j++) {
            nums.push(buckets[i][j])
        }
    }

    return nums
}

// 创建桶以及映射函数
function HashMap(nums, size) {
    let max = nums[0]
    let min = nums[0]
    let buckets = new Array(size)

    for(let i=1; i<nums.length; i++) {
        max = max > nums[i] ? max : nums[i]
        min = min < nums[i] ? min : nums[i]
    }
    
    for(let i=0; i<buckets.length; i++) {
        buckets[i] = []
    }

    let pivot = Math.floor((max - min) / size) + 1

    return [buckets, function(num) {
        return Math.floor(num / pivot)
    }]
}

// 插入排序
function insertionSort(nums) {
    for(let i=1; i<nums.length; i++) {
        let j = i - 1
        let num = nums[i]
        while(j > 0 && num < nums[j]) j--
        nums.splice(i, 1)
        nums.splice(j + 1, 0, num)
    }
    return nums
}

3.9 基数排序(Radix Sort)

描述

先比较个位、从0到9划分重组、再比较十位、依次比较

引用: 基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

图解

849589-20171015232453668-1397662527.gif

代码

function radixSort(nums) {
    let digit = getMax(nums)
    for(let i=0; i<digit; i++) {
        let newNums = []
        let res = new Array(10).fill(1).map(() => [])
        for(let j=0; j<nums.length; j++) {
            let level = getNum(nums[j], i)
            res[level].push(nums[j])
        }
        for(let k=0; k<10; k++) {
            while(res[k].length) {
                newNums.push(res[k].shift())
            }
        }
        nums = newNums
    }
    return nums
}

// 获取当前位的值
function getNum(val, i) {
    return (val % Math.pow(10, i + 1)) / Math.pow(10, i)
}

// 获取最大数与其位数
function getMax(nums) {
    let max = 0, digit = 0
    for(let i=1; i<nums.length; i++) {
        if (nums[i] > nums[max]) max = i
    }
    max = nums[max]
    digit = (max + '').length
    return digit
}

四、前端TOY版排序方法扩展

function setTimeOutSort(nums) {
    let res = []
    for(num of nums) {
        setTimeOut(() => res.push(num), num)
    }
    return res
}

五、个人笔记

1.选择排序和冒泡排序的最好时间复杂度为什么不一样

2.堆排序(TODO)、希尔排序、快速排序不熟练

3.每个算法的分析未写