C++之闭散列哈希表

目录

unordered_set和unordered_map

哈希概念

哈希表基本结构

哈希冲突

线性探测​编辑

二次探测


前几期我们学习了红黑树和红黑树的模拟实现,最终使用红黑树封装了map和set。本期开始我们将学习下一个重要的知识点---哈希表,最终使用哈希表封装unordered_map和unordered_set。

unordered_set和unordered_map

在学习哈希之前,我们先了解一下unordered_set和unordered_map,这两个容器也都是关联式容器,我们之前已经学习了map和set。unordered_set和unordered_map与set和map大体上是一样的,但是也有几点不同。

**1.**map和set的底层是用红黑树进行封装实现的,也正因为如此,红黑树是一个搜索二叉树,搜索二叉树的元素我们是按照中序遍历的方法进行遍历的。所以map和set的元素的遍历是有序的,而unordered_set和undered_map的元素的遍历是无序的。

**2.**map和set是双向迭代器,而unordered_set和unordered_map是单项迭代器,通过C++官方文档就可以看出来。

**3.**map和set查找一个元素时,因为底层是红黑树,所以最多查找高度次,所以查找一个元素的时间复杂度为O(logN),而unordered_set和unordered_map底层是哈希,所以查找一个元素的时间复杂度为O(1),为什么是O(1),下面会为大家阐述。

哈希概念

有N个数的集合,这N个数的范围为[1,26],N个数中有重复的数,现在要统计这N个数中,每个数出现的次数,我们该如何进行统计呢?

有一种方法,就是开辟一个大小为27的一维数组,让这N个数的每个数都%27,然后将取模之后得到的数作为数组的下标,对应数组元素的值为这一数字出现的次数,通过这一方法就可以统计得到N个数每个元素出现的次数。这其实就是哈希的一种体现。
哈希表:通过哈希函数,使得关键码和元素位置建立一一映射的表,我们称之为哈希表。

哈希表基本结构

cpp 复制代码
namespace CloseHash
{
	enum Status
	{
		EMPTY,
		DELETE,
		EXIST
	};
	template<class K, class V>
	struct HashData
	{

	private:
		pair<K, V> _kv;
		Status  _st = EMPTY;
	};


	template<class K,class V>
	class HashTable
	{
	private:
		vector<HashData<K, V>> _v;
		int _n;  //哈希表中有效元素的个数
	};

}

哈希冲突

何为哈希冲突呢?前提为我们要用一个一维数组存储数据。

有1组数为1,7,2,8,映射在大小为7的数组中,1映射到下标为1的位置,7也映射到1的位置,但是数组的一个元素中只能存储一个数据,那么1先存入之后,7要存入时这就产生了冲突,我们就称这为哈希冲突

简单来说,哈希冲突就是不想同的元素,通过哈希映射,映射到了哈希表的同一位置。

如何解决哈希冲突呢?我们使用了闭散列的方法,即为线性探测和二次探测法。什么是线性探测?什么又是二次探测法呢?

线性探测

线性探测就是,当第二个元素与第一个元素产生哈希冲突时,从产生冲突的位置开始,依次往后继续寻找空的位置,寻找到空的位置之后,将产生冲突的元素放在空的位置。代码实现如下。

代码如下。

cpp 复制代码
	//哈希表中元素的插入
		bool insert( const pair<K,V>& kv)
		{
			//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
			HashData<K, V>* ret = find(kv.first);
			if (ret != nullptr)
			{
				return false;
			}
			if (_v.size() == 0 || _n*10 / _v.size() > 7)
			{
				int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
				HashTable<K, V> ht;
				ht._v.resize(NewSize);
				for (int i = 0; i< _v.size(); i++)
				{
					if (_v[i]._st == EXIST)
					{
						ht.insert(_v[i]._kv);
					}
				}
				_v.swap(ht._v);

			}
			HashFunc<K> hf;
			int  start = hf(kv.first) % _v.size();
			int i = 0;
			size_t index = start;
			while (_v[start]._st == EXIST)
			{
				++i;
				start = index + i;
				start% _v.size();
			}

			_v[start]._kv = kv;
			_v[start]._st = EXIST;
			++_n;
			return true;

		}

大家来想一个问题?当哈希表中的元素越来越多时,产生哈希冲突的概率必然也是越来越大的,怎么样去避免这类现象出现呢?

唯一的办法就是不断的去扩容,所以我们引入了平衡因子的概念,就是哈希表中的有效元素个数除以哈希表的容量,注意,这里的容量是size(),即vector中申请并初始化之后的元素的数目。

二次探测

何为二次探测,并不是探测两次,而是2次方。图示如下。

二次探测和线性探测类似。线性探测有一个缺点,就是当相同位置的元素过多时,就会侵占当前位置附近的其他位置,这就导致,本来处在附近位置的元素又去侵占其它元素的位置,这样会降低整个哈希表的查找效率,所以我们引入了二次探测,即插入的元素引起哈希冲突时,并不会在后续的第一个空位置插入冲突的元素,而是会去寻找当前位置+i*i的位置,如果当前位置为空,则插入元素。

代码如下。

cpp 复制代码
bool insert( const pair<K,V>& kv)
		{
			//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
			HashData<K, V>* ret = find(kv.first);
			if (ret != nullptr)
			{
				return false;
			}
			if (_v.size() == 0 || _n*10 / _v.size() > 7)
			{
				int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
				HashTable<K, V> ht;
				ht._v.resize(NewSize);
				for (int i = 0; i< _v.size(); i++)
				{
					if (_v[i]._st == EXIST)
					{
						ht.insert(_v[i]._kv);
					}
				}
				_v.swap(ht._v);

			}
			HashFunc<K> hf;
			int  start = hf(kv.first) % _v.size();
			int i = 0;
			size_t index = start;
			while (_v[start]._st == EXIST)
			{
				++i;
				//start = index + i;
				start = index + i * i;
				start% _v.size();
			}

			_v[start]._kv = kv;
			_v[start]._st = EXIST;
			++_n;
			return true;

		}

闭散列哈希表整体代码如下。

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include <string>
using namespace std;

namespace CloseHash
{
	enum Status
	{
		EMPTY,
		DELETE,
		EXIST
	};

	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};

	template<>
	struct HashFunc <string>
	{
		size_t operator()(const string& string)
		{
			size_t value = 0;
			for (auto e : string)
			{
				value *= 31;
				value += e;
			}

			return value;
		}

	};
	template<class K, class V>
	struct HashData
	{


		pair<K, V> _kv;
		Status  _st = EMPTY;
	};


	template<class K,class V,class hashFunc=HashFunc<K>>
	class HashTable
	{
	public:
		bool  erase(const K& key)
		{
			HashData<K, V>* ret = find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_st = DELETE;
				_n--;
				return true;
			}
		}

		HashData<K,V>* find(const K& key)
		{
			if (_v.size() == 0)
			{
				return nullptr;
			}
			HashFunc<K> hf;
			size_t start = hf(key) % _v.size();
			int i = 0;
			size_t index = start;
			
			while (_v[start]._st != EMPTY)
			{
				if (_v[start]._kv.first == key && _v[start]._st == EXIST)
				{
					return &_v[start];
				}
				++i;
				start = index + i;
			}
			return nullptr;
		}

	

		//哈希表中元素的插入
		bool insert( const pair<K,V>& kv)
		{
			//先去判断哈希表中是否存在当前要插入的元素,因为哈希表的原理是不排序加去重
			HashData<K, V>* ret = find(kv.first);
			if (ret != nullptr)
			{
				return false;
			}
			if (_v.size() == 0 || _n*10 / _v.size() > 7)
			{
				int NewSize = _v.size() == 0 ? 10 : _v.size() * 2;
				HashTable<K, V> ht;
				ht._v.resize(NewSize);
				for (int i = 0; i< _v.size(); i++)
				{
					if (_v[i]._st == EXIST)
					{
						ht.insert(_v[i]._kv);
					}
				}
				_v.swap(ht._v);

			}
			HashFunc<K> hf;
			int  start = hf(kv.first) % _v.size();
			int i = 0;
			size_t index = start;
			while (_v[start]._st == EXIST)
			{
				++i;
				//start = index + i;
				start = index + i * i;
				start% _v.size();
			}

			_v[start]._kv = kv;
			_v[start]._st = EXIST;
			++_n;
			return true;

		}



	private:
		vector<HashData<K,V>> _v;
		int _n;  //哈希表中有效元素的个数
	};


	void  HashTest()
	{
		HashTable<int, int> ht;
		ht.insert(make_pair(1, 1));
		ht.insert(make_pair(2, 1));
		ht.insert(make_pair(3, 1));
		ht.insert(make_pair(4, 1));

		cout << ht.find(1) << endl;
		cout << ht.find(5) << endl;

	}



}

调试代码,发现运行结果符合预期。

以上便是哈希表中闭散列哈希表的所有内容,下期将为大家带来开散列哈希表的内容。

本期内容到此结束^_^

相关推荐
17´6 小时前
使用QT+OpenCV+C++完成一个简单的图像处理工具
c++·图像处理·qt·opencv
苹果6 小时前
C++二十三种设计模式之迭代器模式
c++·设计模式·迭代器模式
飞yu流星7 小时前
C++ 函数 模板
开发语言·c++·算法
Goldinger8 小时前
vscode 配置c/c++环境 中文乱码
c语言·c++·vscode
nSponge8 小时前
【Duilib】 List控件支持多选和获取选择的多条数据
c++·windows·工具
Y Shy9 小时前
Windows C++开发环境:VSCode + cmake + ninja + msvc (cl.exe) + msys2/bash shell
c++·windows·vscode·msvc·cmake·msys2·ninja
越甲八千9 小时前
详细全面讲解C++中重载、隐藏、覆盖的区别
开发语言·c++
智驾11 小时前
SOLID原则学习,单一职责原则(Single Responsibility Principle)
c++·单一职责原则·solid
daopuyun11 小时前
C/C++编程安全标准GJB-8114解读——名称、符号与变量使用类
java·c语言·c++