1. 程式人生 > >程式設計師,你應該知道的資料結構之雜湊表

程式設計師,你應該知道的資料結構之雜湊表

雜湊表簡介

雜湊表也叫散列表,雜湊表是一種資料結構,它提供了快速的插入操作和查詢操作,無論雜湊表總中有多少條資料,插入和查詢的時間複雜度都是為O(1),因為雜湊表的查詢速度非常快,所以在很多程式中都有使用雜湊表,例如拼音檢查器。

雜湊表也有自己的缺點,雜湊表是基於陣列的,我們知道陣列建立後擴容成本比較高,所以當雜湊表被填滿時,效能下降的比較嚴重。

雜湊表採用的是一種轉換思想,其中一箇中要的概念是如何將鍵或者關鍵字轉換成陣列下標?在雜湊表中,這個過程有雜湊函式來完成,但是並不是每個鍵或者關鍵字都需要通過雜湊函式來將其轉換成陣列下標,有些鍵或者關鍵字可以直接作為陣列的下標。我們先來通過一個例子來理解這句話。

我們上學的時候,大家都會有一個學號1-n號中的一個號碼,如果我們用雜湊表來存放班級裡面學生資訊的話,我們利用學號作為鍵或者關鍵字,這個鍵或者關鍵字就可以直接作為資料的下標,不需要通過雜湊函式進行轉化。如果我們需要安裝學生姓名作為鍵或者關鍵字,這時候我們就需要雜湊函式來幫我們轉換成陣列的下標。

雜湊函式

雜湊函式的作用是幫我們把非int的鍵或者關鍵字轉化成int,可以用來做陣列的下標。比如我們上面說的將學生的姓名作為鍵或者關鍵字,這是就需要雜湊函式來完成,下圖是雜湊函式的轉換示意圖。


雜湊函式的寫法有很多中,我們來看看HashMap中的雜湊函式

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

HashMap中利用了hashCode來完成這個轉換。雜湊函式不管怎麼實現,都應該滿足下面三個基本條件:

  • 雜湊函式計算得到的雜湊值是一個非負整數
  • 如果 key1 = key2,那 hash(key1) == hash(key2)
  • 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)

第一點:因為陣列的下標是從0開始,所以雜湊函式生成的雜湊值也應該是非負數

第二點:同一個key生成的雜湊值應該是一樣的,因為我們需要通過key查詢雜湊表中的資料

第三點:看起來非常合理,但是兩個不一樣的值通過雜湊函式之後可能才生相同的值,因為我們把巨大的空間轉出成較小的陣列空間時,不能保證每個數字都對映到陣列空白處。所以這裡就會才生衝突,在雜湊表中我們稱之為雜湊衝突

雜湊衝突

雜湊衝突是不可避免的,我們常用解決雜湊衝突的方法有兩種開放地址法和連結串列法

開放地址法

在開放地址法中,若資料不能直接存放在雜湊函式計算出來的陣列下標時,就需要尋找其他位置來存放。在開放地址法中有三種方式來尋找其他的位置,分別是線性探測、二次探測、再雜湊法

線性探測

線性探測的插入

線上性探測雜湊表中,資料的插入是線性的查詢空白單元,例如我們將數88經過雜湊函式後得到的陣列下標是16,但是在陣列下標為16的地方已經存在元素,那麼就找17,17還存在元素就找18,一直往下找,直到找到空白地方存放元素。我們來看下面這張圖


我們向雜湊表中新增一個元素錢多多錢多多經過雜湊函式後得到的陣列下標為0,但是在0的位置已經有張三了,所以下標往前移,直到下標4才為空,所以就將元素錢多多新增到陣列下標為4的地方。

線性探測雜湊表的插入實現起來也非常簡單,我們來看看雜湊表的插入程式碼

/**
 * 雜湊函式
 * @param key
 * @return
 */
private int hash(int key) {
    return (key % size);
}
/**
 * 插入
 * @param student
 */
public void insert(Student student){
    int key = student.getKey();
    int hashVal = hash(key);
    while (array[hashVal] !=null && array[hashVal].getKey() !=-1){
        ++hashVal;
        // 如果超過陣列大小,則從第一個開始找
        hashVal %=size;
    }
    array[hashVal] = student;
}

測試插入

public static void main(String[] args) {
    LinearProbingHash hash = new LinearProbingHash(10);
    Student student = new Student(1,"張三");
    Student student1 = new Student(2,"王強");
    Student student2 = new Student(5,"張偉");
    Student student3 = new Student(11,"寶強");
    hash.insert(student);
    hash.insert(student1);
    hash.insert(student2);
    hash.insert(student3);
    hash.disPlayTable();
}

按照上面學習的線性探測知識,studentstudent2雜湊函式得到的值應該都為1,由於1已經被student佔據,下標為2的位置被student1佔據,所以student2只能存放在下標為3的位置。下圖為測試結果。

線性探測的查詢

線性探測雜湊表的查詢過程有點兒類似插入過程。我們通過雜湊函式求出要查詢元素的鍵值對應的雜湊值,然後比較陣列中下標為雜湊值的元素和要查詢的元素。如果相等,則說明就是我們要找的元素;否則就順序往後依次查詢。如果遍歷到陣列中的空閒位置,還沒有找到,就說明要查詢的元素並沒有在雜湊表中。

線性探測雜湊表的查詢程式碼

/**
 * 查詢
 * @param key
 * @return
 */
public Student find(int key){
    int hashVal = hash(key);
    while (array[hashVal] !=null){
        if (array[hashVal].getKey() == key){
            return array[hashVal];
        }
        ++hashVal;
        hashVal %=size;
    }

    return null;
}

線性探測的刪除

線性探測雜湊表的刪除相對來說比較複雜一點,我們不能簡單的把這一項資料刪除,讓它變成空,為什麼呢?

線性探測雜湊表在查詢的時候,一旦我們通過線性探測方法,找到一個空閒位置,我們就可以認定雜湊表中不存在這個資料。但是,如果這個空閒位置是我們後來刪除的,就會導致原來的查詢演算法失效。本來存在的資料,會被認定為不存在。?

因此我們需要一個特殊的資料來頂替這個被刪除的資料,因為我們的學生學號都是正數,所以我們用學號等於-1來代表被刪除的資料。

這樣會帶來一個問題,如何線上性探測雜湊表中做了多次操作,會導致雜湊表中充滿了學號為-1的資料項,使的雜湊表的效率下降,所以很多雜湊表中沒有提供刪除操作,即使提供了刪除操作的,也儘量少使用刪除函式。

線性探測雜湊表的刪除程式碼實現

/**
 * 刪除
 * @param key
 * @return
 */
public Student delete(int key){
    int hashVal = hash(key);
    while (array[hashVal] !=null){
        if (array[hashVal].getKey() == key){
            Student temp = array[hashVal];
            array[hashVal]= noStudent;
            return temp;
        }
        ++hashVal;
        hashVal %=size;
    }
    return null;
}

二次探測

線上性探測雜湊表中,資料會發生聚集,一旦聚集形成,它就會變的越來越大,那些雜湊函式後落在聚集範圍內的資料項,都需要一步一步往後移動,並且插入到聚集的後面,因此聚集變的越大,聚集增長的越快。這個就像我們在逛超市一樣,當某個地方人很多時,人只會越來越多,大家都只是想知道這裡在幹什麼。

二次探測是防止聚集產生的一種嘗試,思想是探測相隔較遠的單元,而不是和原始位置相鄰的單元。線上性探測中,如果雜湊函式得到的原始下標是x,線性探測就是x+1,x+2,x+3......,以此類推,而在二次探測中,探測過程是x+1,x+4,x+9,x+16,x+25......,以此類推,到原始距離的步數平方,為了方便理解,我們來看下面這張圖


還是使用線性探測中的例子,線上性探測中,我們從原始探測位置每次往後推一位,最後找到空位置,線上性探測中我們找到錢多多的儲存位置需要經過4步。在二次探測中,每次是原始距離步數的平方,所以我們只需要兩次就找到錢多多的儲存位置。

二次探測的問題

二次探測消除了線性探測的聚集問題,這種聚集問題叫做原始聚集,然而,二次探測也產生了新的聚集問題,之所以會產生新的聚集問題,是因為所有對映到同一位置的關鍵字在尋找空位時,探測的位置都是一樣的。

比如講1、11、21、31、41依次插入到雜湊表中,它們對映的位置都是1,那麼11需要以一為步長探測,21需要以四為步長探測,31需要為九為步長探測,41需要以十六為步長探測,只要有一項對映到1的位置,就需要更長的步長來探測,這個現象叫做二次聚集。

二次聚集不是一個嚴重的問題,因為二次探測不怎麼使用,這裡我就不貼出二次探測的原始碼,因為雙雜湊是一種更加好的解決辦法。

雙雜湊

雙雜湊是為了消除原始聚集和二次聚集問題,不管是線性探測還是二次探測,每次的探測步長都是固定的。雙雜湊是除了第一個雜湊函式外再增加一個雜湊函式用來根據關鍵字生成探測步長,這樣即使第一個雜湊函式對映到了陣列的同一下標,但是探測步長不一樣,這樣就能夠解決聚集的問題。

第二個雜湊函式必須具備如下特點

  • 和第一個雜湊函式不一樣
  • 不能輸出為0,因為步長為0,每次探測都是指向同一個位置,將進入死迴圈,經過試驗得出stepSize = constant-(key%constant);形式的雜湊函式效果非常好,constant是一個質數並且小於陣列容量

我們將上面的新增改變成雙雜湊探測,示意圖如下:

雙雜湊的雜湊表寫起來來線性探測差不多,就是把探測步長通過關鍵字來生成

新增第二個雜湊函式
 /**
 * 根據關鍵字生成探測步長
 * @param key
 * @return
 */
private int stepHash(int key) {
    return 7 - (key % 7);
}
雙雜湊的插入
/**
 * 雙雜湊插入
 *
 * @param student
 */
public void insert(Student student) {
    int key = student.getKey();
    int hashVal = hash(key);
    // 獲取步長
    int stepSize = stepHash(key);
    while (array[hashVal] != null && array[hashVal].getKey() != -1) {
        hashVal +=stepSize;
        // 如果超過陣列大小,則從第一個開始找
        hashVal %= size;
    }
    array[hashVal] = student;
}
雙雜湊的查詢
/**
 * 雙雜湊查詢
 *
 * @param key
 * @return
 */
public Student find(int key) {
    int hashVal = hash(key);
    int stepSize = stepHash(key);
    while (array[hashVal] != null) {
        if (array[hashVal].getKey() == key) {
            return array[hashVal];
        }
        hashVal +=stepSize;
        hashVal %= size;
    }

    return null;
}
雙雜湊的刪除
/**
 * 雙雜湊刪除
 *
 * @param key
 * @return
 */
public Student delete(int key) {
    int hashVal = hash(key);
    int stepSize = stepHash(key);
    while (array[hashVal] != null) {
        if (array[hashVal].getKey() == key) {
            Student temp = array[hashVal];
            array[hashVal] = noStudent;
            return temp;
        }
        hashVal +=stepSize;
        hashVal %= size;
    }
    return null;
}

雙雜湊的實現比較簡單,但是雙雜湊有一個特別高的要求就是表的容量需要是一個質數,為什麼呢?

為什麼雙雜湊需要雜湊表的容量是一個質數?

假設我們雜湊表的容量為15,某個關鍵字經過雙雜湊函式後得到的陣列下標為0,步長為5。那麼這個探測過程是0,5,10,0,5,10,一直只會嘗試這三個位置,永遠找不到空白位置來存放,最終會導致崩潰。

如果我們雜湊表的大小為13,某個關鍵字經過雙雜湊函式後得到的陣列下標為0,步長為5。那麼這個探測過程是0,5,10,2,7,12,4,9,1,6,11,3。會查詢到雜湊表中的每一個位置。

使用開放地址法,不管使用那種策略都會有各種問題,開放地址法不怎麼使用,在開放地址法中使用較多的是雙雜湊策略。

連結串列法

開放地址法中,通過在雜湊表中再尋找一個空位解決衝突的問題,還有一種更加常用的辦法是使用連結串列法來解決雜湊衝突。連結串列法相對簡單很多,連結串列法是每個陣列對應一條連結串列。當某項關鍵字通過雜湊後落到雜湊表中的某個位置,把該條資料新增到連結串列中,其他同樣對映到這個位置的資料項也只需要新增到連結串列中,並不需要在原始陣列中尋找空位來儲存。下圖是連結串列法的示意圖。

連結串列法解決雜湊衝突程式碼比較簡單,但是程式碼比較多,因為需要維護一個連結串列的操作,我們這裡採用有序連結串列,有序連結串列不能加快成功的查詢,但是可以減少不成功的查詢時間,因為只要有一項比查詢值大,就說明沒有我們需要查詢的值,刪除時間跟查詢時間一樣,有序連結串列能夠縮短刪除時間。但是有序連結串列增加了插入時間,我們需要在有序連結串列中找到正確的插入位置。

有序連結串列操作類
public class SortedLinkList {
    private Link first;
    public SortedLinkList(){
        first = null;
    }
    /**
     *連結串列插入
     * @param link
     */
    public void insert(Link link){
        int key = link.getKey();
        Link previous = null;
        Link current = first;
        while (current!=null && key >current.getKey()){
            previous = current;
            current = current.next;
        }
        if (previous == null)
            first = link;
        else
            previous.next = link;
        link.next = current;
    }

    /**
     * 連結串列刪除
     * @param key
     */
    public void delete(int key){
        Link previous = null;
        Link current = first;
        while (current !=null && key !=current.getKey()){
            previous = current;
            current = current.next;
        }
        if (previous == null)
            first = first.next;
        else
            previous.next = current.next;
    }

    /**
     * 連結串列查詢
     * @param key
     * @return
     */
    public Link find(int key){
        Link current = first;
        while (current !=null && current.getKey() <=key){
            if (current.getKey() == key){
                return current;
            }
            current = current.next;
        }
        return null;
    }
    public void displayList(){
        System.out.print("List (first-->last): ");
        Link current = first;
        while (current !=null){
            current.displayLink();
            current = current.next;
        }
        System.out.println(" ");
    }
}
連結串列法雜湊表插入

在連結串列法中由於產生雜湊衝的元素都存放在連結串列中,所以連結串列法的插入非常簡單,只需要在對應下標的連結串列中新增一個元素即可。

/**
 * 連結串列法插入
 *
 * @param data
 */
public void insert(int data) {
    Link link = new Link(data);
    int key = link.getKey();
    int hashVal = hash(key);
    array[hashVal].insert(link);
}
連結串列法雜湊表查詢
/**
 * 連結串列法-查詢
 *
 * @param key
 * @return
 */
public Link find(int key) {
    int hashVal = hash(key);
    return array[hashVal].find(key);
}
連結串列法雜湊表刪除

連結串列法中的刪除就不需要向開放地址法那樣將元素置為某個特定值,連結串列法中只需要找到相應的連結串列將這一項直接移除。

/**
 * 連結串列法-刪除
 *
 * @param key
 */
public void delete(int key) {
    int hashVal = hash(key);
    array[hashVal].delete(key);
}

雜湊表的效率

在雜湊表中執行插入和搜尋操作都可以達到O(1)的時間複雜度,在沒有雜湊衝突的情況下,只需要使用一次雜湊函式就可以插入一個新資料項或者查詢到一個已經存在的資料項。

如果發生雜湊衝突,插入和查詢的時間跟探測長度成正比關係,探測長度取決於裝載因子,裝載因子是用來表示空位的多少

裝載因子的計算公式:

裝載因子 = 表中已存的元素 / 表的長度

裝載因子越大,說明空閒位置越少,衝突越多,散列表的效能會下降。

開放地址法和連結串列法的比較

如果使用開放地址法,對於小型的雜湊表,雙雜湊法要比二次探測的效果好,如果記憶體充足並且雜湊表一經建立,就不再修改其容量,在這種情況下,線性探測效果相對比較好,實現起來也比較簡單,在裝載因子低於0.5的情況下,基本沒有什麼效能下降。

如果在建立雜湊表時,不知道未來儲存的資料有多少,使用連結串列法要比開放地址法好,如果使用開放地址法,隨著裝載因子的變大,效能會直線下降。

當兩者都可以選時,使用連結串列法,因為連結串列法對應不確定性更強,當資料超過預期時,效能不會直線下降。

雜湊表在JDK中有不少的實現,例如HahsMapHashTable等,對雜湊表感興趣的可以閱讀本文後去檢視JDK的相應實現,相信這可以增強你對雜湊表的理解。

如果您發現文中錯誤,還請多多指教。歡迎關注個人公眾號,一起交流學習。

個人公眾號

相關推薦

程式設計師應該知道資料結構跳錶

跳錶的原理 跳錶也叫跳躍表,是一種動態的資料結構。如果我們需要在有序連結串列中進行查詢某個值,需要遍歷整個連結串列,二分查詢對連結串列不支援,二分查詢的底層要求為陣列,遍歷整個連結串列的時間複雜度為O(n)。我們可以把連結串列改造成B樹、紅黑樹、AVL樹等資料結構來提升查詢效率,但是B樹、紅黑樹、AVL樹這些

程式設計師應該知道資料結構

雜湊表簡介 雜湊表也叫散列表,雜湊表是一種資料結構,它提供了快速的插入操作和查詢操作,無論雜湊表總中有多少條資料,插入和查詢的時間複雜度都是為O(1),因為雜湊表的查詢速度非常快,所以在很多程式中都有使用雜湊表,例如拼音檢查器。 雜湊表也有自己的缺點,雜湊表是基於陣列的,我們知道陣列建立後擴容成本比較高,所以

程式設計師應該知道資料結構

資料結構中的棧不要與 Java 中的棧混淆,他們倆不是一回事,資料結構中的棧是一種受限制的線性表,棧具有先進後出、後進先出的特點,因為棧只允許訪問最後一個數據項,即最後插入的資料項。也許你會有疑問,棧既然有這麼多限制,為什麼不用陣列或者連結串列而使用棧?在開發中,我們有特定的場景,根據特定的場景去選用資料結構

資料結構與連結串列、陣列

雜湊表 主要描述雜湊表的定義:通過關鍵碼尋找值的資料對映結構,類似於查字典 當存在雜湊衝突時,有兩種常用的方式:開發定址法和鏈地址法 開發定址法通俗的來說就是判斷該地址是否存資料,沒存就放進去,存了就找下一個地址,依次類推,問題是如果空間不足,無法處理衝突。 鏈地

資料結構(HASH)

前言    當我們在程式設計過程中,往往需要對線性表進行查詢操作。在順序表中查詢時,需要從表頭開始,依次遍歷比較a[i]與key的值是否相等,直到相等才返回索引i;在有序表中查詢時,我們經常使用的是二分查詢,通過比較key與a[i]的大小來折半查詢,直到相等時

linux核心分析--核心中使用的資料結構hlist(三)

前言: 1.基本概念: 散列表(Hash table,也叫雜湊表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。這個對映函式叫做雜湊函式,存放記錄的陣列叫做散列表。 2. 常用的構造雜湊函式的方法

資料結構的java實現

雜湊表是一種資料結構,提供快速的插入和查詢功能。雜湊表基於陣列儲存資料,因此能在O(1)時間內定位資料。關鍵字值通過雜湊函式對映為陣列下標。缺點就是陣列建立後容量固定,如果資料較多需要不斷擴充套件其長度。如何將關鍵字轉換為陣列下標?這個操作是通過雜湊函式完成的。比如,下面就

Python程式設計師必須知道的面試題

Python越來越火之後,把python作為自己的終生事業來做的話,是很多的終極目標,可是要做到知己知彼,百戰不殆,那麼你需要了解面試官出什麼題, 這些面試問題大致可以分為四類:什麼(what)?如何做(how)?說區別/談優勢(difference)以及實踐操作(practice)。 &n

一個優秀的java程式設計師需要知道的10個程式碼優化方式!

程式碼優化不息以來都是一個軌範員經常要掛在嘴邊的一個詞,特別是對付如今軌範員越來越普及,網上教程一大把的時代,良多軌範員寫出的程式碼都是為了了局而寫程式碼,從來不去考慮程式碼的優化問題,如許的程式碼拿去應聘也是非常虧損的,程式碼的優化可以直接浮現出來一個軌範員的根基功以及可塑性. 而程式碼

資料結構基礎--

雜湊函式 雜湊函式 輸入域無窮大 輸出域有邊界(1<<64) 輸入相同的樣本,一定得到相同的輸出結果 不同的樣本,有可能發生碰撞(結果相同) 在輸入源樣本量足夠大的情況下,結果將在輸出域上均勻分佈。 雜湊函式的離散性,能夠打亂樣本規律。

資料結構(Hash Table)

雜湊表定義 雜湊表是一種根據關鍵碼去尋找值的資料對映結構,該結構通過把關鍵碼對映的位置去尋找存放值的地方。 本質是一個數組,陣列中每一個元素稱為一個箱子(bin),箱子中存放的是鍵值對。 雜湊表的儲存過程如下: 根據 key 計算出它的雜湊值 h。 假設箱子的個數

Redis資料結構詳解

簡介 Redis本身是鍵值對資料庫,但是值對應多種資料結構,其中就有雜湊(即鍵值對),值中的鍵值對稱為field和value。 基本命令 命令 命令描述 hset key field

玩轉資料結構(21)--

雜湊表 一、雜湊表基礎 從習題入手【題目連結】 思路:可以不使用樹結構來實現對映,可以直接設定包含 26個 元素的陣列,對陣列中每一位表示某一個字元對應的頻率即可;索引為 0 的位置表示 a ,索引為 1 的位置表示 b ,以此類推。 雜湊表定義:把所關心的鍵通過

資料結構以及衝突的解決方案

前言 基於先前的學習計劃,最近打算深入學習Java的集合類,首先要研究的就是HashMap,在學習HashMap前,我花了幾天時間溫習了一下類中用到的資料結構 (雜湊表,二叉樹),並決定把所學的知識記錄寫成文章,本文講述的就是關於雜湊表的知識。 什麼是雜湊表 在之前的部落格文章裡,我們簡單介紹了資料結構的幾種

資料結構/散列表

本篇博文,旨在介紹雜湊表的基本概念及其用法;介紹了減少雜湊衝突的方法;並用程式碼實現了雜湊表的線性探測和雜湊桶 雜湊表的基本概念 雜湊表是一種儲存結構,它通過key值可以直接訪問該key值在記憶體中

資料結構桶的基本操作

  順序搜尋和二叉搜尋樹中,元素儲存位置和元素各關鍵碼之間沒有對應的關係,這就導致在查詢一個元素時,必須經過關鍵碼的多次比較。那麼是否有這樣一種資料結構,可以不經過任何比較,直接找到想要搜尋的元素呢?答案是肯定的,那就是通過某種函式(hashFunc)使得元素的儲存位置與它的

資料結構(散列表)

轉自:http://blog.chinaunix.net/uid-26548237-id-3480645.html 一、散列表相關概念     雜湊技術是在記錄的儲存位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個儲存位置f(key)。

淺談演算法和資料結構

在前面的系列文章中,依次介紹了基於無序列表的順序查詢,基於有序陣列的二分查詢,平衡查詢樹,以及紅黑樹,下圖是它們在平均以及最差情況下的時間複雜度: 可以看到在時間複雜度上,紅黑樹在平均情況下插入,查詢以及刪除上都達到了lgN的時間複雜度。 那麼有沒

c語言資料結構實現-/桶(hashtable/hashbucket)

一、需求 以“key-value”的形式進行插入、查詢、刪除,是否可以考慮犧牲空間換時間的做法? 二、相關知識 雜湊表(Hashtable)又稱為“雜湊表”,Hashtable是會根據索引鍵的雜湊程式程式碼組織成的索引鍵(Key)和值(Value)配對的集合。Hashtab

演算法與資料結構基礎 - (Hash Table)

Hash Table基礎 雜湊表(Hash Table)是常用的資料結構,其運用雜湊函式(hash function)實現對映,內部使用開放定址、拉鍊法等方式解決雜湊衝突,使得讀寫時間複雜度平均為O(1)。   HashMap(std::unordered_map)、HashSet(std::