哈希表的概念數組
哈希表(Hash Table)是一種特殊的數據結構,它最大的特色就是能夠快速實現查找、插入和刪除。數據結構
咱們知道,數組的最大特色就是:尋址容易,插入和刪除困難;而鏈表正好相反,尋址困難,而插入和刪除操做容易。那麼若是可以結合二者的優勢,作出一種尋址、插入和刪除操做一樣快速容易的數據結構,那該有多好。這就是哈希表建立的基本思想,而實際上哈希表也實現了這樣的一個「夙願」,哈希表就是這樣一個集查找、插入和刪除操做於一身的數據結構。函數
哈希表(Hash Table):也叫散列表,是根據關鍵碼值(key-value)而直接進行訪問的數據結構,也就是咱們經常使用到的map。加密
哈希函數:也稱爲是散列函數,是Hash表的映射函數,它能夠把任意長度的輸入變換成固定長度的輸出,該輸出就是哈希值。哈希函數能使對一個數據序列的訪問過程變得更加迅速有效,經過哈希函數,數據元素可以被很快的進行定位。spa
哈希表和哈希函數的標準定義:若關鍵字爲k,則其值存放在h(k)的存儲位置上。由此,不需比較即可直接取得所查記錄。稱這個對應關係f爲哈希函數,按這個思想創建的表爲哈希表。設計
設計出一個簡單、均勻、存儲利用率高的散列函數是散列技術中最關鍵的問題。
可是,通常散列函數都面臨着衝突的問題。兩個不一樣的關鍵字,因爲散列函數值相同,於是被映射到同一表位置上。該現象稱爲衝突(Collision)或碰撞。發生衝突的兩個關鍵字稱爲該散列函數的同義詞(Synonym)。3d
設m和n分別表示表長和表中填入的結點數,則將α=n/m定義爲散列表的裝填因子(Load Factor)。α越大,表越滿,衝突的機會也越大。一般取α≤1。指針
哈希表的實現就是映射函數構造,看某個元素具體屬於哪個類別。如何構造咱們要考慮兩個問題:code
不管用什麼方法存儲,目的都是儘可能均勻地存放元素,以免衝突。blog
因此,我哈希表的映射函數構造方法也有不少,常見的有:直接定址法、 除留餘數法、 乘餘取整法、 數字分析法、 平方取中法、 摺疊法、 隨機數法等。
一、直接定位法
Hash(key) = a·key + b (a、b爲常數)
優勢:以關鍵碼key的某個線性函數值爲哈希地址,不會產生衝突.
缺點:要佔用連續地址空間,空間效率低。
例:關鍵碼集合爲{100,300,500,700,800,900}, 選取哈希函數爲Hash(key)=key/100, 則存儲結構(哈希表)以下:
二、除留餘數法
Hash(key) = key mod p (p是一個整數)
特色:以關鍵碼除以p的餘數做爲哈希地址。
關鍵:如何選取合適的p?
技巧:若設計的哈希表長爲m,則通常取p≤m且爲質數 (也能夠是不包含小於20質因子的合數)。
三、乘餘取整法
Hash(key) = [B*( A*key mod 1 ) ]下取整 (A、B均爲常數,且0<A<1,B爲整數)
特色:以關鍵碼key乘以A,取其小數部分,而後再放大B倍並取整,做爲哈希地址。
例:欲以學號最後兩位做爲地址,則哈希函數應爲: H(k)=100*(0.01*k % 1 ) 其實也能夠用法2實現: H(k)=k % 100
四、數字分析法
特色:某關鍵字的某幾位組合成哈希地址。所選的位應當是:各類符號在該位上出現的頻率大體相同。
例:有一組(例如80個)關鍵碼,其樣式以下:
五、平方取中法
特色:對關鍵碼平方後,按哈希表大小,取中間的若干位做爲哈希地址。
理由:由於中間幾位與數據的每一位都相關。
例:2589的平方值爲6702921,能夠取中間的029爲地址。
六、摺疊法
特色:將關鍵碼自左到右分紅位數相等的幾部分(最後一部分位數能夠短些),而後將這幾部分疊加求和,並按哈希表表長,取後幾位做爲哈希地址。
適用於:每一位上各符號出現機率大體相同的狀況。
法1:移位法 ── 將各部分的最後一位對齊相加。
法2:間界疊加法──從一端向另外一端沿分割界來回摺疊後,最後一位對齊相加。
例:元素42751896, 用法1: 427+518+96=1041 用法2: 427 518 96—> 724+518+69 =1311
Hash表解決衝突的方法主要有如下幾種:
開放定址法(開地址法)、 鏈地址法(拉鍊法)、 再哈希法(雙哈希函數法)、 創建一個公共溢出區,而最經常使用的就是開發定址法和鏈地址法。
一、開放定址法
若是兩個數據元素的哈希值相同,則在哈希表中爲後插入的數據元素另外選擇一個表項。當程序查找哈希表時,若是沒有在第一個對應的哈希表項中找到符合查找要求的數據元素,程序就會繼續日後查找,直到找到一個符合查找要求的數據元素,或者遇到一個空的表項。線性探測帶來的最大問題就是衝突的堆積,你把別人預約的坑佔了,別人也就要像你同樣去找坑。改進的辦法有二次方探測法和隨機數探測法。開放地址法包括線性探測、二次探測以及雙重散列等方法。
設計思路:有衝突時就去尋找下一個空的哈希地址,只要哈希表足夠大,空的哈希地址總能找到,並將數據元素存入。
含義:一旦衝突,就找附近(下一個)空地址存入。
具體實現:
1) 線性探測法
Hi=(Hash(key)+di) mod m ( 1≤i < m ) 其中: Hash(key)爲哈希函數 m爲哈希表長度 di 爲增量序列 1,2,…m-1,且di=i
例:
關鍵碼集爲 {47,7,29,11,16,92,22,8,3},
設:哈希表表長爲m=11; 哈希函數爲Hash(key)=key mod 11; 擬用線性探測法處理衝突。建哈希表以下:
解釋:
① 4七、7(以及十一、1六、92)均是由哈希函數獲得的沒有衝突的哈希地址;
② Hash(29)=7,哈希地址有衝突,需尋找下一個空的哈希地址:由H1=(Hash(29)+1) mod 11=8,哈希地址8爲空,所以將29存入。
③ 另外,2二、八、3一樣在哈希地址上有衝突,也是由H1找到空的哈希地址的。其中3 還連續移動了兩次(二次彙集)
int FindHash(SeqList* pL, KeyType K) { int c=0; int p=Hash(K); /*求得哈希地址*/ while(pL->data[p].key!=NULL_KEY && K!=pL->data[p].key && ++c<MAXNUM) p=Hash(K+c); if(K==pL->data[p].key) { printf("\n成功找到 %d", K); return p; /*查找成功,p返回待查數據元素下標*/ } else if(pL->data[p].key==NULL_KEY) { printf("\n沒法找到 %d , 在位置 %d 插入。", K,p); pL->data[p].key = K; pL->n++; return p; } else { printf("\n沒法找到 %d , 表已滿。", K); return -1; } }
討論:
線性探測法的優勢:只要哈希表未被填滿,保證能找到一個空地址單元存放有衝突的元素;
線性探測法的缺點:可能使第i個哈希地址的同義詞存入第i+1個哈希地址,這樣本應存入第i+1個哈希地址的元素變成了第i+2個哈希地址的同義詞,……,所以,可能出現不少元素在相鄰的哈希地址上「堆積」起來,大大下降了查找效率。
解決方案:可採用二次探測法或僞隨機探測法,以改善「堆積」問題。
2) 二次探測法
仍舉上例,改用二次探測法處理衝突,建表以下:
Hi=(Hash(key)±di) mod m 其中:Hash(key)爲哈希函數 m爲哈希表長度,m要求是某個4k+3的質數; di爲增量序列 12,-12,22,-22,…,q2
注:只有3這個關鍵碼的衝突處理與上例不一樣, Hash(3)=3,哈希地址上衝突,由 H1=(Hash(3)+12) mod 11=4,仍然衝突; H2=(Hash(3)-12) mod 11=2,找到空的哈希地址,存入。
2、鏈地址法(拉鍊法)
基本思想:將具備相同哈希地址的記錄鏈成一個單鏈表,m個哈希地址就設m個單鏈表,而後用一個數組將m個單鏈表的表頭指針存儲起來,造成一個動態的結構。
注:有衝突的元素能夠插在表尾,也能夠插在表頭
例:設{ 47, 7, 29, 11, 16, 92, 22, 8, 3, 50, 37, 89 }的哈希函數爲: Hash(key)=key mod 11, 用拉鍊法處理衝突,則建表以下圖所示。
三、再哈希法(雙哈希函數法)
Hi=RHi(key) i=1, 2, …,k
RHi均是不一樣的哈希函數,當產生衝突時就計算另外一個哈希函數,直到衝突再也不發生。
優勢:不易產生彙集;
缺點:增長了計算時間。
4. 創建一個公共溢出區
思路:除設立哈希基本表外,另設立一個溢出向量表。 全部關鍵字和基本表中關鍵字爲同義詞的記錄,無論它們由哈希函數獲得的地址是什麼,一旦發生衝突,都填入溢出表。
哈希表的查找及分析
哈希查找過程
哈希表的主要目的是用於快速查找,且插入和刪除操做都要用到查找。因爲散列表的特殊組織形式,其查找有特殊的方法。 設散列爲HT[0…m-1],散列函數爲H(key),解決衝突的方法爲R(x, i) ,則在散列表上查找定值爲K的記錄的過程如圖所示。
查找效率分析
明確:散列函數沒有「萬能」通式,要根據元素集合的特性而分別構造。
討論:哈希查找的速度是否爲真正的O(1)?
不是。因爲衝突的產生,使得哈希表的查找過程仍然要進行比較,仍然要以平均查找長度ASL來衡量。 通常地,ASL依賴於哈希表的裝填因子,它標誌着哈希表的裝滿程度。
討論:
1) 散列存儲的查找效率究竟是多少?
答:ASL與裝填因子α有關!既不是嚴格的O(1),也不是O(n)
2)「衝突」是否是特別討厭?
答:不必定!正由於有衝突,使得文件加密後沒法破譯(不可逆,是單向散列函數,可用於數字簽名)。 利用了哈希表性質:源文件稍稍改動,會致使哈希表變更很大。
練習:
給定關鍵字序列11,78,10,1,3,2,4,21,試分別用順序查找、二分查找、二叉排序樹查找、散列查找(用線性探查法和拉鍊法)來實現查找,試畫出它們的對應存儲形式(順序查找的順序表,二分查找的斷定樹,二叉排序樹查找的二叉排序樹及兩種散列查找的散列表),並求出每一種查找的成功平均查找長度。散列函數H(k)=k%11。
1) 順序查找的成功平均查找長度爲: ASL=(1+2+3+4+5+6+7+8)/8=4.5;
2) 二分查找的斷定樹(中序序列爲從小到大排列的有序序列)如圖所示
從上圖能夠獲得二分查找的成功平均查找長度爲: ASL=(1+2*2+3*4+4)/8=2.625;
3) 二叉排序樹(關鍵字順序已肯定,該二叉排序樹應惟一)如圖 所示
從圖能夠獲得二叉排序樹查找的成功平均查找長度爲: ASL=(1+2*2+3*2+4+5*2)=3.125;
4) 線性探查法解決衝突的散列表如圖所示
從圖能夠獲得線性探查法的成功平均查找長度爲: ASL=(1+1+2+1+3+2+1+8)/8=2.375;
5) 拉鍊法解決衝突的散列表 如圖所示
從圖能夠獲得拉鍊法的成功平均查找長度爲: ASL=(1*6+2*2)/8=1.25。