數據結構與算法之美-5 排序算法3 [MD]

博文地址html

個人GitHub 個人博客 個人微信 個人郵箱
baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目錄

13 | 線性排序:如何根據年齡給100萬用戶數據排序?

今天講的是三種時間複雜度是 O(n) 的排序算法:桶排序、計數排序、基數排序。git

由於這些排序算法的時間複雜度是線性的,因此咱們把這類排序算法叫做線性排序(Linear sort)。github

之因此能作到線性的時間複雜度,主要緣由是,這三個算法是非基於比較的排序算法,都不涉及元素之間的比較操做。算法

這幾種排序算法理解起來都不難,時間、空間複雜度分析起來也很簡單,可是對要排序的數據要求很苛刻,咱們學習的重點是掌握這些排序算法的適用場景編程

總結

  • 桶排序時間複雜度是O(n+k),計數排序時間複雜度是O(n+k),基數排序時間複雜度是O(n*k)
  • 桶排序空間複雜度是O(n),計數排序空間複雜度是O(k),基數排序空間複雜度是O(n+k)
  • 都是穩定的排序算法

桶排序 Bucket sort

核心思想是將要排序的數據分到幾個有序的桶裏,每一個桶裏的數據再單獨進行排序。桶內排完序以後,再把每一個桶裏的數據按照順序依次取出,組成的序列就是有序的了。數組

時間複雜度分析

  • 若是要排序的數據有 n 個,咱們把它們均勻地劃分到 m 個桶內,每一個桶裏就有 k=n/m 個元素。
  • 每一個桶內部使用快速排序,時間複雜度爲 O(k * logk)
  • m 個桶排序的時間複雜度就是 O(m * k * logk),由於 k=n/m,因此整個桶排序的時間複雜度就是 O(n*log(n/m))
  • 當桶的個數 m 接近數據個數 n 時log(n/m) 就是一個很是小的常量,這個時候桶排序的時間複雜度接近 O(n)

桶排序對要排序數據的要求

  • 首先,要排序的數據須要很容易就能劃分紅 m 個桶,而且,桶與桶之間有着自然的大小順序。這樣每一個桶內的數據都排序完以後,桶與桶之間的數據不須要再進行排序。
  • 其次,數據在各個桶之間的分佈是比較均勻的。若是數據通過桶的劃分以後,有些桶裏的數據很是多,有些很是少,很不平均,那桶內數據排序的時間複雜度就不是常量級了。在極端狀況下,若是數據都被劃分到一個桶裏,那就退化爲 O(nlogn) 的排序算法了。

桶排序比較適合用在外部排序中

所謂的外部排序就是數據存儲在外部磁盤中,數據量比較大,內存有限,沒法將數據所有加載到內存中。微信

好比說咱們有 10GB 的訂單數據,咱們但願按訂單金額進行排序,可是咱們的內存有限,只有幾百 MB,這個時候該怎麼辦呢?函數

  • 咱們能夠先掃描一遍文件,看訂單金額所處的數據範圍。假設通過掃描以後咱們獲得,訂單金額最小是 1 元,最大是 10 萬元。
  • 咱們將全部訂單根據金額劃分到 100 個桶裏,第一個桶咱們存儲金額在 1 元到 1000 元以內的訂單,第二桶存儲金額在 1001 元到 2000 元以內的訂單,以此類推。每個桶對應一個文件,而且按照金額範圍的大小順序編號命名(00,01,02...99)
  • 理想的狀況下,若是訂單金額在 1 到 10 萬之間均勻分佈,那訂單會被均勻劃分到 100 個文件中,每一個小文件中存儲大約 100MB 的訂單數據,咱們就能夠將這 100 個小文件依次放到內存中,用快排來排序。
  • 等全部文件都排好序以後,咱們只須要按照文件編號,從小到大依次讀取每一個小文件中的訂單數據,並將其寫入到一個文件中,那這個文件中存儲的就是按照金額從小到大排序的訂單數據了。

若是訂單金額分佈不均勻,能夠對數據較多的區間再次使用桶排序劃分爲更小的區間性能

計數排序 Counting sort

計數排序實際上是桶排序的一種特殊狀況。學習

當要排序的 n 個數據所處的範圍並不大的時候,好比最大值是 k,咱們就能夠把數據劃分紅 k 個桶。每一個桶內的數據值都是相同的,省掉了桶內排序的時間。

好比,高考考生成績排名。

計數排序動圖

這個動圖並不許確,其更像是桶排序的過程,由於看起來他是先把待排的元素一個個的放到了桶裏,這樣的空間複雜度只能是O(n),就無法優化爲O(k)

計數排序過程分析

假設有 8 個考生,其成績放在一個數組 A[8] 中:2,5,3,0,2,3,0,3。考生的成績從 0 到 5 分,咱們使用大小爲 6 的數組 C[6] 表示桶,其中下標爲分數、值爲對應的考生個數

  • 首先咱們須要遍歷一遍考生分數,而後就能夠獲得 C[6] 的值:[2,0,2,3,0,1]
  • 從中能夠看出,分數爲 3 分的考生有 3 個,小於 3 分的考生有 4 個
  • 因此,成績爲 3 分的考生在排序以後的有序數組 R[8] 中,會保存在下標爲 4,5,6 的位置

那咱們如何快速計算出每一個分數的考生在有序數組中對應的存儲位置呢?

  • 咱們首先對 C[6] 數組順序求和,求和後 C[k] 裏存儲的就是小於等於分數 k 的考生個數。順序求和後 C[6] = [2,2,4,7,7,8]

  • 咱們從後到前依次掃描數組 A:2,5,3,0,2,3,0,3
    • 好比,當掃描到 3 時,咱們能夠從數組 C 中取出下標爲 3 的值 7,也就是說,到目前爲止,包括本身在內,分數小於等於 3 的考生有 7 個,也就是說 3 是數組 R 中的第 7 個元素(也就是數組 R 中下標爲 6 的位置)。當 3 放入到數組 R 中後,小於等於 3 的元素就只剩下了 6 個了,因此相應的 C[3] 要減 1,變成 6。
    • 以此類推,當咱們掃描到第 2 個分數爲 3 的考生的時候,就會把它放入數組 R 中的第 6 個元素的位置(也就是下標爲 5 的位置)。
    • 當咱們掃描完整個數組 A 後,數組 R 內的數據就是按照分數從小到大有序排列的了。

從數組 A 中取數,也是能夠從頭開始取,可是就不是穩定排序算法了(由於最早取到的元素會被放到後面)

總結

  • 數排序只能用在數據範圍不大的場景中,若是數據範圍 k 比要排序的數據 n 大不少,就不適合用計數排序了。
  • 並且,計數排序只能給非負整數排序,若是要排序的數據是其餘類型的,要將其在不改變相對大小的狀況下,轉化爲非負整數。

桶排序空間複雜度是O(n),而計數排序空間複雜度是O(k)

基數排序 Radix sort

假設咱們有 10 萬個手機號碼,但願將這 10 萬個手機號碼從小到大排序,你有什麼比較快速的排序方法呢?

這個問題裏有這樣的規律:假設要比較兩個手機號碼 a,b 的大小,若是在前面幾位中,a 手機號碼已經比 b 手機號碼大了,那後面的幾位就不用看了。

排序過程

先按照最後一位來排序手機號碼,而後再按照倒數第二位從新排序,以此類推,最後按照第一位從新排序。通過 11 次排序以後,手機號碼就都有序了。

注意,這裏按照每位來排序的排序算法必定要是穩定的

時間複雜度分析

  • 根據每一位來排序,咱們能夠用剛講過的桶排序或者計數排序,它們的時間複雜度能夠作到 O(n)
  • 若是要排序的數據有 k 位,那咱們就須要 k 次桶排序或者計數排序,總的時間複雜度是 O(k*n)
  • 當 k 不大的時候,好比手機號碼排序的例子,k 最大就是 11,因此基數排序的時間複雜度就近似於 O(n)

基數排序對要排序數據的要求

  • 須要能夠分割出獨立的位來比較
  • 位之間有遞進的關係,若是 a 數據的高位比 b 數據大,那剩下的低位就不用比較了
  • 每一位的數據範圍不能太大,要能夠用線性排序算法來排序,不然,基數排序的時間複雜度就沒法作到 O(n)

實際上,有時候要排序的數據並不都是等長的,好比排序牛津字典中的 20 萬個英文單詞。這時,咱們能夠把全部的單詞補齊到相同長度(好比在後面補0),這樣就能夠繼續用基數排序了。

14 | 排序優化:如何實現一個通用的、高性能的排序函數?

如何選擇合適的排序算法?

  • 線性排序算法的時間複雜度雖然比較低,但適用場景比較特殊,因此不適合做爲通用的排序函數。
  • 若是對小規模數據進行排序,能夠選擇時間複雜度是 O(n^2) 的算法
  • 若是對大規模數據進行排序,時間複雜度是 O(nlogn) 的算法更加高效

爲了兼顧任意規模數據的排序,通常都會首選時間複雜度是 O(nlogn) 的排序算法來實現排序函數。

  • 歸併排序能夠作到平均狀況、最壞狀況下的時間複雜度都是 O(nlogn),可是歸併排序不是原地排序算法,空間複雜度是 O(n),佔用空間過大
  • 快速排序空間複雜度是O(logn),雖然快速排序在最壞狀況下的時間複雜度是 O(n^2),可是有不少方法能夠優化

如何優化快速排序?

時間複雜度退化爲O(n^2)的主要緣由是由於咱們分區點選得不夠合理。

爲了提升快速排序算法的性能,咱們要儘量地讓每次分區都比較平均。最理想的分區點是:被分區點分開的兩個分區中,數據的數量差很少。

兩個比較經常使用、比較簡單的分區算法:

  • 三數取中法:從區間的首、尾、中間,分別取出一個數,而後取這 3 個數的中間值做爲分區點
  • 隨機法:每次從要排序的區間中,隨機選擇一個元素做爲分區點

分析 C 語言中的排序函數 qsort()

雖然說 qsort() 從名字上看,很像是基於快速排序算法實現的,實際上它並不只僅用了快排這一種算法。

  • 要排序的數據量比較小的時候,qsort() 會優先使用歸併排序
    • 對於小數據量的排序,歸併排序須要額外內存空間的問題不大,用空間換時間
  • 要排序的數據量比較大的時候,qsort() 會改成用快速排序
    • qsort() 選擇分區點的方法就是三數取中法
    • qsort() 經過本身實現一個堆上的棧,手動模擬遞歸來解決遞歸太深會致使堆棧溢出的問題
  • 在快速排序的過程當中,當要排序的區間中元素的個數小於等於 4 時,qsort() 就退化爲比較簡單、不須要遞歸的插入排序
    • 由於在小規模數據面前,O(n^2) 時間複雜度的算法並不必定比 O(nlogn) 的算法執行時間長
    • qsort() 插入排序的算法實現中,也利用了哨兵這種編程技巧。雖然哨兵可能只是少作一次判斷,可是畢竟排序函數是很是經常使用、很是基礎的函數,性能的優化要作到極致。

2021-8-13

相關文章
相關標籤/搜索