这边的哈希表是使用桶加链表,然后链表里面是哈希节点(键值对)
下面边实现边讲吧
首先肯定是模版定义
1
| template<typename Key, typename Value, typename Hash = hash<Key>>
|
Key
是键的类型
Value
是值的类型
Hash
是哈希函数的类型,如果用户没有自定义,默认的是标准库的hash<Key>
,也就是对Key
的哈希
然后实现一下哈希节点
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct HashNode{ Key key; Value value;
HashNode(const Key& key_, const Value& value_) : key(key_), value(value_) {} explicit HashNode(const Key& key_) : key(key_), value() {} bool operator== (const HashNode& other) const { return key == other.key; } bool operator< (const HashNode& other) const { return key < other.key; } bool operator> (const HashNode& other) const { return key > other.key; } bool operator!= (const HashNode& other) const { return key != other.key; } bool operator== (const Key& key_) const { return key == key_; } };
|
-
注意C++规定函数主体必须用{}
包裹,哪怕是实现只有一行
-
两个构造函数,一个只有键进行初始化(value就为默认值),一个是键值都有初始化
-
然后就是比较了,只对键比较,如果是字符串的话就按照字典序进行比较
-
explicit
是为了防止隐式转换,比如说对于以下两种初始化
1 2 3 4 5 6 7 8
| HashNode node1("hello");
HashNode node2 = "hello";
someFunction("hello"); someFunction(HashNode("hello"));
|
然后再解释一下value()
这种使用默认构造函数直接初始化(在构造函数的初始化列表中,也是HashNode
的初始化列表中)
等价于value = Value();
- 会根据
Value
类型的不同,进行相应的初始化(调用默认构造函数)
1 2 3 4 5 6 7 8 9
| int value(); double value(); bool value(); char* value();
string value(); vector<int> value();
|
然后是成员变量
1 2 3 4 5 6 7
| using bucket = list<HashNode>; vector<bucket> buckets; Hash hashFunction; size_t TableSize; size_t ElementsNums; float MaxLoadFactor = 0.75;
|
using bucket = list<HashNode>;
就是给list<HashNode>
设置了一个别名,叫bucket
,可以让代码看起来更加简洁
- 和
using namespace std;
是不一样的,这个是在当前作用域中,使用 std 命名空间里的所有名字时不需要加 std:: 前缀。”,当然在大型工程文件当中一般是不建议的,可能会和第三方库或者自己命名的冲突,所以还有using std::cout;
、using std::endl;
或者using std::vector;
等等
- 有
=
就是取别名,没有的就是用于命名空间的
buckets
就是整个哈希表,里面都是bucket
这些桶,也就是链表,用于处理哈希冲突(碰撞)
类似于
1 2 3 4
| buckets[0]: list<HashNode> -> node1 -> node2 ... buckets[1]: list<HashNode> -> ... ... buckets[4]: list<HashNode> -> ...
|
Hash hashFunction;
:创建一个哈希函数对象,用vector<int>
想一下就懂了,都是创建对象
TableSize
:桶的数量
ElementsNums
:哈希表中实际元素的数量
MaxLoadFactor
:负载因子,定义是ElementsNums / TableSize
,当他们的比值大于0.75(0.75 是查找效率 和 空间利用率 的良好平衡点,也是 std::unordered_map 默认使用的负载因子阈值)的时候,就要进行扩容,并且所有哈希节点重新分配到桶中,否则会影响搜索效率(从O(1)
到接近O(n)
然后就是根据哈希节点的键值映射到对应的桶当中
1
| size_t hash(const Key& key_) const { return hashFunction(key_) % TableSize; }
|
- 就是键的哈希值
%
桶的数量,就可以得到节点应该在哪个桶
构造函数肯定有的,这个比较长,当时也是花了挺多时间来理解,后面发现了对于某些基础确实还没完全弄明白
1 2
| HashTable(size_t TableSize_ = 10, const Hash& hashFunction_ = Hash()) : buckets(TableSize_), TableSize(TableSize_), hashFunction(hashFunction_), ElementsNums(0) {}
|
- 先举个例子,
vector<int> res()
,这个<>
里面的就是template
里面的那些模版参数(例如int
、float
等等),而()
里面的参数是传给构造函数的(都是这些模版参数对应的对象),两者不要搞混
可看如下例子
1
| HashTable<std::string, int> table;
|
- 10个桶
std::hash<std::string>
作为哈希函数
1
| HashTable<std::string, int> table(100);
|
- 100个桶
- 哈希函数仍是
std::hash<std::string>
1 2 3 4 5 6 7 8 9
| struct MyHash { size_t operator()(const std::string &s) const { return s.length(); } };
MyHash h; HashTable<std::string, int, MyHash> table(50, h);
|
- 使用
MyHash
作为哈希函数
- 初始化时分配 50 个桶
- 构造函数两个参数,另一个参数使用
Hash()
,这是一个临时对象,必须加上const
引用才可以,就是如果不传参的话默认使用hash<Key>
关于临时对象的可以看如下例子
1 2 3 4 5 6 7 8
| Hash obj; Hash& ref1 = obj; Hash& ref2 = Hash();
const Hash& ref3 = obj; const Hash& ref4 = Hash();
|
- 桶一般情况下,包括标准的STL,默认是10
接下去是扩容函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| void rehash(size_t newTableSize){ vector<bucket> newBuckets(newTableSize);
for (bucket& tmp1 : buckets){ for (HashNode& tmp2 : tmp1){ size_t index = hash(tmp2.key); newBuckets[index].push_back(tmp2); } }
buckets = std::move(newBuckets); TableSize = newTableSize; }
|
- 就是创建一个新的哈希表然后把元素都移过去
move
是直接移动元素,效率更高,同时移动完成之后newBuckets
里面就没有元素了,相比于直接=
(也就是拷贝),效率会高很多
insert
和insertKey
的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void insert(const Key& key, const Value& value){ if (ElementsNums + 1 > TableSize * MaxLoadFactor){ rehash(2 * TableSize); } size_t index = hash(key); list<HashNode>& tmp = buckets[index];
if (std::find(tmp.begin(), tmp.end(), key) == tmp.end()){ tmp.push_back(HashNode(key, value)); ElementsNums++; } }
void insertKey(const Key& key){ insert(key, Value{}); }
|
erase
的实现
1 2 3 4 5 6 7 8 9 10 11
| void erase(const Key& key){ size_t index = hash(key); bucket& buc = buckets[index];
typename bucket::iterator it = std::find(buc.begin(), buc.end(), key); if (it != buc.end()){ buc.erase(it); ElementsNums--; } }
|
- 链表中去除元素有两种常用的,
erase
和remove
erase
是需要拿到迭代器iterator
,在这边是list<HashNode>::iterator
,也就是bucket::iterator
,不是指针,同时使用到类模版参数的时候,这边使用到了HashNode
,里面有Key
和Value
,所以需要显示说明它是一个类型(使用typename
),不过最简单还是auto
自动判断
remove
需要拿到值,不合适元素是键值对的
find
的实现,返回的是键所对应值的指针
1 2 3 4 5 6 7 8 9 10
| Value* find(const Key& key){ size_t index = hash(key); bucket& buc = buckets[index];
typename bucket::iterator it = std::find(buc.begin(), buc.end(), key); if (it != buc.end()){ return & it -> value; } return nullptr; }
|
跟上面差不多,找到了返回值的指针即可
size
的实现
1 2 3
| size_t size(){ return ElementsNums; }
|
注意返回的是实际的键值对
clear
的实现
1 2 3 4 5
| void clear() { buckets.clear(); ElementsNums = 0; TableSize = 0; }
|
最终的完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #include <list> #include <functional> #include <vector> #include <cstddef> #include <algorithm>
using namespace std;
template<typename Key, typename Value, typename Hash = hash<Key>> class HashTable{ struct HashNode{ Key key; Value value;
HashNode(const Key& key_, const Value& value_) : key(key_), value(value_) {} explicit HashNode(const Key& key_) : key(key_), value() {} bool operator== (const HashNode& other) const { return key == other.key; } bool operator< (const HashNode& other) const { return key < other.key; } bool operator> (const HashNode& other) const { return key > other.key; } bool operator!= (const HashNode& other) const { return key != other.key; } bool operator== (const Key& key_) const { return key == key_; } };
private: using bucket = list<HashNode>; vector<bucket> buckets; Hash hashFunction; size_t TableSize; size_t ElementsNums; float MaxLoadFactor = 0.75;
public: HashTable(size_t TableSize_ = 10, const Hash& hashFunction_ = Hash()) : buckets(TableSize_), TableSize(TableSize_), hashFunction(hashFunction_), ElementsNums(0) {}
size_t hash(const Key& key_) const { return hashFunction(key_) % TableSize; } void rehash(size_t newTableSize){ vector<bucket> newBuckets(newTableSize);
for (bucket& tmp1 : buckets){ for (HashNode& tmp2 : tmp1){ size_t index = hash(tmp2.key); newBuckets[index].push_back(tmp2); } }
buckets = std::move(newBuckets); TableSize = newTableSize; }
void insert(const Key& key, const Value& value){ if (ElementsNums + 1 > TableSize * MaxLoadFactor){ rehash(2 * TableSize); } size_t index = hash(key); list<HashNode>& tmp = buckets[index];
if (std::find(tmp.begin(), tmp.end(), key) == tmp.end()){ tmp.push_back(HashNode(key, value)); ElementsNums++; } }
void insertKey(const Key& key){ insert(key, Value{}); }
void erase(const Key& key){ size_t index = hash(key); bucket& buc = buckets[index];
typename bucket::iterator it = std::find(buc.begin(), buc.end(), key); if (it != buc.end()){ buc.erase(it); ElementsNums--; } }
Value* find(const Key& key){ size_t index = hash(key); bucket& buc = buckets[index];
typename bucket::iterator it = std::find(buc.begin(), buc.end(), key); if (it != buc.end()){ return & it -> value; } return nullptr; }
size_t size(){ return ElementsNums; }
void clear() { buckets.clear(); ElementsNums = 0; TableSize = 0; }
};
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #include <iostream> #include <string>
int main() { HashTable<std::string, int> table;
std::cout << "=== 插入元素 ===\n"; table.insert("apple", 1); table.insert("banana", 2); table.insert("cherry", 3);
std::cout << "Size: " << table.size() << "\n";
std::cout << "=== 查找元素 ===\n"; if (int* val = table.find("banana")) { std::cout << "Found banana: " << *val << "\n"; } else { std::cout << "banana not found\n"; }
if (int* val = table.find("durian")) { std::cout << "Found durian: " << *val << "\n"; } else { std::cout << "durian not found\n"; }
std::cout << "=== insertKey() 插入默认值 ===\n"; table.insertKey("durian"); if (int* val = table.find("durian")) { std::cout << "durian: " << *val << " (默认值)\n"; }
std::cout << "Size after insertKey: " << table.size() << "\n";
std::cout << "=== 删除元素 ===\n"; table.erase("banana"); if (table.find("banana")) { std::cout << "banana still exists\n"; } else { std::cout << "banana deleted successfully\n"; }
std::cout << "Size after erase: " << table.size() << "\n";
std::cout << "=== 批量插入触发扩容(rehash) ===\n"; for (int i = 0; i < 20; ++i) { table.insert("key" + std::to_string(i), i); } std::cout << "Size after rehash insertions: " << table.size() << "\n";
std::cout << "=== 清空表 ===\n"; table.clear(); std::cout << "Size after clear: " << table.size() << "\n";
std::cout << "All tests completed.\n"; return 0; }
|
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| === 插入元素 === Size: 3 === 查找元素 === Found banana: 2 durian not found === insertKey() 插入默认值 === durian: 0 (默认值) Size after insertKey: 4 === 删除元素 === banana deleted successfully Size after erase: 3 === 批量插入触发扩容(rehash) === Size after rehash insertions: 23 === 清空表 === Size after clear: 0 All tests completed.
|