哈希表

哈希表

我們通過將我們要查找的某種數據類型轉化爲一個索引index,然後通過索引去數組中查找,這時它的複雜度就是O(1)級別的。而將某個數據類型轉化爲索引的函數我們就稱爲是哈希函數,比如說將26個小寫字母轉化爲索引,我們可以這麼寫

index = ch - 'a';

這樣就建立起了一一對應的關係,但是並不是所有的對應關係都是一一對應的,因爲數組的容量是有限的,而輸入的範圍可能是無窮的,所以很有可能不同的鍵對應着同一個索引,比如說鍵是字符串,因爲字符串的組合方式是非常的多,可以看做是無窮的,我們不可能去開闢一個無窮的空間去與這些字符串一一對應,所以不同的字符串生成的索引很有可能會有衝突,我們稱這種情況爲哈希衝突。我們定義我們在上面提到的索引叫做哈希值,由於上面講到的哈希衝突,所以我們要設計好哈希函數(hashCode())使得發生哈希衝突的可能性小,即使哈希函數產生的哈希值均勻的分佈在數組中。

哈希函數的設計

哈希函數應該滿足上面提到的:哈希函數產生的哈希值均勻的分佈在數組中。數據的類型五花八門,對於特殊的領域有特殊領域的哈希函數的設計方式,甚至還有專門的論文,說這麼多就是想說哈希函數的設計十分的複雜,在這裏我們只提最簡單的一種,哈希函數的設計應該滿足

  • 一致性
    • 如果a == b,那麼hashCode(a) == hashCode(b)
  • 高效性
    • 計算迅速
  • 均勻性
    • 輸出儘可能均勻

由於Java中基本數據類型和字符串類型有默認的hashCode()計算,所以我們就用Java自帶的hashCode計算基本數據類型和字符串的哈希值,而對於引用類型Java是根據地址計算的哈希值,所以可能會出現問題,需要我們自己自定義規則,比如對於一個Student類,我們規定學號以及姓名相同(不區分大小寫)就是同一個學生,所以根據一致性原則,它們應該產生相同的哈希值,但是由於Java默認是根據地址產生哈希值,由於二者的地址是不同的,所以產生的哈希值有極大的概率是不同的,所以我們需要自己創建哈希函數。

鏈地址法

現在我們來演示往哈希表中添加元素的步驟

import java.util.TreeMap;

public class HashTable<K, V> {
    //數組中存儲的是TreeMap這種查找表
    private TreeMap<K, V>[] hashTable;
    private int M;
    private int size;

    public HashTable(int M) {
        this.M = M;
        size = 0;
        hashTable = new TreeMap[M];

        for (int i = 0; i < hashTable.length; i++) {
            hashTable[i] = new TreeMap<>();
        }
    }

    public HashTable() {
        this(97);
    }

    public int getSize() {
        return size;
    }

    //得到在數組中的索引
    private int hash(K key) {
        //與0x7fffffff是爲了消除負數
        return (key.hashCode() & 0x7fffffff) % M;
    }

    public void add(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        //先查看已經是否有這個鍵了
        if (map.containsKey(key)) {
            //有則更新
            map.put(key, value);
        } else {
            //沒有則進行添加,並維護size
            map.put(key, value);
            size++;
        }
    }

    public V remove(K key, V value) {
        V ret = null;
        TreeMap<K, V> map = hashTable[hash(key)];
        //如果包含鍵則刪除,沒有返回null
        if (map.containsKey(key)) {
            ret = map.remove(key);
            size--;
        }

        return ret;
    }
    
    public void set(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        //沒有該鍵拋出異常
        if (!map.containsKey(key)) {
            throw new IllegalArgumentException("鍵不存在");
        }
        
        map.put(key,value);
    }
    
    //直接得到相應的TreeMap,然後去查,TreeMap有檢查步驟
    public V get(K key) {
        return hashTable[hash(key)].get(key);
    }
}

import java.util.TreeMap;

public class HashTable<K, V> {
    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private static final int initCapacity = 7;

    //數組中存儲的是TreeMap這種查找表
    private TreeMap<K, V>[] hashTable;
    private int M;
    private int size;

    public HashTable(int M) {
        //只顯示改變的內容
        //...
        hashTable = new TreeMap[initCapacity];
    }

    public void add(K key, V value) {
        //...

        if (size >= upperTol * M) {
            resize(2 * M);
        }
    }

    public V remove(K key, V value) {
        //...

        if (size < M * lowerTol && M / 2 >= initCapacity) {
            resize(M / 2);
        }

        return ret;
    }

    private void resize(int newM) {
        TreeMap<K,V>[] newHashTable = new TreeMap[newM];

        //後面要更新M,但是還需要舊M遍歷數組
        int oldM = M;
        //由於後面要重新計算下標,所以這裏要更新M
        M = newM;

        for (int i = 0; i < oldM; i++) {
            TreeMap<K, V> map = hashTable[i];
            for (K key: map.keySet()) {
                //重新計算下標並賦值
                newHashTable[hash(key)].put(key, map.get(key));
            }
        }

        hashTable = newHashTable;
    }
}

但是我們發現每次我們都擴容爲2 * M,這時M就不是一個素數了,爲了解決這一個問題,我們準備一個素數表,讓M取素數表中的值,每次擴容M在素數表中的索引+1,縮容-1

import java.util.TreeMap;

public class HashTable<K, V> {
    //素數表
    private static final int[] capacity = {};
    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private  int capacityIndex = 0;

    //數組中存儲的是TreeMap這種查找表
    private TreeMap<K, V>[] hashTable;
    private int M;
    private int size;

    public HashTable() {
        this.M = capacity[capacityIndex];
        size = 0;
        hashTable = new TreeMap[M];

        for (int i = 0; i < hashTable.length; i++) {
            hashTable[i] = new TreeMap<>();
        }
    }

    public void add(K key, V value) {
        //...
        if (size >= upperTol * M && capacityIndex + 1 < size) {
            capacityIndex++;
            resize(capacity[capacityIndex]);
        }
    }

    public V remove(K key, V value) {
        //...

        if (size < M * lowerTol && capacityIndex - 1 >= 0) {
            capacityIndex--;
            resize(capacity[capacityIndex]);
        }

        return ret;
    }
}

完整代碼

import java.util.TreeMap;

public class HashTable<K, V> {
    private static final int[] capacity = {};
    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private  int capacityIndex = 0;

    //數組中存儲的是TreeMap這種查找表
    private TreeMap<K, V>[] hashTable;
    private int M;
    private int size;

    public HashTable() {
        this.M = capacity[capacityIndex];
        size = 0;
        hashTable = new TreeMap[M];

        for (int i = 0; i < hashTable.length; i++) {
            hashTable[i] = new TreeMap<>();
        }
    }

    public int getSize() {
        return size;
    }

    //得到在數組中的索引
    private int hash(K key) {
        //與0x7fffffff是爲了消除負數
        return (key.hashCode() & 0x7fffffff) % M;
    }

    public void add(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        //先查看已經是否有這個鍵了
        if (map.containsKey(key)) {
            //有則更新
            map.put(key, value);
        } else {
            //沒有則進行添加,並維護size
            map.put(key, value);
            size++;
        }

        if (size >= upperTol * M && capacityIndex + 1 < capacity.length) {
            capacityIndex++;
            resize(capacity[capacityIndex]);
        }
    }

    public V remove(K key, V value) {
        V ret = null;
        TreeMap<K, V> map = hashTable[hash(key)];
        //如果包含鍵則刪除,沒有返回null
        if (map.containsKey(key)) {
            ret = map.remove(key);
            size--;
        }

        if (size < M * lowerTol && capacityIndex - 1 >= 0) {
            capacityIndex--;
            resize(capacity[capacityIndex]);
        }

        return ret;
    }

    public void set(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        //沒有該鍵拋出異常
        if (!map.containsKey(key)) {
            throw new IllegalArgumentException("鍵不存在");
        }

        map.put(key,value);
    }

    //直接得到相應的TreeMap,然後去查,TreeMap有檢查步驟
    public V get(K key) {
        return hashTable[hash(key)].get(key);
    }

    private void resize(int newM) {
        TreeMap<K,V>[] newHashTable = new TreeMap[newM];

        //後面要更新M,但是還需要舊M遍歷數組
        int oldM = M;
        //由於後面要重新計算下標,所以這裏要更新M
        M = newM;

        for (int i = 0; i < oldM; i++) {
            TreeMap<K, V> map = hashTable[i];
            for (K key: map.keySet()) {
                //重新計算下標並賦值
                newHashTable[hash(key)].put(key, map.get(key));
            }
        }

        hashTable = newHashTable;
    }
}

發佈了113 篇原創文章 · 獲贊 44 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章