C++ hash(STL hash)及其函數模板用法詳解

2020-07-16 10:04:29
如果在容器中儲存物件及其關聯的鍵,並且不用鍵來決定 鍵/物件 對的順序,那就必須對鍵值釆用其他方式來確定元素在記憶體中的位置。如果使用像 string 這樣的物件作為鍵,就會遇到一些問題,可能的變數的數目是巨大的。具有 10 個字元的字母字串可能的個數是 2610。這個索引範圍沒有多大用處。我們需要一種機制來將它變為可接受的範圍;而且理想情況下,這個機制可以為每個鍵生成唯一的值。這也是雜湊需要做的事情之一。

雜湊是用給定範圍的基本型別的資料項,或者用像 string 這樣的物件,生成整數值的過程。雜湊產生的值叫作雜湊值或雜湊碼,它們通常被用在容器中,用來確定表中物件的位置。像前面所說的那樣,理想情況下,每個物件應該產生唯一的雜湊值,但這一般是不可能的。當不同鍵值的個數大於可能的雜湊值個數時,顯然就會出現上面所說的這種情況,我們早晚會得到重複的雜湊值。重複的雜湊值也叫作碰撞。

雜湊不僅可以在容器中儲存物件,它也被應用到很多其他地方,例如密碼和加密資料的安全系統中,密碼識別有時也包含雜湊。在系統中儲存明文密碼是有很大風險的。儲存密碼的雜湊值要比儲存明文密碼更安全,更能防範駭客。得到雜湊值的駭客需要將雜湊值轉換為對他們有用的原始密碼,而這是一個不可能完成的任務。因此STL提供的對不同型別資料雜湊的能力不僅可以用在關聯容器上,也可以被用在更加廣闊的場景中。

雖然理解容器的雜湊機制沒在必要,但是這能讓我們對它們能做些什麼有一個基本的了解。雜湊演算法有很多,但卻沒有可以通用的。為某個場景確定合適的雜湊演算法並不總是簡單。通常都需要對資料分割後再計算。這可能是最簡單的處理鍵的演算法了,不管什麼型別的鍵,都會作為數值處理。所以雜湊值可能是表示式k%m產生的。

顯然,這種方法最多允許有 m 個不同的雜湊值,值的範圍為 0 到 m-1。可以很容易地看到哪裡會產生重複的雜湊值。值為 k+m、k+2*m 的鍵會有重複的雜湊值,m 值的選擇對於減少重複雜湊值的出現至關重要,而且可以保證值是均勻分布的。如果 m 是 2 的冪,也就是 2n,雜湊值的最小位為 k 的 n 位。這顯然不是一個好的結果,因為 k 的大多數位都沒有影響到雜湊值;理想情況下,鍵的所有數位應該都可以影響雜湊結果。m 通常是一個質數,因為它可以使雜湊值更加均勻地分布在這個範圍內。

另一種更好的計算雜湊值的方式是選擇一個常數 a,將它和 k 相乘,用 a*k 除以整數 m 來計算它的餘數,然後從 (a*k)%m 的結果中選擇一個長度值作為雜湊值。顯然 a 和 m 的選擇是非常重要的。對於 32 位的計算機來說,m 通常選為 232。乘數 a 是和 m 相近的質數,這就意味著 a 和 m 除了 1 之外沒有其他的公共因子。此外,a 的二進位制表示中頭部和尾部不能為 0,否則會和其他頭部有 0 或尾部有 0 的鍵值產生碰撞。基於這些原因,這個演算法也被叫作乘法雜湊

也有幾個專門雜湊字串的演算法。其中一個將字串看作一定個數的單詞,使用像乘法演算法這樣的方法來計算第一個單詞的雜湊值,然後加上下一個單詞,再計算它的雜湊值,重複這個過程,直到計算出所有單詞最後的雜湊值。

生成雜湊值的函數

functional 標頭檔案中定義了無序關聯容器使用的特例化 hash<K> 模板。hash<K> 模板定義了可以從 K 型別的物件生成雜湊值的函數物件的型別。hash<K> 範例的成員函數  operator()() 接受 K 型別的單個引數,然後返回 size_t 型別的雜湊值。對於基本型別和指標型別,也定義了特例化的 hash<K> 模板。

hash<K> 模板專用的演算法取決於實現,但是如果它們遵循 C++14 標準的話,需要滿足一些具體的要求。這些要求如下:
  • 不能拋出異常
  • 對於相等的鍵必須產生相等的雜湊值
  • 對於不相等的鍵產生碰撞的可能性必須最小接近 size_t 最大值的倒數

注意,相等鍵生成相等的雜湊值只適用於單次執行。這也就意味著,在不同的場合允許給定的鍵可以生成不同的雜湊值。這就使我們可以在雜湊演算法中使用亂數,當對密碼進行雜湊時,這是我們所希望使用的。注意,C++14 為了保持一致性並沒有排除給定型別的鍵的雜湊值等同於鍵的可能。在無序關聯容器中,用雜湊函數雜湊整數值可能就是這種情況。

下面是一個用 hash<K> 生成整數的雜湊值的範例:
std::hash<int> hash_int;// Function object to hash int
std::vector<int> {-5, -2, 2, 5, 10};
std::transform(std::begin(n), std::end(n),std::ostream_iterator<size_t> (std:: cout," "),hash_int);
這裡使用 transform() 演算法來雜湊 vector 中的元素。transform() 引數中的前兩個疊代器指定了被操作元素的範圍,第三個引數是一個指定輸出地址的疊代器,這裡是一個 ostream 疊代器,最後一個引數是應用到範圍元素上的函數物件 hash<int>。某次測試的輸出結果為:

554121069 2388331168 3958272823 3132668352 1833987007

在你的 C++ 編譯器和庫中,可能會產生不同的雜湊值,所有的雜湊值都是這樣。下面是一個雜湊浮點數值的範例:
std::hash<double> hash_double;
std::vector<double> x {3.14,-2.71828, 99.0, 1.61803399,6.62606957E-34};
std::transform(std::begin(x), std::end(x),std::ostream_iterator<size_t>(std::cout," "),hash_double);
某次測試的輸出結果為:

4023697370 332724328 2014146765 3488612130 3968187275

指標也很容易雜湊:
std::hash<Box*> hash_box; // Box class as in Chapter 2
Box box{1, 2, 3};
std:: cout << "Hash value = " << hash_box (&box)<<std::endl;    // Hash value = 2916986638
可以用相同的函數物件來雜湊智慧指標:
std::hash<Box*> hash_box; // Box class as in Chapter 2
auto upbox = std::make_unique<Box>(1A 2, 3);
std::cout << "Hash value = " << hash_box(upbox.get())<<std::endl; // Hash value = 1143026886
這裡呼叫 unique_ptr<Box> 物件的成員函數 get() 來獲取儲存自由儲存區地址的原生指標,然後將它傳給雜湊函數。這裡使用的 hash<K> 模板也是 unique_ptr<T> 和 shared_ptr<T> 物件的特例化模板。例如,可以對 unique_ptr<Box> 物件而不是對它所包含的原生指標進行雜湊:
std::hash<std::unique_ptr<Box>>hash_box; // Box class as in Chapter 2
auto upbox = std::make_unique<Box>(1, 2, 3);
std::cout << "Hash value = "<< hash_box (upbox)<< std::endl; // Hash value = 4291053140
原生指標和 unique_ptr 的雜湊值是相同的。不要被這個誤導,考慮到當一個型別的鍵沒有具體的雜湊函數時,這種對指標雜湊的能力是很有用的。可以對地址進行雜湊,而不是對物件自己。這和指標指向的物件無關。

如果想在無序容器中以指向鍵的指標為鍵,而不是以鍵為鍵,儲存一些物件,思考一下會發生什麼。指向鍵的指標的雜湊值和原始鍵的雜湊值有很大的不同,因為它們的地址不同,因而無法用它來檢索物件。需要一種可以為使用的任何型別的鍵生成雜湊值的方式。如果鍵的型別是我們所定義的,我們有一個選擇,可以用 STL 提供的雜湊函數來為我們定義的類的資料成員生成雜湊值。

string 標頭檔案中定義了一些特例化的 hash<K> 模板,它們會生成一些函數物件,這些函數物件生成表示字串的物件的雜湊值。有 4 個特例化的模板,它們分別對應於字串型別:string、wstring、u16string 和 u32string。

wstring 型別的字串包含的是 wchar_t 型別的字元;u16string 型別包含的是 charl6_t 型別的字元,它是用 UTF-16 編碼的 Unicode 字元;u32string 型別包含的是 char32_t 型別的字元,它是用 UTF-32 編碼的 Unicode 字元。

當然,字元型別 char、wchar_t、charl6_t 和 char32_t 都是C++14 中的基本型別。下面是一個對字串物件進行雜湊的範例:
std::hash<std::string> hash_str;
std::string food {"corned beef"};
std::cout << "corned beef hash is " <<hash_str (food) <<std::endl;
這裡生成了一個函數物件,它用和前面章節中範例相同的方式來雜湊 string 物件。本次測試的輸出結果如下:

corned beef hash is 3803755380

這裡對 C 風格字串的雜湊沒有具體的規定。使用 const char* 型別的 hash<T> 模板會為指標進行特例化。如果想將 C 風格的字串當作字元序列來雜湊生成雜湊值,可以先用它生成一個 string 物件,然後使用函數物件 hash<string>。

程式碼段生成的雜湊值都是非常大的數,這看起來對於確定物件在無序容器中的位置沒有什麼幫助。有幾種方式可以用雜湊值確定物件在容器中的位置。一個常見的用法是用雜湊值的位元序列作為物件在表或樹中的索引。