C++11里新的容器unorderedmap,其底層是一個hashtable,在C++98中,unorderedmap其實已經有過,叫做hashmap。
unorderedmap(hashmap)是一個模板,模板參數有5個,以下是可能的偽代碼(不同的編譯器有不同的實現)
1 template <class Key, 2 class Value, 3 class HashFcn = hash<Key>, 4 class EqualKey = equals_to<Key>, 5 class Alloc = alloc> 6 class hash_map 7 { 8 ... 9 };
Key是鍵值類型,Value是實值類型,HashFcn是一個用來尋找bucket位置的函數,稍后重點講述,EqualKey是用來確定兩個對象如何算相等,Alloc是內存分配相關。
hashtale實際上是由一層vector形成一個bucket的集合,每個bucket上掛有一個鏈表,在不同編譯器下鏈表實現可能不同。既是所謂的sperate chaning開鏈法,分開鏈接。
一個元素想要插入到hashtable,必須要算出掛在哪一個bucket上,再插入到bucket上的鏈表后端。
計算出插入到哪一個bucket上的操作,便是hashfunction的工作。
hashmap是通過Key來決定一個元素的在hashtable中的位置的,因此Key的類型尤為關鍵。
如果Key是一個簡單的系統類型,比如int,char。那么STL已經幫我們定義好了對應的hashfunction。
class HashFcn = hash<Key>
我們注意到HashFcn模板的缺省參數hash<Key>,便是STL給我們實現好的hashfunciton。
舉例hashfunction之前,先用講到一個計算元素在bucket落腳處的函數bkt_num_key
1 size_type bkt_num_key(const key_type& key, size_t n) const 2 { 3 return hash(key) % n; 4 }
STL最終都會走到hash(key)來計算元素的bucket位置。
STL通過偏特化的方式實現了一些簡單類型的hashfunction。舉幾個例子:
1 template <class Key> struct hash {}; 2 3 template<> struct hash<char> { 4 size_t operator()(char x) const { return x; } 5 }; 6 7 template<> struct hash<int> { 8 size_t operator()(int x) const { return x; } 9 };
那么其實簡單類型的hashfunction就是返回自己本身。有稍微復雜一些的char*
1 template<> struct hash<const char*> { 2 size_t operator()(const char* s) const { return __stl_hash_string; } 3 } 4 5 // hash function越亂越好 6 inline size_t __stl_hash_string(const char* s) 7 { 8 unsigned_long h = 0; 9 for (: *s; ++s) 10 h = 5*h + *s; 11 12 return size_t(h); 13 }
那么如果一個Key是自定義的類型,hashfunction應該怎么做呢。
假定我們的自定義類型如下:
struct CustomerInfo { int x = 0; int y = 0; int z = 0; }
這個自定義結構當成unordermap的key,如果你不定義hashfunction應該是不能過編譯的。
也許你會簡單想一個hashfunction,三個int分別用STL的hashfunction版本,然后加起來!天才=_=。
1 struct HashFuncCustomer 2 { 3 std::size_t operator()(const CustomerInfo &c) const 4 { 5 using std::size_t; 6 using std::hash; 7 8 return hash<int>()(c.x) 9 + hash<int>()(c.y) 10 + hash<int>()(c.z)); 11 } 12 };
能過編譯。但是其實這個hashfunction所造成的碰撞是很多的。hashfunction的目標應該是越散越好,因此哈希表又叫散列表。
碰撞的時候,新加的元素會掛在bucket碰撞位置的鏈表后面。hashtable的查找復雜度應該是O(1)~O(n),明顯碰撞越多復雜度越趨近於O(n)。
TR1版本(簡單理解成C++98和C++11之間的過渡版本)提供了一種萬用的hashfunction,外層調用如下即可:
1 struct HashFuncCustomer 2 { 3 std::size_t operator()(const CustomerInfo &c) const 4 { 5 using std::size_t; 6 using std::hash; 7 8 return hash_value(c.x, c.y, c.z); 9 } 10 };
內部的實現其實並不重要,內部的實現是個數學問題=_=。這里面內部用了黃金分割比例,移位等各種操作。
另外一個版本,我在實際項目中使用過的一個版本:
1 struct HashFuncCustomer 2 { 3 std::size_t operator()(const CustomerInfo &c) const 4 { 5 using std::size_t; 6 using std::hash; 7 8 return ((hash<int>()(c.x) 9 ^ (hash<int>()(c.y) << 1)) >> 1) 10 ^ (hash<int>()(c.z) << 1); 11 } 12 };
僅供參考。
hashfunction怎么寫的好,應該是個數學問題。