欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

javascript中可能用得到的全部的排序算法

 更新時間:2020年03月05日 22:09:19   作者:louis  
因此本篇重拾了出鏡概率比較高的十來種排序算法, 逐一分析其排序思想, 并批注注意事項. 歡迎對算法提出改進(jìn)和討論

導(dǎo)讀

排序算法可以稱得上是我的盲點, 曾幾何時當(dāng)我知道Chrome的Array.prototype.sort使用了快速排序時, 我的內(nèi)心是奔潰的(啥是快排, 我只知道冒泡啊?!), 要知道學(xué)習(xí)一門技術(shù)最好的時間是三年前, 但愿我現(xiàn)在補習(xí)還來得及(捂臉).

因此本篇重拾了出鏡概率比較高的十來種排序算法, 逐一分析其排序思想, 并批注注意事項. 歡迎對算法提出改進(jìn)和討論.

冒泡排序

冒泡排序需要兩個嵌套的循環(huán). 其中, 外層循環(huán)移動游標(biāo); 內(nèi)層循環(huán)遍歷游標(biāo)及之后(或之前)的元素, 通過兩兩交換的方式, 每次只確保該內(nèi)循環(huán)結(jié)束位置排序正確, 然后內(nèi)層循環(huán)周期結(jié)束, 交由外層循環(huán)往后(或前)移動游標(biāo), 隨即開始下一輪內(nèi)層循環(huán), 以此類推, 直至循環(huán)結(jié)束.

Tips: 由于冒泡排序只在相鄰元素大小不符合要求時才調(diào)換他們的位置, 它并不改變相同元素之間的相對順序, 因此它是穩(wěn)定的排序算法.

由于有兩層循環(huán), 因此可以有四種實現(xiàn)方式.

方案 外層循環(huán) 內(nèi)層循環(huán)
1 正序 正序
2 正序 逆序
3 逆序 正序
4 逆序 逆序

四種不同循環(huán)方向, 實現(xiàn)方式略有差異.

如下是動圖效果(對應(yīng)于方案1: 內(nèi)/外層循環(huán)均是正序遍歷.

冒泡排序

如下是上圖的算法實現(xiàn)(對應(yīng)方案一: 內(nèi)/外層循環(huán)均是正序遍歷).

//先將交換元素部分抽象出來
function swap(i,j,array){
 var temp = array[j];
 array[j] = array[i];
 array[i] = temp;
}
function bubbleSort(array) {
 var length = array.length, isSwap;
 for (var i = 0; i < length; i++) { //正序
 isSwap = false;
 for (var j = 0; j < length - 1 - i; j++) { //正序
 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
 }
 if(!isSwap)
 break;
 }
 return array;
}

以上, 排序的特點就是: 靠后的元素位置先確定.

方案二: 外循環(huán)正序遍歷, 內(nèi)循環(huán)逆序遍歷, 代碼如下:

function bubbleSort(array) {
 var length = array.length, isSwap;
 for (var i = 0; i < length; i++) { //正序
 isSwap = false;
 for (var j = length - 1; j >= i+1; j--) { //逆序
 array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
 }
 if(!isSwap)
 break;
 }
 return array;
}

以上, 靠前的元素位置先確定.

方案三: 外循環(huán)逆序遍歷, 內(nèi)循環(huán)正序遍歷, 代碼如下:

function bubbleSort(array) {
 var length = array.length, isSwap;
 for (var i = length - 1; i >= 0; i--) { //逆序
 isSwap = false;
 for (var j = 0; j < i; j++) { //正序
 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
 }
 if(!isSwap)
 break;
 }
 return array;
}

以上, 由于內(nèi)循環(huán)是正序遍歷, 因此靠后的元素位置先確定.

方案四: 外循環(huán)逆序遍歷, 內(nèi)循環(huán)逆序遍歷, 代碼如下:

function bubbleSort(array) {
 var length = array.length, isSwap;
 for (var i = length - 1; i >= 0; i--) { //逆序
 isSwap = false;
 for (var j = length - 1; j >= length - 1 - i; j--) { //逆序
 array[j] < array[j-1] && (isSwap = true) && swap(j,j-1,array);
 }
 if(!isSwap)
 break;
 }
 return array;
}

以上, 由于內(nèi)循環(huán)是逆序遍歷, 因此靠前的元素位置先確定.

以下是其算法復(fù)雜度:

平均時間復(fù)雜度 最好情況 最壞情況 空間復(fù)雜度
O(n²) O(n) O(n²) O(1)

冒泡排序是最容易實現(xiàn)的排序, 最壞的情況是每次都需要交換, 共需遍歷并交換將近n²/2次, 時間復(fù)雜度為O(n²). 最佳的情況是內(nèi)循環(huán)遍歷一次后發(fā)現(xiàn)排序是對的, 因此退出循環(huán), 時間復(fù)雜度為O(n). 平均來講, 時間復(fù)雜度為O(n²). 由于冒泡排序中只有緩存的temp變量需要內(nèi)存空間, 因此空間復(fù)雜度為常量O(1).

雙向冒泡排序

雙向冒泡排序是冒泡排序的一個簡易升級版, 又稱雞尾酒排序. 冒泡排序是從低到高(或者從高到低)單向排序, 雙向冒泡排序顧名思義就是從兩個方向分別排序(通常, 先從低到高, 然后從高到低). 因此它比冒泡排序性能稍好一些.

如下是算法實現(xiàn):

function bothwayBubbleSort(array){
 var tail = array.length-1, i, isSwap = false;
 for(i = 0; i < tail; tail--){
 for(var j = tail; j > i; j--){ //第一輪, 先將最小的數(shù)據(jù)冒泡到前面
 array[j-1] > array[j] && (isSwap = true) && swap(j,j-1,array);
 }
 i++;
 for(j = i; j < tail; j++){ //第二輪, 將最大的數(shù)據(jù)冒泡到后面
 array[j] > array[j+1] && (isSwap = true) && swap(j,j+1,array);
 }
 }
 return array;
}

選擇排序

從算法邏輯上看, 選擇排序是一種簡單且直觀的排序算法. 它也是兩層循環(huán). 內(nèi)層循環(huán)就像工人一樣, 它是真正做事情的, 內(nèi)層循環(huán)每執(zhí)行一遍, 將選出本次待排序的元素中最小(或最大)的一個, 存放在數(shù)組的起始位置. 而 外層循環(huán)則像老板一樣, 它告訴內(nèi)層循環(huán)你需要不停的工作, 直到工作完成(也就是全部的元素排序完成).

Tips: 選擇排序每次交換的元素都有可能不是相鄰的, 因此它有可能打破原來值為相同的元素之間的順序. 比如數(shù)組[2,2,1,3], 正向排序時, 第一個數(shù)字2將與數(shù)字1交換, 那么兩個數(shù)字2之間的順序?qū)⒑驮瓉淼捻樞虿灰恢? 雖然它們的值相同, 但它們相對的順序卻發(fā)生了變化. 我們將這種現(xiàn)象稱作 不穩(wěn)定性 .

如下是動圖效果:

選擇排序

如下是上圖的算法實現(xiàn):

function selectSort(array) {
 var length = array.length, min;
 for (var i = 0; i < length - 1; i++) {
 min = i;
 for (var j = i + 1; j < length; j++) {
 array[j] < array[min] && (min = j); //記住最小數(shù)的下標(biāo)
 }
 min!=i && swap(i,min,array);
 }
 return array;
}

以下是其算法復(fù)雜度:

平均時間復(fù)雜度 最好情況 最壞情況 空間復(fù)雜度
O(n²) O(n²) O(n²) O(1)

選擇排序的簡單和直觀名副其實, 這也造就了它”出了名的慢性子”, 無論是哪種情況, 哪怕原數(shù)組已排序完成, 它也將花費將近n²/2次遍歷來確認(rèn)一遍. 即便是這樣, 它的排序結(jié)果也還是不穩(wěn)定的. 唯一值得高興的是, 它并不耗費額外的內(nèi)存空間.

插入排序

插入排序的設(shè)計初衷是往有序的數(shù)組中快速插入一個新的元素. 它的算法思想是: 把要排序的數(shù)組分為了兩個部分, 一部分是數(shù)組的全部元素(除去待插入的元素), 另一部分是待插入的元素; 先將第一部分排序完成, 然后再插入這個元素. 其中第一部分的排序也是通過再次拆分為兩部分來進(jìn)行的.

插入排序由于操作不盡相同, 可分為 直接插入排序 , 折半插入排序(又稱二分插入排序), 鏈表插入排序 , 希爾排序 .

直接插入排序

它的基本思想是: 將待排序的元素按照大小順序, 依次插入到一個已經(jīng)排好序的數(shù)組之中, 直到所有的元素都插入進(jìn)去.

如下是動圖效果:

直接插入排序

如下是上圖的算法實現(xiàn):

function directInsertionSort(array) {
 var length = array.length, index, current;
 for (var i = 1; i < length; i++) {
 index = i - 1; //待比較元素的下標(biāo)
 current = array[i]; //當(dāng)前元素
 while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當(dāng)前元素大
 array[index+1] = array[index]; //將待比較元素后移一位
 index--;  //游標(biāo)前移一位
 //console.log(array);
 }
 if(index+1 != i){  //避免同一個元素賦值給自身
 array[index+1] = current; //將當(dāng)前元素插入預(yù)留空位
 //console.log(array);
 } 
 }
 return array;
}

為了更好的觀察到直接插入排序的實現(xiàn)過程, 我們不妨將上述代碼中的注釋部分加入. 以數(shù)組 [5,4,3,2,1] 為例, 如下便是原數(shù)組的演化過程.

可見, 數(shù)組的各個元素, 從后往前, 只要比前面的元素小, 都依次插入到了合理的位置.

Tips: 由于直接插入排序每次只移動一個元素的位置, 并不會改變值相同的元素之間的排序, 因此它是一種穩(wěn)定排序.

折半插入排序

折半插入排序是直接插入排序的升級版. 鑒于插入排序第一部分為已排好序的數(shù)組, 我們不必按順序依次尋找插入點, 只需比較它們的中間值與待插入元素的大小即可.

Tips: 同直接插入排序類似, 折半插入排序每次交換的是相鄰的且值為不同的元素, 它并不會改變值相同的元素之間的順序. 因此它是穩(wěn)定的.

算法基本思想是:

  1. 取0 ~ i-1的中間點( m = (i-1)>>1 ), array[i] 與 array[m] 進(jìn)行比較, 若array[i] < array[m] , 則說明待插入的元素array[i] 應(yīng)該處于數(shù)組的 0 ~ m 索引之間; 反之, 則說明它應(yīng)該處于數(shù)組的 m ~ i-1 索引之間.
  2. 重復(fù)步驟1, 每次縮小一半的查找范圍, 直至找到插入的位置.
  3. 將數(shù)組中插入位置之后的元素全部后移一位.
  4. 在指定位置插入第 i 個元素.

注: x>>1 是位運算中的右移運算, 表示右移一位, 等同于x除以2再取整, 即 x>>1 == Math.floor(x/2) .

如下是算法實現(xiàn):

function binaryInsertionSort(array){
 var current, i, j, low, high, m;
 for(i = 1; i < array.length; i++){
 low = 0;
 high = i - 1;
 current = array[i];

 while(low <= high){ //步驟1&2:折半查找
 m = (low + high)>>1;
 if(array[i] >= array[m]){//值相同時, 切換到高半?yún)^(qū),保證穩(wěn)定性
 low = m + 1; //插入點在高半?yún)^(qū)
 }else{
 high = m - 1; //插入點在低半?yún)^(qū)
 }
 }
 for(j = i; j > low; j--){ //步驟3:插入位置之后的元素全部后移一位
 array[j] = array[j-1];
 }
 array[low] = current; //步驟4:插入該元素
 }
 return array;
}

為了便于對比, 同樣以數(shù)組 [5,4,3,2,1] 舉例🌰. 原數(shù)組的演化過程如下(與上述一樣):

折半插入排序

雖然折半插入排序明顯減少了查詢的次數(shù), 但是數(shù)組元素移動的次數(shù)卻沒有改變. 它們的時間復(fù)雜度都是O(n²).

希爾排序

希爾排序也稱縮小增量排序, 它是直接插入排序的另外一個升級版, 實質(zhì)就是分組插入排序. 希爾排序以其設(shè)計者希爾(Donald Shell)的名字命名, 并于1959年公布.

算法的基本思想:

  1. 將數(shù)組拆分為若干個子分組, 每個分組由相距一定”增量”的元素組成. 比方說將[0,1,2,3,4,5,6,7,8,9,10]的數(shù)組拆分為”增量”為5的分組, 那么子分組分別為 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
  2. 然后對每個子分組應(yīng)用直接插入排序.
  3. 逐步減小”增量”, 重復(fù)步驟1,2.
  4. 直至”增量”為1, 這是最后一個排序, 此時的排序, 也就是對全數(shù)組進(jìn)行直接插入排序.

如下是排序的示意圖:

希爾排序示意圖

可見, 希爾排序?qū)嶋H上就是不斷的進(jìn)行直接插入排序, 分組是為了先將局部元素有序化. 因為直接插入排序在元素基本有序的狀態(tài)下, 效率非常高. 而希爾排序呢, 通過先分組后排序的方式, 制造了直接插入排序高效運行的場景. 因此希爾排序效率更高.

我們試著抽象出共同點, 便不難發(fā)現(xiàn)上述希爾排序的第四步就是一次直接插入排序, 而希爾排序原本就是從”增量”為n開始, 直至”增量”為1, 循環(huán)應(yīng)用直接插入排序的一種封裝. 因此直接插入排序就可以看做是步長為1的希爾排序. 為此我們先來封裝下直接插入排序.

//形參增加步數(shù)gap(實際上就相當(dāng)于gap替換了原來的數(shù)字1)
function directInsertionSort(array, gap) {
 gap = (gap == undefined) ? 1 : gap; //默認(rèn)從下標(biāo)為1的元素開始遍歷
 var length = array.length, index, current;
 for (var i = gap; i < length; i++) {
 index = i - gap; //待比較元素的下標(biāo)
 current = array[i]; //當(dāng)前元素
 while(index >= 0 && array[index] > current) { //前置條件之一:待比較元素比當(dāng)前元素大
 array[index + gap] = array[index]; //將待比較元素后移gap位
 index -= gap;  //游標(biāo)前移gap位
 }
 if(index + gap != i){  //避免同一個元素賦值給自身
 array[index + gap] = current; //將當(dāng)前元素插入預(yù)留空位
 }
 }
 return array;
}

那么希爾排序的算法實現(xiàn)如下:

function shellSort(array){
 var length = array.length, gap = length>>1, current, i, j;
 while(gap > 0){
 directInsertionSort(array, gap); //按指定步長進(jìn)行直接插入排序
 gap = gap>>1;
 }
 return array;
}

同樣以數(shù)組[5,4,3,2,1] 舉例🌰. 原數(shù)組的演化過程如下:

希爾排序

對比上述直接插入排序和折半插入排序, 數(shù)組元素的移動次數(shù)由14次減少為7次. 通過拆分原數(shù)組為粒度更小的子數(shù)組, 希爾排序進(jìn)一步提高了排序的效率.

不僅如此, 以上步長設(shè)置為了 {N/2, (N/2)/2, …, 1}. 該序列即希爾增量, 其它的增量序列 還有Hibbard:{1, 3, …, 2^k-1}. 通過合理調(diào)節(jié)步長, 還能進(jìn)一步提升排序效率. 實際上已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,…). 該序列中的項或者是9*4^i - 9*2^i + 1或者是4^i - 3*2^i + 1. 具體請戳 希爾排序-維基百科 .

Tips: 我們知道, 單次直接插入排序是穩(wěn)定的, 它不會改變相同元素之間的相對順序, 但在多次不同的插入排序過程中, 相同的元素可能在各自的插入排序中移動, 可能導(dǎo)致相同元素相對順序發(fā)生變化. 因此, 希爾排序并不穩(wěn)定.

歸并排序

歸并排序建立在歸并操作之上, 它采取分而治之的思想, 將數(shù)組拆分為兩個子數(shù)組, 分別排序, 最后才將兩個子數(shù)組合并; 拆分的兩個子數(shù)組, 再繼續(xù)遞歸拆分為更小的子數(shù)組, 進(jìn)而分別排序, 直到數(shù)組長度為1, 直接返回該數(shù)組為止.

Tips: 歸并排序嚴(yán)格按照從左往右(或從右往左)的順序去合并子數(shù)組, 它并不會改變相同元素之間的相對順序, 因此它也是一種穩(wěn)定的排序算法.

如下是動圖效果:

歸并排序

歸并排序可通過兩種方式實現(xiàn):

  1. 自上而下的遞歸
  2. 自下而上的迭代

如下是算法實現(xiàn)(方式1:遞歸):

function mergeSort(array) { //采用自上而下的遞歸方法
 var length = array.length;
 if(length < 2) {
 return array;
 }
 var m = (length >> 1),
 left = array.slice(0, m),
 right = array.slice(m); //拆分為兩個子數(shù)組
 return merge(mergeSort(left), mergeSort(right));//子數(shù)組繼續(xù)遞歸拆分,然后再合并
}
function merge(left, right){ //合并兩個子數(shù)組
 var result = [];
 while (left.length && right.length) {
 var item = left[0] <= right[0] ? left.shift() : right.shift();//注意:判斷的條件是小于或等于,如果只是小于,那么排序?qū)⒉环€(wěn)定.
 result.push(item);
 }
 return result.concat(left.length ? left : right);
}

由上, 長度為n的數(shù)組, 最終會調(diào)用mergeSort函數(shù)2n-1次. 通過自上而下的遞歸實現(xiàn)的歸并排序, 將存在堆棧溢出的風(fēng)險. 親測各瀏覽器的堆棧溢出所需的遞歸調(diào)用次數(shù)大致為:

Chrome v55: 15670
Firefox v50: 44488
Safari v9.1.2: 50755

以下是測試代碼:

function computeMaxCallStackSize() {
 try {
 return 1 + computeMaxCallStackSize();
 } catch (e) {
 // Call stack overflow
 return 1;
 }
}
var time = computeMaxCallStackSize();
console.log(time);

為此, ES6規(guī)范中提出了尾調(diào)優(yōu)化的思想: 如果一個函數(shù)的最后一步也是一個函數(shù)調(diào)用, 那么該函數(shù)所需要的棧空間將被釋放, 它將直接進(jìn)入到下次調(diào)用中, 最終調(diào)用棧里只保留最后一次的調(diào)用記錄.

雖然ES6規(guī)范如此誘人, 然而目前并沒有瀏覽器支持尾調(diào)優(yōu)化, 相信在不久的將來, 尾調(diào)優(yōu)化就會得到主流瀏覽器的支持.

以下是其算法復(fù)雜度:

平均時間復(fù)雜度 最好情況 最壞情況 空間復(fù)雜度
O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n)

從效率上看, 歸并排序可算是排序算法中的”佼佼者”. 假設(shè)數(shù)組長度為n, 那么拆分?jǐn)?shù)組共需logn步, 又每步都是一個普通的合并子數(shù)組的過程, 時間復(fù)雜度為O(n), 故其綜合時間復(fù)雜度為O(nlogn). 另一方面, 歸并排序多次遞歸過程中拆分的子數(shù)組需要保存在內(nèi)存空間, 其空間復(fù)雜度為O(n).

快速排序

快速排序借用了分治的思想, 并且基于冒泡排序做了改進(jìn). 它由C. A. R. Hoare在1962年提出. 它將數(shù)組拆分為兩個子數(shù)組, 其中一個子數(shù)組的所有元素都比另一個子數(shù)組的元素小, 然后對這兩個子數(shù)組再重復(fù)進(jìn)行上述操作, 直到數(shù)組不可拆分, 排序完成.

如下是動圖效果:

快速排序

如下是算法實現(xiàn):

function quickSort(array, left, right) {
 var partitionIndex,
 left = typeof left == 'number' ? left : 0,
 right = typeof right == 'number' ? right : array.length-1;
 if (left < right) {
 partitionIndex = partition(array, left, right);//切分的基準(zhǔn)值
 quickSort(array, left, partitionIndex-1);
 quickSort(array, partitionIndex+1, right);
 }
 return array;
}
function partition(array, left ,right) { //分區(qū)操作
 for (var i = left+1, j = left; i <= right; i++) {//j是較小值存儲位置的游標(biāo)
 array[i] < array[left] && swap(i, ++j, array);//以第一個元素為基準(zhǔn)
 }
 swap(left, j, array); //將第一個元素移至中間
 return j;
}

以下是其算法復(fù)雜度:

平均時間復(fù)雜度 最好情況 最壞情況 空間復(fù)雜度
O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n)

快速排序排序效率非常高. 雖然它運行最糟糕時將達(dá)到O(n²)的時間復(fù)雜度, 但通常, 平均來看, 它的時間復(fù)雜為O(nlogn), 比同樣為O(nlogn)時間復(fù)雜度的歸并排序還要快. 快速排序似乎更偏愛亂序的數(shù)列, 越是亂序的數(shù)列, 它相比其他排序而言, 相對效率更高. 之前在 捋一捋JS的數(shù)組 一文中就提到: Chrome的v8引擎為了高效排序, 在排序數(shù)據(jù)超過了10條時, 便會采用快速排序. 對于10條及以下的數(shù)據(jù)采用的便是插入排序.

Tips: 同選擇排序相似, 快速排序每次交換的元素都有可能不是相鄰的, 因此它有可能打破原來值為相同的元素之間的順序. 因此, 快速排序并不穩(wěn)定.

堆排序

1991年的計算機先驅(qū)獎獲得者、斯坦福大學(xué)計算機科學(xué)系教授羅伯特·弗洛伊德(Robert W.Floyd) 和威廉姆斯(J.Williams) 在1964年共同發(fā)明了著名的堆排序算法(Heap Sort).

堆排序是利用堆這種數(shù)據(jù)結(jié)構(gòu)所設(shè)計的一種排序算法. 它是選擇排序的一種. 堆分為大根堆和小根堆. 大根堆要求每個子節(jié)點的值都不大于其父節(jié)點的值, 即array[childIndex] <= array[parentIndex], 最大的值一定在堆頂. 小根堆與之相反, 即每個子節(jié)點的值都不小于其父節(jié)點的值, 最小的值一定在堆頂. 因此我們可使用大根堆進(jìn)行升序排序, 使用小根堆進(jìn)行降序排序.

并非所有的序列都是堆, 對于序列k1, k2,…kn, 需要滿足如下條件才行:

ki <= k(2i) 且 ki<=k(2i+1)(1≤i≤ n/2), 即為小根堆, 將<=換成>=, 那么則是大根堆. 我們可以將這里的堆看作完全二叉樹, k(i) 相當(dāng)于是二叉樹的非葉子節(jié)點, k(2i) 則是左子節(jié)點, k(2i+1)是右子節(jié)點.

算法的基本思想(以大根堆為例):

  1. 先將初始序列K[1..n]建成一個大根堆, 此堆為初始的無序區(qū).
  2. 再將關(guān)鍵字最大的記錄K1 (即堆頂)和無序區(qū)的最后一個記錄K[n]交換, 由此得到新的無序區(qū)K[1..n-1]和有序區(qū)K[n], 且滿足K[1..n-1].keys≤K[n].key
  3. 交換K1 和 K[n] 后, 堆頂可能違反堆性質(zhì), 因此需將K[1..n-1]調(diào)整為堆. 然后重復(fù)步驟2, 直到無序區(qū)只有一個元素時停止.

如下是動圖效果:

如下是算法實現(xiàn):

function heapAdjust(array, i, length) {//堆調(diào)整
 var left = 2 * i + 1,
 right = 2 * i + 2,
 largest = i;
 if (left < length && array[largest] < array[left]) {
 largest = left;
 }
 if (right < length && array[largest] < array[right]) {
 largest = right;
 }
 if (largest != i) {
 swap(i, largest, array);
 heapAdjust(array, largest, length);
 }
}
function heapSort(array) {
 //建立大頂堆
 length = array.length;
 for (var i = length>>1; i >= 0; i--) {
 heapAdjust(array, i, length);
 }
 //調(diào)換第一個與最后一個元素,重新調(diào)整為大頂堆
 for (var i = length - 1; i > 0; i--) {
 swap(0, i, array);
 heapAdjust(array, 0, --length);
 }
 return array;
}

以上, ①建立堆的過程, 從length/2 一直處理到0, 時間復(fù)雜度為O(n);

②調(diào)整堆的過程是沿著堆的父子節(jié)點進(jìn)行調(diào)整, 執(zhí)行次數(shù)為堆的深度, 時間復(fù)雜度為O(lgn);

③堆排序的過程由n次第②步完成, 時間復(fù)雜度為O(nlgn).

Tips: 由于堆排序中初始化堆的過程比較次數(shù)較多, 因此它不太適用于小序列. 同時由于多次任意下標(biāo)相互交換位置, 相同元素之間原本相對的順序被破壞了, 因此, 它是不穩(wěn)定的排序.

計數(shù)排序

計數(shù)排序幾乎是唯一一個不基于比較的排序算法, 該算法于1954年由 Harold H. Seward 提出. 使用它處理一定范圍內(nèi)的整數(shù)排序時, 時間復(fù)雜度為O(n+k), 其中k是整數(shù)的范圍, 它幾乎比任何基于比較的排序算法都要快( 只有當(dāng)O(k)>O(n*log(n))的時候其效率反而不如基于比較的排序, 如歸并排序和堆排序).

使用計數(shù)排序需要滿足如下條件:

  • 待排序的序列全部為整數(shù)
  • 排序需要額外的存儲空間

算法的基本思想:

計數(shù)排序利用了一個特性, 對于數(shù)組的某個元素, 一旦知道了有多少個其它元素比它小(假設(shè)為m個), 那么就可以確定出該元素的正確位置(第m+1位)

  1. 獲取待排序數(shù)組A的最大值, 最小值.
  2. 將最大值與最小值的差值+1作為長度新建計數(shù)數(shù)組B,并將相同元素的數(shù)量作為值存入計數(shù)數(shù)組.
  3. 對計數(shù)數(shù)組B累加計數(shù), 存儲不同值的初始下標(biāo).
  4. 從原數(shù)組A挨個取值, 賦值給一個新的數(shù)組C相應(yīng)的下標(biāo), 最終返回數(shù)組C.

注意: 如果原數(shù)組A是包含若干個對象的數(shù)組,需要基于對象的某個屬性進(jìn)行排序,那么算法開始時,需要將原數(shù)組A處理為一個只包含對象屬性值的簡單數(shù)組simpleA, 接下來便基于simpleA進(jìn)行計數(shù)、累加計數(shù), 其它同上.

如下是動圖效果:

計數(shù)排序

如下是算法實現(xiàn):

function countSort(array, keyName){
 var length = array.length,
 output = new Array(length),
 max,
 min,
 simpleArray = keyName ? array.map(function(v){
 return v[keyName];
 }) : array; // 如果keyName是存在的,那么就創(chuàng)建一個只有keyValue的簡單數(shù)組

 // 獲取最大最小值
 max = min = simpleArray[0];
 simpleArray.forEach(function(v){
 v > max && (max = v);
 v < min && (min = v);
 });
 // 獲取計數(shù)數(shù)組的長度
 var k = max - min + 1;
 // 新建并初始化計數(shù)數(shù)組
 var countArray = new Array(k);
 simpleArray.forEach(function(v){
 countArray[v - min]= (countArray[v - min] || 0) + 1;
 });
 // 累加計數(shù),存儲不同值的初始下標(biāo)
 countArray.reduce(function(prev, current, i, arr){
 arr[i] = prev;
 return prev + current;
 }, 0);
 // 從原數(shù)組挨個取值(因取的是原數(shù)組的相應(yīng)值,只能通過遍歷原數(shù)組來實現(xiàn))
 simpleArray.forEach(function(v, i){
 var j = countArray[v - min]++;
 output[j] = array[i];
 });
 return output;
}

以上實現(xiàn)不僅支持了數(shù)值序列的排序,還支持根據(jù)對象的某個屬性值來排序。測試如下:

var a = [2, 1, 1, 3, 2, 1, 4, 2],
 b = [
 {id: 2, s:'a'}, 
 {id: 1, s: 'b'}, 
 {id: 1, s: 'c'}, 
 {id: 3, s: 'd'}, 
 {id: 2, s: 'e'}, 
 {id: 1, s: 'f'}, 
 {id: 4, s: 'g'}, 
 {id: 2, s: 'h'}
 ];
countSort(a); // [1, 1, 1, 2, 2, 2, 3, 4]
countSort(b, 'id'); // [{id:1,s:'b'},{id:1,s:'c'},{id:1,s:'f'},{id:2,s:'a'},{id:2,s:'e'},{id:2,s:'h'},{id:3,s:'d'},{id:4,s:'g'}]

Tips: 計數(shù)排序不改變相同元素之間原本相對的順序, 因此它是穩(wěn)定的排序算法.

桶排序

桶排序即所謂的箱排序, 它是將數(shù)組分配到有限數(shù)量的桶子里. 每個桶里再各自排序(因此有可能使用別的排序算法或以遞歸方式繼續(xù)桶排序). 當(dāng)每個桶里的元素個數(shù)趨于一致時, 桶排序只需花費O(n)的時間. 桶排序通過空間換時間的方式提高了效率, 因此它需要額外的存儲空間(即桶的空間).

算法的基本思想:

桶排序的核心就在于怎么把元素平均分配到每個桶里, 合理的分配將大大提高排序的效率.

如下是算法實現(xiàn):

function bucketSort(array, bucketSize) {
 if (array.length === 0) {
 return array;
 }

 var i = 1,
 min = array[0],
 max = min;
 while (i++ < array.length) {
 if (array[i] < min) {
 min = array[i]; //輸入數(shù)據(jù)的最小值
 } else if (array[i] > max) {
 max = array[i]; //輸入數(shù)據(jù)的最大值
 }
 }

 //桶的初始化
 bucketSize = bucketSize || 5; //設(shè)置桶的默認(rèn)大小為5
 var bucketCount = ~~((max - min) / bucketSize) + 1, //桶的個數(shù)
 buckets = new Array(bucketCount); //創(chuàng)建桶
 for (i = 0; i < buckets.length; i++) {
 buckets[i] = []; //初始化桶
 }

 //將數(shù)據(jù)分配到各個桶中,這里直接按照數(shù)據(jù)值的分布來分配,一定范圍內(nèi)均勻分布的數(shù)據(jù)效率最為高效
 for (i = 0; i < array.length; i++) {
 buckets[~~((array[i] - min) / bucketSize)].push(array[i]);
 }

 array.length = 0;
 for (i = 0; i < buckets.length; i++) {
 quickSort(buckets[i]); //對每個桶進(jìn)行排序,這里使用了快速排序
 for (var j = 0; j < buckets[i].length; j++) {
 array.push(buckets[i][j]); //將已排序的數(shù)據(jù)寫回數(shù)組中
 }
 }
 return array;
}

Tips: 桶排序本身是穩(wěn)定的排序, 因此它的穩(wěn)定性與桶內(nèi)排序的穩(wěn)定性保持一致.

實際上, 桶也只是一個抽象的概念, 它的思想與歸并排序,快速排序等類似, 都是通過將大量數(shù)據(jù)分配到N個不同的容器中, 分別排序, 最后再合并數(shù)據(jù). 這種方式大大減少了排序時整體的遍歷次數(shù), 提高了算法效率.

基數(shù)排序

基數(shù)排序源于老式穿孔機, 排序器每次只能看到一個列. 它是基于元素值的每個位上的字符來排序的. 對于數(shù)字而言就是分別基于個位, 十位, 百位 或千位等等數(shù)字來排序. (不明白不要緊, 我也不懂, 請接著往下讀)

按照優(yōu)先從高位或低位來排序有兩種實現(xiàn)方案:

  • MSD: 由高位為基底, 先按k1排序分組, 同一組中記錄, 關(guān)鍵碼k1相等, 再對各組按k2排序分成子組, 之后, 對后面的關(guān)鍵碼繼續(xù)這樣的排序分組, 直到按最次位關(guān)鍵碼kd對各子組排序后. 再將各組連接起來, 便得到一個有序序列. MSD方式適用于位數(shù)多的序列.
  • LSD: 由低位為基底, 先從kd開始排序,再對kd-1進(jìn)行排序,依次重復(fù),直到對k1排序后便得到一個有序序列. LSD方式適用于位數(shù)少的序列.

如下是LSD的動圖效果:

基數(shù)排序

如下是算法實現(xiàn):

function radixSort(array, max) {
 var buckets = [],
 unit = 10,
 base = 1;
 for (var i = 0; i < max; i++, base *= 10, unit *= 10) {
 for(var j = 0; j < array.length; j++) {
 var index = ~~((array[j] % unit) / base);//依次過濾出個位,十位等等數(shù)字
 if(buckets[index] == null) {
 buckets[index] = []; //初始化桶
 }
 buckets[index].push(array[j]);//往不同桶里添加數(shù)據(jù)
 }
 var pos = 0,
 value;
 for(var j = 0, length = buckets.length; j < length; j++) {
 if(buckets[j] != null) {
 while ((value = buckets[j].shift()) != null) {
  array[pos++] = value; //將不同桶里數(shù)據(jù)挨個撈出來,為下一輪高位排序做準(zhǔn)備,由于靠近桶底的元素排名靠前,因此從桶底先撈
 }
 }
 }
 }
 return array;
}

以上算法, 如果用來比較時間, 先按日排序, 再按月排序, 最后按年排序, 僅需排序三次.

基數(shù)排序更適合用于對時間, 字符串等這些整體權(quán)值未知的數(shù)據(jù)進(jìn)行排序.

Tips: 基數(shù)排序不改變相同元素之間的相對順序, 因此它是穩(wěn)定的排序算法.

小結(jié)

各種排序性能對比如下:

排序類型 平均情況 最好情況 最壞情況 輔助空間 穩(wěn)定性
冒泡排序 O(n²) O(n) O(n²) O(1) 穩(wěn)定
選擇排序 O(n²) O(n²) O(n²) O(1) 不穩(wěn)定
直接插入排序 O(n²) O(n) O(n²) O(1) 穩(wěn)定
折半插入排序 O(n²) O(n) O(n²) O(1) 穩(wěn)定
希爾排序 O(n^1.3) O(nlogn) O(n²) O(1) 不穩(wěn)定
歸并排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(n) 穩(wěn)定
快速排序 O(nlog₂n) O(nlog₂n) O(n²) O(nlog₂n) 不穩(wěn)定
堆排序 O(nlog₂n) O(nlog₂n) O(nlog₂n) O(1) 不穩(wěn)定
計數(shù)排序 O(n+k) O(n+k) O(n+k) O(k) 穩(wěn)定
桶排序 O(n+k) O(n+k) O(n²) O(n+k) (不)穩(wěn)定
基數(shù)排序 O(d(n+k)) O(d(n+k)) O(d(n+kd)) O(n+kd) 穩(wěn)定

注: 桶排序的穩(wěn)定性取決于桶內(nèi)排序的穩(wěn)定性, 因此其穩(wěn)定性不確定. 基數(shù)排序中, k代表關(guān)鍵字的基數(shù), d代表長度, n代表關(guān)鍵字的個數(shù).

愿以此文懷念下我那遠(yuǎn)去的算法課程.

未完待續(xù)…

感謝 http://visualgo.net/ 提供圖片支持. 特別感謝 不是小羊的肖恩 在簡書上發(fā)布的 JS家的排序算法 提供的講解.

相關(guān)文章

  • Bootstrap源碼解讀媒體對象、列表組和面板(10)

    Bootstrap源碼解讀媒體對象、列表組和面板(10)

    這篇文章主要源碼解讀了Bootstrap媒體對象、列表組和面板,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-12-12
  • 用Div仿showModalDialog模式菜單的效果的代碼

    用Div仿showModalDialog模式菜單的效果的代碼

    用Div仿showModalDialog模式菜單的效果的代碼...
    2007-03-03
  • 可插入圖片的TEXT文本框

    可插入圖片的TEXT文本框

    這篇文章主要介紹了可插入圖片的TEXT文本框,有需要的朋友可以參考一下
    2013-12-12
  • javascript事件問題

    javascript事件問題

    咱們都知道,在DOM事件模型中,有捕獲和冒泡兩個階段,而在IE事件模型中,只有冒泡階段。寫在html里的事件響應(yīng)函數(shù)只有在冒泡階段才會被執(zhí)行
    2009-09-09
  • TypeScript聲明合并的實現(xiàn)

    TypeScript聲明合并的實現(xiàn)

    本文主要介紹了TypeScript聲明合并的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • 用最簡單的方法判斷JavaScript中this的指向(推薦)

    用最簡單的方法判斷JavaScript中this的指向(推薦)

    都說 JavaScript 是一種很靈活的語言,這其實也可以說它是一個混亂的語言,下面通過本文給大家分享JavaScript中this的指向知識,感興趣的朋友一起看看吧
    2017-09-09
  • 詳解JavaScript堆棧與拷貝

    詳解JavaScript堆棧與拷貝

    本文主要介紹了JavaScript中的堆棧與拷貝,能更好的幫助你理解JS的內(nèi)存機制,感興趣的同學(xué),一定要認(rèn)真看下
    2021-06-06
  • 一個獲取第n個元素節(jié)點的js函數(shù)

    一個獲取第n個元素節(jié)點的js函數(shù)

    這篇文章主要介紹了一個獲取第n個元素節(jié)點的js函數(shù),功能還不完善 ,需要的朋友可以參考下
    2014-09-09
  • javascript實現(xiàn)貪吃蛇經(jīng)典游戲

    javascript實現(xiàn)貪吃蛇經(jīng)典游戲

    這篇文章主要為大家詳細(xì)介紹了javascript實現(xiàn)貪吃蛇經(jīng)典游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-04-04
  • JavaScript?數(shù)組方法filter與reduce

    JavaScript?數(shù)組方法filter與reduce

    這篇文章主要介紹了JavaScript?數(shù)組方法filter與reduce,在ES6新增的數(shù)組方法中,包含了多個遍歷方法,其中包含了用于篩選的filter和reduce
    2022-07-07

最新評論