1. 程式人生 > >記憶體資料庫中的索引技術

記憶體資料庫中的索引技術

原創性申明

本文地址http://blog.csdn.net/zhujunxxxxx/article/details/42490335 轉載請註明出處

引言

傳統的資料庫管理系統把所有資料都放在磁碟上進行管理,所以稱作磁碟資料庫(DRDB: Disk-Resident Database)。磁碟資料庫需要頻繁地訪問磁碟來進行資料的操作,磁碟的讀寫速度遠遠小於CPU處理資料的速度,所以磁碟資料庫的瓶頸出現在磁碟讀寫上。

基於此,記憶體資料庫的概念被提出來了。記憶體資料庫(MMDB:Main Memory Database,也叫主存資料庫)[1],就是將資料全部或者大部分放在記憶體中進行操作的資料庫管理系統,對查詢處理、併發控制與恢復的演算法和資料結構進行重新設計,以更有效地使用CPU週期和記憶體。相對於磁碟,記憶體的資料讀寫速度要高出幾個數量級,將資料儲存在記憶體中相比從磁碟上訪問能夠極大地提高應用的效能。

近十幾年來,記憶體的發展一直遵循摩爾定律[2],記憶體的價格一直下降,而記憶體的容量一直在增加。現在的主流伺服器,幾百GB或者幾TB的記憶體都很常見,記憶體的發展使得記憶體資料庫得以實現。

由於記憶體資料庫與傳統的磁碟資料庫在設計和架構上都大不相同,所以傳統的資料庫索引不適用於記憶體資料庫。研究者為改進記憶體資料庫的索引結構做了相當多的研究跟工作。其中,影響較大的索引有早期的T樹、基於快取敏感(cacheconscious)的CSS/CSB+樹,Trie-tree和Hash等等。本文就這幾種有代表性的索引演算法進行研究和分析,為進一步改進記憶體資料庫索引演算法和提高索引效能打下堅實的基礎。

2、T-tree

2.1 T-tree

T-tree是針對主存訪問優化的索引技術[3]。T-tree是一種一個節點中包含多個索引條目的平衡二叉樹,T-tree的索引項無論是從大小還是演算法上都比B-tree精簡得多。T-tree的搜尋演算法不分搜尋的值在當前的節點還是在記憶體中的其他地方,每訪問到一個新的索引節點,索引的範圍減少一半。


圖2-1T-Tree的結點

T-tree索引用來實現關鍵字的範圍查詢。T-tree是一棵特殊平衡的二叉樹(AVL),它的每個節點儲存了按鍵值排序的一組關鍵字。T-tree除了較高的節點空間佔有率,遍歷一棵樹的查詢演算法在複雜程度和執行時間上也佔有優勢。現在T-tree己經成為記憶體資料庫中最主要的一種索引方式。

T-tree具有以下特點:1)左子樹與右子樹之差不超過1,2)在一個儲存節點可以儲存多個鍵值,它的最左與最右鍵值分別為這個節點的最小與最大鍵值,它的左子樹僅僅包含那些鍵值小於或等於最小鍵值的一記錄,同理右子樹只包括那些鍵值大於或等於最大鍵值的記錄,3)同時擁有左右子樹的節點被稱為內部節點,只擁有一個子樹的節點被稱為半葉節點,沒有子樹的節點被稱為葉子,4)為了保持空間的利用率,每一個內部節點都需要包含一個最小數目的鍵值。由此可知T-tree是一個每個結點含有多個關鍵字的平衡二叉樹,每個節點內的關鍵字有序排列,左子樹都要比根節點關鍵字小,右子樹都要比根節點關鍵字大。

在上述T-tree結點結構中,包含如下資訊:

(1)balance(平衡因子),其絕對值不大於1,balance=右子樹高度-左子樹高度;

(2)Left_child_ptr和Right_child_ptr分別表示當前結點的左子樹和右子樹指標;

(3)Max_Item表示結點中所能容納的鍵值的最大數;

(4)Key[0]至K[Max_Item-1]為結點記憶體放的關鍵字;

(5)nItem是當前節點實際儲存的關鍵字個數。

對於T-tree有如下特徵:

(1)與AVL樹相似,T-tree中任何結點的左右子樹的高度之差最大為1;

(2)與AVL樹不同,T-tree的結點中可儲存多個鍵值,並且這些鍵值排列有序;

(3)T-tree結點的左子樹中容納的鍵值不大於該結點中的最左鍵值;右子樹中容納的鍵值不小於該結點中的最右鍵值;

(4)為了保證每個結點具有較高的空間佔用率,每個內部結點所包含的鍵值數目必須不小於某個指定的值,通常為(Max_Item-2)(Max_Item為結點中最大鍵值目)。

2.2 T-tree索引的操作

用T-tree作為索引方式主要完成三個工作:查詢,插入,刪除。其中插入和刪除都是以查詢為基礎。下面分別介紹三種操作的流程。

2.2.1 查詢

T-tree的查詢類似於二叉樹,不同之處主要在於每一結點上的比較不是針對結點中的各個元素值,而是首先檢查所要查詢的目標鍵值是否包含在當前結點的最左鍵值和最右鍵值所確定的範圍內,如果是的話,則在當前結點的鍵值列表中使用二分法進行查詢;如果目標鍵值小於當前結點的最左鍵值,則類似地搜尋當前結點的左孩子結點;如果目標鍵值大於當前結點的最右鍵值,則類似地搜尋當前結點的右孩子結點。

2. 2.2 插入

T-tree的插入是以查詢為基礎,應用查詢操作定位目標鍵值插入位置,並記下查詢過程所遇到的最後結點。如果查詢成功,判斷此結點中是否有足夠的儲存空間。如果有,則將目標鍵值插入結點中;否則將目標鍵值插入此結點,然後將結點中的最左鍵值插入到它的左子樹中(此時是遞迴插入操作),之後結束;否則分配新結點,並插入目標鍵值;然後根據目標鍵值與結點的最大最小鍵值之間的關係,將新分配的結點連結為結點的左孩子或右孩子;對樹進行檢查,判斷T-tree的平衡因子是否滿足條件,如果平衡因子不滿足則執行旋轉操作。

2.2.3 刪除

T-tree的刪除操作也是以查詢為基礎,應用查詢操作定位目標鍵值。如果查詢失敗,則結束;否則令N為目標鍵值所在的結點,並從結點N中刪除目標鍵值;刪除節點後,如果結點N為空,則刪除結點N,並對樹的平衡因子進行檢查,判斷是否需要執行旋轉操作;如果結點N中的鍵值數量少於最小值,則根據N的平衡因子決定從結點N的左子樹中移出最大的鍵值或者右子樹中移出最小值來填充。

2.3 T-tree索引實現關鍵技術

實現T-tree索引即要實現T-tree的查詢,插入和刪除。其中又以查詢為基礎,對T-tree的維護也就是T-tree的旋轉為關鍵。當由於插入或刪除鍵值導致樹的失衡,則要進行T-tree的旋轉。使之重新達到平衡。

在插入情況下,需要依次對所有沿著從新建立結點到根結點路徑中的結點進行檢查,直到出現如下兩種情況之一時中止:某個被檢查結點的兩個子樹高度相等,此時不需要執行旋轉操作;某個被檢查結點的兩個子樹的高度之差大於1,此時對該結點僅需執行一次旋轉操作即可。

在刪除情況下,類似地需要依次對所有沿著從待刪除結點的父結點到根結點路徑中的結點進行檢查,在檢查過程中當發現某個結點的左右子樹高度之差越界時,需要執行一次旋轉操作。與插入操作不同的是,執行完旋轉操作之後,檢查過程不能中止,而是必須一直執行到檢查完根結點。

由此可以看出,對於插入操作,最多隻需要一次旋轉操作即可使T-tree恢復到平衡狀態;而對於刪除操作則可能會引起向上的連鎖反應,使高層結點發生旋轉,因而可能需要進行多次旋轉操作。

為了對T-tree進行平衡,需要進行旋轉操作,旋轉是T-tree中最關鍵也是最難的的操作,下面介紹T-tree旋轉的技術。旋轉可分為四種情況:由左孩子的左子樹的插入(或者刪除)引起的旋轉記為LL旋轉,類似有LR,RR及RL旋轉。插入時的情況與刪除類似。

3、CSS/CSB+樹

3.1 CSS-trees

3.1.1Introduction

CSS-trees(Cache-SensitiveSearch Trees),可以提供比二分查詢更為迅速的查詢操作而又不需大量額外的空間[4]。該技術在在一個以排好序的陣列頂端儲存一個目錄結構,且該目錄結構的節點大小與機器cache-line大小相匹配。將該目錄結構儲存在陣列中而無需儲存內部節點的指標,子節點可通過陣列偏移量定位,這與B+-trees不同。

3.2 FULL CSS-Tree

構造一棵結點包含m個鍵值的查詢樹,樹的深度是d,那麼一直到d-1的深度這棵樹是一棵完全(m+1)-查詢樹,而在d層葉子結點從左往右分佈。一棵m=4的例項樹圖3-1所示,其中方塊數就是結點數,且每個結點有四個鍵值。


CSS-Tree的結點可以儲存在陣列中,如圖3-2所示:


3.2.1 構造FULL CSS-Tree

將一個已排好序的陣列構造一棵相應的Full CSS-Tree,首先將陣列分為兩部分,並且在葉子節點和陣列元素間建立匹配。然後從最後一個內部節點開始,將節點直接左子樹的最大鍵值作為節點入口。對於某一些內部節點,也就是最深層最後一個葉子節點的祖先,可能完全鍵值,可以用陣列前半部分最後的一個元素來填充這些鍵值,所以在某些內部節點會有一些複製的鍵值。儘管要增量更新一棵Full CSS-Tree樹是很困難的,但構造這樣一棵樹花費並不大。實驗表明對於有著兩千五百萬鍵值的陣列,構造其相應的Full CSS-Tree花費的時間不足一秒。

3.2.2查詢Full CSS-Tree

從根節點開始,每次都查詢一個內部節點,利用二分查詢來決定查詢哪一個分支,重複上述行為直到葉子節點,最後將葉子節點與排好序的陣列進行匹配。

在節點內所有的查詢都由if-else構成,在內部節點進行二分查詢時,一直比較左邊的鍵值是否不小於要查詢的鍵值,當找到第一個比要查詢的鍵值小時,停止比較並進入右邊的分支(如果找不到這樣的值,就進入最左邊的分支)。這樣可以保證當在節點中有複製的值時,我們可以在所有複製的鍵值中找到最左邊的鍵值。

3.3 LevelCSS-Tree

對於每個節點有m個記錄的Full CSS-Tree,有嚴格的m個鍵值,所有的記錄都會被利用到。對於m=2t,我們定義每個節點只有只有m-1條記錄,並且有一個分支因子m。一棵Level CSS-Tree樹比一棵相應的Full CSS-Tree樹的深度大,因為分支因子是m而不是m+1,然後對於每一個節點,需要的同伴數更少。若N為一個已排好序的陣列元素所對應的節點數,Level CSS-Tree有logmN層,而Full CSS-Tree有logm+1N層。每個節點的同伴數是t,而Full CSS-tree是t*(1+2/(m+1)),所以Level CSS-tree的總的同伴數是logmN*t=log2N,而Full CSS-tree是logm+1N*t*(1+2/(m+1))=log2N*logm+1m*(1+2(m+1)).因此,Level CSS-Tree所需的companion數比Full CSS-tree少。另一方面,Level CSS-Tree需要logmN個cache accesses,遍歷logmN個節點,而Full CSS-Tree需logm+1N。

構建一棵Level CSS-Tree與Full CSS-Tree類似,我們也可以利用每個節點的空槽,來儲存最後一個分支的最大值,來避免遍歷整棵子樹來獲取最大元素值。查詢一棵Level CSS-Tree也與查詢Full CSS-Tree類似,唯一的不同就是子節點偏移量的計算。

3.4 CSB+-Tree

3.4.1Introduction

儘管CSS-Tree相比二分查詢和T-Trees查詢效能更好,但是它是於決策支援的有著相對靜態資料的工作負載設計的。CSB+-Tree(CacheSensitive B+-Trees)[4],是B+-Trees的變體,連續儲存給定節點的子節點,並且只儲存節點的第一個子節點的地址,其他子節點的地址可以通過相對這個子節點的偏移量計算獲得。由於只儲存一個子節點的指標,cache的利用率是很高的,與B+-Tree類似,CSB+-Tree支援增量更新。

CSB+-Tree有兩種變體,分段CSB+-Tree(SegmentedCSB+-tree)和完全CSB+-tree(FullCSB+-Tree).分段CSB+-Tree將子節點分段,在同一段的子節點連續儲存,在每個節點中,只有每一段的起始地址才會被儲存。當有分裂時,分段CSB+-Tree可以減少複製開銷,因為只有一個分段需要移動。完全CSB+-Tree為整個節點重新分配空間,因此減少了分裂開銷。

3.4.2CSB+-Tree上的操作

1)  Bulkload.

對於CSB+-Tree樹,一個有效的bulkload方法就是一層一層的建立索引結構。為每一個葉節點分配空間,計算在高層需要的節點數,並給該層分配連續的儲存空間。通過將低層每一個節點的最大值填入高層的節點,並設定每一個高層節點的第一個子節點指標。重複上述操作直到高層只有一個節點,且這個節點為根節點。因為同一層的所有節點是連續的,所以構造節點組無需額外的複製操作。

2)  Search

查詢CSB+-Tree與查詢B+-Tree類似,當最右邊節點的鍵值K比要查詢的鍵值小,給第一個子節點增加K的偏移量來獲得子節點的地址。例如,K是節點的第三個鍵值,可以用一個c語句找到子節點:child=first_child+3,其中child和first_child是節點的指標。

3)  Insertion

對CSB+-Tree的插入操作也與B+-Tree類似,首先要查詢鍵值的插入口,一旦定位至相應葉節點,判斷該葉節點是否有足夠的空間,如果有,就簡單的將鍵值放置在該葉節點中,否則,需要分裂該葉節點。

當需要分裂葉節點時,基於父節點是否有足夠的空間存放鍵值會產生兩種情況。假設父節點p有足夠的空間,令f為p的第一個子節點的指標,g為f指向的節點組,構建一個新的比g多了一個節點的節點組g’,將g中所有的節點複製到g’,g中要分裂的節點在g’中變為兩個節點,更新p中第一個子節點的指標f,使它指向g’,並且重新分配g。

當父節點沒有額外的空間並且自身需要分裂時,問題顯得更為複雜。令f為p中第一個節點的指標,需要構建新的節點組g’,將g中的節點均分至g’和g中,p中一半的鍵值轉移至g’中。為了將p分裂為p和p’,包含p的節點組需要像第一種情況一樣進行復制,或者,如果節點組也是滿的,我們需要遞迴的分裂p的父節點。父節點再重複上述操作。

4)Deletion

刪除操作類似於插入操作,一般的,簡單的定位資料入口並且加以刪除。無需調整樹保證50%的occupancy[5]

3.4.3Segmented CSB+-Tree

考慮128位元組的cache-line,CSB+-Tree中每個節點最多有30個鍵值,意味著每個節點可以有31個子節點,那麼一個節點組最大可達31*128近4KB,因此每一個分裂,需要複製4KB的資料來建立一個節點組,若cache-line更大,分裂一個節點的開銷將會更大。

修改節點結構可以減少分裂時的複製操作。可以將子節點分段,將每一段的地址儲存在節點中,每一段形成了一個節點組,只有在同一段的子節點被連續儲存。第一種考慮是固定每一個分段的大小,填充第一個分段的節點,一旦第一個分段滿了,就將節點放在第二個分段。若一個節點落在第二個分段,我們只需將第二個分段的節點複製到新的段中,而無需管第一個分段,若新的節點落在第一個分段(已經滿了),我們需要將資料從第一個分段移至第二個分段,在上述例子中,針對隨機插入,分裂產生的資料複製將會減少至1/2(1/2+3/4)*4KB=2.5KB.另一種就是允許每個分段的大小不同,最終將節點分為兩段。當有節點插入時,為這個節點所屬的分段建立一個新的分段,並更新相應分段的大小。在這種方法中,嚴格來說每次插入只涉及到一個分段(但當父節點也需要分裂,此時兩個分段都要複製),若一個新的節點等可能的落入其中一個分段,一個分裂產生的資料複製量為1/2*4KB=2KB,這種方法可以進一步的減少資料複製量。有兩個分段的SegmentedCSB+Tree如圖3-3所示(每個葉節點只有兩個鍵值):


分段CSB+-Tree可支援所有對樹的操作,方法與非分段CSB+-Tree類似,然而,查詢每個節點的右孩子比起非分段的CSB+-Tree的開銷大,因為需要找到孩子所在的分段。

3.4.4FULLCSB+-Tree

在FULLCSB+-Tree中,節點分裂的開銷比CSB+-Tree小。在CSB+-Tree中,當節點分裂時,需要將節點組整個複製到新的組中,而在FullCSB+-Tree中,只需訪問節點組的一半。對於這種轉移操作的源地址和目的地址有大的交叉,訪問的cache-line的數目限制在s內。FULLCSB+-Tree在分裂上的平均時間開銷是0.5s,而CSB+-Tree需時2s。

3.5 時間空間分析

假定鍵值、子節點指標、元組ID有著相同的空間大小K,n為葉節點數,c為cache-line的位元組數,t為分段CSB+-Tree的分段數。每個節點的槽值為m,其中m=c/K,假定節點大小與cache-line相同,各個引數及其相應的值如圖3-4所示:


圖3-5顯示了各種方法間分支因子、鍵值差異數、cache未命中數、每個節點其他差異的比較。B+-Tree的分支因子比CSS-Tree小,而CSB+-Tree儲存的子節點指標少,所需的分支因子與CSS-Tree相近。這導致每個方法的cache未命中次數不一樣。節點的分支因子越大,cache未命中次數相應的越小。在CSB+-Tree每增加一個分段,分支因子就會減少2,這是由於需要一個槽來儲存子節點指標,另一個槽來儲存新增分段的大小。一般而言,B+-Tree中節點的70%空間是滿的,需要相應的調整分支因子大小。[6]


圖 3-5  CSB+-Tree查詢時間分析

圖3-6顯示了在分裂時預期要訪問的cache-line數。由於複製時源地址和目的地址有交叉,所以FullCSB+-Tree所需的數目小。分裂開銷是插入操作總開銷的一部分,另一部分是定位優葉子節點產生的查詢開銷。分裂開銷相對獨立於樹的深度,這是由於大多數的分裂都發生在葉節點。然而,當樹的規模越來越大時,相應的查詢產生的開銷也會增大。CSB+-Tree的分裂開銷比B+-Tree大,但是插入產生的總開銷還與樹的規模有關。


圖3-7 顯示了不同演算法的空間需求。假定所有節點70%的空間是滿的[6],且分別計算內部節點和葉節點的空間大小,假定每個葉節點有2個兄弟節點指標。內部節點空間大小等於葉節點空間乘以1/(q-1)(q為分支因子),這裡不比較CSS-Tree,因為CSS-Tree不可能部分滿。


4    Trie-tree索引

4.1 Trie-tree

Trie-Tree[7]又稱單詞查詢樹鍵樹,是一種形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。

它有三個基本的性質:

1)根節點不包含字元,除根節點以外每一個節點都只包含一個字元

2)從根節點到某一節點,路徑上經過的的字元連線起來,為改節點對應的字串。

3)每個節點的所有子節點包含的字元都不相同。

圖4-1 展示了一個基本的tire-tree結構

圖4-1 tire-tree

字母樹的插入、刪除和查詢都非常簡單,用一個一重迴圈即可,即第i次迴圈找到前i個字母所對應的子樹,然後進行相應的操作。實現這棵字母樹,我們用最常見的陣列儲存(靜態開闢記憶體)即可,當然也可以開動態的指標型別(動態開闢記憶體)。至於結點對兒子的指向,一般有三種方法:

1)對每個結點開一個字母集大小的陣列,對應的下標是兒子所表示的字母,內容則是這個兒子對應在大陣列上的位置,即標號;

2)對每個結點掛一個連結串列,按一定順序記錄每個兒子是誰;

3)使用左兒子右兄弟表示法記錄這棵樹。

三種方法,各有特點。第一種易實現,但實際的空間要求較大;第二種,較易實現,空間要求相對較小,但比較費時;第三種,空間要求最小,但相對費時且不易寫。

搜尋字典[8]專案的方法為:

1) 從根結點開始一次搜尋;

2) 取得要查詢關鍵詞的第一個字母,並根據該字母選擇對應的子樹並轉到該子樹繼續進行檢索;

3) 在相應的子樹上,取得要查詢關鍵詞的第二個字母,並進一步選擇對應的子樹進行檢索。

4) 迭代過程……

5) 在某個結點處,關鍵詞的所有字母已被取出,則讀取附在該結點上的資訊,即完成查詢。

其他操作類似處理

Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

4.1.3 Trie樹的高階實現Double-Array 實現

可以採用雙陣列(Double-Array)實現,如圖1.3。利用雙陣列可以大大減小記憶體使用量,具體實現

兩個陣列,一個是base[],另一個是check[]。設陣列下標為i,如果base[i], check[i]均為0,表示該位置為空。如果base[i]為負值,表示該狀態為終止態(即詞語)。check[i]表示該狀態的前一狀態。

定義 1.對於輸入字元 c,從狀態 s 轉移到狀態 t,雙陣列字典樹滿足如下條件(4-2):

check[base[s] + c] = s

base[s] + c = t 


從定義1中,我們能得到查詢演算法,對於給定的狀態 s 和輸入字元 c 


t := base[s] + c;
if check[t] = s then
    next state := t
else
    fail
endif


我們知道雙陣列的實現方法是當狀態有新轉移時才分配空間給新狀態,或可以表述為只分配需要轉移的狀態的空間。當遇到無法滿足上述條件時再進行調整,使得其
 base 值滿足上述條件,這種調整隻影響當前節點下一層節點的重分配,因為所有節點的地址分配是靠 base 陣列指定的起始下標所決定的。插入的操作,假設以某字元開頭的 base 值為i,第二個字元的字元序列碼依次為c1, c2, c3…cn,則肯定要滿足base[i+c1], check[i+c1], base[i+c2], check[i+c2], base[i+c3], check[i+c3]…base[i+cn],check[i+cn]均為0

圖4-3 Double Array 實現

假設,Tire裡有n個節點,字符集大小為m,則DATrie的空間大小是n+cmc是依賴於Trie稀疏程度的一個係數。而多路查詢樹的空間大小是nm注意,這裡的複雜度都是按離線演算法(offline algorithm)計算的,即處理時已經得到整個詞庫。線上演算法(online algorithm)的空間複雜度還和單詞出現的順序有關,越有序的單詞順序空間佔用越小。查詢演算法的複雜度和被查詢的字串長度相關的,這個複雜度和多路查詢樹是一樣的。插入演算法中,如果出現重分配的情況,我們要附加上掃描子節點的時間複雜度,還有新base值確定的演算法複雜度。假如這兒我們都是用暴力演算法(for迴圈掃描),那插入演算法時間複雜度是O(nm + cm2)。。

實際編碼過程中,DATrie程式碼難度大過多路查詢樹,主要是狀態的表示不如樹結構那樣的清晰,下標很容易搞混掉。有個地方需要注意的是,base值正數表示起始偏移量,負數表示該狀態為終止態,所以在查詢新base值時,要保證查到的值是正數。如:空Trie狀態下,插入d時,因為第一個空地址是1,所以得到base=1-4=-3,這樣base正負的含義就被破壞了。

Trie是一種非常簡單高效的資料結構,但有大量的應用例項。

1字串檢索

事先將已知的一些字串(字典)的有關資訊儲存到trie樹裡,查詢另外一些未知字串是否出現過或者出現頻率。例如:

1)給出N個單片語成的熟詞表,以及一篇全用小寫英文書寫的文章,請你按最早出現的順序寫出所有不在熟詞表中的生詞。

2)給出一個詞典,其中的單詞為不良單詞。單詞均為小寫字母。再給出一段文字,文字的每一行也由小寫字母構成。判斷文字中是否含有任何不良單詞。例如,若rob是不良單詞,那麼文字problem含有不良單詞。

2)字串最長公共字首

Trie樹利用多個字串的公共字首來節省儲存空間,反之,當我們把大量字串儲存到一棵trie樹上時,我們可以快速得到某些字串的公共字首。

例如:給出N個小寫英文字母串,以及Q個詢問,即詢問某兩個串的最長公共字首的長度是多少?

解決方案:首先對所有的串建立其對應的字母樹。此時發現,對於兩個串的最長公共字首的長度即它們所在結點的公共祖先個數,於是,問題就轉化為了離線(Offline)的最近公共祖先(LeastCommon Ancestor,簡稱LCA)問題。

而最近公共祖先問題同樣是一個經典問題,可以用下面幾種方法:

1)利用並查集(Disjoint Set),可以採用採用經典的Tarjan演算法;

2)求出字母樹的尤拉序列(Euler Sequence)後,就可以轉為經典的最小值查詢(Range Minimum Query,簡稱RMQ)問題

3)排序

Trie樹是一棵多叉樹,只要先序遍歷整棵樹,輸出相應的字串便是按字典序排序的結果。例如:給你N個互不相同的僅由一個單詞構成的英文名,讓你將它們按字典序從小到大排序輸出。

4作為其他資料結構和演算法的輔助結構,如字尾樹,AC自動機等

1插入、查詢的時間複雜度均為O(N),其中N為字串長度。

2空間複雜度是26^n級別的,非常龐大(可採用雙陣列實現改善)。

4.1.6總結

Trie樹是一種非常重要的資料結構,它在資訊檢索,字串匹配等領域有廣泛的應用,同時,它也是很多演算法和複雜資料結構的基礎,如字尾樹,AC自動機等。

Trie Memory[9]是一種在記憶體中儲存和檢索資訊的方式,這種方式的優點是訪問速度快,具有冗餘儲存資訊的優點,主要的缺點是儲存空間利用率很低。

4.2.1基本的Trie Memory模型

假設我們需要跟蹤一系列的單詞集合,這些集合是字母組成的序列。這些單詞序列有各種各樣的長度,我們必須記住的是這些字母組成的有限序列在這個集合中。總得來說,我們需要判斷一個序列是不是這個集合的成員。
剛開始trie僅僅是register組成的一個集合,除此之外還有兩個register,一個是α另一個是δ,每一個register都有cell來儲存整個字母表,如果我們要儲存“space”的話,每個register必須擁有27個cell。
每一個cell都有空間來儲存其它register在記憶體中的地址,trie中的cell還沒有用來儲存資訊,通常包含的是register α的地址資訊。一個cell如果包含了非register α的register地址,則它表示儲存了資訊,這些資訊代表了這個cell的名稱,“A”表示A cell,“B”表示B cell。下一個register的地址在序列中。
下面用一個例子(圖2.1)來說明,為了讓例子簡單些,我們使用字母表的前5個字元來表示整體。然後用▽表示“space”,假設我們想儲存DAB,BAD,BADE,BE,BED,BEAD,CAB,CAD和A,接下來用圖來說明整個流程。在圖中每一行代表一個register,每個register有6個cell,最後一行代表第三個特殊的register叫做portal register,是我們進入系統記憶體的通道它除了是入口外,也和其它register是一樣的。其它register是編號的。register α將會選擇它們。剛開始的時候 register α是register 2。


圖4-4 基本Tire Memory模型

為了儲存DAB,我們引入地址“2”進入portal register的D 單元格,然後我們移動到register 2然後引入地址“3”到A單元格,然後我們進入到register 3後把地址“4”放入單元格B,最後我們移動到register 4 並且把地址“1”放入▽單元,它是終止引數,至此DAB儲存結束。然後我們轉到第二個單詞BAD,引入地址“5”進入portal register的B單元格來表示字母B,然後到register 5的A單元格寫入地址“6”,再到register 6的D單元格寫入地址“7”,最後到register 7的▽單元格寫入地址“1”。當我們開始儲存BADE時,我們發現B,A,D已經在trie中了,因此我們沿著已經存在的BAD的路徑到register 7然後引入地址“8”到單元格E中去,然後把地址“1”放入register 8的▽單元。

4.2.2 Register的型別

在剛才提到的結構中我們可以把register分為4種類型:

1)α(address) register來指向下一個儲存資訊的地址

2)δ(deletion) register

3)ν(next) register,下一步將要儲存的資訊(在空記憶體中,它是portal register)

4χ(exterior)型別χ是所有register中還沒有接受儲存資訊並且沒有被指向為下一個儲存位置的register

5ο(occupied)型別ο是存有資訊的register

在上述的所有的register中除了χ都在trie中,儲存和讀取操作現在能夠被簡單的公平的定義如下。

1)把第i個引數字元傳入下一個register,如果是第一個字元,則是portal register

2)選擇對應字串的的cell,如果第i個引數字元是字母表的第j個字元,選擇第jcell

3)檢測來自第i個單元的聯結

4)如果這種聯結使得register α

a) 通過αregister把聯結投射到連結的頭部,這樣就可以儲存資訊。

b)投射從αregister到連結頭部的聯結來建立一個“next”register(ν)

c)最後,把所有的從ν發出來的聯結指向αregister

5)如果源於第jcell的聯結指向非 αregister的話,移動到那個register去:

a) 如果是第一個register,這引數是一個儲存集合的成員(結束流程)

b)如果不是register 1的話,i1並且轉到第二步去。

使用相同的流程,但是不要使用投射,不要投射任何關係,如果聯結指向register 1,則這個引數是儲存集合的一個成員,如果任何點的聯結指向αregister,換句話說這個引數不是儲存集合的成員。

5HASH索引

HASH就是把關鍵詞直接對映為儲存地址,達到快速定址的目的,即Addr=H(key),其中key為關鍵詞;H為雜湊函式。主要有以下幾種常用的雜湊函式:

1)除留餘數法(DivisionMethod),H(key)=keyMOD p,p一般為質數;

2)隨機數法(RandomMethod),H(key)=random(key),random為隨機函式;

3)平方取中法(MidsquareMethod)。

HASH索引結構不需要額外的儲存空間,並且能夠在O(1)的時間複雜度下準確定位到所查詢的資料,將磁碟資料庫中的資料查詢時間代價優化至最小。Hash索引結構由於以上優點在磁碟資料庫中廣泛的運用。經歷長久的研究,先後發展出了連結桶雜湊(chainedbucket hash)[10],可擴充套件雜湊(extendible hash)[11]、線性雜湊(linearhash)[12]和修正的線性雜湊(modified linear hash)[13]。但是這些雜湊演算法雖然針對記憶體資料庫進行了少許優化,但是與傳統資料庫中所用的雜湊演算法沒有明顯不同。到了2007年,KennethA. Ross提出了基於現代處理器的Hash預取演算法[14]將SIMD指令集融入到Hash演算法中,才真正從記憶體索引的角度改進了雜湊演算法,提升資料組織的效率。

5.1 連結桶雜湊

連結桶雜湊(圖5-1)是一個靜態的結構,可用於記憶體中與磁碟中。因為它是靜態結構,不用對資料進行重組織,所以它速度很快。但這也是它的缺陷,面對動態資料,就顯得不合適了,因為連結桶雜湊必須在使用之前知道雜湊表的大小,而這恰恰很難預測。如果預測的表大小過小,其效能會大受影響;如果過大,空間浪費較為嚴重。最好情況下,它只有一些空間的浪費,用來存放指向下一個桶的指標。


5.2 可擴充套件雜湊

可擴充套件雜湊(圖5-2)引入了目錄檔案的概念,採用可隨資料增長的動態雜湊表,因此克服了連結桶雜湊的缺陷,其雜湊表大小不需要預先知道,一個雜湊節點包含多個項,當節點數量溢位時將其分裂為兩個節點。目錄按2的指數倍增長,當一個節點裝滿而且到達了一個特定的目錄大小目錄就會倍增。雜湊函式為每個鍵計算一個K位的二進位制序列,桶的數量總是使用從序列第一位或者最後一位算起的若干位[]。但是可擴充套件雜湊的一個問題是任意一個節點都會引起目錄的分裂,當雜湊函式不夠隨機時,目錄很可能增長的很巨大。


5.3 線性雜湊

線性雜湊(圖5-3)也使用動態的雜湊表,但是同可擴充套件雜湊有較大差別。線性雜湊選擇桶數總是使儲存塊的平均記錄保持與容量成一個固定的比例。而且雜湊桶不總是可以分裂,允許有溢位塊。當插入的記錄沒有對應的桶,將其雜湊值首位改為0,再次插入,否則直接插入對應桶或其溢位塊中。當記錄數量比容量達到一個閾值,增加一個桶,再分配。相對於可擴充套件雜湊,線性雜湊的增長較為緩慢,重組織的次數和代價都較小。同時,線性雜湊不需要存放資料桶指標的專門目錄項,且能更自然的處理資料桶已滿的情況,允許更靈活的選擇桶分裂的時機。


5.4 修正的線性雜湊

修正的線性雜湊相對於線性雜湊主要面向記憶體環境。通過使用更大的連續節點替代目錄,普通的線性雜湊由於有空節點而浪費空間。而且,除非有一個巧妙的方案解決潛在的虛擬記憶體對映機制問題,不然每次目錄增長時那個連續的節點都要被拷貝到一個更大的記憶體塊。修正的線性雜湊採用跟可擴充套件雜湊一樣的目錄,除了目錄為線性增長的,連結的是單個專案的節點和分配記憶體是從一個常規的記憶體池。這個演算法節點分裂的準則是基於效能,舉例來說,監控雜湊鏈的平均長度比監控儲存利用率能夠更直接的控制平均搜尋和更新時間[13]

5.5 Hash預取演算法

Hash預取演算法面向的是鍵和雜湊值都是32位的場景,特地對記憶體環境進行了優化。此演算法使用乘法雜湊,這種方法十分普遍、計算高效,更重要的是適用於向量,達到了一次計算多個雜湊函式的目的[14]。針對現代處理器的SIMD架構,將鍵值與雜湊值共同放在一個指令當中,達到大大減少指令數的目的,令每次所需的資料長度恰好等於L2的cacheline,大大降低了效能代價,在記憶體環境中,大大提高了cache的效能。

參考文獻:

[1] Garcia-Molina H, Salem K. Main memorydatabase systems: An overview[J]. Knowledge and Data Engineering, IEEETransactions on, 1992, 4(6): 509-516.

[2] Moore G E. Cramming more components ontointegrated circuits[J]. 1965.

[3] Lehman T J, Carey M J. A study of indexstructures for main memory database management systems[C]//Conference on VeryLarge Data Bases. 1986, 294.

[4] Jun Rao,Kenneth A. Ross:CacheConscious Indexing for Decision-Support in Main Memory,VLDB 1999: 78-89

[5]RaghuRamakrishnan. Database Management Systems. McGraw-Hill, 1997.

[6] AndrewYao. On random 2-3 trees. Acta Informatica, 9:159{170, 1978.

[7]Black,Paul E. (2009-11-16)."trie".Dictionary of Algorithms and Data Structures.NationalInstitute of Standards and Technology. Archived fromthe originalon2010-05-19.

[8] Knuth,Donald(1997). "6.3: Digital Searching".The Art of ComputerProgramming Volume 3: Sorting and Searching.Addison-Wesley.p.492.

[9] FREDKIN E.Tire Memory[J]. Communication of theACM,1960,3(9):490-499.

[10] Knuth D. The Art of ComputerProgramming 1: Fundamental Algorithms 2: Seminumerical Algorithms 3: Sortingand Searching[J]. 1968.

[11] Fagin R, Nievergelt J, Pippenger N, et al.Extendible hashing—a fast access method for dynamic files[J]. ACM Transactionson Database Systems (TODS), 1979, 4(3): 315-344.

[12] Litwin W. Linear Hashing: a new tool forfile and table addressing[C]//VLDB. 1980, 80: 1-3.

[13] Lehman T J, Carey M J. A study of indexstructures for main memory database management systems[C]//Conference on VeryLarge Data Bases. 1986, 294.

[14] Ross K A. Efficient Hash Probes on ModernProcessors[C]//ICDE. 2007: 1297-1301.