日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

白话算法(6) 散列表(Hash Table)从理论到实用(中)

發(fā)布時(shí)間:2023/12/31 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 白话算法(6) 散列表(Hash Table)从理论到实用(中) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

  不用鏈接法,還有別的方法能處理碰撞嗎?捫心自問(wèn),我不敢問(wèn)這個(gè)問(wèn)題。鏈接法如此的自然、直接,以至于我不敢相信還有別的(甚至是更好的)方法。推動(dòng)科技進(jìn)步的人,永遠(yuǎn)是那些敢于問(wèn)出比外行更天真、更外行的問(wèn)題,并且善于運(yùn)用豐富的想象力找到新的可能性,而且有能力運(yùn)用科學(xué)的方法實(shí)踐的人。
  如果可以不用鏈表,把節(jié)省下來(lái)的鏈表的指針?biāo)加玫目臻g用作空槽,就可以減少碰撞的機(jī)會(huì),提高查找速度。

使用開(kāi)放尋址法處理碰撞

  不用額外的鏈表,以及任何其它額外的數(shù)據(jù)結(jié)構(gòu),就只用一個(gè)數(shù)組,在發(fā)生碰撞的時(shí)候怎么辦呢?答案只能是,再找另一個(gè)空著的槽啦!這就是開(kāi)放尋址法(open addressing)。但是這樣難道不是很不負(fù)責(zé)任的嗎?想象一下,有一趟對(duì)號(hào)入座的火車,假設(shè)它只有一節(jié)車廂,上來(lái)一位坐7號(hào)座位的旅客。過(guò)了一會(huì)兒,又上來(lái)一位旅客,他買(mǎi)到的是一張假票,也是7號(hào)座位,這時(shí)怎么辦呢?列車長(zhǎng)想了想,讓拿假票的旅客去坐8號(hào)座位。過(guò)了一會(huì)兒,應(yīng)該坐8號(hào)座位的旅客上來(lái)了,列車長(zhǎng)對(duì)他說(shuō)8號(hào)座位已經(jīng)有人了,你去坐9號(hào)座位吧。哦?9號(hào)早就有人了?10號(hào)也有人了?那你去坐11號(hào)吧。可以想見(jiàn),越到后來(lái),當(dāng)空座越來(lái)越少時(shí),碰撞的幾率就越大,尋找空座愈發(fā)地費(fèi)勁。但是,如果是火車的上座率只有50%或者更少的情況呢?也許真正坐8號(hào)座位的乘客永遠(yuǎn)不會(huì)上車,那么讓拿假票的乘客坐8號(hào)座位就是一個(gè)很好的策略了。所以,這是一個(gè)空間換時(shí)間的游戲。玩好這個(gè)游戲的關(guān)鍵是,讓旅客分散地坐在車廂里。如何才能做到這一點(diǎn)呢?答案是,對(duì)于每位不同的旅客使用不同的探查序列。例如,對(duì)于旅客 A,探查座位 7,8,23,56……直到找到一個(gè)空位;對(duì)于旅客B,探查座位 25,66,77,1,3……直到找到一個(gè)空位。如果有 m 個(gè)座位,每位旅客可以使用 <0, 1, 2, ..., m-1> 的 m! 個(gè)排列中的一個(gè)。顯而易見(jiàn),最好減少兩個(gè)旅客使用相同的探查序列的情況。也就是說(shuō),希望把每位旅客盡量分散地映射到 m! 種探查序列上。換句話說(shuō),理想狀態(tài)下,如果能夠讓每個(gè)上車的旅客,使用 m! 個(gè)探查序列中的任意一個(gè)的可能性是相同的,我們就說(shuō)實(shí)現(xiàn)了一致散列。(這里沒(méi)有用“隨機(jī)”這個(gè)詞兒,因?yàn)閷?shí)際是不可能隨機(jī)取一個(gè)探查序列的,因?yàn)樵诓檎疫@名旅客時(shí)還要使用相同的探查序列)。
  真正的一致散列是難以實(shí)現(xiàn)的,實(shí)踐中,常常采用它的一些近似方法。常用的產(chǎn)生探查序列的方法有:線性探查,二次探查,以及雙重探查。這些方法都不能實(shí)現(xiàn)一致散列,因?yàn)樗鼈兡墚a(chǎn)生的不同探查序列數(shù)都不超過(guò) m2 個(gè)(一致散列要求有 m! 個(gè)探查序列)。在這三種方法中,雙重散列能產(chǎn)生的探查序列數(shù)最多,因而能給出最好的結(jié)果(注:.net framework 的 HashTable 就是使用的雙重散列法)。
  在上一篇中,我們實(shí)現(xiàn)了一個(gè)函數(shù) h(k),它的任務(wù)是把數(shù)值 k 映射為一個(gè)數(shù)組(盡量分散)的地址。這次,我們使用開(kāi)發(fā)尋找法,需要實(shí)現(xiàn)一個(gè)函數(shù) h(k, i),它的任務(wù)是把數(shù)值 k 映射為一個(gè)地址序列,序列的第一個(gè)地址是 h(k, 0),第二個(gè)地址是 h(k, 1)……序列中的每個(gè)地址都要盡可能的分散。

線性探查

  有這樣一個(gè)可以用 10 個(gè)槽保存 0~int.MatValue (但是不能處理碰撞)的 IntSet1:

public class IntSet1 {private object[] _values = new object[10];private int H(int value){return value % 10;}public void Add(int item){_values[H(item)] = item;}public void Remove(int item){_values[H(item)] = null;}public bool Contains(int item){if (_values[H(item)] == null)return false;elsereturn (int)_values[H(item)] == item;} }

現(xiàn)在想用開(kāi)放尋址法處理碰撞,該怎么改造它?最簡(jiǎn)單的方法是,如果發(fā)現(xiàn) values[8] 已經(jīng)被占用了,就看看 values[9] 是否空著,如果 values[9] 也被占用了,就看看 values[0] 是不是還空著。完整的描述是,先使用 H() 函數(shù)獲取 k 的第一個(gè)地址,如果這個(gè)地址已被占用,就探查下一個(gè)緊挨著的地址,如果還是不能用,就探查下一個(gè)緊挨著的地址,如果到達(dá)了數(shù)組的末尾,就卷繞到數(shù)組的開(kāi)頭,如果探查了 m 次還是沒(méi)有找到空槽,就說(shuō)明數(shù)組已經(jīng)滿了,這就是線性探查(linear probing)。實(shí)現(xiàn)代碼是:

public class IntSet2 {private object[] _values = new object[10];private int H(int value){return value % 10;}private int LH(int value, int i){return (H(value) + i) % 10;}public void Add(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量do{int j = LH(item, i); // 想要探查的地址if (_values[j] == null){_values[j] = item;return;}else{i += 1;} } while (i <= 10);throw new Exception("集合溢出");}public bool Contains(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量int j = 0; // 想要探查的地址do{j = LH(item, i);if (_values[j] == null)return false;if ((int)_values[j] == item)return true;elsei += 1;} while (i <= 10);return false;}public void Remove(int item){// 有點(diǎn)不太好辦} }

  在 Add() 函數(shù)中,先探查 LH(value, 0),它等于 H(value),如果發(fā)生了碰撞,就繼續(xù)探查 LH(value, 1),它是 H(value) 的下一個(gè)地址,LH() 里面的 “... % 10”的意思是數(shù)組最后一個(gè)槽的下一個(gè)槽是第一個(gè)槽的意思。在 Contains() 函數(shù)里,使用和 Add() 函數(shù)一樣的探查序列,如果找到了 item 返回 true;如果遇到了 null,說(shuō)明 item 不在數(shù)組中。
  比較麻煩的是 Remove() 函數(shù)。不能簡(jiǎn)單地把要?jiǎng)h除的槽設(shè)為 null,那樣會(huì)導(dǎo)致 Contains() 出錯(cuò)。舉個(gè)例子,如果依次把 3,13,23 添加到 IntSet2 中,會(huì)執(zhí)行 _values[3] = 3,_values[4] = 13,_values[5] = 23。然后,Remove(13) 執(zhí)行 _values[4] = null。這時(shí),再調(diào)用 Contains(23),會(huì)依次檢查 _values[3]、_values[4]、_values[5] 直到找到 23 或遇到 null,由于 _values[4] 已經(jīng)被設(shè)為 null 了,所以 Contains(23) 會(huì)返回 false。有一個(gè)解決此問(wèn)題的方法是,在 Remove(23) 時(shí)把 _values[4] 設(shè)為一個(gè)特殊的值(例如 -1)而不是 null。這樣 Contains(23) 就不會(huì)在 _values[4] 那里因?yàn)橛龅?null 而返回錯(cuò)誤的 false 了。并且在 Add() 里,遇到 null 或 -1 都視為空槽,修改之后的代碼如下:

public class IntSet2 {private object[] _values = new object[10];private readonly int DELETED = -1;private int H(int value){return value % 10;}private int LH(int value, int i){return (H(value) + i) % 10;}public void Add(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量do{int j = LH(item, i); // 想要探查的地址if (_values[j] == null || (int)_values[j] == DELETED){_values[j] = item;return;}else{i += 1;} } while (i <= 10);throw new Exception("集合溢出");}public bool Contains(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量int j = 0; // 想要探查的地址do{j = LH(item, i);if (_values[j] == null)return false;if ((int)_values[j] == item)return true;elsei += 1;} while (i <= 10);return false;}public void Remove(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量int j = 0; // 想要探查的地址do{j = LH(item, i);if (_values[j] == null)return;if ((int)_values[j] == item){_values[j] = DELETED;return;}else{i += 1;}} while (i <= 10);} }

  但是這種實(shí)現(xiàn) Remove() 函數(shù)的方法有個(gè)很大的問(wèn)題。想象一下,如果依次添加 0、1、2、3、4、5、6、7、8、9,然后再 Remove 0、1、2、3、4、5、6、7、8,這時(shí)再調(diào)用 Contains(0),此函數(shù)會(huì)依次檢查 _values[0]、_values[1]..._values[9],這是完全無(wú)法接受的!這個(gè)問(wèn)題先放一放,我們?cè)谙乱黄€會(huì)繼續(xù)討論解決這個(gè)問(wèn)題的方法。
  線性探查法雖然比較容易實(shí)現(xiàn),但是它有一個(gè)叫做一次群集(primary clustering)的問(wèn)題。就像本文開(kāi)篇所討論的,如果 7、8、9 號(hào)座位已被占用,下一個(gè)上車的旅客,無(wú)論他的票是7號(hào)、8號(hào)還是9號(hào),都會(huì)被安排去坐10號(hào);下一個(gè)上車的旅客,無(wú)論他的票是7號(hào)、8號(hào)、9號(hào)還是10號(hào),都會(huì)被安排去坐11號(hào)……如果有 i 個(gè)連續(xù)被占用的槽,下一個(gè)空槽被占用的概率就會(huì)是 (i + 1)/m,就像血栓一樣,一旦堵住,就會(huì)越堵越厲害。這樣,使用線性探查法,很容易產(chǎn)生一長(zhǎng)串連續(xù)被占用的槽,導(dǎo)致 Contains() 函數(shù)速度變慢。
  對(duì)于線性探查法,由于初始位置 LH(k, 0) = H(k) 確定了整個(gè)探查序列,所以只有 m 種不同的探查序列。

二次探查

  可以在發(fā)生碰撞時(shí),不像線性探查那樣探查下一個(gè)緊挨著的槽,而是多偏移一些,以此緩解一次群集的問(wèn)題。二次探查(quadratic probing)讓這個(gè)偏移量依賴 i 的平方:
  h(k, i) = (h'(k) + c1i + c2i2) mod m
其中,c1 和 c2 是不為0的常數(shù)。例如,如果取 c1 = c2 = 1,二次探查的散列函數(shù)為:

private int QH(int value, int i) {return (H(value) + i + i * i) % 10; }

對(duì)于數(shù)值 7,QH() 給出的探查序列是 7、9、3、9……由于初始位置 QH(k, 0) = H(k) 確定了整個(gè)探查序列,所以二次探查同樣只有 m 種不同的探查序列。通過(guò)讓下一個(gè)探查位置以 i 的平方偏移,不容易像線性探查那樣讓被占用的槽連成一片。但是,由于只要探查的初始位置相同,探查序列就會(huì)完全相同,所以會(huì)連成一小片、一小片的,這一性質(zhì)導(dǎo)致一種程度較輕的群集現(xiàn)象,稱為二次群集(secondary clusering)

雙重散列

  造成線性探查法和二次探查法的群集現(xiàn)象的罪魁禍?zhǔn)资且坏┏跏继讲槲恢孟嗤?#xff0c;整個(gè)探查序列就相同。這樣,一旦出現(xiàn)碰撞,事情就會(huì)變得更糟。是什么造成一旦初始探查位置相同,整個(gè)探查序列就相同呢?是因?yàn)榫€性探查法和二次探查法都是讓后續(xù)的探查位置基于初始探查位置(即 H(k))向后偏移幾個(gè)位置,而這個(gè)偏移量,不管是線性的還是二次的,都僅僅是 i 的函數(shù),但是只有 k 是不同的對(duì)不對(duì)?所以必須想辦法讓偏移量是 k?的函數(shù)才行。以線性探查為例,要想辦法讓 LH(k, i) 是 k 和 i 的函數(shù),而不是 H(k) 和 i 的函數(shù)。說(shuō)干就干,我們?cè)囍丫€性探查
H(k) = k % 10
LH(k, i) = (H(k) + i) % 10
改造一下,先試試把 k 乘到 i 上面去,即
H(k) = k % 10
LH(k, i) = (H(k) + i * k) % 10
這有效果嗎?很不幸,
LH(k, i) = (H(k) + i * k) % 10
?????????? = (H(k) + i * (k%10) % 10
?????????? = (H(k) + i * H(k)) % 10
?????????? = (H(k) * (1 + i)) % 10
結(jié)果 LH(k, i) 還是 H(k) 和 i 的函數(shù)。
再試試把 k 加到 i 上,即
H(k) = k % 10
LH(k, i) = (H(k) + i?+ k) % 10
這個(gè)怎么樣?
LH(k, i) = (H(k) + i?+ k) % 10
?????????? = (H(k) + i + k%10) % 10
?????????? = (H(k) + i + H(k)) % 10
?????????? = (2*H(k) + i) % 10
太不幸了,LH(k) 仍然是 H(k) 和 i 的函數(shù)。好像怎么折騰都不行,除非把 H(K) 變成乘法散列法,或者使用雙重散列(double hashing)法:
h(k, i) = (h1(k) + i*h2(k)) mod m
其中 h1(k) 和 h2(k) 是兩個(gè)不同的散列函數(shù)。例如可以讓
h1(k) = k mod 13
h2(k) = k mod 11
h(k, i) = (h1(k) + i*h2(k)) mod 10
這樣,h(7, i) 產(chǎn)生的探查序列是 7、4、1、8、5……
h(20, i) 產(chǎn)生的探查序列是 7、6、5、4、3……
這回終于達(dá)到了初始探查位置相同,但是后續(xù)探查位置不同的目標(biāo)。
  h2(k) 的設(shè)計(jì)很有講究,搞不好會(huì)無(wú)法探查到每個(gè)空槽。以剛剛實(shí)現(xiàn)的?h(k, i) 為例,h(6, i) 的探查序列是“6、2、8、4、0、6、2、8、4、0”,如果恰巧數(shù)組中的“6、2、8、4、0”這幾個(gè)位置都被占用了,將會(huì)導(dǎo)致程序在還有空槽的狀態(tài)下拋出“集合溢出”的異常。要避免這種情況,要求 h2(k) 與 m 必須互質(zhì)。可以看一看如果 h2(k) 與 m 不是互質(zhì)的話,為什么會(huì)有無(wú)法探查數(shù)組的所有的槽的后果。例如 h2(6)=6 與 10 有公約數(shù)2,把它們代入 h(k, i):
h(6, i) = (h1(6) + i * h2(6)) mod 10
????????? = (6 + i * 6) mod 10
????????? = (6 + (i * 6) mod 10) mod 10
????????? = (6 + 2*((i*6) mod 5)) mod 10
由于 (i*6) mod 5) 只有 5 個(gè)不同的值,所以 h(6, i) 也只有 5 個(gè)值。而 h(16, i) = (3 + 5*((i*5) mod 2)) mod?10 只有2個(gè)值,真是太糟糕了。
  要想讓 h2(k) 與 m 互質(zhì),有2種方法。一種方法是讓 m 為 2 的冪,并且設(shè)計(jì)一個(gè)總是產(chǎn)生奇數(shù)的 h2(k),利用的是奇數(shù)和 2 的 m 次冪總是互質(zhì)的原理。另一種方法是讓 m 為質(zhì)數(shù),并設(shè)計(jì)一個(gè)總是產(chǎn)生比 m 小的正整數(shù)的 h2(k)。可以這么實(shí)現(xiàn)后一種方法:首先使用上一篇實(shí)現(xiàn)的 GetPrime() 函數(shù)取得一個(gè)合適的質(zhì)數(shù)作為 m,然后讓
h1(k) = k mod m
h2(k) = 1 + (k mod (m-1))
在 h2(k) 里之所以要把 (k mod (m-1))?加上個(gè) 1 是為了讓 h2(k) 永不為0。因?yàn)?h2(k) 為 0 會(huì)讓 i 不起作用,一旦正巧 h1(k) 產(chǎn)生碰撞就無(wú)法取得下一個(gè)空槽了。
這是一份完整的示例代碼,我們將會(huì)在下一篇繼續(xù)完善它:

public class IntSet4 {private object[] _values;private readonly int DELETED = -1;public IntSet4(int capacity){int size = GetPrime(capacity);_values = new object[size];}// 質(zhì)數(shù)表private readonly int[] primes = {3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};// 判斷 candidate 是否是質(zhì)數(shù)private bool IsPrime(int candidate){if ((candidate & 1) != 0) // 是奇數(shù){int limit = (int)Math.Sqrt(candidate);for (int divisor = 3; divisor <= limit; divisor += 2) // divisor = 3、5、7...candidate的平方根{if ((candidate % divisor) == 0)return false;}return true;}return (candidate == 2); // 除了2,其它偶是全都不是質(zhì)數(shù)}// 如果 min 是質(zhì)數(shù),返回 min;否則返回比 min 稍大的那個(gè)質(zhì)數(shù)private int GetPrime(int min){// 從質(zhì)數(shù)表中查找比 min 稍大的質(zhì)數(shù)for (int i = 0; i < primes.Length; i++){int prime = primes[i];if (prime >= min) return prime;}// min 超過(guò)了質(zhì)數(shù)表的范圍時(shí),探查 min 之后的每一個(gè)奇數(shù),直到發(fā)現(xiàn)下一個(gè)質(zhì)數(shù)for (int i = (min | 1); i < Int32.MaxValue; i += 2){if (IsPrime(i))return i;}return min;}int H1(int value){return value % _values.Length;}int H2(int value){return 1 + (value % (_values.Length - 1));}int DH(int value, int i){return (H1(value) + i * H2(value)) % _values.Length;}public void Add(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量do{int j = DH(item, i); // 想要探查的地址if (_values[j] == null || (int)_values[j] == DELETED){_values[j] = item;return;}else{i += 1;}} while (i <= _values.Length);throw new Exception("集合溢出");}public bool Contains(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量int j = 0; // 想要探查的地址do{j = DH(item, i);if (_values[j] == null)return false;if ((int)_values[j] == item)return true;elsei += 1;} while (i <= _values.Length);return false;}public void Remove(int item){int i = 0; // 已經(jīng)探查過(guò)的槽的數(shù)量int j = 0; // 想要探查的地址do{j = DH(item, i);if (_values[j] == null)return;if ((int)_values[j] == item){_values[j] = DELETED;return;}else{i += 1;}} while (i <= _values.Length);} }


  除了鏈接法和開(kāi)放尋址法,還有更好的方法嗎?人類永遠(yuǎn)不會(huì)停止追問(wèn),本篇卻必須結(jié)束了。下一篇,我們將參考 .net framework 源代碼,討論實(shí)現(xiàn)散列表的一些重要的細(xì)節(jié)問(wèn)題。

轉(zhuǎn)載于:https://www.cnblogs.com/1-2-3/archive/2010/10/12/hash-table-part2.html

總結(jié)

以上是生活随笔為你收集整理的白话算法(6) 散列表(Hash Table)从理论到实用(中)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。