“後半”有序的分組

技術小能手發表於2018-11-21

回顧一下前半有序的說法:我們要把資料集T按欄位a,b排序時,如果T已經對a有序,則可以利用這一特點實現高效能演算法。但後半有序卻不是對稱地把問題理解成T已經對b有序時要對a,b排序的任務,這個“後半”序資訊並沒有多大利用價值。這裡說的“後半”有序問題是指:如果T已經對a,b有序,現在我們要將T按b排序。

這其實是很常見的情況。多維分析的資料集一般總會按某套(比如按日期、帳戶)維度已經排好序,但我們可能希望按第二維度(帳戶)再排序或分組,是否可以利用這個特點提高效能呢?

我們用a(i),b(i)表示表T中第i條記錄的欄位a,b取值。T對a,b有序,意味著如果a(i)=a(i+1)則必然有b(i)<=b(i+1),即相鄰的a值相同的記錄的b值會構成一個有序子集。這時候如果我們能夠把這些有序子集都找出來(遍歷一次將a值發生變化的位置記下來即可)再進行歸併排序,就可以得到對b有序的結果集了。而歸併排序的複雜度相對較低(nlogk,k是歸併路數,一般遠小於n),遍歷一次的成本也不算高(相對於排序演算法的複雜度nlogn),看起來這會是一個有效的手段。

但是,我們產生了一批有這樣特徵的資料來做實驗,發現並沒有比直接針對b使用快速排序(Quicksort)演算法有明顯的優勢。

其實道理很簡單,快速排序本來就是一種遞迴的分段排序再歸併的演算法,對於一個已經大體有序的陣列,快速排序的速度已經能夠很快。它的複雜度nlogn是指平均情況,對於上述這種“後半”有序的b值來講,快速排序的複雜度也就是相當於nlogk的複雜度,和我們設想的辦法一樣。

這個方法對於排序沒有效果,但對於分組卻有意義。

我們把任務改變一下:對T按b分組並計算每個組的某種聚合值。

通用分組演算法一般採用hash方案,即計算每個分組鍵的hash值,相同hash值的記錄被分揀到一個小集合,然後在這個小集合內遍歷找同值再聚合。複雜度,也就是比較次數,取決於hash函式重位元速率。在hash空間比較小的時候,重位元速率就會比較高,比較次數會較多,效能就受影響。為了保證效能,就需要分配較大的記憶體來存放hash表。另外,有些資料型別(長字串)的hash計算也比較慢,這也會影響效能。

而如果利用“後半”有序的特徵則可以避免hash運算。我們將資料按b排序,然後再執行有序分組,即每條記錄只與上一條記錄比較,發現有不同時則新建一個分組,相同則聚合到當前組中。這樣的分組運算的複雜度為n,而且沒有hash計算和重位元速率的問題。如果排序足夠快,就可以獲得比hash分組更快的效能,而且並不需要太多記憶體用於存放hash表。

沒有這個“後半”有序的前提時,先排序的時間成本通常會很高,超過hash分組的耗時。所以,排序分組的方案雖然簡單易行,但商用資料庫一般卻不採用,有些開源資料庫或報表工具為了圖省事才會使用。然而,如果資料滿足了這個條件時,排序就會快很多,有序分組的時間複雜度又很低,這樣配合起來就可以獲得更高的分組效能。

排序分組還有個好處在於,結果集是天然有序的。這樣,易於實現大資料以及分散式分組運算。

目前的大資料分組一般也是採用hash方案,將原始資料先根據hash值分成幾堆,然後分別針對每一堆再做分組。如果是分散式系統,在“分堆”時就會有大量的網路傳輸動作。而如果資料滿足“後半”有序的條件,則可以採用類似大排序的方案來做分組,即讀入每一段後進行排序分組再寫出成臨時資料,然後再將這些臨時資料歸併,因為每一段臨時資料都是有序的,後續的歸併就可以進行。在分散式運算時也可以每個分機獨立做分組,最後統一歸併彙總即可,過程中只有歸併那一步需要網路傳輸。

排序分組演算法因為效能不佳而在大多數場合被棄用,但在特定條件下卻又能發揮出更好的效果。其實很少有什麼演算法一定好或一定壞,適合資料和運算特徵的演算法才是好演算法。

在記憶體中要執行排序分組演算法很簡單,只要分兩步先排序再分組就可以了,用基本方法組合即可,不需要實現專門方法。而對於大資料則不可以先做排序後再基於結果再做分組,因為大排序本身就要寫出快取資料,再來一輪大分組又寫一次,就得不償失了。針對大資料執行排序分組演算法需要把這個演算法固化到分組運算中,而不能用基本運算組合出來。所以,集算器在大分組方法中提供了選項供程式設計師根據現場情況決定是否使用排序分組演算法。

原文釋出時間為:2018-11-20

本文作者:蔣步星

本文來自雲棲社群合作伙伴“資料蔣堂”,瞭解相關資訊可以關注“資料蔣堂”。


相關文章