雜湊表的兩種實現

弒曉風發表於2019-03-05
 We all make choices in life. The hard thing is to live with them. 人一生要做很多選擇,最困難的是要帶著自己的選擇生活下去。

本文主要分享的雜湊表的定義以及它的兩種實現。一種是線性探測;一種是拉鍊法。所有原始碼均已上傳至github: 連結

定義

我們先假設一下,如果所有的值都是小整數,那麼,我們可以用一個陣列來實現這樣一個無序的符號表,並且將鍵作為陣列的索引,那陣列中鍵key處所儲存的就是它所對應的值value,這就是雜湊表

雜湊表也叫雜湊表。

三個條件

總體來講,一個優秀的雜湊方法需要滿足三個條件:

  1. 一致性---等價的鍵比如產生相等的雜湊值
  2. 高效性---計算起來要簡便,不能設計的太複雜
  3. 均勻性---雜湊函式生成的值要儘可能的隨機並且均勻分佈

舉例

雜湊表的應用非常廣泛,業界的MD5,SHA,CRC等雜湊演算法;Redis的有序集合;java的LinkedHashMap,hashCode()。

雜湊衝突

金無足赤,人無完人。再好的雜湊函式也無法避免雜湊衝突。那究竟該如何解決雜湊衝突問題呢?

常用的雜湊衝突解決方法有兩類:

  • 連結串列法
  • 開放定址法

連結串列法的核心思想是,在雜湊表中,每個“桶(bucket)”或者“槽(slot)”會對應一條連結串列,所有雜湊值相同的元素我們都放到相同槽位對應的連結串列中。

優點:對記憶體利用率比較高,連結串列節點可以在需要的時候建立。對大裝載因子的容忍度更高,只要雜湊函式的值隨機均勻,即便裝載因子變成 10,也就是連結串列的長度變長了而已,雖然查詢效率有所下降,但是比起順序查詢還是快很多。

缺點:因為連結串列要儲存指標,所以對於比較小的物件的儲存,是比較消耗記憶體的,還有可能會讓記憶體的消耗翻倍。而且,因為連結串列中的結點是零散分佈在記憶體中的,不是連續的,所以對 CPU 快取是不友好的,這方面對於執行效率也有一定的影響。

開放定址法的核心思想是,如果出現了雜湊衝突,就重新探測一個空閒位置,將其插入。

優點:開放定址法不像連結串列法,需要拉很多連結串列。雜湊表中的資料都儲存在陣列中,可以有效地利用 CPU 快取加快查詢速度。而且,這種方法實現的雜湊表,序列化起來比較簡單。連結串列法包含指標,序列化就不是很容易。

缺點:用開放定址法解決衝突的雜湊表,刪除資料的時候比較麻煩,需要特殊標記已經刪除掉的資料。而且,在開放定址法中,所有的資料都儲存在一個陣列中,比起連結串列法來說,衝突的代價更高。所以,使用開放定址法解決衝突的雜湊表,裝載因子的上限不能太大。這也導致這種方法比連結串列法更浪費記憶體空間。

總結:當資料量比較小、裝載因子小的時候,適合採用開放定址法。儲存大物件、大資料量的雜湊表,適合採用連結串列法。

實現

開發定址法(以線性探測為例)

兩個構造方法

    private LinearProbingHT() {
        keys = (Key[]) new Object[size];
        values = (Value[]) new Object[size];
    }
    private LinearProbingHT(int capacity) {
        keys = (Key[]) new Object[capacity];
        values = (Value[]) new Object[capacity];
    }複製程式碼

小雜湊演算法

private int hash(Key key) {    return (key.hashCode() & 0x7fffffff) % size;}複製程式碼

擴容

    private void resize(int capacity) {
        System.out.println(size > capacity ? "縮容..." : "擴容...");
        LinearProbingHT<Key, Value> linearProbingHT;
        linearProbingHT = new LinearProbingHT<>(capacity);
        for (int i = 0; i < size; i++) {
            if (null != keys[i]) linearProbingHT.put(keys[i], values[i]);
        }
        keys = linearProbingHT.keys;
        values = linearProbingHT.values;
        size = linearProbingHT.size;
    }複製程式碼

put方法,當存的鍵值對的數量大於容器的一半的時候,擴容。

第一個for迴圈是,先查詢key是否存在,如果不存在,則存入value陣列

    private void put(Key key, Value value) {
        if (count >= size / 2) resize(size * 2);//擴容
        int i;
        for (i = hash(key); null != keys[i]; i = (i + 1) % size) {
            if (keys[i].equals(key)) {
                values[i] = value;
                return;
            }
        }
        keys[i] = key;
        values[i] = value;
        ++count;
    }複製程式碼

get方法,根據key查詢value

    private Value get(Key key) {
        for (int i = hash(key); null != keys[i]; i = (i + 1) % size) {
            if (keys[i].equals(key)) {
                return values[i];
            }
        }
        return null;
    }複製程式碼

delete方法

delete方法是線性探測法裡比較難的,第一個while循序用來查詢key的位置,然後需要將簇中被刪除的key的右側的所有key重新插入到雜湊表中,這個過程比想象的要複雜的多。

    private void delete(Key key) {
        if (!contains(key)) return;
        int i = hash(key);
        while (!key.equals(keys[i])) {
            i = (i + 1) % size;
        }
        System.out.println("del-value:" + values[i]);
        keys[i] = null;
        values[i] = null;
        i = (i + 1) % size;
        while (null != keys[i]) {
            Key keyToRedo = keys[i];
            Value valueToRedo = values[i];
            keys[i] = null;
            values[i] = null;
            --count;
            put(keyToRedo, valueToRedo);
            i = (i + 1) % size;
        }
        --count;
        if (count > 0 && count == size / 8) resize(size / 2);//縮容
    }複製程式碼

keys方法

    private Iterable<Key> keys() {
        LinkedList<Key> linkedList = new LinkedList<>();
        for (int i = 0; i < size; i++)
            if (keys[i] != null) linkedList.add(keys[i]);
        return linkedList;
    }複製程式碼

測試結果

雜湊表的兩種實現

雜湊表的兩種實現

在實現連結串列法之前先簡單的實現了一個順序查詢的無序連結串列

    private class Node {
        private Key key;
        private Value value;
        private Node next;

        Node(Key key, Value value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }複製程式碼

keys方法

    Iterable<Key> keys() {
        LinkedList<Key> linkedList = new LinkedList<>();
        Node node = head;
        while (null != node) {
            linkedList.add(node.key);
            node = node.next;
        }
        return linkedList;
    }複製程式碼

get方法

    public Value get(Key key) {
        if (null == key) return null;
        Node node = head;
        while (null != node) {
            if (key.equals(node.key)) {
                return node.value;
            }
            node = node.next;
        }
        return null;
    }複製程式碼

put方法

    public void put(Key key, Value value) {
        if (null == key) return;
        Node node = head;
        while (null != node) {
            if (key.equals(node.key)) {
                node.value = value;
                return;
            }
            node = node.next;
        }
        head = new Node(key, value, head);
    }複製程式碼

測試結果

雜湊表的兩種實現

連結串列法(以拉鍊法為例)

初始化有參構造方法

	private SeparateChainHT(int capacity) {
        //建立M條連結串列
        this.size = capacity;
        sequentialSearchSTS = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[capacity];
        for (int i = 0; i < size; i++) {
            sequentialSearchSTS[i] = new SequentialSearchST<>();
        }
    }複製程式碼

get,put方法

    private void put(Key key, Value value) {
        sequentialSearchSTS[hash(key)].put(key, value);
    }
	private void put(Key key, Value value) {
        sequentialSearchSTS[hash(key)].put(key, value);
    }複製程式碼

測試結果

雜湊表的兩種實現


end

雜湊表的兩種實現

您的點贊和關注是對我最大的支援,謝謝!


相關文章