深刻了解STL中set與hash_set,hash表基礎

一,set和hash_set簡介ios

在STL中,set是以紅黑樹(RB-Tree)做爲底層數據結構的,hash_set是以哈希表(Hash table)做爲底層數據結構的。set能夠在時間複雜度爲O(logN)的狀況下插入,刪除和查找數據。hash_set操做的時間度則比較複雜,取決於哈希函數和哈希表的負載狀況。windows

二,SET使用範例(hash_set相似)數據結構

 1 #include <set>
 2 #include <ctime>
 3 #include <cstdio>
 4 using namespace std;
 5 
 6 int main()
 7 {
 8     const int MAXN = 15;
 9     int a[MAXN];
10     int i;
11     srand(time(NULL));
12     for (i = 0; i < MAXN; ++i)
13         a[i] = rand() % (MAXN * 2);
14 
15     set<int> iset;   
16     set<int>::iterator pos; 
17 
18     //插入數據 insert()有三種重載
19     iset.insert(a, a + MAXN);
20 
21     //當前集合中個數 最大容納數據量
22     printf("當前集合中個數: %d     最大容納數據量: %d\n", iset.size(), iset.max_size());
23 
24     //依次輸出
25     printf("依次輸出集合中全部元素-------\n");
26     for (pos = iset.begin(); pos != iset.end(); ++pos)
27         printf("%d ", *pos);
28     putchar('\n');
29 
30     //查找
31     int findNum = MAXN;
32     printf("查找 %d是否存在-----------------------\n", findNum);
33     pos = iset.find(findNum);
34     if (pos != iset.end())
35         printf("%d 存在\n", findNum);
36     else
37         printf("%d 不存在\n", findNum);
38 
39     //在最後位置插入數據,若是給定的位置不正確,會從新找個正確的位置並返回該位置
40     pos  = iset.insert(--iset.end(), MAXN * 2); 
41     printf("已經插入%d\n", *pos);
42 
43     //刪除
44     iset.erase(MAXN);
45     printf("已經刪除%d\n", MAXN);
46 
47     //依次輸出
48     printf("依次輸出集合中全部元素-------\n");
49     for (pos = iset.begin(); pos != iset.end(); ++pos)
50         printf("%d ", *pos);
51     putchar('\n');
52     return 0;
53 }

運行結果函數

三,SET與HASH_SET性能對比性能

 1 #include <set>
 2 #include <hash_set>
 3 #include <iostream>
 4 #include <ctime>
 5 #include <cstdio>
 6 #include <cstdlib>
 7 using namespace std;
 8 using namespace stdext;  //hash_set
 9 
10 // MAXN個數據 MAXQUERY次查詢
11 const int MAXN = 10000, MAXQUERY = 5000000;
12 int a[MAXN], query[MAXQUERY];
13 
14 void PrintfContainertElapseTime(char *pszContainerName, char *pszOperator, long lElapsetime)
15 {
16     printf("%s 的%s操做 用時 %d毫秒\n", pszContainerName, pszOperator, lElapsetime);
17 }
18 
19 int main()
20 {
21     printf("set VS hash_set 性能測試 數據容量 %d個 查詢次數 %d次\n", MAXN, MAXQUERY);
22     const int MAXNUM = MAXN * 4;
23     const int MAXQUERYNUM = MAXN * 4;
24     printf("容器中數據範圍 [0, %d) 查詢數據範圍[0, %d)\n", MAXNUM, MAXQUERYNUM);
25     
26     //隨機生成在[0, MAXNUM)範圍內的MAXN個數
27     int i;
28     srand(time(NULL));
29     for (i = 0; i < MAXN; ++i)
30         a[i] = (rand() * rand()) % MAXNUM;
31     //隨機生成在[0, MAXQUERYNUM)範圍內的MAXQUERY個數
32     srand(time(NULL));
33     for (i = 0; i < MAXQUERY; ++i)
34         query[i] = (rand() * rand()) % MAXQUERYNUM;
35 
36     set<int>       nset;
37     hash_set<int> nhashset;
38     clock_t  clockBegin, clockEnd;
39 
40 
41     //insert
42     printf("-----插入數據-----------\n");
43 
44     clockBegin = clock();  
45     nset.insert(a, a + MAXN); 
46     clockEnd = clock();
47     printf("set中有數據%d個\n", nset.size());
48     PrintfContainertElapseTime("set", "insert", clockEnd - clockBegin);
49 
50     clockBegin = clock();  
51     nhashset.insert(a, a + MAXN); 
52     clockEnd = clock();
53     printf("hash_set中有數據%d個\n", nhashset.size());
54     PrintfContainertElapseTime("hase_set", "insert", clockEnd - clockBegin);
55 
56 
57     //find
58     printf("-----查詢數據-----------\n");
59 
60     int nFindSucceedCount, nFindFailedCount; 
61     nFindSucceedCount = nFindFailedCount = 0;
62     clockBegin = clock(); 
63     for (i = 0; i < MAXQUERY; ++i)
64         if (nset.find(query[i]) != nset.end())
65             ++nFindSucceedCount;
66         else
67             ++nFindFailedCount;
68     clockEnd = clock();
69     PrintfContainertElapseTime("set", "find", clockEnd - clockBegin);
70     printf("查詢成功次數: %d    查詢失敗次數: %d\n", nFindSucceedCount, nFindFailedCount);
71     
72     nFindSucceedCount = nFindFailedCount = 0;
73     clockBegin = clock();  
74     for (i = 0; i < MAXQUERY; ++i)
75         if (nhashset.find(query[i]) != nhashset.end())
76             ++nFindSucceedCount;
77         else
78             ++nFindFailedCount;
79     clockEnd = clock();
80     PrintfContainertElapseTime("hash_set", "find", clockEnd - clockBegin);
81     printf("查詢成功次數: %d    查詢失敗次數: %d\n", nFindSucceedCount, nFindFailedCount);
82     return 0;
83 }

運行結果以下:測試

因爲查詢的失敗次數太多,此次將查詢範圍變小使用再測試下:大數據

因爲結點過多,80多萬個結點,set的紅黑樹樹高約爲19(2^19=524288,2^20=1048576),查詢起來仍是比較費時的。hash_set在時間性能上比set要好一些,而且若是查詢成功的概率比較大的話,hash_set會有更好的表現。ui

四,深刻分析hash_set編碼

1. hash tablespa

  hash_set的底層數據結構是哈希表,所以要深刻了解hash_set,必須先分析哈希表。 hash表的出現主要是爲了對內存中數據的快速、隨機的訪問。它主要有三個關鍵點:Hash表的大小、Hash函數、衝突的解決。哈希表是根據關鍵碼值(Key-Value)而直接進行訪問的數據結構,它用哈希函數處理數據獲得關鍵碼值,關鍵碼值對應表中一個特定位置再由應該位置來訪問記錄,這樣能夠在時間複雜性度爲O(1)內訪問到數據。可是頗有可能出現多個數據經哈希函數處理後獲得同一個關鍵碼——這就產生了衝突,解決衝突的方法也有不少,各大數據結構教材及考研輔導書上都會介紹大把方法。這裏採用最方便最有效的一種——鏈地址法,當有衝突發生時將具同一關鍵碼的數據組成一個鏈表。下圖展現了鏈地址法的使用:

2. 關於Hash表的大小

  Hash表的大小通常是定長的,若是太大,則浪費空間,若是過小,衝突發生的機率變大,體現不出效率。因此,選擇合適的Hash表的大小是Hash表性能的關鍵。

  對於Hash表大小的選擇一般會考慮兩點:

  第一,確保Hash表的大小是一個素數。常識告訴咱們,當除以一個素數時,會產生最分散的餘數,可能最糟糕的除法是除以2的倍數,由於這隻會屏蔽被除數中的位。因爲咱們一般使用表的大小對hash函數的結果進行模運算,若是表的大小是一個素數,就能夠得到最佳的結果。

  第二,建立大小合理的hash表。這就涉及到hash表的一個概念:裝填因子。設裝填因子爲a,則:

a=表中記錄數/hash表表長

  一般,咱們關注的是使hash表的平均查找長度最小,而平均查找長度是裝填因子的函數,而不是表長n的函數。a的取值越小,產生衝突的機會就越小,但若是a取值太小,則會形成較大的空間浪費,一般,只要a的取值合適,hash表的平均查找長度就是一個常數,即hash表的平均查找長度爲O(1)。

  固然,根據不一樣的數據量,會有不一樣的哈希表的大小。對於數據量時多時少的應用,最好的設計是使用動態可變尺寸的哈希表,那麼若是你發現哈希表尺寸過小了,好比其中的元素是哈希表尺寸的2倍時,咱們就須要擴大哈希表尺寸,通常是擴大一倍。
  下面是哈希表尺寸大小的可能取值(素數,後邊是前邊的2倍左右):
  17,            37,          79,        163,          331,   673,           1361,        2729,       5471,         10949,  21911,          43853,      87719,      175447,      350899,701819,         1403641,    2807303,     5614657, 11229331, 22458671,       44917381,    89834777,    179669557,   359339171,  718678369,      1437356741,  2147483647

  那麼C++的STL中hash_set是如何實現動態增長哈希表長度的呢?

  首先來看看VS2008中hash_set是如何實現動態的增長表的大小,hash_set是在hash_set.h中聲明的,在hash_set.h中能夠發現hash_set是繼承_Hash類的,hash_set自己並無太多的代碼,只是對_Hash做了進一步的封裝,這種作法在STL中很是常見,如stack棧和queue單向隊列都是以deque雙向隊列做底層數據結構再加一層封裝。

_Hash類的定義和實現都在xhash.h類中,微軟對_Hash類的第一句註釋以下——

        hash table -- list with vector of iterators for quick access。

  這說明_Hash實際上就是由vector和list組成哈希表。再閱讀下代碼能夠發現_Hash類增長空間由_Grow()函數完成,當空間不足時就倍增(或者近2被的素數),而且表中原有數據都要從新計算hash值以肯定新的位置。也就是從新申請一個更大的空間,同時將原來hash_set中的值逐個放到新的hash_set中。

3. 哈希函數

實際工做中需視不一樣的狀況採用不一樣的哈希函數,一般考慮的因素有:
· 計算哈希函數所需時間
· 關鍵字的長度
· 哈希表的大小
· 關鍵字的分佈狀況
· 記錄的查找頻率
1. 直接尋址法:取關鍵字或關鍵字的某個線性函數值爲散列地址。即H(key)=key或H(key) = a·key + b,其中a和b爲常數(這種散列函數叫作自身函數)。若其中H(key)中已經有值了,就往下一個找,直到H(key)中沒有值了,就放進去。
2. 數字分析法:分析一組數據,好比一組員工的出生年月日,這時咱們發現出生年月日的前幾位數字大致相同,這樣的話,出現衝突的概率就會很大,可是咱們發現年月日的後幾位表示月份和具體日期的數字差異很大,若是用後面的數字來構成散列地址,則衝突的概率會明顯下降。所以數字分析法就是找出數字的規律,儘量利用這些數據來構造衝突概率較低的散列地址。
3. 平方取中法:當沒法肯定關鍵字中哪幾位分佈較均勻時,能夠先求出關鍵字的平方值,而後按須要取平方值的中間幾位做爲哈希地址。這是由於:平方後中間幾位和關鍵字中每一位都相關,故不一樣關鍵字會以較高的機率產生不一樣的哈希地址。
例:咱們把英文字母在字母表中的位置序號做爲該英文字母的內部編碼。例如K的內部編碼爲11,E的內部編碼爲05,Y的內部編碼爲25,A的內部編碼爲01, B的內部編碼爲02。由此組成關鍵字「KEYA」的內部代碼爲11052501,同理咱們能夠獲得關鍵字「KYAB」、「AKEY」、「BKEY」的內部編碼。以後對關鍵字進行平方運算後,取出第7到第9位做爲該關鍵字哈希地址,以下圖所示
關鍵字
內部編碼
內部編碼的平方值
H(k)關鍵字的哈希地址
KEYA
11050201
122157778355001
778
KYAB
11250102
126564795010404
795
AKEY
01110525
001233265775625
265
BKEY
02110525
004454315775625
315
 
4. 摺疊法:將關鍵字分割成位數相同的幾部分,最後一部分位數能夠不一樣,而後取這幾部分的疊加和(去除進位)做爲散列地址。數位疊加能夠有移位疊加和間界疊加兩種方法。移位疊加是將分割後的每一部分的最低位對齊,而後相加;間界疊加是從一端向另外一端沿分割界來回摺疊,而後對齊相加。
5. 隨機數法:選擇一隨機函數,取關鍵字的隨機值做爲散列地址,一般用於關鍵字長度不一樣的場合。
6. 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key MOD p,p<=m。不只能夠對關鍵字直接取模,也可在摺疊、平方取中等運算以後取模。對p的選擇很重要,通常取素數或m,若p選的很差,容易產生同義詞。
 
4. 衝突處理方法
1. 開放尋址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)爲散列函數,m爲散列表長,di爲增量序列,可有下列三種取法:
  1.1. di=1,2,3,…,m-1,稱線性探測再散列;
  1.2. di=1^2,-1^2,2^2,-2^2,±⑶^2,…,±(k)^2,(k<=m/2)稱二次探測再散列;
  1.3. di=僞隨機數序列,稱僞隨機探測再散列。
2. 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不一樣的散列函數,即在同義詞產生地址衝突時計算另外一個散列函數地址,直到衝突再也不發生,這種方法不易產生「彙集」,但增長了計算時間。
3. 鏈地址法(拉鍊法)
4. 創建一個公共溢出區

 

參考文章

http://blog.csdn.net/morewindows/article/details/7029587

http://blog.csdn.net/morewindows/article/details/7330323

http://blog.csdn.net/qll125596718/article/details/6997850

http://baike.baidu.com/view/329976.htm?fromtitle=%E6%95%A3%E5%88%97%E8%A1%A8&fromid=10027933&type=syn

相關文章
相關標籤/搜索