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

前言

本系列最開始是爲了本身面試準備的.後來發現整理愈來愈多,差很少有十二萬字符,最後決定仍是分享出來給你們.javascript

爲了分享整理出來,花費了本身大量的時間,起碼是隻本身用的三倍時間.若是喜歡的話,歡迎收藏,關注我!謝謝!html

文章連接

合集篇:

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

後續更新計劃

後續還會繼續添加設計模式,前端工程化,項目流程,部署,閉環,vue常考知識點 等內容.若是以爲內容不錯的話歡迎收藏,關注我!謝謝!vue

求一分內推

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

算法術語

  • 穩定:若是a本來在b前面,而a=b,排序以後a仍然在b的前面;
  • 不穩定:若是a本來在b的前面,而a=b,排序以後a可能會出如今b的後面;
  • 內排序:全部排序操做都在內存中完成;
  • 外排序:因爲數據太大,所以把數據放在磁盤中,而排序經過磁盤和內存的數據傳輸才能進行;
  • 時間複雜度: 一個算法執行所耗費的時間。
  • 空間複雜度: 運行完一個程序所需內存的大小。

時間複雜度和空間複雜度能夠查看這篇文章: 時間複雜度和空間複雜度詳解node

數據結構

  • :一種聽從先進後出 (LIFO) 原則的有序集合;新添加的或待刪除的元素都保存在棧的末尾,稱做棧頂,另外一端爲棧底。在棧裏,新元素都靠近棧頂,舊元素都接近棧底。
  • 隊列:與上相反,一種遵循先進先出 (FIFO / First In First Out) 原則的一組有序的項;隊列在尾部添加新元素,並從頭部移除元素。最新添加的元素必須排在隊列的末尾。
  • 鏈表:存儲有序的元素集合,但不一樣於數組,鏈表中的元素在內存中並非連續放置的;每一個元素由一個存儲元素自己的節點和一個指向下一個元素的引用(指針/連接)組成。
  • 集合:由一組無序且惟一(即不能重複)的項組成;這個數據結構使用了與有限集合相同的數學概念,但應用在計算機科學的數據結構中。
  • 字典:以 [鍵,值] 對爲數據形態的數據結構,其中鍵名用來查詢特定元素,相似於 Javascript 中的Object
  • 散列:根據關鍵碼值(Key value)直接進行訪問的數據結構;它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度;這個映射函數叫作散列函數,存放記錄的數組叫作散列表。
  • :由 n(n>=1)個有限節點組成一個具備層次關係的集合;把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的,基本呈一對多關係,樹也能夠看作是圖的特殊形式。
  • :圖是網絡結構的抽象模型;圖是一組由邊鏈接的節點(頂點);任何二元關係均可以用圖來表示,常見的好比:道路圖、關係圖,呈多對多關係。

更詳細的解讀能夠查看這: 這篇文章這篇文章git

排序對比:

圖片名詞解釋: n: 數據規模 k:「桶」的個數 In-place: 佔用常數內存,不佔用額外內存 Out-place: 佔用額外內存github

排序分類:

關於排序算法的說明

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

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

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)的空間。

感謝及參考

相關文章
相關標籤/搜索