哈希的應用
哈希思想在算法中的應用繁多其重要性是不言而喻的,這裏簡單介紹兩種哈希在大數據中的應用。
位圖
算法思路
假如說有這麼一種情景:給40億個不重複的無符號整數,沒排過序,判斷一個無符號整數是否在這40億個數中。
首先我們從時間考慮,假如說我們遍歷40億個數,事件複雜度是On
的,如果我們先排序再用二分查找,排序要ONlogN
二分查找要OlogN
也還是不夠快。不過這道題最重要的不是它的時間,而是空間,如果我們把40億個整形全放到內存中需要4G * 4 = 16G
內存,40億字節 == 4G
,不難發現我們根本存不下,那麼怎麼辦呢?這裏就需要用到位圖。
我們標記一個數是否存在根本不需要存儲完整整數,我們只需要用存在或者不存在兩種狀態對其進行標記即可,而兩種狀態的標記,只需要1位數據即可,由此我們可以用40億比特位來標記40億個數是否存在。並且無符號整形的上限差不多也就在42億,我們就算標記完全部數字用到40億位也只需要4 G / 8 = 500M
內存,由此我們使用位圖進行標記差不多相當於將空間壓縮了32倍。
標記思路就是40多億位分表標識40多億無符號整型,一個數如果存在則它對應位標記爲1
,否則爲0
。假如說0存在,則第0位標記爲1
,32不存在則第32位標記爲0,而無符號整形也是有上限的,40多億位完全可以標記所有無符合整形。
實現
#include <iostream>
#include <vector>
class BitSet
{
public:
//要保證每一個數據都能映射到一個唯一的位置,位圖的大小與最大映射數據上限有關
//因此這裏的range代表的是映射的最大數據
BitSet(size_t range)
{
_bs.resize((range >> 5) + 1);
}
//存儲
void Set(int num)
{
int index = num >> 5;
int bitIdx = num % 32;
_bs[index] |= (1 << bitIdx);
}
bool Find(int num)
{
int index = num >> 5;
int bitIdx = num % 32;
return 1 & (_bs[index] >> bitIdx);
}
void ReSet(int num)
{
int index = num >> 5;
int bitIdx = num % 32;
_bs[index] &= (~(1 << bitIdx));
}
private:
std::vector<int> _bs;
};
int main()
{
BitSet bs(64);
bs.Set(1);
bs.Set(64);
bs.Set(4);
std::cout << bs.Find(1) << std::endl;
std::cout << bs.Find(64) << std::endl;
std::cout << bs.Find(4) << std::endl;
std::cout << bs.Find(3) << std::endl;
bs.ReSet(4);
std::cout << bs.Find(4) << std::endl;
}
1
1
1
0
0
但是使用位圖有一個缺陷,就是我們無法解決哈希衝突,如果我們想要判斷字符串,當字符串轉換爲整數時就有可能會造成哈希衝突,因爲算法的原因兩個不同的字符串可能會最終會轉換爲相同的整數,所以在判斷字符串等其他需要轉換並且可能會造成哈希衝突的類型時不能直接使用位圖,於是一種進化版的位圖誕生了。
布隆過濾器
思想
布隆過濾器是專門爲了解決爲途中轉換會造成哈希衝突的情況。例如我們現在要用位圖標記字符串,我們兩個不相同的字符串經過哈希函數轉換後可能會出現最終一樣的轉換結果於是便出現了哈希衝突,例如str1
哈希轉換後爲24
於是我們將第24位置1表示str1
存在,但是此時我們在判斷str2
是否存在的時候發現str2
哈希後的值也爲24
,但是str2
並不存在,於是這裏就出現了哈希衝突,進行了誤判。
爲了解決它,布隆過濾器會選擇利用多個不同的哈希函數對一個字符串進行哈希,並將所有哈希結果的對應位全部置1,這裏與位圖的思想無異。當我們查找一個字符串是否存在時再用同樣的多個哈希函數對其進行哈希,然後依次查找每一位哈希結果,如果全爲1則可大機率認定爲這個字符串是存在的。例如str1
存儲利用三個哈希函數得到結果爲24, 26, 28
,於是我們將這3位置,我們在查找str2
時利用同樣三個哈希函數轉換得到結果24, 25, 27
,雖然24
造成了衝突,但是由於25, 26
並不爲1,所以也並不會誤判str2
存在,只有str1
三次哈希後結果在位圖中全都爲1時纔會判斷其存在,這樣可以大概率解決衝突。
爲什麼說是大機率存在的呢?因爲也有可能這個字符串的所有哈希結果都與其他字符串造成衝突,此時也會有誤判,不過這種情況的機率很小,布隆過濾器可以保證絕大多數情況是正確的,但偶爾也難免會有特殊情況發生。
#include <iostream>
#include <vector>
#include <string>
#include "BitSet.hpp"
struct HFun1
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
for(auto& ch : str)
{
hash = hash * 131 + ch;
}
return hash;
}
};
struct HFun2
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
for(auto& ch : str)
{
hash = hash * 65599 + ch;
}
return hash;
}
};
struct HFun3
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
size_t magic = 63689;
for(auto& ch : str)
{
hash = hash * magic + ch;
magic *= 378551;
}
return hash;
}
};
//HFun爲3個自定義的哈希函數
template<class T, class HFun1, class HFun2, class HFun3>
class BloomFilter
{
public:
//k = (m / n) * ln2
//k:哈希函數數量
//m:位圖大小
//n:元素個數
//m = k * n / ln2
//number表示元素個數,布隆這裏不用元素最大上限作爲位圖的大小,因爲可能會造成大量數據浪費
//這裏利用二次哈希,節省空間
BloomFilter(size_t number)
:_bitCount(5 * number)
,_bs(_bitCount)
{
}
void Set(const T& data)
{
int index1 = HFun1()(data) % _bitCount;
int index2 = HFun2()(data) % _bitCount;
int index3 = HFun3()(data) % _bitCount;
_bs.Set(index1);
_bs.Set(index2);
_bs.Set(index3);
}
bool Find(const T& data)
{
int index1 = HFun1()(data) % _bitCount;
int index2 = HFun2()(data) % _bitCount;
int index3 = HFun3()(data) % _bitCount;
if(!_bs.Find(index1) || !_bs.Find(index2) || !_bs.Find(index3))
{
return false;
}
return true;//可能會有誤判
}
//布隆爲了防止誤判不提供刪除操作
private:
BitSet _bs;
size_t _bitCount;
};
int main()
{
BloomFilter<std::string, HFun1, HFun2, HFun3> bf(1000);
std::string str1 = "https://misakifx.github.io/";
std::string str2 = "https://blog.csdn.net/qq_41669298";
std::string str3 = "https://space.bilibili.com/14406161/#/fans/follow";
std::string str4 = "https://space.bilibili.com/#/fans/follow";
std::string str5 = "https://space.bilibili.com/4406161/#/fans/follow";
std::string str6 = "https://space.bilibili.com/146161/#/fans/follow";
bf.Set(str1);
bf.Set(str2);
bf.Set(str3);
bool ret = bf.Find(str1);
std::cout << ret << std::endl;
ret = bf.Find(str2);
std::cout << ret << std::endl;
ret = bf.Find(str3);
std::cout << ret << std::endl;
ret = bf.Find(str4);
std::cout << ret << std::endl;
ret = bf.Find(str5);
std::cout << ret << std::endl;
ret = bf.Find(str6);
std::cout << ret << std::endl;
}
1
1
1
0
0
0
布隆過濾器一般來說不提供刪除操作,但是其實是可以實現的,但是這裏就需要藉助引用計數,如果借用引用計數那麼一個位肯定解決不了,就需要用多個位,那麼就需要開闢更多空間,這裏就需要根據需求來設計。