一種神奇的數據結構—小波樹

本文轉載自:http://chuansong.me/n/2035229

Succinct簡潔數據結構是一種來自生物信息學的研究成果,根據Wiki百科的定義是在數據壓縮存儲達到接近信息熵下界時仍然保持高效的查詢性能的一類數據結構。聽起來有些拗口,通俗點說就是既能壓縮存儲還能高速檢索。Succinct數據結構有很多,小波樹(wavelet tree)是其中最常見有效的之一。小波(wavelet)跟圖像裏的小波變換沒什麼關係,爲什麼起了這麼迷惑的名字很難知曉,筆者猜測大概從結構上看起來有些像圖像處理裏小波變換吧。


小波樹總體上是針對一個字符串構造的一種數據結構,用來回答Rank和Select這樣的查詢。Rank操作代表這樣的含義:對於一個{0,1}構造的位圖向量,Rank(position,1)的含義是位圖中position位置之前1的數量。那麼對於一個字符串來說,Rank(position, alpha)代表字符串中position位置之前字符alpha的數量,例如下圖的字符串中,Rank(5,e) = 2。

Select是Rank的反向操作:對於一個{0,1}構造的位圖向量,Select(frequency,1)代表第frequency次出現{1}的位置。例如在下面的位圖向量中,Select(4,1) = 7。


能夠有效支持Rank/Select操作的位圖向量是許多Succinct數據結構構造的基石,也包括小波樹。假如位圖向量可以正好放入一個word中,比如64bit,那麼Rank其實就是一次popcnt操作,Intel的CPU可以採用SSE指令在幾個週期內完成該操作。當位圖擴大之後,爲了能夠支持高速的Rank操作,就需要設計內存佈局,使得最終的操作都將轉化爲單個word之上的popcnt,因此Rank性能的瓶頸將取決於cache miss的次數——一次cache miss將導致最長100ns的延遲,相比之下幾個指令週期的popcnt可以忽略不計。在Succinct數據結構剛剛出現的時候,早期的位圖向量做一次Rank操作需要至少5,6次cache miss,後來日本人Takeshi發明了3次cache miss的位圖向量,而我們此前團隊的August進一步改進,做到了僅需1次cache miss,這是目前最優的位圖向量佈局。相比Rank操作,Select要昂貴得多,可以類比數學中的積分對比求導的性能差異。


下邊我們來看一下最常見的二叉小波樹是如何構造的。二叉小波樹構造過程就是把字符串轉化爲一顆平衡二叉樹位圖的過程,0代表一半的符號,1代表另一半符號。在樹的每一層,字符表都要重新編碼,直到最底層沒有任何歧義。遞歸的構造過程如下:

  1. 取字符串的字母表,將前半部分編碼爲0,後半部分編碼爲1,例如{a,b,c,d}就變成了{0,0,1,1}。這個時候編碼是有歧義的,比如你不能根據0就猜測該字符是a還是b。

  2. 把0表示的字符{a,b}分組做爲一個子樹;把1表示的字符{c,d}分組做爲另一顆一個子樹。

  3. 在每一顆上都重複如上步驟直到子樹只包含1個或者2個字符,這樣0或者1就可以明確表示而沒有任何歧義了。


例如對於字符串"Peter Piper picked a peck of pickled peppers",構造出的二叉小波樹如圖所示,這裏,空格和字符串終止符我們分別特殊符號來表示,比如"_"和"$",那麼整個串的字符表包含{$,P,_,a,c,d,e,f,i,k,l,o,p,r,s,t}, 首先它們會被編碼映射成{0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1},左邊的子樹基於編碼爲0的字符集創建,包含{$,P,_,a,c,d,e,f} 然後該子樹重新編碼爲{0,0,0,0,1,1,1,1},再重複子樹創建過程。


可以看到整個二叉樹是平衡的,因此,我們可以把每層所有的位圖連接在一起合併成一個大的位圖,這樣樹的每一層都是一個等長的位圖,而樹的高度則是字符集尺寸的對數O(log_2{A})。

在一個小波樹構造好之後,一個字符串上的rank操作需要從樹的最頂端位圖開始操作,直到最底層的位圖,因此一共需要N次位圖上的rank操作,這裏N等於小波樹的高度。例如查詢Rank(5,e)的過程可以由圖看出來:首先在最上層,{e}我們編碼爲0,因此在這一層執行rank(5,0)的操作,我們可以得到0的數量是4。



這個結果可以引導我們到下一層從那個位置開始執行rank操作——在{0}表示的子樹中,第4個位置,由於在該層{e}已經編碼爲1,因此我們需要執行rank(4,1),重複該步驟直到最底層。

除了二叉小波樹之外,還有霍夫曼小波樹,針對文本型序列可以提供更高的壓縮比,以及更加快速的小波矩陣,本文的題圖採納了Matrix電影的片頭,就是類比小波矩陣看起來成片的位圖向量。


那麼擁有可以提供Rank/Select能力的小波樹,我們都可以做一些什麼工作呢?這裏有一些基於Rank/Select的擴展型查詢:

  1. Lookup(T, p) : 在一個字符串序列T,返回位置在p的序列項。

  2. Quantile(T, p, sp, ep) : 在一個字符串序列T,返回位置在sp和ep之間的第p個最大值。

  3. FreqList (T, k, sp, ep) : 在一個字符串序列T,返回位置在sp和ep之間的最頻繁的k個值。

  4. RangeList (T, sp, ep, min, max) : 在一個字符串序列T,返回位置在sp和ep之間,取值範圍在min和max之間的所有項。

  5. RangeList (T, sp, ep, min, max) : 在一個字符串序列T,返回位置在sp和ep之間,取值範圍在min和max之間的所有項的頻率。

這些擴展型查詢基本都是圍繞一個字符串序列某個區間之內的統計信息來做,聰明的讀者一定可以想到可以拿它來做數據分析。它們大部分的複雜度跟Rank/Select相差不大,都是正比於小波樹的高度。因此可以看到,如果拿小波樹去表示一個序列,不論該序列有多長,查詢性能都不受影響,因爲它只受限於序列字符集的大小。舉例來說,如果小波樹表示的是中文文本,那麼任何操作的時間只跟log_2(65536)這個值有關,這意味着小波樹的高度最多隻有16層,這是效率多麼驚人的數據結構!當然,在實際工程實現中,還會受到其他的限制,比如cache miss,內存分配,等等。做爲一個壓縮型的數據結構,表徵全部文本,所消耗的空間最多隻有16個位圖向量。

如果嫌上邊的敘述仍然過於抽象,我們接下來可以舉一些更加實際的例子。

第一個例子是全文搜索。全文搜索的意思是,給定一段文本,我們可以快速的查詢任意子串是否在該文本中出現,並且統計它的頻率和出現的位置。常規的做法是構造後綴樹或者後綴數組,Ferragina和Manzini在前人工作的基礎之上把經過BWT變換後的後綴數組放到了小波樹上,這樣使得查詢複雜度僅跟小波樹的高度有關,而跟文本的尺寸無關,在巨大的文本序列基礎上,這是多麼大的性能提升!這就是著名的以他們名字首字母命名的FM-Index,是已知小波樹最成功的用途之一。

第二個例子是倒排索引。假定目前我們針對某文檔集合需要構建索引,同時還需要提供詞在每個文檔中的位置信息,如果按照倒排索引的做法,我們需要同時存儲文本信息和倒排索引本身,而如果把文檔表示爲一個巨大的字符串插入到小波樹之中,我們可以僅僅使用一個壓縮數據結構小波樹而無需任何其他開銷。在該小波樹中,爲了能夠提取任何原始文本,我們只需要採用Lookup操作;爲了訪問某個詞C對應的倒排鏈的第i個位置,我們只需要調用Select(i,C)操作,進一步的,Rank操作可以繼續擴展爲針對序列多個區間之間求交,所以我們甚至可以直接把構建好的倒排索引插入到小波樹中,這甚至能夠提供比原始倒排表還快的檢索性能——因爲小波樹的求交複雜度跟文檔數量無關!

第三個例子是圖。一個圖常見的表示形式是鄰接列表。給定這樣一種數據結構,我們可以列出任何一個圖節點的相鄰節點。如果我們把這樣的鄰接列表放到小波樹中會如何呢?我們可以以常數時間獲取到任何一個圖節點的某一個相鄰節點,以常數時間獲取到某幾個圖節點共同的相鄰節點——看到這裏聰明的讀者想到了什麼?微博的共同關注,以常數時間獲取!

當然,小波樹並不是沒有缺點的,首先,它只能夠存在於內存之中,迄今我們還沒有發現一個可以在硬盤或者SSD上表徵小波樹的手段;其次,它是一個靜態數據結構,這意味着只要有數據發生變化,我們不得不重新構建整個小波樹而無法做到增量更新。然而,這並不妨礙它成爲一款來自學術界的出色工具,有了它的協助,我們將得以在某些場景下提供極速的訪問性能。目前,小波樹在國內基本上屬於無人知曉包括BAT(
轉載注:據我所知,國內的一款開源的搜索引擎:sf1r中的一種索引算法就是類似的算法!14年還是13年在github上開源的!),然而所有它的使用者都獲得了豐厚的回報——在系統設計上,永遠是算法 > 架構,因爲前者往往帶來的是數量級的性能提升。最後順道提及一下,在看到“算法”二字時,大多數人的反應是諸如<算法導論>,程序設計競賽等等,固然這些很重要,也很燒腦,然而對於現代大型系統構建,這些通用的算法早已經不是在工作中能夠用到的範疇,我們所提的算法,更多是指能夠在系統層面上帶來質變的創新,獲取它們的渠道幾乎只有源源不斷的學術界創新。

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