第二十三、四章:楊氏矩陣查找,倒排索引關(guān)鍵詞Hash不重復(fù)編碼實(shí)踐
作者:July、yansha。編程藝術(shù)室出品。
出處:結(jié)構(gòu)之法算法之道。
前言
本文闡述兩個問題,第二十三章是楊氏矩陣查找問題,第二十四章是有關(guān)倒排索引中關(guān)鍵詞Hash編碼的問題,主要要解決不重復(fù)以及追加的功能,同時也是經(jīng)典算法研究系列十一、從頭到尾徹底解析Hash表算法之續(xù)。
OK,有任何問題,也歡迎隨時交流或批評指正。謝謝。
第二十三章、楊氏矩陣查找
楊氏矩陣查找
先看一個來自算法導(dǎo)論習(xí)題里6-3與劍指offer的一道編程題(也被經(jīng)常用作面試題,本人此前去搜狗二面時便遇到了):
在一個二維數(shù)組中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數(shù),輸入這樣的一個二維數(shù)組和一個整數(shù),判斷數(shù)組中是否含有該整數(shù)。
例如下面的二維數(shù)組就是每行、每列都遞增排序。如果在這個數(shù)組中查找數(shù)字6,則返回true;如果查找數(shù)字5,由于數(shù)組不含有該數(shù)字,則返回false。

本Young問題解法有二(如查找數(shù)字6):
1、分治法,分為四個矩形,配以二分查找,如果要找的數(shù)是6介于對角線上相鄰的兩個數(shù)4、10,可以排除掉左上和右下的兩個矩形,而遞歸在左下和右上的兩個矩形繼續(xù)找,如下圖所示:

2、首先直接定位到最右上角的元素,再配以二分查找,比要找的數(shù)(6)大就往左走,比要找數(shù)(6)的小就往下走,直到找到要找的數(shù)字(6)為止,如下圖所示:
上述方法二的關(guān)鍵代碼+程序運(yùn)行如下圖所示:

試問,上述算法復(fù)雜么?不復(fù)雜,只要稍微動點(diǎn)腦筋便能想到,還可以參看友人老夢的文章,Young氏矩陣:http://blog.csdn.net/zhanglei8893/article/details/6234564,以及IT練兵場的:http://www.jobcoding.com/array/matrix/young-tableau-problem/,除此之外,何海濤先生一書劍指offer中也收集了此題,感興趣的朋友也可以去看看。
第二十四章、經(jīng)典算法十一Hash表算法(續(xù))、倒排索引關(guān)鍵詞不重復(fù)Hash編碼
本章要介紹這樣一個問題,對倒排索引中的關(guān)鍵詞進(jìn)行編碼。那么,這個問題將分為兩個個步驟:
- 首先,要提取倒排索引內(nèi)詞典文件中的關(guān)鍵詞;
- 對提取出來的關(guān)鍵詞進(jìn)行編碼。本章采取hash編碼的方式。既然要用hash編碼,那么最重要的就是要解決hash沖突的問題,下文會詳細(xì)介紹。
有一點(diǎn)必須提醒讀者的是,倒排索引包含詞典和倒排記錄表兩個部分,詞典一般有詞項(xiàng)(或稱為關(guān)鍵詞)和詞項(xiàng)頻率(即這個詞項(xiàng)或關(guān)鍵詞出現(xiàn)的次數(shù)),倒排記錄表則記錄著上述詞項(xiàng)(或關(guān)鍵詞)所出現(xiàn)的位置,或出現(xiàn)的文檔及網(wǎng)頁ID等相關(guān)信息。
24.1、正排索引與倒排索引
咱們先來看什么是倒排索引,以及倒排索引與正排索引之間的區(qū)別:
我們知道,搜索引擎的關(guān)鍵步驟就是建立倒排索引,所謂倒排索引一般表示為一個關(guān)鍵詞,然后是它的頻度(出現(xiàn)的次數(shù)),位置(出現(xiàn)在哪一篇文章或網(wǎng)頁中,及有關(guān)的日期,作者等信息),它相當(dāng)于為互聯(lián)網(wǎng)上幾千億頁網(wǎng)頁做了一個索引,好比一本書的目錄、標(biāo)簽一般。讀者想看哪一個主題相關(guān)的章節(jié),直接根據(jù)目錄即可找到相關(guān)的頁面。不必再從書的第一頁到最后一頁,一頁一頁的查找。
接下來,闡述下正排索引與倒排索引的區(qū)別:
一般索引(正排索引)
正排表是以文檔的ID為關(guān)鍵字,表中記錄文檔中每個字的位置信息,查找時掃描表中每個文檔中字的信息直到找出所有包含查詢關(guān)鍵字的文檔。正排表結(jié)構(gòu)如圖1所示,這種組織方法在建立索引的時候結(jié)構(gòu)比較簡單,建立比較方便且易于維護(hù);因?yàn)樗饕腔谖臋n建立的,若是有新的文檔假如,直接為該文檔建立一個新的索引塊,掛接在原來索引文件的后面。若是有文檔刪除,則直接找到該文檔號文檔對因的索引信息,將其直接刪除。但是在查詢的時候需對所有的文檔進(jìn)行掃描以確保沒有遺漏,這樣就使得檢索時間大大延長,檢索效率低下。
盡管正排表的工作原理非常的簡單,但是由于其檢索效率太低,除非在特定情況下,否則實(shí)用性價值不大。

倒排索引
倒排表以字或詞為關(guān)鍵字進(jìn)行索引,表中關(guān)鍵字所對應(yīng)的記錄表項(xiàng)記錄了出現(xiàn)這個字或詞的所有文檔,一個表項(xiàng)就是一個字表段,它記錄該文檔的ID和字符在該文檔中出現(xiàn)的位置情況。由于每個字或詞對應(yīng)的文檔數(shù)量在動態(tài)變化,所以倒排表的建立和維護(hù)都較為復(fù)雜,但是在查詢的時候由于可以一次得到查詢關(guān)鍵字所對應(yīng)的所有文檔,所以效率高于正排表。在全文檢索中,檢索的快速響應(yīng)是一個最為關(guān)鍵的性能,而索引建立由于在后臺進(jìn)行,盡管效率相對低一些,但不會影響整個搜索引擎的效率。
倒排表的結(jié)構(gòu)圖如圖2:

倒排表的索引信息保存的是字或詞后繼數(shù)組模型、互關(guān)聯(lián)后繼數(shù)組模型條在文檔內(nèi)的位置,在同一篇文檔內(nèi)相鄰的字或詞條的前后關(guān)系沒有被保存到索引文件內(nèi)。
24.2、倒排索引中提取關(guān)鍵詞
倒排索引是搜索引擎之基石。建成了倒排索引后,用戶要查找某個query,如在搜索框輸入某個關(guān)鍵詞:“結(jié)構(gòu)之法”后,搜索引擎不會再次使用爬蟲又一個一個去抓取每一個網(wǎng)頁,從上到下掃描網(wǎng)頁,看這個網(wǎng)頁有沒有出現(xiàn)這個關(guān)鍵詞,而是會在它預(yù)先生成的倒排索引文件中查找和匹配包含這個關(guān)鍵詞“結(jié)構(gòu)之法”的所有網(wǎng)頁。找到了之后,再按相關(guān)性度排序,最終把排序后的結(jié)果顯示給用戶。

如下,即是一個倒排索引文件(不全),我們把它取名為big_index,
文件中每一較短的,不包含有“#####”符號的便是某個關(guān)鍵詞,及這個關(guān)鍵詞的出現(xiàn)次數(shù)。現(xiàn)在要從這個大索引文件中提取出這些關(guān)鍵詞,--Firelf--,-11,-Winter-,.,007,007:天降殺機(jī),02Chan..如何做到呢?一行一行的掃描整個索引文件么?
何意?之前已經(jīng)說過:倒排索引包含詞典和倒排記錄表兩個部分,詞典一般有詞項(xiàng)(或稱為關(guān)鍵詞)和詞項(xiàng)頻率(即這個詞項(xiàng)或關(guān)鍵詞出現(xiàn)的次數(shù)),倒排記錄表則記錄著上述詞項(xiàng)(或關(guān)鍵詞)所出現(xiàn)的位置,或出現(xiàn)的文檔及網(wǎng)頁ID等相關(guān)信息。
最簡單的講,就是要提取詞典中的詞項(xiàng)(關(guān)鍵詞):--Firelf--,-11,-Winter-,.,007,007:天降殺機(jī),02Chan...。
--Firelf--(關(guān)鍵詞) 8(出現(xiàn)次數(shù))

我們可以試著這么解決:通過查找#####便可判斷某一行出現(xiàn)的詞是不是關(guān)鍵詞,但如果這樣做的話,便要掃描整個索引文件的每一行,代價實(shí)在巨大。如何提高速度呢?對了,關(guān)鍵詞后面的那個出現(xiàn)次數(shù)為我們問題的解決起到了很好的作用,如下注釋所示:
// 本身沒有##### 的行判定為關(guān)鍵詞行,后跟這個關(guān)鍵詞的行數(shù)N(即詞項(xiàng)頻率)
// 接下來,截取關(guān)鍵詞--Firelf--,然后讀取后面關(guān)鍵詞的行數(shù)N
// 再跳過N行(濾過和避免掃描中間的倒排記錄表信息)
// 讀取下一個關(guān)鍵詞..
有朋友指出,上述方法雖然減少了掃描的行數(shù),但并沒有減少I0開銷。讀者是否有更好地辦法?歡迎隨時交流。
24.2、為提取出來的關(guān)鍵詞編碼
愛思考的朋友可能會問,上述從倒排索引文件中提取出那些關(guān)鍵詞(詞項(xiàng))的操作是為了什么呢?其實(shí)如我個人微博上12月12日所述的Hash詞典編碼:
詞典文件的編碼:1、詞典怎么生成(存儲和構(gòu)造詞典);2、如何運(yùn)用hash對輸入的漢字進(jìn)行編碼;3、如何更好的解決沖突,即不重復(fù)以及追加功能。具體例子為:事先構(gòu)造好詞典文件后,輸入一個詞,要求找到這個詞的編碼,然后將其編碼輸出。且要有不斷能添加詞的功能,不得重復(fù)。
步驟應(yīng)該是如下:1、讀索引文件;2、提取索引中的詞出來;3、詞典怎么生成,存儲和構(gòu)造詞典;4、詞典文件的編碼:不重復(fù)與追加功能。編碼比如,輸入中國,他的編碼可以為10001,然后輸入銀行,他的編碼可以為10002。只要實(shí)現(xiàn)不斷添加詞功能,以及不重復(fù)即可,詞典類的大文件,hash最重要的是怎樣避免沖突。
也就是說,現(xiàn)在我要對上述提取出來后的關(guān)鍵詞進(jìn)行編碼,采取何種方式編碼呢?暫時用hash函數(shù)編碼。編碼之后的效果將是每一個關(guān)鍵詞都有一個特定的編碼,如下圖所示(與上文big_index文件比較一下便知):
--Firelf-- 對應(yīng)編碼為:135942
-11 對應(yīng)編碼為:106101
....

但細(xì)心的朋友一看上圖便知,其中第34~39行顯示,有重復(fù)的編碼,那么如何解決這個不重復(fù)編碼的問題呢?
用hash表編碼?但其極易產(chǎn)生沖突碰撞,為什么?請看:
哈希表是一種查找效率極高的數(shù)據(jù)結(jié)構(gòu),很多語言都在內(nèi)部實(shí)現(xiàn)了哈希表。PHP中的哈希表是一種極為重要的數(shù)據(jù)結(jié)構(gòu),不但用于表示Array數(shù)據(jù)類型,還在Zend虛擬機(jī)內(nèi)部用于存儲上下文環(huán)境信息(執(zhí)行上下文的變量及函數(shù)均使用哈希表結(jié)構(gòu)存儲)。
理想情況下哈希表插入和查找操作的時間復(fù)雜度均為O(1),任何一個數(shù)據(jù)項(xiàng)可以在一個與哈希表長度無關(guān)的時間內(nèi)計(jì)算出一個哈希值(key),然后在常量時間內(nèi)定位到一個桶(術(shù)語bucket,表示哈希表中的一個位置)。當(dāng)然這是理想情況下,因?yàn)槿魏喂1淼拈L度都是有限的,所以一定存在不同的數(shù)據(jù)項(xiàng)具有相同哈希值的情況,此時不同數(shù)據(jù)項(xiàng)被定為到同一個桶,稱為碰撞(collision)。
哈希表的實(shí)現(xiàn)需要解決碰撞問題,碰撞解決大體有兩種思路,
- 第一種是根據(jù)某種原則將被碰撞數(shù)據(jù)定為到其它桶,例如線性探測——如果數(shù)據(jù)在插入時發(fā)生了碰撞,則順序查找這個桶后面的桶,將其放入第一個沒有被使用的桶;
- 第二種策略是每個桶不是一個只能容納單個數(shù)據(jù)項(xiàng)的位置,而是一個可容納多個數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)(例如鏈表或紅黑樹),所有碰撞的數(shù)據(jù)以某種數(shù)據(jù)結(jié)構(gòu)的形式組織起來。
不論使用了哪種碰撞解決策略,都導(dǎo)致插入和查找操作的時間復(fù)雜度不再是O(1)。以查找為例,不能通過key定位到桶就結(jié)束,必須還要比較原始key(即未做哈希之前的key)是否相等,如果不相等,則要使用與插入相同的算法繼續(xù)查找,直到找到匹配的值或確認(rèn)數(shù)據(jù)不在哈希表中。
PHP是使用單鏈表存儲碰撞的數(shù)據(jù),因此實(shí)際上PHP哈希表的平均查找復(fù)雜度為O(L),其中L為桶鏈表的平均長度;而最壞復(fù)雜度為O(N),此時所有數(shù)據(jù)全部碰撞,哈希表退化成單鏈表。下圖PHP中正常哈希表和退化哈希表的示意圖。

哈希表碰撞攻擊就是通過精心構(gòu)造數(shù)據(jù),使得所有數(shù)據(jù)全部碰撞,人為將哈希表變成一個退化的單鏈表,此時哈希表各種操作的時間均提升了一個數(shù)量級,因此會消耗大量CPU資源,導(dǎo)致系統(tǒng)無法快速響應(yīng)請求,從而達(dá)到拒絕服務(wù)攻擊(DoS)的目的。
可以看到,進(jìn)行哈希碰撞攻擊的前提是哈希算法特別容易找出碰撞,如果是MD5或者SHA1那基本就沒戲了,幸運(yùn)的是(也可以說不幸的是)大多數(shù)編程語言使用的哈希算法都十分簡單(這是為了效率考慮),因此可以不費(fèi)吹灰之力之力構(gòu)造出攻擊數(shù)據(jù).(上述五段文字引自:http://www.codinglabs.org/html/hash-collisions-attack-on-php.html)。
24.4、暴雪的Hash算法
值得一提的是,在解決Hash沖突的時候,搞的焦頭爛額,結(jié)果今天上午在自己的博客內(nèi)的一篇文章(十一、從頭到尾徹底解析Hash表算法)內(nèi)找到了解決辦法:網(wǎng)上流傳甚廣的暴雪的Hash算法。 OK,接下來,咱們回顧下暴雪的hash表算法:
“接下來,咱們來具體分析一下一個最快的Hash表算法。
我們由一個簡單的問題逐步入手:有一個龐大的字符串?dāng)?shù)組,然后給你一個單獨(dú)的字符串,讓你從這個數(shù)組中查找是否有這個字符串并找到它,你會怎么做?
有一個方法最簡單,老老實(shí)實(shí)從頭查到尾,一個一個比較,直到找到為止,我想只要學(xué)過程序設(shè)計(jì)的人都能把這樣一個程序作出來,但要是有程序員把這樣的程序交給用戶,我只能用無語來評價,或許它真的能工作,但...也只能如此了。
最合適的算法自然是使用HashTable(哈希表),先介紹介紹其中的基本知識,所謂Hash,一般是一個整數(shù),通過某種算法,可以把一個字符串"壓縮" 成一個整數(shù)。當(dāng)然,無論如何,一個32位整數(shù)是無法對應(yīng)回一個字符串的,但在程序中,兩個字符串計(jì)算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:
函數(shù)prepareCryptTable以下的函數(shù)生成一個長度為0x500(合10進(jìn)制數(shù):1280)的cryptTable[0x500]
- //函數(shù)prepareCryptTable以下的函數(shù)生成一個長度為0x500(合10進(jìn)制數(shù):1280)的cryptTable[0x500]
- void prepareCryptTable()
- {
- unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;
-
- for( index1 = 0; index1 < 0x100; index1++ )
- {
- for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 )
- {
- unsigned long temp1, temp2;
-
- seed = (seed * 125 + 3) % 0x2AAAAB;
- temp1 = (seed & 0xFFFF) << 0x10;
-
- seed = (seed * 125 + 3) % 0x2AAAAB;
- temp2 = (seed & 0xFFFF);
-
- cryptTable[index2] = ( temp1 | temp2 );
- }
- }
- }
函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,
- //函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,
- unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType )
- {
- unsigned char *key = (unsigned char *)lpszkeyName;
- unsigned long seed1 = 0x7FED7FED;
- unsigned long seed2 = 0xEEEEEEEE;
- int ch;
-
- while( *key != 0 )
- {
- ch = *key++;
- seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2);
- seed2 = ch + seed1 + seed2 + (seed2<<5) + 3;
- }
- return seed1;
- }
Blizzard的這個算法是非常高效的,被稱為"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)。舉個例子,字符串"unitneutralacritter.grp"通過這個算法得到的結(jié)果是0xA26067F3。 是不是把第一個算法改進(jìn)一下,改成逐個比較字符串的Hash值就可以了呢,答案是,遠(yuǎn)遠(yuǎn)不夠,要想得到最快的算法,就不能進(jìn)行逐個的比較,通常是構(gòu)造一個哈希表(Hash Table)來解決問題,哈希表是一個大數(shù)組,這個數(shù)組的容量根據(jù)程序的要求來定義, 例如1024,每一個Hash值通過取模運(yùn)算 (mod) 對應(yīng)到數(shù)組中的一個位置,這樣,只要比較這個字符串的哈希值對應(yīng)的位置有沒有被占用,就可以得到最后的結(jié)果了,想想這是什么速度?是的,是最快的O(1),現(xiàn)在仔細(xì)看看這個算法吧:- typedef struct
- {
- int nHashA;
- int nHashB;
- char bExists;
- ......
- } SOMESTRUCTRUE;
- //一種可能的結(jié)構(gòu)體定義?
函數(shù)GetHashTablePos下述函數(shù)為在Hash表中查找是否存在目標(biāo)字符串,有則返回要查找字符串的Hash值,無則,return -1.- //函數(shù)GetHashTablePos下述函數(shù)為在Hash表中查找是否存在目標(biāo)字符串,有則返回要查找字符串的Hash值,無則,return -1.
- int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable )
- //lpszString要在Hash表中查找的字符串,lpTable為存儲字符串Hash值的Hash表。
- {
- int nHash = HashString(lpszString); //調(diào)用上述函數(shù)HashString,返回要查找字符串lpszString的Hash值。
- int nHashPos = nHash % nTableSize;
-
- if ( lpTable[nHashPos].bExists && !strcmp( lpTable[nHashPos].pString, lpszString ) )
- { //如果找到的Hash值在表中存在,且要查找的字符串與表中對應(yīng)位置的字符串相同,
- return nHashPos; //返回找到的Hash值
- }
- else
- {
- return -1;
- }
- }
看到此,我想大家都在想一個很嚴(yán)重的問題:“如果兩個字符串在哈希表中對應(yīng)的位置相同怎么辦?”,畢竟一個數(shù)組容量是有限的,這種可能性很大。解決該問題的方法很多,我首先想到的就是用“鏈表”,感謝大學(xué)里學(xué)的數(shù)據(jù)結(jié)構(gòu)教會了這個百試百靈的法寶,我遇到的很多算法都可以轉(zhuǎn)化成鏈表來解決,只要在哈希表的每個入口掛一個鏈表,保存所有對應(yīng)的字符串就OK了。事情到此似乎有了完美的結(jié)局,如果是把問題獨(dú)自交給我解決,此時我可能就要開始定義數(shù)據(jù)結(jié)構(gòu)然后寫代碼了。 然而Blizzard的程序員使用的方法則是更精妙的方法。基本原理就是:他們在哈希表中不是用一個哈希值而是用三個哈希值來校驗(yàn)字符串。” “MPQ使用文件名哈希表來跟蹤內(nèi)部的所有文件。但是這個表的格式與正常的哈希表有一些不同。首先,它沒有使用哈希作為下標(biāo),把實(shí)際的文件名存儲在表中用于驗(yàn)證,實(shí)際上它根本就沒有存儲文件名。而是使用了3種不同的哈希:一個用于哈希表的下標(biāo),兩個用于驗(yàn)證。這兩個驗(yàn)證哈希替代了實(shí)際文件名。 當(dāng)然了,這樣仍然會出現(xiàn)2個不同的文件名哈希到3個同樣的哈希。但是這種情況發(fā)生的概率平均是:1:18889465931478580854784,這個概率對于任何人來說應(yīng)該都是足夠小的。現(xiàn)在再回到數(shù)據(jù)結(jié)構(gòu)上,Blizzard使用的哈希表沒有使用鏈表,而采用"順延"的方式來解決問題。”下面,咱們來看看這個網(wǎng)上流傳甚廣的暴雪hash算法: 函數(shù)GetHashTablePos中,lpszString 為要在hash表中查找的字符串;lpTable 為存儲字符串hash值的hash表;nTableSize 為hash表的長度: - //函數(shù)GetHashTablePos中,lpszString 為要在hash表中查找的字符串;lpTable 為存儲字符串hash值的hash表;nTableSize 為hash表的長度:
- int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize )
- {
- const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
-
- int nHash = HashString( lpszString, HASH_OFFSET );
- int nHashA = HashString( lpszString, HASH_A );
- int nHashB = HashString( lpszString, HASH_B );
- int nHashStart = nHash % nTableSize;
- int nHashPos = nHashStart;
-
- while ( lpTable[nHashPos].bExists )
- {
- // 如果僅僅是判斷在該表中時候存在這個字符串,就比較這兩個hash值就可以了,不用對結(jié)構(gòu)體中的字符串進(jìn)行比較。
- // 這樣會加快運(yùn)行的速度?減少hash表占用的空間?這種方法一般應(yīng)用在什么場合?
- if ( lpTable[nHashPos].nHashA == nHashA
- && lpTable[nHashPos].nHashB == nHashB )
- {
- return nHashPos;
- }
- else
- {
- nHashPos = (nHashPos + 1) % nTableSize;
- }
-
- if (nHashPos == nHashStart)
- break;
- }
- return -1;
- }
上述程序解釋:
- 計(jì)算出字符串的三個哈希值(一個用來確定位置,另外兩個用來校驗(yàn))
- 察看哈希表中的這個位置
- 哈希表中這個位置為空嗎?如果為空,則肯定該字符串不存在,返回-1。
- 如果存在,則檢查其他兩個哈希值是否也匹配,如果匹配,則表示找到了該字符串,返回其Hash值。
- 移到下一個位置,如果已經(jīng)移到了表的末尾,則反繞到表的開始位置起繼續(xù)查詢
- 看看是不是又回到了原來的位置,如果是,則返回沒找到
- 回到3。
24.4、不重復(fù)Hash編碼
有了上面的暴雪Hash算法。咱們的問題便可解決了。不過,有兩點(diǎn)必須先提醒讀者:1、Hash表起初要初始化;2、暴雪的Hash算法對于查詢那樣處理可以,但對插入就不能那么解決。
關(guān)鍵主體代碼如下:
- //函數(shù)prepareCryptTable以下的函數(shù)生成一個長度為0x500(合10進(jìn)制數(shù):1280)的cryptTable[0x500]
- void prepareCryptTable()
- {
- unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;
-
- for( index1 = 0; index1 <0x100; index1++ )
- {
- for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
- {
- unsigned long temp1, temp2;
- seed = (seed * 125 + 3) % 0x2AAAAB;
- temp1 = (seed & 0xFFFF)<<0x10;
- seed = (seed * 125 + 3) % 0x2AAAAB;
- temp2 = (seed & 0xFFFF);
- cryptTable[index2] = ( temp1 | temp2 );
- }
- }
- }
-
- //函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,
- unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType )
- {
- unsigned char *key = (unsigned char *)lpszkeyName;
- unsigned long seed1 = 0x7FED7FED;
- unsigned long seed2 = 0xEEEEEEEE;
- int ch;
-
- while( *key != 0 )
- {
- ch = *key++;
- seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2);
- seed2 = ch + seed1 + seed2 + (seed2<<5) + 3;
- }
- return seed1;
- }
-
- /////////////////////////////////////////////////////////////////////
- //function: 哈希詞典 編碼
- //parameter:
- //author: lei.zhou
- //time: 2011-12-14
- /////////////////////////////////////////////////////////////////////
- MPQHASHTABLE TestHashTable[nTableSize];
- int TestHashCTable[nTableSize];
- int TestHashDTable[nTableSize];
- key_list test_data[nTableSize];
-
- //直接調(diào)用上面的hashstring,nHashPos就是對應(yīng)的HASH值。
- int insert_string(const char *string_in)
- {
- const int HASH_OFFSET = 0, HASH_C = 1, HASH_D = 2;
- unsigned int nHash = HashString(string_in, HASH_OFFSET);
- unsigned int nHashC = HashString(string_in, HASH_C);
- unsigned int nHashD = HashString(string_in, HASH_D);
- unsigned int nHashStart = nHash % nTableSize;
- unsigned int nHashPos = nHashStart;
- int ln, ires = 0;
-
- while (TestHashTable[nHashPos].bExists)
- {
- // if (TestHashCTable[nHashPos] == (int) nHashC && TestHashDTable[nHashPos] == (int) nHashD)
- // break;
- // //...
- // else
- //如之前所提示讀者的那般,暴雪的Hash算法對于查詢那樣處理可以,但對插入就不能那么解決
- nHashPos = (nHashPos + 1) % nTableSize;
-
- if (nHashPos == nHashStart)
- break;
- }
-
- ln = strlen(string_in);
- if (!TestHashTable[nHashPos].bExists && (ln < nMaxStrLen))
- {
- TestHashCTable[nHashPos] = nHashC;
- TestHashDTable[nHashPos] = nHashD;
-
- test_data[nHashPos] = (KEYNODE *) malloc (sizeof(KEYNODE) * 1);
- if(test_data[nHashPos] == NULL)
- {
- printf("10000 EMS ERROR !!!!\n");
- return 0;
- }
-
- test_data[nHashPos]->pkey = (char *)malloc(ln+1);
- if(test_data[nHashPos]->pkey == NULL)
- {
- printf("10000 EMS ERROR !!!!\n");
- return 0;
- }
-
- memset(test_data[nHashPos]->pkey, 0, ln+1);
- strncpy(test_data[nHashPos]->pkey, string_in, ln);
- *((test_data[nHashPos]->pkey)+ln) = 0;
- test_data[nHashPos]->weight = nHashPos;
-
- TestHashTable[nHashPos].bExists = 1;
- }
- else
- {
- if(TestHashTable[nHashPos].bExists)
- printf("30000 in the hash table %s !!!\n", string_in);
- else
- printf("90000 strkey error !!!\n");
- }
- return nHashPos;
- }
接下來要讀取索引文件big_index對其中的關(guān)鍵詞進(jìn)行編碼(為了簡單起見,直接一行一行掃描讀寫,沒有跳過行數(shù)了):
- void bigIndex_hash(const char *docpath, const char *hashpath)
- {
- FILE *fr, *fw;
- int len;
- char *pbuf, *p;
- char dockey[TERM_MAX_LENG];
-
- if(docpath == NULL || *docpath == '\0')
- return;
-
- if(hashpath == NULL || *hashpath == '\0')
- return;
-
- fr = fopen(docpath, "rb"); //讀取文件docpath
- fw = fopen(hashpath, "wb");
- if(fr == NULL || fw == NULL)
- {
- printf("open read or write file error!\n");
- return;
- }
-
- pbuf = (char*)malloc(BUFF_MAX_LENG);
- if(pbuf == NULL)
- {
- fclose(fr);
- return ;
- }
-
- memset(pbuf, 0, BUFF_MAX_LENG);
-
- while(fgets(pbuf, BUFF_MAX_LENG, fr))
- {
- len = GetRealString(pbuf);
- if(len <= 1)
- continue;
- p = strstr(pbuf, "#####");
- if(p != NULL)
- continue;
-
- p = strstr(pbuf, " ");
- if (p == NULL)
- {
- printf("file contents error!");
- }
-
- len = p - pbuf;
- dockey[0] = 0;
- strncpy(dockey, pbuf, len);
-
- dockey[len] = 0;
-
- int num = insert_string(dockey);
-
- dockey[len] = ' ';
- dockey[len+1] = '\0';
- char str[20];
- itoa(num, str, 10);
-
- strcat(dockey, str);
- dockey[len+strlen(str)+1] = '\0';
- fprintf (fw, "%s\n", dockey);
-
- }
- free(pbuf);
- fclose(fr);
- fclose(fw);
- }
主函數(shù)已經(jīng)很簡單了,如下:
- int main()
- {
- prepareCryptTable(); //Hash表起初要初始化
-
- //現(xiàn)在要把整個big_index文件插入hash表,以取得編碼結(jié)果
- bigIndex_hash("big_index.txt", "hashpath.txt");
- system("pause");
-
- return 0;
- }
程序運(yùn)行后生成的hashpath.txt文件如下:

如上所示,采取暴雪的Hash算法并在插入的時候做適當(dāng)處理,當(dāng)再次對上文中的索引文件big_index進(jìn)行Hash編碼后,沖突問題已經(jīng)得到初步解決。當(dāng)然,還有待更進(jìn)一步更深入的測試。
后續(xù)添上數(shù)目索引1~10000...
后來又為上述文件中的關(guān)鍵詞編了碼一個計(jì)數(shù)的內(nèi)碼,不過,奇怪的是,同樣的代碼,在Dev C++ 與VS2010上運(yùn)行結(jié)果卻不同(左邊dev上計(jì)數(shù)從"1"開始,VS上計(jì)數(shù)從“1994014002”開始),如下圖所示:

在上面的bigIndex_hashcode函數(shù)的基礎(chǔ)上,修改如下,即可得到上面的效果:
- void bigIndex_hashcode(const char *in_file_path, const char *out_file_path)
- {
- FILE *fr, *fw;
- int len, value;
- char *pbuf, *pleft, *p;
- char keyvalue[TERM_MAX_LENG], str[WORD_MAX_LENG];
-
- if(in_file_path == NULL || *in_file_path == '\0') {
- printf("input file path error!\n");
- return;
- }
-
- if(out_file_path == NULL || *out_file_path == '\0') {
- printf("output file path error!\n");
- return;
- }
-
- fr = fopen(in_file_path, "r"); //讀取in_file_path路徑文件
- fw = fopen(out_file_path, "w");
-
- if(fr == NULL || fw == NULL)
- {
- printf("open read or write file error!\n");
- return;
- }
-
- pbuf = (char*)malloc(BUFF_MAX_LENG);
- pleft = (char*)malloc(BUFF_MAX_LENG);
- if(pbuf == NULL || pleft == NULL)
- {
- printf("allocate memory error!");
- fclose(fr);
- return ;
- }
-
- memset(pbuf, 0, BUFF_MAX_LENG);
-
- int offset = 1;
- while(fgets(pbuf, BUFF_MAX_LENG, fr))
- {
- if (--offset > 0)
- continue;
-
- if(GetRealString(pbuf) <= 1)
- continue;
-
- p = strstr(pbuf, "#####");
- if(p != NULL)
- continue;
-
- p = strstr(pbuf, " ");
- if (p == NULL)
- {
- printf("file contents error!");
- }
-
- len = p - pbuf;
-
- // 確定跳過行數(shù)
- strcpy(pleft, p+1);
- offset = atoi(pleft) + 1;
-
- strncpy(keyvalue, pbuf, len);
- keyvalue[len] = '\0';
- value = insert_string(keyvalue);
-
- if (value != -1) {
-
- // key value中插入空格
- keyvalue[len] = ' ';
- keyvalue[len+1] = '\0';
-
- itoa(value, str, 10);
- strcat(keyvalue, str);
-
- keyvalue[len+strlen(str)+1] = ' ';
- keyvalue[len+strlen(str)+2] = '\0';
-
- keysize++;
- itoa(keysize, str, 10);
- strcat(keyvalue, str);
-
- // 將key value寫入文件
- fprintf (fw, "%s\n", keyvalue);
-
- }
- }
- free(pbuf);
- fclose(fr);
- fclose(fw);
- }
小結(jié)
本文有一點(diǎn)值得一提的是,在此前的這篇文章(十一、從頭到尾徹底解析Hash表算法)之中,只是對Hash表及暴雪的Hash算法有過學(xué)習(xí)和了解,但尚未真正運(yùn)用過它,而今在本章中體現(xiàn),證明還是之前寫的文章,及之前對Hash表等算法的學(xué)習(xí)還是有一定作用的。同時,也順便對暴雪的Hash函數(shù)算是做了個測試,其的確能解決一般的沖突性問題,創(chuàng)造這個算法的人不簡單吶。
后記
再次感謝老大xiaoqi,以及藝術(shù)室內(nèi)朋友xiaolin,555,yansha的指導(dǎo)。沒有他們的幫助,我將寸步難行。日后,自己博客內(nèi)的文章要經(jīng)常回顧,好好體會。同時,寫作本文時,剛接觸倒排索引等相關(guān)問題不久,若有任何問題,歡迎隨時交流或批評指正。謝謝。完。