大家好,我是程式設計熊。
往期文章介紹了《線性表》中的陣列、連結串列、棧、佇列,以及單調棧和滑動視窗。
本期我們學習雜湊,其主要作用是加速我們查詢資料的速度。
文章將從以下幾個方面展開,內容通俗易懂。
若不想了解雜湊原理,直接使用雜湊表刷題的話,可以直接下拉到"常見的雜湊結構"部分。
雜湊概述
雜湊表又稱雜湊表,表現形式為將任意長度的輸入,通過雜湊演算法變成固定長度的輸出,雜湊表是一種使用空間換取時間的資料結構。
通常是儲存 <key,value>
鍵值對,假設沒有雜湊表,將 <key,value>
鍵值對儲存在陣列中,給定key
查詢的對應的value
的時間複雜度為O(n)
;
陣列就是常見的雜湊表,下標就是key
,對應儲存的值就是value
。
通過引如雜湊表,將任意長度的輸入key
轉化為雜湊表中的下標,將<key,value>
鍵值對對映到雜湊表中,進而加速給定給定key
,查詢value
的速度,時間複雜度降低到O(1)
。
下圖 以兩個鍵值對<key1,value1>
、<key2,value2>
為例,演示了雜湊函式和雜湊表之間的關係,以及在雜湊中起到的作用。
雜湊表基本操作
插入
將鍵值對<key,value>
插入到雜湊表中。
更新
若雜湊表中已存在鍵值為key
的鍵值對,更新雜湊表鍵值對<key,value>
。
刪除
將鍵值對<key,value>
從雜湊表中刪除。
查詢
給定key
,有兩種查詢方式。
- 查詢
key
是否存在於雜湊表中。 - 查詢
key
對應的value
。
雜湊函式
雜湊函式又稱雜湊函式,即將給定的任意長度的輸入值轉化為陣列的索引(下標)。
如果有一個長度為n
的陣列,其可以儲存n
對鍵值對,對應的下標為[0,n-1]
,通常陣列的長度是大於等於鍵值對的數量。
因此我們需要一個雜湊函式,將任意長度的輸入對映到[0,n-1]
,並且每個不同的key
對應的陣列下標一定是不一樣的,即每個陣列下標唯一對應一個key
。
下圖以三對<key,value>
為例,演示了雜湊函式hash
將原始key
,對映到陣列下標的過程,具體雜湊函式實現可以有很多方法,感興趣的讀者可以自行探究。
雜湊衝突
雜湊衝突的出現源於雜湊函式對兩個不同的鍵key1
、key2
(key1≠key2)
,但經過雜湊函式,hash(key1)=hash(key2)
,將兩個不同的key
,對映到了同一個陣列下標位置,導致了雜湊衝突。
下圖以key1="abc"
,key2="bcd"
,兩個不同的key
,經過雜湊函式,對映到同一個陣列下標X
。
解決雜湊衝突的方法
拉鍊法
將hash
值相同的key
放到一個連結串列中,查詢時從前往後遍歷連結串列,找到想要查詢的key
即可。
設需要插入雜湊表的陣列a
長度為n
,雜湊表陣列長度為m
,則拉鍊法查詢任意一個key
的期望時間複雜度為O(1+n/m)
。
下圖展示了需要插入雜湊表的陣列a
,雜湊函式h(x)
,使用拉鍊法解決雜湊衝突的例子。
開放地址法
從發生衝突的位置起,按照某種規則找到雜湊表中其他空閒的位置,將衝突的元素放入這個空閒的位置。
可以找到空閒位置的條件是: 雜湊表的長度一定要大於存放元素的個數。
發生衝突後,以什麼樣的”規則“找到空閒的位置,有很多種方法:
- 線行探查法: 從衝突的位置開始,依次判斷下一個位置是否空閒,直至找到空閒位置。
- 平方探查法: 從衝突的位置x開始,第一次增加
1^2
個位置,第二次增加2^2
...,直至找到空閒的位置。 - 雙雜湊函式探查法等等
再雜湊法
構造多個雜湊函式,發生衝突時,更換雜湊函式,直至找到空閒位置。
建立公共溢位區
建立公共溢位區,在雜湊表中發生雜湊衝突時,將資料儲存到公共溢位區。
常見的雜湊結構
當解決問題需要快速查詢一個元素/鍵值對,就可以考慮利用雜湊表加速查詢的速度。
C++中常用的雜湊結構有以下三個:
- 陣列
- unordered_set(集合)
- unordered_map(對映: 鍵值對)
種類 | 底層實現 | Key是否有序 | Key是否可以重複 | Key是否可以修改 | 增刪查效率 |
---|---|---|---|---|---|
std::unordered_set(集合) | 雜湊表 | Key無序 | Key不可重複 | Key不可修改 | O(1) |
std::unordered_map(對映: 鍵值對) | 雜湊表 | Key無序 | Key不可重複 | Key不可修改 | O(1) |
C++標準庫中的set、map底層基於紅黑樹,將會在後續章節中詳細介紹。
std::unordered_set用法
下面介紹常見的用法,一般可以滿足刷題需要,詳細見https://zh.cppreference.com/w/cpp/container/unordered_set
。
// 定義一個std::unordered_set
std::unordered_set q;
// 迭代器
// begin: 返回指向起始的迭代器
auto iter = q.begin();
// end: 返回指向末尾的迭代器
auto iter = q.end();
// 容量
// empty: 檢查容器是否為空
bool is_empty = q.empty();
// size: 返回容納的元素數量
int s = q.size();
// 修改器
// clear: 清除內容
q.clear();
// insert: 插入元素或結點
q.insert(key);
// erase: 擦除元素
q.erase(key);
// 查詢
// count: 返回匹配特定鍵的元素數量
int num = q.count(key);
// find: 尋找帶有特定鍵的元素
auto iter = q.find(key);
// contains: 檢查容器是否含有帶特定鍵的元素
bool is_contains = q.contains(key);
std::unordered_map用法
下面介紹常見的用法,一般可以滿足刷題需要,詳細見https://zh.cppreference.com/w/cpp/container/unordered_map
。
// 定義一個std::unordered_map
std::unordered_map q;
// 迭代器
// begin: 返回指向起始的迭代器
auto iter = q.begin();
// end: 返回指向末尾的迭代器
auto iter = q.end();
// 容量
// empty: 檢查容器是否為空
bool is_empty = q.empty();
// size: 返回容納的元素數量
int s = q.size();
// 修改器
// clear: 清除內容
q.clear();
// insert: 插入元素或結點
q.insert(key);
// erase: 擦除元素
q.erase(key);
// 查詢
// count: 返回匹配特定鍵的元素數量
int num = q.count(key);
// find: 尋找帶有特定鍵的元素
auto iter = q.find(key);
// contains: 檢查容器是否含有帶特定鍵的元素
bool is_contains = q.contains(key);
例題
LeetCode 1. 兩數之和
題意
給定一個整數陣列 nums
和一個整數目標值 target
,請你在該陣列中找出 和為目標值 target
的那 兩個 整數,並返回它們的陣列下標。
示例
輸入:nums = [2,7,11,15], target = 9
輸出:[0,1]
解釋:因為 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
下圖以示例演示一下雜湊表,將陣列插入到雜湊表中,查詢給定的key
,即可以在O(1)
的時間複雜度查詢到,圖中a,b,c,d
指代雜湊表的下標。
題解
建立雜湊表,key等於陣列的值,value等於值所對應的下標。
然後遍歷陣列,每次遍歷到位置i
時,檢查 target-num[i]
是否存在,注意target-num[i]
的位置不能等於i
。
程式碼
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> numExist = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (numExist.containsKey(target - nums[i])) {
return new int[]{i, numExist.get(target - nums[i])};
}
numExist.put(nums[i], i);
}
return new int[2];
}
}
LeetCode 128. 最長連續序列
題意
給定一個未排序的整數陣列 nums
,找出數字連續的最長序列(不要求序列元素在原陣列中連續)的長度。
示例
輸入:nums = [100,4,200,1,3,2]
輸出:4
解釋:最長數字連續序列是 [1, 2, 3, 4]。它的長度為 4。
題解
方法一
對陣列數字排序,然後遍歷排序後的陣列,找到最長的連續序列。
時間複雜度O(nlogn)
方法二
雜湊可以快速查詢一個數字。
將陣列數字插入到雜湊表,每次隨便拿出一個,刪除其連續的數字,直至找不到連續的,記錄刪除的長度,可以找到最長連續序列。
下圖以示例展示,如何利用雜湊表,找到最長連續序列。
程式碼
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> q;
for (int i = 0; i < nums.size(); i++) {
q.insert(nums[i]);
}
int ans = 0;
while (!q.empty()) {
int now = *q.begin();
q.erase(now);
int l = now - 1, r = now + 1;
while (q.find(l) != q.end()) {
q.erase(l);
l--;
}
while(q.find(r) != q.end()) {
q.erase(r);
r++;
}
l = l + 1, r = r - 1;
ans = max(ans, r - l + 1);
}
return ans;
}
};
習題推薦
- LeetCode 217. 存在重複元素
- LeetCode 594. 最長和諧子序列
- LeetCode 149. 直線上最多的點數
- LeetCode 332. 重新安排行程
【下面是粉絲福利】
【計算機學習核心資源】: 涵蓋了所有計算機學習核心資源,多看看進大廠問題不大。
【github寶藏倉庫】: 對學習和麵試都非常有幫助,學完超過99%同齡人。
如果對你有所幫助,歡迎點贊支援~