【C++/STL】:哈希 -- 线性探测&哈希桶


点击跳转至文章: 【C++/STL】:set和map的介绍及基本使用

💡前言

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次。

在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,unordered系列的底层是哈希表

本篇文章的内容是学习unordered系列的容器,重点学习什么是哈希,知道什么是哈希冲突,并且掌握解决哈希冲突的两种常用方法:线性探测&哈希桶。

一,unordered系列容器

unordered系列容器有4个:
(1) unordered_map
(2) unordered_set
(3) unordered_multimap
(4) unordered_multiset

它们与 map/set 系列容器的核心功能的重叠度90%,使用方法基本类似,这里不再重复演示。最主要的区别是:遍历unordered系列容器中存储的数据是无序的,并且就性能而言,unordered系列容器在插入,查找,删除方面时间复杂度是 O(1)因此在平时,我们更加推荐使用unordered系列容器

二,哈希

2.1 哈希的概念

构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与元素之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

2.2 哈希函数

这里介绍两种常见的哈希函数:直接定址法除留余数法

(1) 直接定址法:用 key 值直接在哈希表中映射一个绝对位置或相对位置
优点:快,无冲突
缺点:要事先知道 key 值的分步情况,并且适用于范围集中的数据

(2) 除留余数法:用 key 值模(%)表的大小 N,利用得到的余数(0 ~ N-1)把 key 值映射到表中的位置

这种方法适用任何类型的数据(一些非整形类型可以通过某种方法转换),但是这也会导致一个不可避免的问题:产生哈希冲突

2.3 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

如果用上面的除留余数法来通俗的解释,就是不同的 key 值,取模后映射到了相同的位置

三,哈希冲突解决(重点)

解决哈希冲突两种常见的方法是:开放地址法哈希桶

3.1 开放定址法

当发生哈希冲突时(自己的位置被占了),如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 值存放到冲突位置中的"下一个" 空位置中去

那如何寻找下一个空位置呢?
(1) 线性探测:从发生冲突的位置开始,按 +1,+2,+3……依次向后探测,直到寻找到下一个空位置为止

比如下表中现在需要插入元素44,先通过哈希函数计算出映射位置为4,但是4位置被占,所以要从这个位置开始,依次向后探测,找到8空位置。

在这里插入图片描述

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同 key 值占据了可利用的空位置,使得寻找某 key 值的位置需要许多次比较,导致搜索效率降低。如何缓解呢?

(2) 二次探测:从发生冲突的位置开始,按 +1 ^ 2 , + 2 ^ 2,+3 ^ 2……依次向后探测,直到寻找到下一个空位置为止。

3.2 哈希桶(重点)

哈希桶也叫拉链法,首先对 key 值用哈希函数计算出位置,具有相同位置的 key 值归于同一集合,每一个集合称为一个桶各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

unordered系列的底层哈希表解决哈希冲突的结构就是哈希桶

从下图可以看出,哈希表中每个桶中放的都是发生哈希冲突的元素

在这里插入图片描述

四,线性探测的实现

4.1 线性探测的基本框架

哈希表中的每一个位置给一个状态标记。这样可以避免值存在但是由于冲突位置删除后置空导致查找不到的问题

//枚举三种状态
enum State
{
	EMPTY,  //空
	EXIST,  //存在元素
	DELETE  //元素删除
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
	_tables.resize(10); //初始化10个空间
}

	//插入,查找,删除等功能……

private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0;  //表中的数据个数
};

4.2 插入操作

(1) 计算出映射下标,这里用除留余数法要除以数据的个数size,而不是容量的大小capacity,因为要用下标访问,如果除以容量用下标访问会越界。
(2) 插入时发生哈希冲突,用线性探测往后找空位置。
(3) 扩容负载因子:表中的元素个数 / 表的长度 ,一般控制在0.7~0.8
(4) 去重操作,插入时可以复用Find进行判断,值存在,插入失败。

bool Insert(const pair<K, V>& kv)
{
	//去冗余,值存在,插入失败
	if (Find(kv.first))
		return false;

	// 扩容
	//负载因子:表中的元素个数 / 表的长度 一般控制在0.7~0.8
	if (_n * 10 / _tables.size() >= 7)
	{
		HashTable<K, V, Hash> newHT;

		//不要在原表扩容,会破坏映射关系
		//_tables.resize(_tables.size() * 2); //err

		//重开一个新的哈希表
		newHT._tables.resize(_tables.size() * 2);

		//遍历旧表,复用Insert,(可避免重复写映射下标的逻辑)
		// 把旧表中存在的数据插入新表
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
				newHT.Insert(_tables[i]._kv);
		}

		//交换
		_tables.swap(newHT._tables);
	}

	//计算出映射下标
	//这里用除留余数法。要除以数据的个数size,而不是容量的大小capacity
	//因为要用下标访问,如果除以容量用下标访问会越界
	Hash hs;
	size_t hashi = hs(kv.first) % _tables.size();

	//哈希冲突,线性探测继续找下一个位置
	while (_tables[hashi]._state == EXIST)
	{
		hashi++;
		hashi %= _tables.size(); //解决回绕
	}

	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	_n++;

	return true;
}

4.3 查找操作

算出映射下标确定查找起点,再继续往后找,值相等且为存在状态就找到了,避免删除后还能找到

HashData<K, V>* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size();

	while (_tables[hashi]._state != EMPTY)
	{
		//值相等且为存在状态,避免删除后还能找到
		if (_tables[hashi]._kv.first == key
			&& _tables[hashi]._state == EXIST)
		{
			return &_tables[hashi];
		}

		hashi++;
		hashi %= _tables.size(); //解决回绕
	}

	return nullptr;
}

4.4 删除操作

复用Find,值存在就删除

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret == nullptr)
		return false;
	else
	{
		ret->_state = DELETE;
		_n--;
		return true;
	}
}

五,哈希桶的实现(重点)

5.1 哈希桶的基本框架

哈希桶的结构通俗的说就是在顺序表中挂一个个链表,所以这里的顺序表是一个指针数组

在这里插入图片描述

template <class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode<K, V>* _next;

	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};

template <class K, class V, class Hash = HashFunc<K>>
class HashBucket
{
	typedef HashNode<K, V> Node;
public:
	HashBucket()
	{
		_tables.resize(10, nullptr); //初始化10个空间
	}

	//插入,删除,查找等功能……

private:
	vector<Node*> _tables;  // 指针数组
	size_t _n = 0;          // 表中储存的数据个数
};

5.2 插入操作

(1) 核心操作就是链表的头插

(2) 扩容:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

在这里插入图片描述

bool Insert(const pair<K, V>& kv)
{
	//去冗余,值存在,插入失败
	if (Find(kv.first))
		return false;
		
	//负载因子==1,扩容

	//方式1
	//if(_n == _tables.size())
	//{
	//	HashBucket<K, V> newHB;
	//	newHB._tables.resize(_tables.size() * 2);

	//	//遍历旧表,复用Insert,(可避免重复写映射下标的逻辑)
	//	// 把旧表中存在的数据插入新表
	//	for (size_t i = 0; i < _tables.size(); i++)
	//	{
	//		Node* cur = _tables[i];
	//		if (cur)
	//		{
	//			newHB.Insert(cur->_kv);
	//			cur = cur->_next;
	//		}
	//	}
	//	_tables.swap(newHB._tables);
	//}

	
	//方式1的扩容逻辑消耗的空间太多,假设有一万个节点,就要重新new一万个
	//又要销毁一万个,效率不高
	
	//方式2
	// 是在扩容后也重新利用旧表中的旧节点,把旧节点挪动到新表中
	Hash hs;
	if (_n == _tables.size())
	{
		vector<Node*> newtables(_tables.size() * 2, nullptr);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;

				//旧节点挪到新表的下标映射
				size_t hashi = hs(cur->_kv.first) % newtables.size();
				
				//头插到新表
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;

				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}

	size_t hashi = hs(kv.first) % _tables.size(); //计算映射下标

	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	_n++;

	return true;
}

5.3 查找操作

核心操作就是链表的查找

Node* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size(); //计算映射下标
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

5.4 删除操作

核心操作就是链表的删除
在这里插入图片描述

bool Erase(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size(); //计算映射下标
	Node* cur = _tables[hashi];
	Node* prev = nullptr;

	while (cur)
	{
		if (cur->_kv.first == key)
		{
			//第一个节点
			if (prev == nullptr)
				_tables[hashi] = cur->_next;
			else
				//中间节点
				prev->_next = cur->_next;

			delete cur;
			_n--;
			return true;
		}
		//没找到,继续向下走
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

六,优化思考

只能存储key为整形的元素,其他类型怎么解决?

需要支持转整形的仿函数:
(1) 当key为浮点数,负数,指针等可以直接强转为无符号整形的类型
(2) 当key为 string 类(频繁使用)

//当key为浮点数,负数,指针等时,强转成无符号整形,再进行映射
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//用原模板对 string 进行特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t n = 0;

		for (auto& ch : key)
		{
			n *= 31; //尽量减少冲突
			n += ch;
		}
		return n;
	}
};

要在C++中实现哈希表,可以通过以下步骤进行: 1. 定义一个哈希函数,将键(key)映射到哈希表中的索引。哈希函数应该是高效的,尽可能地减少哈希冲突。 2. 定义一个数组,用于存储哈希表中的元素(键值对)。 3. 在数组中插入一个元素时,先使用哈希函数计算出该元素的索引,然后将元素插入到该索引处。如果该索引处已经有元素,则需要解决哈希冲突,例如使用开放寻址法或链表法。 4. 在数组中查找一个元素时,同样使用哈希函数计算出该元素的索引,然后在该索引处查找元素。如果该索引处没有元素,则说明该元素不在哈希表中。 以下是一个简单的示例代码,演示了如何实现一个基本的哈希表,其中使用了开放寻址法来解决哈希冲突: ```c++ #include <iostream> const int TABLE_SIZE = 128; class HashTable { private: struct Node { int key; int value; bool is_deleted; // 标记是否被删除 }; Node table[TABLE_SIZE]; // 哈希函数 int hash(int key) const { return key % TABLE_SIZE; } public: HashTable() { for (int i = 0; i < TABLE_SIZE; ++i) { table[i].key = -1; table[i].value = -1; table[i].is_deleted = false; } } // 插入元素 void insert(int key, int value) { int index = hash(key); while (table[index].key != -1 && table[index].is_deleted == false) { // 线性探测 index = (index + 1) % TABLE_SIZE; } table[index].key = key; table[index].value = value; table[index].is_deleted = false; } // 查找元素 int find(int key) const { int index = hash(key); while (table[index].key != -1) { if (table[index].key == key && table[index].is_deleted == false) { return table[index].value; } index = (index + 1) % TABLE_SIZE; } return -1; } // 删除元素 void remove(int key) { int index = hash(key); while (table[index].key != -1) { if (table[index].key == key && table[index].is_deleted == false) { table[index].is_deleted = true; return; } index = (index + 1) % TABLE_SIZE; } } }; int main() { HashTable ht; ht.insert(1, 10); ht.insert(2, 20); ht.insert(3, 30); std::cout << ht.find(2) << std::endl; // 输出 20 ht.remove(2); std::cout << ht.find(2) << std::endl; // 输出 -1 return 0; } ```
评论 84
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值