1145 Hashing - Average Search Time + 哈希表 + 布隆过滤器

江柏英的技术博客 / 2023-05-03 / 原文

一、哈希的整体思想

最简单的哈希表其实就是数组,从数组中取出一个数的时间复杂度是O(1)的。但是数组下标类型是整型的,万一我的下标类型不是整型了该怎么办呢?比如说字符串型,典型的就是我想查找某个单词存不存在。还有些更复杂的数据类型,比如自定义的类型。那么问题就来了,如何满足任意数据类型的索引需求呢?最简单直接的想法,其实就是先对任意数据类型与整型的数组下标做一个映射,往后就又回到数组取数的环节了。这种映射是一种高维空间到低维空间的映射。低维空间是有限的,而高维空间的变化情况要复杂得多。如果采用一个萝卜一个坑的思想,必然会产生重复占位,这就是哈希冲突。当我们在设计哈希表的时候一定要考虑到哈希冲突。为什么说哈希表的设计感极强,其实就在于,解决哈希冲突的方法是多种多样的。还有一个问题,哈希表装满了怎么办?有一个概念叫做装填因子(=存储元素数/哈希表size大小)一般来说,装填因子达到0.75的情况下,意味着哈希表可能要进行扩容了。

布隆过滤器

好了,说回布隆过滤器,传统哈希表有一个缺点,就是存储空间与元素数量有关,在大数据的场景下,经常需要判重操作。如果用一张哈希表存,数据量可能过于庞大。而布隆过滤器,可以实现存储空间与元素数量无关。它是通过几组不同的哈希函数,映射到不同的数组下标,然后在对应位置的数组上标记为1,默认是0,如果这些位置上的数组值一旦只要出现一个0,那么都可以直接判断出,该元素不存在,但是如果全为1,也不能说明它就一定存在哈希表中,只能说明它大概率存在。布隆过滤器经常应用于大数据和信息安全相关的场景中。

二、哈希冲突解决办法

1、开放地址法

思想:如果7的位置发生冲突了,那就试一试8,8也不行,那就试一试9,依次往后“探测”,直到找到空位。(线性探测法)
注意:往后探测位置的计算规则是很灵活的,这里只是举了最简单的线性探测法。

平方探测法也有两种,可以简要看看找找区别,做题的时候格外注意,到底是哪一种:
第一种:7->8->12->21->37 (a[i+1] = a[i] + i^2)
第二种:7->8->11->16->23 (a[i+1] = a[0] + i^2)
值得注意的是,第二种只需要判断 i 从0-Msize-1 即可,因为(key+Msize*Msize)%Msize代表搜索回到了原地,这时可以认为无法搜索到这个数字

2、再哈希法

思想:比方说可以设计三种不同的哈希函数,如果第一种冲突了,就试试第二种,第二种也冲突了就试试第三种,依次类推。
注意:这种方法治标不治本,一般配合其他的哈希冲突解决方法使用。

3、建立公共溢出区

思想:另外建立一个公共溢出区,我想找的元素哈希表中找不到,就去公共溢出区再找找,其实就是用另外一种数据结构来维护,比方说红黑树。

4、链式地址法(拉链法)

思想:把哈希表的每一个坑看成链表的头节点。如果两个元素都想占用这个坑,直接在该坑上往后形成一个链表即可。

三、哈希表代码实现:

#include<iostream>
#include<vector>
using namespace std;

// 开放寻址法
class HashTable {
public:
    HashTable(int n = 100) : data(n), flag(n), cnt(0) {} 
    void insert(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        if (!flag[ind]) {
            data[ind] = s;
            flag[ind] = true;
            cnt++;
            if (cnt * 100 > data.size() * 75) {
                expand();
            }
        }
    }
    bool find(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        return flag[ind];
    }
private:
    int cnt; // 记录有多少元素
    vector<string> data;
    vector<bool> flag; // 记录相应位置是否存数据

    void expand() {
        int n = data.size() * 2;
        HashTable h(n);
        for (int i = 0; i < data.size(); i++) { // 看似很高的时间复杂度,其实分摊到每一个元素,扩容所造成的时间复杂度是O(1)的。
            if (flag[i] == false) continue;
            h.insert(data[i]);
        }
        *this = h;
        return ;
    }
    int hash_func(string &s) {
        int seed = 131, hash = 0;
        for (int i = 0; s[i]; i++) {
            hash = hash * seed + s[i];
        }
        return hash & 0x7fffffff; // 最高位是0,最后肯定是一个正数
    }
    void recalc_ind(int &ind, string s) {
        int t = 1;
        while (flag[ind] && data[ind] != s) {
            ind += t * t;
            t += 1;
            ind %= data.size();
        }
        return ;
    }

};


// 公共溢出区法:
class HashTable {
public:
    HashTable(int n = 100) : flag(n), data(n), cnt(0) {}
    void insert(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        if (flag[ind] == false) {
            data[ind] = s;
            flag[ind] = true;
            cnt += 1;
            if (cnt * 100 > data.size() * 75) {
                expand();
            }
        } else if (data[ind] != s) {
            buff.insert(s);
        }
        return ;
    }
    bool find(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        if (flag[ind] == false) return false;
        if (data[ind] == s) return true;
        return buff.find(s) != buff.end();
    }

private:
    int cnt;
    vector<string> data;
    vector<bool> flag;
    set<string> buff;
    
    void expand() {
        int n = data.size() * 2;
        HashTable h(n);
        for (int i = 0; i < data.size(); i++) {
            if (flag[i] == false) continue;
            h.insert(data[i]);
        }
        for (auto x : buff) {
            h.insert(x);
        }
        *this = h;
        return ;
    }
    int hash_func(string &s) {
        int seed = 131, hash = 0;
        for (int i = 0; s[i]; i++) {
            hash = hash * seed + s[i];
        }
        return hash & 0x7fffffff;
    }
    void recalc_ind(int &ind, string &s) {
        return ;
    }
};

// 拉链法
class Node {
public :
    Node(string data = "", Node *next = nullptr) : data(), next(nullptr) {}
    string data;
    Node *next;
    void insert(Node *node) {
        node->next = this->next;
        this->next = node;
        return ;
    }
};

class HashTable {
public:
    HashTable(int n = 100) : data(n), cnt(0) {}
    void insert(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        Node *p = &data[ind];
        while (p->next && p->next->data != s) p = p->next;
        if (p->next == nullptr) {
            p->insert(new Node(s));
            cnt += 1;
            if (cnt > data.size() * 3) expand();
        }
        return ;
    }
    bool find(string s) {
        int ind = hash_func(s) % data.size(); // 计算哈希值
        recalc_ind(ind, s); // 冲突处理
        Node *p = data[ind].next;
        while (p && p->data != s) p = p->next;
        return p != nullptr;
    }

private:
    int cnt;
    vector<Node> data;
    
    void expand() {
        int n = data.size() * 2;
        HashTable h(n);
        for (int i = 0; i < data.size(); i++) {
            Node *p = data[i].next;
            while (p) {
                h.insert(p->data);
                p = p->next;
            }
        }
        *this = h;
        return ;
    }
    int hash_func(string &s) {
        int seed = 131, hash = 0;
        for (int i = 0; s[i]; i++) {
            hash = hash * seed + s[i];
        }
        return hash & 0x7fffffff;
    }
    void recalc_ind(int &ind, string &s) {
        return ;
    }
};


int main() {
    int op;
    string s;
    HashTable h;
    while (cin >> op >> s) {
        switch (op) {
            case 1: h.insert(s); break;
            case 2: cout << "find " << s << " : " << h.find(s) << endl; break;
        }
    }

    return 0;
}

看到这里,不妨来一道PAT的题目练练手吧,PAT题目链接:https://pintia.cn/problem-sets/994805342720868352/exam/problems/994805343236767744
题解代码:

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
#define MAX_N 20000

int Msize, n, m;
int a[MAX_N + 5], flag[MAX_N + 5];
int cnt;

bool insert(int s) {
    if (cnt == Msize) return false;
    for (int t = 0; t < Msize; t++) {
        int ind = (s + t * t) % Msize;
        if (!flag[ind]) {
            a[ind] = s;
            flag[ind] = 1;
            cnt++;
            return true;
        }
    }
    return false;
}
bool is_prime(int x) {
    if (x < 2) return false;
    for (int i = 2, I = sqrt(x); i <= I; i++) {
        if (x % i == 0) return false;
    }
    return true;
}
int main() {
    // 初始化
    int x;
    scanf("%d%d%d",&Msize, &n, &m);
    while (!is_prime(Msize)) Msize++;

    // 插入部分
    for (int i = 0 ; i < n; i++) {
        scanf("%d", &x);
        bool insert_status = insert(x);
        if (!insert_status) printf("%d cannot be inserted.\n", x);
    }

    // 查找部分
    int ans = 0;
    for (int i = 0; i < m; i++) {
        scanf("%d", &x);
        int t;
        for (t = 0; t <= Msize; t++) {
            ans += 1;
            int ind = (x + t * t) % Msize;
            if (a[ind] == x || flag[ind] == 0) break;
        }
    }
    printf("%.1f\n", ans * 1.0 / m);

    return 0;
}