哈希表详解

1.哈希表是一种描述映射关系的数据结构,由于元素与映射并不是顺序排列,所以也叫散列表

2.哈希函数是数据映射的规则,有以下特征:1.其对应的散列表必须存储全部数据,数据的范围必须在散列表的范围中2.分布应该是均匀分布3.简单

3.常用函数:

        1.取余函数key=key%mod(哈希表长):优点:不需知道key的分布,缺点:哈希冲突,减少冲突的方法:选择质数作为除数。原因:数具有较好的分布性质,能够更均匀地将输入数据映射到不同的哈希值上。相比于选择非素数的除数,选择素数可以降低哈希地址集中在某些特定值上的可能性,从而减少冲突的发生

        2.直接地址法:key=key*A+B:优点:简单均匀,缺点:需要知道数据的分布状况,不然容易越界,一般在查找小且连续的情况下

4.哈希冲突:当不同数根据哈希函数的计算值相同时,会被映射到一个地址里,这就是哈希冲突

5.哈希冲突的问题:1.当两个不同的键值被映射到同一个槽位时,其中一个键值的数据可能会被覆盖2.在发生冲突的槽位中,需要通过额外的操作来查找目标键值,这会增加查找的时间复杂度3.频繁的哈希冲突会导致哈希表的装载因子增加,使得哈希表的性能下降

6.解决方法:闭散列与开散列(线性探测和链式)

7.哈希表的两种模拟实现

数据类型

enum State
{
	EMPTY,//该位置为空
	EXIST,//该位置已经有元素
	DLETE//该位置元素已被删除
};
 
template<class K, class V>
struct HashData
{
	State _state = EMPTY;//标记当前位置的状态为空
	pair < K, V> _kv;
};

   存储变量

template<class K,class V>
class HashTable
{
protected:
	vector<HashData> _tables;
	size_t size = 0;//存储数据个数
};

构造函数

HashTable(size_t size=10)
{
	_tables.resize(size);
}

 查找

为了防止越界,要先对size取模

往后找,直到出现空缺或者越界,返回空指针

‘找到了则返回指向key的指针

HashData<K,V>* Find(const K&key)
{
	size_t hash = key % _tables.size();
	size_t pos = hash;
	size_t i = 1;
	while (_tables[pos]._state != EMPTY)
	{
		if (_tables[pos].state == EXIST && _tables[pos]._kv.first == key)
			return &_tables[pos];
		pos += i;
		if (pos >= _tables.size())
			return nullptr;
	}
	return nullptr;
}

插入

首先由于哈希表不存在相同的元素,必须先保证找不到元素

通过哈希函数获取位置

哈希冲突如果存在,则线性探测法往后找

bool Insert(pair<K,V>& kv)
{
	
	if (Find(kv.first))
		return false;
	size_t hash = key % _tables.size();
	size_t pos = hash;
	size_t i = 1;
	while (_tables[pos]._state == EXIST)
	{
		pos += i;
		if (pos >= _tables.size())
			return false;
	}
	_tables[pos]._kv = kv;
	_tables[pos]._states = EXIST;
	return true;
	_size++;
}

扩容:先引入一个概念:负载因子:有效数据个数/哈希表长度

要尽量把负载因子控制在0.7以下,否则容易出现哈希冲突和哈希践踏,只要小于0.7就扩容

if (_size * 10 / _tables.size() > 7)//扩容
	{
		vector<HashData> newtables(_tables.size() * 2);
		for (auto hashdata : _tables)
		{
			pair<K, V>kv = hashdata._kv.first;
			size_t hash = key % _tables.size();
			size_t pos = hash;
			size_t i = 1;
			while (newtables[pos]._state == EXIST)
			{
				pos += i;
			}
			_tables[pos]._kv = kv;
			_tables[pos]._states = EXIST;
		}
		_tables.swap(newtables);
	}

以上为扩容函数,判断时左右同乘10以避免边界情况,扩容为两倍(应该是接近2的素数),将哈希表的元素一一映射到新表中,最后交换新表和旧表
删除函数

先查找存在再删除,删除只是更换一下状态,如果物理删除会影响其他元素的线性探测,所以采用只改变元素性质的方式进行删除

	bool Erase(const K& key)
	{
		HashData<K, V>* ptr = Find(key);
		if (ptr)
		{
			(*ptr)._state = DELETE;
			--_size;
			return true;
		}
		return false;
	}

为了解决线性探测法容易造成堆积(即一个占了一个的位置,它又占了其他数的位置),一般采用链地址法

节点

template<class K,class V>
struct HashNode
{
	HashNode<K, V> _next;
	pair<K, v> _kv;
	HashNode(pair<K,V> kv=pair<K,V>())
		:_next(nullptr)
		,_kv(kv)
	{}
 
};

成员变量(由于string类等的取模操作,要写一个取模类)

template<class K, class V,class Hash=HashFunck<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
protected:
	vector<Node*> _tables;
	size_t _size = 0;
};

构造函数

HashTable(size_t size=10)
{
	_tables.resize(size);
}

析构函数

~HashTable()
{
	for (auto hash_node : tables)
	{
		while (hash_node)
		{
			Node* new_node = hash_node->_next;
			delete hash_node;
			hash_node = new_node;
		}
	}
}

查找

Node* Find(const K&key)
{
	size_t hash = Hash(key)%_tables.size();
	Node* cur = _tables[hash];
	while (cur)
	{
		if (cur->_kv.first == key)
			return cur;
		cur = cur->_next;
	}
	return nullptr;
}

插入(头插)

	bool Insert(pair<K, V>& kv)
	{
		if (Find(kv.first))
			return false;
		size_t hash = Hash(key) % _tables.size();
		Node*cur = _tables[hash];
				Node* p(kv);
				p->_next=cur;
				_tables[hash] = p;
				_size++;
				return true;
				
	}

为了防止链表过长影响效率,一般在负载因子为1时进行扩容

if (_size == _tables.size())
{
	vector<Node*> new_tables(_size*2);
	for (auto node : _tables)
	{
		while (node)
		{
			Node* next = node->_next;
			size_t hash = Hash(node->_kv.firsh) % new_tables.size();
			node->_next=new_tables[hash];
			new_tables[hash] = node;
			node = next;
		}
	}
	_tables.swap(new_tables);
}

删除(也要分类讨论)

bool Erase(const K& key)
{
	size_t hash = Hash(key) % _tables.size();
	Node* cur = _tables[hash];
	Node* pre = nullptr;
	while (cur)
	{
		if (cur->_kv.first == key)
			break;
		pre = cur;
		cur = cur->_next;
	}
	if (cur == nullptr)
		return false;
	if (pre == nullptr)
		_tables[hash] = cur->_next;
	else
		pre->_next = cur->_next;
	delete cur;
	return true;
}

由于字符串等无法比较,要将它们转化为整型进行比较

template<class K>
struct HashFunck
{
	size_t operator()(K s)
	{
		return s;
	}
};
template<>
struct HashFunck<string>
{
	size_t operator()(const string& s)
	{
		size_t number=0;
int multiply=31;
		for (auto ch : s)
			number = number * multiply + ch;//multiply可以取这些值131, 31 131 1313 13131 131313
			return number;
	}
};

可能链地址法要很多链指针会增大空间消耗,但是事实上线性探测必须要有大量的空间去保证搜索效率,负载因子更小就是特点之一,所以,反而链地址法更省空间

哈希与红黑树的比较

哈希:o(1)的查找和插入,空间利用率高,但是存在哈希冲突且不支持有序性

红黑树:支持有序性操作,平衡性可以保证极端情况下性能不退化,但是查找插入时间复杂度更高,且内存占用更大

所以哈希的使用情景:快速查找插入且不要求有序性,红黑树的使用情景:有序性操作并且对平衡性有要求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值