1. unordered系列關聯式容器
1.1 unordered_map
1.1.1 unordered_map的文檔介紹
- unordered_map是存儲<key, value>鍵值對的關聯式容器,其允許通過key快速的索引到與其對應的value。
- 在unordered_map中,鍵值通常用於惟一地標識元素,而映射值是一個對象,其內容與此鍵關聯。鍵和映射值的類型可能不同。
- 在內部,unordered_map沒有對<kye, value>按照任何特定的順序排序, 爲了能在常數範圍內找到key所對應的value,unordered_map將相同哈希值的鍵值對放在相同的桶中。
- unordered_map容器通過key訪問單個元素要比map快,但它通常在遍歷元素子集的範圍迭代方面效率較低。
- unordered_maps實現了直接訪問操作符(operator[]),它允許使用key作爲參數直接訪問value。
- 它的迭代器至少是前向迭代器。
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 哈希衝突
即:不同關鍵字通過相同哈希哈數計算出相同的哈希地址,該種現象稱爲哈希衝突或哈希碰撞。
把具有不同關鍵碼而具有相同哈希地址的數據元素稱爲“同義詞”。
2.3 哈希函數
引起哈希衝突的一個原因可能是:哈希函數設計不夠合理。 哈希函數設計原則:
- 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0到m-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存放到衝突位置中的“下一個” 空位置中去。那如何尋找下一個空位置呢?
- 線性探測
比如下圖的場景,現在需要插入元素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,如果超出必須考慮增容。
因此:閉散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷。
開散列
- 開散列概念
開散列法又叫鏈地址法(開鏈法),首先對關鍵碼集合用散列函數計算散列地址,具有相同地址的關鍵碼歸於同一子集合,每一個子集合稱爲一個桶,各個桶中的元素通過一個單鏈錶鏈接起來,各鏈表的頭結點存儲在哈希表中。
從上圖可以看出,開散列中每個桶中放的都是發生哈希衝突的元素。 - 開散列實現
#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; // 哈希表中有效元素的個數
};
- 開散列增容
桶的個數是一定的,隨着元素的不斷插入,每個桶中元素的個數不斷增多,極端情況下,可能會導致一個桶中鏈表節點非常多,會影響的哈希表的性能,因此在一定條件下需要對哈希表進行增容,那該條件怎麼確認呢?開散列最好的情況是:每個哈希桶中剛好掛一個節點,再繼續插入元素時,每一次都會發生哈希衝突,因此**,在元素個數剛好等於桶的個數時,可以給哈希表增容**。
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);
}
}
- 開散列的思考
- 只能存儲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();
}
};
- 除留餘數法,最好模一個素數,如何每次快速取一個類似兩倍關係的素數?
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];
}
- 開散列與閉散列比較
應用鏈地址法處理溢出,需要增設鏈接指針,似乎增加了存儲開銷。事實上: 由於開地址法必須保持大量的空閒空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.7,而表項所佔空間又比指針大的多,所以使用鏈地址法反而比開地址法節省存儲空間。
模擬實現
哈希表的改造
- 模板參數列表的改造
// 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;
- 增加迭代器操作
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; // 哈希桶--主要是爲了找下一個空桶時候方便
};
- 增加通過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;
};
位圖的應用
- 快速查找某個數據是否在一個集合中
- 排序
- 求兩個集合的交集、並集等
- 操作系統中磁盤塊標記
布隆過濾器
布隆過濾器提出
我們在使用新聞客戶端看新聞時,它會給我們不停地推薦新的內容,它每次推薦時要去重,去掉那些已經看過的內容。問題來了,新聞客戶端推薦系統如何實現推送去重的? 用服務器記錄了用戶看過的所有歷史記錄,當推薦系統推薦新聞時會從每個用戶的歷史記錄裏進行篩選,過濾掉那些已經存在的記錄。 如何快速查找呢?
- 用哈希表存儲用戶記錄,缺點:浪費空間
- 用位圖存儲用戶記錄,缺點:不能處理哈希衝突
- 將哈希與位圖結合,即布隆過濾器
布隆過濾器概念
布隆過濾器是由布隆(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個計數器減一,通過多佔用幾倍存儲空間的代價來增加刪除操作。
缺陷:
- 無法確認元素是否真正在布隆過濾器中
- 存在計數迴繞
布隆過濾器優點
- 增加和查詢元素的時間複雜度爲:O(K), (K爲哈希函數的個數,一般比較小),與數據量大小無關
- 哈希函數相互之間沒有關係,方便硬件並行運算
- 布隆過濾器不需要存儲元素本身,在某些對保密要求比較嚴格的場合有很大優勢
- 在能夠承受一定的誤判時,布隆過濾器比其他數據結構有這很大的空間優勢
- 數據量很大時,布隆過濾器可以表示全集,其他數據結構不能
- 使用同一組散列函數的布隆過濾器可以進行交、並、差運算
布隆過濾器缺陷
- 有誤判率,即存在假陽性,即不能準確判斷元素是否在集合中(補救方法:再建立一個白名單,存儲可能會誤判的數據)
- 不能獲取元素本身
- 一般情況下不能從布隆過濾器中刪除元素
- 如果採用計數方式刪除,可能會存在計數迴繞問題