C++知识点 – 哈希的应用
一、位图
1.概念
如果给40亿个不重复的无符号整数,没排过序,给一个无符号整数,如何判断一个数是否在这40亿个数中?
如果我们使用搜索树或者哈希表来结局这个问题,那么会面临40亿个整数无法整个放进数据结构中,内存会放不下;
如果我们使用排序+二分查找的办法,大型数据一般是存放在磁盘上的,不好支持二分查找,效率低;
为此,我们引入位图的概念,位图就是用每一位来存放某种状态,一个数据是否在给定的整形数据中,结果是在与不在两种状态,就可以使用一个二进制位来表示数据是否存在,用1表示存在,0表示不存在;
位图适用于海量数据,数据无重复的场景,通常是用来判断一个数据是否存在的;
注:(大型数据存储的空间占用)
1G约等于10亿字节
2.位图开空间
位图开开空间是跟范围有关的,与数据个数无关,因为映射都是用整形值的大小去映射的,整型值最多只有42亿个,所以位图开空间最多42亿bit,也就是512M;
开空间时,以字节为单位,我们可以使用vector的结构来做位图的基础数据结构;
3.位图的特点
1.搜索快,节省空间;
2.相对局限,只能映射处理整形;
3.对位图遍历后,还可以实现怕排序+去重的效果;
4.位图的实现
namespace lmx
{
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1);//开空间时多开一个字节,保证所有数据都能放得下
}
//对应位置1
void set(size_t x)
{
size_t i = x / 8;//x在哪一个字节
size_t j = x % 8;//x在该字节的第几位
_bits[i] |= (1 << j);//将位图中x对应的位置1
}
//对应位置0
void reset(size_t x)
{
size_t i = x / 8;//x在哪一个字节
size_t j = x % 8;//x在该字节的第几位
_bits[i] &= ~(1 << j);//将位图中x对应的位置0
}
//探测某位是0还是1
bool test(size_t x)
{
size_t i = x / 8;//x在哪一个字节
size_t j = x % 8;//x在该字节的第几位
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
}
5.位图的应用
1.给定100亿个整数,设计算法找到只出现一次的树数;
kv模型统计次数,可以建立一个两个位表示一个数据的位图结构,00表示出现0次,01出现1次,10表示出现2次及以上;
template<size_t N>
class twobitset
{
public:
void set(size_t x)
{
bool inset1 = _bs1.test(x);
bool inset2 = _bs2.test(x);
// 00
if (inset1 == false && inset2 == false)
{
// -> 01
_bs2.set(x);
}
else if (inset1 == false && inset2 == true)
{
// ->10
_bs1.set(x);
_bs2.reset(x);
}
else if (inset1 == true && inset2 == false)
{
// ->11
_bs1.set(x);
_bs2.set(x);
}
}
void print_once_num()
{
for (size_t i = 0; i < N; ++i)
{
if (_bs1.test(i) == false && _bs2.test(i) == true)
{
cout << i << endl;
}
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
二、布隆过滤器
1.概念
在进行字符串去重时,如果只将字符串通过哈希算法转换为整型值,不可避免地会出现相同码值的字符串,就会造成误判,这时就需要引入布隆过滤器;
布隆过滤器是使用多个哈希函数,将一个数据映射到位图结构的多个位中;
布隆过滤器判断数据是否存在,对于存在的判断是不准确的,存在误判的可能;对于不存在的判断是准确的,不会出现误判;
可以通过一个数据多映射几个位,来达到降低误判率的效果,但是误判几率不会降低到0;
2.布隆过滤器的实现
struct HashBKDR
{
// BKDR
size_t operator()(const string& key)
{
size_t val = 0;
for (auto ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
struct HashAP
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 0;
for (size_t i = 0; i < key.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ key[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct HashDJB
{
// BKDR
size_t operator()(const string& key)
{
size_t hash = 5381;
for (auto ch : key)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,
class K = string, class Hash1 = HashBKDR, class Hash2 = HashAP, class Hash3 = HashDJB>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);//哈希函数算出来的位置并不一定在已经开好的数据范围内,所以还要取模一下,把对应的bit位set成1
_bits->set(hash1);
size_t hash2 = Hash2()(key) % (_ratio * N);
_bits->set(hash2);
size_t hash3 = Hash3()(key) % (_ratio * N);
_bits->set(hash3);
}
bool Test(const K& key)
{
size_t hash1 = Hash1()(key) % (_ratio * N);
if (!_bits->test(hash1))
{
return false;//准确的
}
size_t hash2 = Hash2()(key) % (_ratio * N);
if (!_bits->test(hash2))
{
return false;//准确的
}
size_t hash3 = Hash3()(key) % (_ratio * N);
if (!_bits->test(hash3))
{
return false;//准确的
}
return true;//可能存在误判
}
private:
const static size_t _ratio = 5;//平均一个数据开几个bit位
std::bitset<_ratio* N>* _bits = new std::bitset<_ratio* N>;
};
3.布隆过滤器的删除
布隆过滤器如果想删除一个元素,就需要把这个元素对应的几个位全部置0,这种操作有可能会影响到其他数据的查找;
可以使用多个位表示一个数据的布隆过滤器,每个位被选中后,次数就+1,删除时该位的次数就-1,这样一个数据的删除就不会影响到其他数据了;
但是如果这样设计,就会消耗更多的空间,布隆过滤器的优势就削弱了;
三、哈希切分
例一
如果要求精确算法,布隆过滤器是不能够满足要求的,此时需要用到哈希切分;
分别读取两个文件的内容,使用哈希函数将文件中的query分成n个小文件;
相同的query一定会进入相同编号的小文件,这时我们只需在两个文件的对应编号的小文件中找交集就行了;
例二
给一个超过100G大小的log file,log中存着ip地址,设计算法找到出现次数最多的IP地址;
同样可以使用哈希切分来将大文件切分为小文件,那么相同的IP地址一定会进入相同的小文件中,我们只需依次使用map<string, int>对每个小文件统计次数即可;