一.unordered系列关联式容器
unordered_map 使用和介绍
(1). unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
(2). 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
(3). 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
(4). unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
(5). unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
(6). 它的迭代器是单向迭代器
unordered_map 常见接口总览
// 检测unordered_map是否为空
bool empty() const;
// 获取unordered_map的有效元素个数
size_t size() const;
// 返回unordered_map第一个元素的迭代器
iterator begin() noexcept;
// 返回unordered_map最后一个元素下一个位置的迭代器
iterator end() noexcept;
// 返回unordered_map第一个元素的const迭代器
const_iterator cbegin() const noexcept;
// 返回unordered_map最后一个元素下一个位置的const迭代器
const_iterator cend() const noexcept;
// 返回与key对应的value,没有一个默认值
mapped_type& operator[] ( const key_type& k );
// 返回key在哈希桶中的位置
iterator find ( const key_type& k );
// 返回哈希桶中关键码为key的键值对的个数
size_type count ( const key_type& k ) const;
// 向容器中插入键值对
pair<iterator,bool> insert ( const value_type& val );
// 删除容器中的键值对
iterator erase ( const_iterator position );
// 清空容器中有效元素个数
void clear() noexcept;
// 交换两个容器中的元素
void swap ( unordered_map& ump );
使用
#include<iostream>
#include<unordered_map>
using namespace std;
int main()
{
unordered_map<int,int> um;
um.insert(make_pair(1,1));
um.insert(make_pair(10,10));
um.insert(make_pair(8,8));
um.insert(make_pair(5,5));
um.insert(make_pair(2,2));
cout<<um.empty()<<endl;
cout<<um.size()<<endl;
auto ret = um.find(10);
if(ret != end()) cout<<ret->first<<" "<<ret->second<<endl;
else cout<<"没找到"<<endl;
for(auto it = um.begin();it != um.end();it++)
{
cout<<it->first<<" "<<it->second<<endl;
}
}
unordered_set 介绍和使用
unordered_set 常见接口总览
// 检测unordered_set是否为空
bool empty() const;
// 获取unordered_set的有效元素个数
size_t size() const;
// 返回unordered_set第一个元素的迭代器
iterator begin() noexcept;
// 返回unordered_set最后一个元素下一个位置的迭代器
iterator end() noexcept;
// 返回unordered_set第一个元素的const迭代器
const_iterator cbegin() const noexcept;
// 返回unordered_set最后一个元素下一个位置的const迭代器
const_iterator cend() const noexcept;
// 返回key在哈希桶中的位置
iterator find ( const key_type& k );
// 返回哈希桶中关键码为key的键值对的个数
size_type count ( const key_type& k ) const;
// 向容器中插入键值对
pair<iterator,bool> insert ( const value_type& val );
// 删除容器中的键值对
iterator erase ( const_iterator position );
// 清空容器中有效元素个数
void clear() noexcept;
// 交换两个容器中的元素
void swap ( unordered_set& ump );
使用
#include<iostream>
#include<unordered_set>
using namespace std;
int main()
{
unordered_set<int> us;
us.insert(1);
us.insert(10);
us.insert(8);
us.insert(5);
us.insert(2);
cout << us.empty() << endl;
cout << us.size() << endl;
auto ret = us.find(10);
if (ret != us.end()) cout << *ret << endl;
else cout << "没找到" << endl;
for (auto it = us.begin(); it != us.end(); it++)
{
cout << *it << endl;
}
}
二.哈希概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。(哈希函数)
常见哈希函数
直接定址法 : 该方法是取关键字的某个线性函数值为哈希地址,适用于整数且数据范围比较集中
缺陷 :
(1). 如果数据范围大,直接定址法会浪费很多空间
(2). 不能处理浮点数,字符串等等场景
优点 : 速度快( O(1) ),每一个值都对应一个唯一位置
除留余数法 : 解决数据范围很大的情况
如 1 5 10 100000 100 18 15 7这8个值,使用直接定址法,由于100000的存在浪费了很多空间,采用除留余数法把数据映射到有效的空间里面,将值模10作为地址,但这样会导致不同的值映射到同一位置(哈希冲突)
如何解决哈希冲突?
一. 闭散列(开放地址法)
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去
(1). 线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
线性探测缺点 : 某些连续位置出现冲突,出现踩踏效应
此时如果再插入模10为0的值,插入和查找的时间复杂度就退化成O(N)了
(2). 二次探测
二次探测为了避免由于线性探测找空位置一个一个往后找所导致的踩踏效应,找下一个空位置的方法为 : Hi = (H0 + i^2) % m
负载因子/载荷因子 = 存储的有效数据个数 / 空间的大小
负载因子越大,冲突的概率越高,增删查改的效率越低
负载因子越小,冲突的概率越低,增删查改的效率越高,但空间利用率低
一个类型去做 map/set 的 Key 有什么要求?
能支持比较大小
一个类型去做 unordered_map / unordered_set 的 Key 有什么要求?
能支持转换成整数 + 相等比较
二. 开散列(哈希桶/拉链法)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
闭散列的开放定址法,负载因子不能超过1,建议控制在[0.0,0,7]左右
开散列的拉链法,负载因子可以超过1,建议控制在[0.0,1]左右
实际中哈希桶结构更实用
(1). 空间利用率高
(2). 极端情况下还有解决方案(数据不多,负载因子低,但是这些数据大部分冲突了),将冲突数据多的这个桶改成红黑树结构
闭散列实现
(1). 每一个节点由数据和数据的状态构成,默认数据的状态为空(EMPTY)
(2). KeyOfT仿函数用来获取data中的key值
(3). HashFunc仿函数用来将key值转换成整形以此来取模
(4). 插入时使用 key % _table.size() 来获取对应映射位置,如发生冲突采用线性探测或二次探测的方法,当负载因子大于 0.7 时,冲突的概率会较高,因此需要增容并重新计算每个数据对应的映射位置
(5). 查找时也采用线性探测或二次探测的方法,数据状态不为空就继续向后查找,找到后返回节点地址,没找到返回空
(6). 删除只需要修改节点的状态为DELETE即可
namespace CloseHash
{
// 节点状态
enum State
{
EMPTY, // 空
EXIST, // 存在
DELETE, // 删除
};
// 节点定义
template<class T>
struct HashData
{
T _data;
State _state = EMPTY;
};
template<class K>
struct Hash
{
int operator()(const K& key)
{
return key;
}
};
// 模板特化来处理字符串的情况
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
size_t val = 0;
for (auto& e : s)
{
val = val * 131 + e;
}
return val;
}
};
template<class K, class T,class KeyOfT,class HashFunc = Hash<K>>
class HashTable
{
public:
bool Insert(const T& data)
{
HashData<T>* ret = Find(data);
// 不能出现重复数据
if (ret) return false;
// 第一次插入先开空间
if (_table.size() == 0)
{
_table.resize(10);
}
// 负载因子大于 0.7 开始扩容
if ((double)_n / (double)_table.size() > 0.7)
{
HashTable<K, T,KeyOfT,HashFunc> newHT;
newHT._table.resize(_table.size() * 2);
for (auto& e : _table)
{
if (e._state == EXIST)
newHT.Insert(e._data);
}
_table.swap(newHT._table);
}
HashFunc hf;
KeyOfT kot;
// 获取对应映射的起始位置
size_t start = hf(kot(data)) % _table.size();
size_t index = start;
size_t i = 1;
// 线性探测
while (_table[index]._state == EXIST)
{
index = start + i;
index %= _table.size();
i++;
}
// 放入数据
_table[index]._data = data;
// 修改数据状态为存在
_table[index]._state = EXIST;
// 数据个数++
_n++;
return true;
}
HashData<T>* Find(const K& key)
{
if (_table.size() == 0)
{
return nullptr;
}
HashFunc hf;
KeyOfT kot;
size_t start = hf(key) % _table.size();
size_t index = start;
size_t i = 1;
while (_table[index]._state != EMPTY)
{
if (_table[index].kot(data) == key && _table[index]._state != DELETE) return &_table[index];
index = start + i;
index %= _table.size();
i++;
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<T>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
_n--;
return true;
}
else
{
return false;
}
}
private:
vector<HashData<T>> _table;
size_t _n = 0; // 存储的有效数据个数
};
}
开散列实现 :
(1). 迭代器类对节点指针进行了封装,重载了++ / * / -> / != / == 运算符
(2). 插入操作将节点头插即可,当负载因子等于1时,需要将哈希桶进行扩容
(3). 删除/查找操作和单链表的删除/查找操作一致
namespace OpenHash
{
template<class T>
struct HashNode
{
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
HashNode<T>* _next;
T _data;
};
// 将key值转换成整形以便取模
template<class K>
struct Hash
{
int operator()(const K& key)
{
return key;
}
};
// 模板特化,将string转换成整形
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
int val = 0;
for (auto& e : s)
{
val = val * 131 + e;
}
return val;
}
};
// 前置声明
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable;
template<class K,class T,class KeyOfT,class HashFunc = Hash<K>>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef __HashIterator<K, T,KeyOfT,HashFunc> Self;
typedef HashTable<K, T, KeyOfT,HashFunc> HT;
Node* _node;
HT* _ht;
__HashIterator(Node* node,HT* ht)
:_node(node)
,_ht(ht)
{}
Self& operator++()
{
// 遍历当前桶
if (_node->_next)
{
_node = _node->_next;
}
// 当前桶遍历完毕,找下一个桶
else
{
HashFunc hf;
KeyOfT kot;
size_t index = hf(kot(_node->_data)) % _ht->_table.size();
++index;
while (index < _ht->_table.size() && _ht->_table[index] == nullptr) ++index;
if (index == _ht->_table.size()) _node = nullptr;
else _node = _ht->_table[index];
}
return *this;
}
// 返回数据的引用
T& operator*()
{
return _node->_data;
}
// 返回数据的地址
T* operator->()
{
return &_node->_data;
}
// 比较指针的指向是否不相等即可
bool operator!=(const Self& s)const
{
return _node != s._node;
}
// 比较指针的指向是否相等即可
bool operator==(const Self& s)const
{
return _node == s._node;
}
};
template<class K, class T,class KeyOfT,class HashFunc = Hash<K>>
class HashTable
{
public:
typedef HashNode<T> Node;
typedef __HashIterator<K, T, KeyOfT, HashFunc> iterator;
// 友元
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
friend struct __HashIterator;
public:
// 返回哈希表中第一个节点的迭代器
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
return iterator(_table[i],this);
}
return end();
}
iterator end()
{
return iterator(nullptr,this);
}
pair<iterator,bool> Insert(const T& data)
{
// 第一次插入先扩容
if (_table.size() == 0)
{
_table.resize(10);
}
KeyOfT kot;
HashFunc hf;
// 不能出现重复的数据
iterator ret = Find(kot(data));
if (ret != end())
return make_pair(ret, false);
// 负载因子到1,扩容
if (_n == _table.size())
{
vector<Node*> newtable;
newtable.resize(_table.size() * 2, nullptr);
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
size_t index = hf(kot(cur->_data)) % newtable.size();
Node* next = cur->_next;
cur->_next = newtable[index];
newtable[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newtable);
}
// 计算对应映射位置
size_t index = hf(kot(data)) % _table.size();
// 进行头插节点操作
Node* newnode = new Node(data);
newnode->_next = _table[index];
_table[index] = newnode;
_n++;
return make_pair(iterator(_table[index],this),true);
}
iterator Find(const K& key)
{
if (_table.size() == 0)
return end();
KeyOfT kot;
HashFunc hf;
// 遍历哈希表进行查找
size_t index = hf(key) % _table.size();
for (auto it = _table[index]; it != nullptr; it = it->_next)
{
if (kot(it->_data) == key) return iterator(it,this);
}
return end();
}
bool Erase(const K& key)
{
size_t index = key % _table.size();
Node* pre = _table[index], * cur = _table[index];
while (cur)
{
if (kot(cur->_data) == key)
{
if (pre == cur) _table[index] = cur->_next;
else pre->_next = cur->_next;
delete cur;
_n--;
return true;
}
pre = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _table;
size_t _n = 0; // 有效数据个数
};
}
使用开散列封装 unordered_set
unordered_set
#pragma once
#include"HashTable.h"
namespace lyp
{
template<class K>
class unordered_set
{
struct SetOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename OpenHash::HashTable<K, K, SetOfT>::iterator iterator;
iterator begin()
{
return ht.begin();
}
iterator end()
{
return ht.end();
}
pair<iterator,bool> Insert(const K& data)
{
return ht.Insert(data);
}
private:
OpenHash::HashTable<K, K, SetOfT> ht;
};
}
使用开散列封装 unordered_map
unordered_map
#pragma once
#include"HashTable.h"
namespace lyp
{
template<class K,class V>
class unordered_map
{
struct MapOfT
{
const K& operator()(const pair<K,V>& data)
{
return data.first;
}
};
public:
typedef typename OpenHash::HashTable<K, pair<K,V>, MapOfT>::iterator iterator;
iterator begin()
{
return ht.begin();
}
iterator end()
{
return ht.end();
}
pair<iterator,bool> Insert(const pair<K,V>& data)
{
return ht.Insert(data);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = ht.Insert(make_pair(key,V()));
return ret.first->second;
}
private:
OpenHash::HashTable<K, pair<K, V>, MapOfT> ht;
};
}
三.位图
位图,就是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。
位图其实是用数组实现的,数组的每一个元素的每一个二进制位都可以表示一个数据在或者不在,0表示数据不存在,1表示数据存在。因为比特位只有两种状态,要不是0,要不就是1,所以位图其实就是一种直接定址法的哈希,只不过位图只能表示这个值在或者不在
位图实现
#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace lyp
{
template<size_t N>
class BitSet
{
public:
BitSet()
{
_bits.resize(N / 32 + 1, 0);
}
// 置 1
void Set(size_t x)
{
size_t i = x / 32; // 第几个整数
size_t j = x % 32; // 整数的第几位
_bits[i] |= (1 << j);
}
// 置 0
void ReSet(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= (~(1 << j));
}
// 检测在不在
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1 << j);
}
private:
vector<int> _bits;
};
}
位图应用 :
(1). 给定100亿个整数,设计算法找到只出现一次的整数?
方案一 : 将上述代码中的32改为16,给每个整数两个比特位,00出现0次,01出现1次,10出现2次及以上
方案二 : 定义两个位图,00出现0次,01出现1次,10出现2次及以上
(2). 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
方案一 : 依次读取第一个文件的所有整数映射到一个位图,再依次读取第二个文件的所有整数判断在不在位图,在就是交集,不在就不是交集
方案二 : 依次读取第一个文件的所有整数映射到位图1,再依次读取第二个文件的所有整数映射到位图2,最后将两个位图 & 操作,& 后还是1的位就是交集
(3). 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
同(1),00出现0次,01出现1次,10出现2次,11出现3次及以上
四. 布隆过滤器
位图的缺点是只能处理整形,针对字符串等等更复杂的类型,布隆过滤器就是来解决位图的缺点的,例如在注册某个网站时,常常需要注册一个昵称,如何判断你所取的昵称和其他人不重复呢?
我们很容易想到将昵称字符串转化成整形,但会出现哈希冲突,所以布隆过滤器用多个哈希函
数,将一个数据映射到位图结构中以此来减少哈希冲突
注意 :
(1). 判断昵称用过存在误判,判断昵称没用过不存在误判
(2). 布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素
template<size_t N,class K = string,class Hash1,class Hash2,class Hash3>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t i1 = Hash1()(key) % N;
size_t i2 = Hash2()(key) % N;
size_t i3 = Hash3()(key) % N;
_bitset.Set(i1);
_bitset.Set(i2);
_bitset.Set(i3);
}
void Test(const K& key)
{
size_t i1 = Hash1()(key) % N;
if (_bitset.Test(i1) == false) return false;
size_t i2 = Hash2()(key) % N;
if (_bitset.Test(i2) == false) return false;
size_t i3 = Hash3()(key) % N;
if (_bitset.Test(i3) == false) return false;
return true;
}
private:
BitSet<N> _bitset;
};
(1). 给两个文件,分别有100亿个query(假设每个query20byte),我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
近似算法 : 将一个文件映射到布隆过滤器中,遍历另一个文件,检测是否在布隆过滤器中,在就是交集,不在就不是交集(但在会存在误判)
精确算法(哈希切分) : 一个文件大小200G,我们有1G内存,可以进行哈希切分,将A文件切分成400份,创建出 A0~A399 文件,根据字符串哈希算法(i = HashBKDR() % 400),将内容分别写到各个小文件中,如果A0 ~ A399 文件中有大于 1G 的文件,则将该文件再次进行切分,B文件同理,将A0 ~ A399 文件加载到哈希表中,遍历 B0 ~ B399文件判断在 A0 ~ A399 中是否存在(A0和B0找交集,A1和B1找交集…)
(2).给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,如何找到top K的IP?
哈希切分 : 将文件切分成100份,创建出 A0 ~ A99 个文件,将ip地址按照字符串哈希算法映射到各个文件中,分别将 A0 ~A99 文件加载到哈希表中统计出现次数最多的ip地址,建立k个数的小堆即可
(3) .如何扩展BloomFilter使得它支持删除元素的操作
将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作