位图与布隆过滤器

目录

一、位图的引入

二、概念

三、位图的实现 

1、set

2、reset

 3、test

4、位图实现

四、位图的应用

五、布隆过滤器

1、布隆过滤器的引入

2、概念

3、实现

* 基本框架

* 插入:set

* 查找:test

* 删除

4、布隆过滤器的优点

5、布隆过滤器的缺点

六、哈希切割


一、位图的引入

我们先来看一道题。

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

思路一:遍历

拿到这道题,我们最先想到的方法就是直接遍历了。从头到尾遍历一遍,一定可以判断出一个数是否在其中。时间复杂度为 o(N)。

思路二:排序+二分查找

时间复杂度为 排序 o(N*logN) + 二分查找 o(logN)。

思路三:位图

上面两种方法很常用,但是在面对40亿个数据时,无论是空间复杂度还是时间复杂度都很大。那么有没有比它们更加优秀的方法呢?

这里,我们就可以用位图去帮助我们解决这个问题:数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。


二、概念

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

如下图就是一个位图


三、位图的实现 

在标准库里也是实现了位图的。

上图只截取了它的三个主要操作。下面我们分别来讲一讲它的三个操作。

1、set

set的作用是将x对应的比特位置设为1,表示其存在。相当于插入的作用。

void set(size_t x)
{
	size_t i = x / 8;
	size_t j = x % 8;

	_bits[i] |= (1 << j);
}

2、reset

reset的作用是将x对应的比特位置设为0,用来表示删除。

void reset(size_t x)
{
	size_t i = x / 8;
	size_t j = x % 8;

	_bits[i] &= ~(1 << j);
}

 3、test

test的作用是用来查看x在不在。

bool test(size_t x)
{
	size_t i = x / 8;
	size_t j = x % 8;

	return _bits[i] & (1 << j);
}

4、位图实现

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

namespace zdl
{
	template<size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_bits.resize(N / 8 + 1, 0);
		}

		void set(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			_bits[i] |= (1 << j);
		}

		void reset(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			_bits[i] &= ~(1 << j);
		}

		bool test(size_t x)
		{
			size_t i = x / 8;
			size_t j = x % 8;

			return _bits[i] & (1 << j);
		}

	private:
		vector<char> _bits;
	};

	void test01()
	{
		bitset<100> bs;
		bs.set(8);
		bs.set(9);
		bs.set(20);

		cout << bs.test(8) << endl;
		cout << bs.test(17) << endl;

        bs.reset(8);
		cout << bs.test(8) << endl;
	}
}


四、位图的应用

我们直接来看相关的题。

1、给定 100 亿个整数,设计算法找到只出现一次的整数。

 我们首先来分析一下:我们需要从100亿个整数中,找到只出现一次的整数,数据量很大,那么我们就可以考虑使用位图的思想。而且这些数有三种状态,即:出现0次,1次以及1次以上。所以说我们只要保存一个数是否是三种状态的一种。但是,位图只能有两种状态:存在或者不存在。那么我们应该怎么解决这个问题呢?

这里我们就可以使用两个位图来帮助我们存储三种状态。如下图: 

template<size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		if (!_bs1.test(x) && !_bs2.test(x))//00
		{
			_bs2.set(x);//01
		}
		else if (!_bs1.test(x) && _bs2.test(x))//01
		{
			_bs1.set(x);
			_bs2.reset(x);//10
		}
		//10不变
	}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};

2、给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集?

思路:将两个文件中的整数分别映射到两个位图中,然后将两个位图按位与,得到一个位图,如果这个位图的某个位上是1,那这个就是交集。


五、布隆过滤器

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

1、布隆过滤器的引入

针对上面的问题,我们有如下的解决方法:

1、用哈希表存储用户记录,缺点:浪费空间。
2、用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。(不同的字符串,转化成整型后,可能会映射到相同的位置
而解决这类问题的最好方法,就是使用布隆过滤器。将哈希与位图结合,即布隆过滤器。

2、概念

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

根据上图,我们可以简单总结一下布隆过滤器的思想:同一个数据,根据不同的哈希函数转换成不同的整型,来映射到不同的位。也就是说同一个数据会映射多个位。这样就可以区分不同的字符串了。当然,也不排除不同字符串任然会映射到完全相同位置的可能。但是,哈希函数越多,可能性越小。

我们首先根据大佬们总结出来的算法,来确定几个哈希函数(这里我们确定三个)。

struct BKDRHash
{
    size_t operator()(const string& s)
    {
        // BKDR
        size_t value = 0;
        for (auto ch : s)
        {
            value *= 31;
            value += ch;
        }
        return value;
    }
};
struct APHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 0;
        for (long i = 0; i < s.size(); i++)
        {
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
            }
        }
        return hash;
    }
}
struct DJBHash
{
    size_t operator()(const string& s)
    {
        size_t hash = 5381;
        for (auto ch : s)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};

3、实现

* 基本框架

template<size_t N,
        class K = string,
        class HashFunc1 = BKDRHash,
        class HashFunc2 = APHash,
        class HashFunc3 = DJBHash>
class BloomFilter
{
public:
private:
	const static size_t _ratio = 5;
	bitset<_ratio* N> _bits;
};

* 插入:set

void Set(const K& key)
{
	size_t hash1 = Hash1()(key) % (_ratio * N);
	_bits.set(hash1);

	size_t hash2 = Hash2()(key) % (_ratio * N);
	_bits.set(hash2);

	size_t hash3 = Hash3()(key) % (_ratio * N);
	_bits.set(hash3);
}

* 查找:test

bool Test(const K& key)
{
	size_t hash1 = Hash1()(key) % (_ratio * N);
	if (!_bits.test(hash1))
		return false;

	size_t hash2 = Hash2()(key) % (_ratio * N);
	if (!_bits.test(hash2))
		return false;

	size_t hash3 = Hash3()(key) % (_ratio * N);
	if (!_bits.test(hash3))
		return false;

	return true;
}

* 删除

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

4、布隆过滤器的优点

1、增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关。
2、哈希函数相互之间没有关系,方便硬件并行运算。
3、布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
4、在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。 

5、布隆过滤器的缺点

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


六、哈希切割

我们直接来看一个问题

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?如何找到top K的IP?

 思路:读取每个ip,算哈希值 i = hash(ip) % 100,这个ip进入第i个小文件(相同的ip,一定进入相同的文件),利用 map<string, int>,对每个小文件统计次数。

若要寻找top K 的 ip,建一个K个值为<ip, cout>的小堆,去解决top K问题。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值