1. 程式人生 > 實用技巧 >HashTable、函式物件 學習筆記

HashTable、函式物件 學習筆記

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)這個只是簡單的概述,沒有特別具體深入,但希望對你有幫助~