当前位置:   article > 正文

【C++】哈希表_c++哈希表

c++哈希表

【C++】哈希表

1、定义介绍

哈希表(Hash Table),也被称为哈希映射(Hash Map)或字典(Dictionary),是一种常见的数据结构,用于高效地存储和检索数据。它利用哈希函数(Hash Function)将关键码(Key)映射到存储位置,从而实现快速的查找、插入和删除操作。

哈希表的基本思想是将关键码通过哈希函数转换成一个固定长度的哈希值(Hash Value),然后将该哈希值作为索引来访问数组(或者称为哈希桶)中的特定位置。这样,可以通过哈希函数直接计算出元素的存储位置,从而快速定位到所需的数据。

哈希表的主要特点包括:

  1. 高效的查找操作:通过哈希函数计算关键码的哈希值,可以直接访问到对应位置的数据,避免了逐个比较的过程。在理想情况下,查找操作的时间复杂度为常数时间O(1)。

  2. 灵活的插入和删除操作:通过哈希函数计算哈希值,可以将数据插入到对应位置,或者删除对应位置的数据。在哈希表中插入和删除数据的时间复杂度也是常数时间O(1)。

  3. 冲突处理:由于哈希函数可能将不同的关键码映射到相同的哈希值,可能会发生冲突(Collision)。冲突处理是哈希表设计中的一个重要问题,常见的方法包括开放寻址法和链地址法。

  4. 空间效率:哈希表使用数组来存储数据,相对于其他数据结构如平衡树,它通常具有更高的空间效率。

需要注意的是,哈希表的性能在一定程度上取决于哈希函数的设计和冲突处理方法的选择。好的哈希函数应该将关键码均匀地映射到不同的位置,以尽量减少冲突的发生。冲突处理方法的选择也会影响到哈希表的性能和效率。

哈希表在实际应用中非常广泛,例如在编程中常用的哈希表数据结构包括Python中的字典(dict)、Java中的HashMap、C++中的std::unordered_map等。它们提供了高效的键值对存储和查找功能,适用于快速的数据检索、缓存管理、唯一性判断等场景。

2、原理解释

哈希表是一种通过哈希函数将元素的关键码与其存储位置建立映射关系的数据结构,以实现高效的元素查找。

  1. 存储结构:构造一种存储结构,可以通过哈希函数将元素的关键码与其存储位置建立一一映射的关系。这个存储结构即为哈希表。

  2. 插入元素:当向哈希表中插入元素时,根据元素的关键码,通过哈希函数计算出该元素的存储位置,并按此位置进行存放。

  3. 搜索元素:当搜索元素时,通过哈希函数对元素的关键码进行同样的计算,将计算得到的哈希值作为元素的存储位置,在哈希表中按此位置取元素进行比较。如果关键码相等,则搜索成功。

  4. 搜索效率:哈希表的搜索效率高,因为通过哈希函数可以直接计算出元素的存储位置,而无需进行多次关键码比较。理想情况下,哈希表的搜索时间复杂度为O(1),即常数时间。

总结起来,哈希表利用哈希函数将元素的关键码与其存储位置建立映射关系,使得在查找元素时可以直接通过哈希值定位到元素的存储位置,从而实现高效的搜索操作。

3、哈希函数

哈希函数是一种特殊的函数,它接受输入并返回一个固定长度的哈希值。哈希函数的作用类似于一个"魔术公式",它能将任意大小的输入数据转换成一个较小且固定长度的输出。

想象一下,你要在一本电话簿中找到某个人的电话号码,但你只知道他的名字。电话簿按照姓氏的字母顺序排列,如果你只知道名字,那么你需要从头到尾逐个比较姓名,直到找到匹配的条目。

现在,假设有一个神奇的函数可以将人的名字转换成一个数字,而且这个数字就是电话簿中对应的位置。这样,你只需要使用这个函数将名字转换成数字,然后直接跳到对应的位置,就能快速找到电话号码,而无需逐个比较。

这个神奇的函数就是哈希函数。它接受一个名字作为输入,并通过一系列的计算将其转换成一个数字,这个数字就是该名字在电话簿中的位置。哈希函数可以将大量的输入映射到有限的输出空间中,通常是一个固定大小的数组。

哈希函数的关键是要确保具有相同输入的哈希函数始终返回相同的哈希值。这样才能保证当你需要查找或存储数据时,通过哈希函数计算得到的位置是一致的。

一个好的哈希函数应该具备以下特点:

  • 易于计算:哈希函数的计算过程应该是高效的,能够在短时间内完成。
  • 均匀分布:哈希函数应该能够将输入数据均匀地映射到输出空间,避免数据在哈希表中的聚集或冲突。
  • 最小冲突:好的哈希函数应该能够最大程度地减少冲突,即不同的输入尽量映射到不同的输出值。

总结起来,哈希函数是一种通过计算将输入数据映射成固定长度的输出值的函数。它在哈希表等数据结构中发挥着重要的作用,用于快速存储和检索数据。通过哈希函数,我们可以将大量的输入数据转换成固定长度的哈希值,从而实现快速的数据访问和查找。

3.1 简单的例子

假设我们有一个字符串列表,其中包含一些人的姓名:

[“Alice”, “Bob”, “Charlie”, “Dave”]

我们希望将这些姓名存储在一个哈希表中,并能够通过姓名快速查找到对应的信息。

首先,我们可以选择一个简单的哈希函数,例如将每个字母的ASCII码相加并取余哈希表大小的方式。假设我们的哈希表大小为10,即有10个槽位。

现在,我们使用哈希函数将每个姓名转换为哈希值,并将其存储在对应的槽位中:

  • "Alice"的哈希值为(65 + 108 + 105 + 99 + 101) % 10 = 478 % 10 = 8,所以将"Alice"存储在槽位8中。
  • "Bob"的哈希值为(66 + 111 + 98) % 10 = 275 % 10 = 5,所以将"Bob"存储在槽位5中。
  • "Charlie"的哈希值为(67 + 104 + 97 + 114 + 108 + 105 + 101) % 10 = 746 % 10 = 6,所以将"Charlie"存储在槽位6中。
  • "Dave"的哈希值为(68 + 97 + 118 + 101) % 10 = 384 % 10 = 4,所以将"Dave"存储在槽位4中。

现在,我们可以通过哈希函数计算每个姓名的哈希值,并直接访问对应的槽位来查找数据。例如,如果我们要查找"Charlie"的信息,我们可以计算出它的哈希值为6,并直接访问槽位6,从而快速获取到相关信息。

这个例子简要展示了哈希函数的作用和哈希表的存储过程。通过合适的哈希函数,我们可以将数据快速地映射到哈希表中,并实现高效的数据查找操作。

3.2 哈希冲突

在上述例子中,假设再加入一个名为“Eve”的人,哈希冲突出现在存储"Eve"这个姓名。根据哈希函数的计算,"Eve"的哈希值为8,但是槽位8已经被"Alice"占用了。这就是一个典型的哈希冲突情况。

当哈希函数将不同的键映射到相同的哈希值时,就会发生哈希冲突。

3.2.1 解决方案

在这种情况下,多个键需要存储到同一个槽位中,但是槽位只能存储一个键。解决哈希冲突的常用方法是冲突处理技术,其中两个常见的方法是开放寻址法和链地址法。

开放寻址法(Open Addressing)和链地址法(Chaining)是两种常用的冲突处理方法,用于解决哈希表中发生的哈希冲突。

  1. 开放寻址法:
    开放寻址法是一种冲突解决方法,它尝试在哈希表中找到下一个可用的槽位,直到找到一个空槽位来存储冲突的元素。当发生哈希冲突时,开放寻址法按照一定的探测序列(如线性探测、二次探测、双重哈希等)在哈希表中逐个查找下一个槽位,直到找到一个空槽位。

​ 如果某个槽位已经被占用,开放寻址法会依次查找下一个槽位,直到找到一个空槽位为止。这样,冲突的元素会被依次存放在哈希表中的相邻位置。当 需要查找元素时,通过哈希函数计算哈希值,然后按照相同的探测序列逐个查找,直到找到目标元素或者遇到空槽位。

​ 开放寻址法的优点是简单且节省空间,因为它不需要额外的存储空间来存储链表。然而,它的缺点是容易产生聚集现象,即相邻的槽位被频繁占用,导 致性能下降。此外,删除操作也比较复杂,需要使用特殊的标记来标记已删除的槽位。

  1. 链地址法:
    链地址法是一种冲突解决方法,它使用链表来处理哈希冲突。每个槽位在哈希表中存储一个链表或其他动态数据结构,当多个元素映射到同一个槽位时,它们会按顺序添加到链表中。

​ 当发生哈希冲突时,新的元素将会被插入到对应槽位的链表末尾。在查找元素时,通过哈希函数计算哈希值,然后定位到对应槽位的链表,再遍历链表 进行比较,直到找到目标元素或者链表结束。

​ 链地址法的优点是解决了聚集现象,可以有效地处理大量的冲突。它的缺点是相对于开放寻址法,需要额外的存储空间来存储链表节点,可能会占用较 多的内存。

​ 选择使用开放寻址法还是链地址法取决于应用的需求和数据的特性。通常情况下,开放寻址法适用于存储小规模数据和对内存使用有限制的场景,而链 地址法适用于存储大规模数据和对内存使用要求较松的场景。

3.2.2 开放寻址法

开放寻址法是一种解决哈希冲突的方法,它尝试在哈希表中寻找下一个可用的槽位,直到找到一个空槽位来存储冲突的元素。具体来说,开放寻址法按照一定的探测序列在哈希表中逐个查找下一个槽位。

常见的开放寻址法包括线性探测、二次探测和双重哈希。下面分别介绍这些探测方法的工作原理:

  1. 线性探测(Linear Probing):
    线性探测是最简单的开放寻址法。当发生哈希冲突时,线性探测会按顺序查找下一个槽位,直到找到一个空槽位。

具体步骤如下:

  • 如果哈希函数将元素映射到槽位i,并且该槽位已经被占用,那么线性探测将继续查找下一个槽位i+1,然后是i+2,以此类推,直到找到一个空槽位。
  • 当需要查找元素时,使用哈希函数计算哈希值,然后从对应槽位开始顺序查找,直到找到目标元素或者遇到空槽位。
  1. 二次探测(Quadratic Probing):
    二次探测是一种改进的开放寻址法。当发生哈希冲突时,二次探测通过二次探测序列来查找下一个槽位。

具体步骤如下:

  • 如果哈希函数将元素映射到槽位i,并且该槽位已经被占用,那么二次探测将按照二次探测序列查找下一个槽位,即依次查找槽位i+12,i-12,i+22,i-22,以此类推,直到找到一个空槽位。
  • 当需要查找元素时,使用哈希函数计算哈希值,然后从对应槽位开始按照二次探测序列查找,直到找到目标元素或者遇到空槽位。
  1. 双重哈希(Double Hashing):
    双重哈希是一种改进的开放寻址法,它使用两个不同的哈希函数来计算下一个探测位置。当发生哈希冲突时,双重哈希通过第二个哈希函数计算出的步长来查找下一个槽位。

具体步骤如下:

  • 如果哈希函数将元素映射到槽位i,并且该槽位已经被占用,那么双重哈希将使用第二个哈希函数计算一个步长,然后查找下一个槽位i+步长。
  • 当需要查找元素时,使用哈希函数计算哈希值,然后从对应槽位开始按照步长查找,直到找到目标元素或者遇到空槽位。

开放寻址法的优点是简单且节省空间,因为它不需要额外的存储空间来存储链表。然而,开放寻址法容易产生聚集现象,即相邻的槽位被频繁占用,导致性能下降。此外,删除操作也比较复杂,需要使用特殊的标记来标记已删除的槽位。

在实现开放寻址法时,需要注意哈希表的装载因子(Load Factor)控制,即哈希表中元素的数量与槽位总数的比例。装载因子过高会导致冲突增加,从而降低性能。因此,当装载因子超过一定阈值时,通常需要进行哈希表的扩容操作,重新调整槽位的数量,以保持哈希表的性能。

template<class K, class V>
class HashTable {
    struct Elem {
        pair<K, V> _val;
        State _state;
    };

public:
    HashTable(size_t capacity = 3) : _ht(capacity), _size(0) {
        for (size_t i = 0; i < capacity; ++i)
            _ht[i]._state = EMPTY;
    }

    bool Insert(const pair<K, V>& val) {
        size_t hashAddr = HashFunc(key);
        while (_ht[hashAddr]._state != EMPTY) {
            if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
                return false;

            hashAddr++;
            if (hashAddr == _ht.capacity())
                hashAddr = 0;
        }

        _ht[hashAddr]._state = EXIST;
        _ht[hashAddr]._val = val;
        _size++;
        return true;
    }

    int Find(const K& key) {
        size_t hashAddr = HashFunc(key);
        while (_ht[hashAddr]._state != EMPTY) {
            if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
                return hashAddr;

            hashAddr++;
        }
        return hashAddr;
    }

    bool Erase(const K& key) {
        int index = Find(key);
        if (-1 != index) {
            _ht[index]._state = DELETE;
            _size++;
            return true;
        }
        return false;
    }

    size_t Size() const;
    bool Empty() const;
    void Swap(HashTable<K, V, HF>& ht);

private:
    size_t HashFunc(const K& key) {
        return key % _ht.capacity();
    }

private:
    vector<Elem> _ht;
    size_t _size;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

这段代码展示了一个使用开放寻址法实现的哈希表(HashTable)的模板类。

在这个哈希表中,元素类型被定义为pair<K, V>,其中K表示关键码的类型,V表示值的类型。哈希表的底层使用一个名为Elem的结构体来存储每个元素,其中包含一个pair<K, V>类型的值和一个State类型的状态。

哈希表的构造函数使用给定的容量创建了一个内部的vector<Elem>类型的哈希表,初始时所有的槽位状态被设置为EMPTY。

哈希表提供了以下公有成员函数:

  • Insert函数用于插入一个新的键值对到哈希表中。它首先计算关键码的哈希值,然后根据开放寻址法,顺序查找下一个可用的槽位,直到找到一个空槽位或者找到已存在的相同关键码的元素。如果成功插入,返回true;如果哈希表已满或者存在相同关键码的元素,返回false。

  • Find函数用于查找给定关键码的元素在哈希表中的位置。它首先计算关键码的哈希值,然后按照开放寻址法,顺序查找下一个槽位,直到找到目标元素或者遇到空槽位。如果找到目标元素,返回其在哈希表中的位置(即哈希地址);如果未找到,返回哈希表的大小(-1)。

  • Erase函数用于从哈希表中删除给定关键码的元素。它通过调用Find函数查找元素的位置,如果找到则将对应槽位的状态标记为DELETE,并递增哈希表的大小。如果成功删除,返回true;如果未找到目标元素,返回false。

此外,还提供了Size函数用于返回哈希表中元素的数量,Empty函数用于判断哈希表是否为空,以及Swap函数用于交换两个哈希表的内容。

私有成员函数HashFunc实现了哈希函数,它通过取关键码对哈希表的容量取余来计算哈希值。

总体而言,这段代码展示了一个简单的使用开放寻址法实现的哈希表模板类,提供了基本的插入、查找和删除操作,用于存储和操作键值对。

3.2.3 链地址法

链地址法是一种常见的解决哈希冲突的方法,它使用链表来处理冲突。每个槽位在哈希表中存储一个链表或其他动态数据结构,当多个元素映射到同一个槽位时,它们会按顺序添加到链表中。

以下是使用链地址法实现的哈希表的基本流程和要点:

  1. 定义一个链表节点结构体(Node)用于存储哈希表中的每个元素,该结构体包含关键码(key)和对应的值(value)。

  2. 创建一个哈希表数组,每个槽位存储链表的头节点指针,初始时所有槽位都为空。

  3. 哈希函数将关键码映射到槽位的索引值。根据哈希函数计算关键码的哈希值,然后将该值与槽位总数取模,得到要插入或查找的槽位索引。

  4. 插入操作:

    • 根据哈希函数计算关键码的哈希值,获取要插入的槽位索引。
    • 在该槽位上的链表中顺序查找,判断关键码是否已经存在。
    • 如果存在,可以选择更新对应的值,或者不允许重复插入。
    • 如果不存在,创建一个新的节点,并将其插入到链表的末尾。
  5. 查找操作:

    • 根据哈希函数计算关键码的哈希值,获取要查找的槽位索引。
    • 在该槽位上的链表中顺序查找,直到找到目标节点或者遍历到链表末尾。
    • 如果找到目标节点,返回对应的值。
    • 如果遍历到链表末尾仍未找到目标节点,表示关键码不存在。
  6. 删除操作:

    • 根据哈希函数计算关键码的哈希值,获取要删除的槽位索引。
    • 在该槽位上的链表中顺序查找,找到目标节点后进行删除操作。
    • 删除操作可以直接删除节点,或者通过修改节点的状态来标记删除。
    • 注意,删除节点后需要处理链表的连接,确保链表仍然保持完整。

链地址法的优点是简单且易于实现,可以有效地处理大量的冲突。每个槽位存储一个链表,当哈希冲突发生时,新的元素会按顺序添加到链表中,不需要进行搬迁或重新哈希操作。此外,链地址法相对于开放寻址法来说,对于删除操作也较为方便。

然而,链地址法也有一些缺点。它需要额外的存储空间来存储链表节点,这可能会占用较多的内存。而且,当链表过长时,会影响到查找效率,因为需要遍历链表来找到目标节点。因此,在设计哈希表时,需要根据具体应用场景和数据量来选择合适的哈希函数和调整槽位数量,以平衡存储空间和查找效率的需求。

template<class V>
struct HashBucketNode {
    HashBucketNode(const V& data)
        : _pNext(nullptr), _data(data)
    {}

    HashBucketNode<V>* _pNext;
    V _data;
};

template<class V>
class HashBucket {
    typedef HashBucketNode<V> Node;
    typedef Node* PNode;

public:
    HashBucket(size_t capacity = 3)
        : _size(0)
    {
        _ht.resize(GetNextPrime(capacity), nullptr);
    }

    PNode Insert(const V& data) {
        // Check if capacity needs to be increased...
        // _CheckCapacity();

        size_t bucketNo = HashFunc(data);

        PNode pCur = _ht[bucketNo];
        while (pCur) {
            if (pCur->_data == data)
                return pCur;

            pCur = pCur->_pNext;
        }

        pCur = new Node(data);
        pCur->_pNext = _ht[bucketNo];
        _ht[bucketNo] = pCur;
        _size++;
        return pCur;
    }

    PNode Erase(const V& data) {
        size_t bucketNo = HashFunc(data);
        PNode pCur = _ht[bucketNo];
        PNode pPrev = nullptr, pRet = nullptr;

        while (pCur) {
            if (pCur->_data == data) {
                if (pCur == _ht[bucketNo])
                    _ht[bucketNo] = pCur->_pNext;
                else
                    pPrev->_pNext = pCur->_pNext;

                pRet = pCur->_pNext;
                delete pCur;
                _size--;
                return pRet;
            }

            pPrev = pCur;
            pCur = pCur->_pNext;
        }

        return nullptr;
    }

    PNode Find(const V& data) {
        size_t bucketNo = HashFunc(data);
        PNode pCur = _ht[bucketNo];

        while (pCur) {
            if (pCur->_data == data)
                return pCur;

            pCur = pCur->_pNext;
        }

        return nullptr;
    }

    size_t Size() const {
        return _size;
    }

    bool Empty() const {
        return _size == 0;
    }

    void Clear() {
        for (auto& bucket : _ht) {
            PNode pCur = bucket;
            while (pCur) {
                PNode pNext = pCur->_pNext;
                delete pCur;
                pCur = pNext;
            }
            bucket = nullptr;
        }
        _size = 0;
    }

    bool BucketCount() const {
        return _ht.size();
    }

    void Swap(HashBucket<V>& ht) {
        _ht.swap(ht._ht);
        std::swap(_size, ht._size);
    }

    ~HashBucket() {
        Clear();
    }

private:
    size_t HashFunc(const V& data) {
        return data % _ht.capacity();
    }

private:
    vector<PNode> _ht;
    size_t _size;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125

这段代码展示了使用链地址法实现的哈希桶(HashBucket)的模板类。

在这个哈希桶中,元素类型为V。每个元素被包装在一个名为HashBucketNode的结构体中,结构体包含一个指向下一个节点的指针_pNext和数据成员_data

哈希桶类定义了一个私有类型别名NodePNode,分别表示节点类型和节点指针类型。

哈希桶提供了以下公有成员函数:

  • 构造函数HashBucket:创建一个指定容量的哈希桶,默认容量为3。内部使用一个vector来存储桶的链表头节点指针,并初始化每个槽位为nullptr。

  • Insert函数:插入一个新的元素到哈希桶中。根据哈希函数计算元素所在的桶号,然后在该桶的链表中查找是否已经存在相同的元素。如果存在,返回已存在的节点指针;如果不存在,则创建一个新节点,将其插入到链表的头部,并返回该节点指针。

  • Erase函数:删除哈希桶中指定数据的元素。根据哈希函数计算元素所在的桶号,并在该桶的链表中查找要删除的元素。如果找到目标节点,删除该节点并返回删除节点的下一个节点指针;如果未找到目标节点,返回nullptr。

  • 其他成员函数包括FindSizeEmptyClearBucketCountSwap,用于查找元素、获取元素数量、判断是否为空、清空哈希桶、获取桶的数量和交换哈希桶内容。

私有成员函数HashFunc实现了哈希函数,它根据元素的值对哈希桶的容量取模,以计算元素所在的桶号。

4、哈希的应用

位图(Bitmap)和布隆过滤器(Bloom Filter)都是基于位操作的数据结构,用于高效地存储和查询数据集合的成员关系。虽然它们在某些方面有相似之处,但它们解决的问题和使用场景略有不同。

  1. 位图(Bitmap):

    • 概述:位图是一种使用位向量(Bit Vector)表示数据集合的数据结构。它将每个元素映射到位向量中的一个位,用于表示元素是否存在于集合中。位图通常用于表示稀疏数据集合,并且适用于静态数据集合,其中元素不经常被插入或删除。
    • 实现方式:位图使用一个固定大小的位向量,每个位对应一个元素。如果元素存在于集合中,对应的位被设置为1;如果元素不存在,对应的位被设置为0。
    • 应用场景:位图常用于位集合操作、数据压缩、布尔查询等场景。它在数据库管理、网络编程、图像处理等领域有广泛应用。
  2. 布隆过滤器(Bloom Filter):

    • 概述:布隆过滤器是一种概率型数据结构,用于高效地判断一个元素是否存在于一个大型集合中。它使用多个哈希函数和位向量来表示集合的成员关系,允许一定的误判率。
    • 实现方式:布隆过滤器使用一个位向量和多个哈希函数。对于要插入的元素,通过多个哈希函数计算得到多个哈希值,然后将对应的位设置为1。对于查询元素,同样通过多个哈希函数计算得到多个哈希值,并检查对应的位是否都为1,如果有任何一个位为0,则元素一定不存在;如果所有位都为1,则元素可能存在(有一定的误判率)。
    • 应用场景:布隆过滤器适用于需要快速判断元素是否属于一个大型集合的场景,并且可以容忍一定的误判率。它常用于缓存、数据库查询加速、垃圾邮件过滤、URL去重等场景。

虽然位图和布隆过滤器都是基于位操作的数据结构,但它们的主要区别在于:

  • 位图用于表示稀疏数据集合,而布隆过滤器用于判断元素是否存在于一个大型集合中。
  • 位图是精确数据结构,每个元素的存在与否可以被确定。布隆过滤器是概率型数据结构,存在一定的误判率。
  • 位图适用于静态数据集合,而布隆过滤器可以动态地插入和查询元素。

位图和布隆过滤器都与哈希有关,但它们在使用和目的上有所不同。

  1. 位图与哈希的关系:

    • 哈希函数在位图中用于将元素映射到位向量中的位置。通过哈希函数计算得到的哈希值,确定了位图中哪些位对应的元素存在或不存在。
    • 位图的每个位可以看作是一个哈希桶,而哈希函数确定了元素应该放置在哪个哈希桶中。
  2. 布隆过滤器与哈希的关系:

    • 布隆过滤器使用多个哈希函数来计算元素的哈希值,并将对应的位设置为1。这些哈希函数通常是独立的,互不相关。
    • 哈希函数在布隆过滤器中用于产生多个哈希值,从而在位向量中设置多个位。这些位表示元素的可能存在性。

在位图中,哈希函数用于确定元素的位置,以实现高效的插入和查询操作。在布隆过滤器中,哈希函数用于生成多个哈希值,以在位向量中设置位。哈希函数在这两种情况下都起到关键的作用,但应用的方式和目的有所不同。

需要注意的是,位图和布隆过滤器并不是哈希表的替代品,它们解决的问题和应用场景略有不同。哈希表是一种更通用的数据结构,用于存储键值对并提供高效的插入、查找和删除操作。而位图和布隆过滤器更专注于特定的数据集合成员关系的处理和查询操作。

4.1 位图

位图就像是在一个表格中给一个格子打上勾一样,说明那格已被占用,即说明数据的存在

class BitMap
{
public:
	BitMap(size_t range)
	{
		_bitTable.resize((range >> 5) + 1);
	}

	//标识一个数字在位图中的位置
	void SetBit(size_t x)
	{
		size_t index = x >> 5;
		size_t num = x % 32;

		_bitTable[index] |= (1 << num);
	}

	//取消数字在位图当中的标识.
	void RemoveBit(size_t x)
	{
		size_t index = x >> 5;
		size_t num = x % 32;

		_bitTable[index] &= ~(1 << num);
	}


	bool TestBit(size_t x)
	{
		size_t index = x >> 5;
		size_t num = x % 32;

		return _bitTable[index] & (1 << num);
	}

private:
	vector<int> _bitTable;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 构造函数 BitMap(size_t range):接受一个范围参数 range,用于确定位图的大小。根据范围计算所需的位图大小,并通过 _bitTableresize 方法进行初始化。

  • 成员函数 SetBit(size_t x):将数字 x 在位图中标记为1。首先根据 x 计算出在 _bitTable 中的索引 index,以及在该索引位置上的偏移量 num。然后使用位运算 (1 << num) 得到一个只有第 num 位为1的数,将其与 _bitTable[index] 进行按位或操作,将对应的位标记为1。

  • 成员函数 RemoveBit(size_t x):将数字 x 在位图中取消标记。与 SetBit 方法类似,根据 x 计算出索引 index 和偏移量 num,然后使用位运算 ~(1 << num) 得到一个只有第 num 位为0的数,与 _bitTable[index] 进行按位与操作,将对应的位取消标记。

  • 成员函数 TestBit(size_t x):检测数字 x 在位图中的标记。同样,根据 x 计算出索引 index 和偏移量 num,然后通过位运算 _bitTable[index] & (1 << num) 检查对应的位是否为1,若为1则返回 true,否则返回 false

注意,位图中的位索引从0开始,从小到大依次对应数字0到 range-1

4.2 布隆过滤器

布隆过滤器就像是翻译器,提供数据,过滤器就能通过多个无偏函数变化翻译出数组下标值,从而提供了判断数据是否存在的依据

布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。其查询元素的过程如下:

  • 通过k个无偏hash函数计算得到k个hash值
  • 依次取模数组长度,得到数组索引
  • 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。

比如:删除上图中"tencent"元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也 被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计 数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储 空间的代价来增加删除操作。

缺陷:

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/437594
推荐阅读
相关标签
  

闽ICP备14008679号