哈希算法原理【Java實現】


前言

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

散列 

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

hash = hashfunc(key)
index = hash%array_size

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

易於計算:它應該易於計算,並且不能成為算法本身(不是為了算法而算法)。

統一分布:它應該在哈希表中提供統一分布,不應導致聚集。

較少的沖突:應盡量避免不同元素映射到相同的哈希值時發生的沖突。

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

        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) 建立公共溢出區。在這里呢我給大家演示常見的兩種,線性探測或稱為開放尋址法和鏈地址法,首先我們來看看開放尋址法。

散列沖突之開放尋址法

在開放式尋址中,所有條目記錄都存儲在數組本身中,而非鏈接列表中。當我們插入新的元素或條目時,首先計算散列值的哈希索引,然后檢查數組(從散列索引開始)。如果散列索引地址未被占用,則將條目記錄插入散列索引處地址中,否則它將以某個探測序列繼續進行,直到找到未占用的地址。探測序列是遍歷條目時遵循的序列。在不同的探測序列中,連續的入口槽或探針之間可以有不同的間隔。搜索條目時,將以相同的順序掃描陣列,直到找到目標元素或找到未使用的地址,這也就表明哈希表中沒有這樣的鍵,名為“開放尋址”指的是元素地址不是由其散列值所確定。線性探測是指連續探測之間的間隔固定(通常為1)。假設特定條目的散列索引是索引。線性探測的探測序列將是:

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做鋪墊,好了,本節內容我們到此為止,感謝您的閱讀,我們下節見。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM