C++進階:封裝unordered_map和unordered_set以及海量數據面試題

unordered系列關聯式容器
  • unordered_map
    1、概念
    unordered_map是存儲<key,value>鍵值對的關聯式容器,允許通過key快速的索引到與其對應的value。
    在unordered_map中,鍵值通常用於惟一地標識元素,而映射值是一個對象,其內容與此鍵關聯。鍵和映射值的類型可能不同。在內部,unordered_map沒有對<key, value>按照任何特定的順序排序, 爲了能在常數範圍內找到key所對應的value,unordered_map將相同哈希值的鍵值對放在相同的桶中。unordered_map容器通過key訪問單個元素要比map快,但它通常在遍歷元素子集的範圍迭代方面效率較低。unordered_map實現了直接訪問操作符(operator[]),它允許使用key作爲參數直接訪問value。 它的迭代器至少是前向迭代器。
    2、利用哈希桶來實現unordered_map
    我們在上一篇關於開散列的介紹中,已經實現了哈希桶的基本功能,但是所實現的並不能夠封裝unordered_map,因爲unordered_map的存儲結構是<key,value>的鍵值對,而我們所實現的只是存放普通的元素,而且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<IPINT,次數>,使用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代表不存在。
    先給一個例子:
    如圖:
    在這裏插入圖片描述
    這個數組中最大的數據是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<<data%8)檢測是0還是非0,是0表示這個bit位是0,說明不存在這個數據,非0表示存在這個數據)
    這時我們使用位圖的思想處理這個面試題
    我們此時不知道具體的數據是什麼,這時無符號整形的最大的是2^32bit,此時我們使用(2 ^32)/8*2 ^10 * 2 ^10=512M的空間,將所有的數據保存起來;
    位圖在標準庫中已經提供了,叫做bitset,它提供了各種方法,例如set(將數據所映射的bit位置1)test檢驗元素是否存在在位圖中,我們可以使用:
#include <bitset>
#include <iostream>
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 <iostream>
#include <vector>
#include <assert.h>
using namespace std;
namespace Daisy
{
	template<size_t N>//N代表bit位的個數
	class bitset
	{
	public:
		bitset()
		{
			_bs.resize((N >> 3) + 1);//也就是將bit位個數/8+1就是所需字節數
		}
		//將num的bit位置1
		bitset<N>& 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<N>& 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<unsigned char> _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—數據出現多次;

  • 布隆過濾器
    布隆過濾器是一種緊湊的概率型數據結構,特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”,它是用多個哈希函數,將一個數據映射到位圖結構中。此種方式不僅可以提升查詢效率,也可以節省大量的內存空間。
    如圖:
    在這裏插入圖片描述
    1、布隆過濾器的插入:
    就是利用位圖來插入:
#pragma once
#include <iostream>
#include <bitset>
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<N> _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++標準庫提供的用來進行搜索的關聯式容器,裏面存儲的都是<key,value>的鍵值對;
    不同:
    (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》插入
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章