????????STL提供了两种关联式容器——树型和哈希关联式容器,本章就是关于哈希关联式容器的介绍。
该容器的迭代器只有正向迭代器,没有rbegin。
使用方式和之前的容器一样,可以进行 ++,--,解引用等操作。
emplace
这个函数返回一个 pair 类型,pair 的 first 为 iterator,seconde 为 bool 类型。
而且插入数据时不需要构建一个pair,内部会通过传入数据自动构建一个pair然后插入到set中。
不过当 unordered_map 中没有这个数据时,emplace 返回的 pair 的 first 是指向这个数据位置的迭代器,seconde 值为1,而有这个数据时,返回的 pair?的 first 是指向这个数据位置的迭代器,seconde 值为0;
emplace_hint
而这个函数只会返回一个指向插入数据的迭代器,并且我们可以指定插入的位置(容器可能会使用也可能不会使用此位置来插入数据)
只有当容器中没有元素和要插入的元素等效的键才会插入。
unordered_map之所以被称为哈希型结构,就是因为底层采用的哈希结构。
在树型结构中,key值和value并没有直接的关系,因此在查找一个元素时需要多次比较才行,顺序查找的时间复杂度为 O(N),平衡树中为树的高度,即O(logN)。
而最理想的搜索方法就是不用进行比较,直接找到需要的元素。
那么,通过某种函数,来使元素的位置和它的键值构建一种关系,使之能够一一映射,那么查找时的速度就是最理想的。
而这就是哈希方法,该方法中的函数称为哈希函数,结构称为哈希结构。
通过不同的关键字计算出相同的哈希地址,这种情况就是哈希冲突,而具有不同关键字但是有相同的哈希地址的数据元素称为"同义词"。
引起哈希冲突的原因就是哈希函数的设计不合理。
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址,其值域必须包含在0-m-1之中
- 哈希函数计算出来的地址能够均匀的分布到整个空间中
- 哈希函数比较简单
哈希冲突的解决方式有两种:闭散列和开散列?
????????闭散列又叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表还有空位,那么就可以将数据填到哈希冲突位置的下一个空位置去。这种方法就是线性探测。
线性探测:当发生冲突的位置开始,依次向后探测,知道找到下一个空位置为止。
当我们了解线性探测后,就能够实现线性探测的哈希表了。
哈希函数
using namespace std;
template<class K>
class DefaultHashFunc
{
public:
size_t operator() (const K& data)
{
return (size_t)data;
}
};
template<>
class DefaultHashFunc<string>
{
public:
size_t operator() (const string& data)
{
size_t ch = 0;
for (auto i : data)
{
ch *= 131;
ch += i;
}
return ch;
}
};
如果是整型类型的哈希表,哈希函数就直接返回元素的值,但如果是string类型的话,我们需要对哈希函数做一个特化,这样才能保证不会有哈希冲突的出现。
闭散列的实现
namespace openaddress
{
enum STATE
{
EXIST,
EMPTY,
DELETE
};
template<class K,class V>
struct HashData
{
public:
pair<K,V> _data;
STATE _state = EMPTY;
};
template<class K,class V,class HashFunc = DefaultHashFunc<K>>
class HashTable
{
private:
void CheckCapacity()
{
//说明要扩容
if (_n * 10 / _table.size() >= 7)
{
HashTable<K, V, HashFunc> hs(_table.size()*2);
for (int i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXIST)
{
hs.Insert(_table[i]._data);
}
}
_table.swap(hs._table);
}
}
public:
HashTable(int n)
:_table(n),
_n(0)
{
}
HashTable()
:_table(10),
_n(0)
{
}
bool Insert(pair<K, V> kv)
{
if (Find(kv))
{
return false;
}
CheckCapacity();//检查是否需要扩容
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
while (_table[index]._state == EXIST)
{
++index;
index %= _table.size();
}
_table[index]._data = kv;
_table[index]._state = EXIST;
++_n;
return true;
}
HashData<const K, V>* Find(pair<K, V> kv)
{
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
while (_table[index]._state != EMPTY)
{
if (_table[index]._state == EXIST && _table[index]._data.first == kv.first)
return (HashData<const K, V>*) & _table[index];
index++;
}
return nullptr;
}
bool Erase(pair<K, V> kv)
{
HashData<const K, V>* ret = Find(kv);
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
return false;
}
private:
vector<HashData<K,V>> _table;
size_t _n = 0;
};
}
散列表的载荷因子定义:α = 填入表中的元素个数 / 表的长度
对于闭散列来说,载荷因子是特别中要的因素,当散列表的载荷因子大于 0.7 时,就需要给闭散列扩容了。
线性探测的优缺点
线性探测的优点是实现十分简单,但是它的缺点十分致命:一旦发生哈希冲突,所有哈希冲突连在一起,容易形成数据堆积,即不同的关键码占据了重要位置,使得寻找位置需要多次比较,导致效率降低。
这就需要二次探测来避免该问题:将寻找下一个位置的方法改为:Hi = (H0 + i^2) % capacity。
其中 i 为常数,H0 是通过哈希函数获取的关键码,capacity 是表的大小。
因此,闭散列的最大问题就是空间利用率比较低。
开散列又叫链地址法,首先通过哈希函数获取散列地址,相同的散列地址归于同一子集,每一个子集称为一个桶,桶内元素通过链表连接起来,各链表的头节点放在哈希表中。
每一个桶内的元素都发生了哈希冲突。
namespace hash_bucket
{
template<class K,class V>
struct HashTableNode
{
HashTableNode(const pair<K,V>& kv)
:_next(nullptr),
_kv(kv)
{
}
pair<K, V> _kv;
HashTableNode<K, V>* _next;
};
template<class K,class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashTableNode<K, V> Node;
typedef Node* pNode;
private:
void Checkcapacity()
{
size_t bucket = BucketCount();
HashFunc hf;
if (_n == bucket && bucket != 0)//若有效数据个数等于桶数就扩容
{
HashTable<K, V, HashFunc> hs(bucket*2);//创造一个新表
for (int i = 0; i < bucket; i++)
{
pNode cur = _table[i];
while (cur)
{
pNode next = cur->_next;
//获取在新表的位置
size_t index = hf(cur->_kv.first) % hs._table.size();
//头插进位置
cur->_next = hs._table[index];
hs._table[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(hs._table);
}
}
public:
HashTable()
:_table(10,nullptr)
,_n(0)
{
}
HashTable(size_t n)
:_table(n,nullptr)
,_n(0)
{
}
~HashTable()
{}
bool Insert(pair<K, V> kv)
{
if (Find(kv))
{
return false;
}
//检查是否扩容
Checkcapacity();
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
pNode newnode = new Node(kv);
newnode->_next = _table[index];
_table[index] = newnode;
++_n;
return true;
}
pNode Find(pair<K, V> kv)
{
HashFunc hf;
size_t index = hf(kv.first) % _table.size();
pNode cur = _table[index];
while (cur)
{
if (cur->_kv.first == kv.first)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(pair<K, V> kv)
{
HashFunc hf;
size_t index = hf(kv.first);
pNode cur = _table[index];
pNode parent = nullptr;
while (cur)
{
if (cur->_kv.first == kv.first)
{
//若父节点不为空
if (parent)
{
parent->_next = cur->_next;
}
//为空
else
{
_table[index] = nullptr;
}
delete cur;
--_n;
return true;
}
parent = cur;
cur = cur->_next;
}
return false;
}
size_t BucketCount()
{
size_t count = 0;
for (int i = 0; i < _table.size(); i++)
{
if (_table[i] != nullptr)
{
count++;
}
}
return count;
}
private:
vector<pNode> _table;
size_t _n = 0;
};
}
开散列的扩容和闭散列不同,开散列扩容最好的情况是每个桶只有一个元素,当元素个数等于桶的个数时,就需要扩容。
void Checkcapacity()
{
size_t bucket = BucketCount();
HashFunc hf;
if (_n == bucket && bucket != 0)//若有效数据个数等于桶数就扩容
{
HashTable<K, V, HashFunc> hs(bucket*2);//创造一个新表
for (int i = 0; i < bucket; i++)
{
pNode cur = _table[i];
while (cur)
{
pNode next = cur->_next;
//获取在新表的位置
size_t index = hf(cur->_kv.first) % hs._table.size();
//头插进位置
cur->_next = hs._table[index];
hs._table[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(hs._table);
}
}
当元素个数等于桶的个数后,就将哈希表的个数扩展为原来的两倍(自己定义),这样就能够成功扩容,并且提升闭散列的效率。
本文开散列对于string类型的插入的操作方法和闭散列一样。
都是对泛型的函数进行特化。
template<>
class DefaultHashFunc<string>
{
public:
size_t operator() (const string& data)
{
size_t ch = 0;
for (auto i : data)
{
ch *= 131;
ch += i;
}
return ch;
}
};