1. 程式人生 > >C++ 雜湊表 hash_map詳解

C++ 雜湊表 hash_map詳解

0 為什麼需要hash_map

用過map吧?map提供一個很常用的功能,那就是提供key-value的儲存和查詢功能。例如,我要記錄一個人名和相應的儲存,而且隨時增加,要快速查詢和修改:

嶽不群-華山派掌門人,人稱君子劍
張三丰-武當掌門人,太極拳創始人
東方不敗-第一高手,葵花寶典
...

這些資訊如果儲存下來並不複雜,但是找起來比較麻煩。例如我要找"張三丰"的資訊,最傻的方法就是取得所有的記錄,然後按照名字一個一個比較。如果要速度快,就需要把這些記錄按照字母順序排列,然後按照二分法查詢。但是增加記錄的時候同時需要保持記錄有序,因此需要插入排序。考慮到效率,這就需要用到二叉樹。講下去會沒完沒了,如果你使用STL 的map容器,你可以非常方便的實現這個功能,而不用關心其細節。關於map的資料結構細節,感興趣的朋友可以參看

學習STL map, STL set之資料結構基礎。看看map的實現:

#include <map>
#include <string>
using namespace std;
...
map<string, string> namemap;

//增加。。。 namemap[“嶽不群”]=“華山派掌門人,人稱君子劍”; namemap[“張三丰”]=“武當掌門人,太極拳創始人”; namemap[“東方不敗”]=“第一高手,葵花寶典”; …

//查詢。。 if(namemap.find(“嶽不群”) != namemap.end()){ … }

不覺得用起來很easy嗎?而且效率很高,100萬條記錄,最多也只要20次的string.compare的比較,就能找到你要找的記錄;200萬條記錄事,也只要用21次的比較。

速度永遠都滿足不了現實的需求。如果有100萬條記錄,我需要頻繁進行搜尋時,20次比較也會成為瓶頸,要是能降到一次或者兩次比較是否有可能?而且當記錄數到200萬的時候也是一次或者兩次的比較,是否有可能?而且還需要和map一樣的方便使用。

答案是肯定的。這時你需要has_map. 雖然hash_map目前並沒有納入C++ 標準模板庫中,但幾乎每個版本的STL都提供了相應的實現。而且應用十分廣泛。在正式使用hash_map之前,先看看hash_map的原理。

1 資料結構:hash_map原理

這是一節讓你深入理解hash_map的介紹,如果你只是想囫圇吞棗,不想理解其原理,你倒是可以略過這一節,但我還是建議你看看,多瞭解一些沒有壞處。

hash_map基於hash table(雜湊表)。 雜湊表最大的優點,就是把資料的儲存和查詢消耗的時間大大降低,幾乎可以看成是常數時間;而代價僅僅是消耗比較多的記憶體。然而在當前可利用記憶體越來越多的情況下,用空間換時間的做法是值得的。另外,編碼比較容易也是它的特點之一。

其基本原理是:使用一個下標範圍比較大的陣列來儲存元素。可以設計一個函式(雜湊函式,也叫做雜湊函式),使得每個元素的關鍵字都與一個函式值(即陣列下標,hash值)相對應,於是用這個陣列單元來儲存這個元素;也可以簡單的理解為,按照關鍵字為每一個元素“分類”,然後將這個元素儲存在相應“類”所對應的地方,稱為桶。

但是,不能夠保證每個元素的關鍵字與函式值是一一對應的,因此極有可能出現對於不同的元素,卻計算出了相同的函式值,這樣就產生了“衝突”,換句話說,就是把不同的元素分在了相同的“類”之中。 總的來說,“直接定址”與“解決衝突”是雜湊表的兩大特點。

hash_map,首先分配一大片記憶體,形成許多桶。是利用hash函式,對key進行對映到不同區域(桶)進行儲存。其插入過程是:

  1. 得到key
  2. 通過hash函式得到hash值
  3. 得到桶號(一般都為hash值對桶數求模)
  4. 存放key和value在桶內。

其取值過程是:

  1. 得到key
  2. 通過hash函式得到hash值
  3. 得到桶號(一般都為hash值對桶數求模)
  4. 比較桶的內部元素是否與key相等,若都不相等,則沒有找到。
  5. 取出相等的記錄的value。

hash_map中直接地址用hash函式生成,解決衝突,用比較函式解決。這裡可以看出,如果每個桶內部只有一個元素,那麼查詢的時候只有一次比較。當許多桶內沒有值時,許多查詢就會更快了(指查不到的時候).

由此可見,要實現雜湊表, 和使用者相關的是:hash函式和比較函式。這兩個引數剛好是我們在使用hash_map時需要指定的引數。

2 hash_map 使用

2.1 一個簡單例項

不要著急如何把"嶽不群"用hash_map表示,我們先看一個簡單的例子:隨機給你一個ID號和ID號相應的資訊,ID號的範圍是1~2的31次方。如何快速儲存查詢。

#include <hash_map>
#include <string>
using namespace std;
int main(){
        hash_map<int, string> mymap;
        mymap[9527]="唐伯虎點秋香";
        mymap[1000000]="百萬富翁的生活";
        mymap[10000]="白領的工資底線";
        ...
        if(mymap.find(10000) != mymap.end()){
                ...
        }

夠簡單,和map使用方法一樣。這時你或許會問?hash函式和比較函式呢?不是要指定麼?你說對了,但是在你沒有指定hash函式和比較函式的時候,你會有一個預設的函式,看看hash_map的宣告,你會更加明白。下面是SGI STL的宣告:

template <class _Key, class _Tp, class _HashFcn = hash<_Key>,
class _EqualKey = equal_to<_Key>,
class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class hash_map
{
        ...
}

也就是說,在上例中,有以下等同關係:

...
hash_map<int, string> mymap;
//等同於:
hash_map<int, string, hash<int>, equal_to<int> > mymap;

Alloc我們就不要取關注太多了(希望深入瞭解Allocator的朋友可以參看標準庫 STL :Allocator能做什麼)

2.2 hash_map 的hash函式

hash< int>到底是什麼樣子?看看原始碼:

struct hash<int> {
        size_t operator()(int __x) const { return __x; }
};

原來是個函式物件。在SGI STL中,提供了以下hash函式:

struct hash<char*>
struct hash<const char*>
struct hash<char> 
struct hash<unsigned char> 
struct hash<signed char>
struct hash<short>
struct hash<unsigned short> 
struct hash<int> 
struct hash<unsigned int>
struct hash<long> 
struct hash<unsigned long>

也就是說,如果你的key使用的是以上型別中的一種,你都可以使用預設的hash函式。當然你自己也可以定義自己的hash函式。對於自定義變數,你只能如此,例如對於string,就必須自定義hash函式。例如:

struct str_hash{
        size_t operator()(const string& str) const
        {
                unsigned long __h = 0;
                for (size_t i = 0 ; i < str.size() ; i ++)
                __h = 5*__h + str[i];
                return size_t(__h);
        }
};
//如果你希望利用系統定義的字串hash函式,你可以這樣寫:
struct str_hash{
        size_t operator()(const string& str) const
        {
                return __stl_hash_string(str.c_str());
        }
};

在宣告自己的雜湊函式時要注意以下幾點:

  1. 使用struct,然後過載operator().
  2. 返回是size_t
  3. 引數是你要hash的key的型別。
  4. 函式是const型別的。

如果這些比較難記,最簡單的方法就是照貓畫虎,找一個函式改改就是了。

現在可以對開頭的"嶽不群"進行雜湊化了 smile . 直接替換成下面的宣告即可:

map<string, string> namemap; 
//改為:
hash_map<string, string, str_hash> namemap;

其他用法都不用邊。當然不要忘了吧str_hash的宣告以及標頭檔案改為hash_map。

你或許會問:比較函式呢?彆著急,這裡就開始介紹hash_map中的比較函式。

2.3 hash_map 的比較函式

在map中的比較函式,需要提供less函式。如果沒有提供,預設的也是less< Key> 。在hash_map中,要比較桶內的資料和key是否相等,因此需要的是是否等於的函式:equal_to< Key> 。先看看equal_to的原始碼:

//本程式碼可以從SGI STL
//先看看binary_function 函式宣告,其實只是定義一些型別而已。
template <class _Arg1, class _Arg2, class _Result>
struct binary_function {
        typedef _Arg1 first_argument_type;
        typedef _Arg2 second_argument_type;
        typedef _Result result_type;
};
//看看equal_to的定義:
template <class _Tp>
struct equal_to : public binary_function<_Tp,_Tp,bool>
{
        bool operator()(const _Tp& __x, const _Tp& __y) const { return __x == __y; }
};

如果你使用一個自定義的資料型別,如struct mystruct, 或者const char* 的字串,如何使用比較函式?使用比較函式,有兩種方法. 第一種是:過載==操作符,利用equal_to;看看下面的例子:

struct mystruct{
        int iID;
        int  len;
        bool operator==(const mystruct & my) const{
                return (iID==my.iID) && (len==my.len) ;
        }
};

這樣,就可以使用equal_to< mystruct>作為比較函數了。另一種方法就是使用函式物件。自定義一個比較函式體:

struct compare_str{
        bool operator()(const char* p1, const char*p2) const{
                return strcmp(p1,p2)==0;
        }
};

有了compare_str,就可以使用hash_map了。

typedef hash_map<const char*, string, hash<const char*>, compare_str> StrIntMap;
StrIntMap namemap;
namemap["嶽不群"]="華山派掌門人,人稱君子劍";
namemap["張三丰"]="武當掌門人,太極拳創始人";
namemap["東方不敗"]="第一高手,葵花寶典";

2.4 hash_map 函式

hash_map的函式和map的函式差不多。具體函式的引數和解釋,請參看:STL 程式設計手冊:Hash_map,這裡主要介紹幾個常用函式。

  1. hash_map(size_type n) 如果講究效率,這個引數是必須要設定的。n 主要用來設定hash_map 容器中hash桶的個數。桶個數越多,hash函式發生衝突的概率就越小,重新申請記憶體的概率就越小。n越大,效率越高,但是記憶體消耗也越大。
  2. const_iterator find(const key_type& k) const. 用查詢,輸入為鍵值,返回為迭代器。
  3. data_type& operator[](const key_type& k) . 這是我最常用的一個函式。因為其特別方便,可像使用陣列一樣使用。不過需要注意的是,當你使用[key ]操作符時,如果容器中沒有key元素,這就相當於自動增加了一個key元素。因此當你只是想知道容器中是否有key元素時,你可以使用find。如果你希望插入該元素時,你可以直接使用[]操作符。
  4. insert 函式。在容器中不包含key值時,insert函式和[]操作符的功能差不多。但是當容器中元素越來越多,每個桶中的元素會增加,為了保證效率,hash_map會自動申請更大的記憶體,以生成更多的桶。因此在insert以後,以前的iterator有可能是不可用的。
  5. erase 函式。在insert的過程中,當每個桶的元素太多時,hash_map可能會自動擴充容器的記憶體。但在sgi stl中是erase並不自動回收記憶體。因此你呼叫erase後,其他元素的iterator還是可用的。

3 相關hash容器

hash 容器除了hash_map之外,還有hash_set, hash_multimap, has_multiset, 這些容器使用起來和set, multimap, multiset的區別與hash_map和map的區別一樣,我想不需要我一一細說了吧。

4 其他

這裡列幾個常見問題,應該對你理解和使用hash_map比較有幫助。

4.1 hash_map和map的區別在哪裡?

  • 建構函式。hash_map需要hash函式,等於函式;map只需要比較函式(小於函式).
  • 儲存結構。hash_map採用hash表儲存,map一般採用紅黑樹(RB Tree)實現。因此其memory資料結構是不一樣的。

4.2 什麼時候需要用hash_map,什麼時候需要用map?

總體來說,hash_map 查詢速度會比map快,而且查詢速度基本和資料資料量大小,屬於常數級別;而map的查詢速度是log(n)級別。並不一定常數就比log(n)小,hash還有hash函式的耗時,明白了吧,如果你考慮效率,特別是在元素達到一定數量級時,考慮考慮hash_map。但若你對記憶體使用特別嚴格,希望程式儘可能少消耗記憶體,那麼一定要小心,hash_map可能會讓你陷入尷尬,特別是當你的hash_map物件特別多時,你就更無法控制了,而且hash_map的構造速度較慢。

現在知道如何選擇了嗎?權衡三個因素: 查詢速度, 資料量, 記憶體使用。

4.3 如何在hash_map中加入自己定義的型別?

你只要做兩件事, 定義hash函式,定義等於比較函式。下面的程式碼是一個例子:

-bash-2.05b$ cat my.cpp
#include <hash_map>
#include <string>
#include <iostream>

using namespace std; //define the class class ClassA{ public: ClassA(int a):c_a(a){} int getvalue()const { return c_a;} void setvalue(int a){c_a;} private: int c_a; };

//1 define the hash function struct hash_A{ size_t operator()(const class ClassA & A)const{ // return hash<int>(classA.getvalue()); return A.getvalue(); } };

//2 define the equal function struct equal_A{ bool operator()(const class ClassA & a1, const class ClassA & a2)const{ return a1.getvalue() == a2.getvalue(); } };

int main() { hash_map<ClassA, string, hash_A, equal_A> hmap; ClassA a1(12); hmap[a1]=“I am 12”; ClassA a2(198877); hmap[a2]=“I am 198877”;

    cout&lt;&lt;hmap[a1]&lt;&lt;endl;
    cout&lt;&lt;hmap[a2]&lt;&lt;endl;
    <span style="color:#a52a2a;">return</span> 0;

} -bash-2.05b$ make my c++ -O -pipe -march=pentiumpro my.cpp -o my -bash-2.05b$ ./my I am 12 I am 198877

4.4如何用hash_map替換程式中已有的map容器?

這個很容易,但需要你有良好的程式設計風格。建議你儘量使用typedef來定義你的型別:

typedef map<Key, Value> KeyMap;

當你希望使用hash_map來替換的時候,只需要修改:

typedef hash_map<Key, Value> KeyMap;

其他的基本不變。當然,你需要注意是否有Key型別的hash函式和比較函式。

4.5為什麼hash_map不是標準的?

具體為什麼不是標準的,我也不清楚,有個解釋說在STL加入標準C++之時,hash_map系列當時還沒有完全實現,以後應該會成為標準。如果誰知道更合理的解釋,也希望告訴我。但我想表達的是,正是因為hash_map不是標準的,所以許多平臺上安裝了g++編譯器,不一定有hash_map的實現。我就遇到了這樣的例子。因此在使用這些非標準庫的時候,一定要事先測試。另外,如果考慮到平臺移植,還是少用為佳。

常見問題:

本來想用hash_map實現大數量的快速查詢,後來發現效率並不快,而且有些問題也很不解,比如看如下程式碼:

C/C++ code
#include <iostream> #include <hash_map.h> using namespace std; int main(){ hash_map<int,string> hm(3); //初始化hash_map的桶的個數 hm.insert(make_pair(0,"hello")); hm.insert(make_pair(1,"ok")); hm.insert(make_pair(2,"bye")); hm.insert(make_pair(3,"world")); cout<<hm.size()<<endl; cout<<hm.bucket_count()<<endl; return 0; }

輸出結果: 4 53 對這個結果很疑惑,明明我定義了桶的個數,為什麼後面得到桶的個數為53? hash_map預設對int型別的Key如何hash,hash函式是什麼? 如何使得查詢能更高效?可以用空間來換 各位大俠請教啊

這是我對hash的曾經的一點嘗試,僅供參考:

C/C++ code
#include <iostream> #include <map> #include <string>

#ifdef GNUC #include <ext/hash_map> #else #include <hash_map> #endif

#ifdef GXX_EXPERIMENTAL_CXX0X #include <unordered_map> #endif

namespace std { using namespace __gnu_cxx; }

namespace __gnu_cxx { template<> struct hash< std::string > { size_t operator()( const std::string& x ) const { return hash< const char* >()(x.c_str()); } }; }

int main() { std::map<std::string, std::string> stdMap; stdMap["_GLIBCXX_STD"] = “std”; stdMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE"] = “+namespace”; stdMap["_GLIBCXX_BEGIN_NAMESPACE"] = “+namespace”; stdMap["_GLIBCXX_END_NESTED_NAMESPACE"] = “}”; stdMap["_GLIBCXX_END_NAMESPACE"] = “}”; stdMap["_GLIBCXX_END_NAMESPACE_TR1"] = “}”; stdMap["_GLIBCXX_BEGIN_NAMESPACE_TR1"] = “-namespace tr1 {”; stdMap["_GLIBCXX_STD2"] = “2std”; stdMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE2"] = “2+namespace”; stdMap["_GLIBCXX_BEGIN_NAMESPACE2"] = “2+namespace”; stdMap["_GLIBCXX_END_NESTED_NAMESPACE2"] = “2}”; stdMap["_GLIBCXX_END_NAMESPACE2"] = “2}”; stdMap["_GLIBCXX_END_NAMESPACE_TR12"] = “2}”; stdMap["_GLIBCXX_BEGIN_NAMESPACE_TR12"] = “2-namespace tr1 {”; stdMap["_XXGLIBCXX_END_NAMESPACE_TR12"] = “X2}”; stdMap["_XXGLIBCXX_BEGIN_NAMESPACE_TR12"] = “X2-namespace tr1 {”;

std::hash_map&lt;std::string, std::string&gt; hashMap;
hashMap["_GLIBCXX_STD"] = "std";
hashMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE"] = "+namespace";
hashMap["_GLIBCXX_BEGIN_NAMESPACE"] = "+namespace";
hashMap["_GLIBCXX_END_NESTED_NAMESPACE"] = "}";
hashMap["_GLIBCXX_END_NAMESPACE"] = "}";
hashMap["_GLIBCXX_END_NAMESPACE_TR1"] = "}";
hashMap["_GLIBCXX_BEGIN_NAMESPACE_TR1"] = "-namespace tr1 {";
hashMap["_GLIBCXX_STD2"] = "2std";
hashMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE2"] = "2+namespace";
hashMap["_GLIBCXX_BEGIN_NAMESPACE2"] = "2+namespace";
hashMap["_GLIBCXX_END_NESTED_NAMESPACE2"] = "2}";
hashMap["_GLIBCXX_END_NAMESPACE2"] = "2}";
hashMap["_GLIBCXX_END_NAMESPACE_TR12"] = "2}";
hashMap["_GLIBCXX_BEGIN_NAMESPACE_TR12"] = "2-namespace tr1 {";
hashMap["_XXGLIBCXX_END_NAMESPACE_TR12"] = "X2}";
hashMap["_XXGLIBCXX_BEGIN_NAMESPACE_TR12"] = "X2-namespace tr1 {";

#ifdef GXX_EXPERIMENTAL_CXX0X std::unordered_map<std::string, std::string> unorderedMap; unorderedMap["_GLIBCXX_STD"] = “std”; unorderedMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE"] = “+namespace”; unorderedMap["_GLIBCXX_BEGIN_NAMESPACE"] = “+namespace”; unorderedMap["_GLIBCXX_END_NESTED_NAMESPACE"] = “}”; unorderedMap["_GLIBCXX_END_NAMESPACE"] = “}”; unorderedMap["_GLIBCXX_END_NAMESPACE_TR1"] = “}”; unorderedMap["_GLIBCXX_BEGIN_NAMESPACE_TR1"] = “-namespace tr1 {”; unorderedMap["_GLIBCXX_STD2"] = “2std”; unorderedMap["_GLIBCXX_BEGIN_NESTED_NAMESPACE2"] = “2+namespace”; unorderedMap["_GLIBCXX_BEGIN_NAMESPACE2"] = “2+namespace”; unorderedMap["_GLIBCXX_END_NESTED_NAMESPACE2"] = “2}”; unorderedMap["_GLIBCXX_END_NAMESPACE2"] = “2}”; unorderedMap["_GLIBCXX_END_NAMESPACE_TR12"] = “2}”; unorderedMap["_GLIBCXX_BEGIN_NAMESPACE_TR12"] = “2-namespace tr1 {”; unorderedMap["_XXGLIBCXX_END_NAMESPACE_TR12"] = “X2}”; unorderedMap["_XXGLIBCXX_BEGIN_NAMESPACE_TR12"] = “X2-namespace tr1 {”; #endif

for (int i = 0; i &lt; 5; ++i)
{
    const clock_t t = clock();
    for (int j = 0; j &lt; 1000000; ++j) stdMap.find("testfindkey");
    std::cout &lt;&lt; "stdMap " &lt;&lt; i + 1 &lt;&lt; " : " &lt;&lt; clock() - t &lt;&lt; std::endl;
}

std::cout &lt;&lt; "/n---------------/n" &lt;&lt; std::endl;

for (int i = 0; i &lt; 5; ++i)
{
    const clock_t t = clock();
    for (int j = 0; j &lt; 1000000; ++j) hashMap.find("testfindkey");
    std::cout &lt;&lt; "hashMap " &lt;&lt; i + 1 &lt;&lt; " : " &lt;&lt; clock() - t &lt;&lt; std::endl;
}

#ifdef GXX_EXPERIMENTAL_CXX0X std::cout << “/n---------------/n” << std::endl;

for (int i = 0; i &lt; 5; ++i)
{
    const clock_t t = clock();
    for (int j = 0; j &lt; 1000000; ++j) unorderedMap.find("testfindkey");
    std::cout &lt;&lt; "unorderedMap " &lt;&lt; i + 1 &lt;&lt; " : " &lt;&lt; clock() - t &lt;&lt; std::endl;
}

#endif

return 0;

}

如果你使用的vc自帶的hash函式,那麼它的定義中如下:
C/C++ code
template<class _Kty, class _Pr = less> class hash_compare1 { // traits class for hash containers public: //const static long lBucketSize = 0; enum { // parameters for hash table bucket_size = 4, // 0 < bucket_size min_buckets = 8 // min_buckets = 2 ^^ N, 0 < N }; 。。。
每次增長會2倍增加預分配記憶體,你的hash_map是哪個版本的?

先看看alvin_lee 朋友做的解析,我覺得還是很正確的,從演算法角度闡述了他們之間的問題!

實際上這個問題不光C++會遇到,其他所有語言的標準容器的實現及選擇上都是要考慮的。做應用程式你可能覺得影響不大,但是寫演算法或者核心程式碼就要小心了。今天改進程式碼,順便又來溫習基礎功課了。

  還記得Herb Sutter那極有味道的《C++對話系列》麼,在其中《產生真正的hash物件》這個故事裡就講了map的選擇。順便回顧一下,也講一下我在實用中的理解。

  選擇map容器,是為了更快的從關鍵字查詢到相關的物件。與使用list這樣的線性表容器相比,一可以簡化查詢的演算法,二可以使任意的關鍵字做索引,並與目標物件配對,優化查詢演算法。在C++的STL中map是使用樹來做查詢演算法,這種演算法差不多相當與list線性容器的折半查詢的效率一樣,都是O (log2N),而list就沒有map這樣易定製和操作了。

  相比hash_map,hash_map使用hash表來排列配對,hash表是使用關鍵字來計算表位置。當這個表的大小合適,並且計算演算法合適的情況下,hash表的演算法複雜度為O(1)的,但是這是理想的情況下的,如果hash表的關鍵字計算與表位置存在衝突,那麼最壞的複雜度為O(n)。

  那麼有了這樣的認識,我們應該怎麼樣選用演算法呢?前兩天看Python文章的時候,不知道哪個小子說Python的map比c++的map快,如何如何的。但是他並不知道Python是預設使用的 hash_map,而且這些語言特徵本質上是使用c/c++寫出來的,問題在與演算法和手段,而不是在於語言本身的優劣,你熟悉了各種演算法,各種語言的細節、設計思想,還能在這偏激的嚷嚷孰好孰壞(片面與偏激的看待事物只能表明愚昧與無知,任何事物都有存在的價值,包括技術)。顯然C++的STL預設使用樹結構來實現map,是有考究的。

  樹查詢,在總查詢效率上比不上hash表,但是它很穩定,它的演算法複雜度不會出現波動。在一次查詢中,你可以斷定它最壞的情況下其複雜度不會超過O(log2N)。而hash表就不一樣,是O(1),還是O(N),或者在其之間,你並不能把握。假若你在開發一個供外部呼叫的介面,其內部有關鍵字的查詢,但是這個介面呼叫並不頻繁,你是會希望其呼叫速度快、但不穩定呢,還是希望其呼叫時間平均、且穩定呢。反之假若你的程式需要查詢一個關鍵字,這個操作非常頻繁,你希望這些操作在總體上的時間較短,那麼hash表查詢在總時間上會比其他要短,平均操作時間也會短。這裡就需要權衡了。

  這裡總結一下,選用map還是hash_map,關鍵是看關鍵字查詢操作次數,以及你所需要保證的是查詢總體時間還是單個查詢的時間。如果是要很多次操作,要求其整體效率,那麼使用hash_map,平均處理時間短。如果是少數次的操作,使用 hash_map可能造成不確定的O(N),那麼使用平均處理時間相對較慢、單次處理時間恆定的map,考慮整體穩定性應該要高於整體效率,因為前提在操作次數較少。如果在一次流程中,使用hash_map的少數操作產生一個最壞情況O(N),那麼hash_map的優勢也因此喪盡了。

下面先看一段程式碼,從Codeproject的 Jay Kint:

// familiar month example used  // mandatory contrived example to show a simple point // compiled using MinGW gcc 3.2.3 with gcc -c -o file.o  // file.cpp

#include <string> #include <ext/hash_map> #include <iostream>

using namespace std; // some STL implementations do not put hash_map in std using namespace __gnu_cxx;

hash_map<const char*, int> days_in_month;

class MyClass {    static int totalDaysInYear; public:    void add_days( int days ) { totalDaysInYear += days; }    static void printTotalDaysInYear(void)     {         cout << "Total Days in a year are "             << totalDaysInYear << endl;     } };

int MyClass::totalDaysInYear = 0;

int main(void) {    days_in_month["january"] = 31;    days_in_month["february"] = 28;    days_in_month["march"] = 31;    days_in_month["april"] = 30;    days_in_month["may"] = 31;    days_in_month["june"] = 30;    days_in_month["july"] = 31;    days_in_month["august"] = 31;    days_in_month["september"] = 30;    days_in_month["october"] = 31;    days_in_month["november"] = 30;    days_in_month["december"] = 31;

   // ERROR: This line doesn't compile.    accumulate( days_in_month.begin(), days_in_month.end(),        mem_fun( &MyClass::add_days ));

   MyClass::printTotalDaysInYear();

   return 0; }

當然上面的程式碼完全可以使用STL來實現:

引用

Standard C++ Solutions The Standard C++ Library defines certain function adaptors, select1st, select2nd and compose1, that can be used to call a single parameter function with either the key or the data element of a pair associative container.

select1st and select2nd do pretty much what their respective names say they do. They return either the first or second parameter from a pair.

compose1 allows the use of functional composition, such that the return value of one function can be used as the argument to another. compose1(f,g) is the same as f(g(x)).

Using these function adaptors, we can use for_each to call our function.

hash_map my_map; for_each( my_map.begin(), my_map.end(),           compose1( mem_fun( &MyType::do_something ),                     select2nd                    MyType>::value_type>())); Certainly, this is much better than having to define helper functions for each pair, but it still seems a bit cumbersome, especially when compared with the clarity that a comparable for loop has.

for( hash_map::iterator i =          my_map.begin();     i != my_map.end(), ++i ) {

    i->second.do_something(); } Considering it was avoiding the for loop for clarity's sake that inspired the use of the STL algorithms in the first place, it doesn't help the case of algorithms vs. hand written loops that the for loop is more clear and concise.

with_data and with_key with_data and with_key are function adaptors that strive for clarity while allowing the easy use of the STL algorithms with pair associative containers. They have been parameterized much the same way mem_fun has been. This is not exactly rocket science, but it is quickly easy to see that they are much cleaner than the standard function adaptor expansion using compose1 and select2nd.

Using with_data and with_key, any function can be called and will use the data_type or key_type as the function's argument respectively. This allows hash_map, map, and any other pair associative containers in the STL to be used easily with the standard algorithms. It is even possible to use it with other function adaptors, such as mem_fun.

hash_map my_vert_buffers;

void ReleaseBuffers(void) {    // release the vertex buffers created so far.    std::for_each( my_vert_buffers.begin(),         my_vert_buffers.end(),         with_data( boost::mem_fn(             &IDirect3DVertexBuffer9::Release ))); } Here boost::mem_fn is used instead of mem_fun since it recognizes the __stdcall methods used by COM, if the BOOST_MEM_FN_ENABLE_STDCALL macro is defined.

摘錄如下:

引用

一直都用的STL的map,直到最近庫裡資料量急劇增大,聽別的做檢索的同學說到hash_map,一直都打算換回來,今天好好做了個實驗測試了哈hash_map的功能,效果以及與map比較的效能.      首先,要說的是這兩種資料結構的都提供了KEY-VALUE的儲存和查詢的功能.但是實現是不一樣的,map是用的紅黑樹,查詢時間複雜度為log (n),而hash_map是用的雜湊表.查詢時間複雜度理論上可以是常數,但是消耗記憶體大,是一種以儲存換時間的方法.      就應用來說,map已經是STL標準庫的東西,可是hash_map暫時還未進入標準庫,但也是非常常用也非常重要的庫.      這次所做的測試是對於100W及的檔案列表,去重的表現,即是對檔名string,做map! 用到的標頭檔案:

#include <time.h>    //計算時間效能用      #include <ext/hash_map>   //包含hash_map 的標頭檔案      #include <map>             //stl的map      using namespace std;        //std 名稱空間      using namespace __gnu_cxx;    //而hash_map是在__gnu_cxx的名稱空間裡的

//測試3個環節:用map的效率,hash_map系統hash函式的效率及自寫hash函式的效率.

    11 struct str_hash{      //自寫hash函式     12     size_t operator()(const string& str) const     13     {     14         unsigned long __h = 0;     15         for (size_t i = 0 ; i < str.size() ; i ++)     16         {     17             __h = 107*__h + str[i];     18         }     19         return size_t(__h);     20     }     21 };

    23 //struct str_hash{    //自帶的string hash函式     24 //        size_t operator()(const string& str) const     25 //      {     26 //          return __stl_hash_string(str.c_str());     27 //      }     28 //};

    30 struct str_equal{      //string 判斷相等函式     31     bool operator()(const string& s1,const string& s2) const     32     {     33         return s1==s2;     34     }     35 };

//用的時候     37 int main(void)     38 {     39     vector<string> filtered_list;     40     hash_map<string,int,str_hash,str_equal> file_map;     41     map<string,int> file2_map;     42     ifstream in("/dev/shm/list");     43     time_t now1 = time(NULL);     44     struct tm * curtime;     45     curtime = localtime ( &now1 );     46     cout<<now1<<endl;     47     char ctemp[20];     48     strftime(ctemp, 20, "%Y-%m-%d %H:%M:%S" , curtime);     49     cout<<ctemp<<endl;     50     string temp;     51     int i=0;     52     if(!in)     53     {     54         cout<<"open failed!~"<<endl;     55     }     56     while(in>>temp)     57     {     58         string sub=temp.substr(0,65);     59         if(file_map.find(sub)==file_map.end())     60 //      if(file2_map.find(sub)==file2_map.end())     61         {     62             file_map[sub]=i;     63 //          file2_map[sub]=i;     64             filtered_list.push_back(temp);     65             i++;     66 //          cout<<sub<<endl;     67         }     68     }     69     in.close();     70     cout<<"the total unique file number is:"<<i<<endl;     71     ofstream out("./file_list");     72     if(!out)     73     {     74         cout<<"failed open"<<endl;     75     }     76     for(int j=0;j<filtered_list.size();j++)     77     {     78         out<<filtered_list[j]<<endl;     79     }     80     time_t now2=time(NULL);     81     cout<<now2<<endl;     82     curtime = localtime ( &now2 );     83     strftime(ctemp, 20, "%Y-%m-%d %H:%M:%S" , curtime);     84     cout<<now2-now1<<"\t"<<ctemp<<endl;     85     return 0;     86 }

引用

得出來的結論是:(檔案list有106W,去重後有51W)    1.map完成去重耗時34秒    2.hash_map用系統自帶的函式,耗時22秒    3.hash_map用自己寫的函式,耗時14秒       測試結果充分說明了hash_map比map的優勢,另外,不同的hash函式對效能的提升也是不同的,上述hash函式為一同學,測試N多資料後得出的經驗函式. 可以預見,當數量級越大時越能體現出hash_map的優勢來!~

當然最後作者的結論是錯誤的,hash_map的原理理解錯誤!從第一個朋友的回答就可以體會到這個問題!

最後對於C++Builder使用者,應該通過以下方法新增: #include "stlport\hash_map" 才可以正確的使用hash_map

一個簡單的例子

/*  *用來測試STL hash_map   *簡單例子2008.5.5*/ #include  <cstdlib> #include  <iostream> #include  <string> #include  <hash_map.h>/*因為hash_map暫不為CPP標準所以沒辦法寫為<hash_map>*//*-------------------------------------------*/using  std::cout;using  std::endl;using  std::string;/*-------------------------------------------*//*函式類  *作為hash_map的hash函式   *string沒有預設的hash函式   */class str_hash{       public:        size_t operator()(conststring& str) const         {                 unsigned long __h =0;                 for (size_t i =0 ; i < str.size() ; i ++)                 __h =5*__h + str[i];                 return size_t(__h);         } };/*-------------------------------------------*//*函式類   *作為hash_map的比較函式 )  *(查詢的時候不同的key往往可能對用到相同的hash值*/class str_compare {       public:              booloperator()(conststring& str1,conststring& str2)const              {return   str1==str2;} };/*-------------------------------------------*/int  main(int argc, char*argv[]) {       hash_map<string,string,str_hash,str_compare>  myhash;          myhash["google"]="newplan";         myhash["baidu"]="zhaoziming";         if(myhash.find("google")!=myhash.end())       cout<<myhash["google"]<<endl;          system("PAUSE");          return EXIT_SUCCESS; }/*-------------------------------------------*/

另一個簡單例子

hash_map

程式碼實現:

#include

#include

#include

using namespace __gnu_cxx;

using namespace std;

struct str_hash{

    size_t operator()(const string& str) const {

        return __stl_hash_string(str.c_str());

    }

};

struct compare_str {

    bool operator()(const string& str1,const string& str2) const{

        return str1 == str2;

    }

};

int main()

{

    typedef hash_map namehash;

    namehash strhash;

    namehash::iterator it;

    //通過[]方式新增hash_map

    strhash["嶽不群"]="華山派掌門人,人稱君子劍";

    strhash["張三丰"]="武當掌門人,太極拳創始人";

    strhash["東方不敗"]="第一高手,葵花寶典";

    //通過pair方式插入hash_map

    strhash.insert(pair("IntPassion", "林元已於,風而存在"));

    strhash.insert(make_pair("陳英俊", "玉樹臨風,英俊瀟灑"));

    //通過find方式檢視hash_map的元素

    it = strhash.find("陳英俊");

    cout<<it->first<<" -> "<<it->second<<endl;

    //通過[]方式獲取hash_map元素

    cout<<"IntPassion -> "<<strhash["IntPassion"]<<endl;

    cout<<"\n遍歷輸出hash_map\n";

    for(namehash::iterator itb = strhash.begin(); itb!=strhash.end();itb++)

        cout<<itb->first<<" -> "<<itb->second<<endl;

    return 0;

}

從效能上來說,const char* 作為鍵要比string作為鍵要高;(後面的一篇文章會專門說明效能測試的結果)但是,使用string作為鍵更安全。使用const char*作為鍵的時候,要切記const char*指向的位置總是長期有效的。(尤其不要把一個棧內的字元陣列加到全域性的hash_map中作為鍵)

在這裡幾點要非常的注意,否則則會像我在編寫該程式的時候,很簡單但是卻一直編譯錯誤。

首先,要注意的是包含的標頭檔案。hash_map不是C++標準庫的一部分,但因其重要性很多庫(sgi stlboost)實現了hash_map,包括g++編譯器所帶的標頭檔案也包含了hash_map的實現程式碼(其實現為sgi stl的版本),其在include/ext目錄下,該目錄還包含了hash_setrope等的實現。,hash_map定義在__gnu_cxx名稱空間中,故你必須在使用時限定名字空間__gnu_cxx::hash_map,或者使用using關鍵字,如下例:

#include

using namespace __gnu_cxx;

windows  linux下引入hash_sethash_map標頭檔案

推薦使用方法:在原始碼的前面寫入一下程式碼:

// just for "#include " in linux

#if __GNUC__>2

#include

#include

using namespace __gnu_cxx;

#else

#include

#include

using namespace stdext;

#endif

其它解釋和方法:

因為hash_map以前不屬於標準庫,而是後來引入的。所以在windows下需要使用stlport,然後在setting中加入Additional library path。在linux下使用gcc的時候,引入,使用的時候也說找不到hash_map,而這種後來引入標準庫的有兩種可能:一種是它被放在了stdext名空間裡,那麼就要使用using namespace stdext引入該名空間並#include ;另一種可能就是它被放在標準庫的ext目錄底下,這時就仍舊需要使用屬於std名空間,這時你的原始檔應當#include ;

其次,這裡就是hash_maphash_set實現上的注意事項。因為,在hash_map中,鍵型別的hash函式實現只是對於一些基本的型別,char*ntchar並沒有實現stringdouble

float這些型別了,所以要這對string型別必須實現hash函式(第三項於結構體或者自定義型別,則必須還要實現比較函式(第四項)。說實話,使用string型別作為鍵有更好的擴充套件性和應用性。比使用char*更加的靈活和方便。

 最後,比較重要的一點。上面實現的hash函式和比較函式都是const函式,並且函式的引數也是const型別的,如果少了其中一個都是無法編譯通過的。

第3個簡單例子

//hash_map,map,都是將記錄型的元素劃分為鍵值和映照資料兩個部分; //不同的是:hash_map採用雜湊表的結構而map採用紅黑樹的結構; //hash_map鍵值比較次數少,佔用較多的空間,遍歷出來的元素是非排序的而map是排序的

#include<hash_map> #include<iostream> using namespace std;

int main(void) {  hash_map<const char*,float>hm;  //元素的插入:pair<iterator,bool>insert<const value_type &v>  //insert(inputiterator first,inputiterator last)  hm["apple"]=1.0f;  hm["pear"]=1.5f;  hm["orange"]=2.0f;  hm["banana"]=1.8f;  //元素的訪問可以用,陣列的方式也可以用迭代器的方式  hash_map<const char*,float>::iterator i,iend,j;  iend=hm.end();  for(i=hm.begin();i!=iend;i++)  {   cout<<(*i).first<<"   "    <<(*i).second<<endl;  }  cout<<"**************************************************"<<endl;  //元素的刪除erase(),clear()  hm.erase("pear");  for(i=hm.begin();i!=iend;i++)  {   cout<<(*i).first<<"   "    <<(*i).second<<endl;  }    //元素的搜尋  j=hm.find("pear");  i=hm.find("apple");  cout<<"水果:"<<(*i).first<<"  "   <<"價錢:"<<(*i).second<<endl;  cout<<"**************************************************"<<endl;  if(j!=hm.end())  {   cout<<"hash_map容器的個數"<<hm.size()<<endl;  }  else  {   cout<<"雜湊表的表長:"<<hm.bucket_count()<<endl;  }  return 0; }