在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文对unordered_map和unordered_set进行介绍。
unordered_map在线文档说明
函数声明 | 功能介绍 |
---|---|
unordered_map | 构造不同格式的unordered_map对象 |
函数声明 | 功能介绍 |
---|---|
bool empty() const | 检测unordered_map是否为空 |
size_t size() const | 获取unordered_map的有效元素个数 |
函数声明 | 功能介绍 |
---|---|
begin | 返回unordered_map第一个元素的迭代器 |
end | 返回unordered_map最后一个元素下一个位置的迭代器 |
cbegin | 返回unordered_map第一个元素的const迭代器 |
cend | 返回unordered_map最后一个元素下一个位置的const迭代器 |
函数声明 | 功能介绍 |
---|---|
operator[] | 返回与key对应的value,没有一个默认值 |
注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返回。
函数声明 | 功能介绍 |
---|---|
iterator find(const K& key) | 返回key在哈希桶中的位置 |
size_t count(const K& key) | 返回哈希桶中关键码为key的键值对的个数 |
注意:unordered_map中key是不能重复的,因此count函数的返回值最大为1
函数声明 | 功能介绍 |
---|---|
insert | 向容器中插入键值对 |
erase | 删除容器中的键值对 |
void clear() | 清空容器中有效元素个数 |
void swap(unordered_map&) | 交换两个容器中的元素 |
函数声明 | 功能介绍 |
---|---|
size_t bucket_count() const | 返回哈希桶中桶的总个数 |
size_t bucket_size(size_t n) const | 返回n号桶中有效元素的总个数 |
size_t bucket(const K& key) | 返回元素key所在的桶号 |
参见 unordered_set在线文档说明
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?
答:44会和4出现哈希冲突。
对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
常见哈希函数
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1. 线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
使用线性探测找到下一个空位置,插入新元素
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
线性探测的实现
#pragma once
#include
//返回数据的key作为插入的下标
template<class K>
struct DefaultHashFunc
{
size_t operator() (const K& key)
{
return (size_t)key;
}
};
//string类型需要特化
template<>
struct DefaultHashFunc<string>
{
size_t operator() (const string& str)
{
//BKDR
size_t hash = 0;
for (auto ch : str)
{
hash *= 131;//为了尽量避免出现abcd,acbd等情况出现哈希冲突
hash += ch;
}
return hash;
}
};
namespace open_address
{
enum STATE
{
EXIST,
EMPTY,
DELETE
};
//每个哈希节点存数据和
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
STATE _state = EMPTY;
};
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
public:
HashTable()
{
_table.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
//利用Find函数判断要插入的key是否存在,存在的话直接返回false
if (Find(kv.first))
{
return false;
}
//判断是否需要扩容
if (_n * 10 / _table.size() >= 7)//负载因子大于0.7就进行扩容
{
size_t newsize = _table.size() * 2;//二倍扩容
//创建新表
HashTable<K, V, HashFunc> newHT;
newHT._table.resize(newsize);
//遍历旧表,重新映射到新表
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXIST)
{
newHT.Insert(_table[i]._kv);//这一步插入会进行重新进行更合理的放置元素到哈希表
}
}
//newHT是临时创建的,直接将这两个表交换
_table.swap(newHT._table);
}
//线性探测
HashFunc hf;
size_t hashi = hf(kv.first) % _table.size();
while (_table[hashi]._state == EXIST)
{
++hashi;
hashi %= _table.size();//防止越界访问
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
HashData<const K, V>* Find(const K& key)
{
//线性探测
HashFunc hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._state != EMPTY)
{
if(_table[hashi]._state == EXIST //这里要判断是否为EXIST,因为还有可能是DELETE状态
&& _table[hashi]._kv.first == key)
{
return (HashData<const K, V>*) &_table[hashi];//将key强转为const类型
}
//hashi位置没有找到,继续往后查找
++hashi;
hashi %= _table.size();
}
//遍历完_table没有找到。返回空
return nullptr;
}
//按需编译
bool Erase(const K& key)
{
//利用Find函数找到要删除的位置
HashData<const K, V>* ret = Find(key);
//如果要删除的数存在,将他的状态变为DELETE,注意这里不能变为EMPTY,因为这会影响在删除之前插入的数的查找
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
//要删除的数据不在哈希表中
return false;
}
private:
vector<HashData<K, V>> _table;
size_t _n = 0;// 存储有效数据的个数
};
}
//判断是否需要扩容
if (_n * 10 / _table.size() >= 7)//负载因子大于0.7就进行扩容
{
size_t newsize = _table.size() * 2;//二倍扩容
//创建新表
HashTable<K, V, HashFunc> newHT;
newHT._table.resize(newsize);
//遍历旧表,重新映射到新表
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i]._state == EXIST)
{
newHT.Insert(_table[i]._kv);//这一步插入会进行重新进行更合理的放置元素到哈希表
}
}
//newHT是临时创建的,直接将这两个表交换
_table.swap(newHT._table);
}
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?
2. 二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
对于2.1中如果要插入44,产生冲突,使用解决后的情况为:
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
1. 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
2. 开散列实现
#pragma once
#include
//哈希函数
template<class K>
struct DefaultHashFunc
{
size_t operator() (const K& key)
{
return (size_t)key;
}
};
//string类型的哈希函数
//特化
template<>
struct DefaultHashFunc<string>
{
size_t operator() (const string& str)
{
//BKDR
size_t hash = 0;
for (auto ch : str)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
// 泛型编程:不是针对某种具体类型,针对广泛的类型(两种及以上) -- 模板
namespace hash_bucket
{
//每个节点存储自身的数据和连接下一个节点
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
//前置声明,因为HTIterator需要用到HashTable,要用到HashTable的私有变量_table
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
struct HTIterator
{
typedef HashNode<T> Node;
typedef HTIterator<K, T,Ptr, Ref, KeyOfT, HashFunc> Self;//迭代器自身类型
typedef HTIterator<K, T,T*,T&, KeyOfT, HashFunc> Iterator;//永远都是普通迭代器,用于普通迭代器构造const迭代器时使用
Node* _node;
const HashTable<K, T, KeyOfT, HashFunc>* _pht;//这里也要加上const,否则下面构造函数用const的pht初始化的时候会报错(权限放大)
HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)//这里第二个参数要加上const,因为const迭代器返回调用这个构造传入的this指针是const指针
:_node(node)
,_pht(pht)
{}
// 普通迭代器时,他是拷贝构造
// const迭代器时,他是构造
//如果拷贝构造函数的参数不是引用类型,而是传值(by value),这会触发另一个拷贝构造函数的调用,形成无限递归的循环,导致栈溢出或程序崩溃。
//通过引用,拷贝构造函数只会获取源对象的引用,并不会触发额外的拷贝构造函数调用。另外,常量引用作为参数类型可以接受常量对象和非常量对象,使拷贝构造函数更加灵活
HTIterator(const Iterator& it)//拷贝构造这里要加上引用(拷贝构造函数的参数类型必须是引用)
:_node(it._node)
,_pht(it._pht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
if (_node->_next)
{
//当前桶还没完
_node = _node->_next;
}
//当前桶完了,返回下一个不为空的桶的头节点
else
{
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
++hashi;
//从下一个位置查找下一个不为空的桶
while (hashi < _pht->_table.size())
{
//下一个位置的桶不为空,返回头节点
if (_pht->_table[hashi])
{
_node = _pht->_table[hashi];
return *this;
}
//下一个位置的桶为空,继续往后查找
else
{
++hashi;
}
}
//走到这里说明已经走到最后一个节点的下一个位置,要把他置为空指针,否则使用迭代器打印会一直循环打印最后一位节点
_node = nullptr;
}
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
};
// 1、哈希表
// 2、封装map和set
// 3、普通迭代器
// 4、const迭代器
// 5、insert返回值 operator[]
// 6、key不能修改的问题
template<class K, class T, class KeyOfT ,class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashNode<T> Node;
//友元声明
template<class K, class T,class Ptr, class Ref, class KeyOfT, class HashFunc>
friend struct HTIterator;
public:
typedef HTIterator<K, T,T*,T&, KeyOfT, HashFunc> iterator;
typedef HTIterator<K, T,const T*,const T&,KeyOfT, HashFunc> const_iterator;
iterator begin()
{
//第一个桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return iterator(cur, this);
}
}
return iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin() const
{
//第一个桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return const_iterator(cur, this);
}
}
return const_iterator(nullptr, this);
}
const_iterator end() const//这里的const修饰的是*this
{
return const_iterator(nullptr, this);
}
HashTable()
{
_table.resize(10, nullptr);
}
//为什么闭散列不需要自己写析构?
//这里有动态开辟的节点,需要自己写析构函数
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
{
return make_pair(it, false);//要插入的数据已经存在,返回已经存在数据的迭代器和false
}
HashFunc hf;
//负载因子到1就扩容
if (_n == _table.size())
{
size_t newSize = _table.size() * 2;
//创建一个newsize大小的table
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
for (size_t i = 0; i < newTable.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
//头插到新表
size_t hashi = hf(kot(cur->_data)) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;//把旧表清空
}
_table.swap(newTable);
}
size_t hashi = hf(kot(data)) % _table.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode,this),true);
}
iterator Find(const K& key)
{
HashFunc hf;
KeyOfT kot;
//找到key在的哈希桶
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
//遍历该哈希桶查找key的数据
while (cur)
{
//找到数据,返回他的迭代器
if (kot(cur->_data) == key)
{
return iterator(cur,this);
}
cur = cur->_next;
}
//找不到该数据
return end();
}
bool Erase(const K& key)
{
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
//cur是头节点
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
//cur是中间节点
else
{
//将cur从当前哈希桶中断开
prev->_next = cur->_next;
}
//找到要删除的节点,有效数据个数-1,释放要删除的节点,返回true
--_n;
delete cur;
return true;
}
//记录前一个节点,方便删除的时候链接要删除节点的下一个节点
prev = cur;
cur = cur->_next;
}
//要删除的节点没有在当前的哈希表中,返回false
return false;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
printf("[&d]->", i);
Node* cur = _table[i];
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << "->";
cur = cur->_next;
}
printf("NULL\n");
}
cout << endl;
}
private:
vector<Node*> _table;//指针数组
size_t _n = 0;//存储有效数据
};
}
3. 开散列增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
HashFunc hf;
//负载因子到1就扩容
if (_n == _table.size())
{
size_t newSize = _table.size() * 2;
//创建一个newsize大小的table
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
for (size_t i = 0; i < newTable.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
//头插到新表
size_t hashi = hf(kot(cur->_data)) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;//把旧表清空
}
_table.swap(newTable);
}
// 整形数据不需要转化
template<class K>
struct DefaultHashFunc
{
size_t operator() (const K& key)
{
return (size_t)key;
}
};
//string类型的哈希函数
//特化
// key为字符串类型,需要将其转化为整形
template<>
struct DefaultHashFunc<string>
{
size_t operator() (const string& str)
{
//BKDR
size_t hash = 0;
for (auto ch : str)
{
hash *= 131;
hash += ch;
}
return hash;
}
};
// 为了实现简单,我们将比较直接与元素绑定在一起
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable
{
//.........
private:
vector<Node*> _table;//指针数组
size_t _n = 0;//存储有效数据
}
2.除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
size_t GetNextPrime(size_t prime)
{
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
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
字符串哈希算法
5. 开散列与闭散列比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
// K:关键码类型
// V: 不同容器V的类型不同,如果是unordered_map,V代表一个键值对,如果是unordered_set,V 为 K
// KeyOfValue: 因为V的类型不同,通过value取key的方式就不同,详细见unordered_map/set的实现
// HashFunc: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能取模
template<class K, class T, class KeyOfT ,class HashFunc = DefaultHashFunc<K>>
class HashTable
//前置声明,为了实现简单,HTIterator需要用到HashTable,要用到HashTable的私有变量_table
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;
// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
struct HTIterator
{
typedef HashNode<T> Node;
typedef HTIterator<K, T,Ptr, Ref, KeyOfT, HashFunc> Self;//迭代器自身类型
typedef HTIterator<K, T,T*,T&, KeyOfT, HashFunc> Iterator;//永远都是普通迭代器,用于普通迭代器构造const迭代器时使用
Node* _node;
const HashTable<K, T, KeyOfT, HashFunc>* _pht;//这里也要加上const,否则下面构造函数用const的pht初始化的时候会报错(权限放大)
HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)//这里第二个参数要加上const,因为const迭代器返回调用这个构造传入的this指针是const指针
:_node(node)
,_pht(pht)
{}
// 普通迭代器时,他是拷贝构造
// const迭代器时,他是构造
//如果拷贝构造函数的参数不是引用类型,而是传值(by value),这会触发另一个拷贝构造函数的调用,形成无限递归的循环,导致栈溢出或程序崩溃。
//通过引用,拷贝构造函数只会获取源对象的引用,并不会触发额外的拷贝构造函数调用。另外,常量引用作为参数类型可以接受常量对象和非常量对象,使拷贝构造函数更加灵活
HTIterator(const Iterator& it)//拷贝构造这里要加上引用(拷贝构造函数的参数类型必须是引用)
:_node(it._node)
,_pht(it._pht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
// 当前迭代器所指节点后还有节点时直接取其下一个节点
if (_node->_next)
{
//当前桶还没完
_node = _node->_next;
}
//当前桶完了,返回下一个不为空的桶的头节点
else
{
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
++hashi;
//从下一个位置查找下一个不为空的桶
while (hashi < _pht->_table.size())
{
//下一个位置的桶不为空,返回头节点
if (_pht->_table[hashi])
{
_node = _pht->_table[hashi];
return *this;
}
//下一个位置的桶为空,继续往后查找
else
{
++hashi;
}
}
//走到这里说明已经走到最后一个节点的下一个位置,要把他置为空指针,否则使用迭代器打印会一直循环打印最后一位节点
_node = nullptr;
}
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
};
template<class K, class T, class KeyOfT ,class HashFunc = DefaultHashFunc<K>>
class HashTable
{
typedef HashNode<T> Node;
//友元声明
template<class K, class T,class Ptr, class Ref, class KeyOfT, class HashFunc>
friend struct HTIterator;
public:
typedef HTIterator<K, T,T*,T&, KeyOfT, HashFunc> iterator;
typedef HTIterator<K, T,const T*,const T&,KeyOfT, HashFunc> const_iterator;
iterator begin()
{
//第一个桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return iterator(cur, this);
}
}
return iterator(nullptr, this);
}
iterator end()
{
return iterator(nullptr, this);
}
const_iterator begin() const
{
//第一个桶
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
if (cur)
{
return const_iterator(cur, this);
}
}
return const_iterator(nullptr, this);
}
const_iterator end() const//这里的const修饰的是*this
{
return const_iterator(nullptr, this);
}
size_t GetNextPrime(size_t prime)
{
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
};
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
HashTable()
{
_table.resize(GetNextPrime(1), nullptr);
}
//为什么闭散列不需要自己写析构?
//这里有动态开辟的节点,需要自己写析构函数
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end())
{
return make_pair(it, false);//要插入的数据已经存在,返回已经存在数据的迭代器和false
}
HashFunc hf;
//负载因子到1就扩容
if (_n == _table.size())
{
size_t newSize = GetNextPrime(_table.size());
//创建一个newsize大小的table
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
for (size_t i = 0; i < newTable.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
//头插到新表
size_t hashi = hf(kot(cur->_data)) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;//把旧表清空
}
_table.swap(newTable);
}
size_t hashi = hf(kot(data)) % _table.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode,this),true);
}
iterator Find(const K& key)
{
HashFunc hf;
KeyOfT kot;
//找到key在的哈希桶
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
//遍历该哈希桶查找key的数据
while (cur)
{
//找到数据,返回他的迭代器
if (kot(cur->_data) == key)
{
return iterator(cur,this);
}
cur = cur->_next;
}
//找不到该数据
return end();
}
bool Erase(const K& key)
{
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
//cur是头节点
if (prev == nullptr)
{
_table[hashi] = cur->_next;
}
//cur是中间节点
else
{
//将cur从当前哈希桶中断开
prev->_next = cur->_next;
}
//找到要删除的节点,有效数据个数-1,释放要删除的节点,返回true
--_n;
delete cur;
return true;
}
//记录前一个节点,方便删除的时候链接要删除节点的下一个节点
prev = cur;
cur = cur->_next;
}
//要删除的节点没有在当前的哈希表中,返回false
return false;
}
void Print()
{
for (size_t i = 0; i < _table.size(); i++)
{
printf("[&d]->", i);
Node* cur = _table[i];
while (cur)
{
cout << cur->_kv.first << ":" << cur->_kv.second << "->";
cur = cur->_next;
}
printf("NULL\n");
}
cout << endl;
}
private:
vector<Node*> _table;//指针数组
size_t _n = 0;//存储有效数据
};
}
namespace gty
{
// unordered_map中存储的是pair的键值对,K为key的类型,V为value的类型
// unordered_map在实现时,只需将hashtable中的接口重新封装即可
template<class K,class V>
class unordered_map
{
// 通过key获取value的操作
struct KeyOfMap
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, KeyOfMap>::iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, KeyOfMap>::const_iterator const_iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
//这里将insert返回值处理成pair是为了重载[]运算符
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));//key存在就返回它的V,不存在就返回缺省值V()
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<const K, V>, KeyOfMap> _ht;
};
}
namespace gty
{
template<class K>
class unordered_set
{
struct KeyOfSet
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, K, KeyOfSet>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, KeyOfSet>::const_iterator const_iterator;
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
//unordered_set中的iterator也是const_iterator,所以这里我们需要调用HashTable中的普通迭代器来接受
//然后再利用HashTable中的普通迭代器构造const迭代器的构造函数,再进行返回
pair<iterator,bool> insert(const K& key)
{
//return _ht.Insert(key);
pair<typename hash_bucket::HashTable<K,K,KeyOfSet>::iterator, bool> ret = _ht.Insert(key);
return pair<const_iterator, bool>(ret.first, ret.second);
}
private:
hash_bucket::HashTable<K, K, KeyOfSet> _ht;
};
}