一篇文章理解map、set、哈希、位图、布隆过滤器

本文详细介绍了C++中的map,set,unordered_map,和unordered_set容器的特性和使用方法,包括它们的底层实现(如红黑树和哈希),以及哈希冲突的处理策略(闭散列和开散列)。同时讨论了位图和布隆过滤器在大数据场景的应用。
摘要由CSDN通过智能技术生成

map和set

1.set

1. set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行
排序。
4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对
子集进行直接迭代。
5. set在底层是用二叉搜索树(红黑树)实现的。

 注意:

  1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
  2. set中插入元素时,只需要插入value即可,不需要构造键值对。
  3. set中的元素不可以重复(因此可以使用set进行去重)。
  4. 使用set的迭代器遍历set中的元素,可以得到有序序列
  5. set中的元素默认按照小于来比较
  6. set中查找某个元素,时间复杂度为:$log_2 n$
  7.  set中的元素不允许修改(为什么?)
  8. set中的底层使用二叉搜索树(红黑树)来实现

2.set使用

#include <set>
void TestSet()
{
// 用数组array中的元素构造set
    int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,
    6, 8, 0 };
    set<int> s(array, array+sizeof(array)/sizeof(array));
    cout << s.size() << endl;
    // 正向打印set中的元素,从打印结果中可以看出:set可去重
    for (auto& e : s)
    cout << e << " ";
    cout << endl;
    // 使用迭代器逆向打印set中的元素
    for (auto it = s.rbegin(); it != s.rend(); ++it)
    cout << *it << " ";
    cout << endl;
    // set中值为3的元素出现了几次
    cout << s.count(3) << endl;
}

3.map

1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元
素。
2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的
内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型
value_type绑定在一起,为其取别名称为pair:
typedef pair<const key, T> value_type;
3. 在内部,map中的元素总是按照键值key进行比较排序的。
4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序
对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

4.map的使用

#include <string>
#include <map>
void TestMap()
{
    map<string, string> m;
    // 向map中插入元素的方式:
    // 将键值对<"peach","桃子">插入map中,用pair直接来构造键值对
    m.insert(pair<string, string>("peach", "桃子"));
    // 将键值对<"peach","桃子">插入map中,用make_pair函数来构造键值对
    m.insert(make_pair("banan", "香蕉"));
    // 借用operator[]向map中插入元素
    /*
    operator[]的原理是:
    用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中
    如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器
    如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
    operator[]函数最后将insert返回值键值对中的value返回
    */
    // 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引
    用结果,
    m["apple"] = "苹果";
    // key不存在时抛异常
    //m.at("waterme") = "水蜜桃";
    cout << m.size() << endl;
    // 用迭代器去遍历map中的元素,可以得到一个按照key排序的序列
    for (auto& e : m)
    cout << e.first << "--->" << e.second << endl;
    cout << endl;
    // map中的键值对key一定是唯一的,如果key存在将插入失败
    auto ret = m.insert(make_pair("peach", "桃色"));
    if (ret.second)
    cout << "<peach, 桃色>不在map中, 已经插入" << endl;
    else
    cout << "键值为peach的元素已经存在:" << ret.first->first << "--->"
    << ret.first->second <<" 插入失败"<< endl;
    // 删除key为"apple"的元素
    m.erase("apple");
    if (1 == m.count("apple"))
    cout << "apple还在" << endl;
    else
    cout << "apple被吃了" << endl;
}

【总结】
1. map中的的元素是键值对
2. map中的key是唯一的,并且不能修改
3. 默认按照小于的方式对key进行比较
4. map中的元素如果用迭代器去遍历,可以得到一个有序的序列
5. map的底层为平衡搜索树(红黑树),查找效率比较高$O(log_2 N)$
6. 支持[]操作符,operator[]中实际进行插入查找。

5.multiset和multimap

这两个容器和map,set差不多  区别就是允许存在键值冗余。

哈希

unordered_map

1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与
其对应的value。

2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内
找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭
代方面效率较低。

5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
6. 它的迭代器至少是前向迭代器。

unordered_set

类似与unordered_map

底层结构

与map的底层不同,unordered_map底层是通过存储位置和关键码之间建立映射的方式来存储数据,而map底层是红黑树。

 通过除留余数法确定要存储的数据在哈希表中的位置。

1.哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

由于除留余数法会存在不同的数映射到同一个位置上,因此解决方法有以下两种。

1.1闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

1.线性探测:插入

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

1.通过哈希函数获取插入元素在哈希表中的位置

2.如果该位置没有元素,则插入新元素,如果该位置发生哈希冲突,则依次往后查找,直到遇到下一个空位置则插入新元素。

线性探测:删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起
,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。

2.二次探测

与线性探测不同的是,寻找下个位置的方法,从一直向后寻找,变成向后寻找 i 的平方。

1.2开散列(哈希桶)

概念:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

哈希桶 类似于在vector上挂着一个个单向链表的形成,其中每个单向链表中存放的数据既为哈希冲突的数据。

1.3 开散列扩容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

void _CheckCapacity()
{
    size_t bucketCount = BucketCount();
    if(_size == bucketCount)
    {
        HashBucket<V, HF> newHt(bucketCount);
        for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
        {
            PNode pCur = _ht[bucketIdx];
            while(pCur)
            {
                // 将该节点从原哈希表中拆出来
                _ht[bucketIdx] = pCur->_pNext;
                // 将该节点插入到新哈希表中
                size_t bucketNo = newHt.HashFunc(pCur->_data);
                pCur->_pNext = newHt._ht[bucketNo];
                newHt._ht[bucketNo] = pCur;
                pCur = _ht[bucketIdx];
            }
        }
         newHt._size=_size;
         this->Swap(newHt);
    }
}

原理:找到哈希桶的第一个节点,通过该节点指针找到第一个存储的值,通过重新对哈希函数进行计算得到该值在新哈希表中的位置,然后将该节点插入到新哈希表对应的位置,如果一个哈希表所有的节点都呗插入新哈希表后,依次对后面的哈希桶进行此操作。

哈希的应用

1.位图

1.1概念

位图也是利用映射的原理来实现的。所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

例:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。

1. 遍历,时间复杂度O(N)
2. 排序(O(NlogN)),利用二分查找: logN
3. 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:

 位图的实现

class bitset
{
    public:
    bitset(size_t bitCount)
    : _bit((bitCount>>5)+1), _bitCount(bitCount)
    {}
    // 将which比特位置1
    void set(size_t which)
    {
        if(which > _bitCount)
        return;
        size_t index = (which >> 5);
        size_t pos = which % 32;
        _bit[index] |= (1 << pos);
    }
    // 将which比特位置0
    void reset(size_t which)
    {
        if(which > _bitCount)
        return;
        size_t index = (which >> 5);
        size_t pos = which % 32;
        _bit[index] &= ~(1<<pos);
    }
    // 检测位图中which是否为1
    bool test(size_t which)
    {
        if(which > _bitCount)
        return false;
        size_t index = (which >> 5);
        size_t pos = which % 32;
        return _bit[index] & (1<<pos);
    }
    // 获取位图中比特位的总个数
    size_t size()const{ return _bitCount;}
private:
    vector<int> _bit;
    size_t _bitCount;
};
1.2应用

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

2.布隆过滤器

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。
3. 将哈希与位图结合,即布隆过滤器

2.1 概念

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。

2.2布隆过滤器的插入 

​​​​​​​

class BloomFilter
{
public:
    void Set(const K& key)
    {
        //通过3个仿函数实现哈希函数
        size_t len = X*N;
        size_t index1 = HashFunc1()(key) % len;
        size_t index2 = HashFunc2()(key) % len;
        size_t index3 = HashFunc3()(key) % len;
        _bs.set(index1);
        _bs.set(index2);
        _bs.set(index3);
    bool Test(const K& key)
    {
        size_t len = X*N;
        size_t index1 = HashFunc1()(key) % len;
        if (_bs.test(index1) == false)
            return false;
        size_t index2 = HashFunc2()(key) % len;
        if (_bs.test(index2) == false)
            return false;
        size_t index3 = HashFunc3()(key) % len;
        if (_bs.test(index3) == false)
            return false;
        return true; // 存在误判的
    }
    // 不支持删除,删除可能会影响其他值。
    void Reset(const K& key);
private:
    bitset<X*N> _bs;
};

注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。

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

2.3优点

1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无

2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

2.4缺点

1. 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再
建立一个白名单,存储可能会误判的数据)
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值