大家好,我是編程熊。
往期文章介紹了《線性表》中的數組、鏈表、棧、隊列,以及單調棧和滑動窗口。
本期我們學習哈希,其主要作用是加速我們查找數據的速度。
文章將從以下幾個方面展開,內容通俗易懂。
若不想了解哈希原理,直接使用哈希表刷題的話,可以直接下拉到"常見的哈希結構"部分。
哈希概述
哈希表又稱散列表,表現形式為將任意長度的輸入,通過哈希算法變成固定長度的輸出,哈希表是一種使用空間換取時間的數據結構。
通常是存儲 <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%同齡人。
如果對你有所幫助,歡迎點贊支持~