數據結構:哈希表

 你受的苦,吃的虧,擔的責,扛的罪,忍的痛,到最後都會變成光,照亮你的路。

什麼是哈希表?

哈希表(Hash table,散列),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做哈希函數,存放記錄的數組叫做哈希表。

舉個栗子:

一個班有30名學生,他們的學號是1-30的,我們用數組來存儲這些學生:

學號 數組下標
1 0
2 1
3 2
30 29

事實上,這個數組就是一個哈希表,班裏每個學生的學號都對應了數組中的一個下標。

具體的對應關係爲 : 下標 = 學號 - 1

而 f(key) = value,給定一個鍵值,計算得到一個地址,這樣的關係函數就是哈希函數。

在這個具體的例子中, 下標 = 學號 - 1 就是哈希函數,給定一個學號,就能知道這個學生在數組中的存儲位置。這樣的話,想要查詢一個學生的信息只需要O(1)的時間複雜度!

當然這是一種最爲簡單的哈希表,想要使用哈希表的方法進行查找,就必須解決下面兩個問題:

  • Hash函數的構建
  • Hash衝突的解決方式

所謂的Hash衝突是指不同的key通過Hash函數所求的地址值value相同,則這些key就產生了Hash衝突

Hash函數的構建方法

哈希表之所以能達到O(1)的複雜度,本質上是以空間換時間。如果空間足夠大,相應的我們就能在O(1)的複雜度下完成各項操作,而如果只有O(1)的空間,那麼就需要O(n) (線性表)的時間複雜度。

Hash表就是時間和空間之間的平衡,因此Hash函數的構建是非常重要的。通常應該遵循下列原則:

  • 高效性:hash函數本身運算儘量簡單,便與運算
  • 一致性:若a = b ,則 hash(a)= hash(b)
  • 均勻性:通過Hash函數得到的hash函數值必須在Hash地址範圍內,且分佈均勻,地址衝突儘量小

在上一個學號的例子中,我們直接以 下標 = 學號 - 1 的方式,很快的完成了Hash函數的構建,但事實上不是所有的問題都可以如此簡單的構建出來,下面就來討論更復雜的情況下如何構建Hash函數。

  1. 大整數:除留餘數法
    在我國,居民身份證號是由18位數字組成的,比如:110323198512166666。
    哈希表充分體現了空間換時間的思想,如果我們真的有9999999999999999999個空間,那麼我們完全可以從下標爲0開始存放,當然這是不切實際的,而且如果真的這樣做,也是對空間的極大浪費。對於較大的整數,並且這個整數的每幾位還存在某些含義時,我們處理方法是模以一個素數。

下面給出了大整數在lwr-upr之間的最佳取模的素數:或者點擊這個鏈接–>最佳取模的素數
在這裏插入圖片描述
之所以模以一個素數,是因爲模以一個素數可以減少hash衝突並且能較爲充分地利用到大整數的每部分數據。

比如:
在這裏插入圖片描述
在模以4時產生了嚴重的Hash衝突,而模以素數7在這組數據中沒有發生Hash衝突。

  1. 特殊類型構建Hash函數
    對於特殊類型的數據,我們依然是將其轉化爲整數:比如字符串
    在這裏插入圖片描述

根據實際需求,我們也可以將字符串轉化成B進制的整數。那麼hash函數就是這樣的:
在這裏插入圖片描述
上面三個hash函數是等價的,只是在第一個hash函數下簡化了計算而已。

Hash衝突解決方法

由於具體問題的複雜性,Hash衝突不可避免的存在,因此就需要對Hash衝突進行處理,通常較好的方式是:鏈地址法。

例如通過Hash函數計算得到 k1的地址爲4,k2的地址爲1,分別插入後又來了一個k3且地址也是1,此時k1和k3發生了衝突,如何處理呢?
在這裏插入圖片描述
鏈地址法就是讓發生衝突的元素以鏈表插在前一個元素後面:
在這裏插入圖片描述
事實上發生衝突時並不一定要構成一個鏈表,只要是查找表就行,也就是說我們完全可以鏈接一個AVL樹或者紅黑樹。

在Java中HashMap就是TreeMap的數組;HashSet是TreeSet的數組。

基於TreeMap實現HashMap

package cn.boom.hash;

import java.util.Arrays;
import java.util.TreeMap;

public class HashTable<K, V> {

    //取模的素數
    private static int[] prime = {53,97,193,389,769,1543,3079,6151,12289,
                                  24593,49157,98317,196613,393241,786433,
                                  1572869,3145739,6291469,12582917,25165843,
                                  50331653,100663319,201326611,402653189,805306457,1610612741};

    private static final int upperTol = 10; //平均hash衝突上界
    private static final int lowerTol = 2; //平均hash衝突下界

    private TreeMap<K,V>[] hashTable;
    private int capacity;
    private int size;
    private int capacityIndex;

    public HashTable(){

        this.size = 0;
        this.capacityIndex = 0;
        this.capacity = prime[capacityIndex];
        this.hashTable = new TreeMap[capacity];

        for (int i = 0; i < capacity; i++) {
            hashTable[i] = new TreeMap<K, V>();
        }
    }

    public int getSize() {
        return size;
    }

    public boolean contains(K key) {
        int address = hash(key);
        return hashTable[address].containsKey(key);
    }

    //hash函數
    private int hash(K key) {
        return key.hashCode() & 0x7fffffff % capacity;//取key hashCode的正值並計算hash值
    }

    private void resize(int newCapacity){

        TreeMap<K, V>[] newHashTable = new TreeMap[newCapacity];
        for(int i = 0 ; i < newCapacity ; i ++)
            newHashTable[i] = new TreeMap<K,V>();

        int oldCapacity = this.capacity;
        this.capacity = newCapacity;

        for(int i = 0 ; i < oldCapacity ; i ++)
            for(K key: hashTable[i].keySet())
                newHashTable[hash(key)].put(key, hashTable[i].get(key));

        this.hashTable = newHashTable;
    }


    public void add(K key, V value) {

        int address = hash(key);

        if (hashTable[address].containsKey(key)) {
            hashTable[address].put(key, value);
        } else {
            hashTable[address].put(key, value);
            this.size++;

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

        }
    }

    public V remove(K key) {

        int address = hash(key);

        if (!hashTable[address].containsKey(key)) {
            throw new IllegalArgumentException(key + " doesn't exist ! ");
        }

        V ret = hashTable[address].remove(key);
        size--;

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

        return ret;
    }

    @Override
    public String toString() {
        return "HashTable{" +
                "hashTable=" + Arrays.toString(hashTable) +
                ", capacity=" + capacity +
                ", size=" + size +
                ", capacityIndex=" + capacityIndex +
                '}';
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章