hash_map 与map介绍

[摘要] 

  哈希表是一种高效的数据结构。本文分五个部分:首先提出了哈希表的优点,其次介绍了它的基础操作,接着从简单的例子中作了效率对比,指出其适用范围以及特点,然后通过例子说明了如何在题目中运用哈希表以及需要注意的问题,最后总结全文。

[正文]

1. 引言

  哈希表(Hash Table)的应用近两年才在NOI中出现,作为一种高效的数据结构,它正在竞赛中发挥着越来越重要的作用。 

  哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。

  哈希表又叫做散列表,分为"开散列" 和"闭散列"。考虑到竞赛时多数人通常避免使用动态存储结构,本文中的"哈希表"仅指"闭散列",关于其他方面读者可参阅其他书籍。

2. 基础操作

  2.1 基本原理

  我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素" 分类",然后将这个元素存储在相应"类"所对应的地方。

  但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。

  总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。

  2.2 函数构造

  构造函数的常用方法(下面为了叙述简洁,设 h(k) 表示关键字为 k 的元素所对应的函数值):

  a) 除余法:

  选择一个适当的正整数 p ,令 h(k ) = k mod p 

  这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。

  b) 数字选择法:

  如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。

  2.3 冲突处理

  线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。

  2.4 支持运算

  哈希表支持的运算主要有:初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素(member)。

  设插入的元素的关键字为 x ,A 为存储的数组。

  初始化比较容易,例如

  const empty=maxlongint; // 用非常大的整数代表这个位置没有存储元素

     p=9997;      // 表的大小

  procedure makenull;

   var i:integer;

   begin

    for i:=0 to p-1 do

     A[i]:=empty;

   End;

  哈希函数值的运算根据函数的不同而变化,例如除余法的一个例子:

  function h(x:longint):Integer;

   begin

    h:= x mod p;

   end;

  我们注意到,插入和查找首先都需要对这个元素定位,即如果这个元素若存在,它应该存储在什么位置,因此加入一个定位的函数 locate 

  function locate(x:longint):integer;

   var orig,i:integer;

   begin

    orig:=h(x);

    i:=0;

    while (i<S)and(A[(orig+i)mod S]<>x)and(A[(orig+i)mod S]<>empty) do

     inc(i); 

     //当这个循环停下来时,要么找到一个空的存储单元,要么找到这个元

     //素存储的单元,要么表已经满了

    locate:=(orig+i) mod S;

   end;

  插入元素

  procedure insert(x:longint);

   var posi:integer;

   begin

    posi:=locate(x);      //定位函数的返回值

    if A[posi]=empty then A[posi]:=x

          else error; //error 即为发生了错误,当然这是可以避免的

   end; 

  查找元素是否已经在表中

  procedure member(x:longint):boolean;

    var posi:integer; 

    begin

     posi:=locate(x);

     if A[posi]=x then member:=true

             else member:=false;

    end;

  这些就是建立在哈希表上的常用基本运算。

  下文提到的所有程序都能在附录中找到。

3. 效率对比

  3.1简单的例子与实验

   下面是一个比较简单的例子:

             集合 ( Subset )

问题描述:

  给定两个集合A、B,集合内的任一元素x满足1 ≤ x ≤ ,并且每个集合的元素个数不大于 个。我们希望求出A、B之间的关系。只需确定在B 中但是不在 A 中的元素的个数即可。(这个题目是根据 OIBH NOIP 2002 模拟赛 # 1 的第一题改编的。)

  分析:我们先不管A 与 B 的具体关系如何,注意到这个问题的本质就是对于给定的集合 A ,确定B 中的元素是否在 A 中。所以,我们使用哈希表来处理。至于哈希函数,只要按照除余法就行了,由于故意扩大了原题的数据规模, H(x) = x mod 15889;

  当然本题可以利用别的方法解决,所以选取了速度最快的快速排序+二分查找,让这两种方法作效率对比。

  我们假定 |A|=|B| ,对于随机生成的数据,计算程序重复运行50次所用时间。

  对比表格如下:

 哈希表(sec) 快速排序+二分查找(sec) 

复杂度 O(N) (只有忽略了冲突才是这个结果。当然实际情况会比这个大,但是重复的几率与哈希函数有关,不容易估计) O(N log N+ N) = O(N log N) 

测试数据规模 -- -- 

500 0.957 0.578 

1000 1.101 0.825 

2500 1.476 1.565 

5000 2.145 2.820 

7500 2.905 4.203 

10000 3.740 5.579 

13500 7.775 7.753 

15000 27.550 8.673 

 

  对于数据的说明:在 Celeron566 下用 TP 测试,为了使时间的差距明显,让程序重复运了行50次。同时哈希表中的P= 15889 ,下标范围 0..15888 。由于快速排序不稳定,因此使用了随机数据。

  3.2 对试验结果的分析:

  注意到两个程序的用时并不像我们期望的那样,总是哈希表快。设哈希表的大小为 P .

  首先,当规模比较小的时候(大约为a< 10% * P,这个数据仅仅是通过若干数据估记出来的,没有严格证明,下同),第二种方法比哈希表快。这是由于,虽然每次计算哈希函数用O(1) 的时间,但是这个系数比较大。例如这道题的 H(x)=x mod 15589 ,通过与做同样次数的加法相比较,测试发现系数 > 12 ,因为 mod 运算本身与快速排序的比较大小和交换元素运算相比,比较费时间。所以规模小的时候,O(N)(忽略冲突)的算法反而不如 O(NlogN)。这一点在更复杂的哈希函数上会体现的更明显,因为更复杂的函数系数会更大。

  其次,当规模稍大 (大约为 15%*P < a < 85%*P) 的时候,很明显哈希表的效率高。这是因为冲突的次数较少。

  再次,当规模再大 (大约为 90%*P < a < P )的时候,哈希表的效率大幅下降。这是因为冲突的次数大大提高了,为了解决冲突,程序不得不遍历一段都存储了元素的数组空间来寻找空位置。用白箱测试的方法统计,当规模为13500的时候,为了找空位置,线性重新散列平均做了150000 次运算;而当规模为15000 的时候,平均竟然高达2000000 次运算,某些数据甚至能达到4265833次。显然浪费这么多次运算来解决冲突是不合算的,解决这个问题可以扩大表的规模,或者使用"开散列"(尽管它是动态数据结构)。然而需要指出的是,冲突是不可避免的。

  初步结论:

  当数据规模接近哈希表上界或者下界的时候,哈希表完全不能够体现高效的特点,甚至还不如一般算法。但是如果规模在中央,它高效的特点可以充分体现。我们可以从图像直观的观察到这一点。

 

 

 

 

  试验表明当元素充满哈希表的 90% 的时候,效率就已经开始明显下降。这就给了我们提示:如果确定使用哈希表,应该尽量使数组开大(由于竞赛中可利用内存越来越多,大数组通常不是问题,当然也有少数情况例外),但对最太大的数组进行操作也比较费时间,需要找到一个平衡点。通常使它的容量至少是题目最大需求的 120% ,效果比较好(这个仅仅是经验,没有严格证明)。

4. 应用举例

  4.1 应用的简单原则

  什么时候适合应用哈希表呢?如果发现解决这个问题时经常要询问:"某个元素是否在已知集合中?",也就是需要高效的数据存储和查找,则使用哈希表是最好不过的了!那么,在应用哈希表的过程中,值得注意的是什么呢?

  哈希函数的设计很重要。一个不好的哈希函数,就是指造成很多冲突的情况,从前面的例子已经可以看出来,解决冲突会浪费掉大量时间,因此我们的目标就是尽力避免冲突。前面提到,在使用"除余法"的时候,h(k)=k mod p ,p 最好是一个大素数。这就是为了尽力避免冲突。为什么呢?假设 p=1000 ,则哈希函数分类的标准实际上就变成了按照末三位数分类,这样最多1000类,冲突会很多。一般地说,如果 p 的约数越多,那么冲突的几率就越大。

  简单的证明:假设 p 是一个有较多约数的数,同时在数据中存在 q 满足 gcd(p,q)=d >1 ,即有 p=a*d , q=b*d, 则有 q mod p= q - p* [q div p] =q - p*[b div a] . ① 其中 [b div a ] 的取值范围是不会超过 [0,b] 的正整数。也就是说, [b div a] 的值只有 b+1 种可能,而 p 是一个预先确定的数。因此 ① 式的值就只有 b+1 种可能了。这样,虽然mod 运算之后的余数仍然在 [0,p-1] 内,但是它的取值仅限于 ① 可能取到的那些值。也就是说余数的分布变得不均匀了。容易看出, p 的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率越高。而素数的约数是最少的,因此我们选用大素数。记住"素数是我们的得力助手"。

  另一方面,一味的追求低冲突率也不好。理论上,是可以设计出一个几乎完美,几乎没有冲突的函数的。然而,这样做显然不值得,因为这样的函数设计很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还不如用一个虽然冲突多一些但是编码简单的函数。因此,函数还需要易于编码,即易于实现。

  综上所述,设计一个好的哈希函数是很关键的。而"好"的标准,就是较低的冲突率和易于实现。

  另外,使用哈希表并不是记住了前面的基本操作就能以不变应万变的。有的时候,需要按照题目的要求对哈希表的结构作一些改进。往往一些简单的改进就可以带来巨大的方便。

  这些只是一般原则,真正遇到试题的时候实际情况千变万化,需要具体问题具体分析才行。下面,我们看几个例子,看看这些原则是如何体现的。

4.2 有关字符串的例子

  我们经常会遇到处理字符串的问题,下面我们来看这个例子:

                找名字

问题描述:

  给定一个全部由字符串组成的字典,字符串全部由大写字母构成。其中为每个字符串编写密码,编写的方式是对于 n 位字符串,给定一个 n 位数,大写字母与数字的对应方式按照电话键盘的方式:

      2: A,B,C   5: J,K,L   8: T,U,V

      3: D,E,F   6: M,N,O   9: W,X,Y

      4: G,H,I   7: P,R,S

  题目给出一个 1--12 位的数,找出在字典中出现且密码是这个数的所有字符串。字典中字符串的个数不超过 8000 。(这个是 USACO Training Gate 1.2.4 的一道题。)

  分析:看懂题目之后,对于给定的编码,只需要一个回溯的过程,所有可能的原字符串都可以被列举出来,剩下的就是检查这个字符串是否在给定的字典中了。所以这个问题需要的还是"某个元素是否在已知集合中?"由于给出的"姓名"都是字符串,因此我们可以利用字符的 ASCII 码。那么,如何设计这个哈希函数呢?注意到题目给出的字典中,最多能有5000 个不同元素,而一个字符的 ASCII 码只能有26 种不同的取值,因此至少需要用在3个位置上的字符(26^3 > 5000,但是 26^2 < 5000 ),于是我们就选取3个位置上的字符。由于给定的字符串的长度从 1--12 都有可能,为了容易实现,选取最开始的 1个字符,和最末尾的2个字符。让这3个字符组成27进制的3位数,则这个数的值就是这个字符串的编码。这样哈希函数就设计出来了!

  不过,由于可能出现只有1位的字符串,在写函数代码的时候需要特殊考虑;大素数选取 13883 。

  这个函数是这样的:

  function hash(s:string):integer;

    var i,tmp:longint;

    begin

     tmp:=0; {用来记录27进制数的值}

     if length(s)>1 then begin

     tmp:=tmp*27+ord(s[1])-64;

     for i:=1 downto 0 do

      tmp:=tmp*27+ord(s[length(s)-i])-64; {取第一位和后两位}

             end

            else for i:=1 to 3 do

             tmp:=tmp*27+ord(s[1])-64;{当长度为1的时候特殊处理}

     hash:=tmp mod 13883;

    end;

  值得指出的是,本题给出的字符串大都没有什么规律,用哈希表可以做到近似"平均",但是对于大多数情况,字符串是有规律的(例如英文单词),这个时候用哈希表反而不好(例如英语中有很多以 con 开头的单词),通常用检索树解决这样的查找问题。

4.3 在广度优先搜索中应用的例子

  在广度优先搜索中,一个通用而且有效的剪枝就是在拓展节点之前先判重。而判重的本质也是数据的存储与查找,因此哈希表大有用武之地。来看下面的例子:

                 转花盆

题意描述: 

    给定两个正6边形的花坛,要求求出从第一个变化到第二个的最小操作次数以及操作方式。一次操作是:选定不在边上的一盆花,将这盆花周围的6盆花按照顺时针或者逆时针的顺序依次移动一个单位。限定一个花坛里摆放的不同种类的花不超过3 种,对于任意两种花,数量多的花的盆数至少是数量少的花的2倍 。(这是 SGOI-8 的一道题)

  分析:首先确定本题可以用广度优先搜索处理,然后来看问题的规模。正6边形共有19个格子可以用来放花,而且根据最后一句限定条件,至多只能存在 C(2,19) * C(5,17) = 1058148 种状态,用搜索完全可行。然而操作的时候,可以预料产生的重复节点是相当多的,需要迅速判重才能在限定时间内出解,因此想到了哈希表。那么这个哈希函数如何设计呢?注意到19个格子组成6 边形是有顺序的,而且每一个格子只有3种可能情况,那么用3进制19位数最大 3^20-1=3486784400 用 Cardinal 完全可以承受。于是我们将每一个状态与一个整数对应起来,使用除余法就可以了。

4.4 小结

  从这两个例子可以发现,对于字符串的查找,哈希表虽然不是最好的方法,但是每个字符都有"天生"的 ASCII 码,在设计哈希函数的时候可以直接利用。而其他方法,例如利用检索树的查找,编写代码不如哈希表简洁。至于广度优先搜索中的判重更是直接利用了哈希表的特点。

  另外,我们看到这两个题目都是设计好哈希函数之后,直接利用前面的基本操作就可以了,因此重点应该是在哈希函数的设计上(尽管这两个例子的设计都很简单),需要注意题目本身可以利用的条件,以及估计值域的范围。下面我们看两个需要在哈希表基础上作一些变化的例子。

4.5 需要微小变化的例子

  下面,我们来分析一道 NOI 的试题:

              方程的解数

问题描述

  已知一个n元高次方程:

 

  其中:是未知数,是系数,是指数。且方程中的所有数均为整数。

  假设未知数1≤ ≤M, i=1,,,n,求这个方程的整数解的个数。 

约束条件

  1≤n≤6;1≤M≤150;

 

  方程的整数解的个数小于。

  本题中,指数Pi(i=1,2,……,n)均为正整数。

  这个是 NOI 2001 的第二试中的《方程的解数》。

  分析:初看此题,题目要求出给定的方程解的个数,这个方程在最坏的情况下可以有6个未知数,而且次数由输入决定。这样就不能利用数学方法直接求出解的个数,而且注意到解的范围最多150个数,因此恐怕只能使用枚举法了。最简单的思路是穷举所有未知数的取值,这样时间复杂度是 O(M^6) ,无法承受。因此我们需要寻找更好的方法,自然想到能否缩小枚举的范围呢?但是发现这样也有很大的困难。我们再次注意到M 的范围,若想不超时,似乎算法的复杂度上限应该是 O(M^3) 左右,这是因为 150^3 < 10000000 。这就启示我们能否仅仅通过枚举3个未知数的值来找到答案呢?如果这样,前一半式子的值 S 可以确定,这时只要枚举后3 个数的值,检查他们的和是否等于 -S 即可。这样只相当于在 O(M^3) 前面加了一个系数,当然还需要预先算出 1 到 150 的各个幂次的值。想到了这里,问题就是如何迅速的找到某个 S 是否曾经出现过,以及出现过了多少次,于是又变成了"某个元素是否在给定集合中"这个问题。所以,我们还是使用哈希表解决这个问题。至于哈希函数不是问题,还是把 S 的值作为关键字使用除余法即可。然而有一点需要注意,这个例子我们不仅需要纪录某个 S 是否出现,出现的次数也很重要,所以可以用一个2维数组,仅仅是加了一个存储出现次数的域而已。 

  Var 

  e:array[0..max-1,1..2]of longint; {e[x,1] 记录哈希函数值为 x 的 S 值, e[x,2] 记录这个 S 值出现了几次}

  因此 insert 过程也需要一些变化:

  procedure ins(x:longint);

   var posi:longint;

   begin

    posi:=locate(x);

    e[posi,1]:=x;

    inc(e[posi,2]); {仅仅这一条语句,就可以记录下来 S 出现了几次}

   end;

4.6 最后一个例子

  下面我们来仔细分析下面这个问题:

               迷宫的墙 

题意描述:

  神话中byte山边有一个井之迷宫。迷宫的入口在山顶。迷宫中有许多房间,每个的颜色是以下之一:红、绿、蓝。两个相同颜色的房间看起来相似而不可区分。每个房间里有三口井标以1,2,3。从一个房间到另一间只有一种方式:从上面房间的井里跳到(不一定竖直地)井底的房间。可以从入口房间到达任何其他房间。迷宫中的所有通路走向坐落在最底部的龙宫。所有的迷宫之旅对应了一系列在相继访问的房间里选择的井的标号。这一列数称为一个旅行计划。一个走过好几次迷宫的英雄bytezar画好了图,然而有的房间重复出现了多次。

输入:

  第一行有一个整数n,2<=n<=6000,房间数(包括龙宫)。房间从1到n标号,较大编号的房间再较低处(入口房间编号1,龙宫编号n)。接下的n-1行描述迷宫的房间(除了龙宫)和井。每行有一个字母,一个空格,和三个由空格分隔的整数。字母代表了房间的颜色(C--红,Z--绿,N--蓝),第i(i=1,2,3)个数是第i个井通往的房间号。

输出:

迷宫最少的房间数目

  这是 IOI 2003 中国国家集训队难题讨论活动的 0020 题。

  分析:题目的意思是给出这个迷宫的地图,去掉重复出现的房间,找出这个迷宫的最少房间数目。于是关键就是确定什么样的房间是重复的。通过对样例的分析,可以看出这样的房间是重复的:如果两个房间 i 和 j (1<=i,j & lt;=n-1),他们的颜色相同,而且第 k (k=1,2,3) 堵墙通向的房间或者相同、或者重复。因为这样从 i 和 j 可到达的房间是完全相同的。

  所以,我们只需要记录下每个房间的情况和已经被确定相同的房间,然后挨个比较即可。于是又需要用到高效的数据存储与查找,自然想到哈希表。然而,这里面需要对哈希表作更大的改进:首先每个房间只能是3种颜色之一,因此针对每种颜色分别建立哈希表,可以使哈希函数的自变量减少一个;其次还需要纪录每个不重复的房间每堵墙都通向哪个房间,还有哪些房间是重复的。

  具体这样实现:

  var

  e:array[0..2,0..p-1,1..4]of longint; {0..2 代表共有3种颜色,0..p-1 是哈希函数的值域,而 1..4 中的 1..3 表示三堵墙连接到那个房间,4 表示这个单元存储的是哪个节点}

  r:array[1..maxn]of longint; {r[i] 表示与 i 相同的节点。如果有多个节点都是相同的,择取其中最大的(这一点不需要特殊的操作,只要在处理节点的时候注意就行了)}

  至于哈希函数,最开始我是随意的写了一个(因为越是随意的,就越是随机的!),定位函数是这样的:

  function locate(var a,b,c,d:longint):longint;

    var t:longint;

       i:integer;

    begin

     t:=r[b]*10037+r[c]*5953+r[d]*2999; {用3堵墙的值任意乘大素数相加再取余数,使得结果分布比较随机,也就比较均匀}

     t:=t mod p;

     i:=0;

     while (e[a,(t+i)mod p,1]<>0)and(e[a,(t+i)mod p,1]<>r[b]) do

      if (e[a,(t+i)mod p,2]<>r[c])or(e[a,(t+i)mod p,3]<>r[d]) then inc(i); {线性重新散列}

     locate:=(t+i)mod p;

    end;

  但是后来发现完全没有必要这样做,这样的哈希函数在计算 t 的时候浪费了很多时间(不过数据规模不是很大,所以这点不十分明显),而且素数起到的作用也不应当是这样的。其实让 r[b],r[c],r[d] 组成 n 进制数就完全能够达到目的了,加入了素数不仅是小规模数据计算浪费时间,对大数据最后结果的分布平均也没有起到比 n 进制数更多的作用。因此改为

  t:=r[b]*sqr(n)+r[c]*n+r[d];

  当然肯定会有更好的哈希函数的。

4.7 小结

  第一个例子,乍一看与哈希表毫无关系;第二个例子叙述比较复杂,但是经过仔细分析,发现问题的本质都是确定"某个元素是否在给定集合中",这正是哈希表的特点。所以,不论题目的表面看起来如何,只要本质是需要高效的数据检索,哈希表通常就是最好的选择!

  另外,这两个例子都在原来哈希表的基础上作了一些变化。第一个例子加入了纪录某个值出现次数的域,第二个例子加入了纪录所有墙的情况以及原节点编号的域。虽然都只是很小的变化,但是却给问题的解决带来了不小的方便。因此我们得到提示:哈希表虽然有标准的操作,但也不是一成不变的,需要具体问题具体分析,根据题目的要求和特点作出相应变化。

5. 总结

  本文介绍了有关哈希表方面的内容,分析了它的特点和优点,指出了应用需要注意的问题,并且重点举了几个例子来说明它在竞赛中的应用。希望读者读完本文能够对哈希表有更全面的了解,并能在竞赛中应用自如!

 

转自:http://blog.chinaunix.net/u2/82382/showart.php?id=2072959

C++ STL中哈希表 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进行映射到不同区域(桶)进行保存。其插入过程是:


得到key 
通过hash函数得到hash值 
得到桶号(一般都为hash值对桶数求模) 
存放key和value在桶内。 

其取值过程是:


得到key 
通过hash函数得到hash值 
得到桶号(一般都为hash值对桶数求模) 
比较桶的内部元素是否与key相等,若都不相等,则没有找到。 
取出相等的记录的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());
              }
      };


在声明自己的哈希函数时要注意以下几点:


使用struct,然后重载operator(). 
返回是size_t 
参数是你要hash的key的类型。 
函数是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 ,这里主要介绍几个常用函数。


hash_map(size_type n) 如果讲究效率,这个参数是必须要设置的。n 主要用来设置hash_map 容器中hash桶的个数。桶个数越多,hash函数发生冲突的概率就越小,重新申请内存的概率就越小。n越大,效率越高,但是内存消耗也越大。 
const_iterator find(const key_type& k) const. 用查找,输入为键值,返回为迭代器。 
data_type& operator[](const key_type& k) . 这是我最常用的一个函数。因为其特别方便,可像使用数组一样使用。不过需要注意的是,当你使用[key ]操作符时,如果容器中没有key元素,这就相当于自动增加了一个key元素。因此当你只是想知道容器中是否有key元素时,你可以使用find。如果你希望插入该元素时,你可以直接使用[]操作符。 
insert 函数。在容器中不包含key值时,insert函数和[]操作符的功能差不多。但是当容器中元素越来越多,每个桶中的元素会增加,为了保证效率,hash_map会自动申请更大的内存,以生成更多的桶。因此在insert以后,以前的iterator有可能是不可用的。 
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的构造速度较慢。

现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用。

这里还有个关于hash_map和map的小故事,看看:http://dev.csdn.net/Develop/article/14/14019.shtm


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<<hmap[a1]<<endl;
              cout<<hmap[a2]<<endl;
              return       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的实现。我就遇到了这样的例子。因此在使用这些非标准库的时候,一定要事先测试。另外,如果考虑到平台移植,还是少用为佳

展开阅读全文

maphash_map 性能比较

07-26

由于工作需要,最近不得不关注 map 与 hash_map 的性能,于是花了两天的功夫验证了一下,下面把验证的结果贴出来,给大家看看,希望对大家有帮助。rnrn我主要对 map 和 hash_map 的插入、查询、遍历、清除(也就是清空)作了对比。rnrn先来看一下我的源程序(可以直接在Windows下编译):rn[code=C/C++]#include rn#include rn#include rn#include rn#include rn#include rn#include rn#include rnrnusing namespace std;rnrn#define PACKETS_COUNT 2000000rnrn// Struct of response packetrntypedef struct _PACKET_RESPONSE_rnrn int packetNum; // The num of current packetrn int count404; // The count of 404 Not Foundrn clock_t clockTick; // The clock tick of the packet that start at the process openedrnPACKET_RESPONSE;rnrntypedef map RESPONSE_HASH_MAP;rnrnPACKET_RESPONSE **response = NULL;rnrn// 插入rnint insert(RESPONSE_HASH_MAP &responseHashMap)rnrn for (int i=0; icount404 = i;rn newPacketResponse->packetNum = i;rn newPacketResponse->clockTick = clock();rnrn responseHashMap.insert(RESPONSE_HASH_MAP::value_type(i, newPacketResponse));rn rnrn cout << "OK!" << endl;rn return 0;rnrnrn// 查找rnint find(RESPONSE_HASH_MAP::iterator &iter, RESPONSE_HASH_MAP *responseHashMap, int key)rnrn iter = responseHashMap->find(key);rn if (responseHashMap->end() == iter)rn rn cout << "not find!" << endl;rn return -1;rn rnrn cout << "find OK !\nKey: " << key << "\ncount404: " << iter->second->count404 << "\npacketNum: " << iter->second->packetNum << "\nclockTick: " << iter->second->clockTick << endl;rnrnrn// 遍历rnint visit(RESPONSE_HASH_MAP &responseHashMap)rnrn int MaxPacketNum = 0;rn RESPONSE_HASH_MAP::iterator iter = responseHashMap.begin();rn while (responseHashMap.end() != iter)rn rn if (MaxPacketNum < iter->second->packetNum)rn rn MaxPacketNum = iter->second->packetNum;rn rn ++iter;rn rn cout << "MaxPacketNum is: " << MaxPacketNum << endl;rn return 0;rnrnrn// 清理rnint clear(RESPONSE_HASH_MAP &responseHashMap)rnrn if (0 != responseHashMap.size())rn rn responseHashMap.clear();rn rnrn return 0;rnrnrnDWORD getCpuUsage()rn rnrn MEMORYSTATUS ms;rn ::GlobalMemoryStatus(&ms);rnrn return ms.dwMemoryLoad;rn rnrnint main()rnrn response = new PACKET_RESPONSE*[PACKETS_COUNT];rn clock_t t = clock();rn for (int i=0; i clock is:" << clock()-t << endl;rnrn RESPONSE_HASH_MAP responseHashMap;rn RESPONSE_HASH_MAP::iterator iter = responseHashMap.begin();rn DWORD dwStart = getCpuUsage();rnrn clock_t start = clock();rn insert(responseHashMap);rn cout << "====> insert clock is: " << clock()-start << " ms" << endl;rn DWORD dwInsert = getCpuUsage();rn cout << "insert 内存使用率为: " << dwInsert-dwStart << "%" << endl;rnrn RESPONSE_HASH_MAP::iterator it = responseHashMap.begin();rn clock_t eraseStart = clock();rn for (int i=0; i<1000; i++)rn rn PACKET_RESPONSE *newNode = new PACKET_RESPONSE;rn newNode->packetNum = PACKETS_COUNT + i;rn newNode->count404 = PACKETS_COUNT + i;rn newNode->clockTick = clock();rn responseHashMap.insert(RESPONSE_HASH_MAP::value_type(PACKETS_COUNT + i, newNode));rnrn it = responseHashMap.erase(it);rn rn cout << "=====> erase clock is: " << clock()-eraseStart << " ms" << endl;rn clock_t findStart = clock();rn find(iter, &responseHashMap, PACKETS_COUNT-1);rn cout << "====> find clock is: " << clock()-findStart << " ms" << endl;rn DWORD dwFind = getCpuUsage();rn cout << "find 内存使用率为: " << dwFind - dwInsert << "%" << endl;rnrn clock_t visitStart = clock();rn visit(responseHashMap);rn cout << "====> visit clock is: " << clock()-visitStart << " ms" << endl;rn DWORD dwVisit = getCpuUsage();rn cout << "visit 内存使用率为:" << dwVisit << "%" << endl;rnrn clock_t clearStart = clock();rn clear(responseHashMap);rn cout << "======> clear clock is: " << clock()-clearStart << " ms" << endl;rn DWORD dwClear = getCpuUsage();rn cout << "clear 内存使用率为:" << dwClear << "%" << endl;rnrn return 0;rn[/code]rnrn 由于我是验证 map 与 hash_map 的性能,所以有些变量的命名是按照之前 hash_map 来命名的,不要觉得奇怪。rnrnrnrn我把验证的结果贴出来。rnrnrn -- 自己管理内存。rn 插入 查询 遍历rn200条 4ms 2ms 1ms hash_maprn 2ms 2ms 1ms maprnrn2000条 33ms 3ms 3ms hash_maprn 17ms 2ms 3ms maprnrn20000条 400ms 2ms 21ms hash_maprn 178ms 2ms 27ms maprnrn200000条 10s 3ms 185ms hash_maprn 2s 3ms 250ms maprnrn2000000条 8min 3ms 1.8s hash_maprn 22s 4ms 2.6s maprnrnrn -- 把内存交给 map 或者 hash_maprn 插入 查询 遍历rn200条 6ms 2ms 1ms hash_maprn 2ms 2ms 1ms maprnrn2000条 33ms 3ms 3ms hash_maprn 17ms 2ms 3ms maprnrn20000条 400ms 3ms 21ms hash_maprn 178ms 2ms 27ms maprnrn200000条 10s 3ms 185ms hash_maprn 2s 3ms 250ms maprnrn2000000条 9min 3ms 1.7s hash_maprn 22s 3ms 2.5s maprnrnrnrn -- value 类型为内置类型rn 插入 查询 遍历rn200条 6ms 1ms 1ms hash_maprn 2ms 2ms 1ms maprnrn2000条 34ms 1ms 3ms hash_maprn 17ms 1ms 3ms maprnrn20000条 400ms 1ms 22ms hash_maprn 175ms 2ms 27ms maprnrn200000条 10s 1ms 186ms hash_maprn 2s 1ms 249ms maprnrn2000000条 9min 1ms 1.8s hash_maprn 22s 2ms 2.5s maprnrnrn --自己管理内存 批量申请内存rn 插入 查询 遍历 清除 rn200条 4ms 2ms 1ms 1ms hash_maprn 2ms 2ms 1ms 0ms maprnrn2000条 31ms 2ms 3ms 34ms hash_maprn 15ms 3ms 4ms 1ms maprnrn20000条 400ms 3ms 21ms 1.3s hash_maprn 186ms 3ms 35ms 10ms maprnrn200000条 10s 2ms 183ms 74s hash_maprn 2.1s 4ms 288ms 110ms maprnrn2000000条 8min 2ms 1.7s 1h hash_maprn 22s 3ms 2.5s 900ms maprnrnrn我是根据 value 的类型不同而作的验证。从结果中可以看出,map 的插入、清除性能远比 hash_map 的要高,尤其是清除,大家看最后一个,清除 200W 条的记录时,map 只花了900ms,而 hash_map 却花了一个小时,我开始还以为程序有问题,后来我花了几个小时验证了好几遍,都是这样。而 hash_map 的遍历性能比 map 要高。如果你使用的时候频繁插入或者频繁清除的话,就选择 map,如果你频繁的遍历那就建议使用 hash_map。还有,这个跟 value 的类型没多少关系,我开始在网上看有些人说 map 对内置类型的处理性能比 hash_map 高,而对于自定义类型的处理 hash_map 要高,事实证明,不是这样的。rnrnrnrn后来我怀疑是 new 操作影响了性能,所以我用了一个批量申请内存,但是从验证的结果来看,这个对性能没多大的影响。如果你要自己管理内存的话,我建议还是批量申请内存好。如果你不想自己管理内存,那就把内存管理交给 map 或者 hash_map,它们有着一套非常不错的内存管理机制,当它们释放完内存后,并不是交给 ISO,而是留着自己下次使用。这个就是我上面所列出的情况,是将自定义的结构体直接插入 map 或 hash_map中。rnrnrnrn我上面的代码是使用了批量申请内存,然后自己管理内存的。如果大家想看看其它的情况的话,可以自己改下代码。rnrnrnrn希望对大家有所帮助,还有如果有人觉得哪里不对的话可以告诉我,大家共同进步。 论坛

没有更多推荐了,返回首页