哈希表应用

位图

概念
  • 背景:现在有这样一道题,给 40 亿个不重复的未排序的无符号整数,输入一个无符号整数,请你判断一下这个数是否在这 40 亿个数中;
    这个题的解法可以有很多种:直接遍历查找、先排序再二分查找等等,但是有一个问题,那就是你这些操作的前提是要先将这些数据存入内存中,但是这么庞大的数据肯定是放不下的,那么你该怎么办呢?
    其实我们并不需要存储数据的完整值,我们只需要标记一下这个数是否存在,存在或者不存在这是两个状态,因此一个比特位就可以完成这件事;
  • 概念:所谓位图,就是使用内存中的每一个比特位来存放数据的状态(0——状态 1,1——状态 2),适用于海量数据、数据无重复的场景,通常是用来判断某个数据存不存在(0——不存在,1——存在);
  • 性质:位图中的每一个比特位代表了一个数据,这是一 一对应的关系,不存在冲突,所以也可以看做是直接定址法;
  • 使用
    1. 首先确定环境,在 32 位的环境下,整型数据 int 占据 32 位,所以一个整数可以记录 32 个数据的状态;
    2. 其次我们需要确定数据的位置,一个数据就需要一个比特位,所以先获取所有数据中的最大值 max,此时我们就需要创建 max 个比特位;
    3. max 个比特位至少需要 needint = max / 32 + 1 个整型数据才能表示,所以创建一个大小为 needint 数组来表示位图;
    4. 数组下标为 0 的整数的每一比特位可以依次表示 0 ~ 31 这些数据的状态,数组下标为 1 的整数的每一比特位可以依次表示 32 ~ 63 这些数据的状态,以此类推;
      在这里插入图片描述
实现
//位图的模拟实现
class BitSet {
public:
	//构造函数
	BitSet(const size_t& count)
		//加一代表了冗余位,就是说:不满32个数据,也要用一个整形来表示
		:_bit(count / 32 + 1)
		,_bitcount(count)
	{}
	//存储信息
	void set(size_t num) {
		//参数合法性检验
		if (num > _bitcount)
			return;
		//确定是哪个整型数据
		int idx1 = num / 32;
		//确定是该整型数据的哪一位
		int idx2 = num % 32;
		//然后将该位置为1
		_bit[idx1] = _bit[idx1] | (1 << idx2);
	}
	//查找信息
	bool test(size_t num) {
		//参数合法性检验
		if (num > _bitcount)
			return false;
		//确定是哪个整型数据
		int idx1 = num / 32;
		//确定是该整型数据的哪一位
		int idx2 = num % 32;
		//然后判断该位是否为1,是1则存在,否则不存在
		return _bit[idx1] & (1 << idx2);
	}
	//删除信息
	void reset(size_t num) {
		//参数合法性检验
		if (num > _bitcount)
			return;
		//确定是哪个整型数据
		int idx1 = num / 32;
		//确定是该整型数据的哪一位
		int idx2 = num % 32;
		//将该位置为0即可
		_bit[idx1] = _bit[idx1] & ~(1 << idx2);
	}
	//获取位图中有效比特位的个数
	size_t count()const {
		return _bitcount;
	}
private:
	//整形数组,其中每个数据的每一位都代表了一个数据的状态
	vector<int> _bit;
	size_t _bitcount;
};

布隆过滤器

概念
  • 背景:我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,不过它每次推荐时要进行去重,就是去掉那些已经看过的内容,那么问题来了,新闻客户端推荐系统如何实现推送去重的?
    做法:用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录,可是该如何快速查找呢?
    1. 用哈希表存储用户记录,缺点:浪费空间;
    2. 用位图存储用户记录,缺点:不能处理哈希冲突;
    3. 将哈希与位图结合,即布隆过滤器;
  • 概念:布隆过滤器是由布隆(Burton Howard Bloom)在 1970 年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中,此种方式不仅可以提升查询效率,也可以节省大量的内存空间;
    简言之,就是用 n 个哈希表对每一个数据计算出 n 个位置,然后将这 n 个位置对位图总容量取余得到 n 个新的位置,再将位图中这 n 个新的位置置为 1 即可;
    在这里插入图片描述
  • 性质:布隆过滤器其实就是相当于是除留余数法的哈希表,只是这个表中存放的不是数据,而是数据的状态,并且数据的位置并不是由一个哈希函数确定,而是由多个哈希函数共同确定;
  • 缺点:
    • 可能会造成误判,当我们判断一个数据是否存在时,先对数据通过哈希函数(假设有三个)计算出位置,假设这些位置为 d1、d2、d3,如果这三个位置中只要有一个等于 0,那么则说明这个数据不存在,这是可以确定的,但是如果这三个位置全为 1,那么就能说明它存在吗?
      这是不可以的,因为可能会有其他数据最终使得这三个位置变为 1,所以这是不能确定的,不过这种情况的几率很小,布隆过滤器可以保证绝大多数情况是正确的,所以可以放心使用;
    • 布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素,一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计数器 ( k 个哈希函数计算出的哈希地址) 加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储空间的代价来增加删除操作,这十分的麻烦;
  • 优点:
    1. 增加和查询元素的时间复杂度为:O(K) (K 为哈希函数的个数,一般比较小),与数据量大小无关;
    2. 哈希函数相互之间没有关系,方便硬件并行运算;
    3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势;
    4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势;
    5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能;
    6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算;
实现
//提供给布隆过滤器的哈希函数1
struct HFun1
{
    size_t operator()(const std::string& str)
    {
        size_t hash = 0;
        for (auto& ch : str)
        {
            hash = hash * 131 + ch;
        }
        return hash;
    }
};
//提供给布隆过滤器的哈希函数2
struct HFun2
{
    size_t operator()(const std::string& str)
    {
        size_t hash = 0;
        for (auto& ch : str)
        {
            hash = hash * 65599 + ch;
        }
        return hash;
    }
};
//提供给布隆过滤器的哈希函数3
struct HFun3
{
    size_t operator()(const std::string& str)
    {
        size_t hash = 0;
        size_t magic = 63689;
        for (auto& ch : str)
        {
            hash = hash * magic + ch;
            magic *= 378551;
        }
        return hash;
    }
};
//布隆过滤器的简单模拟实现
template<class T, class HFun1, class HFun2, class HFun3>
class BloomFilter
{
public:
    //k = (m / n) * ln2
    //k:哈希函数个数
    //m:需要的比特位大小
    //n:元素个数
    //m = k * n / ln2
    //n 表示元素个数,布隆这里不用元素最大上限作为位图的大小,因为可能会造成大量数据浪费,这里利用二次哈希,节省空间
    

    //构造函数
    BloomFilter(size_t number)
        :_bitCount(5 * number)
        , _bs(_bitCount)
    {}
    //存储信息:使用多个比特位存储信息
    void Set(const T& data){
        //使用三个哈希函数来计算出三个位置,然后再用这三个位置对总容量取余
        int index1 = HFun1()(data) % _bitCount;
        int index2 = HFun2()(data) % _bitCount;
        int index3 = HFun3()(data) % _bitCount;
        //然后将这三个位置插入位图中
        _bs.set(index1);
        _bs.set(index2);
        _bs.set(index3);
    }

    //查找函数,可能会有误判
    bool Find(const T& data){
        //使用三个哈希函数来计算出三个位置,然后再用这三个位置对总容量取余
        int index1 = HFun1()(data) % _bitCount;
        int index2 = HFun2()(data) % _bitCount;
        int index3 = HFun3()(data) % _bitCount;
        //如果这三个值中只要有一个不为1,则说明不存在
        if (!_bs.Find(index1) || !_bs.Find(index2) || !_bs.Find(index3)){
            return false;
        }
        //如果三个值全为1,则说明可能存在
        return true;
    }
    //布隆为了防止误判不提供删除操作

private:
    BitSet _bs;
    size_t _bitCount;
};

海量数据问题

哈希应用
  • 给两个文件,分别有 100 亿个 query,我们只有 1G 内存,如何找到两个文件交集?分别给出精确算法和近似算法;
  • 精确结果:
    错误:注意该题不能直接使用位图,因为:假设将文件 1 中的每条 query 映射到位图中,然后检测第二个文件中的 query 是否在位图中出现过,在用 query 对位图进行操作时,必须将 query 转换为整形数字,而两个不同的 query 可能会转化为同一个记录,再到位图中进行查找时,可能就会认为这两条 query 是同一个,而将其当成交集,就会出错,导致结果不精确;
    正确:采用哈希切割进行解决:
    1. 将文件 1 进行哈希切割,将每条 query 按照切割结果放到对应的文件中;
    2. 对文件 2 中的 query 进行转化处理,看起能落在那个文件中,然后在该文件中检查该 query 是否出现过,如果出现过,则是交集,否则不是交集,对文件 2 中的每条 query 进行该种操作,最终就可以找到交集;
  • 近似结果:
    采用位图可以得到近似结果,不过误差可能大,因为一条记录对应一个比特位,冲突的概率比较多;
    采用布隆过滤器,用多个比特位表示一条query,可以降低出错的概率;
位图应用
  • 1 个文件有 100 亿个 int,1G 内存,设计算法找到出现次数不超过 2 次的所有整数;
  • 采用两个比特位表示一个数据
    00:表示未出现
    01:表示出现一次
    10:表示出现两次
    11:表示出现多次
    将所有的数据按照这样的方式进行映射:如果该数据对应的两个比特位是 00,将其改为 01;如果是 01,将其改为 10;如果是 10,将其改为 11;
    按照上述方式映射完成后,两个两个比特位进行统计,只要这两个比特位为 01 或者 10 即为次数不超过两次的数据;

  • 给两个文件,分别有 100 亿个整数,我们只有 1G 内存,如何找到两个文件交集?
  • 1. 将两个文件中的数据分别映射到两个位图中,一个位图映射完的那个文件数据需要 512M 空间;
    2. 将两个位图对应的字节进行逻辑与;
    3. 统计与之后位图中为 1 的比特位,该比特位对应的数字即为两个文件的交集;

  • 给定 100 亿个整数,设计算法找到只出现一次的整数?
  • 这个题的做法也是用两个比特位来查找;
布隆过滤器
  • 如何扩展 BloomFilter 使得它支持删除元素的操作;
  • 不使用比特位表示数据状态,可以将比特位扩展成整形变量,在插入数据时,该位置每被使用一次,就给该位置计数加 1,删除数据时,给该位置数据减一即可,这样每删除一个数据,即使有位置重复,也不会对其他数据造成影响;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值