前端面試查漏補缺--(十四) 演算法及排序

shotCat發表於2019-02-24

前言

本系列最開始是為了自己面試準備的.後來發現整理越來越多,差不多有十二萬字元,最後決定還是分享出來給大家.

為了分享整理出來,花費了自己大量的時間,起碼是隻自己用的三倍時間.如果喜歡的話,歡迎收藏,關注我!謝謝!

文章連結

合集篇:

前端面試查漏補缺--Index篇(12萬字元合集) 包含目前已寫好的系列其他十幾篇文章.後續新增值文章不會再在每篇新增連結,強烈建議議點贊,關注合集篇!!!!,謝謝!~

後續更新計劃

後續還會繼續新增設計模式,前端工程化,專案流程,部署,閉環,vue常考知識點 等內容.如果覺得內容不錯的話歡迎收藏,關注我!謝謝!

求一份內推

目前本人也在準備跳槽,希望各位大佬和HR小姐姐可以內推一份靠譜的武漢 前端崗位!郵箱:bupabuku@foxmail.com.謝謝啦!~

演算法術語

  • 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
  • 不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
  • 內排序:所有排序操作都在記憶體中完成;
  • 外排序:由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料傳輸才能進行;
  • 時間複雜度: 一個演算法執行所耗費的時間。
  • 空間複雜度: 執行完一個程式所需記憶體的大小。

時間複雜度和空間複雜度可以檢視這篇文章: 時間複雜度和空間複雜度詳解

資料結構

  • :一種遵從先進後出 (LIFO) 原則的有序集合;新新增的或待刪除的元素都儲存在棧的末尾,稱作棧頂,另一端為棧底。在棧裡,新元素都靠近棧頂,舊元素都接近棧底。
  • 佇列:與上相反,一種遵循先進先出 (FIFO / First In First Out) 原則的一組有序的項;佇列在尾部新增新元素,並從頭部移除元素。最新新增的元素必須排在佇列的末尾。
  • 連結串列:儲存有序的元素集合,但不同於陣列,連結串列中的元素在記憶體中並不是連續放置的;每個元素由一個儲存元素本身的節點和一個指向下一個元素的引用(指標/連結)組成。
  • 集合:由一組無序且唯一(即不能重複)的項組成;這個資料結構使用了與有限集合相同的數學概念,但應用在電腦科學的資料結構中。
  • 字典:以 [鍵,值] 對為資料形態的資料結構,其中鍵名用來查詢特定元素,類似於 Javascript 中的Object
  • 雜湊:根據關鍵碼值(Key value)直接進行訪問的資料結構;它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度;這個對映函式叫做雜湊函式,存放記錄的陣列叫做雜湊表。
  • :由 n(n>=1)個有限節點組成一個具有層次關係的集合;把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的,基本呈一對多關係,樹也可以看做是圖的特殊形式。
  • :圖是網路結構的抽象模型;圖是一組由邊連線的節點(頂點);任何二元關係都可以用圖來表示,常見的比如:道路圖、關係圖,呈多對多關係。

更詳細的解讀可以檢視這: 這篇文章這篇文章

排序對比:

前端面試查漏補缺--(十四) 演算法及排序

圖片名詞解釋: n: 資料規模 k:“桶”的個數 In-place: 佔用常數記憶體,不佔用額外記憶體 Out-place: 佔用額外記憶體

排序分類:

前端面試查漏補缺--(十四) 演算法及排序

關於排序演算法的說明

  • 千萬不要死記實現程式碼!
  • 記住演算法動畫
  • 通過動畫,理解演算法的思想和實現方法

只有這樣你才能做到正在自信地在面試官面前手寫程式碼,還能邊寫邊和他講解思路!

1.氣泡排序(Bubble Sort)

演算法描述

氣泡排序是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。因此取名氣泡排序.

氣泡排序

演算法步驟及實現程式碼

思路: 氣泡排序屬於基本排序演算法,大致思路是兩層迴圈巢狀.結合下面的動圖,整理思路: 外迴圈遍歷陣列的每一項,確定兩兩比較迴圈的次數(其實最後一次可以省略),內迴圈則用於確定單次迴圈兩兩元素比較的次數,注意外層每迴圈一次,內迴圈兩兩比較的次數就會減1,即動圖中的黃色塊,表示已經排序好的柱形。

步驟:

  • 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  • 針對所有的元素重複以上的步驟,除了最後一個。
  • 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

JS程式碼實現:

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        //相鄰元素兩兩對比
			[arr[j],arr[j+1]] = [arr[j+1],arr[j]]  //通過解構完成元素交換
                
            }
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

說明:關於冒泡演算法的其他實現思路:逆序,雙向等實現方法,可以檢視這篇文章 ,這裡就不費筆墨了.

2.選擇排序(Selection Sort)

演算法描述

選擇排序(Selection-sort)是一種簡單直觀的排序演算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

選擇排序也是表現最穩定的排序演算法之一,因為無論什麼資料進去都是O(n²)的時間複雜度.所以用到它的時候,資料規模越小越好。唯一的好處可能就是不佔用額外的記憶體空間了吧。 理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。

選擇演算法

演算法步驟及實現程式碼

思路: 選擇排序也屬於基本排序演算法,大致思路也是兩層迴圈巢狀.結合下面的動圖和它的工作原理:首先外迴圈,每迴圈一次就確定了一個值在排序中的位置(動圖中為從左依次確定).那要經過多少次,這樣的迴圈?答案就是數列的長度減1. 接著是內迴圈: 確定剩下的未排序的柱形需要逐個比較的次數.

步驟: n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體演算法描述如下:

  • 1.初始狀態:無序區為R[1..n],有序區為空;
  • 2.第i趟排序(i=1,2,3...n-1)開始時,當前有序區和無序區分別為R[1..i-1]和R(i..n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]R[i+1..n)分別變為記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
  • 3.n-1趟結束,陣列有序化了。

JS程式碼實現:

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;   //用來儲存最小數
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     //尋找最小的數
                minIndex = j;                 //將最小數的索引儲存
            }
        }
		[arr[minIndex],arr[i]] = [arr[i],arr[minIndex]]  //通過解構完成元素交換
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(selectionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

3.插入排序(Insertion Sort)

演算法描述

插入排序(Insertion-Sort)的演算法描述是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

插入排序核心--撲克牌思想: 就想著自己在打撲克牌,接起來第一張,放哪裡無所謂,再接起來一張,比第一張小,放左邊,繼續接,可能是中間數,就插在中間.後面起的牌從後向前依次比較,並插入.

插入排序

演算法步驟及實現程式碼

思路: 插入排序也屬於基本排序演算法,大致思路也是兩層迴圈巢狀.首先,按照其撲克牌的思路.將要排序的數列分為兩部分.左邊為有序數列(起在手中的牌),剛開始為空.右邊部分為待排序的數列(即亂序的撲克牌).

有了上面大致思想後,開始設定迴圈.首先外迴圈為你需要起多少張牌.那是多少?毫無疑問就是數列的長度,但是為了方便,我們可以預設讓數列第一個數作為有序數列,可以減少一次迴圈.故外迴圈次數為數列長度減1;內迴圈則迴圈有序數列,並從右往左,比較大小,將較小數插在前面(結合動圖)

步驟:

  • 1.從第一個元素開始,該元素可以認為已經被排序;
  • 2.取出下一個元素,在已經排序的元素序列中從後向前掃描;
  • 3.如果該元素(已排序)大於新元素,將該元素移到下一位置;
  • 4.重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  • 5.將新元素插入到該位置後;
  • 6.重複步驟2~5。

JS程式碼實現:

function insertSort(arr) {
    for(let i = 1; i < arr.length; i++) {  //外迴圈從1開始,預設arr[0]是有序段
        for(let j = i; j > 0; j--) {  //j = i,表示此時你起在手上的那張牌,將arr[j]依次比較插入有序段中
            if(arr[j] < arr[j-1]) {
                [arr[j],arr[j-1]] = [arr[j-1],arr[j]];  //其實這裡內迴圈中,只要比它前一個數小就交換,直到沒有更小的,就break退出.這和動圖表示的插入還是有點區別的,但最後結果其實是一樣的.
            } else {
                break;
            }
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

4.快速排序(Quick Sort)

演算法描述

快速排序的名字起的是簡單粗暴,因為一聽到這個名字你就知道它存在的意義,就是快,而且效率高! 它是處理大資料最快的排序演算法之一了。

它是在氣泡排序基礎上的遞迴分治法。通過遞迴的方式將資料依次分解為包含較小元素和較大元素的不同子序列。該演算法不斷重複這個步驟直至所有資料都是有序的。

注意: 快速排序也是面試是最最最容易考到的演算法題,經常就會讓你進行手寫.

快速排序

演算法步驟及實現程式碼

思路: 快速排序屬於高階排序演算法,此時就不是相似的迴圈巢狀.它的大概思想就是: 找到一個數作為參考,比這個數字大的放在數字左邊,比它小的放在右邊; 然後分別再對左邊和右變的序列做相同的操作(遞迴).

注意: 涉及到遞迴的演算法,一定要記得設定出口,跳出遞迴!

步驟:

  • 1.從數列中挑出一個元素,稱為 “基準”(pivot);
  • 2.重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;
  • 3.遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序;

JS程式碼實現:

function quickSort (arr) {
	if(arr.length <= 1) {
        return arr;  //遞迴出口
    }
	let left = [],
        right = [],
		//這裡我們預設選擇陣列第一個為基準,PS:其實這樣偷懶是不好的,如果陣列是已經排好序了的.則很有可能變成最差情況的時間複雜度
		//pivotIndex = Math.floor(arr.length / 2),
	    pivot = arr[0];    //阮一峰版:  arr.splice(pivotIndex, 1)[0];   使用splice在大量資料時,會消耗大量記憶體;但也不至於被噴得一無是處! 它的思路是沒有任何問題的! 
	for (var i = 1; i < arr.length; i++) {
		if (arr[i] < pivot) {
			left.push(arr[i])
		} else {
			right.push(arr[i])
		}
	}
	//concat也不適合大量資料的排序,會消耗大量記憶體
	return quickSort(left).concat(pivot, quickSort(right))
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

//改進版:
function partition2(arr, low, high) {
  let pivot = arr[low];
  while (low < high) {
    while (low < high && arr[high] > pivot) {
      --high;
    }
    arr[low] = arr[high];
    while (low < high && arr[low] <= pivot) {
      ++low;
    }
    arr[high] = arr[low];
  }
  arr[low] = pivot;
  return low;
}

function quickSort2(arr, low, high) {
  if (low < high) {
    let pivot = partition2(arr, low, high);
    quickSort2(arr, low, pivot - 1);
    quickSort2(arr, pivot + 1, high);
  }
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort2(arr,0,arr.length-1));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

5.希爾排序(Shell Sort)

演算法描述

1959年Shell發明; 第一個突破O(n^2)的排序演算法;是簡單插入排序的改進版;它與插入排序的不同之處在於,它會優先比較距離較遠的元素。 希爾排序又叫縮小增量排序.並且排序也是不穩定的

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率;
  • 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位;

希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

演算法步驟及實現程式碼

思路: 希爾排序其實大體思路很簡單,就是將陣列(長度為len)分成間隔為t1的若干陣列.進行插入排序;排完後,將陣列再分成間隔為t2(逐步減小)的若干陣列,進行插入排序;然後繼續上述操作,直到分成間隔為1的陣列,再進行最後一次插入排序則完成.

方便理解可以檢視下圖:

前端面試查漏補缺--(十四) 演算法及排序

步驟:

  • 1,選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 2,按增量序列個數k,對序列進行k 趟排序;
  • 3,每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。

JS程式碼實現:

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    while(gap < len/5) {          //動態定義間隔序列
        gap =gap*5+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/5)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
                arr[j+gap] = arr[j];
            }
            arr[j+gap] = temp;
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(shellSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

複製程式碼

6.歸併排序(Merge Sort)

演算法描述

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。

歸併排序是一種穩定的排序方法。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。

歸併排序

演算法步驟及實現程式碼

思路: 將陣列分為左和右兩部分,然後繼續將左右兩部分繼續(遞迴)拆分,直到拆分成單個為止;然後將拆分為最小的兩個陣列,進行比較,合併排成一個陣列.接著繼續遞迴比較合併.直到最後合併為一個陣列.

步驟:

  • 1.把長度為n的輸入序列分成兩個長度為n/2的子序列;
  • 2.對這兩個子序列分別採用歸併排序;
  • 3.將兩個排序好的子序列合併成一個最終的排序序列。

JS程式碼實現:

function mergeSort(arr) {  //採用自上而下的遞迴方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
    var 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;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

7.堆排序(Heap Sort)

演算法描述

堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分為兩種方法:

  • 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序演算法中用於升序排列;
  • 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序演算法中用於降序排列;

歸併排序

步驟:

  • 1.將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆為初始的無序區;
  • 2.將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn),且滿足R[1,2...n-1]<=R[n];
  • 3.由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,......Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。

JS程式碼實現:


function buildMaxHeap(arr,len) {   // 建立大頂堆
    
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i,len);
    }
}

function heapify(arr, i,len) {     // 堆調整
    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,len);
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    var len = arr.length;
    buildMaxHeap(arr,len);

    for (var i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0,len);
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(heapSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

8.計數排序(Counting Sort)

演算法描述

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

計數排序的核心在於將輸入的資料值轉化為鍵儲存在額外開闢的陣列空間中。

計數排序

演算法步驟及實現程式碼

思路: 計數排序利用了一個特性, 對於陣列的某個元素, 一旦知道了有多少個其它元素比它小(假設為m個), 那麼就可以確定出該元素的正確位置(第m+1位)

步驟:

  • 1, 獲取待排序陣列A的最大值, 最小值.
  • 2, 將最大值與最小值的差值+1作為長度新建計數陣列B,並將相同元素的數量作為值存入計數陣列.
  • 3, 對計數陣列B累加計數, 儲存不同值的初始下標.
  • 4, 從原陣列A挨個取值, 賦值給一個新的陣列C相應的下標, 最終返回陣列C.

注意: 如果原陣列A是包含若干個物件的陣列,需要基於物件的某個屬性進行排序,那麼演算法開始時,需要將原陣列A處理為一個只包含物件屬性值的簡單陣列simpleA, 接下來便基於simpleA進行計數、累加計數, 其它同上.

JS程式碼實現:

//以下實現不僅支援了數值序列的排序,還支援根據物件的某個屬性值來排序。
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是存在的,那麼就建立一個只有keyValue的簡單陣列

  // 獲取最大最小值
  max = min = simpleArray[0];
  simpleArray.forEach(function(v){
    v > max && (max = v);
    v < min && (min = v);
  });
  // 獲取計數陣列的長度
  var k = max - min + 1;
  // 新建並初始化計數陣列
  var countArray = new Array(k);
  simpleArray.forEach(function(v){
    countArray[v - min]= (countArray[v - min] || 0) + 1;
  });
  // 累加計數,儲存不同值的初始下標
  countArray.reduce(function(prev, current, i, arr){
    arr[i] = prev;
    return prev + current;
  }, 0);
  // 從原陣列挨個取值(因取的是原陣列的相應值,只能通過遍歷原陣列來實現)
  simpleArray.forEach(function(v, i){
    var j = countArray[v - min]++;
    output[j] = array[i];
  });
  return output;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(countSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

9.桶排序(Bucket Sort)

演算法描述

桶排序是計數排序的升級版。它利用了函式的對映關係,高效與否的關鍵就在於這個對映函式的確定。為了使桶排序更加高效,我們需要做到這兩點:

  • 在額外空間充足的情況下,儘量增大桶的數量
  • 使用的對映函式能夠將輸入的 N 個資料均勻的分配到 K 個桶中

同時,對於桶中元素的排序,選擇何種比較排序演算法對於效能的影響至關重要。很顯然,桶劃分的越小,各個桶之間的資料越少,排序所用的時間也會越少。但相應的空間消耗就會增大。

桶排序

演算法步驟及實現程式碼

思路: 桶排序 (Bucket sort)的工作的原理:假設輸入資料服從均勻分佈,將資料分到有限數量的桶裡,每個桶再分別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排

步驟:

  • 1.設定一個定量的陣列當作空桶;
  • 2.遍歷輸入資料,並且把資料一個一個放到對應的桶裡去;
  • 3.對每個不是空的桶進行排序;
  • 4.從不是空的桶裡把排好序的資料拼接起來。

注意: 如果原陣列A是包含若干個物件的陣列,需要基於物件的某個屬性進行排序,那麼演算法開始時,需要將原陣列A處理為一個只包含物件屬性值的簡單陣列simpleA, 接下來便基於simpleA進行計數、累加計數, 其它同上.

JS程式碼實現:

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

    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
      if (arr[i] < minValue) {
          minValue = arr[i];                // 輸入資料的最小值
      } else if (arr[i] > maxValue) {
          maxValue = arr[i];                // 輸入資料的最大值
      }
    }

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;            // 設定桶的預設數量為5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;   
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }

    //利用對映函式將資料分配到各個桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i]);                      // 對每個桶進行排序,這裡使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                      
        }
    }

    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bucketSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

10.基數排序(Radix Sort)

演算法描述

基數排序是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

按照優先從高位或低位來排序有兩種實現方案:

  • MSD: 由高位為基底, 先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分成子組, 之後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組連線起來, 便得到一個有序序列. MSD方式適用於位數多的序列.
  • LSD: 由低位為基底, 先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便得到一個有序序列. LSD方式適用於位數少的序列.

基數排序,計數排序,桶排序.這三種排序演算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  • 基數排序:根據鍵值的每位數字來分配桶;
  • 計數排序:每個桶只儲存單一鍵值;
  • 桶排序:每個桶儲存一定範圍的數值;

基數排序

演算法步驟及實現程式碼

步驟:

  • 1.取得陣列中的最大數,並取得位數;
  • 2.arr為原始陣列,從最低位開始取每個位組成radix陣列;
  • 3.對radix進行計數排序(利用計數排序適用於小範圍數的特點);

JS程式碼實現:

/**
 * 基數排序適用於:
 *  (1)資料範圍較小,建議在小於1000
 *  (2)每個數值都要大於等於0
 * @author xiazdong
 * @param  arr 待排序陣列
 * @param  maxDigit 最大位數
 */
//LSD Radix Sort

function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    console.time('基數排序耗時');
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]== null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    console.timeEnd('基數排序耗時');
    return arr;
}
var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log(radixSort(arr,2)); //[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
複製程式碼

二叉樹和二叉查詢樹

樹是一種非順序資料結構,一種分層資料的抽象模型,它對於儲存需要快速查詢的資料非常有用。

現實生活中最常見的樹的例子是家譜,或是公司的組織架構圖.

一個樹結構包含一系列存在父子關係的節點。每個節點都有一個父節點(除了頂部的第一個 節點)以及零個或多個子節點.

樹常見結構/屬性:

  • 節點
    • 根節點
    • 內部節點:非根節點、且有子節點的節點
    • 外部節點/頁節點:無子節點的節點
  • 子樹:就是大大小小節點組成的樹
  • 深度:節點到根節點的節點數量
  • 高度:樹的高度取決於所有節點深度中的最大值
  • 層級:也可以按照節點級別來分層

二叉樹

二叉樹,是一種特殊的樹,即子節點最多隻有兩個,這個限制可以使得寫出高效的插入、刪除、和查詢資料。在二叉樹中,子節點分別叫左節點和右節點。

二叉樹

二叉查詢樹

二叉查詢樹是一種特殊的二叉樹,相對較小的值儲存在左節點中,較大的值(或者等於)儲存在右節點中,這一特性使得查詢的效率很高,對於數值型和非數值型資料,比如字母和字串,都是如此。 現在通過JS實現一個二叉查詢樹。

節點:

二叉樹的最小元素是節點,所以先定義一個節點

function Node(data,left,right) {
    this.left = left;
    this.right = right;
    this.data = data;
    this.show = () => {return this.data}
}
複製程式碼

這個就是二叉樹的最小結構單元

二叉樹

function BST() {
    this.root = null //初始化,root為null
}

複製程式碼

BST初始化時,只有一個根節點,且沒有任何資料。 接下來,我們利用二叉查詢樹的規則,定義一個插入方法,這個方法的基本思想是:

  1. 如果BST.root === null ,那麼就將節點作為根節點
  2. 如果BST.root !==null ,將插入節點進行一個比較,小於根節點,拿到左邊的節點,否則拿右邊,再次比較、遞迴。

這裡就出現了遞迴了,因為,總是要把較小的放在靠左的分支。換言之

最左變的葉子節點是最小的數,最右的葉子節點是最大的數

function insert(data) {
    var node = new Node(data,null,null);
    if(this.root === null) {
        this.root = node
    } else {
        var current = this.root;
        var parent;
        while(true) {
            parent = current;
            if(data < current.data) {
                current = current.left; //到左子樹
                if(current === null) {  //如果左子樹為空,說明可以將node插入在這裡
                    parent.left = node;
                    break;  //跳出while迴圈
                }
            } else {
                current = current.right;
                if(current === null) {
                    parent.right = node;
                    break;
                }
            }
        }
    }
}
複製程式碼

這裡,是使用了一個迴圈方法,不斷的去向子樹尋找正確的位置。 迴圈和遞迴都有一個核心,就是找到出口,這裡的出口就是當current 為null的時候,代表沒有內容,可以插入。

接下來,將此方法寫入BST即可:

function BST() {
    this.root = null;
    this.insert = insert;
}
複製程式碼

這樣子,就可以使用二叉樹這個自建的資料結構了:

var bst = new BST();
bst.insert(10);
bst.insert(8);
bst.insert(2);
bst.insert(7);
bst.insert(5);
複製程式碼
複製程式碼

但是這個時候,想要看樹中的資料,不是那麼清晰,所以接下來,就要用到遍歷了。

樹的遍歷:

按照根節點訪問的順序不同,樹的遍歷分為以下三種:

  • 前序遍歷 (根節點->左子樹->右子樹)
  • 中序遍歷 (左子樹->根節點->右子樹)
  • 後序遍歷 (左子樹->右子樹->根節點)

先序遍歷:

先序遍歷是以優先於後代節點的順序訪問每個節點的。先序遍歷的一種應用是列印一個結構化的文件。

先序遍歷

function preOrder(node) {
    if(node !== null) {
        //根節點->左子樹->右子樹
        console.log(node.show());
        preOrder(node.left);
        preOrder(node.right);
    }
}
複製程式碼

中序遍歷:

中序遍歷是以從最小到最大的順序訪 問所有節點。中序遍歷的一種應用就是對樹進行排序操作。

中序遍歷

function inOrder(node) {
    if(node !== null) {
        //如果不是null,就一直查詢左變,因此遞迴
		//左子樹->根節點->右子樹
        inOrder(node.left);
        //遞迴結束,列印當前值
        console.log(node.show());
        //上一次遞迴已經把左邊搞完了,右邊
        inOrder(node.right);
    }
}
複製程式碼

後序遍歷:

後序遍歷則是先訪問節點的後代節點,再訪問節點本身。後序遍歷的一種應用是計算一個目錄和它的子目錄中所有檔案所佔空間的大小。

後序遍歷

function postOrder(node) {
    if(node !== null) {
        //左子樹->右子樹->根節點
        postOrder(node.left);
        postOrder(node.right);
        console.log(node.show())
    }
}
複製程式碼

二叉樹的查詢

在二叉樹這種資料結構中進行資料查詢是最方便的:

  • 最小值: 最左子樹的葉子節點
  • 最大值: 最右子樹的葉子節點
  • 特定值: target與current進行比較,如果比current大,在current.right進行查詢,反之類似。

清楚思路後,就動手來寫:

//最小值
function getMin(bst) {
    var current = bst.root;
    while(current.left !== null) {
        current = current.left;
    }
    return current.data;
}

//最大值
function getMax(bst) {
    var current = bst.root;
    while(current.right !== null) {
        current = current.right;
    }
    return current.data;
}
複製程式碼

最大、最小值都是非常簡單的,下面主要看下如何通過

function find(target,bst) {
    var current = bst.root;
    while(current !== null) {
        if(target === current.data) {
            return true;
        }
        else if(target > current.data) {
            current = current.right;
        } else if(target < current.data) {
            current = current.left;
        }
    }
    return -1;
}
複製程式碼

其實核心,仍然是通過一個迴圈和判斷,來不斷的向下去尋找,這裡的思想其實和二分查詢是有點類似的。

AVL樹:

AVL樹是一種自平衡二叉搜尋樹,AVL樹本質上是帶了平衡功能的二叉查詢樹(二叉排序樹,二叉搜尋樹),在AVL樹中任何節點的兩個子樹的高度最大差別為一,也就是說這種樹會在新增或移除節點時儘量試著成為一棵完全樹,所以它也被稱為高度平衡樹。查詢、插入和刪除在平均和最壞情況下都是 O(log n),增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。

紅黑樹:

紅黑樹和AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查詢樹的平衡,從而獲得較高的查詢效能;它雖然是複雜的,但它的最壞情況執行時間也是非常良好的,並且在實踐中是高效的:它可以在O(log n)時間內做查詢,插入和刪除,這裡的 n 是樹中元素的數目。

紅黑樹是每個節點都帶有顏色屬性的二叉查詢樹,顏色或紅色或黑色。在二叉查詢樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求:

  • 節點是紅色或黑色
  • 根節點是黑色
  • 每個葉節點(NIL節點,空節點)是黑色的
  • 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
  • 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點

這些約束強制了紅黑樹的關鍵性質:從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查詢某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查詢樹。

紅黑樹和AVL樹一樣都對插入時間、刪除時間和查詢時間提供了最好可能的最壞情況擔保。這不只是使它們在時間敏感的應用如即時應用(real time application)中有價值,而且使它們有在提供最壞情況擔保的其他資料結構中作為建造板塊的價值;例如,在計算幾何中使用的很多資料結構都可以基於紅黑樹。 紅黑樹在函數語言程式設計中也特別有用,在這裡它們是最常用的持久資料結構之一,它們用來構造關聯陣列和集合,在突變之後它們能保持為以前的版本。除了O(log n)的時間之外,紅黑樹的持久版本對每次插入或刪除需要O(log n)的空間。

感謝及參考

相關文章