lesson4:哈希

1. unordered_map系列

  •  对比map/set区别:
    • map和set遍历是有序的,unordered系列是无序的
    • map和set是双向迭代器,unordered系列是单向
  • 虽然map/set更强大,但是在处理大量数据时,unordered系列的增删查改效率更优,尤其是查

2. 哈希应用->布隆过滤器

  •  布隆过滤器就是字符串哈希算法的一个扩展,底层是用位图来标记的
  • 主要通过一个值映射多个位置标记来解决哈希冲突
  •  布隆过滤器主要应用在黑名单
    • 名字不在黑名单上,直接返回,不存在误判
    • 名字在黑名单上,需要再到数据库中查找,存在误判
    • 理论而言:一个值映射的位置越多,误判概率越低,但是也不敢映射太大,
      映射位越多,那么空间消耗的就越多 

2.1 公式

  • k : 表示哈希函数个数,m: 为布隆过滤器长度(开闭空间大小),
    n: 表示插入元素的个数,p : 表示误判率
  •  ln2取0.7,假设k给3个,3/0.7 * n = m,4.2 * n = m
  • 空间大小 = 5 * n个比特位

 2.2 对布隆过滤器的补充说明

  • 它一般不支持删除的,支持可能会导致影响其他值
    • 比如上面的那个,删除百度再查询美团就找不到美团了
  • 多个位表示一个位置,做计数->就可以支持删除,但是这样的话,空间消耗更多,优势被削弱了

  2.3 布隆过滤器的实现 && 验证误判率

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

// 定义3个字符串转整数的仿函数: 重载运算符()
struct HashBKDR
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

struct HashAP
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (size_t i = 0; i < key.size(); i++)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
			}
		}
		return hash;
	}
};

struct HashDJB
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t hash = 5381;
		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}

		return hash;
	}
};


// N表示准备要映射N个值
template<size_t N,
	class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter
{
public:
	// 标记值: 这里采取一个值映射3个位置,
	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);
	}

	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; // 可能存在误判
	}

	// 能否支持删除->
	void Reset(const K& key);

private:
	const static size_t _ratio = 5;
	// 定义存储空间
	bitset<_ratio* N>* _bits = new bitset<_ratio* N>;
};

void TestBloomFilter1()
{
	BloomFilter<10> bf;
	string arr1[] = { "苹果", "西瓜", "阿里", "美团", "苹果", "字节", "西瓜", "苹果", "香蕉", "苹果", "腾讯" };

	for (auto& str : arr1)
	{
		bf.Set(str);
	}

	for (auto& str : arr1)
	{
		cout << bf.Test(str) << " " ;
	}
	cout << endl << endl;

	string arr2[] = { "苹果111", "西瓜", "阿里2222", "美团", "苹果dadcaddxadx", "字节", "西瓜sSSSX", "苹果 ", "香蕉", "苹果$", "腾讯" };

	for (auto& str : arr2)
	{
		cout << str << ":" << bf.Test(str) << endl;
	}
}

void TestBloomFilter2()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<N> bf;
	//cout << sizeof(bf) << endl;

	vector<string> v1;
	string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";

	for (size_t i = 0; i < N; ++i)
	{
		v1.push_back(url + std::to_string(1234 + i));
	}

	for (auto& str : v1)
	{
		bf.Set(str);
	}

	// 相似
	vector<string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		std::string url = "http://www.cnblogs.com/-clq/archive/2021/05/31/2528153.html";
		url += std::to_string(rand() + i);
		v2.push_back(url);
	}

	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.Test(str))
		{
			++n2;
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	vector<std::string> v3;
	for (size_t i = 0; i < N; ++i)
	{
		string url = "zhihu.com";
		url += std::to_string(rand() + i);
		v3.push_back(url);
	}

	size_t n3 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n3;
		}
	}
	cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
}

 

  •  bitset<_ratio* N>* _bits = new bitset<_ratio* N>;
    • bitset是在栈上开辟空间的,当空间不够就需要在堆上申请空间

2.4 布隆过滤器的缺陷

  • 误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再自建一个白名单,存储可能会误判的数据)
  • 不能获取元素本身
  • 一般情况下不能从布隆过滤器中删除元素

3. 海量数据处理面试题 

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

  •  这个问题用搜索树哈希表都不太行,内存中存不下
  •  排序 + 二分查找,虽然看似可行,但是因为数据太大,只能放在磁盘文件上,不好支持二分查找,效率不行

3.1.1 位图:直接定址法

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

  • 一个比特位映射标记值,1就是在,0就是不在
  • 1G = 1024MB = 1024 * 1024 KB = 1024 * 1024 *1024Byte = 2 ^ 30(约等于10亿字节)
  • 一个char是8个比特位,32位下整数最大值是2 ^ 32 - 1,需要开2 ^ 32 - 1个字节的空间(512MB)
  •  位图跟值的个数没有关系,因为是直接映射,只跟数据范围有关
#pragma once
#include <iostream>
#include <vector>
using namespace std;

namespace bit
{
	// 这里需要根据实际情况来开空间
	template<size_t N>// 定义非类型模板参数
	class bitset
	{
	public:
		bitset()
		{
			// 每次多开一个字节: 防止出现 10 / 8 = 1...2的情况
			_bits.resize(N / 8 + 1, 0);
		}

		// 标记值
		void set(size_t x)
		{
			size_t i = x / 8;// 第几个char
			size_t j = x % 8;// char中的第几个位(0 ~ 7)

			_bits[i] |= (1 << j);// 存在标记为1,不存在标记为0
		}

		// 取消标记
		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 test_bit_set1()
	{
		bitset<100> bs1;
		bs1.set(8);
		bs1.set(9);
		bs1.set(20);

		cout << bs1.test(8) << endl;
		cout << bs1.test(9) << endl;
		cout << bs1.test(20) << endl;

		bs1.reset(8);
		bs1.reset(9);
		bs1.reset(20);

		cout << bs1.test(8) << endl;
		cout << bs1.test(9) << endl;
		cout << bs1.test(20) << endl;
	}

	void test_bit_set2()
	{
		bitset<-1> bs1;// 开整形的最大值个比特位
		//bitset<0xffffffff> bs2;
	}
}

  •  bitset<-1> bs1;// 开整形的最大值个比特位 

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

  • 还是用位图解决,只不过这里要用2个比特位
  • 00:表示只出现0次,01:表示出现了1次,10:表示出现2次及以上

3.2.1 双位图解决 

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

namespace bit
{
	template<size_t N>
	class twobitset
	{
	public:
		// 标记值
		void set(size_t x)
		{
			// 双位图
			bool inset1 = _bs1.test(x);
			bool inset2 = _bs2.test(x);

			// 00: 出现了0次
			if (inset1 == false && inset2 == false)
			{
				// -> 01
				_bs2.set(x);
			}
			// 01: 出现了1次
			else if (inset1 == false && inset2 == true)
			{
				// ->10
				_bs1.set(x);
				_bs2.reset(x);
			}
			// 10: 出现了2次及以上, 不处理
			else{
				;
			}
		}

		// 为了测试写的: 打印出为出只出现一次的值的
		void print_once_num()
		{
			for (size_t i = 0; i < N; ++i)
			{
				if (_bs1.test(i) == false && _bs2.test(i) == true)
				{
					cout << i << endl;
				}
			}
		}

	private:
		bitset<N> _bs1;
		bitset<N> _bs2;
	};

	void test_bit_set3()
	{
		int a[] = { 3, 4, 5, 2, 3, 4, 4, 4, 4, 
        12, 77, 65, 44, 4, 44, 99, 33, 33, 33, 6, 5, 34, 12 };

		twobitset<100> bs;// 双位图
		for (auto e : a)
		{
			bs.set(e);
		}

		bs.print_once_num();
	}
}

 

  •  使用双位图就可以很好的解决,库里面也有bitset

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

3.3.1 哈希切分 

 

  • 假设有两文件A和B,1G = 10亿字节,3000亿字节 = 300G
  • 关键点: 相同的query,一定进入相同编号的小文件

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

3.4.1 哈希切割

  • 关键点: 相同的ip,一定进入同一个小文件 
  •  依次使用map<string,int> 对每个小文件统计次数
  • topK,建立一个K个值为<ip,count>的小堆
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值