C++进阶:哈希结构

本文详细介绍了哈希结构及其优势,强调了哈希冲突的问题及解决方法,包括线性探测和二次探测。同时,讨论了闭散列与开散列两种策略,其中闭散列涉及线性探测和二次探测的实现,开散列则探讨了使用链地址法(哈希桶)和动态扩容的可能性。此外,还提到了哈希函数的设计以及哈希表的负载因子对性能的影响。
摘要由CSDN通过智能技术生成
哈希相关概念

哈希:用来进行高效查找的一种数据结构
首先,我们查找的方式有:
(1)顺序查找,它的时间复杂度是O(n)
(2)二分查找(有序),它的时间复杂度是O(log2N)
(3)利用二叉平衡搜索树(AVL、红黑树):时间复杂度是O(log2N)
(4)哈希,时间复杂度是O(1)
前三种方法元素之间都要进行比较,因此时间复杂度降不下来,而哈希元素之间不需要比较(最差情况下只需少许比较即可找到),因此时间复杂度小。
那么哈希的原理就是通过某种方式,将元素与其在空间中的位置建立一一对应的关系,例如:
给一个容量为capacity的空间,按照func(x)=x%capacity的方法来存储元素,比如存储21、67、90、33、5,func(21)=21%10=1,也就是21放在1号位置;func(67)=67%10=7,放在7号位置,func(90)=0,放在0号位置,func(33)=3,放在3号位置,func(5)=5,放在5号位置,这样就把元素放好了,也就是一个表格;
然后进行查找,第一步就是通过func(x)找元素在表格中的存储位置,然后验证是否为所找的元素即可;
这种思想就是哈希的思想,也可以称作为散列,func(x)称为哈希函数,建立的表格就是哈希表。
但是这种方法有一个缺陷,例如向这个哈希表中放入一个11,func(11)=1,应该将11放在1号位置,而这时1号位置已经放了一个元素,这时如果再向里面放元素就发生了覆盖(冲突),也就是不同的元素计算出相同的哈希地址,这种情况称为哈希冲突
哈希冲突的解决方式
(1)哈希函数可能会导致哈希冲突:可以重新设计哈希函数:
注意:哈希地址必须在哈希表格的范围内,产生的哈希地址尽可能的均匀分布,哈希函数尽可能简单;但是一个哈希函数无论设计的多精妙,都无法完全解决哈希冲突,只能将发生哈希冲突的概率降低。
(2)存放的元素
常见的哈希函数:
(1)直接定值法:Hash(key)=A*key+B,它的优点是简单和均匀,缺点是事先要知道关键字的分布,适用于查找比较小连续的情况,例如在字符串中找第一个只出现一次的字符

class Solution {
public:
    int firstUniqChar(string s) {
        int count[256]={0};//一个字符有256种状态
        //统计每个字符出现的次数
        for(int i=0;i<s.size();++i)
        {
            count[s[i]]++;
        }
        for(int i=0;i<s.size();++i)
        {
            if(1==count[s[i]])
                return i;
        }
        return -1;
    }
};

(2)除留余数法,也就是上述我们所说的func(x)=x%capacity的方法,也就是Hash(key)=key%p(如果p是素数,出现哈希冲突的概率较低)
(3)平方取中法(了解)例如关键字是1234,对他取平方得到1522756,取227作为他的关键字
(4)折叠法(5)随机数法(6)数学分析法

解决哈希冲突的方法
  • 闭散列
    也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,那么如何找到下一个空位置呢?
    1、线性探测
    就是从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    那么怎么区分要插入的位置是否存在元素或者为空?
    我们可以给个标记:EMPTY表示没有元素,EXIST表示有元素
    如图:
    在这里插入图片描述
    (1)插入:
    通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素,如上图插入44,44%10=4,但是4这个位置已经放了值为4的元素,因此要向后找下一个空位置插入
    (2)查找:通过哈希函数计算元素在哈希表中的位置,检测该位置是否有元素,然后检测该元素是否为要查找元素
    (3)删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,例如上图如果将4删除,将标记改为EMPTY,会影响44的搜索,导致44找不到(44的哈希函数计算出来应该在4位置,但是此时4位置标记没有元素,所以会影响)因此不能这样进行删除,因此线性探测采用标记的伪删除法来删除一个元素,也就是使用DELETE标记这个位置删除了元素
    当删除一个元素,理论上这个位置就可以插入元素了,但是有特殊情况,例如上图,将5删除,此时如果要插入44,按理来说可以插到5这个位置,但是此时哈希表中已经有44这个元素,就会冲突(如果哈希表要求元素唯一性)
    (4)判断是否增容?
    那么在插入操作时,什么时候增容,怎么增容,这里涉及到散列表的负载因子(填入表中的个数/散列表的长度),负载因子越大,表示填入表中的元素越多,产生冲突的可能性就越大;负载因子越小,填入表中的元素越少,产生冲突的可能性就越小;
    对于线性探测,负载因子一般控制在0.7到0.8之间,这里我们实现增容将负载因子控制在0.7,在进行扩容时,我们不能使用传统的方法进行扩容(传统的是将容量扩大,然后原封不动地将元素拷贝过来),因为在容量发生改变后,哈希函数也会发生改变,这样在扩容之后,就找不到元素了,因此我们必须再创建一个哈希表,然后设置它的容量是原来哈希表的容量的扩大版(我们这里是扩大2倍),然后将原来哈希表中存在的元素(即状态是EXIST的元素)插入到新的哈希表中,然后将2个哈希表交换即可(这里实现交换函数先交换内容,再交换有效元素个数)
    最终的代码是:
#pragma once
#include <iostream>
using namespace std;
#include <vector>
//假设哈希表格中的元素是唯一的
enum State
{
	EMPTY, EXIST, DELETE
};
template<class T>
struct Elem
{
	T _value;//元素值域
	State _state;//状态
};
template<class T>
class HashTable
{
public:
	HashTable(size_t capacity = 10)
		:_ht(capacity)
		, _size(0)
	{
		for (auto& e : _ht)
		{
			e._state = EMPTY;//将表格的初始状态初始化为空
		}
	}
	bool Insert(const T& val)
	{
		//检测是否需要扩容
		CheckCapacity();

		//通过哈希函数计算元素在哈希表中的存储位置
		size_t HashAddr = HashFunc(val);
		//检测该位置是否可以插入元素
		//发生哈希冲突,使用线性探测来解决
		while (_ht[HashAddr]._state != EMPTY)
		{
			if (EXIST == _ht[HashAddr]._state && val == _ht[HashAddr]._value)
			{
				//就不用插入了,冲突
				return false;
			}
			//使用线性探测继续往后找,直到找到空位
			++HashAddr;
			if (HashAddr == _ht.capacity())
				HashAddr = 0;//如果找到最后一个还没有找到空位,从头开始
		}
		//肯定不会让哈希表中的元素放的太多,因为发生冲突的概率会提高,这样哈希表查找的效率就会降低
		//所以不用考虑造成死循环的情况
		//找到空的位置,进行插入
		_ht[HashAddr]._value = val;
		_ht[HashAddr]._state = EXIST;
		++_size;
		return true;
	}
	int Find(const T& val)
	{
		size_t HashAddr = HashFunc(val);//计算哈希地址
		while (_ht[HashAddr]._state != EMPTY)//这个位置可能有元素
		{
			if (_ht[HashAddr]._state == EXIST && _ht[HashAddr]._value == val)
			{
				return HashAddr;
			}
			//如果这个位置是删除或者不等于要找的值,就哈希冲突,线性探测
			HashAddr++;
			if (HashAddr == _ht.capacity())
				HashAddr = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值