哈希应用及海量数据面试题

哈希的应用

位图

位图概念

  1. 面试题

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

    1. 遍历,时间复杂度O(N)

    2. 排序(O(NlogN)),利用二分查找: logN

    3. 用set/unordered_set(底层是红黑树/哈希表)存起来再查找

      以上方案的问题:数据量太大,放不到内存中

    4. 位图解决
      数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比
      特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
      image-20211126185241835

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

位图的实现

class bitset {
	private:
		vector<int> _bs;
		size_t _num;//总的位数

	public:
		bitset(size_t N)
		{
			//将所有位置为0
			_bs.resize(N/32 + 1, 0);//N/32的只会得到商,余数被省略,还有部分位没有被开辟空间,所以结果加1,将余数补上
			_num = N;
		}

		void set(size_t x)//将第x个数据的状态设置为1
		{
			//算出x在哪个数的哪一位上
			size_t index = x / 32;//求出第x位在哪个数里面
			size_t pos = x % 32;//求出第x位在该数的哪一位

			//将该位设置为1
			_bs[index] |= (1 << pos);
		}

		void reset(size_t x)//将第x个数据的状态设置为0
		{
			//同样先求出x在哪个数的哪一位上
			size_t index = x / 32;//求出第x位在哪个数里面
			size_t pos = x % 32;//求出第x位在该数的哪一位

			//将该位设置为0
			_bs[index] &= ~(1 << pos);
		}

		bool test(size_t x)//检查第x位的状态
		{
			//求出x在哪个数的哪一位
			size_t index = x / 32;
			size_t pos = x % 32;

			return _bs[index] & (1 << pos);
		}
    
        // 获取位图中比特位的总个数
        size_t size()const
        { 
            return _num;
        }

        // 位图中比特为1的个数
        size_t Count()const
        {
            int bitCnttable[256] = {
            0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
            3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
            3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
            4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
            3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
            6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
            4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
            6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
            3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
            4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
            6, 7, 6, 7, 7, 8};

            size_t size = _bs.size();
            size_t count = 0;
            for(size_t i = 0; i < size; ++i)
            {
                int value = _bs[i];
                int j = 0;

                while(j < sizeof(_bs[0]))
                {
                    unsigned char c = value;
                    count += bitCntTable[c];
                    ++j;
                    value >>= 8;
                }
            }
            return count;
         }

};

再谈面试题:将40亿个数据是否存在用位图标记出来,但实际要开42(2^32)亿个空间,可以用下面三种方法开:

  1. bitset(-1)。bitset的构造函数的参数是无符号的,所以-1实际上就对应2^32
  2. bitset(0xffffffff),用十六进制表示
  3. bitset(pow(2, 32) ),用pow函数

位图的应用

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

位图的优缺点

优点:节省空间,效率高

缺点:只能处理整型

布隆过滤器

布隆过滤器提出

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

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:一般只能处理整型,如果内容编号是字符串,就无法处理了
  3. 将哈希与位图结合,即布隆过滤器

布隆过滤器概念

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

image-20211126174533479

如果只用一种哈希算法,可能导致不同的值映射到了同一个位上,这样就会导致误判(哈希冲突),即:可能该类新闻我没有看过,但通过算法计算得到的位置,可能已经被标记成了看过,这样就导致了误判。怎么解决呢?

可以一个数据对应多个位,当这些位都为1时,就表明该数据出现了。就像上图一样,同一个字符串(也可能是其他类型的数据)通过三种哈希算法得到三个位,将这三个位都设置成1。但这只能降低冲突,并不能根除冲突

布隆过滤器的插入

image-20211126174954076

向布隆过滤器中插入:“tencent”

image-20211126174453537

// 假设布隆过滤器中元素类型为K,每个元素对应5个哈希函数
template<class K, class KToInt1 = KeyToInt1, class KToInt2 = KeyToInt2,
class KToInt3 = KeyToInt3, class KToInt4 = KeyToInt4,
class KToInt5 = KeyToInt5>

class BloomFilter
{
    public:
    BloomFilter(size_t size) // 布隆过滤器中元素个数
    	: _bmp(5*size), _size(0)
    {}
    
    bool Insert(const K& key)
    {
        size_t bitCount = _bmp.Size();

        size_t index1 = KToInt1()(key)%bitCount;
        size_t index2 = KToInt2()(key)%bitCount;
        size_t index3 = KToInt3()(key)%bitCount;
        size_t index4 = KToInt4()(key)%bitCount;
        size_t index5 = KToInt5()(key)%bitCount;

        _bmp.Set(index1); 
        _bmp.Set(index2);
        _bmp.Set(index3);
        _bmp.Set(index4);
        _bmp.Set(index5);

        _size++;
    }
private:
    bitset _bmp;
    size_t _size; // 实际元素的个数
}

布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特位一定为1。
所以可以按照以下方式进行查找:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中

bool IsInBloomFilter(const K& key)
{
    size_t bitCount = _bmp.Size();
    
    size_t index1 = KToInt1()(key)%bitCount;
    if(!_bmp.Test(index1))
    	return false;
    
    size_t index2 = KToInt2()(key)%bitCount;
    if(!_bmp.Test(index2))
    	return false;
    
    size_t index3 = KToInt3()(key)%bitCount;
    if(!_bmp.Test(index3))
    	return false;
    
    size_t index4 = KToInt4()(key)%bitCount;
    if(!_bmp.Test(index4))
    	return false;
    
    size_t index5 = KToInt5()(key)%bitCount;
    if(!_bmp.Test(index5))
    	return false;
    
    return true; // 有可能在
}

注意:布隆过滤器如果某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判
比如:在布隆过滤器中查找"alibaba"时,假设3个哈希函数计算的哈希值为:1、3、7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的

布隆过滤器实现(主要针对string类型)

namespace ysj
{
	//BKDR算法
	struct HashStr1 {
		size_t operator()(const string& str)
		{
			size_t ret = 0;
			for (int i = 0; i < str.size(); ++i)
			{
				ret = ret * 131 + str[i];
			}

			return ret;
		}
	};

	//SDBM算法
	struct HashStr2 {
		size_t operator()(const string& str)
		{
			size_t hash = 0;
			size_t i = 0;
			while (size_t ch = (size_t)(str[i]))
			{
				hash = 65599 * hash + ch;
				//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;  
				++i;
				if (i == str.size())
					break;
			}

			return hash;
		}
	};

	//RS算法
	struct HashStr3 {
		size_t operator()(const string& str)
		{
			size_t hash = 0;
			size_t magic = 63689;
			for (size_t i = 0; i < str.size(); ++i)
			{
				hash = hash * magic + str[i];
				magic *= 378551;
			}
			
			return hash;
		}
	};

	//主要实现字符串类型的布隆过滤器
	template<class T, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
	class bloomfilter {
	private:
		bitset _bs;
		size_t N;//统计bit位的长度

	public:
		bloomfilter(size_t num)
			:_bs(5*num)//再准确一点是4.3倍,效率最好,网上有大佬核实了
			,N(5*num)
		{}

		void set(const T& x)
		{
			//通过三种算法获取具体的bit位
			size_t index1 = Hash1()(x) % N;
			size_t index2 = Hash2()(x) % N;
			size_t index3 = Hash3()(x) % N;

			cout << index1 << " " << index2 << " " << index3 << endl;

			//将三个位置都设置成一
			_bs.set(index1);
			_bs.set(index2);
			_bs.set(index3);
		}

		bool test(const T& x)
		{
			//获取每个位置
			size_t index1 = Hash1()(x) % N;
			size_t index2 = Hash2()(x) % N;
			size_t index3 = Hash3()(x) % N;

			//只要有一个位置不对就表示该数据不存在
			if (_bs.test(index1) == false)
				return false;
			if (_bs.test(index2) == false)
				return false;
			if (_bs.test(index3) == false)
				return false;

			//也不一定该数据是存在的,可能是别的数据的位与该数据的位重合了,导致了误判
			//但数据不存在是可以肯定的
			return true;
		}

		//reset:不能简单地将x对应的三个bit位清零,因为有可能将其他数据的位也清零了。所以布隆过滤器不支持删除
	};

布隆过滤器删除

**布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。**因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

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

  1. 无法确认元素是否真正在布隆过滤器中
  2. 存在计数回绕

布隆过滤器优点

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

布隆过滤器缺陷

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

海量数据面试题

哈希切割

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

分析:首先我们要对ip地址的出现次数进行统计,假设使用kv模型的map解决,但这里有100G数据,无法将他们同时都放到内存中。
解法:可以先创建1000个小文件A0,A1,A2…A999,读取IP地址,计算出 i = hashstr(IP) % 1000,i是多少,就把该IP放入对应编号的小文件中,这样相同的IP地址就放入了同一个小文件,方便进行统计。
然后一次将一个小文件加载进内存,用一个map<string, int> countMap统计出该小文件中所有IP出现的次数,再用一个键值对pair<ip, int> max来保存出现次数最多的ip地址。当前小文件遍历完后,clear掉countMap,再加载下一个小文件进入内存,以此类推,最后遍历得出出现次数最多的IP地址。

image-20211128082634179

与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?

找到topK的IP:把max遍历改成大小为k的堆即可,遍历得出topK的IP。求出现次数最多的K个IP就用小堆,出现次数最少的K个IP就用大堆。

位图应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
    方案一:用两个位来表示一个数存在的次数:
    00:0次

    01:1次

    10或11:两次及以上

    方案二:在方案一思路的基础上,我们使用两个位图,第一个位图保存第一位的数据,第二个位图保存第二位的数据。统计次数时,如果该数对应的bit位上:

    1. 第一个位图为0:

      1.第二个位图为0,说明它还没有出现过,要把它设置成01,也就是把第二个位图的该位上设置为1;

      2.第二个位图为1,说明它已经出现一次了,要把它设置成10或11,也就是把第一个位图的该位上设置成1(为了方便设置成11,就不修改第二个位图了)

    2. 第一个位图为1,说明它已经出现两次了,就不需要再改变了

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

    方案一:将文件1中的整数映射到一个位图上,再将文件2上的整数进行对比,存在就说明有交集。消耗约500M内存
    方案二:将两个文件都映射到位图上,再对两个位图进行按位与,按位与的结果位图中,为1的位就是交集。消耗约1G内存

  3. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
    第一题的变形,我们要找出出现1次和两次的整数。同样用两个位来表示出现次数:00表示出现0次,01表示出现1次,10表示出现两次,11表示出现两次以上

布隆过滤器

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法

    query一般是spl查询语句或者网络请求的url等,一般是一个字符串。

    假设平均一个query的大小是30-60bytes,含有100亿个query文件的总大小就在300-600G范围

    近似算法:将文件1中的query映射到一个布隆过滤器中,读取文件2中的query,判断在不在布隆过滤器中,在就是交集。
    缺陷:交集中有些数不准确,布隆过滤器判断数据在是不准确的。

    精确算法:这两个文件都非常大,我们可以将文件切分成多个小文件,将小文件中的数据加载到内存中,然后进行对比,判断是否存在。
    这里一个文件大小是300-600G,我们可以切分1000份,一份就是300-600M,这样就可以将两个小文件加载进内存进行比较了。

    如下图:

    image-20211128091515141

    但有一个问题,就是一个小文件要与另外一个文件的1000个小文件分别进行比较找出交集,效率不高。

    优化:不再平均切分,用哈希切分:计算 i = hashstr(query) % 1000,i是多少,query就进入第Ai / Bi的小文件中,文件A/文件B都分别这样处理——相同的query就对应放到了Ai-Bi中.此时就不需要再进行多次比较,小文件Ai和Bi对应比较就可以了

  2. 如何扩展BloomFilter使得它支持删除元素的操作
    将每个位为都标记为计数器,有一个数据使用了该位就统计一次,删除一个数据就将它映射的所有位减一。但计数器占几个字节是个问题,1字节只能统计256次,万一有数据出现超过256次呢?2字节虽然可以统计6万多次,足够一个数据的统计了,但空间的占用又成为了一个问题

    image-20211128090408822

  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WoLannnnn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值