前端不懂算法(五)--桶排序,計數排序,基數排序

本篇文章參考借鑑了葉老師的筆記和極客時間的數據結構與算法一文,若涉及侵權,請聯繫我刪除。

前言

  上幾篇博客我們一塊學習了幾種常用的排序算法,最低的時間複雜度是 O(nlogn) 如果我們還想提升一下速度,怎麼辦,這就需要我們改變一下思路,不基於比較來進行排序,本篇文章,我們一塊來學習非比較的 線性排序 算法。桶排序,計數排序,基數排序。時間複雜度爲 O(n)

沒看過之前文章的小夥伴可以先了解一下

核心思想

  桶排序,計數排序,基數排序的核心思想是 非比較

帶着問題學習

  如何根據年齡,給100萬用戶排序?

  這幾個問題當然也可以用前幾篇文章提到的排序算法,但是時間消耗還是比較多,有沒有更快的算法,同時這個數據結構比較特殊,這麼多用戶當中,年齡相同的會有很多,這個特點就很符合接下來要學習的算法,桶排序

桶排序原理

  桶排序,顧名思義需要用到桶,核心思想就是把要排序的數據分到幾個有序的桶裏,每個桶裏的數據再單獨進行排序,桶內排完順序之後,再把桶裏的數據,按順序依次取出,組成的序列就是有序的了。
在這裏插入圖片描述
  桶排序的時間複雜度爲什麼是 O(n) ?我們一起分析下,如果要排序的數組有 n 個,我們把它們均勻的劃分到 m 個桶裏,每個桶裏就有 k = n/m 個元素,每個桶內部使用快速排序,時間複雜度爲 O(klogk) m 個桶的排序時間就是 O(mklogk),因爲 k = n/m 所以整個桶的排序時間複雜度就是 O(nlog(n/m)) ,當桶的個數 m 接近數據個數 n 時, log(n/m) 就是個非常小的量,這個時候桶排序的時間複雜度接近 O(n)

  桶排序看起來很優秀,那它可以代替之前學習的排序嗎?

  目前看來是不行的,因爲上面我們做了很多條件的限制,因爲桶排序對數據要求非常苛刻。

  首先,要排序的數據需要很容易的就劃分成 m 個桶,並且桶和桶之間有着天然的大小順序。這樣每個桶排序完之後,桶與桶之間的數據纔不需要排序。

 &emsp其次,數據在各個桶之間的分佈是比較均勻的,如果數據經過桶的劃分之後,有些桶裏的數據非常多,有些非常少,很不平均,那桶內數據的排序時間複雜度就不是常量級別的了,在極端情況下,如果數據都被劃分到一個桶裏,那就退化爲 O(n*log(n/m)) 的排序算法了。

   桶排序比較適合用在外部排序中。所謂的外部排序就是數據存儲在外部磁盤中,數據量比較大,內存有限,無法將數據全部加載到內存中。

  比如說現在有 10 GB的訂單數據,我們希望按訂單金額(假設金額都是正整數)進行排序,但是內存有限,只有幾百 MB ,沒辦法一次性把所有的數據都加載到內存中,怎麼辦?

  這個時候就需要藉助桶排序。

  先掃描一遍文件,查看一下訂單金額範圍。假設掃描後,訂單金額最小是 1 元,最大是 10 萬元。我們將所有訂單,根據金額劃分到 100 個桶裏, 第一個桶 1-1000,第二個 1001-2000 ,以此類推,每一個桶對應一個文件,並且按照金額範圍的大小順序進行編號命名(00,01,02,03…99)。

  理想狀態下,如果訂單在 1 到 10 萬元之間均勻分佈,那訂單會被均勻的劃分到 100 個桶裏,每個文件存儲大小爲 100MB 的數據,我們可以把這 100 個文件一次放入到內存中,通過快排來進行排序,等都排好之後,我們只需要按照文件編號,一個一個讀取到同一個文件中,那數據就完成排序了。

  不過有個問題,訂單數據很難做到均勻分佈,如果有的文件超過 100 MB 無法放入內存怎麼辦?這種情況我們可以單獨對數據較多的桶繼續劃分,然後重複依次排序的過程。直到所有文件都能讀入內存爲止。

計數排序原理

  計數排序比較類似桶排序的特殊情況,當所排序的數據範圍不大的時候,比如說最大值是 K ,我們就可以劃分成 K 個桶。這樣就可以省掉桶內排序的時間。

  舉個例子,高考分數排序,全國幾百萬考生,數據量很大,但是高考分數的範圍很小,滿分 750,最低分 0 份,我們就可以劃分成 751 個桶,然後把學生們按成績放入到每個桶裏,然後再從桶裏一次取出數據,就是排列好的情況了,而且時間複雜度是 O(n)

  計數排序思想很簡單,只是把桶排序的桶最小化,不過爲什麼要叫 計數排序 ?而不是最小桶排序,小桶排序,迷你桶排序呢? 計數的含義是什麼?

  想弄懂“計數”的含義是什麼,那就需要來看計數排序的實現了。簡化一下剛纔的例子,有 8 個考生,成績在 0-5 之間,數據放在一個數組 arr[8] 中。

  考生成績 0-5 ,那我們使用大小爲 6 的數組 C[6] 表示桶,其中下標對應分數。不過 C[6] 內存儲的不是考生,而是考生的個數。上面的例子,只要遍歷一遍考生分數,就可以得到 C[6] 的值。

在這裏插入圖片描述
  從上圖可以看出,分數爲 3 的考生有 3 個,分數小於 3 的考生有 4 個,成績爲 3 的考生在排序之後的有序數組 R[8] 中,會保存下標4,5,6的位置。
在這裏插入圖片描述
  現在關鍵點是如何快速計算每個分數的考生在有序數組中對應的存儲位置呢?這個處理方法非常巧妙。思路如下:

  我們對 C[k] 數組改變成,C[k] = C[0] + C[1] + … + C[k] 的形式。這樣 C[k] 就變成了存儲小於等於分數 K 的考生個數。
在這裏插入圖片描述

計數排序核心

  有了上面的準備,我們就要講最難理解的一部分了,大家仔細閱讀。

  從後到前依次掃描數組 A 。比如當掃描到 3 時,我們可以從數組 C 中取出下標爲 3 的值 7 ,也就是說,到目前爲止,包括自己在內,分數小於等於 3 的考生有 7 個,那 3 就是數組 R 中的第 7 個元素(也就是數組 R 中下標爲 6 的位置)。當 3 放入到數組 R 後,小於等於 3 的元素就只剩下 6 個了,所以響應的 C[3] 就要 -1 變成 6 。

  以此類推,當掃描到第二個分數爲 3 的考生的時候,就把它讓入到數組 R 中的第 6 個元素的位置(也就是下標爲 5 的位置)。當我們掃描完完整的數組 A 後,數組 R 中的數據,就按照分數從小到大排序好了。

(圖片位置)

  上面過程有點複雜,需要結合代碼來看

function countingSort(arr) {
	let n = arr.length;
	if (n <= 1) return;
	
	//查找數組中的數據範圍
	let max = arr[0];
	for (let i = 1; i < n; ++i) {
		if (max < arr[i]) {
			max = arr[i];
		}
	}
	let c = [];
	for (let i = 0; i <= max; ++i) {
		c[i] = 0;
	}
	
	//計算每個元素的個數,放入c中
	for (let i = 0; i < n; ++i) {
		c[arr[i]]++;
	}
	
	//依次累加
	for (let i = 1; i <= max; ++i) {
		c[i] = c[i - 1] + c[i];
	}
	//臨時數組r,存儲排序之後的結果
	let r = [];
	//計算排序的關鍵步驟
	for (let i = n - 1; i >= 0; --i) {
		let index = c[arr[i]] - 1;
		r[index] = arr[i];
		c[arr[i]]--;
	}
	
	//將結果拷貝給arr數組
	for (let i = 0; i < n; ++i) {
		arr[i] = r[i];
	}
}

let abc = [9, 7, 8, 5, 6, 1, 3, 2];
countingSort(abc);
console.log(abc);//[1, 2, 3, 5, 6, 7, 8, 9]

  看完上面代碼是不是明白爲什麼叫計數排序了吧,是利用另一個數組來計數,完成排序實現。上面代碼不必死記硬背,重要的是理解和會用。

計數排序總結

  計數排序只能用在數據範圍不大的場景當中,類似高考按分數排名這樣的例子,如果數據範圍 K 比要排序的數據 n 大很多,就不適合用計數排序了。而且,計數排序只能給非負整數排序,如果要排序的數據是其他類型的,要將其在不改變相對大小的情況下,轉化爲非負整數。

  舉個例子,高考分數可能存在小數,比如 620.5 分,這個時候就需要把所有數據乘 10 之後再進行排序。

基數排序

  現在有一個新問題,如果有 10 萬個手機號,希望這 10 萬個手機號從小到大排序,有什麼比較快速的排序方法?

  我們之前學習過快排,時間複雜度能做到 O(nlogn) 還有沒有更高效的算法?桶排序,計數排序也不能用,因爲手機號的範圍太大了,有 11 位數。針對這種排序,我們需要一種時間複雜度爲 O(n) 的排序算法。

  這時候就需要用到我們接下來要學習的排序算法了, 基數排序

  上面的問題有一個特性,存在一個隱性的規律:假設要比較兩個手機號碼 a ,b 的大小,如果在前幾位中,a 已經比 b 大了,那麼後面的位數就不用比了,a 肯定比 b 大。

  藉助穩定排序算法,這裏有一個巧妙的實現思路。我們之前在講穩定算法的時候舉過一個訂單排序的例子,不記得的同學,可以看下面的文章,在排序算法穩定性那一小節。

  我們可以藉助那個訂單排序的思路,先按照最後一位來排序手機號碼,然後按照倒數第二位,然後按照導數第三位,以此類推,排序11次,手機號碼就有序了。
在這裏插入圖片描述
  憑什麼?憑什麼倒着排就行了?,這個排序效果之前講穩定性的時候就解釋過了,因爲使用的是穩定的排序算法那,那麼相同位置的手機號,在排序之後,前後順序不會變,這樣後面的排序不會影響到前面排序的結果,就可以完成排序了。

  結合一下圖形可以更清楚的認識,手機號有點長,我們用字符串代替一下。
(圖片位置)
  注意,這裏按照每位來排序的算法,必須要穩定排序算法,不然上面思路就是錯誤的。

  根據每一位來排序,我們可以使用桶排序或者計數排序,他們的時間複雜度都可以做到 O(n) 。如果要排序的相機有 K 位,那我們就需要 K 次桶排序,總的時間複雜度是 O(k*n) ,不過由於 K 比較小,最大也就是 11 ,所以基數排序的時間複雜度近似等於 O(n)

  當有時候我們要排序的數據不是等長的時候怎麼辦?比如排序牛津字典中的 20 萬個英文單詞,最短的只有一個字母,最長的有 45 個字母,中文翻譯是塵肺病。對於這種不等長的數據,基數排序還適用嗎?

  實際上, 我們可以把所有單詞補全到相同長度的,位數不夠的可以補“0”,根據 ASCII 碼,所有字母都大於“0”,這樣就可以繼續用基數排序了。

  總結,基數排序要對排序的數據是有要求的,需要可以分割出獨立的“位”來比較,而且“位”之間有遞進關係,如果 a 數據的高位比 b 數據大,那麼後面的數據就不用比較了。除此之外,每一位的數據範圍不能太大,要可以用線性排序算法來排序,否則基數排序的時間複雜度就不能做到 O(n) 了

解答開篇

  還記得我們開篇提出的一個問題嗎?如何根據年齡給100萬用戶排序?現在思考起來是不是很簡單了,用一個桶排序,設置120個桶,分別表示年齡從1歲到120歲,然後把用戶都放入到對應的桶中,再一次取出,就完成了。

內容小結

  本篇文章介紹了三種應用不是很廣的排序算法,對數據要求比較高,但是如果數據的某些特徵符合算法特點,那就可以極大的節省排序時間。會非常高效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章