哈希算法(闭散列)- 支持string(仿函数的应用 / 偏特化)

仿函数的应用

仿函数的应用就是替代C语言中的函数指针,仿函数就是模板 + 结构体 + 运算符重载

闭散列仿函数实现方式1

直接分开定义:模板定义一类结构体,string特殊的单独定义

// 直接string类型单独定义一个结构体
// HashFunc<class K>做Hash(仿函数)的缺省参数
template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t) k;
	}
};

struct HashString
{
	size_t operator()(const string& k)
	{
		// BKDR
		size_t hashi = 0;
		for (auto e : k)
		{
			hashi *= 31;
			hashi += e;
		}

		return hashi;
	}
};

直接string类型单独定义一个结构体
HashFunc<class K>做Hash(仿函数)的缺省参数

使用说明,当使用int参数时可以不加Hash(仿函数的声明),但是要使用string时实例化的是后必须要加class Hash的声明,确定使用的是string转成int的仿函数

模板定义的改变 - 增加了仿函数的类 - class Hash = HashFunc<K>(带缺省值 - int可用)

// Hash是仿函数的模板
// 如果不传默认走模板 - HashFunc<K>
template<class K, class V, class Hash = HashFunc<K>>

如果是以上方式一的仿函数实现方式,那么实例化时若是string类型需要手动传入HashString的结构体声明,确保调用此结构体中的仿函数

// 实例化
HashTable<string, string, HashString> ht;

仿函数实现一完整程序

// 仿函数 - 未使用偏特化(仿函数实现方式1)
template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t) k;
	}
};

struct HashString
{
	size_t operator()(const string& k)
	{
		size_t hashi = 0;
		for (auto e : k)
		{
			hashi *= 31;
			hashi += e;
		}

		return hashi;
	}
};

namespace open_adress
{
	enum Status
	{
		Empty,
		Exist,
		Delete
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _s;
	};

	//一个结构体 / 一个类对应一个模板
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		// 构造函数
		HashTable()
		{
			_tables.resize(10);
		}

		// 线性探测
		bool Insert_Linear(const pair<K, V>& kv)
		{
			// string类型仿函数特殊声明
			Hash _hf; 

			if (Find(kv.first))
				return false;
			// 考虑扩容
			// 当负载因子等于0.7时就扩容
			if (_n * 10 / _tables.size() == 7)
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newSize);

				for (size_t i = 0; i < _tables.size(); ++i)
				{
					if(_tables[i]._s == Exist)
						newHT.Insert_Linear(_tables[i]._kv);
				}

				//_tables.swap(newHT._tables);
				newHT._tables.swap(_tables);
			} 
			
			size_t hashi = _hf(kv.first) % _tables.size();

			while (_tables[hashi]._s == Exist)
			{
				++hashi;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._s = Exist;
			++_n;
			return true;
		}

		// 二次探测
		bool Insert_Twice(const pair<K, V>& kv)
		{
			Hash _hf;

			if (Find(kv.first))
				return false;

			if (_n * 10 / _tables.size() == 7)
			{
				size_t newSize = _tables.size() * 2;
				HashTable<K, V, Hash> newHt;
				newHt._tables.resize(newSize);
				
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._s == Exist)
						newHt.Insert_Twice(_tables[i]._kv);
				}
					
				newHt._tables.swap(_tables);
			}

			size_t hashi = _hf(kv.first) % _tables.size();
			size_t hashtwice = 0;

			while (_tables[hashi]._s == Exist)
			{
				hashi = _hf(kv.first) % _tables.size();
				//++hashtwice; // 优化
				// Warning: +=: 从double转换到size_t,可能丢失数据
				hashi += size_t(pow(++hashtwice, 2));
				hashi %= _tables.size();
			}
			
			_tables[hashi]._kv = kv;
			_tables[hashi]._s = Exist;
			++_n;

			return true;
		}

		// Delete状态存在的意义:如果没有Delete状态,会导致Empty后面的数据均无法访问
		HashData<K, V>* Find(const K& k)
		{
			Hash _hf;
			size_t hashi = _hf(k) % _tables.size();

			while (_tables[hashi]._s != Empty)
			{
				// 易错点:如果此值是已经删除的值,那么也会被当作是存在的值,则被删除的值也会被Find
				//无法重新插入
				// 但插入值是只要Find不到就可以插入,所以会无法插入目标值,或把目标值覆盖
				// if (_tables[hashi]._kv.first == k)
				if(_tables[hashi]._s == Exist
				&& _tables[hashi]._kv.first == k)
				{
					return &_tables[hashi];
				}

				++hashi;
				hashi %= _tables.size();
			}

			return nullptr;
		}

		// 伪删除法
		bool Erase(const K& k)
		{
			HashData<K, V>* ret = Find(k);
			
			if (ret)
			{
				ret->_s = Delete;
				--_n;
				return true;
			}

			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i]._s == Exist)
				{
					//cout << "[" << i << "]-> " << _tables[i]._kv.first << endl;
					cout << "[" << i << "]-> " << _tables[i]._kv.first << " -> " << _tables[i]._kv.second << endl;
				}

				else if (_tables[i]._s == Empty)
				{
					cout << "[" << i << "]-> " << "Empty" << endl;
				}

				else
				{
					cout << "[" << i << "]-> " << "Delete" << endl;
				}
			}

			cout << endl;
		}


	private:
		vector<HashData<K, V>> _tables;
		size_t _n;
	};
	void TestString()
	{
		// 模板实例化
		HashTable<string, string, HashString> ht;
		
		ht.Insert_Twice(make_pair("Sort", "排序"));
	    ht.Insert_Twice(make_pair("Left", "左边"));
		ht.Insert_Twice(make_pair("Right", "右边"));
		ht.Insert_Twice(make_pair("Middle", "中间"));

		ht.Print();
	}
};

闭散列仿函数实现方式2

库中的unordered_map是不用在实例化时传HashString,(模板时要带有参数class Hash = HashFun<K>),要实现此效果就是要使用模板的偏特化template<>,模板偏特化的定义就是找最匹配,将string偏特化,如果是其它类型就走模板template<class K>,其余则走template<>,函数名均是HashFunc,所以缺省值中的HashFun<K>变化,不用再实例化传HashString

 仿函数 - 模板 + 结构体 + 运算符重载 - 意义:替代函数指针
template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t) k;
	}
};

// 偏特化 - 特殊化string,当为string时默认走此模板,偏特化的意义是在类型不是string的情况下走默认K的模板,在string走特殊化模板
template<>
struct HashFunc<string>
{
	size_t operator()(const string& k)
	{
		size_t hashi = 0;
		for (auto e : k)
		{
			hashi *= 31;
			hashi += e;
		}

		return hashi;
	}
};

缺省值是class Hash = HashFun<K>,使用偏特化,两个结构体相同,都调用同一个结构体,所以不用实例化传结构体,只是最优匹配原则,string类型会自动匹配HashFunc<string>,这样就解决了实例化要传HashString的问题,现在仅需要传key / value的类型即可

仿函数实现误区

以下为仿函数使用的错误案例:将string和K类型的封装再一起,在K为int的时候可以正常使用(二者构成函数重载 - 参数类型不同),但当K为string时就会冲突,二者函数名和参数均相同,就会产生冲突

//Error
template<class K>
struct HashFunc
{
	size_t operator()(const K& k)
	{
		return (size_t)k;
 
	}
	
	// string时会产生冲突
	size_t operator()(const string& k)
	{
		size_t hashi = 0;
		for (auto e : k)
		{
			hashi *= 31;
			hashi += e;
		}

		return hashi;
	}
};

string类型支持转换成key的测试代码

封装在Hashtable.h中,main函数中直接域作用名调用

void TestString()
{
	HashTable<string, string> ht;
		
	//ht.Insert_Linear(make_pair("Sort", "排序"));
	//ht.Insert_Linear(make_pair("Left", "左边"));
	//ht.Insert_Linear(make_pair("Right", "右边"));
	//ht.Insert_Linear(make_pair("Middle", "中间"));

	ht.Insert_Twice(make_pair("Sort", "排序"));
	ht.Insert_Twice(make_pair("Left", "左边"));
	ht.Insert_Twice(make_pair("Right", "右边"));
	ht.Insert_Twice(make_pair("Middle", "中间"));

	ht.Print();
}

总结:仿函数:模板 + 结构体 + 运算符重载 / 简化:模板 + 模板偏特化 + 结构体 + 运算符重载

End!!!

<think>好的,我现在需要帮用户解决如何在C++中为自定义类型特化std::hash或者实现自定义哈希函数的问题。用户可能在使用无序容器时遇到了问题,比如std::unordered_map或std::unordered_set,这些容器需要哈希函数来存储自定义类型。用户已经看过一些引用资料,提到可以通过特化std::hash或者定义独立的哈希函数对象来实现,但需要具体的示例和步骤。 首先,我得回忆一下C++中哈希函数的基本要求。哈希函数必须是一个可调用对象,接受自定义类型的实例,返回std::size_t类型的哈希值。同时,还需要提供相等比较函数,因为哈希容器需要处理可能的哈希冲突。 接下来,我需要考虑两种方法:特化std::hash和使用独立哈希函数。对于特化std::hash,需要为自定义类型在std命名空间内特化模板。例如,如果用户有一个Person类,包含name和age,那么如何组合这些成员的哈希值。可能需要使用标准库已有的哈希特化,比如std::hash<std::string>和std::hash<int>,然后将它们组合起来。通常的做法是使用异或或者boost的hash_combine函数来组合哈希值,避免简单异或导致的碰撞问题。 另外,用户可能需要知道如何将自定义哈希函数作为模板参数传递给容器。这时候需要定义一个结构体,重载operator(),然后在声明容器时指定该哈希函数类型。比如,unordered_set<Person, PersonHash>。 还需要注意,当特化std::hash时,必须确保特化在std命名空间内,否则编译会出错。同时,如果自定义类型有多个数据成员,组合哈希值时应该使用可靠的方法,避免不同对象产生相同的哈希值。例如,仅仅异或可能不够,特别是当成员有相同的值时。这时候可能需要像hash_combine这样的方法,虽然标准库没有提供,但可以自己实现。 另外,用户可能关心是否需要同时提供operator==的重载。是的,哈希容器需要比较键是否相等,所以必须为自定义类型定义operator==,或者在容器中指定自定义的比较函数。 然后,我需要提供具体的代码示例。比如,定义一个Person类,特化std::hash<Person>,或者在另一个结构体PersonHash中实现哈希函数。同时,展示如何在unordered_set中使用这些哈希函数。 还要注意,C++标准库已经为很多内置类型和标准类型特化了std::hash,比如std::string、int等,但用户的自定义类型需要自己处理。另外,C++17开始支持透明哈希,但可能用户的问题不需要涉及这些高级特性。 最后,可能需要提到一些常见问题,比如哈希碰撞、性能考虑,以及如何测试哈希函数的质量。但根据用户的问题,可能主要关注实现方法,所以示例和步骤要清晰。 总结步骤: 1. 定义自定义类型,比如Person类。 2. 特化std::hash<Person>,在operator()中组合各成员的哈希值。 3. 或者定义独立的哈希函数结构体PersonHash。 4. 确保自定义类型有operator==的重载。 5. 在声明容器时,指定哈希函数类型,如使用std::hash<Person>或PersonHash。 需要验证示例代码的正确性,比如是否正确使用std命名空间,是否正确组合哈希值,是否存在编译错误等。另外,是否需要包含头文件,比如<functional>和<string>,以及使用using namespace std;是否合适,或者是否需要显式指定std::。 可能还需要提到C++11及以后版本的支持,以及Boost库中的hash_combine作为替代方案,但用户可能希望使用标准库的方法,所以提供一个自己实现的hash_combine函数。 现在把这些思考整理成步骤和示例,确保代码正确,解释清晰,覆盖用户可能的疑问点。</think>在 C++ 中为自定义类型实现哈希函数有两种主要方式:**特化 `std::hash` 模板**或**定义独立的哈希函数对象**。以下是具体实现方法和示例: --- ### 方法 1:特化 `std::hash` 模板 #### 步骤 1. **定义自定义类型**(如 `Person` 类) 2. **在 `namespace std` 中特化 `std::hash`** 3. **实现 `operator()` 函数**,组合各成员变量的哈希值 4. **提供 `operator==` 重载**(哈希容器需要) #### 示例代码 ```cpp #include <string> #include <functional> // 自定义类型 struct Person { std::string name; int age; // 必须提供相等运算符 bool operator==(const Person& other) const { return name == other.name && age == other.age; } }; // 特化 std::hash namespace std { template<> struct hash<Person> { size_t operator()(const Person& p) const { // 组合 name 和 age 的哈希值 size_t name_hash = hash<string>{}(p.name); size_t age_hash = hash<int>{}(p.age); // 使用异或组合哈希(可改进为更复杂的组合方式) return name_hash ^ (age_hash << 1); } }; } // 使用示例 #include <unordered_set> int main() { std::unordered_set<Person> person_set; // 自动使用特化的 std::hash<Person> return 0; } ``` --- ### 方法 2:定义独立哈希函数对象 #### 步骤 1. **定义自定义类型** 2. **定义独立哈希结构体**(如 `PersonHash`) 3. **实现 `operator()` 函数** 4. **显式指定哈希类型**在容器中 #### 示例代码 ```cpp #include <string> #include <functional> struct Person { std::string name; int age; bool operator==(const Person& other) const { return name == other.name && age == other.age; } }; // 独立哈希函数对象 struct PersonHash { size_t operator()(const Person& p) const { size_t name_hash = std::hash<std::string>{}(p.name); size_t age_hash = std::hash<int>{}(p.age); // 更稳健的组合方式(类似 Boost 的 hash_combine) return name_hash ^ (age_hash + 0x9e3779b9 + (name_hash << 6) + (name_hash >> 2)); } }; // 使用示例 #include <unordered_set> int main() { std::unordered_set<Person, PersonHash> person_set; // 显式指定哈希函数 return 0; } ``` --- ### 关键注意事项 1. **哈希组合技巧**:简单异或(`^`)容易导致碰撞,建议使用类似 `hash_combine` 的算法: ```cpp template <class T> inline void hash_combine(std::size_t& seed, const T& v) { seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } ``` 2. **哈希质量**:确保不同对象的哈希值尽可能唯一,否则会影响容器性能[^3] 3. **命名空间限制**:特化 `std::hash` 必须在 `namespace std` 中[^1] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值