9.哈希表

目錄

1.哈希表的基本介紹
2.哈希表的設計思想
3.哈希函數的設計
4.哈希表大小的確定
5.衝突的解決
6.哈希表的實現
7.哈希表總結

1.哈希表的基本介紹

散列表(Hash table,也叫哈希表),是根據關鍵字(Key value)而直接訪問在內存存儲位置的數據結構。也就是說,它通過一個計算鍵值的函數,將所需查詢的數據映射到表中一個位置來方便訪問,這加快了查找速度。這個映射函數稱做散列函數,又叫哈希函數,存放數據的數組稱做散列表(哈希表)。因此它同數組、鏈表以及二叉排序樹等相比較有很明顯的區別,它能夠快速定位到想要查找的記錄,而不是與表中存在的關鍵字進行依次比較來進行查找。

2.哈希表的設計思想

舉例說明哈希表同其他數據結構的區別:

對於一般的線性表,比如鏈表,如果要存儲學生信息:姓名、學號。在JAVA中一般就是將姓名和年齡這些信息作爲"學生類"的成員變量,然後把這些學生對象存放到鏈表中,當要查找如"張三 1001"這條記錄時,需要從鏈表頭節點開始遍歷,並依次將每個結點中的姓名同"張三 "比較,直到查找成功或失敗,這種做法的時間複雜度爲O(n)。即使採用二叉排序樹進行存儲,也最多爲O(logn)。假設能夠通過"張三"這個信息直接獲取到該記錄在表中的存儲位置,就能省掉中間關鍵字比較的環節,複雜度直接降到O(1)。這便是哈希表能夠實現的。

詳解哈希表的設計思想:

Hash表通過上述提到的映射函數(哈希函數),記爲hashFunc(key),直接將關鍵字key(待插入或查找的數據)映射到表中的一個存儲位置上,從而在想要查找該數據時,可以直接根據關鍵字和映射關係計算出該數據在表中的存儲位置。通過Hash函數f和關鍵字計算出來的存儲位置(注意這裏的存儲位置只是表中的存儲位置,並不是實際的物理地址)稱作爲Hash地址。比如上述例子中,假如學生信息採用Hash表存儲,則當想要找到"張三"的信息時,直接將"李四"作爲Hash函數f的參數:hashFunc(“張三”),計算出Hash地址即可。

3.哈希函數的設計

上述提到的哈希函數hashFunc(key)並不是一個固定的規則,而是我們根據實際需求自己設計的。Hash函數設計的好壞直接影響到對Hash表的操作效率。那如何設計哈希函數或者哈希函數的設計有哪些需要注意的呢?

假如對上述的學生信息進行存儲時,採用的Hash函數爲 姓名的每個字的拼音開頭大寫字母的ASCII碼之和
hashFunc(張三)=ASCII(Z)+ASCII(S)=90+83=173;
hashFunc(李四)=ASCII(L)+ASCII(S)=76+83=159;
hashFunc(王五)=ASCII(W)+ASCII(W)=87+87=174;
hashFunc(張帥)=ASCII(Z)+ASCII(S)=90+83=173;
 
通過哈希函數映射後,張三應該存儲在數組的173下標處,李四存儲在159下標,王五和張帥同理。
 
假如只有這4個學生信息需要進行存儲,那這個Hash函數設計的很糟糕。因爲它浪費了大量的存儲空間。下標到了174,那該哈希表數組至少需要開闢174個學生對象的存儲空間,然而空間利用率只有4/174,不到3%。
 
另外,根據Hash函數計算結果之後,hashFunc(張三)和hashFunc(張帥)具有相同的地址173,意味着會有一個學生的信息被覆蓋,這種現象稱作衝突,對於174個存儲空間中只需要存儲4條記錄就發生了衝突,所以這樣的Hash函數設計是很不合理的。
 
所以在構造Hash函數時應儘量考慮關鍵字的分佈特點來設計函數使得Hash地址隨機均勻地分佈在整個地址空間當中。

通常有以下幾種構造Hash函數的方法:

1 直接定址法:
取關鍵字或者關鍵字的某個線性函數爲Hash地址,即hashFunc(key)=a*key+b;例如知道學生的學號從1000開始,最大爲4000,則可以將hashFunc(key)=key-1000作爲Hash地址。
 
2 平方取中法:
對關鍵字進行平方運算,然後取結果的中間幾位作爲Hash地址。假如有以下關鍵字序列{421,423,436},平方之後的結果爲{177241,178929,190096},那麼可以取中間的兩位數{72,89,00}作爲Hash地址。
 
3 摺疊法:
將關鍵字拆分成幾部分,然後將這幾部分組合在一起,以特定的方式進行轉化形成Hash地址。假如知道圖書的ISBN號爲8903-241-23,可以將hashFunc(key)=89+03+24+12+3作爲Hash地址。
 
4 除留取餘法:
如果知道Hash表的最大長度爲m,可以取不大於m的最大質數 p,然後對關鍵字進行取餘運算,hashFunc(key)=key%p。(在這裏p的選取非常關鍵,p選擇的好的話,能夠最大程度地減少衝突,p一般取不大於m的最大質數。)

4.哈希表大小的確定

Hash表大小的確定也非常關鍵,如果Hash表的空間遠遠大於最後實際存儲的數據個數,則造成了很大的空間浪費,如果選取小了的話,則容易造成衝突。在實際情況中,一般需要根據最終數據存儲個數和關鍵字的分佈特點來確定Hash表的大小。還有一種情況時可能事先不知道最終需要存儲的記錄個數,則需要動態維護Hash表的容量(擴容),此時可能需要重新計算Hash地址。

5.衝突的解決

上述哈希函數設計的例子中,發生了衝突現象,因此需要解決該問題,否則數據無法進行正確的存儲。通常情況下有2種解決辦法:

1 開放定址法:
  即當一個關鍵字和另一個關鍵字發生衝突時,使用某種探測技術在Hash表中形成一個探測序列,然後沿着這個探測序列依次查找下去,當碰到一個空的單元時,則插入其中。
  比較常用的探測方法有線性探測法,比如有一組關鍵字{12,13,25,23,38,34,6,84,91},Hash表長爲12,Hash函數爲hashFunc(key)=key%11,當插入12,13,25時可以直接插入到下標爲1,2,3的位置上,而當插入23時,地址1被佔用了,發生衝突,因此沿着地址1依次往下探測(探測步長可以根據情況而定):(1+1)%11=2,依舊衝突,繼續嗅探(2+1)%11=3,仍然衝突繼續嗅探(3+1)%11=4,此時探測到地址4,發現爲空,則將23插入其中,這裏的每次嗅探加的1可以看做我們定的步長。
 
2 鏈地址法:
  採用數組和鏈表相結合的辦法,將Hash地址相同的數據存儲在同一張線性表中。這樣在哈希表相當於一個鏈表數組,通過哈希地址得到的是鏈表表頭,所有哈希地址相同的數據,不斷的插入到該地址處的鏈表中。如上述例子中,採用鏈地址法形成的Hash表存儲表示爲:
在這裏插入圖片描述
雖然我們能夠採用一些辦法去減少衝突,但是衝突是無法完全避免的。因此需要根據實際情況選取解決衝突的辦法。

6.哈希表的實現

通過上述的除留取餘法構造哈希函數,和鏈地址法解決衝突,實現一個簡易哈希表,類似HashMap:

package hash;

import java.util.ArrayList;

/**
 * 哈希表
 * @param <E>不確定鏈表節點數據類型,用泛型佔位,
 *           爲了方便查找時比較,數據對象需繼承Comparable
 */
public class HashTable<E> {
    private HTLinkedList[] linkedListArray;
    private int size;

    public HashTable(int size){
        this.size = size;
        //初始化linkedListArray空間
        linkedListArray = new HTLinkedList[size];
        //初始化linkedListArray鏈表節點
        for (int i = 0; i < size; i++) {
            linkedListArray[i] = new HTLinkedList();
        }
    }

    /**
     * 數據以鍵值對的形式存儲
     * 哈希函數:哈希地址=鍵字符串的首尾字母的ASCII碼值的和 % size
     * @param key
     * @return
     */
    public int hashFunc(String key){
        char firstChar = key.charAt(0);
        char lastChar = key.charAt(key.length()-1);
        return (firstChar+lastChar) % size;
    }

    /**
     * 以key-value形式添加數據
     * @param key
     * @param value
     */
    public void add(String key,E value){
        int index = hashFunc(key);
        linkedListArray[index].add(new Node(key, value));
    }

    public E get(String key){
        int index = hashFunc(key);
        Node node = linkedListArray[index].find(key);
        return (E)node.value;
    }

    public void show(){
        for (int i = 0; i < size; i++) {
            System.out.print("哈希表中下標爲"+i+"處的鏈表中數據爲:");
            linkedListArray[i].show();
            System.out.println("");
        }
    }



    public static void main(String[] args) {
        HashTable<Integer> hashTable = new HashTable<>(10);
        hashTable.add("key1",1);
        hashTable.add("key2",2);
        hashTable.add("key3",3);
        hashTable.add("key4",4);
        hashTable.add("key5",5);
        hashTable.add("key6",6);
        hashTable.add("key7",7);
        hashTable.add("key8",8);
        hashTable.add("key9",9);
        hashTable.add("key10",10);
        hashTable.add("aaa",11);
        hashTable.add("bbb",12);
        hashTable.add("ccc",13);
        hashTable.add("ddd",14);
        hashTable.add("eee",15);
        hashTable.add("fff",16);
        hashTable.add("ggg",17);
        hashTable.show();
    }

}



/**
 * 節點類
 */
class Node{
    public String key;
    public Object value;
    public Node next;
    public Node(String key, Object value){
        this.key = key;
        this.value = value;
    }
}

/**
 * 鏈表類
 */
class HTLinkedList{

    //頭節點
    private Node head;

    /**
     * 鏈表添加節點
     * @param node
     */
    public void add(Node node){
        if (head == null){
            head = node;
            return;
        }
        Node curNode = head;
        while (curNode.next!=null){
            curNode = curNode.next;
        }
        curNode.next = node;
    }

    /**
     * 查找鏈表中節點
     * @param key
     * @return
     */
    public Node find(String key){
        Node curNode = head;
        while (curNode!=null){
            if (curNode.key.equals(key)){
                return curNode;
            }
            curNode = curNode.next;
        }
        return null;
    }

    public void show(){
        if (head==null){
            System.out.print("空");
            return;
        }
        Node curNode = head;
        while (curNode!=null){
            System.out.print("->"+curNode.value);
            curNode = curNode.next;
        }
    }

    /**
     * 刪除、更新操作略
     */

}

7.哈希表總結

優點:

1.不論哈希表中有多少數據,查找、插入、刪除(有時包括刪除)只需要接近常量的時間即O(1) 的時間級。實際上,這隻需要幾條機器指令。
2.哈希表運算得非常快,在計算機程序中,如果需要在一秒種內查找上千條記錄通常使用哈希表(例如拼寫檢查器)哈希表的速度明顯比樹快,樹的操作通常需要O(N)的時間級。哈希表不僅速度快,編程實現也相對容易。
3.如果不需要有序遍歷數據,並且可以提前預測數據量的大小。那麼哈希表在速度和易用性方面是無與倫比的。

缺點:

它是基於數組的,數組創建後難於擴展,某些哈希表被基本填滿時,性能下降得非常嚴重,所以程序員必須要清楚表中將要存儲多少數據,或者準備好定期地把數據轉移到更大的哈希表中,這是個費時的過程。

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