1.unordered系列关联式容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
unordered_set:
1. unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。
2. 在unordered_set中,元素的值同时也是唯一地标识它的key。
3. 在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找4. 到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。
5. unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。
6. 它的迭代器至少是前向迭代器。
部分接口的使用:
#include <iostream>
#include <unordered_set>
using namespace std;
int main()
{
unordered_set<int> us;
//插入元素(去重)
us.insert(1);
us.insert(4);
us.insert(3);
us.insert(3);
us.insert(2);
us.insert(2);
us.insert(3);
for (auto e : us)
{
cout << e << " ";
}
cout << endl;
}
unordered_map
1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过key值快速的索引到与其对应是value。
2. 在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<key, value>按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6. 它的迭代器至少是前向迭代器。
代码练习:
int main()
{
unordered_map<string, int> countMap;
string arr[] = { "苹果","香蕉","苹果" ,"西瓜","西瓜"};
for (auto& e : arr)
{
auto it = countMap.find(e);
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
return 0;
}
unordered_map容器当中还实现了[ ]运算符重载函数,该重载函数的功能非常强大:[key]
若当前容器中已有键值为key的键值对,则返回该键值对value的引用。
若当前容器中没有键值为key的键值对,则先插入键值对<key, value()>,然后再返回该键值对中value的引用。
2.哈希的概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
哈希映射:key值跟存储位置建立关联关系。
哈希冲突:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。
3.解决哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列。
1.闭散列——开放定址法
插入数据
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。常用的寻找下一个位置的方法是线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索,线性探测采用标记的伪删除法来删除一个元素。
扩容:
在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以引入了负载因子:负载因子/载荷因子:等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。当负载因子大于0.7的时候就需要进行扩容了:扩容不能进行直接拷贝,空间的大小发生变化映射的位置也会随着改变,所以需要重新计算映射的位置。
仿函数:考虑到统计出现次数:因为字符串不能够取模,所以我们可以给HashTable
增加一个仿函数Hash
,其可以将不能取模的类型转成可以取模的类型,同时把string特化出来解决字符串不能取模的问题
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”
,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,直到找到空为止,导致搜索效率降低
代码实现
#include <vector>
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
namespace closehash
{
enum State
{
EMPTY, //空
EXIST, //存在
DELETE //删除
};
template<class K,class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
typedef HashData<K, V> Data;
public:
HashTable()
:_n(0)
{
_tables.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first)) //不允许有相同数据的元素插入
return false;
if (_n * 10 / _tables.size() >= 7) //负载因子 到达比例就开始扩容
{
HashTable<K, V, Hash> newHT;
newHT._tables.resize(_tables.size() * 2);
for (auto& e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
Hash hf;
size_t hashi = hf(kv.first)% _tables.size(); //求出原本该在的位置
while (_tables[hashi]._state == EXIST) //开始寻找没有被占用的位置
{
++hashi;
hashi %= _tables.size(); //始终保持在vector内
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_n;
return true;
}
Data* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
size_t starti = hashi;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state == EXIST && key == _tables[hashi]._kv.first)
{
return &_tables[hashi];
}
++hashi;
hashi %= _tables.size();
//如果数据不存在就会导致一直循环的找,最多一圈
if (hashi == starti)
{
break;
}
}
return nullptr;
}
bool Erase(const K& key) //伪删除
{
Data* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
private:
vector<Data> _tables;
size_t _n = 0; //有效数据的个数
};
2.开散列——开链法
开散列
:开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶
,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。由于桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突。所以在元素个数刚好等于桶的个数时,可以给哈希表增容 。研究分析表明:素数作为哈希表的长度可以尽可能减小哈希冲突。所以可提前定义一个素数表。
代码实现:
#include <vector>
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto ch : key)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
namespace buckethash
{
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 Hash=HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
:_n(0)
{
_tables.resize(__stl_next_prime(0));
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
if (_tables.size() == _n)
{
//相当于是将节点从旧的哈希桶中搬到新的哈希桶中
vector<Node*> newTables;
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = Hash()(cur->_kv.first) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = Hash()(kv.first) % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
Node* Find(const K& key)
{
size_t hashi = Hash()(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return nullptr;
}
bool Erase(const K& key)
{
size_t hashi = Hash()(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (cur == _tables[hashi])
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
inline unsigned long __stl_next_prime(unsigned long n)
{
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
for (int i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return __stl_prime_list[__stl_num_primes - 1];
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
3.开散列与闭散列比较