哈希表模拟

认识(哈希表/散列表)

哈希表,这个词若是对学过Java集合的人来说,是再熟悉不过的了,一般从数据库力获取的一切属性和值的映射都可用用hash表来存储,被称为万能的HashMap.在C++的STL里有Map,Set,unorderd_map ,unorderd_set,multmap,multset.
区别:有序和无序(map和unordered_map)

  • map和unordered_map都是关联容器,用于存储键值对。但是它们的区别在于:

  1. map中的元素是有序的,默认按键值升序排列。unordered_map中的元素是无序的。
  2. map底层用红黑树实现,查找、插入、删除的时间复杂度平均为O(logN)。unordered_map底层用哈希表实现,时间复杂度平均为O(1)。
  3. map的迭代器是双向迭代器,unordered_map的迭代器是前向迭代器.
    所以主要区别在于有序与无序,以及时间复杂度。选择使用哪个容器取决于需要。
    关于排序规则:
  • map中的键值排序规则取决于键的类型和比较运算符<的定义。通常会按键升序排列,但也可以自定义比较函数改变排序规则。值的排序是依赖键的排序,值不直接参与排序。
  • unordered_map中元素排序规则是hash函数的输出,元素位置取决于hash值,所以可以看作无序的。
    例如:

#include <map>
#include <unordered_map>
#include <string>

int main() {
    // map, keyed on strings, ascending order
    std::map<std::string, int> map = {{"b", 2}, {"a", 1}, {"c", 3}};
    
    // unordered_map, keyed on strings
    std::unordered_map<std::string, int> umap = {{"b", 2}, {"a", 1}, {"c", 3}};

    // map iterates in order
    for (auto& p : map) {
        std::cout << p.first << " " << p.second << "\n";
    }
    //因为键是字符,按字典序输出
    // a 1
    // b 2 
    // c 3

    // unordered_map iterates in arbitrary order
    for (auto& p : umap) {
        std::cout << p.first << " " << p.second << "\n";
    }
    // c 3
    // a 1 
    // b 2 
}

模拟哈希表

这只在C语言写算法时会用到,因为C++大家会普遍选择STL中的容器.
实现:

  • 插入的数唯一
    一般用取模的方式,把一个插入的数映射到数组中的某一位置.需要解决冲突.
    两种解决映射冲突的方式
  • 开放寻址法

设置一个N个大小的数组,数组通常为最大可能插入的数的2到3倍且是一个素数,若插入的数的范围是1E5,则 N = 2E5+3.这种方法不能避免冲突,只是最大可能的减少冲突.
把插入的数存放在数组里,每次映射的位置 为 ((val%N)+N)%N.若该位置的值不是设定的标记值则说明该位置已经被别的值映射过了,于是向后找,因为数组足够大,若不存在总能找到一个未被映射过的位置,要么就是返回已经存在的位置.
为什么不用 (val+N)%N?

(val+N)%N 与 ((val%N)+N)%N 效果上是等价的,都可以将val映射到0到N-1的范围内。但是 ((val%N)+N)%N 的表达式更优雅一些,原因有:

  • (val+N)%N 在val很大的情况下,val+N可能会溢出,导致结果失真。而((val%N)+N)%N由于先取余,减小了val,溢出的可能性更小。

#include "iostream"
#include "set"
#include "cstring"

using namespace std;


const int N =2E5+3;
int q[N];

const int null = 0x3f3f3f3f;

int  find(int val)
{
    int t = ((val%N) + N)%N;
    
    while(q[t] != null && q[t] != val){
        
        
        if(t++ == N) t = 0;
    }
    return t;
    
    
}
int main()
{
    
    
    memset(q,null,sizeof(q));
    char op[2];
    int n = 0;
    cin >> n;
    
 
    while(n--)
    {
        scanf("%s",op);
        int val;
        
        cin>>val;
        
        if(op[0] == 'I')
        {
              q[find(val)] = val;
            
             
        }
        else 
        {
            
            int  it =find(val);
            if(q[it] == val)
            printf("Yes\n");
            else 
            printf("No\n");
        }
        
        
    }
    
    
    return 0;
    
}


  • 拉链法

把映射到相同位置的数挂载在同一个链表下.

若插入的数的个数范围为1E5,则映射到的数组的长度为N = 1E5,h[N]存储每一个映射结点所在的链表的头结点.ne[] 表示该结点的后继结点,e[]表示结点的实际值.每次插入首先找到映射的地址,然后头插法插入.查找则找到映射地址后对链表进行查找.



#include "iostream"
#include "cstring"

using namespace std;


const int N =2E5+3;
int h[N];       // 映射到的结点坐标
int idx=0;      //当前可用的结点
int ne[N],e[N]; //ne结点的后继坐标 e[] 结点的实际值

void insert(int val)
{
    int t =((val%N)+N)%N;
    
    e[idx] = val;
    ne[idx] = h[t];
    
    h[t] = idx++;
    
}
 
 
bool find(int val)
{
  int t =((val%N)+N)%N;
  for(int i = h[t] ; i !=-1 ; i = ne[i])
  {
      if(e[i] == val) return true;
  }
  
  
    return false;
}
int main()
{
    
    
    
    char op[2];
    int n = 0;
    cin >> n;
    
    memset(h,-1,sizeof(h));
    
 
    while(n--)
    {
        scanf("%s",op);
        int val;
        
        cin>>val;
        
        if(op[0] == 'I')
        {
             
             insert(val);
            
             
        }
        else 
        {
            
            int res = find(val);
            if(res) printf("Yes\n");
            else printf("No\n");
            
        }
        
        
    }
    
    
    return 0;
    
}

字符串hash

用法:用P进制的hash值来表示一个字符串,这种做法确实的非常的精妙.这个P的取法也有讲究,比如取值P = 131 ,1331, 133331.可以看出它是一个素数.这一点在我为单片机写传感器采样的读取代码时,经常需要避开两个传感器同时读取,因为这样就会在RTOS中存在一个不能打断的时期,因为采样对时许有严格的控制需要关闭调度器.所以我也会为他们选择一个素数的时间来采样,这样的话从数学证明的角度上说冲突的概率非常小.

一个字符’a-Z’的hash值就取它自己的ascil码值
"abc"为P进制的hash值,设a,b,c的hash值为 x,y,z.
hash(“abc”) = hash(‘a’)*P2+hash(‘b’)+P1+hash(‘c’);
= hash(“ab”)*P1+hash(‘c’)*P0;

所以推出==>
设字符串 s1,s2为两个字符串则s1s2为它们之间的拼接.
hash(s1s2) = hash(s1)*P^(s2的长度) + hash(s2)
hash(s2) = hash(s1s2) - hash(s1)*P^(s2的长度)

设t(x)表示从0到x个长度的字符串.
则hash(t(n)) = hash(t(n-1))*P + hash(最后一个字符的hash值).

如果求一个字符串的任意子串的hash值
设字符串的长度为n ,求第l到第r个长度的子串的hash值.

hash(t):前t个长度的子串的hash值

hash(l...r)(长度l到r之间的子串的hash值) = hash(r) - hash(l-1)*P^(r-l+1)

//主要是前l-1个字符

比较任意两个字符串的hash值是否相等
输入的下标从1开始.l1,r1,l2,r2.



#include "iostream"


using namespace std;


const int N = 1E5+10;
const int P = 131;

typedef unsigned long long ull;
ull p[N];
ull h[N];

int main()
{
    
    int n,m;
    cin>>n>>m;
    
    string s;
    
    cin>>s;
    
    
    p[0] =1;
    for(int i = 1 ; i <= n ; i++)
    {
        
        
        p[i] = p[i-1]*P;   //存储P进制的值
        h[i] = h[i-1]*P+s[i-1]; //从第0位字符开始计算hash值 默认ascil值位字符的hash值
        
        //所以字符串的哈希值 h[i] : 从1 到第 i个字符的哈希值 h[i] = h[i-1] *p + s[i-1];
        
    }
    
    //m 个查询
    while(m--)
    {
        
        int l1,l2,r1,r2; 
        cin>>l1>>r1>>l2>>r2;
        ull res1,res2;
        res1 = h[r1] -h[l1-1]*p[r1-l1+1];
        res2 = h[r2] -h[l2-1]*p[r2-l2+1];
        
        if(res1 == res2 )
        {
            printf("Yes\n");
            
        }
        else 
        printf("No\n");
        
    }
    
    return 0;
    
}


可参考一篇不错的数学证明
https://www.acwing.com/solution/content/97009/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值