C++进阶:封装unordered_map和unordered_set以及海量数据面试题

unordered系列关联式容器
  • unordered_map
    1、概念
    unordered_map是存储键值对的关联式容器,允许通过key快速的索引到与其对应的value。
    在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。 它的迭代器至少是前向迭代器。
    2、利用哈希桶来实现unordered_map
    我们在上一篇关于开散列的介绍中,已经实现了哈希桶的基本功能,但是所实现的并不能够封装unordered_map,因为unordered_map的存储结构是的键值对,而我们所实现的只是存放普通的元素,而且unordered_map的一些接口(例如插入、删除)也与哈希桶的不同,因此,在这里我们将哈希桶的功能再完善一些
    我们通过仿函数的方式按照key值获取到value,也就是增加一个模板类型KeyOfValue
    让它实现通过key来获取value
  • unordered_set
    unordered_set的实现只需要将unordered_map的实现改造一下即可(注意unordered_set没有通过下标来获取的运算符重载)
    源代码(github):
    https://github.com/wangbiy/C-3/commit/07794688cb909369130761528ed1d4fcff06ed14
海量数据面试题
  • 1、哈希切割
    给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
    如果我们按照传统的方式进行两个循环来统计,由于文件比较大,十分耗费时间,如果采用归并排序的方法也没有办法很好的解决这个问题,这时就要用到哈希切割
    (1)哈希切割就是将一个大文件,利用哈希原理将其分割成为若干个小文件(根据用户的限制估算被分割成文件的份数),然后获取每条IP地址,将IP地址(字符串)转换为整数(网络部分的库函数),然后将每个IP地址映射到相应文件中:IPINT%分割份数,此时相同的数据被分到同一个小文件中;然后统计IP地址的次数:即构建键值对unordered_map,使用unordered_map来统计
    (2)找到出现次数最多的IP地址
    使用multimap<次数,IP地址>(底层是红黑树,已经根据次数排序好了)来查找
    这样我们一共只遍历了一次,提高了性能,I/O次数降低
    与上题条件相同,如何找到top K的IP?如何直接用Linux系统命令实现?
    要建一个小堆—priority_queue(放的元素的类型是pair<次数,IP地址>),通过比较来找前k个次数最多的IP地址,即建立一个K个元素的小堆
    用Linux命令实现:sort log_file | uniq -c | sort -nr k1,1 | head -10;
  • 2、哈希应用:位图
    例如面试题:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中
    首先40亿整形数据大概估算是15G的大小,根本无法直接一次加载到内存中(一般计算机的内存是4G或者8G,还给系统内存分了2G的空间),所以如果采用遍历的方式来查找,需要分割这个数据,效率太低;
    还有一种方法是进行排序,然后进行二分查找,但是这种方式还要加载数据,效率太低;
    因此我们需要位图来解决这种问题
    也就是数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。
    先给一个例子:
    如图:
    C++进阶:封装unordered_map和unordered_set以及海量数据面试题_第1张图片
    这个数组中最大的数据是22,因此至少要给22个bit位,但是内存的最小单位是字节,因此需要给3个字节,共24个bit位,对所有的bit位从0开始编号;
    此时的操作就是:
    (1)估算所需的bit位数—保证将所有的数据映射到位图中,
    (2)将所有的bit位初始化为0
    (3)将数据映射到位图中
    要找到元素所在的字节,只需要array[data/8],就可以找到数据对应的字节,然后在该字节中找所在的bit位,只要data%8就是数据对应在该字节中的第几个bit位,然后如果这个元素存在,将该bit位置1(1<<(data%8)位,然后array[data/8]|=1<<(data%8)即可将该bit位置1);
    在查找指定数据时,只要先找到所在的字节(array[data/8]),然后data%8找到所在的bit位,然后检测该bit位是不是1,(如何检测:只要这个字节(array[data/8])&(1< 这时我们使用位图的思想处理这个面试题
    我们此时不知道具体的数据是什么,这时无符号整形的最大的是2^32bit,此时我们使用(2 ^32)/8*2 ^10 * 2 ^10=512M的空间,将所有的数据保存起来;
    位图在标准库中已经提供了,叫做bitset,它提供了各种方法,例如set(将数据所映射的bit位置1)test检验元素是否存在在位图中,我们可以使用:
#include 
#include 
using namespace std;
void TestBitSet()
{
	bitset<100> bs;//一共100个bit位
	int array[] = { 1, 3, 7, 4, 12, 16, 19, 13, 22, 18 };
	for (auto e : array)
		bs.set(e);//将数据映射到位图中
	cout << bs.count() << endl;//计算有多少个数据
	cout << bs.size() << endl;//bit位总个数
	if (bs.test(13))//检验这个元素在不在位图中
		cout << "13 is in bitset" << endl;
	else
		cout << "13 is not in bitset" << endl;
	bs.reset(13);//将这个bit位清0
	if (bs.test(13))
		cout << "13 is in bitset" << endl;
	else
		cout << "13 is not in bitset" << endl;
}
int main()
{
	TestBitSet();
	return 0;
}

接下来我们模拟实现一个位图:

#pragma once
#include 
#include 
#include 
using namespace std;
namespace Daisy
{
	template//N代表bit位的个数
	class bitset
	{
	public:
		bitset()
		{
			_bs.resize((N >> 3) + 1);//也就是将bit位个数/8+1就是所需字节数
		}
		//将num的bit位置1
		bitset& set(size_t num)
		{
			assert(num < N);
			//计算num在哪一个字节中
			size_t index = num>>3;//也就是/8
			size_t pos = num % 8;//计算bit位
			_bs[index] |= (1 << pos);
			return*this;
		}
		//将num的bit位置0
		bitset& reset(size_t num)
		{
			assert(num < N);
			size_t index = num >> 3;
			size_t pos = num % 8;
			_bs[index] &= ~(1 << pos);//置0就是将1向左移动pos个bit位,然后取反,就只有pos这个位置逻辑与的是0,就置成了0
			return *this;
		}
		bool test(size_t num)const
		{
			assert(num < N);
			//计算num在哪一个字节中
			size_t index = num >> 3;//也就是/8
			size_t pos = num % 8;//计算bit位
			return 0!=(_bs[index] & (1 << pos));
		}
		size_t size()const
		{
			return N;
		}
		size_t count()const//总共有多少个bit位是1
		{
			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 };//对应的是形如0,1,2,3,4,5的对应bit位有几个,比如0是0个bit位,1是1个比特位,2是1个比特位
			size_t szcount = 0;
			for (auto e : _bs)
			{
				szcount += bitCnttable[e];
			}
			return szcount;
		}
	private:
		vector _bs;//我们使用的是unsigned char是一个字节一个字节的,使用unsigned是为了防止负数的出现,也就是8个bit,如果是整形,就是32个bit了
	};
}
void TestBitSet()
{
	Daisy::bitset<100> bs;//一共100个bit位
	int array[] = { 1, 3, 7, 4, 12, 16, 19, 13, 22, 18 };
	for (auto e : array)
		bs.set(e);//将数据映射到位图中
	cout << bs.count() << endl;//计算有多少个数据
	cout << bs.size() << endl;//bit位总个数
	if (bs.test(13))//检验这个元素在不在位图中
		cout << "13 is in bitset" << endl;
	else
		cout << "13 is not in bitset" << endl;
	bs.reset(13);//将这个bit位清0
	if (bs.test(13))
		cout << "13 is in bitset" << endl;
	else
		cout << "13 is not in bitset" << endl;
}
int main()
{
	TestBitSet();
	return 0;
}

源代码(github):
https://github.com/wangbiy/C-3/tree/master/test_2019_11_18/test_2019_11_18
位图的应用:
快速查找某个数据是否在一个集合中;排序(前提是数据不能重复,将元素映射到位图中之后,就可以回收数据就是有序的);求两个集合的交集、并集等(例如集合1:2 3,集合2:2 4,分别映射到位图中,然后逻辑与之后所得的结果,哪个bit位是1,就是对应的元素,即交集);操作系统中磁盘块标记;
例如:
(1)给定100亿个整数,设计算法找到只出现一次的整数?
100亿整数,使用2bit位统计,需要1.25*2G的内存,先进行哈希切割,切成10份,然后执行以下操作:
使用两个bit位代表一个数据的状态信息,例如:00—数据不存在、01—数据只出现一次、10—数据出现多次;
整个整数数据集合用位图映射,那么2个bit位代表一个数据,一个字节存4个数据,也就是来将位图2个2个bit位来遍历一遍,如果是00,表示没有出现,如果是01,表示第一次出现,如果是10表示出现多次,如果是11也是出现多次
(2)给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
只需要用1bit位来表示,100亿整数需要1.25G的内存,因为两个文件需要2.5G,同样,我们可以哈希切分,分为10份,使用两个位图统计,将位图结果按位与,就可以得到交集。
(3)位图的应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有数据
这个也是使用2个bit位来表示,00—数据不存在、01—数据只出现一次、10—数据出现多次;

  • 布隆过滤器
    布隆过滤器是一种紧凑的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
    如图:
    C++进阶:封装unordered_map和unordered_set以及海量数据面试题_第2张图片
    1、布隆过滤器的插入:
    就是利用位图来插入:
#pragma once
#include 
#include 
using namespace std;
namespace Daisy
{
	template < size_t N, class T
						 class HF1, 
	                     class HF2, 
						 class HF3, 
						 class HF4, 
						 class HF5>
	class BloomFilter
	{
	public:
		BloomFilter()
			:_size(0)
		{}
		bool Insert(const T& data)
		{
			size_t index1 = HF1()(data) % N;
			size_t index2 = HF2()(data) % N;
			size_t index3 = HF3()(data) % N;
			size_t index4 = HF4()(data) % N;
			size_t index5 = HF5()(data) % N;
			_bs.set(index1);
			_bs.set(index2);
			_bs.set(index3);
			_bs.set(index4);
			_bs.set(index5);
			++_size;
		}
		bool IsIn(const T& data)
		{
			size_t index = HF1()(data)%N;
			if (_bs.test(index))//如果这个bit位是0,这个数据不在
				return false;
			index = HF2()(data) % N;
			if (_bs.test(index))//如果这个bit位是0,这个数据不在
				return false;
			index = HF3()(data) % N;
			if (_bs.test(index))//如果这个bit位是0,这个数据不在
				return false;
			index = HF4()(data) % N;
			if (_bs.test(index))//如果这个bit位是0,这个数据不在
				return false;
			index = HF5()(data) % N;
			if (_bs.test(index))//如果这个bit位是0,这个数据不在
				return false;
			return true;//可能在
		}
	private:
		bitset _bs;
		size_t _size;
	};
}

==注意:==布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
例如:
比如字符串hello计算出来的是1 4 7,它映射到对应的bit位,字符串haha计算出来的是3 4 8,它映射到对应的bit位,那么如果我们查找haha,计算出来是3 4 7,可以找到,但是如果我们找hi,计算出来是1 3 7 ,它和其他元素的bit位重叠,此时布隆过滤器告诉该元素存在,但其实该元素是不存在的。
源代码(github):
https://github.com/wangbiy/C-3/commit/df39c08fd55670c1cbc1b1a677ba0e14580849c4
2、布隆过滤器的删除
布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
例如上述要删掉hello,即将它所对应的bit位置0,这时1 4 7 这三个位置都是0,这时“haha”元素也被删除了,产生了问题
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。
3、布隆过滤器的优点
(1) 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
(2)哈希函数相互之间没有关系,方便硬件并行运算
(3) 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
(4)在能够承受一定的误判时,布隆过滤器比其他数据结构有着很大的空间优势
(5)数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
(6) 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
4、 布隆过滤器缺陷
(1) 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白名单,存储可能会误判的数据)
(2)不能获取元素本身
(3) 一般情况下不能从布隆过滤器中删除元素
(4) 如果采用计数方式删除,可能会存在计数回绕问题(例如如果是char类型,它的范围是-128-127,如果给127+1就是-127了,产生了回绕)
面试题:
(1)例如:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
因为我们不确定数据类型,不能直接使用位图,所以我们需要使用布隆过滤器。
经过哈希切割,再布隆,每次两个布隆结果按位与记录,
对于近似算法就是使用普通的布隆过滤器就能做到近似准确得到交集
对于精确算法,将布隆进行扩展,一个数据映射n个位这样虽然占用的空间较大,但是出现误判的几率比较小。
(2)倒排索引
通俗地来讲,倒排索引就是通过value来找key,常被用于全文检索系统中的一种单词文档映射结构,现代搜索引擎绝大多数都是使用倒排索引来进行构建索引的,用户在搜索时查找信息往往只输入信息中的某个属性关键字来进行查找,倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两部分组成:“单词词典”和“倒排文件”。
例如给上千个文件,每个文件大小为1K—100M。给n个词,设计算法对每个词找到所有包含它的文件,你只有100K内存
因为这个题有内存限制,我们将n个单词进行分组—》M组----》每一组对应一个unordered_set—》哈希桶(这样相同的单词被放在同一个哈希桶中),将100k内存分为两份,一份用来存哈希桶,一份用来存放文件(有时候可能内存不够,毕竟只有100k的内存,如果对结果是取一个近似的结果的话,可以将哈希桶换成布隆过滤器);然后读取文件,从文件中获取一个单词word,检测word是否在哈希桶中出现,如果出现,说明单词包含在文件中,否则去下一个哈希桶中找word。

  • unordered_map与map的相同和不同
    相同:都是C++标准库提供的用来进行搜索的关联式容器,里面存储的都是的键值对;
    不同:
    (1)map是关于key有序的,而unordered_map不一定是关于key有序的;
    (2)map查找的时间复杂度是O(log2n),unordered_map查找的时间复杂度是O(1);
    (3)map是C++98提出的关联式容器,而unordered_map是C++11中提出的关联式容器;
    (4)map的迭代器移动是按照中序遍历来移动的,unordered_map是逐个桶检测的;
    (5)unordered_map有关于桶的操作和哈希函数,map没有;
    (6)map的应用场景是要求有序,unordered_map不关心是否有序,关心的是查找效率;
    (7)map的底层数据结构是红黑树,unordered_map的底层数据结构是哈希桶;
    (8)既然底层结构不同,那么相关的插入和查找操作就不同,红黑树的插入操作是:
    1》按照二叉搜索树的特性找插入位置
    2》插入结点
    3》检测是否违反红黑树的性质
    哈希桶的插入操作是:
    1》通过哈希函数计算桶号
    2》检测是否发生哈希冲突----key不能重复
    3》插入

你可能感兴趣的:(#,C++)