【C++】哈希

一. 哈希的概念及性质

1. 基本概念

哈希(散列) : 是一种映射思想, 将键值(元素)通过哈希函数与哈希值(存储地址)建立映射关系.
哈希表 : 是以哈希思想实现的数据结构.
哈希函数 : 不是一个固定的函数, 只要符合哈希的映射思想, 可以建立映射关系的函数, 都可以被称为是哈希函数.
哈希冲突 : 不同的键值通过相同的哈希函数得到了相同的哈希地址.

哈希表在理想的情况下, 数据通过哈希函数直接查找到存储地址, 那么时间复杂度就可以达到 O(1).
在这里插入图片描述

2. 哈希函数

2.1 哈希函数的设计原则
  • 哈希函数的定义域必须包括需要存储的全部键值, 且如果哈希表有 n 个地址, 其值域为 [0, n-1].(尽量使映射关系一一对应, 可以快速查找和减少冲突)
  • 哈希函数计算出来的哈希值能均匀分布在整个哈希表中.(使数据映射到地址集合中任何一个地址的概率是相等的, 减少冲突)
  • 哈希函数应该尽可能简单, 实用.
2.2 常见的哈希函数

直接定址法(常用) : 直接将键值或键值的某个线性函数值为哈希值
例: Hash(key) = A*key + B (A B 均为常数)
适用: 范围比较集中, 每个数据分配一个唯一位置.
优点: 简单, 均匀.
缺点: 需要提前知道键值的分布情况.

除留余数法(常用) : 取某个不大于哈希表表长 m 的数 p, 除键值后所得的余数为哈希值.
例: Hash(key) = key % p (p<=m)
适用: 范围不集中, 分布分散的数据.
优点: 简单易用, 性能均衡.
缺点: 容易出现哈希冲突, 需要借助特定方法解决.

平方取中法 : 将键值平方后的中间几位作为哈希值.
例: 假设键值为1111, 平方值为1234321, 将其中间的三位数 343 作为哈希值.
适用: 不知道键值的分布, 而位数又不是很大的情况.

折叠法 : 将键值分割成位数相同的几部分(最后一部分可以不同), 然后将这几部分叠加求和, 并按哈希表表长, 取后几位作为哈希值.
适用: 事先不需要知道键值的分布,且键值位数比较多.

随机数法 : 选择一随机函数, 键值的随机函数值作为哈希值.
例: Hash(key) = random(key)
适用: 键值长度不等时.

3. 哈希冲突的解决方法

3.1 闭散列

闭散列又叫开放定址法, 哈希表的每个存储地址只存放一个的元素.
闭散列的基础结构
简单的使用顺序表模拟下闭散列

template<class K, class V>
class HashTable
{
	//每个存储位置的状态
	enum State { EMPTY, EXIST, DELETE };
	//每个存储位置存放的元素
	struct Elem		
	{
		pair<K, V> _val;
		State _state = EMPTY;	//每个存储位置默认为空状态
	};

public:
	HashTable(size_t capacity = 6)
	: _ht(capacity), _size(0), _totalSize(0)
	{}
	
	//删除
	bool Erase(const K& key)
	{
		size_t pos = HashFunc(key);	//哈希函数获取映射地址
		while (_ht[pos]._state != EMPTY)
		{
			//查找数据存在时跳过删除状态的存储地址
			if (_ht[pos]._state == EXIST && _ht[pos]._val.first == key)	//若存在, 且状态也是存在
			{
				_ht[pos]._state = DELETE;	//存储位置状态设置为删除, 若设为空, 可能会影响其他键值的线性查找
				_size--;	
				return true;
			}
			pos = (pos + 1) % _ht.capacity();
		}
		return false;
	}
	
private:
	size_t HashFunc(const K& key)	//哈希函数
	{	return key % _ht.capacity();	}

private:
	vector<Elem> _ht;	//使用顺序表存放
	size_t _size;		//有效数据个数 负载因子
};

在插入发生哈希冲突时, 若哈希表的负载因子达到一定值, 就扩容空间并在新表重新映射所有数据;
若没有, 就从发生冲突的位置开始, 遍历哈希表, 直到找到空位置存储数据.
在这里插入图片描述

//插入
bool Insert(const pair<K, V>& val)
{
	//是否扩容
	if (_size * 10 / _ht.capacity() >= 7)	//若哈希表中实际存储的数据数量达到了70%, 就扩容
	{
		HashTable<K, V> new_table(_ht.capacity() * 2);	//创建新表
		for (int i = 0; i < _ht.capacity(); i++)		//依次将原表数据映射到新表中
		{
			if (_ht[i]._state == EXIST)
				new_table.Insert(_ht[i]._val);
		}
		Swap(new_table);	
	}

	//查找当前键值是否存在, 保证键值唯一
	size_t pos = HashFunc(val.first);	//哈希函数获取映射地址
	while (_ht[pos]._state != EMPTY)
	{	
		//查找数据存在时跳过删除状态的存储地址
		if (_ht[pos]._state == EXIST && _ht[pos]._val.first == val.first)	//重复则不插入
			return false;
		pos = (pos + 1) % _ht.capacity();
	}
	
	//键值不存在, 插入数据
	pos = HashFunc(val.first);	//重新获取映射地址, 插入时不跳过删除状态的存储地址
	while (_ht[pos]._state == EXIST)
		pos = (pos + 1) % _ht.capacity();

	_ht[pos]._val = val;
	_ht[pos]._state = EXIST;	//存储地址状态设置为存在
	_size++;
	return true;
}

但是这种线性探测当数据分布不均匀时, 可能会导致越来越多的数据都需要线性探测, 平均查找长度可能会越来越长, 插入, 查找都会越来越慢.
虽然可以使用二次探测(每次向后探测 i ^ 2 步)或者左右探测(当前位置的左右同时探测)来优化, 但实际上更推荐使用开散列.

3.2 开散列

开散列又叫链地址法(开链法, 哈希桶), 即存储位置存放的是单链表, 多个键值可以映射相同的哈希值, 数据直接插入链表中即可, 查找时遍历链表查找.
开散列中每个链表中存放的都是发生哈希冲突的元素, 但是开散列的不同冲突之间不会互相影响, 所以效率和适用情况会比闭散列更好.(结构下面单独实现)
在这里插入图片描述
在一般情况下, 单链表的长度不会太长的, 查找效率基本为 O(1);
若长度过长, 可以将链表转换为红黑树, 可以大幅度的减少高度, 保证效率至少为 O(log n).


二. 哈希表模拟实现

1. 哈希表的基本结构

注:
class V: 单链表的节点中存储数据的类型. 若是键值结构, 则为 pair 类型; 若为键结构, 则为 K 类型.
class KeyOfValue: 仿函数, 作用是 从需要存储的数据中得到键(key)值. 若是键值结构, 则需返回 pair.first 的; 若为键结构, 则直接返回 key.
class HF: 仿函数, 作用是 将键值映射为无符号整型, 方便映射存储地址, 不同的类型会有不同的哈希函数.

  • 哈希表的单链表节点结构
//单链表的节点
template<class V>
struct HashBucketNode
{
	typedef HashBucketNode<V> Node;
	HashBucketNode(const V& data)
		: _pNext(nullptr), _data(data)
	{}
	Node* _pNext;	//下一个节点
	V _data;		//节点的数据
};
  • 哈希表的迭代器结构
template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator
{
	typedef HashBucket<V, KeyOfValue, HF> HashBucket;
	typedef HashBucketNode<V>* PNode;
	typedef HBIterator<V, Ref, Ptr, KeyOfValue, HF> Self;
	typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;
	
	//构造函数
	HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
		: _pNode(pNode) , _pHt(pHt) {}
	//针对 const_iterator 的构造函数
	HBIterator(const It& it);	
	
	//前置++
	Self& operator++();
	//后置++
	Self operator++(int);
	
	//解引用
	Ref operator*();
	Ptr operator->();
	
	bool operator==(const Self& it)
	{	return _pNode == it._pNode;	}
	
	bool operator!=(const Self& it)
	{	return _pNode != it._pNode;	}

	PNode _pNode;             // 当前迭代器关联的节点
	HashBucket* _pHt;         // 哈希表对象的指针--方便迭代器寻找下一个节点
};
  • 哈希表的主体
//哈希桶
template<class V, class KeyOfValue, class HF = HashFunc<V>>
class HashBucket
{
	template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
	friend struct HBIterator;
	typedef HashBucketNode<V> Node;
	typedef Node* PNode;
	
public:
	typedef HBIterator<V, V&, V*, KeyOfValue, HF> iterator;
	typedef HBIterator<V, const V&, const V*, KeyOfValue, HF> const_iterator;
	
	//构造函数
	HashBucket(size_t capacity = 16)
		: _table(capacity), _size(0) {}
	
	//析构函数
	~HashBucket()	
	{	clear();	}

	//插入函数
	pair<iterator, bool> insert(const V& data);
	//删除函数
	iterator erase(const V& data);
	//查找函数
	iterator find(const V& data);

	//迭代器
	iterator begin();
	iterator end();

	//清除函数, 释放所有的单链表
	void clear();
	
	//哈希表中哈希桶的数量
	size_t bucket_count()// const
	{	return _table.capacity();	}
	
	//内置交换函数
	void swap(Self& ht)
	{
		_table.swap(ht._table);
		std::swap(_size, ht._size);
	}

private:
	//哈希函数
	size_t HashFunc(const V& data)// const
	{
		return HF()(KeyOfValue()(data)) % _table.capacity();
	}
	
	//检查扩容
	void CheckCapacity();

private:
	vector<Node*> _table;
	size_t _size;      		//哈希表中有效元素的个数
};

2. 哈希表迭代器

2.1 构造函数

在 unordered_set 中所使用的 iterator 迭代器和 const_iterator 迭代器都是哈希表的 const_iterator 迭代器;
为了 unordered_set 能够正常返回 begin() 和 end() 迭代器类型, 所以需要使用 iterator 类型能够构造创建 const_iterator 类型.

typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;

当 iterator 迭代器参数传进来时, It的类型依旧是普通迭代器 HBIterator<V, V&, V*, KeyOfValue, HF>;
当 const_iterator 迭代器参数传进来时, It的类型就会变成普通迭代器 HBIterator<V, V&, V*, KeyOfValue, HF>, 就可以达成使用 iterator 迭代器类型构造 const_iterator 迭代器类型.

HBIterator(const It& it)
	: _pNode(it._pNode)
	, _pHt(it._pHt)
{}

哈希表中的迭代器的 begin() 则定义为哈希桶中的第一个元素, end() 则定义为空.

iterator begin()
{
	for (int i = 0; i < _table.capacity(); i++)	//查找第一个元素
		if (_table[i])
			return {_table[i], this};	//this 为哈希表对象的指针
	return {nullptr, this};
}

iterator end()
{
	return {nullptr, this};		//this 为哈希表对象的指针
}
2.2 operator++

前置++的移动逻辑:
若当前节点的 next 为真, 代表当前哈希桶还有数据, 直接移动至 next 节点;
若当前节点的 next 为空, 代表当前节点为此哈希桶中的最后一个节点, 应该移动至下一个非空哈希桶中的第一个节点.
第二种情况下需要当前节点在哈希表的位置, 所以迭代器需要包含哈希表对象的指针和哈希函数, 并且迭代器类需为哈希表类的友元类.
在这里插入图片描述

//前置++
Self& operator++()
{
	if (!_pNode)	//当前节点为空或为 end()
		return *this;
		
	if (_pNode->_pNext)	//当前节点不为哈希桶的最后一个节点时
		_pNode = _pNode->_pNext;
	else
	{
		//获取当前哈希桶在哈希表中的下标后 +1 找到下一个哈希桶
		size_t bucketNo = _pHt->HashFunc(_pNode->_data) + 1;
		
		//若当前哈希桶为最后一个哈希桶
		_pNode = nullptr;
		
		//寻找下一个非空哈希桶, 返回第一个节点
		for (; bucketNo < _pHt->bucket_count(); ++bucketNo)
		{
			if (_pNode = _pHt->_table[bucketNo])
				break;
		}
	}
	return *this;
}

//后置++
Self operator++(int)
{
	Self tmp = *this;
	operator++();
	return tmp;
}
2.3 解引用

避免空指针解引用即可.

//解引用
V& operator*()
{
	if (!_pNode)
		assert(0);
	return _pNode->_data;
}

V* operator->()
{
	if (!_pNode)
		assert(0);
	return &(operator*());
}

3. 哈希表函数

3.1 insert()

插入函数为了方便 unordered_map/unordered_set 使用, 返回值使用 pair 类型.
若键值存在, 返回节点的迭代器和 false;
若键值不存在, 则直接头插在单链表中, 返回节点的迭代器和 true;

//插入函数
pair<iterator, bool> insert(const V& data)
{
	//是否扩容
	CheckCapacity();

	//查找是否存在相同键值数据存在
	KeyOfValue kov;
	size_t pos = HashFunc(data);
	Node* cur = _table[pos];
	while (cur)
	{
		if (kov(cur->_data) == kov(data))	
			return { {cur,this}, false };	//若键值存在, 返回节点的迭代器和 false;
		cur = cur->_pNext;
	}

	//头插节点
	Node* new_node = new Node(data);
	new_node->_pNext = _table[pos];
	_table[pos] = new_node;
	_size++;
	return { {new_node,this}, true };
}
//是否扩容
void CheckCapacity() 
{
	if (_size == _table.capacity())		//若哈希表中的有效元素等于哈希桶的数量就扩容
	{
		KeyOfValue kov;
		vector<Node*> new_table(_size * 2);			//创建新表
		for (int i = 0; i < _table.capacity(); i++)	//依次从旧表中转移节点至新表
		{
			Node* cur = _table[i];
			while (cur)
			{
				_table[i] = cur->_pNext;
				//旧表节点转移至新表
				size_t pos = HF()(kov(cur->_data)) % new_table.capacity();
				cur->_pNext = new_table[pos];
				new_table[pos] = cur;

				cur = _table[i];
			}
		}
		_table.swap(new_table);
	}
}
3.2 erase()

若删除成功, 返回删除节点的下一个节点的迭代器;
若失败, 返回 end() 的迭代器;

//删除函数
iterator erase(const V& data)
{
	KeyOfValue kov;
	size_t pos = HashFunc(data);	//找到键值的映射地址
	Node* cur = _table[pos];		//获取哈希桶的第一个节点

	Node* prev = nullptr;
	while (cur)
	{
		if (kov(cur->_data) == kov(data))	//若哈希桶中存在需要删除的节点
		{
			iterator ret(cur, this);
			++ret;		//这里偷懒直接调用的迭代器的 operator++
			
			if (!prev)	//若删除节点为哈希桶的第一个节点
				_table[pos] = cur->_pNext;
			else		//若不为第一个节点
				prev->_pNext = cur->_pNext;
			delete cur;	//释放节点
			_size--;
			return ret;
		}
		prev = cur;
		cur = cur->_pNext;
	}

	return { nullptr, this };	//若没有需要删除的节点, 返回 end() 的迭代器
}
3.3 find()

若查找成功, 返回查找节点的迭代器;
若失败, 返回 end() 迭代器.

//查找函数
iterator find(const V& data)
{
	KeyOfValue kov;
	size_t pos = HashFunc(data);
	Node* cur = _table[pos];
	
	while (cur)
	{
		if (kov(cur->_data) == kov(data))
			return {cur, this};
		cur = cur->_pNext;
	}
	return { nullptr, this };
}
3.4 clear()
//清除函数, 释放所有的哈希桶
void clear()
{
	//复用的迭代器和 erase()
	for (auto it = begin(); it != end();)
		it = erase(*it);
	_size = 0;
}

三. 完整代码

Hash.h

#pragma once
#include <iostream>
#include <vector>
#include <utility>
#include <cassert>

using namespace std;

//声明
template<class V, class KeyOfValue, class HF>
class HashBucket;

template<class T>
class HashFunc
{
public:
	size_t operator()(const T& val)
	{
		return val;
	}
};
//特化
template<>
class HashFunc<string>
{
public:
	size_t operator()(const string& s)
	{
		const char* str = s.c_str();
		unsigned int seed = 131; // 31 131 1313 13131 131313
		unsigned int hash = 0;
		while (*str)
		{
			hash = hash * seed + (*str++);
		}

		return hash;
	}
};

template<class V>
struct HashBucketNode
{
	typedef HashBucketNode<V> Node;
	HashBucketNode(const V& data)
		: _pNext(nullptr), _data(data)
	{}
	Node* _pNext;
	V _data;
};

template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator
{
	typedef HashBucket<V, KeyOfValue, HF> HashBucket;
	typedef HashBucketNode<V>* PNode;
	typedef HBIterator<V, Ref, Ptr, KeyOfValue, HF> Self;
	typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;

	HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
		: _pNode(pNode)
		, _pHt(pHt)
	{}

	HBIterator(const It& it)
		: _pNode(it._pNode)
		, _pHt(it._pHt)
	{}
	
	//前置++
	Self& operator++()
	{
		if (!_pNode)	//当前节点为空或为 end()
			return *this;
			
		if (_pNode->_pNext)	//当前节点不为哈希桶的最后一个节点时
			_pNode = _pNode->_pNext;
		else
		{
			//获取当前哈希桶在哈希表中的下标后 +1 找到下一个哈希桶
			size_t bucketNo = _pHt->HashFunc(_pNode->_data) + 1;
			
			//若当前哈希桶为最后一个哈希桶
			_pNode = nullptr;
			
			//寻找下一个非空哈希桶, 返回第一个节点
			for (; bucketNo < _pHt->bucket_count(); ++bucketNo)
			{
				if (_pNode = _pHt->_table[bucketNo])
					break;
			}
		}
		return *this;
	}
	
	//后置++
	Self operator++(int)
	{
		Self tmp = *this;
		operator++();
		return tmp;
	}

	Ref operator*()
	{
		if (!_pNode)
			assert(0);
		return _pNode->_data;
	}

	Ptr operator->()
	{
		if (!_pNode)
			assert(0);
		return &(operator*());
	}

	bool operator==(const Self& it)
	{
		return _pNode == it._pNode;
	}
	bool operator!=(const Self& it)
	{
		return _pNode != it._pNode;
	}

	PNode _pNode;             // 当前迭代器关联的节点
	HashBucket* _pHt;         // 哈希桶--主要是为了找下一个空桶时候方便
};



template<class V, class KeyOfValue, class HF = HashFunc<V>>
class HashBucket
{
	template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
	friend struct HBIterator;
	typedef HashBucketNode<V> Node;
	typedef Node* PNode;

	typedef HashBucket<V, HF> Self;

public:

	typedef HBIterator<V, V&, V*, KeyOfValue, HF> iterator;
	typedef HBIterator<V, const V&, const V*, KeyOfValue, HF> const_iterator;

	HashBucket(size_t capacity = 16)
		: _table(capacity)
		, _size(0)
	{}

	~HashBucket()
	{
		clear();
	}

	//插入函数
	pair<iterator, bool> insert(const V& data)
	{
		//是否扩容
		CheckCapacity();
	
		//查找是否存在相同键值数据存在
		KeyOfValue kov;
		size_t pos = HashFunc(data);
		Node* cur = _table[pos];
		while (cur)
		{
			if (kov(cur->_data) == kov(data))	
				return { {cur,this}, false };	//若键值存在, 返回节点的迭代器和 false;
			cur = cur->_pNext;
		}
	
		//头插节点
		Node* new_node = new Node(data);
		new_node->_pNext = _table[pos];
		_table[pos] = new_node;
		_size++;
		return { {new_node,this}, true };
	}

	//删除函数
	iterator erase(const V& data)
	{
		KeyOfValue kov;
		size_t pos = HashFunc(data);	//找到键值的映射地址
		Node* cur = _table[pos];		//获取哈希桶的第一个节点
	
		Node* prev = nullptr;
		while (cur)
		{
			if (kov(cur->_data) == kov(data))	//若哈希桶中存在需要删除的节点
			{
				iterator ret(cur, this);
				++ret;		//这里偷懒直接调用的迭代器的 operator++
				
				if (!prev)	//若删除节点为哈希桶的第一个节点
					_table[pos] = cur->_pNext;
				else		//若不为第一个节点
					prev->_pNext = cur->_pNext;
				delete cur;	//释放节点
				_size--;
				return ret;
			}
			prev = cur;
			cur = cur->_pNext;
		}
	
		return { nullptr, this };	//若没有需要删除的节点, 返回 end() 的迭代器
	}

	//查找函数
	iterator find(const V& data)
	{
		KeyOfValue kov;
		size_t pos = HashFunc(data);
		Node* cur = _table[pos];
		
		while (cur)
		{
			if (kov(cur->_data) == kov(data))
				return {cur, this};
			cur = cur->_pNext;
		}
		return { nullptr, this };
	}
	//迭代器
	iterator begin()
	{
		for (int i = 0; i < _table.capacity(); i++)	//查找第一个元素
			if (_table[i])
				return {_table[i], this};	//this 为哈希表对象的指针
		return {nullptr, this};
	}
	
	iterator end()
	{
		return {nullptr, this};		//this 为哈希表对象的指针
	}
	

	void clear()
	{
		for (auto it = begin(); it != end();)
			it = erase(*it);
		_size = 0;
	}

	size_t size()//const
	{
		return _size;
	}

	bool empty()//const
	{
		return 0 == _size;
	}
	
	size_t bucket_count()// const
	{
		return _table.capacity();
	}

	size_t bucket_size(const V& data)// const
	{
		size_t pos = HashFunc(data);

		Node* cur = _table[pos];
		size_t count = 0;
		while (cur)	cur = cur->_pNext, count++;

		return count;
	}

	void swap(Self& ht)
	{
		_table.swap(ht._table);
		std::swap(_size, ht._size);
	}

private:
	size_t HashFunc(const V& data)// const
	{
		return HF()(KeyOfValue()(data)) % _table.capacity();
	}

	//是否扩容
	void CheckCapacity() 
	{
		if (_size == _table.capacity())		//若哈希表中的有效元素等于哈希桶的数量就扩容
		{
			KeyOfValue kov;
			vector<Node*> new_table(_size * 2);			//创建新表
			for (int i = 0; i < _table.capacity(); i++)	//依次从旧表中转移节点至新表
			{
				Node* cur = _table[i];
				while (cur)
				{
					_table[i] = cur->_pNext;
					//旧表节点转移至新表
					size_t pos = HF()(kov(cur->_data)) % new_table.capacity();
					cur->_pNext = new_table[pos];
					new_table[pos] = cur;
	
					cur = _table[i];
				}
			}
			_table.swap(new_table);
		}
	}

private:
	vector<Node*> _table;
	size_t _size;      // 哈希表中有效元素的个数
};
  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值