數(shù)據(jù)結(jié)構(gòu)(十六) -- 散列表(Hash table)

散列表(Hash table)——將條目的關(guān)鍵碼視作其在映射結(jié)構(gòu)中的存放位置

散列表由兩個(gè)要素構(gòu)成:桶數(shù)組與散列函數(shù)

桶數(shù)組

散列表使用的桶數(shù)組(Bucket array )幽歼,其實(shí)就是一個(gè)容量為 N 的普通數(shù)組 A[0..N-1],只不過在這里唠粥,我們將其中的每個(gè)單元都想象為一個(gè)“桶”(Bucket)吠式,每個(gè)桶單元里都可以存放一個(gè)條目蠢古。

另外我們還需要某一函數(shù),將任意關(guān)鍵碼轉(zhuǎn)換為介于 0 與 N-1 之間的整數(shù)??這個(gè)函數(shù)就是所謂的散列函數(shù)(Hash function)。

散列函數(shù)

為了將散列技術(shù)推廣至一般類型的關(guān)鍵碼珍坊,我們需要借助散列函數(shù) h侵状,將關(guān)鍵碼 key映射為一個(gè)整數(shù) h(key) ∈ [0..N-1]赞弥,并將對(duì)應(yīng)的條目存放至第 h(key)號(hào)桶內(nèi),其中 N 為桶數(shù)組的容量趣兄。

如果將桶數(shù)組記作 A[ ]绽左,這一技術(shù)就可以總結(jié)為“將條目 e = (key, value)存放至 A [ h ( key ) ] 中”。

反過來(lái)诽俯,為了查找關(guān)鍵碼為 key 的條目妇菱,只需取出桶單元 A[h(key)]中存放的對(duì)象。因此暴区,h(key)也被稱作 e 的散列地址闯团。

不過,若要兌現(xiàn)上述構(gòu)思仙粱,還需要滿足另一個(gè)條件—— h( )是一個(gè)單射房交,即不同的關(guān)鍵碼key1 ≠ key2 必然對(duì)應(yīng)于不同的散列地址h(key1) ≠ h(key2)。不幸的是伐割,在絕大多數(shù)應(yīng)用問題中候味,這一條件都很難滿足。如果不同關(guān)鍵碼的散列地址相同隔心,我們就說該散列發(fā)生了沖(Collision)白群。

一個(gè)好的散列函數(shù) h()必須兼顧以下兩條基本要求:

  • h( )應(yīng)該盡可能接近單射;
  • 對(duì)于任何關(guān)鍵碼 key硬霍,h(key)的計(jì)算必須能夠在 O(1)時(shí)間內(nèi)完成帜慢。

關(guān)于散列函數(shù)的計(jì)算,Java有其特有的習(xí)慣唯卖。Java將h(key)的計(jì)算劃分為兩步:

  • (1)將一般性的關(guān)鍵碼key轉(zhuǎn)換為一個(gè)稱作“散列碼”的整數(shù)粱玲;
  • (2)然后再通過所謂的“壓縮函數(shù)”將該整數(shù)映射至區(qū)間[0..N-1]內(nèi)。
散列碼

Java 可以幫助我們將任意類型的關(guān)鍵碼 key 轉(zhuǎn)換為一個(gè)整數(shù)拜轨,稱作 key 的散列碼(Hash code)抽减。請(qǐng)注意,散列碼距離我們最終所需的散列地址還有很大距離??它不見得落在區(qū)間[0..N-1]內(nèi)橄碾,甚至不見得是正的整數(shù)卵沉。

不過這并不要緊颠锉,在這一階段我們最關(guān)心的是:各關(guān)鍵碼的散列碼之間,應(yīng)盡可能地減少?zèng)_突偎箫。顯然木柬,要是在這一階段就發(fā)生沖突,后面的沖突就無(wú)法避免淹办。

此外眉枕,從判等器的判等效果來(lái)看,散列碼必須與關(guān)鍵碼對(duì)象相互一致:被判等器 EqualityTester 判定為相等的兩個(gè)關(guān)鍵碼怜森,對(duì)應(yīng)的散列碼也應(yīng)該相等速挑。

Java 的通用類 Object 提供了一個(gè)默認(rèn)的散列碼轉(zhuǎn)換方法 hashCode(),利用它可以將任意對(duì)象實(shí)例映射為“代表”該對(duì)象的某個(gè)整數(shù)副硅。具體來(lái)說姥宝,hashCode()方法的返回值是一個(gè) 32 位 int 型整數(shù)(實(shí)際上,這一默認(rèn) hashCode()方法所返回的不過就是對(duì)象在內(nèi)存中的存儲(chǔ)地址)恐疲。

遺憾的是腊满,這一看似再自然不過的方法,實(shí)際上存在著嚴(yán)重的缺
陷培己,因此我們?cè)谑褂脮r(shí)需格外小心碳蛋。

比如,這種散列碼的轉(zhuǎn)換辦法對(duì)字符串型關(guān)鍵碼就極不適用省咨。若兩個(gè)字符串對(duì)象完全相等肃弟,本應(yīng)該將它們轉(zhuǎn)換為同一散列碼,但由于它們的內(nèi)存地址不同零蓉,由 hashCode()得到的散列碼將絕對(duì)不會(huì)一樣笤受。
實(shí)際上,在實(shí)現(xiàn) String 類時(shí)敌蜂,Java 已經(jīng)將 Object 類的 hashCode()方法改寫為一種更加適宜于字符串關(guān)鍵碼的方法箩兽。

壓縮函數(shù)

(1)模余法

最簡(jiǎn)單的壓縮辦法,就是取 N 為素?cái)?shù)章喉,并將散列碼 i 映射為:

** | i | mod N **

之所以將 N 選取為素?cái)?shù)比肄,是為了最大程度地將散列碼均勻地映射至[0..N-1]區(qū)間內(nèi)。
比如囊陡,對(duì)于散列碼集合{200, 205, 210, 215, …, 690, 695, 700},若選取 N = 100掀亥,則其中的每個(gè)散列碼都會(huì)與另外的至少四個(gè)關(guān)鍵碼相沖突撞反;而若改用 N = 101,則不會(huì)有任何沖突搪花。

(2)MAD 法

采用一種將乘法(Mutiply)遏片、加法(Add)和除法(Divide)結(jié)合起來(lái)的方法嘹害,該方法也因此得名。具體來(lái)說吮便,對(duì)于散列碼 i笔呀,MAD 法會(huì)將 i 映射為:

** | a×i + b | mod N **

其中 N 仍為素?cái)?shù),a>0髓需,b>0许师,a mod N ≠ 0,它們都是在確定壓縮函數(shù)時(shí)隨機(jī)選取的常數(shù)僚匆。

基于散列表實(shí)現(xiàn)映射類

我們給出基于散列表實(shí)現(xiàn)的映射結(jié)構(gòu):

package dsa.Map;

import dsa.Iterator.Iterator;
import dsa.Iterator.IteratorElement;
import dsa.List.List;
import dsa.List.List_DLNode;
import dsa.PriorityQueue.Entry;

public class Map_HashTable implements Map {

    /*
     * 基于散列表實(shí)現(xiàn)的映射結(jié)構(gòu) 采用分離鏈策略解決沖突
     */

    private Map[] A;// 桶數(shù)組微渠,每個(gè)桶本身也是一個(gè)(基于列表實(shí)現(xiàn)的)映射結(jié)構(gòu)
    private int N;// 散列表長(zhǎng)
    private final double maxLemda = 0.75;// 裝填因子上限
    private int size;// 映射結(jié)構(gòu)的規(guī)模
    private EqualityTester T;// 判等器
    // 默認(rèn)構(gòu)造方法

    public Map_HashTable() {
        this(0, new EqualityTesterDefault());
    }

    // 構(gòu)造方法
    public Map_HashTable(int n, EqualityTester t) {
        T = t;
        N = p(n);// 桶數(shù)組容量取為不小于n的最小素?cái)?shù)
        A = new Map[N];
        for (int i = 0; i < N; i++)
            A[i] = new Map_DLNode(T);
        size = 0;
    }

    /***************************** 輔助方法 *****************************/
    // 散列定址函數(shù)(采用模余法)
    private int h(Object key) {
        return key.hashCode() % N;
    }

    // 判斷n是否為素?cái)?shù)
    private static boolean prime(int n) {
        for (int i = 3; i < 1 + Math.sqrt(n); i++)
            if (n / i * i == n)
                return false;
        return true;
    }

    // 取不小于n的最小素?cái)?shù)
    private static int p(int n) {
        if (3 > n)
            n = 3;
        n = n | 1;// 奇數(shù)化
        while (!prime(n))
            n += 2;
        return n;
    }

    /***************************** ADT方法 *****************************/
    // 查詢映射結(jié)構(gòu)當(dāng)前的規(guī)模
    public int getSize() {
        return size;
    }

    // 判斷映射結(jié)構(gòu)是否為空
    public boolean isEmpty() {
        return 0 == size;
    }

    // 若M中存在以key為關(guān)鍵碼的條目,則返回該條目的數(shù)據(jù)對(duì)象咧擂;否則逞盆,返回null
    public Object get(Object key) {
        return A[h(key)].get(key);
    }

    // 若M中不存在以key為關(guān)鍵碼的條目,則將條目(key, value)加入到M中并返回null
    // 否則松申,將已有條目的數(shù)據(jù)對(duì)象替換為value云芦,并返回原先的數(shù)據(jù)對(duì)象
    public Object put(Object key, Object value) {
        Object oldValue = A[h(key)].put(key, value);
        if (null == oldValue) {// 若插入的條目未出現(xiàn)于原散列表中,則
            size++;// 更新規(guī)模記錄
            if (size > N * maxLemda)
                rehash();// 若裝填因子過大贸桶,則重散列
        }
        return oldValue;
    }

    // 若M中存在以key為關(guān)鍵碼的條目舅逸,則刪除之并返回其數(shù)據(jù)對(duì)象;否則刨啸,返回null
    public Object remove(Object key) {
        Object oldValue = A[h(key)].remove(key);
        if (null != oldValue)
            size--;
        return oldValue;
    }

    // 返回M中所有條目的一個(gè)迭代器
    // 將各桶對(duì)應(yīng)的映射結(jié)構(gòu)的迭代器串接起來(lái)堡赔,構(gòu)成整體的迭代器
    public Iterator entries() {
        List L = new List_DLNode();
        for (int i = 0; i < N; i++) {
            Iterator it = A[i].entries();
            while (it.hasNext())
                L.insertLast(it.getNext());
        }
        return new IteratorElement(L);
    }

    // 重散列
    private void rehash() {
        Iterator it = this.entries();
        N = p(N << 1);
        A = new Map[N];// 桶數(shù)組容量至少加倍
        for (int i = 0; i < N; i++)
            A[i] = new Map_DLNode(T);// 為每個(gè)桶分配一個(gè)子映射
        while (it.hasNext()) {// 將其對(duì)應(yīng)的映射結(jié)構(gòu)中的
            Entry e = (Entry) it.getNext();// 各條目逐一取出,將其
            Object k = e.getKey();// 關(guān)鍵碼和
            Object v = e.getValue();// 數(shù)據(jù)對(duì)象
            A[h(k)].put(k, v);// 整合為新的條目设联,插入對(duì)應(yīng)的子映射中
        }
    }
}
裝填因子

對(duì)于散列表的性能而言善已,裝填因子λ = n/N是最重要的影響因素。

如果λ > 1离例,則沖突在所難免换团。

實(shí)際上,關(guān)于散列表平均復(fù)雜度的分析結(jié)果指出:

  • 采取分離鏈策略時(shí)應(yīng)該保持λ < 0.9宫蛆,
  • 采取開放定址策略時(shí)則應(yīng)該保持λ < 0.5

這些都得到了實(shí)驗(yàn)統(tǒng)計(jì)的證明艘包。

若采用分離鏈策略,則在發(fā)生沖突的桶中耀盗,對(duì)條目的查找將退化為對(duì)鏈表的查找想虎。因此,隨著λ不斷接近于 1叛拷,發(fā)生沖突的概率也將不斷接近于 100%舌厨,從而導(dǎo)致更多的時(shí)間消耗于對(duì)鏈表的查找,使得各種操作的效率下降忿薇。

當(dāng)采用開放定址策略時(shí)裙椭,隨著λ超過 0.5 并不斷提高躏哩,條目在桶數(shù)組中聚集的程度將急速加劇,于是揉燃,需要經(jīng)過越來(lái)越多次的探測(cè)才能完成一次查找扫尺。

重散列

通常都采用重散列的方法??將所有條目全部取出,將桶數(shù)組的規(guī)模
加倍炊汤,然后將各條目重新插入其中正驻。

例如,Java 內(nèi)建的 java.util.HashMap 類實(shí)現(xiàn)了映射結(jié)構(gòu)的 ADT婿崭,在創(chuàng)建該類的對(duì)象時(shí)拨拓,程序員可以指定裝填因子的上限(默認(rèn)設(shè)置為 0.75)。一旦裝填因子超出這一范圍氓栈,java.util.HashMap會(huì)自動(dòng)進(jìn)行重散列(Rehashing)渣磷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市授瘦,隨后出現(xiàn)的幾起案子醋界,更是在濱河造成了極大的恐慌,老刑警劉巖提完,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件形纺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡徒欣,警方通過查閱死者的電腦和手機(jī)逐样,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)打肝,“玉大人脂新,你說我怎么就攤上這事〈炙螅” “怎么了争便?”我有些...
    開封第一講書人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)断医。 經(jīng)常有香客問我滞乙,道長(zhǎng),這世上最難降的妖魔是什么鉴嗤? 我笑而不...
    開封第一講書人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任斩启,我火速辦了婚禮,結(jié)果婚禮上醉锅,老公的妹妹穿的比我還像新娘兔簇。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開白布男韧。 她就那樣靜靜地躺著,像睡著了一般默垄。 火紅的嫁衣襯著肌膚如雪此虑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評(píng)論 1 290
  • 那天口锭,我揣著相機(jī)與錄音朦前,去河邊找鬼。 笑死鹃操,一個(gè)胖子當(dāng)著我的面吹牛韭寸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荆隘,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼恩伺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了椰拒?” 一聲冷哼從身側(cè)響起晶渠,我...
    開封第一講書人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎燃观,沒想到半個(gè)月后褒脯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缆毁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年番川,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脊框。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡颁督,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缚陷,到底是詐尸還是另有隱情适篙,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布箫爷,位于F島的核電站嚷节,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏虎锚。R本人自食惡果不足惜硫痰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窜护。 院中可真熱鬧效斑,春花似錦、人聲如沸柱徙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至敌完,卻和暖如春储耐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背滨溉。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工什湘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晦攒。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓闽撤,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親脯颜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哟旗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 沖突的普遍性 ? ? 生日悖論 我們可以考慮這樣一個(gè)實(shí)際問題:某課堂上的所有學(xué)生中,是否由某兩位在同一天過生日(稱...
    峰峰小閱讀 840評(píng)論 0 1
  • 因?yàn)橹熬蛷?fù)習(xí)完數(shù)據(jù)結(jié)構(gòu)了伐脖,所以為了保持記憶热幔,整理了一份復(fù)習(xí)綱要,復(fù)習(xí)的時(shí)候可以看著綱要想具體內(nèi)容讼庇。 樹 樹的基本...
    牛富貴兒閱讀 6,836評(píng)論 3 10
  • 9.3.3 快速排序 ??快速排序?qū)⒃瓟?shù)組劃分為兩個(gè)子數(shù)組绎巨,第一個(gè)子數(shù)組中元素小于等于某個(gè)邊界值,第二個(gè)子數(shù)組中的...
    RichardJieChen閱讀 1,839評(píng)論 0 3
  • 生命中最美麗的海, 一簇簇的歌聲歼跟,一朵朵的期待和媳。 花開時(shí)來(lái),花落時(shí)也要來(lái)哈街。 因?yàn)椋?有許多故事留瞳, 開始動(dòng)人,結(jié)局更...
    陽(yáng)光樂生活閱讀 465評(píng)論 3 3
  • 文/小葉 原本這樣一個(gè)氣溫稍稍回升的周末骚秦,我因?yàn)槲覆∷龋瑹o(wú)法賴床,離開了溫柔鄉(xiāng)作箍,買了饅頭豆?jié){硬梁,在看臺(tái)上享受春光溫暖。...
    博土閱讀 192評(píng)論 6 2