哈希表——hashset / hashmap

哈希表

哈希表是一種使用哈希函數組織數據,以支持快速插入和搜索的數據結構。

有兩種不同類型的哈希表:哈希集合和哈希映射。

  • 哈希集合集合數據結構的實現之一,用於存儲非重複值
  • 哈希映射映射 數據結構的實現之一,用於存儲(key, value)鍵值對。

標準模板庫的幫助下,哈希表是易於使用的。大多數常見語言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。

通過選擇合適的哈希函數,哈希表可以在插入和搜索方面實現出色的性能

哈希表的原理

正如我們在介紹中提到的,哈希表是一種數據結構,它使用哈希函數組織數據,以支持快速插入和搜索

哈希表的關鍵思想是使用哈希函數將鍵映射到存儲桶

  1. 當我們插入一個新的鍵時,哈希函數將決定該鍵應該分配到哪個桶中,並將該鍵存儲在相應的桶中;
  2. 當我們想要搜索一個鍵時,哈希表將使用相同的哈希函數來查找對應的桶,並只在特定的桶中進行搜索。
示例

img

在示例中,我們使用 y = x % 5 作爲哈希函數。讓我們使用這個例子來完成插入和搜索策略:

  1. 插入:我們通過哈希函數解析鍵,將它們映射到相應的桶中。
    • 例如,1987 分配給桶 2,而 24 分配給桶 4。
  2. 搜索:我們通過相同的哈希函數解析鍵,並僅在特定存儲桶中搜索。
    • 如果我們搜索 1987,我們將使用相同的哈希函數將1987 映射到 2。因此我們在桶 2 中搜索,我們在那個桶中成功找到了 1987。
    • 例如,如果我們搜索 23,將映射 23 到 3,並在桶 3 中搜索。我們發現 23 不在桶 3 中,這意味着 23 不在哈希表中。

設計哈希表的關鍵

在設計哈希表時,你應該注意兩個基本因素。

1. 哈希函數

哈希函數是哈希表中最重要的組件,該哈希表用於將鍵映射到特定的桶。在示例中,我們使用 y = x % 5 作爲散列函數,其中 x 是鍵值,y 是分配的桶的索引。

散列函數將取決於鍵值的範圍桶的數量。

下面是一些哈希函數的示例:

img

哈希函數的設計是一個開放的問題。其思想是儘可能將鍵分配到桶中,理想情況下,完美的哈希函數將是鍵和桶之間的一對一映射。然而,在大多數情況下,哈希函數並不完美,它需要在桶的數量和桶的容量之間進行權衡。

2. 衝突解決

理想情況下,如果我們的哈希函數是完美的一對一映射,我們將不需要處理衝突。不幸的是,在大多數情況下,衝突幾乎是不可避免的。

例如,在我們之前的哈希函數(y = x % 5)中,1987 和 2 都分配給了桶 2,這是一個衝突

衝突解決算法應該解決以下幾個問題:

  1. 如何組織在同一個桶中的值?
  2. 如果爲同一個桶分配了太多的值,該怎麼辦?
  3. 如何在特定的桶中搜索目標值?

根據我們的哈希函數,這些問題與桶的容量和可能映射到同一個桶鍵的數目有關。

讓我們假設存儲最大鍵數的桶有 N 個鍵。

通常,如果 N 是常數且很小,我們可以簡單地使用一個數組將鍵存儲在同一個桶中。如果 N 是可變的或很大,我們可能需要使用高度平衡的二叉樹來代替。

訓練

到目前爲止,您應該能夠實現基本的哈希表。我們爲您提供了實現哈希集和哈希映射的練習。閱讀需求確定哈希函數並在需要時解決衝突

如果你不熟悉哈希集或是哈希映射的概念,可以返回介紹部分找出答案。.

插入搜索是哈希表中的兩個基本操作。

此外,還有基於這兩個操作的操作。例如,當我們刪除元素時,我們將首先搜索元素,然後在元素存在的情況下從相應位置移除元素。

哈希集 - 用法

哈希集是集合的實現之一,它是一種存儲不重複值的數據結構。

#include <unordered_set>                // 0. include the library

int main() {
    // 1. initialize a hash set
    unordered_set<int> hashset;   
    // 2. insert a new key
    hashset.insert(3);
    hashset.insert(2);
    hashset.insert(1);
    // 3. delete a key
    hashset.erase(2);
    // 4. check if the key is in the hash set
    if (hashset.count(2) <= 0) {
        cout << "Key 2 is not in the hash set." << endl;
    }
    // 5. get the size of the hash set
    cout << "The size of hash set is: " << hashset.size() << endl; 
    // 6. iterate the hash set
    for (auto it = hashset.begin(); it != hashset.end(); ++it) {
        cout << (*it) << " ";
    }
    cout << "are in the hash set." << endl;
    // 7. clear the hash set
    hashset.clear();
    // 8. check if the hash set is empty
    if (hashset.empty()) {
        cout << "hash set is empty now!" << endl;
    }
}

使用哈希集查重

插入新值並檢查值是否在哈希集中是簡單有效的。因此,通常使用哈希集來檢查該值是否已經出現過。

讓我們來看一個例子:

給定一個整數數組,查找數組是否包含任何重複項。

你可以簡單地迭代每個值並將值插入集合中。 如果值已經在哈希集中,則存在重複。

在這裏,我們爲你提供瞭解決此類問題的模板:

/*
 * Template for using hash set to find duplicates.
 */
bool findDuplicates(vector<Type>& keys) {
    // Replace Type with actual type of your key
    unordered_set<Type> hashset;
    for (Type key : keys) {
        if (hashset.count(key) > 0) {
            return true;
        }
        hashset.insert(key);
    }
    return false;
}

哈希映射 - 用法

哈希映射是用於存儲 (key, value) 鍵值對的一種實現。

#include <unordered_map>                // 0. include the library

int main() {
    // 1. initialize a hash map
    unordered_map<int, int> hashmap;
    // 2. insert a new (key, value) pair
    hashmap.insert(make_pair(0, 0));
    hashmap.insert(make_pair(2, 3));
    // 3. insert a new (key, value) pair or update the value of existed key
    hashmap[1] = 1;
    hashmap[1] = 2;
    // 4. get the value of a specific key
    cout << "The value of key 1 is: " << hashmap[1] << endl;
    // 5. delete a key
    hashmap.erase(2);
    // 6. check if a key is in the hash map
    if (hashmap.count(2) <= 0) {
        cout << "Key 2 is not in the hash map." << endl;
    }
    // 7. get the size of the hash map
    cout << "the size of hash map is: " << hashmap.size() << endl; 
    // 8. iterate the hash map
    for (auto it = hashmap.begin(); it != hashmap.end(); ++it) {
        cout << "(" << it->first << "," << it->second << ") ";
    }
    cout << "are in the hash map." << endl;
    // 9. clear the hash map
    hashmap.clear();
    // 10. check if the hash map is empty
    if (hashmap.empty()) {
        cout << "hash map is empty now!" << endl;
    }
}

場景 I - 提供更多信息

使用哈希映射的第一個場景是,我們需要更多的信息,而不僅僅是鍵。然後通過哈希映射建立密鑰與信息之間的映射關係

讓我們來看一個例子:

給定一個整數數組,返回兩個數字的索引,使它們相加得到特定目標。

在這個例子中,如果我們只想在有解決方案時返回 true,我們可以使用哈希集合來存儲迭代數組時的所有值,並檢查 target - current_value 是否在哈希集合中。

但是,我們被要求返回更多信息,這意味着我們不僅關心值,還關心索引。我們不僅需要存儲數字作爲鍵,還需要存儲索引作爲值。因此,我們應該使用哈希映射而不是哈希集合。

更重要的是

在某些情況下,我們需要更多信息,不僅要返回更多信息,還要幫助我們做出決策

在前面的示例中,當我們遇到重複的鍵時,我們將立即返回相應的信息。但有時,我們可能想先檢查鍵的值是否可以接受。

在這裏,我們爲您提供瞭解決此類問題的模板:

/*
 * Template for using hash map to find duplicates.
 * Replace ReturnType with the actual type of your return value.
 */
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    unordered_map<Type, InfoType> hashtable;
    for (Type key : keys) {
        if (hashmap.count(key) > 0) {
            if (hashmap[key] satisfies the requirement) {
                return needed_information;
            }
        }
        // Value can be any information you needed (e.g. index)
        hashmap[key] = value;
    }
    return needed_information;
}

場景 II - 按鍵聚合

另一個常見的場景是按鍵聚合所有信息。我們也可以使用哈希映射來實現這一目標。

這是一個例子:

給定一個字符串,找到它中的第一個非重複字符並返回它的索引。如果它不存在,則返回 -1。

解決此問題的一種簡單方法是首先計算每個字符的出現次數。然後通過結果找出第一個與衆不同的角色。

因此,我們可以維護一個哈希映射,其鍵是字符,而值是相應字符的計數器。每次迭代一個字符時,我們只需將相應的值加 1。

更重要的是

解決此類問題的關鍵是在遇到現有鍵時確定策略

在上面的示例中,我們的策略是計算事件的數量。有時,我們可能會將所有值加起來。有時,我們可能會用最新的值替換原始值。策略取決於問題,實踐將幫助您做出正確的決定。

在這裏,我們爲您提供瞭解決此類問題的模板:

/*
 * Template for using hash map to find duplicates.
 * Replace ReturnType with the actual type of your return value.
 */
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
    // Replace Type and InfoType with actual type of your key and value
    unordered_map<Type, InfoType> hashtable;
    for (Type key : keys) {
        if (hashmap.count(key) > 0) {
            update hashmap[key];
        }
        // Value can be any information you needed (e.g. index)
        hashmap[key] = value;
    }
    return needed_information;
}

設計鍵

在以前的問題中,鍵的選擇相對簡單。不幸的是,有時你必須考慮在使用哈希表時設計合適的鍵

我們來看一個例子:

給定一組字符串,將字母異位詞組合在一起。

衆所周知,哈希映射可以很好地按鍵分組信息。但是我們不能直接使用原始字符串作爲鍵。我們必須設計一個合適的鍵來呈現字母異位詞的類型。例如,有字符串 “eat” 和 “ate” 應該在同一組中。但是 “eat” 和 “act” 不應該組合在一起。

解決方案

實際上,設計關鍵是在原始信息和哈希映射使用的實際鍵之間建立映射關係。設計鍵時,需要保證:

  1. 屬於同一組的所有值都將映射到同一組中。
  2. 需要分成不同組的值不會映射到同一組。

此過程類似於設計哈希函數,但這是一個本質區別。哈希函數滿足第一個規則但可能不滿足第二個規則。但是你的映射函數應該滿足它們。

在上面的示例中,我們的映射策略可以是:對字符串進行排序並使用排序後的字符串作爲鍵。也就是說,“eat” 和 “ate” 都將映射到 “aet”。

設計鍵 - 總結

  1. 當字符串 / 數組中每個元素的順序不重要時,可以使用排序後的字符串 / 數組作爲鍵。img
  2. 如果只關心每個值的偏移量,通常是第一個值的偏移量,則可以使用偏移量作爲鍵。img
  3. 在樹中,你有時可能會希望直接使用 TreeNode 作爲鍵。 但在大多數情況下,採用子樹的序列化表述可能是一個更好的主意。img
  4. 在矩陣中,你可能希望使用行索引列索引作爲鍵。
  5. 在數獨中,可以將行索引和列索引組合來標識此元素屬於哪個img
    每個值的偏移量,通常是第一個值的偏移量,則可以使用偏移量作爲鍵。[外鏈圖片轉存中…(img-kVHyOmRM-1589299271190)]
  6. 在樹中,你有時可能會希望直接使用 TreeNode 作爲鍵。 但在大多數情況下,採用子樹的序列化表述可能是一個更好的主意。[外鏈圖片轉存中…(img-xKP4SGlm-1589299271191)]
  7. 在矩陣中,你可能希望使用行索引列索引作爲鍵。
  8. 在數獨中,可以將行索引和列索引組合來標識此元素屬於哪個。[外鏈圖片轉存中…(img-cqeZQjVr-1589299271193)]
  9. 有時,在矩陣中,您可能希望將值聚合在同一對角線中。img
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章