哈希表
哈希表
是一種使用哈希函數
組織數據,以支持快速插入和搜索的數據結構。
有兩種不同類型的哈希表:哈希集合和哈希映射。
哈希集合
是集合
數據結構的實現之一,用於存儲非重複值
。哈希映射
是映射
數據結構的實現之一,用於存儲(key, value)
鍵值對。
在標準模板庫
的幫助下,哈希表是易於使用的
。大多數常見語言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。
通過選擇合適的哈希函數,哈希表可以在插入和搜索方面實現出色的性能
。
哈希表的原理
正如我們在介紹中提到的,哈希表
是一種數據結構,它使用哈希函數組織數據,以支持快速插入和搜索
。
哈希表的關鍵思想是使用哈希函數將鍵映射到存儲桶
。
- 當我們插入一個新的鍵時,哈希函數將決定該鍵應該分配到哪個桶中,並將該鍵存儲在相應的桶中;
- 當我們想要搜索一個鍵時,哈希表將使用相同的哈希函數來查找對應的桶,並只在特定的桶中進行搜索。
示例
在示例中,我們使用 y = x % 5 作爲哈希函數。讓我們使用這個例子來完成插入和搜索策略:
- 插入:我們通過哈希函數解析鍵,將它們映射到相應的桶中。
- 例如,1987 分配給桶 2,而 24 分配給桶 4。
- 搜索:我們通過相同的哈希函數解析鍵,並僅在特定存儲桶中搜索。
- 如果我們搜索 1987,我們將使用相同的哈希函數將1987 映射到 2。因此我們在桶 2 中搜索,我們在那個桶中成功找到了 1987。
- 例如,如果我們搜索 23,將映射 23 到 3,並在桶 3 中搜索。我們發現 23 不在桶 3 中,這意味着 23 不在哈希表中。
設計哈希表的關鍵
在設計哈希表時,你應該注意兩個基本因素。
1. 哈希函數
哈希函數是哈希表中最重要的組件,該哈希表用於將鍵映射到特定的桶。在示例中,我們使用 y = x % 5
作爲散列函數,其中 x
是鍵值,y
是分配的桶的索引。
散列函數將取決於鍵值的範圍
和桶的數量。
下面是一些哈希函數的示例:
哈希函數的設計是一個開放的問題。其思想是儘可能將鍵分配到桶中,理想情況下,完美的哈希函數將是鍵和桶之間的一對一映射。然而,在大多數情況下,哈希函數並不完美,它需要在桶的數量和桶的容量之間進行權衡。
2. 衝突解決
理想情況下,如果我們的哈希函數是完美的一對一映射,我們將不需要處理衝突。不幸的是,在大多數情況下,衝突幾乎是不可避免的。
例如,在我們之前的哈希函數(y = x % 5)中,1987 和 2 都分配給了桶 2,這是一個衝突
。
衝突解決算法應該解決以下幾個問題:
- 如何組織在同一個桶中的值?
- 如果爲同一個桶分配了太多的值,該怎麼辦?
- 如何在特定的桶中搜索目標值?
根據我們的哈希函數,這些問題與桶的容量
和可能映射到同一個桶
的鍵的數目
有關。
讓我們假設存儲最大鍵數的桶有 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” 不應該組合在一起。
解決方案
實際上,設計關鍵
是在原始信息和哈希映射使用的實際鍵之間建立映射關係
。設計鍵時,需要保證:
- 屬於同一組的所有值都將映射到同一組中。
- 需要分成不同組的值不會映射到同一組。
此過程類似於設計哈希函數,但這是一個本質區別。哈希函數滿足第一個規則但可能不滿足第二個規則
。但是你的映射函數應該滿足它們。
在上面的示例中,我們的映射策略可以是:對字符串進行排序並使用排序後的字符串作爲鍵。也就是說,“eat” 和 “ate” 都將映射到 “aet”。
設計鍵 - 總結
- 當字符串 / 數組中每個元素的順序不重要時,可以使用
排序後的字符串 / 數組
作爲鍵。 - 如果只關心每個值的偏移量,通常是第一個值的偏移量,則可以使用
偏移量
作爲鍵。 - 在樹中,你有時可能會希望直接使用
TreeNode
作爲鍵。 但在大多數情況下,採用子樹的序列化表述
可能是一個更好的主意。 - 在矩陣中,你可能希望使用
行索引
或列索引
作爲鍵。 - 在數獨中,可以將行索引和列索引組合來標識此元素屬於哪個
塊
。
每個值的偏移量,通常是第一個值的偏移量,則可以使用偏移量
作爲鍵。[外鏈圖片轉存中…(img-kVHyOmRM-1589299271190)] - 在樹中,你有時可能會希望直接使用
TreeNode
作爲鍵。 但在大多數情況下,採用子樹的序列化表述
可能是一個更好的主意。[外鏈圖片轉存中…(img-xKP4SGlm-1589299271191)] - 在矩陣中,你可能希望使用
行索引
或列索引
作爲鍵。 - 在數獨中,可以將行索引和列索引組合來標識此元素屬於哪個
塊
。[外鏈圖片轉存中…(img-cqeZQjVr-1589299271193)] - 有時,在矩陣中,您可能希望將值聚合在
同一對角線
中。