排序算法及其複雜度(JavaScript實現)

排序算法經過了很長時間的演變,產生了很多種不同的方法,每種算法都有它特定的使用場合。本文總結了常用的排序算法及其JavaScript實現。
randomArray是一個隨機數組生成方法。

/**
* 生成隨機數組
* @param   {Number}  隨機數組大小
* @param   {Number}  最小值
* @param   {Number}  最大值
* @return  返回隨機數組
*/
function randomArray(len, min, max){    
    if(!len || Object.prototype.toString.call(len) !== '[object Number]' || len < 2){
        //默認隨機數組長度爲10
        len = 10;
    }

    if(!min || Object.prototype.toString.call(min) !== '[object Number]'){
        //默認最小值爲0
        min = 0;
    }

    if(!max || Object.prototype.toString.call(max) !== '[object Number]'){
        //默認最大值爲100
        max = 100;
    }

    if(min >= max){
        min = 0;
        max = 100;
    }

    var i, arr = [], rnd, diff = max - min;
    for(i = 0; i < len; i++){
        rnd = Math.random();
        arr.push(Math.round(rnd * diff) + min);
    }

    return arr;
}


冒泡排序

/**
* 冒泡排序
* 冒泡排序是穩定排序
* 冒泡排序空間複雜度O(1)
* 最優時間複雜度O(n),當序列已經排序好時,時間複雜度爲O(n)
* 最壞時間複雜度O(n^2),當序列是倒序時,時間複雜度爲O(n^2)
* 冒泡排序是一種交換排序
* 
* 冒泡排序原理:
* 在序列中對當前還未排序的數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。
* 即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。
*/
var BubbleSort = {
    /**
    * @param  {Array}  待排序數組
    */
    sort: function(arr){
        console.log(arr);

        var i, j, tmp, len = arr.length;
        for(i = 0; i < len; i++){       
            for(j = len - 1; j > i; j--){
                if(arr[j] < arr[j - 1]){
                    tmp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tmp;
                }
            }
        }

        console.log(arr);
    },

    /**
    * 冒泡排序優化
    * @param  {Array}  待排序數組
    */
    optimzeSort: function(arr){
        console.log(arr);

        var i, j, tmp, flag, len = arr.length;
        for(i = 0; i < len; i++){
            //用flag來判斷是否有交換
            flag = false;
            for(j = len - 1; j > i; j--){
                if(arr[j] < arr[j - 1]){
                    tmp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = tmp;
                    flag = true;
                }
            }

            //如果沒有交換,則數組已排序好,退出循環
            if(!flag){
                break;
            }
        }

        console.log(arr);
    }
};

冒泡排序測試

BubbleSort.sort(randomArray());


插入排序

插入排序分爲直接插入排序、希爾排序

var InserctionSort = {
    /**
    * 直接插入排序
    * 直接插入排序屬於穩定排序
    * 直接插入排序空間複雜度O(1)
    * 最優時間複雜度O(n),當待排序的數組已經排序好時,直接插入排序的時間複雜度爲O(n)
    * 最壞時間複雜度O(n^2),當待排序的數組是倒序時,直接插入排序的時間複雜度爲O(n^2)
    * 直接插入排序適用於數量比較少的數組排序
    * 
    * 基本原理:
    * 將一個記錄插入到已排序好的有序表中,從而得到一個新,記錄數增1的有序表。即:先將序列的第1個記錄看成是一個有序的子序列,然後從第2個記錄逐個進行插入,直至整個序列有序爲止。
    */
    directInsert: function(arr){
        console.log(arr);

        var i, j, tmp, min, len = arr.length;
        for(i = 0; i < len - 1; i++){
            min = i;
            for(j = i + 1; j < len; j++){
                if(arr[j] < arr[min]){
                    min = j;
                }
            }

            if(min !== i){
                tmp = arr[min];
                arr[min] = arr[i];
                arr[i] = tmp;
            }
        }

        console.log(arr);
    },

    /**
    * 二分插入排序是直接插入排序的改進版 
    * 二分插入排序是不穩定排序
    * 
    * 原理:
    * 將一個記錄插入到已排序好的有序序列中,從而得到一個新,記錄數增1的有序序列。
    * 二分插入排序用二分法找出新記錄在有序序列中的位置。
    */
    binaryInsert: function(arr){
        console.log(arr);

        var i, j, left, right, center, tmp, len = arr.length;
        for (i = 1; i < len; i++) {

            //如果新記錄小於有序序列的最大元素,則用二分法找出新紀錄在有序序列中的位置
            if (arr[i] < arr[i - 1]) {
                left = 0;
                right = i - 1;
                while (left < right) {
                    //獲取中間位置索引,把有序序列分成兩個子序列
                    center = Math.ceil((right + left) / 2);

                    if (arr[center] < arr[i]) {
                        //如果新紀錄大於中間位置記錄,則在右邊序列繼續進行二分
                        left = center + 1;
                    } else {
                        //如果新紀錄小於中間位置記錄,則在左邊序列繼續進行二分
                        right = center - 1;
                    }
                }

                tmp = arr[i];

                //把比arr[i]大的記錄往後移
                for (j = i; j > left; j--) {
                    arr[j] = arr[j - 1];
                }

                arr[left] = tmp;
            }
        }

        console.log(arr);
    },

    /**
    * 希爾排序
    * 希爾排序又叫縮小增量排序,是直接插入排序算法的一種更高效的改進版本
    * 希爾排序屬於不穩定排序
    * 希爾排序空間複雜度O(1)
    * 希爾排序的時間複雜度和其增量序列有關係,預計平均時間複雜度O(n^1.3)
    * 
    * 基本原理:
    * 先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
    */
    shell: function(arr){
        console.log(arr);

        var i, j, tmp, swap, len = arr.length, step = Math.floor(len / 2);      
        while(step > 0){
            for(i = step; i < len; i++){
                tmp = arr[i];
                swap = false;
                for(j = i - step; j >= 0 && arr[j] > tmp; j -= step){
                    swap = true;
                    arr[j + step] = arr[j];
                }

                if(swap){
                    arr[j + step] = tmp;
                }
            }

            step = Math.floor(step / 2);
        }

        console.log(arr);
    }
};

插入排序測試

// 直接插入排序
InserctionSort.directInsert(randomArray());
// 希爾排序
InserctionSort.shell(randomArray());


直接選擇排序

/**
* 直接選擇排序
* 直接選擇排序是一種不穩定的排序
* 時間複雜度爲 O(n^2),當記錄佔用字節數較多時,直接選擇排序通常比直接插入排序的執行速度快些。
* 空間複雜度爲O(1)
* 
* 原理:從未排序序列中找到最小元素,存放到已排序序列的末尾,以此類推,直到所有元素均排序完畢。
*/
var DirectSelectionSort = {
    sort: function(arr){
        console.log(arr);

        var i, j, min, tmp, len = arr.length;

        for(i = 0; i < len; i++){
            min = i;

            //從arr[i]~arr[len-1]中找出最小的記錄
            for(j = i + 1; j < len; j++){
                if(arr[min] > arr[j]){
                    min = j;
                }
            }

            //如果最小記錄不是arr[i],把arr[i]和arr[min]交換位置
            //使得arr[0]~arr[i]是有序序列,而且比arr[i+1]~arr[len-1]中任何記錄都小
            if(min != i){
                tmp = arr[i];
                arr[i] = arr[min];
                arr[min] = tmp;
            }
        }

        console.log(arr);
    }
};

直接選擇排序測試

DirectSelectionSort.sort(randomArray());


堆排序

/**
* 堆排序 堆排序是一種樹形選擇排序,是直接選擇排序的有效改進,是不穩定的排序
* 最優時間複雜度O(nlogn)
* 最壞時間複雜度O(nlogn)
* 空間複雜度O(1)
* 
* 堆排序原理:
* 1、根據序列建立大根堆(或者小根堆)。
* 2、將堆頂元素R[1]與最後一個元素R[n]交換,得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn),且滿足R[1,2...n-1]<=R[n]。
* 3、對當前無序區(R1,R2,......Rn-1)調整爲新堆,然後再次將R[1]與無序區最後一個元素交換,
*   得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。 不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。
*/
var HeapSort = {
    sort: function(arr){
        console.log(arr);

        var i, len = arr.length;

        //創建大根堆
        for (i = Math.ceil((len - 1) / 2); i >= 0; i--) {
            HeapSort.adjuestMaxHeap(arr, i, len);
        }

        for (i = len - 1; i > 0; i--) {
            //把大根堆的頂部元素移到arr[i]處,使得arr[i] ~ arr[len - 1]爲有序序列
            HeapSort.swap(arr, 0, i);

            //調整大根堆
            //大根堆的在數組中的位置爲0 ~ i
            HeapSort.adjuestMaxHeap(arr, 0, i);
        }

        console.log(arr);
    },

    /**
    * 交換數組中i,j位置的值
    */
    swap: function(arr, i, j){
        var tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    },

    /**
    * 調整大根堆
    * 
    * @param  {Array}   待排序數組
    * @param  {Number}  父節點在數組中的索引
    * @param  {Number}  調整堆的長度
    */
    adjuestMaxHeap: function(arr, i, size){
        var lchild = 2 * i + 1, // 左孩子在數組中的索引
            rchild = lchild + 1, // 右孩子在數組中的索引
            max = i; // 臨時變量,最大值索引

        while(lchild < size){
            if(arr[lchild] > arr[max]){
                max = lchild;
            }

            if (rchild < size && arr[rchild] > arr[max]) {
                max = rchild;
            }

            if (max !== i) {
                HeapSort.swap(arr, max, i);

                i = max;
                lchild = 2 * i + 1;
                rchild = lchild + 1;
            } else {
                break;
            }
        }
    }
};

堆排序測試

HeapSort.sort(randomArray());


快速排序

/**
 * 快速排序
 * 快速排序是不穩定排序
 * 快速排序是一種交換排序
 * 快速排序對序列的操作空間複雜度爲O(1),如果快速排序用遞歸實現,則遞歸棧的空間複雜度爲O(logn)~O(n)之間。
 * 最佳時間複雜度O(nlogn)
 * 平均時間複雜度O(nlogn) 
 * 快速排序是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短。
 * 
 * 快速排序原理:
 * 通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,
 * 然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
 */
var QuickSort = {
    sort: function(arr){
        console.log(arr);
        var start = 0, end = arr.length - 1;
        QuickSort.recursion(arr, 0, end);
        console.log(arr);
    },

    recursion: function(arr, start, end){
        var split;
        if(start < end){
            split = QuickSort.partition(arr, start, end);
            QuickSort.recursion(arr, start, split - 1);
            QuickSort.recursion(arr, split + 1, end);
        }
    },

    /**
    * 交換數組中i,j位置的值
    */
    swap: function(arr, i, j){
        var tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    },

    /**
    * 對序列進行排序
    * @param   {Array}   待排序數組
    * @param   {Number}  排序開始索引
    * @param   {Number}  排序結束索引
    * @return  {Number}  返回用於比較的基準元素的索引
    */
    partition: function(arr, start, end){
        var privot = arr[start];

        while(start < end){
            while(start < end && arr[end] >= privot){
                end--;
            }

            QuickSort.swap(arr, start, end);

            while(start < end && arr[start] <= privot){
                start++;
            }

            QuickSort.swap(arr, start, end);
        }

        return start;
    }
};

快速排序測試

QuickSort.sort(randomArray());


歸併排序

/**
 * 歸併排序是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法將已有序的子序列合併,最後得到完全有序的序列。
 * 歸併排序是穩定排序,速度僅次於快速排序
 * 時間複雜度爲O(nlogn)
 * 空間複雜度爲O(n) 歸併排序需要n空間的臨時數組來存儲子序列
 * 歸併排序原理:
 * 將待排序序列分爲若干個子序列,,對每個子序列進行排序。
 * 然後再把相鄰的兩個有序子序列合併,並排序成爲新的有序子序列。
 * 依次類推,最終把所有子序列合併成一個有序序列。
 * 
 */
var MergeSort = {
    sort: function(arr){
        console.log(arr);       
        MergeSort._sort(arr, 0, arr.length - 1);        
        console.log(arr);
    },

    _sort: function(arr, start, end){
        if(start >= end){
            return;
        }

        var middle = Math.floor((start + end) / 2);
        MergeSort._sort(arr, start, middle);
        MergeSort._sort(arr, middle + 1, end);
        MergeSort.merge(arr, start, middle, end);
    },

    /**
     * 有序的相鄰子序列合併
     * @param arr 數組
     * @param start 左邊序列開始索引
     * @param middle 左邊序列結束索引,middle+1是右邊序列開始索引
     * @param end 右邊序列結束索引
     */
    merge: function(arr, start, middle, end){
        //臨時數組長度
        var tmplen = end - start + 1;

        //臨時數組
        var tmp = [];

        //左邊數組的索引
        var left = start;

        //右邊數組的索引
        var right = middle + 1;

        //tmp數組的索引
        var i = 0;

        while(left <= middle && right <= end){
            if(arr[left] < arr[right]){
                tmp[i] = arr[left];
                left++;
            }else{
                tmp[i] = arr[right];
                right++;
            }

            i++;
        }

        while(left <= middle){
            tmp[i] = arr[left];
            left++;
            i++;
        }

        while(right <= end){
            tmp[i] = arr[right];
            right++;
            i++;
        }

        for(i = 0; i < tmplen; i++){
            arr[start + i] = tmp[i];
        }
    }
};

歸併排序測試

MergeSort.sort(randomArray());


總結

排序 是否穩定 最好時間複雜度 最壞時間複雜度 平均時間複雜度 空間複雜度
冒泡排序 穩定 O(n) O(n^2) O(n^2) O(1)
直接插入排序 穩定 O(n) O(n^2) O(n^2) O(1)
希爾排序 不穩定 O(n^1.3) O(n^1.3) O(n^1.3) O(1)
直接選擇排序 不穩定 O(n^2) O(n^2) O(n^2) O(1)
堆排序 不穩定 O(nlog2n) O(nlog2n) O(nlog2n) O(1)
快速排序 不穩定 O(nlog2n) O(n^2) O(nlog2n) O(nlog2n)
歸併排序 穩定 O(nlog2n) O(nlog2n) O(nlog2n) O(n)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章