1 目標
結合一道簡單的題目Leetcode-兩數之和,學習HashTable、和函數對象。
2 題意
給定一個整數數組 nums 和一個目標值 target,請你在該數組中找出和為目標值的那 兩個 整數,並返回他們的數組下標。
你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素不能使用兩遍。
示例:
給定 nums = [2, 7, 11, 15], target = 9
因為 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
3 思路
author's blog == http://www.cnblogs.com/toulanboy/
3.1 思路出發點
這是昨天的打卡題(2020年10月3日),雖然之前做過,但是知道有更好解法,故昨晚學習了一下。
談下暴力法:只需雙重循環,兩兩嘗試匹配。復雜度是0(n^2)
但,如果能擁有常數級別時間復雜度的查找find和插入insert的數據結構,那么結合該數據結構,我們可以使用以下邏輯來實現O(n)的解題。
具體邏輯:從前往后遍歷nums數組。對於nums[i],用O(1)查找該數據結構,查看之前是否出現過他的匹配數字。
- 若有,找到答案,退出。
- 若沒有,則把當前數字用O(1)放入到數據結構,然后繼續nums[i+1]。
總體復雜度:O(n)。
而hashtable就是能滿足我們需求的數據機構!
3.2 HashTable
概述:通過數組+鏈表的形式,結合hash算法,查找和插入的時間復雜度為常數級。
3.2.1 HashTable 結構
(1)表面認識
注釋的內容會在后面解析,剛開始學習,我們先看大體,再看細節。
組成架構:該數據結構包含多個桶bucket[1],然后每個桶里面可以放很多數值[2]。
插入邏輯:對於一個新來的數值key[3],通過一個簡單的運算[4],確定該數值key應該放那個桶,然后把它丟進去[5]即可。
查找邏輯:對於需要被查找的數值key,參考插入邏輯(先通過一個運算確定它在哪個桶),再去這個桶里面逐一遍歷出來。
(2)稍微深入的學習
[1] 多個桶bucket:這是通過順序數組來實現的。
[2] 每個桶里面可以放很多數值:實際上,每個桶存儲的都是一個指針,該指針指向一條鏈表。
[3] key:放入的數值,不限定類型。在C++層面,如果是單一類型,那么可以對應標准庫的unodered_set。如果是鍵值對(結構體),那么可以對應標准庫的unodered_map。
[4] 簡單的運算:這個是hash運算。給定指定數據,hash運算會將其轉換為一串數字
[5] 把它丟進去:這個是hash沖突的處理方法,如果多個數據都hash到同一個桶,那么我們將這個視為hash沖突。而這里處理沖突的方法,就是使用一條鏈表,將所有hash到這個桶的數據都串起來,然后只需把鏈表頭指針放到桶里面就行,這個處理方案的方法被稱為鏈地址法。
(3)結構總結
hashtable的結構利用hash運算,將數值映射到某個桶。如果出現沖突,那么就使用鏈表處理沖突。由於hash運算不需要復雜的運算,所以使得他的查找效率和插入效率非常高。
(4)其他
Q:后期數據太多,鏈表太長影響效率?
A:可以設定閾值,當達到閾值時,則進行重哈希rehash(),將當前數組數據遷移到更大的數組。
(5)上面內容主要從以下博文學習得到,建議感興趣的同學可以細看下面的文章。
3.2.2 HashTable 對應的標准庫
C++新標准中有2個STL容器是用hashtable作為底層實現的:
(1)unodered_set,能夠存儲單類型的容器。例如建立一個字符串類型的hashtable。
(2)unodered_map,能夠存儲鍵值對的容器。例如建立一個 <姓名,年齡>的hashtable。
3.2.3 unodered_set使用示例
關於標准庫的使用,如果是int,string,float這些基本類型,那么STL自帶的hash函數能夠處理,那么建立時只需傳遞數據類型。如:
unodered_set<int> age_set;//建立1個int類型的hashtable
unodered_map<string, age> person_map;//建立1個<string, age>類型的hashtable
若是其他類型,則還需要傳遞hash函數以及比較函數。而這2個函數一般通過函數對象的形式的傳遞。
下面代碼使用了函數對象。若暫時不知道的,可以先看下一小節。
/*
unordered_set的樣例代碼。
*/
# include<iostream>
# include<unordered_set>
using namespace std;
//定義1個類
class Point{
public:
int x;
int y;
Point(int x, int y){
this->x = x;
this->y = y;
}
};
//定義Point的hash類
//由於其重載了(),故其實例化后的對象,類似於函數指針。
class PointHash{
public:
size_t operator()(const Point& p)const{
//這里調用STL的hash為我們計算中間值
return hash<int>()(p.x) + hash<int>()(p.y);
}
};
//定義Point的equal類
//由於其重載了(),故其實例化后的對象,類似於函數指針。
class PointEqual{
public:
bool operator()(const Point& a, const Point& b)const{
return a.x == b.x;
}
};
int main(){
unordered_set<Point, PointHash, PointEqual> my_set;
my_set.insert(Point(11, 22));
for(auto it = my_set.begin(); it != my_set.end(); ++it){
cout << it->x << ","<< it->y << endl;
}
/*
輸出:11, 22
*/
auto result = my_set.find(Point(11, 22));
if(result != my_set.end()){
cout << result->x << ","<< result->y << endl;
}
/*
輸出:11, 22
author's blog == http://www.cnblogs.com/toulanboy/
*/
return 0;
}
3.3.4 參考文章
該部分參考文章如下,作者寫得太好了,感謝。
3.3 函數對象
3.3.1 基本概念
函數對象,也被稱為偽函數,在STL容器中經常被使用。
本質是一個類,該類重載了(),其實例化的對象可以實現函數調用的效果。
舉個例子:
class My_Lovely_Add{
public:
//重載()
int operator()(int a, int b){
return a+b;
}
};
int main(){
My_Lovely_Add f;
cout << f(11, 22) << endl;
//輸出:33
return 0;
}
上述My_Lovely_Add類由於重載了(),故其實例化的對象可以實現函數調用的效果。
3.3.2 與函數指針的異同
他們兩者都能實現具體函數的傳遞,從目前的學習來看,函數對象具備以下優點:
- 可以使用inline。
- 可以通過類成員記錄調用情況。
3.3.3 參考文章
4 代碼
然后,就可以使用hashtable的STL之一 unodered_map來解題了!
class Solution {
public:
//學習了官方題解:https://leetcode-cn.com/problems/two-sum/solution/liang-shu-zhi-he-by-leetcode-solution/
vector<int> twoSum(vector<int>& nums, int target) {
//創建unordered_map,利用其O(1)的插入和查找進行快匹配
unordered_map<int, int> u_map;
//創建unordered_map的迭代器
unordered_map<int, int>::iterator it;
for(int i=0; i<nums.size(); ++i){
//看看前面是否出現有匹配的數字
it = u_map.find(target-nums[i]);
//有則輸出
if(it != u_map.end())
return {it->second, i};
//否則,把當前數字放進Map,繼續往下
u_map.insert(pair(nums[i], i));
}
return {};
}
};
寫到最后:
(1)整理這個簡短的內容,不知不覺已經過去2小時,午飯時間都過了。。can~
(2)這個只是簡單的概述,沒有特別具體深入,但希望對你有幫助~
