在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到log_2 N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好 的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是 其底层结构不同,本文中只对unordered_map和unordered_set进行介绍, unordered_multimap和unordered_multiset可自行查看文档介绍。
(因为某种程度上来说,这里unordered_map 和unordered_map的差别不是很大,可以具体参考map和set的差别,因此这里就只着重介绍unordered_map了)
unordered一词直接说明力它与传统map的最大区别(它的数据存储是无序的),其他实际用法其实与map没有特别大的区别。
其次unordered_map也实现了"[ ]"这一运算符的重载,其用法和map一样。
1**.unordered_map 里insert使用中没有make_pair这个函数**
2.unodered_map 的查询和桶操作。
函数声明 | 功能介绍 |
---|---|
iterator find(const K&key) | 返回key在哈希桶中的位置 |
size_t bucket_count() const | 返回哈希桶中桶的总个数 |
size_t bucket_size() const | 返回n号桶中有效元素的总个数 |
size_t bucket(const K& key) | 返回元素key所在的桶号 |
首先unordered_map 效率比 map 快多了是因为其顶层封装的是hash结构,那么下面便让我们来介绍一下这种结构吧。
是否存在这样一种结构可以不经过任何比较,一次直接从表中得到要搜索的元素?如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)
如上图所示,哈希函数的设计其实就是一种元素的对应方式,而这样的对应方式又往往会发生冲突(比如我两个数模10之后得到的结果是一样的,那么这两个数将会发生冲突,通常处理方式是把后面加入进来的那个数往后放一位。
所以面对不同的问题的时候我们哈希函数的设计就显得尤为重要。
常见哈希函数的设计方法:
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种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突的两种方式:闭散列和开散列
那么因该如何去查找这下一个位置呢?
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
tips:(采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。)
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:
若当前key与原来key产生相同的哈希地址,则当前key存在该地址后偏移量为(1,2,3…)的二次方地址处
key1:hash(key)+0
key2:hash(key)+1^2
key3:hash(key)+2^2
当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。
散列表负载因子的定义:a = 填入表中的元素个数 / 散列表的长度
其实控制是否扩容,本质就在控制这个负载因子,一般负载因子控制在0.7~0.8之下。
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
enum Status
{
EXIST,
EMPTY,
DELETE
};
template<class K,class V>
struct HashData
{
pair<K,V> _kv;
Status _status = EMPTY;
};
template<class T>
struct Hash
{
size_t operator() (const K& key)
{
return K;
}
};
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
// BKDR这是一种常用的算法,为了防止不同单词但总字符ascii码值刚好一样
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
template<class K, class V, class HashFunc = Hash<K>>//注意这里等号后面这么用很巧妙,其实目的还是为了支持string类
class HashTable
{
private:
vector<HashData<K, V>> _tables;
size_t _n = 0;// 有效数据个数
};
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
HashFunc hf;
size_t start = hf(key) % _tables.size();
size_t i = 0;
size_t index = start;
//线性探测/二次探测
while (_tables[index]._status != EMPTY)
{
if (_tables[index]._kv.first == key && _tables[index]._status == EXIST) //强调了一个状态和是否一样
{
return &_tables[index];
}
++i;
//index = start + i*i;
index = start + i; //这里可以看出 i 存在的目的就是为了往前加一位,或者使用二次探测
index %= _tables.size(); //防止你下标的越界访问(重点其实就是二次探测万一过界)
}
}
bool Erase(conset K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr)
{
return false;
}
else
{
--_n;
ret->_status = DELETE;//实际我的值还是存在那里只是改了个状态
return true;
}
}
bool Insert(const pair<K,V>& kv)
{
HashData<K,V>* ret = Find(kv.first);
if(ret)
{
return false;
}
// 负载因子到0.7,就扩容(如果不知道负载因子是什么可以看我上述提到的概念系列)
// 负载因子越小,冲突概率越低,效率越高,空间浪费越多
// 负载因子越大,冲突概率越高,效率越低,空间浪费越少
if(_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
//扩容
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V, HashFunc> newHT; //新开一个表
newHT._tables.resize(newSize); //其实还得去找那个vector去新开一个表出来
//将数据拷贝下来
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._status == EXIST)
{
newHT.Insert(_tables[i]._kv);
}
}
_tables.swap(newHT._tables); //直接交换地址
}
//仿照上面find的操作
HashFunc hf;
size_t start = hf(kv.first) % _tables.size();
size_t i = 0;
size_t index = start;
while (_tables[index]._status == EXIST || _tables[index]._status == DELETE)
{
++i;
//index = start + i*i;
index = start + i;
index %= _tables.size();
}
_table[index]._kv = kv;
_tables[index]._status = EXIST;
++_n;
return true;
}
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};
template<class K, class V, class HashFunc = Hash<K>>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _tables;
size_t _n = 0; // 有效数据的个数
};
Node* Find(const K& key)
{
if (_tables.empty())
{
return nullptr;
}
HashFunc hf;
size_t index = hf(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool Erase(const K& key)
{
if (_tables.empty())
{
return false;
}
HashFunc hf;
size_t index = hf(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr) // 头删
{
_tables[index] = cur->_next;
}
else // 中间删除
{
prev->_next = cur->_next;
}
--_n;
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
Node* Find(const K& key)
{
if (_tables.empty())
{
return nullptr;
}
HashFunc hf;
size_t index = hf(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
Node* ret = Find(kv.first);
if (ret)
return false;
HashFunc hf;
// 负载因子 == 1时扩容
if (_n == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
newTables.resize(newSize);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t index = hf(cur->_kv.first) % newTables.size();
// 头插
cur->_next = newTables[index];
newTables[index] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t index = hf(kv.first) % _tables.size();
Node* newnode = new Node(kv);
// 头插
newnode->_next = _tables[index];
_tables[index] = newnode;
++_n;
return true;
}
template<class K, class hash = Hash<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename LinkHash::HashTable<K, K, SetKeyOfT, hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
bool insert(const K& key)
{
return _ht.Insert(key);
}
private:
HashTable<K, K, SetKeyOfT, hash> _ht;
};
template<class K, class V, class hash = Hash<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename LinkHash::HashTable<K, pair<K, V>, MapKeyOfT, hash>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
bool insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
private:
HashTable<K, pair<K, V>, MapKeyOfT, hash> _ht;
};
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一 个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。
比如你有一个数组,那么此时创造一个位图,位图的每一位都用0或者1来表示这个数组对应下标的元素是否存在,这样位图就能节省非常多的空间。
template<size_t N>
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= (~(1 << j));
}
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
std::vector<char> _bits;
//std::vector _bits;
};
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概 率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存 在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也 可以节省大量的内存空间。
当我们需要检索一个元素是否存在于一个集合中时,通常的做法是使用哈希表或者二叉搜索树等数据结构。但是,在一些情况下,这些数据结构的存储和查询成本可能很高,例如,当数据量很大时,或者当内存非常有限时。在这些情况下,布隆过滤器是一种更加高效的数据结构。
布隆过滤器是一种用于检索一个元素是否属于一个集合的数据结构。它的基本原理是利用多个不同的哈希函数,将元素映射到一个固定长度的二进制位数组(位图)中。当要查询一个元素是否属于该集合时,我们可以用同样的哈希函数将该元素映射到位图上,如果对应位置的值为1,则说明该元素可能在集合中,但如果对应位置的值为0,则该元素肯定不在集合中。
由于布隆过滤器使用的是哈希函数,因此它具有很快的查询速度和空间效率。然而,布隆过滤器的缺点是,当位图中有多个元素映射到同一位置时,会出现冲突,这会导致误判。另外,一旦元素被加入到布隆过滤器中,就无法删除。
struct BKDRHash
{
size_t operator()(const string& s)
{
// BKDR
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ s[i] ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ s[i] ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N,
size_t X = 8,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash>
class BloomFilter
{
public:
void Set(const K& key)
{
size_t len = X*N;
size_t index1 = HashFunc1()(key) % len;
size_t index2 = HashFunc2()(key) % len;
size_t index3 = HashFunc3()(key) % len;
/* cout << index1 << endl;
cout << index2 << endl;
cout << index3 << endl<
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
bool Test(const K& key)
{
size_t len = X*N;
size_t index1 = HashFunc1()(key) % len;
if (_bs.test(index1) == false)
return false;
size_t index2 = HashFunc2()(key) % len;
if (_bs.test(index2) == false)
return false;
size_t index3 = HashFunc3()(key) % len;
if (_bs.test(index3) == false)
return false;
return true; // 存在误判的
}
// 不支持删除,删除可能会影响其他值。
void Reset(const K& key);
private:
bitset<X*N> _bs;
};
哈希切割是一种常见的数据分割方法,主要用于将大数据集划分成多个小数据块,以便更快地进行数据查询和处理。
哈希切割的原理是将数据块按照哈希函数的值进行划分,即将每个数据块映射到一个唯一的哈希值,并将具有相同哈希值的数据块放置在同一数据块中。这样,数据查询时可以通过哈希函数快速定位到数据块,从而加快查询速度。
举个例子,假设有一个拥有100万个用户的社交网络平台,为了提高查询速度,我们可以将用户数据按照用户ID的哈希值进行划分,将具有相同哈希值的用户放置在同一数据块中。这样,当用户登录时,系统只需要查询包含其用户ID的数据块,而不需要遍历整个数据集,从而加快查询速度。