目录
1. unordered_set和unordered_map
1.1 unordered_map
1.2 unordered_set
1.3 unordered系列写OJ题
961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣(LeetCode)
349. 两个数组的交集 - 力扣(LeetCode)
217. 存在重复元素 - 力扣(LeetCode)
884. 两句话中的不常见单词 - 力扣(LeetCode)
2. 实现unordered_set和unordered_map
2.1 哈希桶的迭代器
2.2 封装unordered_set和unordered_map
完整unordered_map.h
完整unordered_set.h
2.3 修改哈希桶
完整HashTable.h:
Test.cpp:
3. 题外话+笔试选择题
本篇完。
1. unordered_set和unordered_map
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到(logN),
即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,其底层结构是前一篇讲的哈希结构,本文中只对unordered_set和unordered_map进行介绍,
unordered_multiset和unordered_multimap可查看文档介绍。
unordered系列和我们前面学习的map和set几乎一模一样,只是多了前面的unordered。
正如它的名字一样,unordered系列和map/set比起来,unordered系列打印出来的数据是无序的。
1.1 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_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6. 它的迭代器至少是前向迭代器。
常用接口函数:可以参考map的函数使用,还有一些关于哈希的接口后面再讲解
1.2 unordered_set
1、无序集是一种容器,它以不特定的顺序存储惟一的元素,并允许根据元素的值快速检索单个元素。
2、在unordered_set中,元素的值同时是唯一标识它的键。键是不可变的,只可增删,不可修改
3、在内部,unordered_set中的元素没有按照任何特定的顺序排序,而是根据它们的散列值组织成桶,从而允许通过它们的值直接快速访问单个元素(平均时间复杂度为常数)。
4、unordered_set容器比set容器更快地通过它们的键访问单个元素,尽管它们在元素子集的范围迭代中通常效率较低。
5、容器中的迭代器至少是前向迭代器。
unordered_set 容器提供了和 unordered_map 相似的能力,
但 unordered_set 可以用保存的元素作为它们自己的键。
T 类型的对象在容器中的位置由它们的哈希值决定,因而需要定义一个 Hash< T > 函数。
基本类型可以省去Hash< T >方法。不能存放重复元素。
可指定buckets个数,可进行初始化,也可后期插入元素
常用接口函数:可以参考set的函数使用,还有一些关于哈希的接口后面再讲解
1.3 unordered系列写OJ题
(困难题我唯唯诺诺,简单题我多次重拳出击)
961. 在长度 2N 的数组中找出重复 N 次的元素 - 力扣(LeetCode)
难度简单
给你一个整数数组 nums
,该数组具有以下属性:
nums.length == 2 * n
.nums
包含n + 1
个 不同的 元素nums
中恰有一个元素重复n
次
找出并返回重复了 n
次的那个元素。
示例 1:
输入:nums = [1,2,3,3] 输出:3
示例 2:
输入:nums = [2,1,2,5,3,2] 输出:2
示例 3:
输入:nums = [5,1,5,2,5,3,5,4] 输出:5
提示:
2 <= n <= 5000
nums.length == 2 * n
0 <= nums[i] <= 10^4
nums
由n + 1
个 不同的 元素组成,且其中一个元素恰好重复n
次
class Solution {
public:
int repeatedNTimes(vector<int>& nums) {
}
};
解析代码:(和map一样用)(以下代码改成map也能过,OJ平均效率低一些,后面就知道了)
class Solution {
public:
int repeatedNTimes(vector<int>& nums) {
unordered_map<int,int> countMap;
for(const auto& e : nums)
{
countMap[e]++;
}
unordered_map<int,int> Map;
for(const auto& kv : countMap)
{
if(kv.second == nums.size() / 2)
{
return kv.first;
}
}
return -1; // 不会走到这,顺便返回一个值
}
};
349. 两个数组的交集 - 力扣(LeetCode)
难度简单
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
}
};
解析代码:(这题在从C语言到C++_26讲过了)(当时用set排序了,现在不排序写写)
当时是力扣题解2,现在是力扣题解1:使用哈希集合存储元素,则可以在O(1)的时间内判断一个元素是否在集合中,从而降低时间复杂度。首先使用两个集合分别存储两个数组中的元素,然后遍历较小的集合(顺便遍历一个也行,就是效率低点),判断其中的每个元素是否在另一个集合中,如果元素也在另一个集合中,则将该元素添加到返回值。
该方法的时间复杂度可以降低到O(m+n)。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set s1(nums1.begin(),nums1.end()); // 去重
unordered_set s2(nums2.begin(),nums2.end());
vector<int> retV;
if(s1.size() <= s2.size())
{
for(const auto& e : s1)
{
if(s2.find(e) != s2.end())
{
retV.push_back(e);
}
}
}
else
{
for(const auto& e : s2)
{
if(s1.find(e) != s1.end())
{
retV.push_back(e);
}
}
}
return retV;
}
};
217. 存在重复元素 - 力扣(LeetCode)
难度简单
给你一个整数数组 nums
。如果任一值在数组中出现 至少两次 ,返回 true
;如果数组中每个元素互不相同,返回 false
。
示例 1:
输入:nums = [1,2,3,1] 输出:true
示例 2:
输入:nums = [1,2,3,4] 输出:false
示例 3:
输入:nums = [1,1,1,3,3,4,3,2,4,2] 输出:true
提示:
1 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
}
};
解析代码:(看看返回值,前两个和模拟实现set的一样)
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> s;
for(const auto& e : nums)
{
if(s.insert(e).second == false)
{
return true;
}
}
return false;
}
};
884. 两句话中的不常见单词 - 力扣(LeetCode)
难度简单
句子 是一串由空格分隔的单词。每个 单词 仅由小写字母组成。
如果某个单词在其中一个句子中恰好出现一次,在另一个句子中却 没有出现 ,那么这个单词就是 不常见的 。
给你两个 句子 s1
和 s2
,返回所有 不常用单词 的列表。返回列表中单词可以按 任意顺序 组织。
示例 1:
输入:s1 = "this apple is sweet", s2 = "this apple is sour" 输出:["sweet","sour"]
示例 2:
输入:s1 = "apple apple", s2 = "banana" 输出:["banana"]
提示:
1 <= s1.length, s2.length <= 200
s1
和s2
由小写英文字母和空格组成s1
和s2
都不含前导或尾随空格s1
和s2
中的所有单词间均由单个空格分隔
class Solution {
public:
vector<string> uncommonFromSentences(string s1, string s2) {
}
};
解析代码:(等价于:在两个句子中一共只出现一次的单词。)
大家可以百度stringstream类用法,这里讲一个小技巧
可以将字符串中每个单词按空格隔开。
class Solution {
public:
vector<string> uncommonFromSentences(string s1, string s2) {
unordered_map<string, int> m;
vector<string> retV;
stringstream a, b; // 创建流对象
string s;
a << s1; // 向流中传值
b << s2;
while (a >> s)
{
m[s]++; //流向s中写入值,并且空格会自断开
//cout << s << "+";
}
while (b >> s)
{
m[s]++;
}
for (const auto& m : m)
{
if (m.second == 1)
{
retV.push_back(m.first); //只需要看出现次数是1的单词
}
}
return retV;
}
};
如果解开注释:
2. 实现unordered_set和unordered_map
这里用我们上一篇写的开散列哈希桶的代码,闭散列不用就删掉,去掉命名空间复制一份过来:
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode* _next; // 不用存状态栏了,存下一个结点指针
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};
template<class K>
struct HashFunc // 可以把闭散列的HashFunc放在外面直接用,但是这就不放了
{
size_t operator()(const K& key)
{
return (size_t)key; // 负数,浮点数,指针等可以直接转,string不行
}
};
template<>
struct HashFunc<string> // 上面的特化
{
size_t operator()(const string& key)
{
size_t val = 0;
for (const auto& ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
typedef HashNode<K, V> Node;
~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;
}
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __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 (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1; // 不会走到这,随便返回一个值
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
Hash hs;
if (_size == _tables.size()) // 负载因子到1就扩容
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
//newTables.resize(newSize, nullptr);
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 = hs(cur->_kv.first) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hs(kv.first) % _tables.size(); // 哈希映射
Node* newnode = new Node(kv); // 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
bool Erase(const K& key)
{
if (_tables.size() == 0) // 防止除零错误
{
return false;
}
Hash hs;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr) // 头插,先把指针数组存的指针指向cur的下一个
{
_tables[hashi] = cur->_next;
}
else // 中间删
{
prev->_next = cur->_next;
}
delete cur; // 统一在这delete
return true;
}
prev = cur; // 往后走
cur = cur->_next;
}
return false; // 没找到
}
size_t Size() // 存的数据个数
{
return _size;
}
size_t TablesSize() // 表的长度
{
return _tables.size();
}
size_t BucketNum() // 桶的个数
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]) // 如果不是空就有桶
{
++num;
}
}
return num;
}
size_t MaxBucketLenth() // 最长桶的长度
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
protected:
vector<Node*> _tables; // 指针数组
size_t _size;
};
有了封装set和map的和学习了哈希的经验,直接写出框架:
UnorderedSet.h:
#pragma once
#include "HashTable.h"
namespace rtx
{
template<class K, class K>
class unordered_map
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
protected:
HashTable<K, k, Hash, MapKeyOfT> _ht;
};
}
UnorderedMap.h:
#pragma once
#include "HashTable.h"
namespace rtx
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
protected:
HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
};
}
用命名空间和STL库区分,第二个参数对于unordered_set是key,对于unordered_map是piar,
现在应该把ashNode的两个参数改为一个参数T,_pair 改为 _data
再把HashTable的第二个参数改为T,再加一个获取key的仿函数:
(这里不能在第三个仿函数给默认的了)
2.1 哈希桶的迭代器
迭代器是所有容器必须有的,先来看迭代器的++是如何实现的:
如上图所示,一个哈希表,其中有四个哈希桶,迭代器是it。
++it操作:
- 如果it不是某个桶的最后一个元素(桶里数据下一个不为空),则it指向下一个节点。
- 如果it是桶的最后一个元素(桶里数据下一个为空),则it指向下一个桶的头节点。
要想实现上面的操作,迭代器中不仅需要一个_node来记录当前节点,
还需要一个哈希表的指针,以便找下一个桶,代码如下:
(顺便写迭代器中的其他操作,如解引用,箭头,以及相等等运算符的重载就不再详细介绍了:)
template<class K, class T, class Hash, class KeyOfT>
class HashTable; // 前置声明
template<class K, class T, class Hash, class KeyOfT>
class __HashIterator
{
public:
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef __HashIterator<K, T, Hash, KeyOfT> Self;
Node* _node; // 数据结点
HT* _pht; // 哈希表指针
__HashIterator(Node* node, HT* pht)
:_node(node)
, _pht(pht)
{}
Self& operator++()
{
if (_node->_next) // 不是桶中的最后一个数据
{
_node = _node->_next;
}
else // 是桶中的最后一个数据,找下一个桶
{
Hash hs;
KeyOfT kot;
size_t i = hs(kot(_node->_data)) % _pht->_tables.size() + 1;//没+1是当前桶位置
for (; i < _pht->_tables.size(); ++i)
{
if (_pth->tables[i]) // 向后迭代找到了有桶的位置
{
_node = _pth->tables[i]; // 把这个位置给_node
break;
}
}
if (_pht == _tables.size()) // 后面都没桶了
{
_node = nullptr;
}
}
return *this; // this调用该函数的对象(迭代器),指向下一个后解引用返回
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return s._node != _node;
}
bool operator==(const Self& s) const
{
return s._node == _node;
}
};
- t不是处于某个桶的末尾,直接指向下一个节点。
- 当it是某个桶的末尾时,指向下一个桶。
首先需要确定当前桶的位置:
使用KeyOfT仿函数获取当前数据的key值(因为不知道是map还是set在调用)。
再使用Hash仿函数将key值转换成可以模的整形(因为不知道key是整形还是字符串再或者其他自定义类型)。然后开始寻找下一个桶:
从当前哈希表下标开始向后寻找,直到找到下一个桶,将桶的头节点地址赋值给_node。
如果始终没有找到,说明没有桶了,也就是没有数据了,it指向end,这里使用空指针来代替end。 将++后的迭代器返回。
迭代器中有一个成员变量是哈希表的指针,如上图所示,
所以在迭代器中typedef了HashTable成为 HT,方便我们使用。
根据我们前面实现迭代器的经验,迭代器其实是封装在Hashtable中的,也就是说,在HashTable中也会typedef迭代器:此时HashTable和HashIterator就构成了相互typedef的关系。哈希表和迭代器类的定义势必会有一个先后顺序,这里在定义的时候,在代码顺序上就是先定义迭代器,再定义的哈希表。此时迭代器在typedef的时候就找不到哈希表的定义,因为编译器只会向上寻找而不会向下寻找。所以必须在HashIterator类前面先声明一下HashTable类,这种操作被叫做前置声明。
- 前置声明一定要放在类外面,如果放在迭代器类里面,编译器只会在迭代器的命名空间中寻找哈希表的定义,这样是找不到的。
- 前置声明放在类外面的时候,编译器会在整个命名空间中寻找哈希表的定义,就可以找到。
在++迭代器的时候,会使用到哈希表指针,哈希表指针又会使用到HashTable中的_tables。
- HashTable中的_tables是保护成员,在类外是不能访问的。
解决这个问题可以在HashTable中写一个公有的访问函数,也可以采用友元,这里用下友元。
类模板的友元声明需要写模板参数,在类名前面加friend关键字。
(迭代器要访问HashTable的保护,所以迭代器要成为HashTable的友元)
2.2 封装unordered_set和unordered_map
有了前面的经验(map的方括号重载要改insert的返回值),这里先把完整的unordered_set.h和
unordered_map.h写出来,看看需要怎么改。封装就是套一层,还是很容易的:
完整unordered_map.h
#pragma once
#include "HashTable.h"
namespace rtx
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashTable<K, pair<K, V>, Hash, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv); // 先看下面,所以insert要返回插入后的键值对
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
V& operator[](const K& key) // 根据原功能,返回的是键值对中key对应的value的引用。
{ // 当key不存在时,operator[]用默认value与key构造键值对然后插入
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
protected:
HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
};
}
完整unordered_set.h
#pragma once
#include "HashTable.h"
namespace rtx
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const K& key) //和unordered_map保持一致
{
return _ht.Insert(key);
}
bool find(const K& key)
{
return _ht.Find(key);
}
bool erase(const K& key)
{
return _ht.Erase(key);
}
protected:
HashTable<K, K, Hash, SetKeyOfT> _ht;
};
}
2.3 修改哈希桶
先给哈希桶的模板参数增加两个仿函数,用typedef封装迭代器,并给迭代器传对应的模板参数。
还需要在哈希表中增加获取迭代器起始位置和结束位置的接口:
- 在获取其实位置时,需要从头开始遍历哈希表项,寻找到第一个桶的头节点作为起始位置。
- 使用空指针代替迭代器的结束位置。
- 在构造迭代器时,直接传this指针去定义迭代器中的哈希表指针。
在插入中,凡是使用到key值以及用key取模的地方,都要用仿函数取获得。包括删除和删除中也是,插入之前要查找下,先把查找改了:
让其返回迭代器,如果存在,返回key所在位置的迭代器,如果不存在,返回末尾的迭代器。
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return end();
}
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur,this);
}
cur = cur->_next;
}
return end();
}
然后修改哈希表的Inerst,返回由迭代器和布尔值组成的键值对。
- 先进行查找,如果存在,则返回key所在位置的迭代器和false组成的键值对。
- 查找结构不存在,则返回插入新节点后key所在位置的迭代器和true组成的键值对。
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator ret = Find(kot(data));
if (ret != end())
{
return make_pair(ret, false);
}
Hash hs;
if (_size == _tables.size()) // 负载因子到1就扩容
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
//newTables.resize(newSize, nullptr);
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 = hs(kot(cur->_data) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hs((kot(data) % _tables.size(); // 哈希映射
Node* newnode = new Node(data); // 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
删除只需在移除用上KeyOfT仿函数,然后就改完了,程序就能跑起来了:
完整HashTable.h:
#pragma once
#include <iostream>
#include <vector>
using namespace std;
template<class T>
struct HashNode
{
T _data;
HashNode* _next; // 不用存状态栏了,存下一个结点指针
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
template<class K>
struct HashFunc // 可以把闭散列的HashFunc放在外面直接用,但是这就不放了
{
size_t operator()(const K& key)
{
return (size_t)key; // 负数,浮点数,指针等可以直接转,string不行
}
};
template<>
struct HashFunc<string> // 上面的特化
{
size_t operator()(const string& key)
{
size_t val = 0;
for (const auto& ch : key)
{
val *= 131;
val += ch;
}
return val;
}
};
template<class K, class T, class Hash, class KeyOfT>
class HashTable; // 前置声明
template<class K, class T, class Hash, class KeyOfT>
class __HashIterator
{
public:
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef __HashIterator<K, T, Hash, KeyOfT> Self;
Node* _node; // 数据结点
HT* _pht; // 哈希表指针
__HashIterator(Node* node, HT* pht)
:_node(node)
, _pht(pht)
{}
Self& operator++()
{
if (_node->_next) // 不是桶中的最后一个数据
{
_node = _node->_next;
}
else // 是桶中的最后一个数据,找下一个桶
{
Hash hs;
KeyOfT kot;
size_t i = hs(kot(_node->_data)) % _pht->_tables.size() + 1;//没+1是当前桶位置
for (; i < _pht->_tables.size(); ++i)
{
if (_pht->_tables[i]) // 向后迭代找到了有桶的位置
{
_node = _pht->_tables[i]; // 把这个位置给_node
break;
}
}
if (i == _pht->_tables.size()) // 后面都没桶了
{
_node = nullptr;
}
}
return *this; // this调用该函数的对象(迭代器),指向下一个后解引用返回
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return s._node != _node;
}
bool operator==(const Self& s) const
{
return s._node == _node;
}
};
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
public:
template<class K, class T, class Hash, class KeyOfT>
friend class __HashIterator;
typedef HashNode<T> Node;
typedef __HashIterator<K, T, Hash, KeyOfT> iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return iterator(_tables[i], this); // 构造:(Node * node, HT * pht)
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~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;
}
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return end();
}
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur,this);
}
cur = cur->_next;
}
return end();
}
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __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 (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1; // 不会走到这,随便返回一个值
}
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
iterator ret = Find(kot(data));
if (ret != end())
{
return make_pair(ret, false);
}
Hash hs;
if (_size == _tables.size()) // 负载因子到1就扩容
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
//newTables.resize(newSize, nullptr);
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 = hs(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hs(kot(data)) % _tables.size(); // 哈希映射
Node* newnode = new Node(data); // 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
bool Erase(const K& key)
{
if (_tables.size() == 0) // 防止除零错误
{
return false;
}
Hash hs;
KeyOfT kot;
size_t hashi = hs(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (kot(cur->_data) == key)
{
if (prev == nullptr) // 头插,先把指针数组存的指针指向cur的下一个
{
_tables[hashi] = cur->_next;
}
else // 中间删
{
prev->_next = cur->_next;
}
delete cur; // 统一在这delete
return true;
}
prev = cur; // 往后走
cur = cur->_next;
}
return false; // 没找到
}
size_t Size() // 存的数据个数
{
return _size;
}
size_t TablesSize() // 表的长度
{
return _tables.size();
}
size_t BucketNum() // 桶的个数
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]) // 如果不是空就有桶
{
++num;
}
}
return num;
}
size_t MaxBucketLenth() // 最长桶的长度
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
protected:
vector<Node*> _tables; // 指针数组
size_t _size;
};
Test.cpp:
#include "UnorderedSet.h"
#include "UnorderedMap.h"
namespace rtx
{
void test_unordered_set()
{
unordered_set<int> s;
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(5);
unordered_set<int>::iterator it = s.begin();
//auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl << endl;;
}
void test_unordered_map()
{
unordered_map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict.insert(make_pair("string", "字符串"));
dict.insert(make_pair("left", "左边"));
unordered_map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
unordered_map<string, int> countMap;
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
for (const auto& e : arr)
{
countMap[e]++;
}
for (const auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
}
int main()
{
rtx::test_unordered_set();
rtx::test_unordered_map();
return 0;
}
3. 题外话+笔试选择题
还有一些接口函数和仿函数参数这里并没有实现,正如前面说的,
模拟实现不是为了造一个更好的轮子,而是理解它的底层实现。
值得一提的是库里面unordered系列都提供了比较key相不相等的仿函数:
本篇模拟实现的是直接调用的等于,这样就写死了,
比如key是日期类的指针比较就是比较指针的地址了,但是我们想要比较的是指针指向的内容。
所以应该是要加上这个仿函数的,我们以前写过类似的这里就不加上去了。
所以就会有下面的面试题:
------------------------------------------------分割----------------------------------------------------------------
笔试选择题1:关于unordered_map和unordered_set说法错误的是()
A.它们中存储元素的类型不同,unordered_map存储键值对,而unordered_set中只存储key
B.它们的底层结构相同,都使用哈希桶
C.它们查找的时间复杂度平均都是O(1)
D.它们在进行元素插入时,都得要通过key的比较去找待插入元素的位置
笔试选择题2:关于unordered_map和unordered_set说法错误的是()
A.它们中都存储的键值对
B.map适合key有序的场景,unordered_map没有有序的要求
C.它们中元素查找的方式相同
D.map的底层结构是红黑树,unordered_map的底层结构是哈希桶
答案:
A:正确,参考unordered_map和unordered_set的文档说明
B:正确,都采用的是哈希桶来实现的
C:正确,哈希是通过哈希函数来计算元素的存储位置的,找的时候同样通过哈希函数找元素位 置,不需要循环遍历因此时间复杂度为O(1)
D:错误,不需要比较,只需要通过哈希函数,就可以确认元素需要存储的位置
选D
A:正确,结合文档说明
B:正确,因为map的底层是红黑树,红黑树中序遍历可以得到关于key有序的序列,而unordered _map底层是哈希 桶,哈希对于其存储的元素是否有序,并不关心
C:错误,map按照二叉搜索树的规则查找,unordered_map按照哈希方式进行查找
D:正确
选C
本篇完。
下一篇是又到高阶数据结构的内容:从C语言到C++_32(哈希的应用)位图bitset+布隆过滤器+哈希切割。