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


一,set和hash_set簡介

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

二,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會有更好的表現。

四,深入分析hash_set

1. hash table

  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


免責聲明!

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



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