重温前端10大排序算法(长文建议收藏)

注意:文章中排序算法性能比较都以实际状况为准。 vue

文中代码地址:github.com/collins999/…git

一、冒泡排序

思路

经过相邻元素的比较和交换,使得每一趟循环都能找到未有序数组的最大值或最小值。github

实现

created() {
    let array = [1, 6, 7, 4, 5, 8, 9, 0, 2, 3];
    let res = this.bubbleSort(array);
    console.log(res);
},
methods: {
    bubbleSort(arr) {
        let length = arr.length;
        if (!length) {
            return [];
        }
        for (let i = 0; i < length; i++) {
            for (let j = 0; j < length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                }
            }
            console.log(`第${i+1}次循环`, arr);
        }
        return arr;
    }
}
复制代码

优化:单向冒泡实现

标记在一轮比较汇总中,若是没有须要交换的数据,说明数组已经有序,能够减小排序循环的次数。web

created() {
    let array = [1, 6, 7, 4, 5, 8, 9, 0, 2, 3];
    let res = this.bubbleSort(array);
    console.log(res);
},
methods: {
    bubbleSort(arr) {
        let length = arr.length;
        if (!length) {
            return [];
        }
        for (let i = 0; i < length; i++) {
            let mark = true; // 若是在一轮比较中没有出现须要交互的数据,说明数组已经有序,
            for (let j = 0; j < length - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                    mark = false;
                }
            }
            console.log(mark);
            console.log(`第${i+1}次循环`, arr);
            if (mark) return;
        }
        return arr;
    }
}
复制代码

优化:双向冒泡实现

普通的冒泡排序,在一轮循环中只能找到最大值或者最小值的其中一个,双向冒泡排序则是多一轮的筛选,即找出最大值也找出最小值。算法

created() {
    let array = [1, 6, 7, 4, 5, 8, 9, 0, 2, 3];
    let res = this.bubbleSortTow(array);
    console.log(res);
},
methods: {
    bubbleSortTow(arr) {
        let low = 0;
        let high = arr.length - 1;
        while (low < high) {
            let mark = true;
            // 找到最大值放到右边
            for (let i = low; i < high; i++) {
                if (arr[i] > arr[i + 1]) {
                    [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
                    mark = false;
                }
            }
            high--;
            // 找到最小值放到左边
            for (let j = high; j > low; j--) {
                if (arr[j] < arr[j - 1]) {
                    [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
                    mark = false;
                }
            }
            low++;
            console.log(mark);
            console.log(`第${low}次循环`, arr);
            if (mark) return arr;
        }
    }
}
复制代码

性能比较

对三种排序的算法进行性能的比较:发现单向冒泡排序性能 > 双向冒泡排序性能 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)shell

提示

大数据量操做时,请勿模仿,页面已经卡死。 数组

二、选择排序

思路

依次找到剩余元素的最小值或者最大值,放置在末尾或者开头。bash

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    let res = this.selectSort(array);
    console.log(res);
},
methods: {
    /**
     * 选择排序
     */
    selectSort(arr) {
        let length = arr.length,
            minIndex;
        for(let i = 0; i < length -1; i++) {
            minIndex = i;
            for(let j = i+1; j < length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
            console.log(`第${i+1}次循环`, arr);
        }
        return arr;
    }
}
复制代码

性能比较

对于等量的数据进行性能比价,发现单向冒泡排序性能 > 双向冒泡排序性能 > 选择排序 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)markdown

提示

选择排序是时间复杂度上表现最稳定的算法之一,由于最快、最慢时间复杂度都是O(n²),用选择排序数据量越小越好。数据结构

三、插入排序

思路

以第一个元素为有序数组,其后的元素经过再这个已有序的数组中找到合适的元素并插入。

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    let res = this.insertSort(array);
    console.log(res);
},
methods: {
    /**
     * 插入排序
     */
    insertSort(arr) {
        let length = arr.length,
            preIndex, current;
        for (let i = 1; i < length; i++) {
            preIndex = i - 1;
            current = arr[i];
            // 和已经排序好的序列进行比较,插入到合适的位置
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + 1] = arr[preIndex];
                preIndex--;
            }
            arr[preIndex + 1] = current;
            console.log(`第${i}次循环`, arr);
        }
        return arr;
    }
}
复制代码

优化:拆半插入排序实现

在直接插入排序的基础上,在插入的时候运用了折半查找法查找要插入的位置,再进行插入。

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    let res = this.binsertSort(array);
    console.log(res);
},
methods: {
    /**
     * 拆半插入排序
     */
    binsertSort(arr) {
        let low, high, j, temp;
        for (let i = 1; i < arr.length; i++) {
            if (arr[i] < arr[i - 1]) {
                temp = arr[i];
                low = 0;
                high = i - 1;
                while (low <= high) {
                    let mid = Math.floor((low + high) / 2);
                    if (temp > arr[mid]) {
                        low = mid + 1;
                    } else {
                        high = mid - 1;
                    }
                }
                for (j = i; j > low; --j) {
                    arr[j] = arr[j - 1];
                }
                arr[j] = temp;
            }
            console.log(`第${i}次循环`, arr);
        }
        return arr;
    }
}
复制代码

性能比较

>=表示性能相差不大

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 选择排序 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

提示

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了。

四、希尔排序

思路

经过某个增量 gap,将整个序列分给若干组,从后往前进行组内成员的比较和交换,随后逐步缩小增量至 1。希尔排序相似于插入排序,只是一开始向前移动的步数从 1 变成了 gap。

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    let res = this.shellSort(array);
    console.log(res);
},
methods: {
    /**
     * 希尔排序
     */
    shellSort(arr) {
        let len = arr.length;
        // 初始步数
        let gap = parseInt(len / 2);
        // 逐渐缩小步数
        while (gap) {
            // 从第gap个元素开始遍历
            for (let i = gap; i < len; i++) {
                // 逐步其和前面其余的组成员进行比较和交换
                for (let j = i - gap; j >= 0; j -= gap) {
                    if (arr[j] > arr[j + gap]) {
                        [arr[j], arr[j + gap]] = [arr[j + gap], arr[j]];
                    } else {
                        break;
                    }
                }
            }
            gap = parseInt(gap / 2);
        }
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 希尔排序 >选择排序 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

五、归并排序

思路

递归将数组分为两个序列,有序合并这两个序列。做为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  1. 自上而下的递归(全部递归的方法均可以用迭代重写,因此就有了第2种方法)。
  2. 自下而上的迭代。

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    let res = this.mergeSort(array);
    console.log(res);
},
methods: {
    /**
     * 归并排序
     */
    mergeSort(arr) {
        let len = arr.length;
        if (len < 2) {
            return arr;
        }
        let middle = Math.floor(len / 2),
            left = arr.slice(0, middle),
            right = arr.slice(middle);
        console.log(`处理过程:`, arr);
        return this.merge(this.mergeSort(left), this.mergeSort(right));
    },
    /**
     * 归并排序辅助方法
     */
    merge(left, right) {
        let 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;
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 希尔排序 > 归并排序 > 选择排序 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

六、快速排序

思路

选择一个元素做为基数(一般是第一个元素),把比基数小的元素放到它左边,比基数大的元素放到它右边(至关于二分),再不断递归基数左右两边的序列。快速排序是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序的名字起的是简单粗暴,由于一听到这个名字你就知道它存在的意义,就是快,并且效率高! 它是处理大数据最快的排序算法之一了。

快速排序的最坏运行状况是O(n²),好比说顺序数列的快排。但它的平摊指望时间是O(n log n) ,且O(n log n)记号中隐含的常数因子很小,比复杂度稳定等于O(n log n)的归并排序要小不少。因此,对绝大多数顺序性较弱的随机数列而言,快速排序老是优于归并排序。

举例说明(详解)

例如对如下10个数进行排序: 6 1 2 7 9 3 4 5 10 8

  1. 以6为基准数(通常状况以第一个为基准数)
  2. 在初始状态下,数字6在序列的第一位,咱们第一轮的目的是将6移动到一个位置(K),使得K左边的数都<=6,K右边的数字都>=6。
  3. 为找到K的位置,咱们须要进行一个搜索过程,从右往左查找一个大于6的数字,位置为j,从左往右查找一个小于6的数字,位置为i,交互j和i上面的数字。
  4. j和i的位置继续移动,重复3步骤。
  5. 当j和i相等时,中止移动,移动到的位置就是位置K,将位置K的数组和6交换。此时6左边的数字都被6小,6右边的数字都比6大。
  6. 将6左边和右边的序列进行上诉操做。

详情更多在:blog.csdn.net/qq_40941722…

实现1

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.quickSort(array, 0, array.length - 1);
    console.log(res);
},
methods: {
    /**
     * 快速排序
     */
    quickSort(arr, begin, end) {
        if (begin > end) return arr;
        let temp = arr[begin],
            i = begin,
            j = end;
        while(i != j) {
            while(arr[j] >= temp && j > i) {
                j--;
            }
            while(arr[i] <= temp && j > i) {
                i++;
            }
            if (j > i) {
                [arr[i], arr[j]] = [arr[j], arr[i]];
            }
        }
        [arr[begin], arr[i]] = [arr[i], temp];
        console.log(`${arr[i]}做为基准点:`, arr);
        this.quickSort(arr, begin, i - 1);
        this.quickSort(arr, i + 1, end);
        return arr;
    },
}
复制代码

实现2

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.quickSortOne(array, 0, array.length - 1);
    console.log(res);
},
methods: {
    /**
     * 快速排序
     */
    quickSortOne(arr, left, right) {
        var len = arr.length,
            partitionIndex,
            left = typeof left != 'number' ? 0 : left,
            right = typeof right != 'number' ? len - 1 : right;

        if (left < right) {
            partitionIndex = this.partition(arr, left, right);
            console.log(`${arr[partitionIndex]}做为基准点:`, arr);
            this.quickSortOne(arr, left, partitionIndex - 1);
            this.quickSortOne(arr, partitionIndex + 1, right);
        }
        return arr;
    },
    /**
     * 快速排序辅助方法-寻找基准值
     */
    partition(arr, left, right) {
        let pivot = left,
            index = pivot + 1;
        for (let i = index; i <= right; i++) {
            if (arr[i] < arr[pivot]) {
                [arr[i], arr[index]] = [arr[index], arr[i]];
                index++;
            }
        }
        [arr[pivot], arr[index - 1]] = [arr[index - 1], arr[pivot]];
        return index - 1;
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 希尔排序 > 归并排序 > 选择排序 >= 快速排序1 > 快速排序2 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

注意

快排实现方法1,虽然代码相对看起来简单,可是在数据量较大时,会出现溢出问题。

七、堆排序

思路

说到堆排序,首先须要了解一种数据结构——堆。堆是一种彻底二叉树,这种结构一般能够用数组表示。在实际应用中,堆又能够分为最小堆和最大堆,二者的区别以下:

  • -max-heap property :对于全部除了根节点(root)的节点 i,A[Parent(i)]≥A[i]

  • -min-heap property :对于全部除了根节点(root)的节点 i,A[Parent(i)]≤A[i]

堆排序能够说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每一个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列
  • 小顶堆:每一个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.quickSortOne(array, 0, array.length - 1);
    console.log(res);
},
methods: {
    /**
     * 堆排序
     */
    heapSort(nums) {
        this.buildHeap(nums);
        // 循环n-1次,每次循环后交换堆顶元素和堆底元素并从新调整堆结构
        for (let i = nums.length - 1; i > 0; i--) {
            [nums[i], nums[0]] = [nums[0], nums[i]];
            this.adjustHeap(nums, 0, i);
            console.log(`${nums[i]}做为堆顶元素:`, nums);
        }
        return nums;
    },
    /**
     * 堆排序辅助方法
     */
    adjustHeap(nums, index, size) {
        // 交换后可能会破坏堆结构,须要循环使得每个父节点都大于左右结点
        while (true) {
            let max = index;
            let left = index * 2 + 1; // 左节点
            let right = index * 2 + 2; // 右节点
            if (left < size && nums[max] < nums[left]) max = left;
            if (right < size && nums[max] < nums[right]) max = right;
            // 若是左右结点大于当前的结点则交换,并再循环一遍判断交换后的左右结点位置是否破坏了堆结构(比左右结点小了)
            if (index !== max) {
                [nums[index], nums[max]] = [nums[max], nums[index]];
                index = max;
            } else {
                break;
            }
        }
    },
    /**
     * 堆排序辅助方法
     */
    buildHeap(nums) {
        // 注意这里的头节点是从0开始的,因此最后一个非叶子结点是 parseInt(nums.length/2)-1
        let start = parseInt(nums.length / 2) - 1;
        let size = nums.length;
        // 从最后一个非叶子结点开始调整,直至堆顶。
        for (let i = start; i >= 0; i--) {
            this.adjustHeap(nums, i, size);
        }
    }
}
复制代码

性能比较

在大数量量和小数据量不用的状况下,堆排序的相对性能排序表动比较大。和他自己的特色有关,虽然堆排序在实践中不经常使用,常常被快速排序的效率战胜,但堆排序的优势是与输入的数据无关,时间复杂度稳定在O(N*lgN),不像快排,最坏的状况下时间复杂度为O(N2)。

八、计数排序

思路

以数组元素值为键,出现次数为值存进一个临时数组,最后再遍历这个临时数组还原回原数组。由于 JavaScript 的数组下标是以字符串形式存储的,因此计数排序能够用来排列负数,但不能够排列小数。

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 做为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有肯定范围的整数。

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.countingSort(array);
    console.log(res);
},
methods: {
    /**
     * 计数排序
     */
    countingSort(nums) {
        let arr = [];
        let max = Math.max(...nums);
        let min = Math.min(...nums);
        // 装桶
        for (let i = 0, len = nums.length; i < len; i++) {
            let temp = nums[i];
            arr[temp] = arr[temp] + 1 || 1;
            console.log(`装桶键为${temp}-值为${arr[temp]}`, arr);
        }
        let index = 0;
        // 还原原数组
        for (let i = min; i <= max; i++) {
            while (arr[i] > 0) {
                nums[index++] = i;
                arr[i]--;
            }
        }
        return nums;
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 计数排序 > 希尔排序 > 归并排序 > 选择排序 >= 快速排序1 > 快速排序2 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

九、桶排序

思路

取 n 个桶,根据数组的最大值和最小值确认每一个桶存放的数的区间,将数组元素插入到相应的桶里,最后再合并各个桶。

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的肯定。 为了使桶排序更加高效,咱们须要作到这两点:

  • 在额外空间充足的状况下,尽可能增大桶的数量。
  • 使用的映射函数可以将输入的N个数据均匀的分配到K个桶中。

何时最快(Best Cases): 当输入的数据能够均匀的分配到每个桶中

何时最慢(Worst Cases): 当输入的数据被分配到了同一个桶中

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.bucketSort(array);
    console.log(res);
},
methods: {
    /**
     * 桶排序
     */
    bucketSort(nums) {
        // 桶的个数,只要是正数便可
        let num = 5;
        let max = Math.max(...nums);
        let min = Math.min(...nums);
        // 计算每一个桶存放的数值范围,至少为1,
        let range = Math.ceil((max - min) / num) || 1;
        // 建立二维数组,第一维表示第几个桶,第二维表示该桶里存放的数
        let arr = Array.from(Array(num)).map(() => Array().fill(0));
        nums.forEach(val => {
            // 计算元素应该分布在哪一个桶
            let index = parseInt((val - min) / range);
            // 防止index越界,例如当[5,1,1,2,0,0]时index会出现5
            index = index >= num ? num - 1 : index;
            let temp = arr[index];
            // 插入排序,将元素有序插入到桶中
            let j = temp.length - 1;
            while (j >= 0 && val < temp[j]) {
                temp[j + 1] = temp[j];
                j--;
            }
            temp[j + 1] = val;
            console.log(temp);
        });
        // 修改回原数组
        let res = [].concat.apply([], arr);
        nums.forEach((val, i) => {
            nums[i] = res[i];
        });
        return nums;
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 计数排序 > 桶排序 > 希尔排序 > 归并排序 > 选择排序 >= 快速排序1 > 快速排序2 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

10 、基数排序

思路

使用十个桶 0-9,把每一个数从低位到高位根据位数放到相应的桶里,以此循环最大值的位数次。但只能排列正整数,由于遇到负号和小数点没法进行比较。

基数排序有两种方法:

  • MSD 从高位开始进行排序
  • LSD 从低位开始进行排序

基数排序 vs 计数排序 vs 桶排序:

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差别:

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每一个桶只存储单一键值
  • 桶排序:每一个桶存储必定范围的数值

实现

created() {
    let array = [];
    for (let i = 0; i < 10; i++) {
        let number = Math.floor(Math.random() * 10);
        array.push(number);
    }
    console.log(array);
    let res = this.radixSort(array);
    console.log(res);
},
methods: {
    /**
     * 基数排序
     */
    radixSort(nums) {
        // 计算位数
        function getDigits(n) {
            let sum = 0;
            while (n) {
                sum++;
                n = parseInt(n / 10);
            }
            return sum;
        }
        // 第一维表示位数即0-9,第二维表示里面存放的值
        let arr = Array.from(Array(10)).map(() => Array());
        let max = Math.max(...nums);
        let maxDigits = getDigits(max);
        for (let i = 0, len = nums.length; i < len; i++) {
            // 用0把每个数都填充成相同的位数
            nums[i] = (nums[i] + '').padStart(maxDigits, 0);
            // 先根据个位数把每个数放到相应的桶里
            let temp = nums[i][nums[i].length - 1];
            arr[temp].push(nums[i]);
        }
        console.table(arr);
        // 循环判断每一个位数
        for (let i = maxDigits - 2; i >= 0; i--) {
            // 循环每个桶
            for (let j = 0; j <= 9; j++) {
                let temp = arr[j]
                let len = temp.length;
                // 根据当前的位数i把桶里的数放到相应的桶里
                while (len--) {
                    let str = temp[0];
                    temp.shift();
                    arr[str[i]].push(str);
                }
            }
        }
        // 修改回原数组
        let res = [].concat.apply([], arr);
        nums.forEach((val, index) => {
            nums[index] = +res[index];
        });
        return nums;
    }
}
复制代码

性能比较

在数据量至关的状况下:发现拆半插入排序 >= 单向冒泡排序性能 > 双向冒泡排序性能 > 插入排序 > 计数排序 > 桶排序 > 希尔排序 > 基数排序 >归并排序 > 选择排序 >= 快速排序1 > 快速排序2 > 大于普通冒泡性能。(产生时间具体取决于所使用的系统)

相关文章
相关标签/搜索