前面已經說了倒排索引的基本原理了,原理非常簡單,也很好理解,關鍵是如何設計第二個倒排表,倒排表的第二列也很好設計,第一列就是關鍵了,為了滿足快速查詢的效能,設計第一列的結構,我們需要滿足以下兩個條件。
- 查詢非常快,能在極短的時間內找到我們需要的關鍵詞所在的位置。
- 新增關鍵詞也需要比較快,能保證輸入文件的時候儘可能的快。
除了上面兩個條件以外,還有一些加分項:
- 如果能儘可能少的使用記憶體,那肯定是好的
- 如果能順序的遍歷整個列,也肯定比較好
為了滿足能查詢,能新增,我們首先想到的是順序表,也就是連結串列了,連結串列的話,新增不成問題,關鍵是查詢的複雜度是O(n),這還能忍?所以連結串列第一個不考慮了。不過有一個連結串列的變種,我們是可以考慮一下,那就是跳躍表。
跳躍表(SkipList)
什麼是跳躍表呢?跳躍表也叫跳錶,我們可以把它看成是連結串列的一個變種,是一個多層順序連結串列的並聯結構的表,維基百科的定義是
是一種隨機化資料結構,基於並聯的連結串列,其效率可比擬於二叉查詢樹(對於大多數操作需要O(log n)平均時間)
我們通過一個圖來看一下跳躍表(圖片來源)
很明顯,最底層是一個順序表,然後在1,3,4,6,9節點上出現了第二層的連結串列,然後繼續在1,4,6節點上面出現了第三層連結串列,這樣構建出來的三層連結串列查詢效率比一層的就高了,一般情況下,跳錶的構建方式是按照概率來決定是否需要為這個節點增加一層,這裡在層 i 中的元素按某個固定的概率 p (通常為0.5或0.25)出現在層 i+1 中。平均起來,每個元素都在 1/(1-p) 個列表中出現,而最高層的元素(通常是在跳躍列表前端的一個特殊的頭元素)在 O(log1/p n) 個列表中出現。
查詢元素的時候,起步於頭元素和頂層列表,並沿著每個連結串列搜尋,直到到達小於或著等於目標的最後一個元素。通過跟蹤起自目標直到到達在更高列表中出現的元素的反向查詢路徑,在每個連結串列中預期的步數顯而易見是 1/p。所以查詢的總體代價是 O((log1/p n) / p),當p 是常數時是 O(log n)。通過選擇不同 p 值,就可以在查詢代價和儲存代價之間作出權衡。
比如還是上面那個圖,我們要查詢7這個元素,需要遍歷1—>4—>6—>7,比一層連結串列效率高不少吧
在實現跳錶的時候,雖然一般是用概率來決定是否需要增加當前節點的層級,但是實際中可以具體問題具體分析,比如我們知道底層連結串列大概有多長,那麼我們每格10個元素增加一個層級,那麼這樣的跳錶的儲存空間我們大概也能估算出來,平均查詢時間我們也能估算出來。
跳躍表是一個非常有用的資料結構,並且實現起來也比較容易,連結串列大家都知道實現,那麼跳躍表就是一組連結串列啦,只是增加和刪除的時候需要操作多個連結串列而已。
我的專案中暫時沒有使用跳躍表,後續有需求的時候再加上吧,所以大家看不到程式碼了。讓你失望了。呵呵。
一般跳躍表可以和hash配合起來使用,因為hash有桶,佔用的記憶體較大,如果將hash值存在跳躍表中,用mmap把跳躍表載入到記憶體中,那麼既節省了記憶體,又有一個較好的查詢速度,而且實現起來還挺簡單。
跳躍表用來實現搜尋引擎的自增長型別的主鍵也比較合適,首先在搜尋引擎中,主鍵的查詢並不是那麼頻繁,一般查詢都是通過關鍵字查詢的,對主鍵來說,對查詢速度要求並不是特別高,只有在修改主鍵的時候需要進行查詢,其次自增長的主鍵一般情況下插入操作直接在連結串列後面append就可以了,不用進行查詢,所以插入的時候也比較快。
雜湊表
處理跳躍表,雜湊表也是一個實現方式,雜湊表是根據關鍵字(Key value)而直接訪問在記憶體儲存位置的資料結構。也就是說,它通過計算一個關於鍵值的函式,將所需查詢的資料對映到表中一個位置來訪問記錄,這加快了查詢速度。這個對映函式稱做雜湊函式,存放記錄的陣列稱做雜湊表,也叫雜湊表。
雜湊是大資料技術的基礎,大家應該都有了解了,這裡就不深度展開了,演算法導論有一章已經講得非常清楚了,這裡說說我覺得比較有意思的一個雜湊的東西。
雜湊表的核心是雜湊演算法,一個好的雜湊演算法可以讓碰撞產生得更少,查詢速度越接近於O(1),所以一個好的雜湊演算法非常重要。
雜湊演算法很多,說都說不完,不同的演算法適應不同的場景,我知道的,傳說中有一個雜湊演算法,來自魔獸世界(!!!!為了部落!!!!),號稱暴雪雜湊,該演算法產生的雜湊值完全無法預測,被稱為"One-Way Hash"( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。
以下是這個演算法的Go語言實現,在我的專案中也有,不過後來我沒有用hash表,所以刪掉了,號稱有這個演算法,所有字串都不在話下,碰撞概率很低。
// 初始化hash計算需要的基礎map table
func initCryptTable() {
var seed, index1, index2 uint64 = 0x00100001, 0, 0
i := 0
for index1 = 0; index1 < 0x100; index1 += 1 {
for index2, i = index1, 0; i < 5; index2 += 0x100 {
seed = (seed*125 + 3) % 0x2aaaab
temp1 := (seed & 0xffff) << 0x10
seed = (seed*125 + 3) % 0x2aaaab
temp2 := seed & 0xffff
cryptTable[index2] = temp1 | temp2
i += 1
}
}
}
// hash, 以及相關校驗hash值
func HashKey(lpszString string, dwHashType int) uint64 {
i, ch := 0, 0
var seed1, seed2 uint64 = 0x7FED7FED, 0xEEEEEEEE
var key uint8
strLen := len(lpszString)
for i < strLen {
key = lpszString[i]
ch = int(toUpper(rune(key)))
i += 1
seed1 = cryptTable[(dwHashType<<8)+ch] ^ (seed1 + seed2)
seed2 = uint64(ch) + seed1 + seed2 + (seed2 << 5) + 3
}
return uint64(seed1)
}複製程式碼
雜湊表的實現方式有很多中,最最基礎的就是陣列+連結串列的形式了,也叫開鏈雜湊,陣列長度就是雜湊的桶的長度,連結串列用來解決衝突,插入資料的時候如果雜湊碰撞了,把具體節點掛在該節點後面的連結串列上,查詢資料時候有衝突,就繼續線性查詢這個節點下的連結串列。
還有一種叫閉鏈雜湊,閉鏈雜湊實際是一個迴圈陣列,陣列長度就是桶的長度,插入資料的時候有衝突的話,移動到該節點的下一個,直到沒有衝突為止,如果移動到了末尾的話,轉到陣列的頭部,查詢資料的時候類似。
這裡又出現一個小問題,如果碰撞了的話,不管是開鏈還是閉鏈雜湊,都需要進行線性匹配,而且比較的是兩個資料的實際值,所以不管是那種雜湊實現,都需要在節點中儲存原始的資料資訊,不然碰撞的時候沒辦法匹配了,這樣就衍生出來兩個問題:
- 如果Key是一個比較長的字串,那麼雜湊表的儲存空間相應就要變得比較大,才能儲存住這個字串用來比較。
- 如果是字串比較,那麼速度比較慢,當碰撞較多的時候,會影響效能,雖然現在的機器這些個比較都不在話下了。
然而,雷霆崖的程式設計師想了一個更好的辦法,用上面那個雜湊函式,通過不同的dwHashType
,雜湊了三次,得到三個整數,第一個整數用來確定位置,第二和第三個整數用來代替原始字串,儲存在雜湊表的節點中用於解決衝突,當要查詢時,先計算待查詢的Key的三個雜湊值,然後用第一個去定位,如果第一個值沒衝突,返回節點,如果衝突了,那麼不管是開鏈實現方式還是閉鏈實現方式,查詢下一個節點,然後比較這兩個節點的第二和第三雜湊值,如果一樣的話,返回節點,不一樣的話繼續查詢下一個,通過這麼倒騰,首先,儲存空間的問題解決了,每個雜湊節點只需要存3個整數,空間固定了,第二個問題也解決了,比較兩個整數總比比較字串快多了吧。
好了,跳躍表和雜湊表就是這些了,在我的程式碼中沒有跳躍表,後續才會加上,雜湊表本來有,後來為了節省記憶體空間,用了B+樹來替代雜湊表了,所以雜湊表的程式碼暫時看不到,不過我已經把暴雪雜湊寫上面了哈。
下面一章會詳細將一下B+樹了,我程式碼裡面也是用的B+樹,而且幾乎所有的資料庫的索引也是用的B+樹。
最後,打一廣告,我的微信公眾號,目前沒什麼訂閱者 T_T,歡迎大家掃描一下下面的微信公眾號訂閱,首先會在這裡發出來:)