• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 200, comments - 8, trackbacks - 0, articles - 0

              第二十三、四章:楊氏矩陣查找,倒排索引關(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)行編碼。那么,這個問題將分為兩個個步驟:

            1. 首先,要提取倒排索引內(nèi)詞典文件中的關(guān)鍵詞;
            2. 對提取出來的關(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)需要解決碰撞問題,碰撞解決大體有兩種思路,

            1. 第一種是根據(jù)某種原則將被碰撞數(shù)據(jù)定為到其它桶,例如線性探測——如果數(shù)據(jù)在插入時發(fā)生了碰撞,則順序查找這個桶后面的桶,將其放入第一個沒有被使用的桶;
            2. 第二種策略是每個桶不是一個只能容納單個數(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]

            1. //函數(shù)prepareCryptTable以下的函數(shù)生成一個長度為0x500(合10進(jìn)制數(shù):1280)的cryptTable[0x500]  
            2. void prepareCryptTable()  
            3. {   
            4.     unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;  
            5.   
            6.     for( index1 = 0; index1 < 0x100; index1++ )  
            7.     {   
            8.         for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 )  
            9.         {   
            10.             unsigned long temp1, temp2;  
            11.   
            12.             seed = (seed * 125 + 3) % 0x2AAAAB;  
            13.             temp1 = (seed & 0xFFFF) << 0x10;  
            14.   
            15.             seed = (seed * 125 + 3) % 0x2AAAAB;  
            16.             temp2 = (seed & 0xFFFF);  
            17.   
            18.             cryptTable[index2] = ( temp1 | temp2 );   
            19.         }   
            20.     }   
            21. }   

                函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,

            1. //函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,  
            2. unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType )  
            3. {  
            4.     unsigned char *key  = (unsigned char *)lpszkeyName;  
            5.     unsigned long seed1 = 0x7FED7FED;  
            6.     unsigned long seed2 = 0xEEEEEEEE;  
            7.     int ch;  
            8.   
            9.     while( *key != 0 )  
            10.     {  
            11.         ch = *key++;  
            12.         seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2);  
            13.         seed2 = ch + seed1 + seed2 + (seed2<<5) + 3;  
            14.     }  
            15.     return seed1;  
            16. }  

                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ì)看看這個算法吧:
            1. typedef struct  
            2. {  
            3.     int nHashA;  
            4.     int nHashB;  
            5.     char bExists;  
            6.    ......  
            7. } SOMESTRUCTRUE;  
            8. //一種可能的結(jié)構(gòu)體定義?  
                函數(shù)GetHashTablePos下述函數(shù)為在Hash表中查找是否存在目標(biāo)字符串,有則返回要查找字符串的Hash值,無則,return -1.
            1. //函數(shù)GetHashTablePos下述函數(shù)為在Hash表中查找是否存在目標(biāo)字符串,有則返回要查找字符串的Hash值,無則,return -1.  
            2. int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable )   
            3. //lpszString要在Hash表中查找的字符串,lpTable為存儲字符串Hash值的Hash表。  
            4. {   
            5.     int nHash = HashString(lpszString);  //調(diào)用上述函數(shù)HashString,返回要查找字符串lpszString的Hash值。  
            6.     int nHashPos = nHash % nTableSize;  
            7.    
            8.     if ( lpTable[nHashPos].bExists  &&  !strcmp( lpTable[nHashPos].pString, lpszString ) )   
            9.     {  //如果找到的Hash值在表中存在,且要查找的字符串與表中對應(yīng)位置的字符串相同,  
            10.         return nHashPos;    //返回找到的Hash值  
            11.     }   
            12.     else  
            13.     {  
            14.         return -1;    
            15.     }   
            16. }  
                看到此,我想大家都在想一個很嚴(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表的長度: 
            1. //函數(shù)GetHashTablePos中,lpszString 為要在hash表中查找的字符串;lpTable 為存儲字符串hash值的hash表;nTableSize 為hash表的長度:   
            2. int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize )  
            3. {  
            4.     const int  HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;  
            5.    
            6.     int  nHash = HashString( lpszString, HASH_OFFSET );  
            7.     int  nHashA = HashString( lpszString, HASH_A );  
            8.     int  nHashB = HashString( lpszString, HASH_B );  
            9.     int  nHashStart = nHash % nTableSize;  
            10.     int  nHashPos = nHashStart;  
            11.    
            12.     while ( lpTable[nHashPos].bExists )  
            13.    {  
            14. //     如果僅僅是判斷在該表中時候存在這個字符串,就比較這兩個hash值就可以了,不用對結(jié)構(gòu)體中的字符串進(jìn)行比較。  
            15. //         這樣會加快運(yùn)行的速度?減少hash表占用的空間?這種方法一般應(yīng)用在什么場合?  
            16.         if (   lpTable[nHashPos].nHashA == nHashA  
            17.         &&  lpTable[nHashPos].nHashB == nHashB )  
            18.        {  
            19.             return nHashPos;  
            20.        }  
            21.        else  
            22.        {  
            23.             nHashPos = (nHashPos + 1) % nTableSize;  
            24.        }  
            25.    
            26.         if (nHashPos == nHashStart)  
            27.               break;  
            28.     }  
            29.      return -1;  
            30. }  

                上述程序解釋:

             

            1. 計(jì)算出字符串的三個哈希值(一個用來確定位置,另外兩個用來校驗(yàn))
            2. 察看哈希表中的這個位置
            3. 哈希表中這個位置為空嗎?如果為空,則肯定該字符串不存在,返回-1。
            4. 如果存在,則檢查其他兩個哈希值是否也匹配,如果匹配,則表示找到了該字符串,返回其Hash值。
            5. 移到下一個位置,如果已經(jīng)移到了表的末尾,則反繞到表的開始位置起繼續(xù)查詢 
            6. 看看是不是又回到了原來的位置,如果是,則返回沒找到
            7. 回到3。

             

            24.4、不重復(fù)Hash編碼

                有了上面的暴雪Hash算法。咱們的問題便可解決了。不過,有兩點(diǎn)必須先提醒讀者:1、Hash表起初要初始化;2、暴雪的Hash算法對于查詢那樣處理可以,但對插入就不能那么解決。

                關(guān)鍵主體代碼如下:

            1. //函數(shù)prepareCryptTable以下的函數(shù)生成一個長度為0x500(合10進(jìn)制數(shù):1280)的cryptTable[0x500]  
            2. void prepareCryptTable()  
            3. {  
            4.     unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;  
            5.   
            6.     for( index1 = 0; index1 <0x100; index1++ )  
            7.     {  
            8.         for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100)  
            9.         {  
            10.             unsigned long temp1, temp2;  
            11.             seed = (seed * 125 + 3) % 0x2AAAAB;  
            12.             temp1 = (seed & 0xFFFF)<<0x10;  
            13.             seed = (seed * 125 + 3) % 0x2AAAAB;  
            14.             temp2 = (seed & 0xFFFF);  
            15.             cryptTable[index2] = ( temp1 | temp2 );  
            16.         }  
            17.     }  
            18. }  
            19.   
            20. //函數(shù)HashString以下函數(shù)計(jì)算lpszFileName 字符串的hash值,其中dwHashType 為hash的類型,  
            21. unsigned long HashString(const char *lpszkeyName, unsigned long dwHashType )  
            22. {  
            23.     unsigned char *key  = (unsigned char *)lpszkeyName;  
            24.     unsigned long seed1 = 0x7FED7FED;  
            25.     unsigned long seed2 = 0xEEEEEEEE;  
            26.     int ch;  
            27.   
            28.     while( *key != 0 )  
            29.     {  
            30.         ch = *key++;  
            31.         seed1 = cryptTable[(dwHashType<<8) + ch] ^ (seed1 + seed2);  
            32.         seed2 = ch + seed1 + seed2 + (seed2<<5) + 3;  
            33.     }  
            34.     return seed1;  
            35. }  
            36.   
            37. /////////////////////////////////////////////////////////////////////  
            38. //function: 哈希詞典 編碼  
            39. //parameter:  
            40. //author: lei.zhou  
            41. //time: 2011-12-14  
            42. /////////////////////////////////////////////////////////////////////  
            43. MPQHASHTABLE TestHashTable[nTableSize];  
            44. int TestHashCTable[nTableSize];  
            45. int TestHashDTable[nTableSize];  
            46. key_list test_data[nTableSize];  
            47.   
            48. //直接調(diào)用上面的hashstring,nHashPos就是對應(yīng)的HASH值。  
            49. int insert_string(const char *string_in)  
            50. {  
            51.     const int HASH_OFFSET = 0, HASH_C = 1, HASH_D = 2;  
            52.     unsigned int nHash = HashString(string_in, HASH_OFFSET);  
            53.     unsigned int nHashC = HashString(string_in, HASH_C);  
            54.     unsigned int nHashD = HashString(string_in, HASH_D);  
            55.     unsigned int nHashStart = nHash % nTableSize;  
            56.     unsigned int nHashPos = nHashStart;  
            57.     int ln, ires = 0;  
            58.   
            59.     while (TestHashTable[nHashPos].bExists)  
            60.     {  
            61. //      if (TestHashCTable[nHashPos]  == (int) nHashC && TestHashDTable[nHashPos] == (int) nHashD)  
            62. //          break;  
            63. //      //...  
            64. //      else  
            65.         //如之前所提示讀者的那般,暴雪的Hash算法對于查詢那樣處理可以,但對插入就不能那么解決  
            66.             nHashPos = (nHashPos + 1) % nTableSize;  
            67.   
            68.         if (nHashPos == nHashStart)  
            69.             break;  
            70.     }  
            71.   
            72.     ln = strlen(string_in);  
            73.     if (!TestHashTable[nHashPos].bExists && (ln < nMaxStrLen))  
            74.     {   
            75.         TestHashCTable[nHashPos] = nHashC;  
            76.         TestHashDTable[nHashPos] = nHashD;  
            77.   
            78.         test_data[nHashPos] = (KEYNODE *) malloc (sizeof(KEYNODE) * 1);  
            79.         if(test_data[nHashPos] == NULL)  
            80.         {  
            81.             printf("10000 EMS ERROR !!!!\n");  
            82.             return 0;  
            83.         }  
            84.   
            85.         test_data[nHashPos]->pkey = (char *)malloc(ln+1);  
            86.         if(test_data[nHashPos]->pkey == NULL)  
            87.         {  
            88.             printf("10000 EMS ERROR !!!!\n");  
            89.             return 0;  
            90.         }  
            91.   
            92.         memset(test_data[nHashPos]->pkey, 0, ln+1);  
            93.         strncpy(test_data[nHashPos]->pkey, string_in, ln);  
            94.         *((test_data[nHashPos]->pkey)+ln) = 0;  
            95.         test_data[nHashPos]->weight = nHashPos;  
            96.   
            97.         TestHashTable[nHashPos].bExists = 1;  
            98.     }  
            99.     else  
            100.     {  
            101.         if(TestHashTable[nHashPos].bExists)  
            102.             printf("30000 in the hash table %s !!!\n", string_in);  
            103.         else  
            104.             printf("90000 strkey error !!!\n");  
            105.     }  
            106.     return nHashPos;  
            107. }  

                接下來要讀取索引文件big_index對其中的關(guān)鍵詞進(jìn)行編碼(為了簡單起見,直接一行一行掃描讀寫,沒有跳過行數(shù)了):

            1. void bigIndex_hash(const char *docpath, const char *hashpath)  
            2. {  
            3.     FILE *fr, *fw;  
            4.     int len;  
            5.     char *pbuf, *p;  
            6.     char dockey[TERM_MAX_LENG];  
            7.   
            8.     if(docpath == NULL || *docpath == '\0')  
            9.         return;  
            10.   
            11.     if(hashpath == NULL || *hashpath == '\0')  
            12.         return;  
            13.   
            14.     fr = fopen(docpath, "rb");  //讀取文件docpath  
            15.     fw = fopen(hashpath, "wb");  
            16.     if(fr == NULL || fw == NULL)  
            17.     {  
            18.         printf("open read or write file error!\n");  
            19.         return;  
            20.     }  
            21.   
            22.     pbuf = (char*)malloc(BUFF_MAX_LENG);  
            23.     if(pbuf == NULL)  
            24.     {  
            25.         fclose(fr);  
            26.         return ;  
            27.     }  
            28.   
            29.     memset(pbuf, 0, BUFF_MAX_LENG);  
            30.   
            31.     while(fgets(pbuf, BUFF_MAX_LENG, fr))  
            32.     {  
            33.         len = GetRealString(pbuf);  
            34.         if(len <= 1)  
            35.             continue;  
            36.         p = strstr(pbuf, "#####");    
            37.         if(p != NULL)  
            38.             continue;  
            39.   
            40.         p = strstr(pbuf, "  ");  
            41.         if (p == NULL)  
            42.         {  
            43.             printf("file contents error!");  
            44.         }  
            45.   
            46.         len = p - pbuf;  
            47.         dockey[0] = 0;  
            48.         strncpy(dockey, pbuf, len);  
            49.   
            50.         dockey[len] = 0;        
            51.   
            52.         int num = insert_string(dockey);   
            53.   
            54.         dockey[len] = ' ';  
            55.         dockey[len+1] = '\0';  
            56.         char str[20];  
            57.         itoa(num, str, 10);  
            58.   
            59.         strcat(dockey, str);  
            60.         dockey[len+strlen(str)+1] = '\0';  
            61.         fprintf (fw, "%s\n", dockey);  
            62.   
            63.     }  
            64.     free(pbuf);  
            65.     fclose(fr);  
            66.     fclose(fw);  
            67. }  

                主函數(shù)已經(jīng)很簡單了,如下:

            1. int main()  
            2. {  
            3.     prepareCryptTable();  //Hash表起初要初始化  
            4.   
            5.     //現(xiàn)在要把整個big_index文件插入hash表,以取得編碼結(jié)果  
            6.     bigIndex_hash("big_index.txt""hashpath.txt");  
            7.     system("pause");  
            8.   
            9.     return 0;  
            10. }  

                程序運(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ǔ)上,修改如下,即可得到上面的效果:

            1. void bigIndex_hashcode(const char *in_file_path, const char *out_file_path)  
            2. {  
            3.     FILE *fr, *fw;  
            4.     int len, value;  
            5.     char *pbuf, *pleft, *p;  
            6.     char keyvalue[TERM_MAX_LENG], str[WORD_MAX_LENG];  
            7.   
            8.     if(in_file_path == NULL || *in_file_path == '\0') {  
            9.         printf("input file path error!\n");  
            10.         return;  
            11.     }  
            12.   
            13.     if(out_file_path == NULL || *out_file_path == '\0') {  
            14.         printf("output file path error!\n");  
            15.         return;  
            16.     }  
            17.   
            18.     fr = fopen(in_file_path, "r");  //讀取in_file_path路徑文件  
            19.     fw = fopen(out_file_path, "w");  
            20.   
            21.     if(fr == NULL || fw == NULL)  
            22.     {  
            23.         printf("open read or write file error!\n");  
            24.         return;  
            25.     }  
            26.   
            27.     pbuf = (char*)malloc(BUFF_MAX_LENG);  
            28.     pleft = (char*)malloc(BUFF_MAX_LENG);  
            29.     if(pbuf == NULL || pleft == NULL)  
            30.     {  
            31.         printf("allocate memory error!");  
            32.         fclose(fr);  
            33.         return ;  
            34.     }  
            35.   
            36.     memset(pbuf, 0, BUFF_MAX_LENG);  
            37.   
            38.     int offset = 1;  
            39.     while(fgets(pbuf, BUFF_MAX_LENG, fr))  
            40.     {  
            41.         if (--offset > 0)  
            42.             continue;  
            43.   
            44.         if(GetRealString(pbuf) <= 1)  
            45.             continue;  
            46.   
            47.         p = strstr(pbuf, "#####");    
            48.         if(p != NULL)  
            49.             continue;  
            50.   
            51.         p = strstr(pbuf, "  ");  
            52.         if (p == NULL)  
            53.         {  
            54.             printf("file contents error!");  
            55.         }  
            56.   
            57.         len = p - pbuf;  
            58.   
            59.         // 確定跳過行數(shù)  
            60.         strcpy(pleft, p+1);   
            61.         offset = atoi(pleft) + 1;  
            62.   
            63.         strncpy(keyvalue, pbuf, len);    
            64.         keyvalue[len] = '\0';  
            65.         value = insert_string(keyvalue);  
            66.   
            67.         if (value != -1) {  
            68.   
            69.             // key value中插入空格  
            70.             keyvalue[len] = ' ';  
            71.             keyvalue[len+1] = '\0';  
            72.   
            73.             itoa(value, str, 10);  
            74.             strcat(keyvalue, str);  
            75.   
            76.             keyvalue[len+strlen(str)+1] = ' ';  
            77.             keyvalue[len+strlen(str)+2] = '\0';  
            78.   
            79.             keysize++;  
            80.             itoa(keysize, str, 10);  
            81.             strcat(keyvalue, str);  
            82.   
            83.             // 將key value寫入文件  
            84.             fprintf (fw, "%s\n", keyvalue);  
            85.   
            86.         }  
            87.     }  
            88.     free(pbuf);  
            89.     fclose(fr);  
            90.     fclose(fw);  
            91. }  

            小結(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)問題不久,若有任何問題,歡迎隨時交流或批評指正。謝謝。完。
            无码8090精品久久一区| 久久狠狠爱亚洲综合影院| 精品免费久久久久久久| 国内精品久久久人妻中文字幕| 久久综合狠狠综合久久综合88| 国内精品九九久久久精品| 伊人丁香狠狠色综合久久| 精品久久久久中文字幕一区| 久久国产亚洲精品| 精品久久久久久无码中文字幕一区 | 老男人久久青草av高清| 国产成人精品白浆久久69| 无码人妻久久一区二区三区蜜桃 | 国产三级观看久久| 亚洲伊人久久大香线蕉综合图片| 99久久精品免费观看国产| 中文字幕无码精品亚洲资源网久久| 日本三级久久网| 久久久老熟女一区二区三区| 午夜肉伦伦影院久久精品免费看国产一区二区三区 | 国产福利电影一区二区三区久久老子无码午夜伦不 | 国产L精品国产亚洲区久久| 成人午夜精品无码区久久| 国产精品无码久久四虎| 久久精品国产网红主播| 青青草原综合久久大伊人| 国产69精品久久久久99尤物| 久久Av无码精品人妻系列 | 中文精品99久久国产| 精品久久国产一区二区三区香蕉| 久久精品人人做人人爽电影蜜月| 国内精品久久国产| 久久久久亚洲AV成人网人人网站 | 久久中文字幕无码专区| 狠狠色丁香婷婷综合久久来来去 | 一本一本久久a久久综合精品蜜桃| 久久夜色撩人精品国产小说| 久久这里有精品| 日韩一区二区久久久久久| 国产成人精品久久一区二区三区| 精品久久人人爽天天玩人人妻|