天天看點

map、unordered_map和vector的初始化和底層機理的差别

先說結論:

map<key, value> a

和vector< int >b

,的預設初始化都是初始化為空的容器,而

unordered_map<key, value>c

, 則不是空的

一. 在做力扣1207題時遇到的問題

原題:

map、unordered_map和vector的初始化和底層機理的差别

标準答案裡的unordered_map是采用的預設初始化,因為我之前一般用vector比較多,而且vector要是空的,直接下标通路會出錯,除非元素是一個一個push_back進去,否則一般都會給vector指定一個大小,并且元素初始化為0,即:

vector< int> vec(nums.size(), 0);

但是這unordered_map居然不用指定初始化時的大小:

class Solution {
public:
    bool uniqueOccurrences(vector<int>& arr) {
        unordered_map<int, int> occur;//次數統計的數組
        for (const auto& x: arr) {
            occur[x]++;//x就是arr的元素,成為key,occur[x]就是出現的次數,成為value
        }
        unordered_set<int> times;//查重數組
        for (const auto& x: occur) {
            times.insert(x.second);
        }
        return times.size() == occur.size();
    }
};
           

這段代碼的解題邏輯就是把數組元素和對應的次數插入unordered_map數組,這樣key就是輸入數組arr的各個元素,value就是對應元素出現的次數。

然後把出現次數,即map的value插入unordered_set數組,因為set數組和unordered_set都不允許有重複元素出現(畢竟key和value是同一個元素,而除了multiset,key是唯一的),是以最後看一下unordered_set的大小和次數統計數組unordered_map大小是否一樣就行。

二. map和unordered_map初始化的差別和底層原理

2.1 map

map預設初始化直接就是一個空的map,然後用下标通路的話(map的下标運算符接受一個是以,下标就是key,是以multimap也不能用下标通路,因為其一個關鍵字可能對應多個值),如果關鍵字不在map中,會為它建立一個元素插入到map中,map底層資料結構是紅黑樹,示例如下:

map<string, size_t> word_count;//empty map
word_count["Anna"] = 1;//插入一個關鍵字為Anna的元素,關聯值進行值初始化;然後将1賦予它
           

該示例來自

C++ primer P387

,其實具體過程應該如下:

(1)在word_count 中搜尋關鍵字為Anna的元素,未找到;

(2)将一個新的 關鍵字-值 (key-value)對插入到word_count中,關鍵字是一個const string,儲存Anna。值value将進行初始化,在本例中因為value是整型意味着值為0

(3)提取出新插入的元素,并将值1賦予它。

也就是說,在map中通路不存在的key的鍵值對,首先是新增一個,key-預設值,然後該鍵值對再被等号指派。

2.2 unordered_map

而unordered_map則與上面map的機制完全不同。因為unordered_map底層資料結構是hash table,即哈希表,該哈希表由vector和linked-list連結清單構成,vector用于放buckets主體,有連結清單是因為STL中的hash table采用開鍊法防止哈希沖突,用于存放那些具有相同索引即沖突的元素。下圖來自

《STL源碼剖析》P253,hashtable章節

unordered_map上的所有操作都是直接移植hashtable這一資料結構的。

map、unordered_map和vector的初始化和底層機理的差别

對于unordered_map<key, value>來說,key被hash function做映射(如除法取餘),然後得到相應下标,在vector中存放value,如果後面某元素經過哈希映射後得到的下标和前面一樣,那就是發生哈希沖突,是以需要在該處對外連接配接連結清單來存放這些沖突的鍵值對,這個連結清單不是list也不是slist,而是自定義的linked-list,也就是上圖的bucket list。

是以unordered_map也是動态擴容的啊,而且他并不是初始化為空!而是按照一組質數,共28個:[53,97,193,…429496729],會根據存入元素的個數N在這組質數裡選大于或等于N的數,是以這裡直接按預設初始化,裡面的vector應該大小為53,而不是0;——當然具體也不一定就是53,版本不一樣

我們使用vector容器,vector< int> m,此時m是初始化為空的,size == 0, capacity == 0。——見primer P319例子

是以hashtable設定預設初始大小,應該是為了提高效率,減少動态擴容的次數

那麼在上述預設初始化情況下,vector裡面全是0,一開始

occur[x]++;

首先哈希函數對x做映射——取模,然後vector中對應下标的值處+1,如果不同的x哈希映射後下标一樣,那就得用開鍊法,沖突的vector處就隻放置指針了,指針指向一個連結清單,連結清單裡放這兩個元素(連結清單的每個節點分别是一個指針和一個value),比如11和18都應該放在下标4處的,那下标4處就應該放一個指向連結清單的指針:

map、unordered_map和vector的初始化和底層機理的差别

當x往後周遊,直到超出53時,就進行動态擴容了,按照那個28個數字的清單[53,97,193,…429496729]來選擇下一步擴容到多大。

我們也可以手動設定unordered_map初始大小:

這樣就不需要擴容了。

那為什麼可以隻指定一個參數呢?下圖可以看到,unordered_map構造函數的後面三個都有預設值,是以我們隻需要指定第一個參數就好了。

map、unordered_map和vector的初始化和底層機理的差别

是以代碼最後被改成:

class Solution {
public:
    bool uniqueOccurrences(vector<int>& arr) {
        unordered_map<int, int>occur(arr.size());//指定大小
        for(const auto& x : arr){
            occur[x]++;
        }
        unordered_set<int>unique;
        for(const auto& x : occur){
            unique.insert(x.second);
        }
        return occur.size() == unique.size();
    }
};