一個萬用的hashfunction


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怎么寫的好,應該是個數學問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM