[C++]模拟散列表

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

思路

① 拉链法
② 寻址法

寻址法

  1. 为什么要找第一个比空间大的质数
1. const int N = 200003; 
    1.1开放寻址操作过程中会出现冲突的情况,一般会开成两倍的空间,减少数据的冲突

    1.2如果使用%来计算索引, 把哈希表的长度设计为素数(质数)可以大大减小哈希冲突
    比如
    10%8 = 2      10%7 = 3
    20%8 = 4      20%7 = 6
    30%8 = 6      30%7 = 2
    40%8 = 0      40%7 = 5
    50%8 = 2      50%7 = 1
    60%8 = 4      60%7 = 4
    70%8 = 6      70%7 = 0

这就是为什么要找第一个比空间大的质数
  1. const int null = 0x3f3f3f3f 和 memset(h, 0x3f, sizeof h)之间的关系;

    首先,必须要清楚memset函数到底是如何工作的
    先考虑一个问题,为什么memset初始化比循环更快?
    答案:memset更快,为什么?因为memset是直接对内存进行操作。memset是按字节(byte)进行复制的

    void * memset(void *_Dst,int _Val,size_t _Size);
    这是memset的函数声明
    第一个参数为一个指针,即要进行初始化的首地址
    第二个参数是初始化值,注意,并不是直接把这个值赋给一个数组单元(对int来说不是这样)
    第三个参数是要初始化首地址后多少个字节
    看到第二个参数和第三个参数,是不是又感觉了
    h是int类型,其为个字节, 第二个参数0x3f八位为一个字节,所以0x3f * 4(从高到低复制4份) = 0x3f3f3f3f

    这也说明了为什么在memset中不设置除了-1, 0以外常见的值
    比如1, 字节表示为00000001,memset(h, 1, 4)则表示为0x01010101
  1. 为什么要取0x3f3f3f,为什么不直接定义无穷大INF = 0x7fffffff,即32个1来初始化呢?
3.1 首先,0x3f3f3f的体验感很好,0x3f3f3f3f的十进制是1061109567,也就是10^9级别的
    (和0x7fffffff一个数量级),而一般场合下的数据都是小于10^9的,所以它可以作为无穷大
    使用而不致出现数据大于无穷大的情形。
    比如0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit,int的表示范围,
    所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求。
    但是INF不同,一旦加上某个值,很容易上溢,数值有可能转成负数,有兴趣的小伙伴可以去试一试。

3.2 0x3f3f3f3f还能给我们带来一个意想不到的额外好处:如果我们想要将某个数组清零,
    我们通常会使用memset(a,0,sizeof(a))这样的代码来实现(方便而高效),但是当我们想
    将某个数组全部赋值为无穷大时(例如解决图论问题时邻接矩阵的初始化),就不能使用
    memset函数而得自己写循环了(写这些不重要的代码真的很痛苦),我们知道这是因为memset
    是按字节操作的,它能够对数组清零是因为0的每个字节都是0,
    现在如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以
    要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。

这也可以参考一下
代码

#include <cstring>
#include <iostream>

using namespace std;

const int N = 200003; //开发寻找,会出现冲突的情况,一般会开成两倍的空间, 同时去下一个质数
const int null = 0x3f3f3f3f;  //这是一个大于10^9的数


int h[N];

int find(int x){
    int k = (x % N + N) % N;
    //冲突情况:当前位置不为空,并且不为x
    while(h[k] != null && h[k] != x){
        k ++; 
        if(k == N) k = 0; //末尾,从头开始
    }
    return k;
}

int main(){
    int n;
    scanf("%d", &n);
    memset(h, 0x3f, sizeof h);
    while(n --){
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        int k = find(x); //找到符合条件的位置
        if(*op == 'I') h[k] = x;
        else{
            if(h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

拉链法
思路

1. 找出大于映射范围的质数 ( 如: 1e5+3 )
2.  设 h [ N ] 存储 N 条链表的头结点 设 e [ N ] 存储元素 设 ne[ N ] 为下一个节点的下标值  idx 表示元素存储到多少了
3. 简单来说, 就是通过 表达式 映射到 某个范围内, 然后通过建立邻接矩阵解决哈希冲突问题

下面的 insert 是头插, 也就是插入一个元素在头结点前面

#include <cstring>
#include <iostream>

using namespace std;

const int N = 1e5 + 3;  // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度

//* 开一个槽 h
int h[N], e[N], ne[N], idx;  //邻接表

void insert(int x) {
    // c++中如果是负数 那他取模也是负的 所以 加N 再 %N 就一定是一个正数
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x) {
    //用上面同样的 Hash函数 讲x映射到 从 0-1e5 之间的数
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i]) {
        if (e[i] == x) {
            return true;
        }
    }
    return false;
}

int n;

int main() {
    cin >> n;

    memset(h, -1, sizeof h);  //将槽先清空 空指针一般用 -1 来表示

    while (n--) {
        string op;
        int x;
        cin >> op >> x;
        if (op == "I") {
            insert(x);
        } else {
            if (find(x)) {
                puts("Yes");
            } else {
                puts("No");
            }
        }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值