当前位置:   article > 正文

哈希表算法详解

哈希表算法

哈希表

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组叫做哈希表

给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

哈希表的存储结构一般有两种,一个是开放寻址法,一个是拉链法。这两种存储结构在处理冲突上效果比较好。

哈希表用途

哈希表可以把一个比较庞大的空间映射到一个比较小的空间,一般情况下是映射到0~N,N一般是1e5,1e6的大小。比较常见的一种场景是把0-1e9的这些数映射到0-1e5。我们这里主要针对哈希表在算法上的用途,然后举一个案例来详细说明,不会讲解哈希表在数据结构上的实现。

哈希表冲突

上面说了什么是哈希函数,比如值域在-1e9~1e9的数,通过一个函数比如H(x)可以把这个值域中的数映射到0~1e5的之间的一个数。这个H(x)就是哈希函数。

哈希函数一般可以写成x mod 1e5这样的形式,这样对1e5取模后的范围一定在0~1e5,但是这样写可能会有冲突,可能有两个不一样的数映射到一个数,因为把一个庞大的空间映射到小空间肯定是映射不完的,一定会有冲突。所以我们要处理冲突,按照不同的处理方式我们把哈希表分成开放寻址发和拉链法。

拉链法

我们开一个一维数组来存储所有数的哈希值,比如我们映射到0~1e5的空间,那么我们就创建一个1e5大小的数组。拉链法是如何处理冲突的呢,我们给这个数组的上的每个位置都拉一条链,用来存储当前这个位置已经有的所有的数

我们举个栗子,假设H(x)是哈希函数,我们给每一个位置都拉一条链,如果H(13)=4,这样就把13映射到了4这个位置,我们把13放到4位置的链上去,如果H(34)=4,我们也把34放到4位置的链上去,如图所示。就是说两个数是冲突的,我们就用一个链把他们都存下来。

哈希表是一个期望算法,每个链可以看做一个常数,它的时间复杂度可以看做O(1)

这里的链我们用邻接表实现

开放寻址法

开放寻址法处理冲突的思路是什么呢,首先它是只开了一个一维数组,没有开一个链表。但是这个一维数组的长度要开到题目要求的两到三倍。它处理冲突的思路是如果某个数的哈希值这个位置被占了那么就去下一个位置,一直找到空的位置,这样就要给每一个哈希值留一定的空间来放置他们,这也是为什么开放寻址法要开这么大数组的原因。

下面我们用一个例题,分别用拉链法和开放寻址法解决 

例题

维护一个集合,支持如下几种操作:

  1. I x,插入一个数 x;
  2. Q x,询问数 x 是否在集合中出现过;

现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤N≤1e5
−1e9≤x≤1e9

输入样例:

  1. 5
  2. I 1
  3. I 2
  4. I 3
  5. Q 2
  6. Q 5

输出样例:

  1. Yes
  2. No

拉链法

  1. #include <iostream>
  2. #include <cmath>
  3. #include <cstring>
  4. using namespace std;
  5. const int N=1e5+10;
  6. int h[N];//映射到的槽
  7. int e[N],ne[N],idx;//邻接表模板
  8. //插入一个数
  9. void insert(int x)
  10. {
  11. int k=abs(x%N);
  12. //邻接表模板,头插法
  13. e[idx]=x;
  14. ne[idx]=h[k];
  15. h[k]=idx++;
  16. }
  17. //查询一个数
  18. bool find(int x)
  19. {
  20. int k=abs(x%N);
  21. for(int i=h[k];i!=-1;i=ne[i])
  22. {
  23. if(x==e[i])
  24. return true;
  25. }
  26. return false;
  27. }
  28. int main()
  29. {
  30. int n;
  31. cin>>n;
  32. //初始化槽,每个槽都是一个头结点
  33. memset(h,-1,sizeof h);
  34. for(int i=0;i<n;i++)
  35. {
  36. char op;
  37. int x;
  38. cin>>op>>x;
  39. if(op=='I')
  40. insert(x);
  41. else
  42. {
  43. if(find(x)) cout<<"Yes"<<endl;
  44. else
  45. cout<<"No"<<endl;
  46. }
  47. }
  48. return 0;
  49. }

开放寻址法

  1. #include <iostream>
  2. #include <cstring>
  3. #include <cmath>
  4. using namespace std;
  5. //开放寻址法一般开数据范围的 2~3倍, 这样大概率就没有冲突了
  6. const int N=2e5+3,null=0x3f3f3f3f;//N为大于范围的第一个质数,规定空指针为0x3f3f3f3f
  7. int h[N];
  8. //find函数可以返回要插入的位置k和查找某个数在h数组的位置
  9. int find(int x)
  10. {
  11. int k=abs(x%N);
  12. //如果这个位置为空的话,返回位置k(插入)
  13. //如果这个位置的h[k]等于这个数,说明找到这个数的位置返回位置k(查找)
  14. while(h[k]!=null&&h[k]!=x)
  15. {
  16. k++;
  17. if(k==N) k=0;
  18. }
  19. return k;
  20. }
  21. int main()
  22. {
  23. int n;
  24. cin>>n;
  25. //memset按一个字节读,h是int类型,一个字节是0x3f,四个字节就是0x3f3f3f3f,所以h数组的每个数都是0x3f3f3f3f
  26. memset(h,0x3f,sizeof h);
  27. while(n--)
  28. {
  29. string op;
  30. cin>>op;
  31. int x;
  32. cin>>x;
  33. if(op=="I")
  34. h[find(x)]=x;//先找到插入的位置,然后再更新h数组的值
  35. else
  36. {
  37. if(h[find(x)]!=null) cout<<"Yes\n";//如果这个位置的h数组的值为null,说明找不到这个数,输入no
  38. else
  39. cout<<"No\n";
  40. }
  41. }
  42. return 0;
  43. }

也可以用stl写,但是理解了原理是最好的

  1. #include <iostream>
  2. #include <unordered_map>
  3. using namespace std;
  4. unordered_map<int,int>m;
  5. int main()
  6. {
  7. int n;
  8. cin>>n;
  9. while(n--)
  10. {
  11. char op;
  12. cin>>op;
  13. int x;
  14. cin>>x;
  15. if(op=='I')
  16. {
  17. m[x]++;
  18. }
  19. else
  20. {
  21. if(m[x]>0)
  22. cout<<"Yes\n";
  23. else
  24. cout<<"No\n";
  25. }
  26. }
  27. return 0;
  28. }

如有错漏之处,敬请指正!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/678444
推荐阅读
相关标签
  

闽ICP备14008679号