unorder(哈希-海量數據處理)

1. unordered系列關聯式容器

1.1 unordered_map

1.1.1 unordered_map的文檔介紹

  1. unordered_map是存儲<key, value>鍵值對的關聯式容器,其允許通過key快速的索引到與其對應的value。
  2. 在unordered_map中,鍵值通常用於惟一地標識元素,而映射值是一個對象,其內容與此鍵關聯。鍵和映射值的類型可能不同。
  3. 在內部,unordered_map沒有對<kye, value>按照任何特定的順序排序, 爲了能在常數範圍內找到key所對應的value,unordered_map將相同哈希值的鍵值對放在相同的桶中。
  4. unordered_map容器通過key訪問單個元素要比map快,但它通常在遍歷元素子集的範圍迭代方面效率較低。
  5. unordered_maps實現了直接訪問操作符(operator[]),它允許使用key作爲參數直接訪問value。
  6. 它的迭代器至少是前向迭代器。

1.1.2 unordered_map的接口說明

1. unordered_map的構造

函數聲明 功能介紹
unordered_map 構造不同格式的unordered_map對象

2. unordered_map的容量

函數聲明 功能介紹
bool empty() const 檢測unordered_map是否爲空
size_t size() const 獲取unordered_map的有效元素個數

3. unordered_map的迭代器

函數聲明 功能介紹
begin 返回unordered_map第一個元素的迭代器
end 返回unordered_map最後一個元素下一個位置的迭代器
cbegin 返回unordered_map第一個元素的const迭代器
cend 返回unordered_map最後一個元素下一個位置的const迭代器

4. unordered_map的元素訪問

函數聲明 功能介紹
operator[] 返回與key對應的value,沒有一個默認值

5. unordered_map的查詢

函數聲明 功能介紹
iterator find(const K& key) 返回key在哈希桶中的位置
size_t count(const K& key) 返回哈希桶中關鍵碼爲key的鍵值對的個數

注意:unordered_map中key是不能重複的,因此count函數的返回值最大爲1

6. unordered_map的修改操作

函數聲明 功能介紹
insert 向容器中插入鍵值對
erase 刪除容器中的鍵值對
void clear() 清空容器中有效元素個數
void swap(unordered_map&) 交換兩個容器中的元素

7. unordered_map的桶操作

函數聲明 功能介紹
size_t bucket_count()const 返回哈希桶中桶的總個數
size_t bucket_size(size_t n)const 返回n號桶中有效元素的總個數
size_t bucket(const K& key) 返回元素key所在的桶號

2. 底層結構

unordered系列的關聯式容器之所以效率比較高,是因爲其底層使用了哈希結構。

2.1 哈希概念

順序結構以及平衡樹中,元素關鍵碼與其存儲位置之間沒有對應的關係,因此在查找一個元素時,必須要經過關鍵碼的多次比較。順序查找時間複雜度爲O(N),平衡樹中爲樹的高度,即O(log N),搜索的效率取決於搜索過程中元素的比較次數。
理想的搜索方法:可以不經過任何比較,一次直接從表中得到要搜索的元素。 如果構造一種存儲結構,通過某種函數(hashFunc)使元素的存儲位置與它的關鍵碼之間能夠建立一一映射的關係,那麼在查找時通過該函數可以很快找到該元素。
當向該結構中:

  • 插入元素
    根據待插入元素的關鍵碼,以此函數計算出該元素的存儲位置並按此位置進行存放
  • 搜索元素
    對元素的關鍵碼進行同樣的計算,把求得的函數值當做元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功

該方式即爲哈希(散列)方法,哈希方法中使用的轉換函數稱爲哈希(散列)函數,構造出來的結構稱爲哈希表(Hash Table)(或者稱散列表)
例如:數據集合{1,7,6,4,5,9};
哈希函數設置爲:hash(key) = key % capacity; capacity爲存儲元素底層空間總的大小。
在這裏插入圖片描述
用該方法進行搜索不必進行多次關鍵碼的比較,因此搜索的速度比較快

2.2 哈希衝突

對於兩個數據元素的關鍵字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),
即:不同關鍵字通過相同哈希哈數計算出相同的哈希地址,該種現象稱爲哈希衝突或哈希碰撞。
把具有不同關鍵碼而具有相同哈希地址的數據元素稱爲“同義詞”。

2.3 哈希函數

引起哈希衝突的一個原因可能是:哈希函數設計不夠合理。 哈希函數設計原則:

  • 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0到m-1之間
  • 哈希函數計算出來的地址能均勻分佈在整個空間中
  • 哈希函數應該比較簡單
常見哈希函數
  1. 直接定製法
    取關鍵字的某個線性函數爲散列地址:Hash(Key)= A*Key + B
    優點:簡單、均勻
    缺點:需要事先知道關鍵字的分佈情況 使用場景:適合查找比較小且連續的情況
    2. 除留餘數法
    設散列表中允許的地址數爲m,取一個不大於m,但最接近或者等於m的質數p作爲除數,按照哈希函數:Hash(key) = key% p(p<=m),將關鍵碼轉換成哈希地址
    3. 平方取中法
    假設關鍵字爲1234,對它平方就是1522756,抽取中間的3位227作爲哈希地址; 再比如關鍵字爲4321,對它平方就是18671041,抽取中間的3位671(或710)作爲哈希地址
    平方取中法比較適合:不知道關鍵字的分佈,而位數又不是很大的情況
    4. 摺疊法
    摺疊法是將關鍵字從左到右分割成位數相等的幾部分(最後一部分位數可以短些),然後將這幾部分疊加求和,並按散列表表長,取後幾位作爲散列地址。
    摺疊法適合事先不需要知道關鍵字的分佈,適合關鍵字位數比較多的情況
    5. 隨機數法
    選擇一個隨機函數,取關鍵字的隨機函數值爲它的哈希地址,即H(key) = random(key),其中random爲隨機數函數。
    通常應用於關鍵字長度不等時採用此法
    6. 數學分析法
    設有n個d位數,每一位可能有r種不同的符號,這r種不同的符號在各位上出現的頻率不一定相同,可能在某些位上分佈比較均勻,每種符號出現的機會均等,在某些位上分佈不均勻只有某幾種符號經常出
    現。可根據散列表的大小,選擇其中各種符號分佈均勻的若干位作爲散列地址。
    數字分析法通常適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分佈且關鍵字的若干位分佈較均勻的情況

注意:哈希函數設計的越精妙,產生哈希衝突的可能性就越低,但是無法避免哈希衝突

2.4 哈希衝突解決

2.4.1 閉散列

閉散列:也叫開放定址法,當發生哈希衝突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那麼可以把key存放到衝突位置中的“下一個” 空位置中去。那如何尋找下一個空位置呢?

  1. 線性探測
    比如下圖的場景,現在需要插入元素44,先通過哈希函數計算哈希地址,hashAddr爲4,因此44理論上應該插在該位置,但是該位置已經放了值爲4的元素,即發生哈希衝突。
    線性探測:從發生衝突的位置開始,依次向後探測,直到尋找到下一個空位置爲止。
  • 插入
    • 通過哈希函數獲取待插入元素在哈希表中的位置
    • 如果該位置中沒有元素則直接插入新元素,如果該位置中有元素髮生哈希衝突,使用線性探測找到下一個空位置,插入新元素
      在這裏插入圖片描述
  • 刪除
    • 採用閉散列處理哈希衝突時,不能隨便物理刪除哈希表中已有的元素,若直接刪除元素會影響其他元素的搜索。比如刪除元素4,如果直接刪除掉,44查找起來可能會受影響。因此線性探測採用標記的僞刪除法來刪除一個元素。
// 哈希表每個空間給個標記
// EMPTY此位置空, EXIST此位置已經有元素, DELETE元素已經刪除
enum State{EMPTY, EXIST, DELETE}; 
線性探測的實現
#include<iostream>
#include<vector>
using namespace std;

enum State{ EMPTY, EXIST, DELETE };
template<class K,class V>
class HashTable{
	struct Elem
	{
		pair<K, V> _val;
		State _state;
	}
public:
	HashTable(size_t capacity = 3)
		: _ht(capacity)
		, _size(0)
	{
		for (size_t i = 0; i < capacity; ++i)
			_ht[i]._state = EMPTY;
	}
	bool Insert(const pair<K, V>& val){
			// 檢測哈希表底層空間是否充足
			// _CheckCapacity();
			size_t hashAddr = HashFunc(key);
			// size_t startAddr = hashAddr;
			while (_ht[hashAddr]._state != EMPTY)
			{
				if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
					return false;

				hashAddr++;
				if (hashAddr == _ht.capacity())
					hashAddr = 0;
				/*
				// 轉一圈也沒有找到,注意:動態哈希表,該種情況可以不用考慮,哈希表中元素個數
				到達一定的數量,哈希衝突概率會增大,需要擴容來降低哈希衝突,因此哈希表中元素是不會存滿的
				if(hashAddr == startAddr)
				return false;
				*/
			}

			// 插入元素
			_ht[hashAddr]._state = EXIST;
			_ht[hashAddr]._val = val;
			_size++;
			return true;
	}
	int Find(const K& key)
	{
		size_t hashAddr = HashFunc(key);
		while (_ht[hashAddr]._state != EMPTY)
		{
			if (_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
				return hashAddr;

			hashAddr++;
		}
		return hashAddr;
	}
	bool Erase(const K& key)
	{
		int index = Find(key);
		if (-1 != index)
		{
			_ht[index]._state = DELETE;
			_size++;
			return true;
		}
		return false;
	}
	size_t Size()const{ return _size; }
	bool Empty() const{ return _size == 0; }
private:
	size_t HashFunc(const K& key)
	{
		return key % _ht.capacity();
	}
private:
	vector<Elem> _ht;
	size_t _size;
};

在這裏插入圖片描述

void CheckCapacity()
{
    if(_size * 10 / _ht.capacity() >= 7)
    {
        HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));
        for(size_t i = 0; i < _ht.capacity(); ++i)
        {
            if(_ht[i]._state == EXIST)
                newHt.Insert(_ht[i]._val);
        }
        
        Swap(newHt);
    }
}

線性探測優點:實現非常簡單,
線性探測缺點:一旦發生哈希衝突,所有的衝突連在一起,容易產生數據“堆積”,即:不同關鍵碼佔據了可利用的空位置,使得尋找某關鍵碼的位置需要許多次比較,導致搜索效率降低

2. 二次探測

線性探測的缺陷是產生衝突的數據堆積在一塊,這與其找下一個空位置有關係,因爲找空位置的方式就是挨着往後逐個去找,因此二次探測爲了避免該問題,找下一個空位置的方法爲: = ( + )% m,或者: = ( - )% m。其中:i = 1,2,3…, 是通過散列函數Hash(x)對元素的關鍵碼 key 進行計算得到的位置,m是表的大小。 對於2.1中如果要插入44,產生衝突,使用解決後的情況爲:
在這裏插入圖片描述
研究表明:當表的長度爲質數且表裝載因子a不超過0.5時,新的表項一定能夠插入,而且任何一個位置都不會被探查兩次。因此只要表中有一半的空位置,就不會存在表滿的問題。在搜索時可以不考慮表裝滿的情況,但在插入時必須確保表的裝載因子a不超過0.5,如果超出必須考慮增容。
因此:閉散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷

開散列

  1. 開散列概念
    開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸於同一子集合,每一個子集合稱爲一個桶,各個桶中的元素通過一個單鏈錶鏈接起來,各鏈表的頭結點存儲在哈希表中
    在這裏插入圖片描述
    在這裏插入圖片描述
    從上圖可以看出,開散列中每個桶中放的都是發生哈希衝突的元素
  2. 開散列實現
#pragma once
#include<iostream>
#include<vector>

template<class V>
struct HashBucketNode
{
	HashBucketNode(const V& data)
	: _pNext(nullptr), _data(data)
	{}
	HashBucketNode<V>* _pNext;
	V _data;
};


template<class V>
class HashBucket
{
	typedef HashBucketNode<V> Node;
	typedef Node* PNode;
public:
	HashBucket(size_t capacity = 3) : _size(0)
	{
		_ht.resize(GetNextPrime(capacity), nullptr);
	}
	// 哈希桶中的元素不能重複
	PNode* Insert(const V& data)
	{
		// 確認是否需要擴容。。。
		// _CheckCapacity();

		// 1. 計算元素所在的桶號
		size_t bucketNo = HashFunc(data);

		// 2. 檢測該元素是否在桶中
		PNode pCur = _ht[bucketNo];
		while (pCur)
		{
			if (pCur->_data == data)
				return pCur;

			pCur = pCur->_pNext;
		}

		// 3. 插入新元素
		pCur = new Node(data);
		pCur->_pNext = _ht[bucketNo];
		_ht[bucketNo] = pCur;
		_size++;
		return pCur;
	}
	// 刪除哈希桶中爲data的元素(data不會重複),返回刪除元素的下一個節點
	PNode* Erase(const V& data)
	{
		size_t bucketNo = HashFunc(data);
		PNode pCur = _ht[bucketNo];
		PNode pPrev = nullptr, pRet = nullptr;

		while (pCur)
		{
			if (pCur->_data == data)
			{
				if (pCur == _ht[bucketNo])
					_ht[bucketNo] = pCur->_pNext;
				else
					pPrev->_pNext = pCur->_pNext;


				pRet = pCur->_pNext;
				delete pCur;
				_size--;
				return pRet;
			}
		}

		return nullptr;
	}

	PNode* Find(const V& data);
	size_t Size()const;
	bool Empty()const;
	void Clear();
	bool BucketCount()const;
	void Swap(HashBucket<V, HF>& ht;
	~HashBucket();
private:
	size_t HashFunc(const V& data)
	{
		return data%_ht.capacity();
	}
private:
	vector<PNode*> _ht;
	size_t _size;      // 哈希表中有效元素的個數
};

  1. 開散列增容
    桶的個數是一定的,隨着元素的不斷插入,每個桶中元素的個數不斷增多,極端情況下,可能會導致一個桶中鏈表節點非常多,會影響的哈希表的性能,因此在一定條件下需要對哈希表進行增容,那該條件怎麼確認呢?開散列最好的情況是:每個哈希桶中剛好掛一個節點,再繼續插入元素時,每一次都會發生哈希衝突,因此**,在元素個數剛好等於桶的個數時,可以給哈希表增容**。
	void _CheckCapacity()
	{
		size_t bucketCount = BucketCount();
		if (_size == bucketCount)
		{
			HashBucket<V, HF> newHt(bucketCount);
			for (size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx)
			{
				PNode pCur = _ht[bucketIdx];
				while (pCur)
				{
					// 將該節點從原哈希表中拆出來
					_ht[bucketIdx] = pCur->_pNext;

					// 將該節點插入到新哈希表中
					size_t bucketNo = newHt.HashFunc(pCur->_data);
					pCur->_pNext = newHt._ht[bucketNo];
					newHt._ht[bucketNo] = pCur;
					pCur = _ht[bucketIdx];
				}
			}

			newHt._size = _size;
			this->Swap(newHt);
		}
	}
  1. 開散列的思考
    1. 只能存儲key爲整形的元素,其他類型怎麼解決?
// 哈希函數採用處理餘數法,被模的key必須要爲整形纔可以處理,此處提供將key轉化爲整形的方法
// 整形數據不需要轉化
template<class T>
class DefHashF
{
public:
	size_t operator()(const T& val)
	{
		return val;
	}
};

// key爲字符串類型,需要將其轉化爲整形
class Str2Int
{
public:
	size_t operator()(const string& s)
	{
		const char* str = s.c_str();
		unsigned int seed = 131; // 31 131 1313 13131 131313
		unsigned int hash = 0;
		while (*str)
		{
			hash = hash * seed + (*str++);
		}

		return (hash & 0x7FFFFFFF);
	}
};

// 爲了實現簡單,此哈希表中我們將比較直接與元素綁定在一起
template<class V, class HF>
class HashBucket
{
	// ……
private:
	size_t HashFunc(const V& data)
	{
		return HF()(data.first) % _ht.capacity();
	}
};
  1. 除留餘數法,最好模一個素數,如何每次快速取一個類似兩倍關係的素數?
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
    53ul,         97ul,         193ul,       389ul,       769ul,
    1543ul,       3079ul,       6151ul,      12289ul,     24593ul,
    49157ul,      98317ul,      196613ul,    393241ul,    786433ul,
    1572869ul,    3145739ul,    6291469ul,   12582917ul,  25165843ul,
    50331653ul,   100663319ul,  201326611ul, 402653189ul, 805306457ul,
    1610612741ul, 3221225473ul, 4294967291ul
};
 
size_t GetNextPrime(size_t prime)
{
    size_t i = 0;
    for(; i < PRIMECOUNT; ++i)
    {
        if(primeList[i] > primeList[i])
            return primeList[i];
    }
    
    return primeList[i];
}
  1. 開散列與閉散列比較
    應用鏈地址法處理溢出,需要增設鏈接指針,似乎增加了存儲開銷。事實上: 由於開地址法必須保持大量的空閒空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.7,而表項所佔空間又比指針大的多,所以使用鏈地址法反而比開地址法節省存儲空間

模擬實現

哈希表的改造

  1. 模板參數列表的改造
// K:關鍵碼類型
// V: 不同容器V的類型不同,如果是unordered_map,V代表一個鍵值對,如果是unordered_set,V 爲 K
// KeyOfValue: 因爲V的類型不同,通過value取key的方式就不同,詳細見unordered_map/set的實現
// HF: 哈希函數仿函數對象類型,哈希函數使用除留餘數法,需要將Key轉換爲整形數字才能取模
template<class K, class V, class KeyOfValue, class HF = DefHashF<T> >
class HashBucket;
  1. 增加迭代器操作
template<class K, class V, class KeyOfValue, class HF>
class HashBucket;
 
// 注意:因爲哈希桶在底層是單鏈表結構,所以哈希桶的迭代器不需要--操作
template <class K, class V, class KeyOfValue, class HF>
struct HBIterator 
{
    typedef HashBucket<K, V, KeyOfValue, HF> HashBucket;
    typedef HashBucketNode<V>* PNode;
    typedef HBIterator<K, V, KeyOfValue, HF> Self;
 
    HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr);
    Self& operator++()
    {
         // 當前迭代器所指節點後還有節點時直接取其下一個節點
        if (_pNode->_pNext)
            _pNode = _pNode->_pNext;
        else
        {
            // 找下一個不空的桶,返回該桶中第一個節點
            size_t bucketNo = _pHt->HashFunc(KeyOfValue()(_pNode->_data))+1;
            for (; bucketNo < _pHt->BucketCount(); ++bucketNo)
            {
                if (_pNode = _pHt->_ht[bucketNo])
                    break;
            }
        }
 
        return *this;
    }
    Self operator++(int);
    V& operator*();
    V* operator->();
    bool operator==(const Self& it) const;
    bool operator!=(const Self& it) const;
    PNode _pNode;             // 當前迭代器關聯的節點
    HashBucket* _pHt;         // 哈希桶--主要是爲了找下一個空桶時候方便
};
  1. 增加通過key獲取value操作
template<class K, class V, class KeyOfValue, class HF = DefHashF<T> >
class HashBucket
{
	friend HBIterator<K, V, KeyOfValue, HF>;
	// ......
public:
	typedef HBIterator<K, V, KeyOfValue, HF> Iterator;
	//////////////////////////////////////////////////////////
	// ...
	// 迭代器
	Iterator Begin()
	{
		size_t bucketNo = 0;
		for (; bucketNo < _ht.capacity(); ++bucketNo)
		{
			if (_ht[bucketNo])
				break;
		}

		if (bucketNo < _ht.capacity())
			return Iterator(_ht[bucketNo], this);
		else
			return Iterator(nullptr, this);
	}

	Iterator End(){ return Iterator(nullptr, this); }
	Iterator Find(const K& key);
	Iterator Insert(const V& data);
	Iterator Erase(const K& key);

	// 爲key的元素在桶中的個數
	size_t Count(const K& key)
	{
		if (Find(key) != End())
			return 1;

		return 0;
	}

	size_t BucketCount()const{ return _ht.capacity(); }
	size_t BucketSize(size_t bucketNo)
	{
		size_t count = 0;
		PNode pCur = _ht[bucketNo];
		while (pCur)
		{
			count++;
			pCur = pCur->_pNext;
		}

		return count;
	}

	// ......
};

unordered_map

// unordered_map中存儲的是pair<K, V>的鍵值對,K爲key的類型,V爲value的類型,HF哈希函數類型
// unordered_map在實現時,只需將hashbucket中的接口重新封裝即可
template<class K, class V, class HF = DefHashF<K>>
class unordered_map
{
	typedef pair<K, V> ValueType;
	typedef HashBucket<K, ValueType, KeyOfValue, HF> HT;
	// 通過key獲取value的操作
	struct KeyOfValue
	{
		const K& operator()(const ValueType& data)
		{
			return data.first;
		}
	};
public:
	typename typedef HT::Iterator iterator;
public:
	unordered_map() : _ht()
	{}
	////////////////////////////////////////////////////
	iterator begin(){ return _ht.Begin(); }
	iterator end(){ return _ht.End(); }
	////////////////////////////////////////////////////////////
	// capacity
	size_t size()const{ return _ht.Size(); }
	bool empty()const{ return _ht.Empty(); }
	///////////////////////////////////////////////////////////
	// Acess
	V& operator[](const K& key)
	{
		return (*(_ht.InsertUnique(ValueType(key, V())).first)).second;
	}
	const V& operator[](const K& key)const;
	//////////////////////////////////////////////////////////
	// lookup
	iterator find(const K& key){ return _ht.Find(key); }
	size_t count(const K& key){ return _ht.Count(key); }
	/////////////////////////////////////////////////
	// modify
	pair<iterator, bool> insert(const ValueType& valye)
	{
		return _ht.Insert(valye);
	}

	iterator erase(iterator position)
	{
		return _ht.Erase(position);
	}
	////////////////////////////////////////////////////////////
	// bucket
	size_t bucket_count(){ return _ht.BucketCount(); }
	size_t bucket_size(const K& key){ return _ht.BucketSize(key); }
private:
	HT _ht;
};

哈希的應用

位圖

位圖概念
  • 面試題
    給40億個不重複的無符號整數,沒排過序。給一個無符號整數,如何快速判斷一個數是否在這40億個數中:
    • 遍歷,時間複雜度O(N)
    • 排序(O(NlogN)),利用二分查找: logN
    • 位圖解決 數據是否在給定的整形數據中,結果是在或者不在,剛好是兩種狀態,那麼可以使用一個二進制比 特位來代表數據是否存在的信息,如果二進制比特位爲1,代表存在,爲0代表不存在。比如:
  • 位圖概念
    所謂位圖,就是用每一位來存放某種狀態,適用於海量數據,數據無重複的場景。通常是用來判斷某個數據存不存在的。
位圖的實現
#include<iostream>
#include<vector>
#include<string>
using namespace std;
class bitset{
public:
	bitset(size_t bitCount)
		: _bit((bitCount >> 5) + 1), _bitCount(bitCount)
	{}
	// 將which比特位置1
	void set(size_t which)
	{
		if (which > _bitCount)
			return;
		size_t index = (which >> 5);
		size_t pos = which % 32;
		_bit[index] |= (1 << pos);
	}
	// 將which比特位置0
	void reset(size_t which)
	{
		if (which > _bitCount)
			return;
		size_t index = (which >> 5);
		size_t pos = which % 32;
		_bit[index] &= ~(1 << pos);
	}
	// 檢測位圖中which是否爲1
	bool test(size_t which)
	{
		if (which > _bitCount)
			return false;
		size_t index = (which >> 5);
		size_t pos = which % 32;
		return _bit[index] & (1 << pos);
	}
	// 獲取位圖中比特位的總個數
	size_t size()const{ return _bitCount; }
	// 位圖中比特爲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 = _bit.size();
		size_t count = 0;
		for (size_t i = 0; i < size; ++i)
		{
			int value = _bit[i];
			int j = 0;
			while (j < sizeof(_bit[0]))
			{
				unsigned char c = value;
				count += bitCnttable[c];
				++j;
				value >>= 8;
			}
		}
		return count;
	}
private:
	vector<int> _bit;
	size_t _bitCount;
};
位圖的應用
  1. 快速查找某個數據是否在一個集合中
  2. 排序
  3. 求兩個集合的交集、並集等
  4. 操作系統中磁盤塊標記

布隆過濾器

布隆過濾器提出

我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,它每次推薦時要去重,去掉那些已經看過的內容。問題來了,新聞客戶端推薦系統如何實現推送去重的? 用服務器記錄了用戶看過的所有歷史記錄,當推薦系統推薦新聞時會從每個用戶的歷史記錄裏進行篩選,過濾掉那些已經存在的記錄。 如何快速查找呢?

  1. 用哈希表存儲用戶記錄,缺點:浪費空間
  2. 用位圖存儲用戶記錄,缺點:不能處理哈希衝突
  3. 將哈希與位圖結合,即布隆過濾器
布隆過濾器概念

布隆過濾器是由布隆(Burton Howard Bloom)在1970年提出的 一種緊湊型的、比較巧妙的概率型數據結構,特點是高效地插入和查詢,可以用來告訴你 “某樣東西一定不存在或者可能存在”,它是用多個哈希函數,將一個數據映射到位圖結構中。此種方式不僅可以提升查詢效率,也可以節省大量的內存空間
在這裏插入圖片描述

布隆過濾器的插入

在這裏插入圖片描述
向布隆過濾器中插入:“baidu”
在這裏插入圖片描述
在這裏插入圖片描述

// 假設布隆過濾器中元素類型爲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; // 有可能在
}

注意:布隆過濾器如果說某個元素不存在時,該元素一定不存在,如果該元素存在時,該元素可能存在,因爲有些哈希函數存在一定的誤判

布隆過濾器刪除

布隆過濾器不能直接支持刪除工作,因爲在刪除一個元素時,可能會影響其他元素。
支持刪除的方法:將布隆過濾器中的每個比特位擴展成一個小的計數器,插入元素時給k個計數器(k個哈希函數計算出的哈希地址)加一,刪除元素時,給k個計數器減一,通過多佔用幾倍存儲空間的代價來增加刪除操作
缺陷:

  1. 無法確認元素是否真正在布隆過濾器中
  2. 存在計數迴繞
布隆過濾器優點
  1. 增加和查詢元素的時間複雜度爲:O(K), (K爲哈希函數的個數,一般比較小),與數據量大小無關
  2. 哈希函數相互之間沒有關係,方便硬件並行運算
  3. 布隆過濾器不需要存儲元素本身,在某些對保密要求比較嚴格的場合有很大優勢
  4. 在能夠承受一定的誤判時,布隆過濾器比其他數據結構有這很大的空間優勢
  5. 數據量很大時,布隆過濾器可以表示全集,其他數據結構不能
  6. 使用同一組散列函數的布隆過濾器可以進行交、並、差運算
布隆過濾器缺陷
  1. 有誤判率,即存在假陽性,即不能準確判斷元素是否在集合中(補救方法:再建立一個白名單,存儲可能會誤判的數據)
  2. 不能獲取元素本身
  3. 一般情況下不能從布隆過濾器中刪除元素
  4. 如果採用計數方式刪除,可能會存在計數迴繞問題
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章