• 深入了解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
    ", iset.size(), iset.max_size());
    23 
    24     //依次输出
    25     printf("依次输出集合中所有元素-------
    ");
    26     for (pos = iset.begin(); pos != iset.end(); ++pos)
    27         printf("%d ", *pos);
    28     putchar('
    ');
    29 
    30     //查找
    31     int findNum = MAXN;
    32     printf("查找 %d是否存在-----------------------
    ", findNum);
    33     pos = iset.find(findNum);
    34     if (pos != iset.end())
    35         printf("%d 存在
    ", findNum);
    36     else
    37         printf("%d 不存在
    ", findNum);
    38 
    39     //在最后位置插入数据,如果给定的位置不正确,会重新找个正确的位置并返回该位置
    40     pos  = iset.insert(--iset.end(), MAXN * 2); 
    41     printf("已经插入%d
    ", *pos);
    42 
    43     //删除
    44     iset.erase(MAXN);
    45     printf("已经删除%d
    ", MAXN);
    46 
    47     //依次输出
    48     printf("依次输出集合中所有元素-------
    ");
    49     for (pos = iset.begin(); pos != iset.end(); ++pos)
    50         printf("%d ", *pos);
    51     putchar('
    ');
    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毫秒
    ", pszContainerName, pszOperator, lElapsetime);
    17 }
    18 
    19 int main()
    20 {
    21     printf("set VS hash_set 性能测试 数据容量 %d个 查询次数 %d次
    ", MAXN, MAXQUERY);
    22     const int MAXNUM = MAXN * 4;
    23     const int MAXQUERYNUM = MAXN * 4;
    24     printf("容器中数据范围 [0, %d) 查询数据范围[0, %d)
    ", 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("-----插入数据-----------
    ");
    43 
    44     clockBegin = clock();  
    45     nset.insert(a, a + MAXN); 
    46     clockEnd = clock();
    47     printf("set中有数据%d个
    ", 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个
    ", nhashset.size());
    54     PrintfContainertElapseTime("hase_set", "insert", clockEnd - clockBegin);
    55 
    56 
    57     //find
    58     printf("-----查询数据-----------
    ");
    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
    ", 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
    ", 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

  • 相关阅读:
    滚动条滚动方向
    阶乘函数-尾递归
    返回顶部
    CommonJS
    vuessr
    随机字符串
    indexedDB
    深层次选择器
    Vue3.0简单替代Vuex
    shell 学习笔记
  • 原文地址:https://www.cnblogs.com/CheeseZH/p/5176970.html
Copyright © 2020-2023  润新知