详解哈希表

本文详细介绍了哈希表的基本概念,包括哈希表的工作原理、哈希冲突的处理方法(开放地址法和哈希桶),以及如何通过模拟实现和优化来提高插入、查找和删除的效率。
摘要由CSDN通过智能技术生成

我是knight-n,本篇文章我们将通过模拟实现哈希表来帮助大家理解哈希表的原理

哈希表简单介绍

哈希表也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做哈希表。

哈希表实现原理

哈希表本质上是一个支持动态增长的数组,其增删查的效率均为O(1)。之所以如此高效是因为其插入和搜索的方式,当插入时我们根据待插入元素的关键码,以散列函数计算出该元素的存储位置并按此位置进行存放。搜素元素时我们对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。通过这样的方法我们可以不经过任何比较,一次直接从表中得到要搜索的元素。

哈希冲突

哈希冲突是指当两个或多个不同的输入值经过哈希函数处理后,产生了相同的哈希值。这种冲突在哈希表等数据结构中尤其重要,因为它们依赖于哈希函数将输入值映射到特定的存储位置。以上图为例,如果插入同时插入24,48这两个数那么这两个数将会被映射到同一个位置。

为了解决哈希冲突,可以采用以下两种方法:

开放地址方法:

线性探测法:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。 再平方探测法:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等,直至不发生哈希冲突。

哈希桶:

对于相同的哈希值,使用链表进行连接。这样,所有哈希地址相同的记录都链接在同一链表中。

哈希表的模拟实现

我们分别用开放地址法和哈希桶对哈希表进行模拟实现

开放地址法

 我们创建一个类,因为哈希表的底层是动态增长的数组,所以我们直接使用vector,这样数组动态增长时可以直接调用vector的函数。代码如下

    //哈希表中储存的结点
    enum STATE { EMPTY, EXIST, DELETE };
	template<class K, class V>
	class HashData
	{
		std::pair<K, V>  _kv;
		STATE  _state;

	};
	template<class K, class V>
	class HashTable
	{
		
	public:
		
		typedef HashData<K, V> HashData;
        //构造函数
		HashTable(size_t capacity = 10)
			: _ht(capacity), _size(0)
		{
			for (size_t i = 0; i < capacity; ++i)
				_ht[i]._state = EMPTY;
		}
	private:
		std::vector<HashData>  _ht;
		size_t  _size; //数据的数量
		
	};
哈希表的插入

首先我们要通过哈希函数计算键的哈希值。然后在使用计算出的哈希值确定键值对在哈希表中的位置,如果该位置的状态为EMPTY,, DELETE 说明该位置为空没有冲突,则直接将新键值对插入该位置。如果该位置状态为EXIST说明已经有其他键值对,我们向后寻找空位置插入并将插入位置状态设为EXIST。插入数据的数据越多,发生哈希冲突的概率越大,所以我们设定一个负载因子,当哈希表元素数量超过指定负载因子时,自动增加哈希表的大小,并将现有元素重新散列到新的哈希表中,以保证查询和插入操作的效率。代码如下:

bool insert(const std::pair<K,V>& kv)
{
	//检查容量
	if (_size * 10 / _ht.size() >= 7)
	{
	//扩容
	    size_t newcapacity = _ht.size() * 2;
	    std::vector<HashData> _newht(newcapacity);
	    for (int i = 0; i < _ht.size(); i++)
	    {
			size_t index = _ht[i]._kv.first % newcapacity;
			if (_newht[index]._state == EMPTY)
			{
				_newht[index] = _ht[i];
			}
			else
			{
				while (true)
				{
					index++;
					index %= newcapacity;
					if (_newht[index]._state == EMPTY)
					{
						_newht[index] = _ht[i];
						break;
					}
				}
			}
		}
	_ht, swap(_newht);
	}
    //插入
	size_t newindex = kv.first % _ht.size();
	if (_ht[newindex]._state == EMPTY || _ht[newindex]._state == DELETE)
	{
		_ht[newindex]._kv =kv;
		_ht[newindex]._state = EXIST;
	}
	else
	{
		while (true)
		{
			newindex++;
			newindex %= _ht.size();;
			if (_ht[newindex]._state == EMPTY|| _ht[newindex]._state == DELETE)
			{
				_ht[newindex]._kv = kv;
				_ht[newindex]._state = EXIST;
				break;
			}
		}
	}
			_size++;
			return true;
}
哈希表的查找

当你想要查找一个键时,首先使用哈希函数计算该键的哈希值。用于确定键在哈希表中的位置。由于哈希函数可能会产生冲突,因为我们使用线性探测法解决哈希冲突,所以如果发生冲突,我们用同样的方法查找,即向后探测。当我们向后探测到位置状态为EMPTY时我们停止探测,查找失败。

                                                                   

代码如下:

size_t find(const K& key)
{
	size_t index = key % _ht.size();
	while (_ht[index]._state != EMPTY)
	{
		if (_ht[index]._kv.first == key && _ht[index]._state == EXIST)
		{
			return index;
		}
		index++;
		index %= _ht.size();
	}
			
	return -1;
			
}
哈希表的删除

删除一个数的操作十分简单,先查找到这个数的位置,然后将该位置的状态设为DELETE即可。代码如下:

bool earse(const K& key)
{
	size_t index = find(key);
	if (index == -1)
	{
		return false;
	}
	_ht[index]._state = DELETE;
	_size--;
	return true;
}
哈希桶

上面我们用线性探测法解决了哈希冲突,但我们不难发现这种方法存在弊端,其核心思想是我的位置被占了,我就去占据其他人的位置。这会导致恶性循环,使哈希冲突发生的概率大大增加。哈希桶则为我们提供另外一个思路。在哈希桶中,每个桶都是一个独立的数据结构,通常是一个链表或其他类型的集合。当发生哈希冲突时,新的键值对会被插入到对应的桶中,形成一个链表或其他类型的集合。这样,即使多个键值对具有相同的哈希值,它们也可以被存储在同一个桶中,而不会相互覆盖。也就是将数组中存储的数据改为链表,哈希值相同的都会被储存在这个链表中。

 //哈希表中存储的结点
template<class K,class V>
struct HashData
{
	T _data;
	HashData* _next;
};

template<class K,class V>
class HashTable
{
	typedef HashData<K,V> Node;
    //构造函数
	HashTable()
		:_num(0)
	{
		_table.resize(10);
	}
	//析构函数
	~HashTable()
	{
		Clear();
	}
	//清理哈希表中的数据
	void Clear()
	{
		for (int i = -0; i < _table.size(); i++)
		{
			if (_table[i])
			{
				Node* cur = _table[i];
				Node* next = cur->_next;
				while (cur)
				{
					delete cur;
					cur = next;
					if (cur)
					{
						next = cur->_next;
					}
				}
				_table[i] = nullptr;
			}
		}
	}
private:
		std::vector<Node*> _table;
		size_t _num = 0;
	};
哈希表的插入

当插入一个新的键值对时,使用哈希函数计算键的哈希值,以确定要插入的键值对应该放在哪个哈希桶中。根据计算出的哈希值,找到对应的哈希桶。遍历哈希桶中的链表,检查是否已存在相同的键。如果链表中不存在相同的键,则在链表的末尾插入新的键值对节点。如果哈希表的负载因子过高,需要进行扩容操作,以减少哈希冲突并提高性能。

bool insert(const pair<K,V>& data)
{
	Hash hash;
	//检查负载因子
	if (_num*10 /_table.size() >= 7)
	{
		//扩容
		std::vector<Node*> newtable;
		size_t newsize = _table.size() * 2;
		newtable.resize(newsize);
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while(cur)
			{
				Node* next = cur->_next;
				size_t newindex = cur->_data->first % _table.size();
				cur->_next = newtable[newindex];
				newtable[newindex] = cur->_next;
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(newtable);
	}
	//算出映射值		
	size_t index = cur->_data->first % _table.size();
	Node* cur = _table[index];
	while (cur)
	{
		if (cur->_data = data)
		{
			return false;
		}
			cur = cur->_next;
	}
	//头插
	Node* newnode = new Node(data);
	newnode->_next = _table[index];
	_table[index] = newnode;
	//改变计数器
	_num++;
	return true;

}
 哈希表的查找

通过哈希函数将待查找的键值转换成对应的哈希地址。查找对应位置的链表,如果为空,那么查找失败。不为空则遍历链表,找到对应的数据。遍历后没有找到则查找失败。代码如下

bool find(const K& key)
{
	size_t index = key % _table.size();
	Node* cur = _table[index];
	while(cur)
	{
		if kofv(cur->_data.first == key)
		{
			return  true;
		}
		else
		{
			cur = cur->_next;
		}
	}
	return false;
}
哈希表的删除

与线性探测法相似,我们同样通过哈希函数计算出待删除键值对应的哈希地址。在该哈希桶对应的单链表上进行查找,找到待删除的节点。然后对结点进行删除。代码如下

bool earse(const K& key)
{
	size_t index = key % _table.size();
	Node* cur = _table[index];
	Node* prev = nullptr;
	while(cur)
	{
		if (cur->_data->first) == key && prev==nullptr)
		{
			_table[index] = cur->_next;
			delete cur;
		    --_num;
			return true;
		}
		else if (cur->_data->first) == key && prev != nullptr)
		{
      		prev->_next = cur->_next;
			delete cur;
			--_num;
			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
	    }
	}
	return false;
}
总结

总之,哈希表是一种高效的数据结构,通过键值对的方式存储数据,并通过哈希函数实现快速查找、插入和删除操作。虽然它有一些缺点,但通过一些优化策略可以克服这些问题,使得哈希表在实际应用中非常有用。

感谢观看,我是knight-n,我们下期见!

                                                                                                                                                                                                                                            

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

knight-n

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值