哈希算法原理【Java實現】(十)

前言

在入學時,學校爲咱們每位童鞋創建一個檔案信息,固然每一個檔案信息都對應檔案編號,還有好比在學校圖書館,圖書館爲每本書都編了惟一的一個書籍號,那麼問題來了,當咱們須要經過檔案號快速查到對應檔案信息或者經過書記號快速查到對應書籍,這個時候咱們能夠經過哪一種數據結構呢?前面幾節咱們詳細講解了ArrayList和LinkedList,咱們知道ArrayList底層就是一維數組,可是咱們事先不知道在數組中的索引,此時查詢到對應檔案編號或書籍號須要循環遍歷,這個時候時間複雜度確定不是O(1),即便咱們知道索引可是若索引鍵很大此時再也不適合做爲數組的索引,若經過LinkedList雙向鏈表查詢,經過咱們的分析確定也不是O(1),這個時候就須要用到哈希算法則獲取的時間複雜度爲恆定時間O(1)。咱們習慣稱之爲哈希,實際叫做散列,散列是一種用於從一組類似對象中惟一標識特定對象的技術。算法

散列 

在散列中,經過使用散列函數將大鍵轉換爲小鍵,而後將這些值存儲在稱爲哈希表的數據結構中。散列的基本思路是在數組中統一分配條目(鍵/值對),爲每一個元素分配一個鍵(轉換鍵),經過鍵查找,咱們能夠在O(1)時間內訪問到對應元素。使用散列函數計算到一個索引,該索引建議能夠找到或插入元素的位置。散列分以下兩步執行:經過使用散列函數將元素轉換爲整數。此元素可用做存儲原始元素的索引,該元素屬於哈希表。該元素存儲在哈希表中,可使用散列鍵快速檢索它。數組

hash = hashfunc(key)
index = hash%array_size數據結構

上述最重要的是經過散列函數獲取鍵的散列值,而後將獲得的散列值對數組大小去模即存放到哈希表中的索引地址。到在此方法中,散列與數組大小無關,而後經過使用模運算符(%)將其縮減爲索引(介於0和array_size之間的數字 - 1)。要實現良好的散列機制,具備如下基本要求的良好散列函數很是重要:函數

易於計算:它應該易於計算,而且不能成爲算法自己(不是爲了算法而算法)。性能

統一分佈:它應該在哈希表中提供統一分佈,不該致使彙集。測試

較少的衝突:應儘可能避免不一樣元素映射到相同的哈希值時發生的衝突。this

注意:不管散列函數有多好,發生衝突是必然的,所以,爲了保持哈希表的性能,經過各類衝突解決技術來管理衝突是很重要的。使用散列函數存儲對象的步驟:建立一個大小爲M的數組。選擇一個哈希函數h,即從對象到整數0,1,...,M-1的映射。 將這些對象放入經過散列函數index = h(object)計算的索引的數組中,這種數組稱爲哈希表。那麼咱們如何選擇哈希函數? 建立哈希函數的一種方法是使用Java的hashCode()方法。 hashCode()方法在Object類中實現,所以Java中的每一個類都繼承它。 哈希碼提供了對象的數字表示,咱們來看看以下代碼示例:spa

        String obj1 = String.valueOf(4);
        String obj2 = String.valueOf(16);
        String obj3 = String.valueOf(68);
        String obj4 = String.valueOf(125);
        String obj5 = String.valueOf(255);

        System.out.println(obj1.hashCode() % 5);
        System.out.println(obj2.hashCode() % 5);
        System.out.println(obj3.hashCode() % 5);
        System.out.println(obj4.hashCode() % 5);
        System.out.println(obj5.hashCode() % 5);

如上哈希數組大小爲5,咱們建立的哈希函數是使用的Java中提供給咱們的hashcode方法,上圖中打印出的數字即爲在哈希表中的索引存放地址。此時咱們發現obj4和obj1在哈希表中的存放地址同樣,這個也就是咱們所說的衝突。在散列中解決衝突有四種方式:(1)開放尋址法或者叫線性探測或者叫閉合散列、(2)再哈希法、(3)鏈地址法、(4) 創建公共溢出區。在這裏呢我給你們演示常見的兩種,線性探測或稱爲開放尋址法和鏈地址法,首先咱們來看看開放尋址法。3d

散列衝突之開放尋址法

在開放式尋址中,全部條目記錄都存儲在數組自己中,而非連接列表中。當咱們插入新的元素或條目時,首先計算散列值的哈希索引,而後檢查數組(從散列索引開始)。若是散列索引地址未被佔用,則將條目記錄插入散列索引處地址中,不然它將以某個探測序列繼續進行,直到找到未佔用的地址。探測序列是遍歷條目時遵循的序列。在不一樣的探測序列中,連續的入口槽或探針之間能夠有不一樣的間隔。搜索條目時,將以相同的順序掃描陣列,直到找到目標元素或找到未使用的地址,這也就代表哈希表中沒有這樣的鍵,名爲「開放尋址」指的是元素地址不是由其散列值所肯定。線性探測是指連續探測之間的間隔固定(一般爲1)。假設特定條目的散列索引是索引。線性探測的探測序列將是:code

index = index % hashTableSize
index = (index + 1) % hashTableSize
index = (index + 2) % hashTableSize
index = (index + 3) % hashTableSize

.......

如上意思代表當指定鍵的哈希值已被佔用,則將哈希值以間隔爲1進行遞增,如此一次遞增直到找到未被佔用的索引存放地址,代碼以下:

public class HashTable {

    //數組容量
    private int capacity;

    //哈希鍵值對數組
    private Entry[] entries = {};

    public HashTable(int capacity) {
        this.capacity = capacity;
        entries = new Entry[this.capacity];
    }

    //添加鍵值對
    public void put(String key, String value) {
        final Entry hashEntry = new Entry(key, value);
        int hash = getHash(key);
        entries[hash] = hashEntry;
    }

    //獲取鍵哈希值
    private int getHash(String key) {
        int hashCode = key.hashCode();
        int hash = hashCode % capacity;
        while (entries[hash] != null) {
            hashCode += 1;
            hash = hashCode % capacity;
        }
        return hash;
    }

    //獲取指定鍵值
    public String get(String key) {
        int hashCode = key.hashCode();
        int hash = hashCode % capacity;
        if (entries[hash] != null) {
            while (!entries[hash].key.equals(key))
            {
                hashCode += 1;
                hash = hashCode % capacity;
            }
            return entries[hash].value;
        }
        return null;
    }

    private class Entry {
        String key;
        String value;

        public Entry(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }
}

咱們在控制檯將如上測試數據添加到咱們自定義的哈希表類中,而後去查詢對應鍵的值,以下:

public class Main {

    public static void main(String[] args) {

        HashTable table = new HashTable(5);

        table.put(String.valueOf(4), String.valueOf(4));
        table.put(String.valueOf(16), String.valueOf(16));
        table.put(String.valueOf(68), String.valueOf(68));
        table.put(String.valueOf(125), String.valueOf(125));
        table.put(String.valueOf(255), String.valueOf(255));

        System.out.println(table.get(String.valueOf(4)));
        System.out.println(table.get(String.valueOf(125)));
    }
}

散列衝突之鏈地址法 

咱們經過使用單鏈表來實現鏈地址法,鏈地址法是最經常使用的衝突解決技術之一,在單鏈表中,哈希表的每一個元素都是鏈表,要想在哈希表中存儲元素,必須將其插入特定的鏈表。若是存在任何衝突(即兩個不一樣的元素具備相同的散列值),則將這兩個元素存儲在同一鏈表中,查找的成本是掃描所選鏈表的條目以得到所需的鍵,若是鍵的分佈足夠均勻,則查找的平均成本僅取決於每一個鏈表的平均鍵數。對於鏈地址法,最壞的狀況是全部條目都插入到同一個鏈表中。查找過程可能必須掃描其全部條目,所以最壞狀況放入成本與表中條目的數量(N)成比例,鏈地址法存在哈希表中以下示意圖:

在開頭咱們所給的例子,鍵16和125的哈希值相同則咱們會將其存放到同一鏈表中,接下來咱們經過示例代碼來實現鏈地址法,以下:

public class HashTable {

    //數組容量
    private int capacity;

    //哈希鍵值對數組
    private Entry[] entries = {};

    public HashTable(int capacity) {
        this.capacity = capacity;
        entries = new Entry[this.capacity];
    }

    //添加鍵值對
    public void put(String key, String value) {
        //獲取鍵哈希值
        int hash = getHash(key);

        //實例化類存放鍵和值
        final Entry hashEntry = new Entry(key, value);

        //若是在數組中未有衝突的鍵則直接存放
        if(entries[hash] == null) {
            entries[hash] = hashEntry;
        }

        //若是找到衝突的哈希值則存放到單鏈表中的下一引用
        else {
            Entry temp = entries[hash];
            while(temp.next != null) {
                temp = temp.next;
            }
            temp.next = hashEntry;
        }
    }


    //獲取鍵哈希值
    private int getHash(String key) {
        return key.hashCode() % capacity;
    }

    //獲取指定鍵值
    public String get(String key) {

        int hash = getHash(key);

        if(entries[hash] != null) {

            Entry temp = entries[hash];

            while( !temp.key.equals(key)
                    && temp.next != null ) {
                temp = temp.next;
            }
            return temp.value;
        }

        return null;
    }

    private class Entry {
        String key;
        String value;
        Entry next;

        public Entry(String key, String value) {
            this.key = key;
            this.value = value;
            this.next = null;
        }
    }
}

控制檯代碼和演示開放尋址法同樣且打印出的結果也一致,這裏就再也不給出。到這裏咱們實現了散列算法以及散列算法中解決衝突最經常使用的技術:開放尋址法和鏈地址法。

總結

本節咱們仍是一如既往先了解對應概念的算法實現爲咱們下一節詳細分析Hashtable作鋪墊,好了,本節內容咱們到此爲止,感謝您的閱讀,咱們下節見。 

相關文章
相關標籤/搜索