今天在使用STL中的hash_map模板遇到使用PTCHAR作為Key時無法對字串進行正確比較的問題,在網上尋找相應的文章可惜沒有找到,但找到了http://www.stlchina.org/twiki/bin/view.pl/Main/STLDetailHashMap和http://www.cppblog.com/guojingjia2006/archive/2008/01/12/41037.aspx兩篇文章對解決我的問題幫了大忙,特將其內容貼出。
hash_map類在標頭檔hash_map中,和所有其它的C++標準庫一樣,標頭檔沒有副檔名。如下聲明:
#include <hash_map>using namespace std;using namespace stdext;
hash_map是一個彙總類,它繼承自_Hash類,包括一個vector,一個list和一個pair,其中vector用於儲存桶,list用於進行衝突處理,pair用於儲存key->value結構,簡要地偽碼如下:
class hash_map<class _Tkey, class _Tval>{private: typedef pair<_Tkey, _Tval> hash_pair; typedef list<hash_pair> hash_list; typedef vector<hash_list> hash_table;};
當然,這隻是一個簡單模型,C++標準庫的泛型模版一向以嵌套複雜而聞名,初學時看類庫,無疑天書啊。微軟的hash_map類還彙總了hash_compare仿函數類,hash_compare類裡又彙總了less仿函數類,亂七八糟的。
下面說說使用方法:
一、簡單變數作為索引:整形、實性、指標型
其實指標型也就是整形,演算法一樣。但是hash_map會對char*, const char*, wchar_t*, const wchar_t*做特殊處理。
這種情況最簡單,下面代碼是整形樣本:
hash_map<int, int> IntHash;IntHash[1] = 123;IntHash[2] = 456;int val = IntHash[1];int val = IntHash[2];
實型和指標型用法和整形一樣,原理如下:
1、使用簡單類型作索引聲明hash_map的時候,不需要聲明模版的後兩個參數(最後一個參數指名hash_map節點的儲存方式,預設為pair,我覺得這就挺好,沒必要修改),使用預設值就好。
2、對於除過字串的其它簡單類型,hash_map使用模版函數 size_t hash_value(const _Kty& _Keyval) 計算hash值,計算方法是經典的掩碼異或法,自動溢出得到索引hash值。微軟的工程師也許開了一個玩笑,這個掩碼被定義為0xdeadbeef(死牛肉,抑或是某個程式員的外號)。
3、對於字串指標作索引的時候,使用定類型函數inline size_t hash_value(const char *_Str)或inline size_t hash_value(const wchar_t *_Str)計算hash值,計算方法是取出每一個字元求和,自動溢出得到hash值。對於字串型的hash索引,要注意需要自訂less仿函數。
因為我們有理由認為,人們使用hash表進行快速尋找的預期成本要比在hash表中插入的預期成本低得多,所以插入可以比尋找昂貴些;基於這個假設,hash_map在有衝突時,插入鏈表是進行排序插入的,這樣在進行查詢衝突解決的時候就能夠更快捷的找到需要的索引。
但是,基於泛型程式設計的原則,hash_map也有理由認為每一種類型都支援使用"<"來判別兩個類型值的大小,這種設計恰好讓字串類型無所適從,眾所周知,兩個字串指標的大小並不代表字串值的大小。見如下代碼:
hash_map<const char*, int> CharHash;CharHash["a"] = 123;CharHash["b"] = 456;char szInput[64] = "";scanf("%s", szInput);int val = CharHash[szInput];
最終的結果就是無論輸入任何字串,都無法找到對應的整數值。因為輸入的字串指標是szInput指標,和"a"或"b"字串常量指標的大小是絕對不會相同。解決方案如下:
首先寫一個仿函數CharLess,繼承自仿函數基類binary_function(當然也可以不繼承,這樣寫只是符合標準,而且寫起來比較方便,不用被類似於指標的指標和指標的引用搞暈。
struct CharLess : public binary_function<const char*, const char*, bool>{public: result_type operator()(const first_argument_type& _Left, const second_argument_type& _Right) const { return(stricmp(_Left, _Right) < 0 ? true : false); }};
很好,有了這個仿函數,就可以正確的使用字串指標型hash_map了。如下:
hash_map<const char*, int, hash_compare<const char*, CharLess> > CharHash;CharHash["a"] = 123;CharHash["b"] = 456;char szInput[64] = "";scanf("%s", szInput);int val = CharHash[szInput];
現在就可以正常工作了。至此,簡單類型的使用方法介紹完畢。
二、使用者自訂類型:比如物件類型,結構體。
這種情況比價複雜,我們先說簡單的,對於C++標準庫的string類。
慶幸的是,微軟為basic_string(string類的基類)提供了hash方法,這使得使用string對象做索引簡單了許多。值得注意(也值得鬱悶)的是,雖然支援string的hash,string類卻沒有重載比較子,所以標準的hash_compare仿函數依舊無法工作。我們繼續重寫less仿函數。
struct string_less : public binary_function<const string, const string, bool>{ public: result_type operator()(const first_argument_type& _Left, const second_argument_type& _Right) const { return(_Left.compare(_Right) < 0 ? true : fase); } };
好了,我們可以書寫如下代碼:
hash_map<string, int, hash_compare<string, string_less> > StringHash;StringHash["a"] = 123;StringHash["b"] = 456;string strKey = "a";int val = CharHash[strKey];
這樣就可以了。
對於另外的一個常用的字串類CString(我認為微軟的CString比標準庫的string設計要洒脫一些)更加複雜一些。很顯然,標準庫裡不包含對於CString的支援,但CString卻重載了比較子(鬱悶)。我們必須重寫hash_compare仿函數。值得一提的是,在Virtual Stdio 2003中,CString不再是MFC的成員,而成為ATL的成員,使用#include <atlstr.h>就可以使用。我沒有採用重寫hash_compare仿函數的策略,而僅僅是繼承了它,在模版庫中的繼承是沒有效能損耗的,而且能讓我偷一點懶。
首先重寫一個hash_value函數:
inline size_t CString_hash_value(const CString& str) { size_t value = _HASH_SEED; size_t size = str.GetLength(); if (size > 0) { size_t temp = (size / 16) + 1; size -= temp; for (size_t idx = 0; idx <= size; idx += temp) { value += (size_t)str[(int)idx]; } } return(value); }
其次重寫hash_compare仿函數:
class CString_hash_compare : public hash_compare<CString> { public: size_t operator()(const CString& _Key) const { return((size_t)CString_hash_value(_Key)); } bool operator()(const CString& _Keyval1, const CString& _Keyval2) const { return (comp(_Keyval1, _Keyval2)); } };
上面的重載忽略了基類對於less仿函數的引入,因為CString具備比較子,我們可以使用預設的less仿函數,在這裡映射為comp。好了,我們可以聲明新的hash_map對象如下:
hash_map<CString, int, CString_hash_compare> CStringHash;
其餘的操作一樣一樣的。
下來就說說對於自訂對象的使用方法:首先定義
struct IHashable { virtual unsigned long hash_value() const = 0; virtual bool operator < (const IHashable& val) const = 0; virtual IHashable& operator = (const IHashable& val) = 0; };
讓我們自寫的類都派生自這裡,有一個標準,接下來定義我們的類:
class CTest : public IHashable { public: int m_value; CString m_message; public: CTest() : m_value(0) {} CTest(const CTest& obj) { m_value = obj.m_value; m_message = obj.m_message; } public: virtual IHashable& operator = (const IHashable& val) { m_value = ((CTest&)val).m_value; m_message = ((CTest&)val).m_message; return(*this); } virtual unsigned long hash_value() const { // 這裡使用類中的m_value域計算hash值,也可以使用更複雜的Function Compute所有域總的hash值 return(m_value ^ 0xdeadbeef } virtual bool operator < (const IHashable& val) const { return(m_value < ((CTest&)val).m_value); } };
用這個類的對象做為hash索引準備工作如下,因為介面中規定了比較子,所以這裡可以使用標準的less仿函數,所以這裡忽略:
template<class _Tkey> class MyHashCompare : public hash_compare<_Tkey> { public: size_t operator()(const _Tkey& _Key) const { return(_Key.hash_value()); } bool operator()(const _Tkey& _Keyval1, const _Tkey& _Keyval2) const { return (comp(_Keyval1, _Keyval2)); } };
下來就這樣寫:
CTest test; test.m_value = 123; test.m_message = "This is a test";MyHash[test] = 2005;int val = MyHash[test];
可以看到正確的數字被返回。
三、關於hash_map的思考:
1、效能分析:採用了內聯代碼和模版技術的hash_map在效率上應該是非常優秀的,但我們還需要注意如下幾點:
* 經過查看代碼,字串索引會比簡單類型索引速度慢,自訂類型索引的效能則和我們選擇hash的內容有很大關係,簡單為主,這是使用hash_map的基本原則。
* 可以通過重寫hash_compair仿函數,更改裡面關於桶數量的定義,如果取值合適,也可以得到更優的效能。如果桶數量大於10,則牢記它應該是一個質數。
* 在自訂類型是,重載的等號(或者拷貝構造)有可能成為效能瓶頸,使用對象指標最為索引將是一個好的想法,但這就必須重寫less仿函數,理由同使用字串指標作為索引。
自己使用上面的方法成功解決了使用PTCHAR作為Key的使用,其解決方案如下:
inline size_t PTCHAR_hash_value(const PTCHAR str){ size_t value = _HASH_SEED; size_t size = _tcslen(str); if (size > 0) { size_t temp = (size/16) + 1; size -= temp; for (size_t idx=0; idx<=size; idx+=temp) { value += (size_t)str[(int)idx]; } } return value;}class PTCHAR_hash_compare : public stdext::hash_compare<PTCHAR>{public: size_t operator()(const PTCHAR _Key) const { return ((size_t)PTCHAR_hash_value(_Key)); } bool operator()(const PTCHAR _Keyval1, const PTCHAR _Keyval2) const { return (_tcscmp(_Keyval1, _Keyval2)); }};stdext::hash_map<PTCHAR, long, PTCHAR_hash_compare > myHash;