重溫前端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 > 大於普通冒泡性能。(產生時間具體取決於所使用的系統)

相關文章
相關標籤/搜索