前面已經說了倒排索引的基本原理了,原理很是簡單,也很好理解,關鍵是如何設計第二個倒排表,倒排表的第二列也很好設計,第一列就是關鍵了,爲了知足快速查找的性能,設計第一列的結構,咱們須要知足如下兩個條件。前端
查找很是快,能在極短的時間內找到咱們須要的關鍵詞所在的位置。程序員
添加關鍵詞也須要比較快,能保證輸入文檔的時候儘量的快。算法
除了上面兩個條件之外,還有一些加分項:數據庫
若是能儘量少的使用內存,那確定是好的數組
若是能順序的遍歷整個列,也確定比較好微信
爲了知足能查找,能添加,咱們首先想到的是順序表,也就是鏈表了,鏈表的話,添加不成問題,關鍵是查找的複雜度是O(n),這還能忍?因此鏈表第一個不考慮了。不過有一個鏈表的變種,咱們是能夠考慮一下,那就是跳躍表。數據結構
什麼是跳躍表呢?跳躍表也叫跳錶,咱們能夠把它當作是鏈表的一個變種,是一個多層順序鏈表的並聯結構的表,維基百科的定義是app
是一種隨機化數據結構,基於並聯的鏈表,其效率可比擬於二叉查找樹(對於大多數操做須要O(log n)平均時間)less
咱們經過一個圖來看一下跳躍表(圖片來源)jsp
很明顯,最底層是一個順序表,而後在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。文章的更新頻率會在一週3到5篇左右吧,歡迎你們掃描一下下面的微信公衆號訂閱,首先會在這裏發出來:)