【C++】哈希unordered_map和set的使用以及哈希表,哈希桶的概念以及底层实现

news2025/4/16 3:07:29

  📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  💡C++ 算法 | 🌐 C 语言

本文章完整代码在下篇文章开头给出

上篇文章:map和set使用红黑树封装的底层实现

下篇文章:封装unordered_map,unordered_set

📌 核心知识点梳理

🔍 哈希容器 vs 有序容器:核心区别

特性

哈希容器(unordered_*)

有序容器(map/set)

底层实现

哈希表(分桶、拉链法/开放寻址)

红黑树(平衡二叉搜索树)

时间复杂度

平均 O(1),最坏 O(n)(哈希冲突时)

稳定 O(log n)

元素顺序

无序(依赖哈希函数)

严格有序(默认升序)

内存占用

较高(需维护桶和链表)

较低(树节点结构固定)

迭代器稳定性

插入可能触发 rehash,导致迭代器失效

插入/删除不影响其他迭代器

典型场景

快速查找、无需顺序遍历

有序遍历、范围查询(如 lower_bound)


⚙️ 底层实现原理

哈希冲突解决

  • 闭散列(开放定址法):线性探测/二次探测,冲突时向后寻找空位。
    🌟 缺点:冲突可能引发“聚集效应”,影响后续插入效率。

  • 开散列(哈希桶):每个桶挂链表,冲突元素链式存储。
    ✅ 优势:减少冲突影响,内存灵活,适合高频插入删除场景。

扩容机制

  • 负载因子 = 元素数 / 桶数。负载因子 ≥ 0.7 时触发扩容(桶数翻倍)。

  • 优化策略:旧桶数据重新哈希到新桶,避免直接拷贝。

自定义键类型

字符串处理:需提供哈希函数(如 BKDR 算法)将字符串转为整型。

template<>
struct HashFunc<string> {
    size_t operator()(const string& key) {
        size_t hash = 0;
        for (auto ch : key) hash = hash * 131 + ch;
        return hash;
    }
};


💻 常用接口与代码示例

  • unordered_map 基本操作

    unordered_map<string, int> scores = {{"Alice", 90}, {"Bob", 85}};
    scores.insert({"Charlie", 95});   // 插入
    scores["Dave"] = 88;              // 下标插入
    auto it = scores.find("Alice");   // 查找
    if (it != scores.end()) cout << it->second; // 输出 90
    scores.erase("Bob");              // 删除

  • 性能对比测试

    // 插入 10w 条数据耗时对比
    set insert: 320ms
    unordered_set insert: 45ms
    // 查找效率对比
    set find: 280ms → unordered_set find: 12ms

🚦 如何选择容器?

  • 选哈希容器
    ✅ 高频查找/插入,无需顺序遍历。
    ✅ 内存充足,哈希函数设计合理。

  • 选有序容器
    ✅ 需要有序遍历或范围查询(如时间区间)。
    ✅ 键类型无法高效哈希(如复杂结构体)。


🔧 实战避坑指南

  1. 迭代器失效:哈希容器插入可能触发 rehash,需谨慎保存迭代器。

  2. 字符串键优化:优先使用特化哈希函数(如 BKDR 算法),减少冲突。

  3. 自定义键类型:需同时实现哈希函数和相等比较运算符(operator==)。


🎯 总结

掌握 unordered_map/unordered_set 的底层原理与使用技巧,能显著提升代码性能!选择容器时,结合场景需求与性能特点,避免“一把梭”用 map/set。文末附完整代码示例,助你快速上手!

1. unordered系列关联式容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到$log_2N$,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍,
unordered_multimap和unordered_multiset可查看文档介绍

unordered_map的文档介绍

unordered_map在线文档说明
1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部,unordered_map没有对<key, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问
value。
6. 它的迭代器是前向迭代器(单向)。没有rbegin和rend

哈希map和set的功能使用使用和map和set基本一致,但哈希输出结果是无序的,map和set是有序的

例如这里使用unordered_map和map的输出区别:

哈希容器的常用接口及使用

哈希容器(unordered_map 和 unordered_set)基于哈希表实现,提供平均 O(1) 时间复杂度的插入、删除和查找操作。

1. unordered_map 常用接口
#include <unordered_map>

// 初始化
unordered_map<string, int> hashMap;

// 插入元素
hashMap.insert({"Alice", 90});
hashMap["Bob"] = 85;          // 使用 operator[](若键不存在,自动插入)

// 查找元素
auto it = hashMap.find("Alice");
if (it != hashMap.end()) {
    cout << it->second << endl; // 输出 90
}

// 删除元素
hashMap.erase("Bob");         // 通过键删除
hashMap.erase(it);            // 通过迭代器删除

// 遍历
for (const auto& pair : hashMap) {
    cout << pair.first << ": " << pair.second << endl;
}

// 其他接口
hashMap.size();      // 元素数量
hashMap.empty();     // 是否为空
hashMap.clear();     // 清空
2. unordered_set 常用接口
#include <unordered_set>

unordered_set<int> hashSet;
hashSet.insert(10);     // 插入
hashSet.erase(10);      // 删除
auto it = hashSet.find(20); // 查找

有序容器(map/set)的常用接口

有序容器基于红黑树实现,元素按键 严格有序(默认升序),所有操作的时间复杂度为 O(log n)

1. map 常用接口
#include <map>

map<string, int> orderedMap;
orderedMap["Alice"] = 90;   // 插入
auto it = orderedMap.find("Bob"); // 查找
orderedMap.erase("Alice");  // 删除

// 遍历按键升序输出
for (const auto& pair : orderedMap) {
    cout << pair.first << ": " << pair.second << endl;
}
2. set 常用接口
#include <set>

set<int> orderedSet;
orderedSet.insert(30);
orderedSet.erase(30);

哈希容器与有序容器的核心区别

特性哈希容器(unordered_*有序容器(map/set
底层实现哈希表(分桶,拉链法/开放寻址)红黑树(平衡二叉搜索树)
时间复杂度平均 O(1),最坏 O(n)(哈希冲突时)稳定 O(log n)
元素顺序无序(依赖哈希函数)严格有序(按键升序或自定义排序)
内存占用较高(需维护桶和链表)较低(树节点结构固定)
迭代器稳定性插入可能触发 rehash,导致迭代器失效插入/删除不影响其他节点的迭代器
自定义键类型要求需提供哈希函数和相等比较需提供键的比较规则(如 operator<
典型使用场景快速查找、无需顺序遍历有序遍历、范围查询(如 lower_bound

这是debug模式下,用测试用例随机生成数,并分别使用哈希set和set进行的查找删除插入的时间性能对比  (有些时候编译器可能会优化)

测试代码:

int test_set2()
{
	const size_t N = 100000;

	unordered_set<int> us;
	set<int> s;

	vector<int> v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; ++i)
	{
		//v.push_back(rand()); // N比较大时,重复值比较多
		//v.push_back(rand()+i); // 重复值相对少
		v.push_back(i); // 没有重复,有序
	}

	size_t begin1 = clock();
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert:" << end1 - begin1 << endl;

	size_t begin2 = clock();
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert:" << end2 - begin2 << endl;

	int m1 = 0;
	size_t begin3 = clock();
	for (auto e : v)
	{
		auto ret = s.find(e);
		if (ret != s.end())
		{
			++m1;
		}
	}
	size_t end3 = clock();
	cout << "set find:" << end3 - begin3 << "->" << m1 << endl;

	int m2 = 0;
	size_t begin4 = clock();
	for (auto e : v)
	{
		auto ret = us.find(e);
		if (ret != us.end())
		{
			++m2;
		}
	}
	size_t end4 = clock();
	cout << "unorered_set find:" << end4 - begin4 << "->" << m2 << endl;

	cout << "插入数据个数:" << s.size() << endl;
	cout << "插入数据个数:" << us.size() << endl << endl;

	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	cout << "set erase:" << end5 - begin5 << endl;

	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;

	return 0;
}


如何选择容器?

  • 用哈希容器(unordered_*)的场景

    • 需要快速查找/插入,且不关心顺序。

    • 内存充足,且键的哈希函数设计合理(减少冲突)。

  • 用有序容器(map/set)的场景

    • 需要按顺序遍历或范围查询(如时间区间、字典序)。

    • 需要稳定的性能(哈希表的最坏情况不可接受)。

    • 键类型无法提供高效的哈希函数。


示例代码对比

// 哈希容器(输出无序)
unordered_map<string, int> scores = {{"Bob", 85}, {"Alice", 90}};
for (const auto& p : scores) { /* 顺序不确定,可能是 Alice→Bob 或 Bob→Alice */ }

// 有序容器(输出按键升序)
map<string, int> orderedScores = {{"Bob", 85}, {"Alice", 90}};
for (const auto& p : orderedScores) { // 固定输出 Alice→Bob }

底层

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构

什么是哈希/散列

1.是映射:值和值进行1对1或者1对多的关联

(哈希表:哈希思想实现数据结构,查找key key/value)

值 - 存储位置建立映射关系

值和对应位置建立关系,会有什么问题?

值和位置直接或间接映射

1.如果值很分散

  • 解决办法:不管值有多分散,只开10个空间(一种比喻),这些值如何映射到对应位置?

除留余数法:

  •  i = key % 空间的大小 余数是多少,就存在所开的空间的哪个位置。

导致问题:

  • 哈希冲突/碰撞:不同的值可能会映射到相同的位置

解决哈希冲突:

  • 闭散列:开放定址法(例如:线性探测/二次探测...)

  • 导致问题:会互相影响导致冲突

  • 开散列/哈希桶/拉链法:

建立节点,将余数相等的多个值,链接起来映射到余数位置,不再会互相影响

什么时候是结束条件?加一下状态标记

在这里就可用来避免因为该处因为值被删除为空而导致需寻找的值存在但无法找到,使用状态标记DELETE来表示该处被删除,继续往后查找

底层实现,闭散列,线性探测

HashTable.h 基础的结构

#pragma once

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 HashTable
{
private:
	vector < HashData<K, V> _tables;
	size_t _n; // 有效数据的个数

};

哈希表的插入Insert:

	bool Insert(const pair<K, V>& kv)
	{
		//表的空间满了需要扩容
		//...

		size_t hashi = kv.first % _tables.size();//size以内而不是capacity
		//走线性探测
		while (_tables[hashi]._state == EXIST)
		{
			++hashi;
			//防止超出size的范围
			hashi %= _tables.size();
		}
		//遇到空状态或者删除状态的位置
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;

	}
扩容

思考:哈希表什么情况下扩容,如何扩容?

以空间换时间

负载因子越高,冲突率越高,效率就越低

负载因子越小,冲突率越低,效率就越高,空间利用率就越低

扩容

我们需要在扩容和插入时解决以下问题

1.非整数相除(因此分子分母各乘10)

2.防止分母_tables.size()一开始是0,因此需要在构造的时候通过resize初始化size

3.resize会去调用HashData的默认构造函数pair有默认构造可以不处理,状态就需要给一个缺省值EMPTY。这时候开出的空间的状态就是空

4.扩容后,size()变大(一般扩大两倍),不能直接将整个原来的值和位置拷贝下来,因为%模的是新的size会导致原来的值找不到,需要将原来的值重新映射到新的空间当中

实际上,哈希表插入的效率平均很高,但是有一些波动,因为在扩容时的效率是不高的,代价很大。解决方案:redis一般用来作缓存,扩容时采取一个方案,当前需要拷贝,并不是一次性将所有数据拷贝下来,每访问一个数据再访问。

此时的负载因子变小,冲突变小,以空间换时间

这里简单提一下Redis其中的一个核心原理:

Redis 是一款高性能的键值(KV)型内存数据库

内存存储与高效数据结构

  • 内存优先:数据主要存储在内存中,读写速度极快(微秒级)。支持异步持久化到磁盘,保证数据安全。

  • 丰富的数据结构:除基础的字符串(String)外,还支持哈希(Hash)、列表(List)、集合(Set)、有序集合(ZSet)、位图(Bitmap)等。每种结构有专门优化的实现,如:

    • 跳跃表(Skip List):实现有序集合的快速范围查询。

    • 压缩列表(ziplist):节省内存的小数据存储结构。

    • 字典(哈希表):用于快速键值查找,通过渐进式 Rehash 避免阻塞。


修改insert,并添加构造函数初始化表大小

	HashTable()
	{
		_tables.resize(10);
	}

	bool Insert(const pair<K, V>& kv)
	{
		//表的空间满了需要扩容
		if (_n * 10 / _tables.size() >= 7)
		{
			//size_t newsize = _tables.size() * 2;
			比较原始的一种方法
			//vector<HashData<K, V>> newtables(newsize);
			旧表重新计算负载到新表
			//for (size_t i = 0; i < _tables.size(); i++)
			//{}

			//新方法 不建立一个vector而是建立一个哈希表
			HashTable<K, V> newHT;
			newHT._tables.resize(_tables.size() * 2);
			//旧表重新计算负载到新表
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					//插入到新表
					newHT.Insert(_tables[i]._kv);//直接复用Insert下的逻辑
				}
			}
			_tables.swap(newHT._tables);
		}

		size_t hashi = kv.first % _tables.size();//size以内而不是capacity
		//走线性探测
		while (_tables[hashi]._state == EXIST)
		{
			++hashi;
			//防止超出size的范围
			hashi %= _tables.size();
		}
		//遇到空状态或者删除状态的位置
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;

		return true;
	}

寻找Find()

HashData<K, V>* Find(const K& key)
{
	size_t hashi = key % _tables.size();

	while (_tables[hashi]._state != EMPTY)//只要不等于空就继续查找
	{
		if (_tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];//找到了就返回这个位置的地址
		}

		++hashi;
		//防止超出size的范围
		hashi %= _tables.size();
	}
	return nullptr;
}

删除Erase()

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret == nullptr)
		{
			return false;
		}
		else
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
	}

 以上代码还具有缺陷,如果我删除一个值,只是更改了这个值位置它的状态为DELETE,而导致就算删除了也还能找到:

请注意在编程的时候如果打错了类模板中成员变量的名字,会导致出现一些未初始化的编译错误,但之前没有。这是因为,模版是按需实例化,例如我这里Find函数中应该写key我写成了kv.first,此时我看到的不会给这里报错,因为模版是按需实例化,并没有调用Find成员函数,因此编译器没发现这里的错误,没有标记。这是在拷贝insert的相关逻辑的代码过来时忘记修改。

就会出现这样的报错: 

修改Find()

添加条件,如果这个地方的值的状态为存在,才返回这个位置的地址

HashData<K, V>* Find(const K& key)
{
	size_t hashi = key % _tables.size();//size以内而不是capacity

	while (_tables[hashi]._state != EMPTY)
	{
		if (_tables[hashi]._state == EXIST &&
			_tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];//找到了就返回这个位置的地址
		}

		++hashi;
		//防止超出size的范围
		hashi %= _tables.size();
	}
	return nullptr;
}

检测扩容机制:

修改insert()

实际上insert是不允许冗余的,当我在测试用例多添加一个32,这个32成功进入到哈希表,这是不被允许的

不需要析构和拷贝,因为size_t 是内置类型会值拷贝,vector自定义类型会调用它的析构和拷贝构造,是满足我们的需求的。


key不支持强转整形取模,自己提供转换成整形的仿函数

以上都是对于整形可以取模,那么如果要存入的是string对象,是结构体对象呢?

struct Person
{
	//string _id;

	string _name;
	int _age;
	string school;
};

// key不支持强转整形取模,那么就要自己提供转换成整形仿函数
void TestHT3()
{
	HashTable<Person, int> xxht;

	//HashTable<string, int, StringHashFunc> ht;
	HashTable<string, int> ht;
	ht.Insert(make_pair("sort", 1));
	ht.Insert(make_pair("left", 1));
	ht.Insert(make_pair("insert", 1));

	/*cout << StringHashFunc()("bacd") << endl;
	cout << StringHashFunc()("abcd") << endl;
	cout << StringHashFunc()("aadd") << endl;*/
}

解决办法:

将值和存储位置建立映射关系,先将string转化为整形,再来和存储位置建立映射关系。

使用仿函数来解决,修改这几处代码:能将double 、float、char转成整形,记得将所有使用HashTable定义对象的时候的模版参数都添加上Hash。

单独写一个能将string转成整形的:这种方案并不好,首字母一样就会冲突

struct StringHashFunc
{
	size_t operator()(const string& key)
	{
		return key[0];//返回首字母
	}
};

修改string的仿函数:

将整个字符串中字符的ascii码值加起来,再去模运算,到相应的位置,这种方案能减少冲突。

//将整个字符串中字符的ascii码值加起来,再去模运算,到相应的位置
struct StringHashFunc
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash += ch;
		}
		return hash;
	}
};

缺陷:ascii码加等值相等但是字符串不一样但冲突

     abcd
	 bcad
	 aadd
	 BKDR

第三种: 字符串哈希算法BKDR

运行结果:字符顺序变,结果就会改变,但是还是会存在重复(在有限的整型表示范围内,字符串无限长度,根据鸽巢原理一定会有重叠)

在实际使用unordered_map时并没有在定义对象的时候模版参数里传仿函数

解决类似正常使用哈希:声明对象时不传仿函数,使用特化

由于string经常做key,因此有特别的方案,对string走特化:这篇文章有专门讲解模版的特化

template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

不是string就直接走普通,如果是string就走string特化

如果使用一个类来做key呢?

假如是一个日期类,写仿函数的时候就可以将日期类的年月日加起来,可以用类似于BKDR的方法来避免日期顺序不同但数字相同的重复。

如果这里是一个自定义Person类来做key呢?

可以用人的身份证号码,如果没有就可以将name转换后加上年龄加上学校的ascii的整形,再取模,让值不那么容易冲突。

struct Person
{
	//string _id;

	string _name;
	int _age;
	string school;
};

底层实现,开散列/哈希桶/拉链法:

hashmap.h结构

namespace hash_bucket
{
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;

		HashNode<K, V>* _next;
	};

	template<class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:

	private:
		vector<Node>* _tables; //方案一:在原生数组上,每个位置挂节点指针
	  //vector<list<pair<K, V>>>* _tables;//方案二:数组中的每个元素是一个链表
		size_t _n;
	};
}

由于后续还要写迭代器,如果这里是方案二list会更麻烦,因此使用方案一原生再挂指针链接。

构造:

        HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 0;
		}

插入Insert()

使用头插

		bool Insert(const pair<K, V>& kv)
		{
			//扩容
			// ..
			


			//1.算对应位置
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			//头插
			newnode->next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

通过几个方法分别看负载因子以及哈希桶的数量的范围 :

运行结果:

在第几个桶的位置Find()

//在第几个桶的位置
Node* Find(const K& key)
{
	size_t hashi = key % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		//链表的遍历
		if (cur->_kv.first == key)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

给哈希桶扩容

bool Insert(const pair<K, V>& kv)
{
	//扩容
	// 负载因子为1则表示平均一个桶下面一个数据
	if (_n == _tables.size())
	{
		HashTable<K, V> newHT;
		newHT._tables.resize(_tables.size() * 2);
		//旧表重新计算负载到新表
		for (size_t i = 0; i < _tables.size(); i++) 
		{
			Node* cur = _tables[i];
			//遍历这个桶,只要cur还有值,就插入到桶里
			while (cur)
			{
				newHT.Insert(cur->_kv);
				cur = cur->_next;
			}	
			
		}
		_tables.swap(newHT._tables);
	}


	//1.算对应位置 
	size_t hashi = kv.first % _tables.size();
	Node* newnode = new Node(kv);
	//头插
	newnode->next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;
	return true;
}

一交换,把旧表的vector换给了新的newHT,旧表出了作用域就会调用析构函数,在析构函数中再释放节点,再释放vector。

由于在释放的时候,vector的释放只会释放自己的空间,但是,下面接的链表需要我们自己释放

		~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;
			}
		}

而实际上,这种方法非常浪费,再new上10个节点之后,又要释放10个节点。

优化:遍历旧表,直接将旧表中的数据挪动到新表

1. 扩容机制

  • 触发条件:当元素数量 _n 等于哈希表的大小(即负载因子为 1)时触发扩容。

  • 新表创建:新表的大小是原表的 2 倍,所有桶初始化为 nullptr

  • 数据迁移

    • 遍历旧表的每个桶,将节点逐个重新哈希到新表。

    • 头插法迁移:将旧节点的 _next 指向新表的桶头,然后更新桶头为当前节点。

    • 旧表的桶置空(实际可省略,因为旧表后续被替换)。

2. 插入新元素

  • 哈希计算:用 kv.first % _tables.size() 确定桶的位置。

  • 头插法插入:新节点插入到对应桶的链表头部。

  • 更新计数:元素数量 _n 自增,返回插入成功 true

		bool Insert(const pair<K, V>& kv)
		{
			//不允许冗余:
			if (Find(kv.first)) return false;
			//扩容
			// 负载因子为1则表示平均一个桶下面一个数据
			if (_n == _tables.size())
			{
            	//遍历旧表,直接将旧表中的数据挪动到新表
				vector<Node*> newTables(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++) 
				{
					Node* cur = _tables[i];
					//遍历这个桶,只要cur还有值,就插入到桶里
					while (cur)
					{
						//先保存next
						Node* next = cur->_next;
						//将当前节点头插到新表的位置
						size_t hashi = cur->_kv.first % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}	
					_tables[i] = nullptr;//将旧表的该位置置空,无所谓
				}
				_tables.swap(newTables);

			}

			//1.算对应位置 
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			//头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

删除Erase()

链表里的删除都需要前后兼顾

1.如果删除的是中间节点,那就需要让前一个指向后一个

2.如果删除的是头结点,那么就要让这(记录cur的prev)指向cur的下一个

bool Erase(const K& key)
{
    size_t hashi = key % _tables.size();
    Node* cur = _tables[hashi];
    Node* prev = nullptr;

    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr)
            {
                _tables[hashi] = cur->_next;
            }
            else
            {
                prev->_next = cur->_next;
            }
            delete cur;
            --_n; 
            return true;
        }
        prev = cur;
        cur = cur->_next;
    }
    return false;
}

实际上现在的代码还存在许多的缺陷,与上面所讲解过的哈希表一样,需要我们一步一步来解决问题,现在,当我们运行测试用例:如果我们 给哈希表传的模版参数是string,会失败,因为不支持取模string。

void TestHT3()
{

	HashTable<string, int> ht;
	ht.Insert(make_pair("sort", 1));
	ht.Insert(make_pair("left", 1));
	ht.Insert(make_pair("insert", 1));
}

按照前面所讲的哈希表,我们可以直接复用前面所写的特化。将string转成整形,再进行取模

使用仿函数来解决,修改这几处代码:能将double 、float、char转成整形,记得将所有使用HashTable定义对象的时候的模版参数都添加上Hash。

结语:

       随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2334884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

设计模式——建造者模式(生成器模式)总结

当我们需要创建一个非常复杂的对象时&#xff0c;可以使用建造者模式&#xff0c;分步骤建造一个对象&#xff0c;最后将完整的对象返回给客户端。 比如&#xff0c;我们要生成一个房子对象&#xff0c;建造一个房子&#xff0c;需要打地基、盖围墙、盖地板、安装门、安装窗户…

使用Python爬虫的2大原因和6大常用库

爬虫其实就是请求http、解析网页、存储数据的过程&#xff0c;并非高深的技术&#xff0c;但凡是编程语言都能做&#xff0c;连Excel VBA都可以实现爬虫&#xff0c;但Python爬虫的使用频率最高、场景最广。 这可不仅仅是因为Python有众多爬虫和数据处理库&#xff0c;还有一个…

Java 架构设计:从单体架构到微服务的转型之路

Java 架构设计&#xff1a;从单体架构到微服务的转型之路 在现代软件开发中&#xff0c;架构设计的选择对系统的可扩展性、可维护性和性能有着深远的影响。随着业务需求的日益复杂和用户规模的不断增长&#xff0c;传统的单体架构逐渐暴露出其局限性&#xff0c;而微服务架构作…

C# 混淆代码工具--ConfuserEx功能与使用指南

目录 1 前言1.1 可能带来的问题 2 ConfuserEx2.1 简介2.2 功能特点2.3 基本使用方法2.4 集成到MSBuild2.5 深入设置2.5.1 保护机制2.5.1.1 ConfuserEx Protection 2.5.2 精细的代码保护主要特性1. decl-type(string)2.full-name(string)3. is-public()4. match(string)5. match…

使用PyTorch实现目标检测边界框转换与可视化

一、引言 在目标检测任务中&#xff0c;边界框&#xff08;Bounding Box&#xff09;的坐标表示与转换是核心基础操作。本文将演示如何&#xff1a; 实现边界框的两种表示形式&#xff08;角点坐标 vs 中心坐标&#xff09;之间的转换 使用Matplotlib在图像上可视化边界框 验…

nlp面试重点

深度学习基本原理&#xff1a;梯度下降公式&#xff0c;将损失函数越来越小&#xff0c;最终预测值和实际值误差比较小。 交叉熵&#xff1a;-p(x)logq(x)&#xff0c;p(x)是one-hot形式。如果不使用softmax计算交叉熵&#xff0c;是不行的。损失函数可能会非常大&#xff0c;…

欢乐力扣:反转链表二

文章目录 1、题目描述2、思路 1、题目描述 反转链表二。  给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 2、思路 参考官方题解&#xff0c;基本思路…

14-大模型微调和训练之-Hugging Face 模型微调训练(基于 BERT 的中文评价情感分析(二分类))

1. datasets 库核心方法 1.1. 列出数据集 使用 datasets 库&#xff0c;你可以轻松列出所有 Hugging Face 平台上的数据集&#xff1a; from datasets import list_datasets # 列出所有数据集 all_datasets list_datasets() print(all_datasets)1.2. 加载数据集 你可以通过…

论文阅读笔记——Reactive Diffusion Policy

RDP 论文 通过 AR 提供实时触觉/力反馈&#xff1b;慢速扩散策略&#xff0c;用于预测低频潜在空间中的高层动作分块&#xff1b;快速非对称分词器实现闭环反馈控制。 ACT、 π 0 \pi_0 π0​ 采取了动作分块&#xff0c;在动作分块执行期间处于开环状态&#xff0c;无法及时响…

ISIS协议(动态路由协议)

ISIS基础 基本概念 IS-IS&#xff08;Intermediate System to Intermediate System&#xff0c;中间系统到中间系统&#xff09;是ISO &#xff08;International Organization for Standardization&#xff0c;国际标准化组织&#xff09;为它的CLNP&#xff08;ConnectionL…

UniApp 实现兼容 H5 和小程序的拖拽排序组件

如何使用 UniApp 实现一个兼容 H5 和小程序的 九宫格拖拽排序组件&#xff0c;实现思路和关键步骤。 一、完整效果图示例 H5端 小程序端 git地址 二、实现目标 支持拖动菜单项改变顺序拖拽过程实时预览移动位置拖拽松开后自动吸附回网格兼容 H5 和小程序平台 三、功能…

【网络协议】WebSocket讲解

目录 webSocket简介 连接原理解析: 客户端API 服务端API&#xff08;java&#xff09; 实战案例 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;编写服务端逻辑 &#xff08;3&#xff09;注册配置类 &#xff08;4&#xff09;前端连接 WebSocket 示例…

啥是Spring,有什么用,既然收费,如何免费创建SpringBoot项目,依赖下载不下来的解决方法,解决99%问题!

一、啥是Spring&#xff0c;为啥选择它 我们平常说的Spring指的是Spring全家桶&#xff0c;我们为什么要选择Spring&#xff0c;看看官方的话&#xff1a; 意思就是&#xff1a;用这个东西&#xff0c;又快又好又安全&#xff0c;反正就是好处全占了&#xff0c;所以我们选择它…

一天时间,我用AI(deepseek)做了一个配色网站

前言 最近在开发颜色搭配主题的相关H5和小程序&#xff0c;想到需要补充一个web网站&#xff0c;因此有了这篇文章。 一、确定需求 向AI要答案之前&#xff0c;一定要清楚自己想要做什么。如果你没有100%了解自己的需求&#xff0c;可以先让AI帮你理清逻辑和思路&#xff0c;…

Spring - 13 ( 11000 字 Spring 入门级教程 )

一&#xff1a; Spring AOP 备注&#xff1a;之前学习 Spring 学到 AOP 就去梳理之前学习的知识点了&#xff0c;后面因为各种原因导致 Spring AOP 的博客一直搁置。。。。。。下面开始正式的讲解。 学习完 Spring 的统一功能后&#xff0c;我们就进入了 Spring AOP 的学习。…

Spring Cloud Alibaba微服务治理实战:Nacos+Sentinel深度解析

一、引言 在微服务架构中&#xff0c;服务发现、配置管理、流量控制是保障系统稳定性的核心问题。Spring Cloud Netflix 生态曾主导微服务解决方案&#xff0c;但其部分组件&#xff08;如 Eureka、Hystrix&#xff09;已进入维护模式。 Spring Cloud Alibaba 凭借 高性能、轻…

红宝书第三十六讲:持续集成(CI)配置入门指南

红宝书第三十六讲&#xff1a;持续集成&#xff08;CI&#xff09;配置入门指南 资料取自《JavaScript高级程序设计&#xff08;第5版&#xff09;》。 查看总目录&#xff1a;红宝书学习大纲 一、什么是持续集成&#xff1f; 持续集成&#xff08;CI&#xff09;就像咖啡厅的…

Java—HTML:3D形变

今天我要介绍的是在Java HTML中CSS的相关知识点内容之一&#xff1a;3D形变&#xff08;3D变换&#xff09;。该内容包含透视&#xff08;属性&#xff1a;perspective&#xff09;&#xff0c;3D变换&#xff0c;3D变换函数以及案例演示&#xff0c; 接下来我将逐一介绍&…

什么是音频预加重与去加重,预加重与去加重的原理是什么,在什么条件下会使用预加重与去加重?

音频预加重与去加重是音频处理中的两个重要概念&#xff0c;以下是对其原理及应用条件的详细介绍&#xff1a; 1、音频预加重与去加重的定义 预加重&#xff1a;在音频信号的发送端&#xff0c;对音频信号的高频部分进行提升&#xff0c;增加高频信号的幅度&#xff0c;使其在…

免费下载 | 2025清华五道口:“十五五”金融规划研究白皮书

《2025清华五道口&#xff1a;“十五五”金融规划研究白皮书》的核心内容主要包括以下几个方面&#xff1a; 一、五年金融规划的重要功能与作用 凝聚共识&#xff1a;五年金融规划是国家金融发展的前瞻性谋划和战略性安排&#xff0c;通过广泛听取社会各界意见&#xff0c;凝…