IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 模拟实现哈希 -> 正文阅读

[数据结构与算法]模拟实现哈希

1. 哈希概念

哈希也叫做散列,本质上是一种映射

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素

时,必须要经过关键码的多次比较顺序查找时间复杂度为O(N),平衡树中为树的高度,即

O(logn),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素

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

假设我们直接建立映射关系,主要会产生两个问题:

  1. 数据范围分布很广、不集中
  2. key的数据不是整数,是字符串怎么办?自定义类型又怎么办?

当向该结构中:

  • 插入元素

    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

  • 搜索元素

    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置

    取元素比较,若关键码相等,则搜索成功

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

对于分布广的整型数据(负数可以先转成size_t),哈希函数可以用除留余数法–(常用)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数

按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

image-20220910153426128

2. 哈希冲突

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

image-20220910153538586

3.哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列开散列

3.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有

空位置,那么可以把key存放到冲突位置中的下一个空位置中去。

那如何寻找下一个空位置呢?

  1. 线性探测

    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

    缺点:我占了你的,你占了他的,拥堵

    image-20220910154050716

  2. 二次探测

    image-20220910154132370

3.2 闭散列的删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素

image-20220910155122723

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

3.3 闭散列的实现

思考:哈希表什么情况下进行扩容?如何扩容?

image-20220910160631952

namespace CloseHash
{

	enum State
	{
		EMPTY,
		EXITS,
		DELETE
	};

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

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


    // 特化就不用传仿函数了
    // 不能取地址:两个对象值相同地址不同
	template<>
	struct DefaultHash<string>
	{
		size_t operator()(const string& key)
		{
			// BKDR:131能有效避免哈希冲突
			size_t hash = 0;
			for (auto ch : key)
			{
				hash = hash * 131 + ch;
			}

			return hash;
		}
	};


    // HashFunc将key存的数据转成size_t
	template<class K, class V, class HashFunc = DefaultHash<K>>
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		bool Insert(const pair<K, V>& kv)
		{
            // 非multi版本去冗余
			if (Find(kv.first))
			{
				return false;
			}

			// 负载因子到0.7及以上,就扩容
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				// 扩容以后,需要重新映射
				HashTable<K, V, HashFunc> newHT;
				newHT._tables.resize(newSize);
				// 遍历旧表,插入newHT
				for (auto& e : _tables)
				{
					if (e._state == EXITS)
					{
						newHT.Insert(e._kv);
					}
				}
				newHT._tables.swap(_tables);
			}

			HashFunc hf;
			size_t starti = hf(kv.first);
			starti %= _tables.size();

			size_t hashi = starti;
			size_t i = 1;
			// 线性探测/二次探测
			while (_tables[hashi]._state == EXITS)
			{
				hashi = starti + i;
				++i;
				hashi %= _tables.size();
			}

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

			return true;
		}

        
        // 返回值不能改引用,不然无法处理空的情况,没有空引用
		Data* Find(const K& key)
		{
            // 防止÷0错误
			if (_tables.size() == 0)
			{
				return nullptr;
			}

            // 有名对象调用仿函数
			HashFunc hf;
			size_t starti = hf(key);
			starti %= _tables.size();

			size_t hashi = starti;
			size_t i = 1;
			while (_tables[hashi]._state != EMPTY)
			{
                // 防止delete了也被查找到,因为是伪删除
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}

				hashi = starti + i;
				++i;
				hashi %= _tables.size();
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Data* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;			// 不--会导致负载因子变大提前扩容
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<Data> _tables;
		size_t _n = 0; // 存储关键字个数
	};


	void TestHT1()
	{
		int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
		//HashTable<int, int, DefaultHash<int>> ht;
		HashTable<int, int> ht;

		if (ht.Find(10))
		{
			cout << "找到了10" << endl;
		}

		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		// 测试扩容
		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(5, 5));
		ht.Insert(make_pair(15, 15));

		if (ht.Find(50))
		{
			cout << "找到了50" << endl;
		}

		if (ht.Find(10))
		{
			cout << "找到了10" << endl;
		}

		ht.Erase(10);
		ht.Erase(10);

		if (ht.Find(50))
		{
			cout << "找到了50" << endl;
		}

		if (ht.Find(10))
		{
			cout << "找到了10" << endl;
		}
	}

	void TestHT2()	// 测试统计次数
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

        // 测试转换后的值是否相同
		/*string s1("苹果");
		string s2("果苹");
		string s3("果果");
		string s4("萍果");

		string s5("abcd");
		string s6("bcad");
		string s7("aadd");

		StringHash hf;
		cout << hf(s1) << endl;
		cout << hf(s2) << endl;
		cout << hf(s3) << endl;
		cout << hf(s4) << endl << endl;
		cout << hf(s5) << endl;
		cout << hf(s6) << endl;
		cout << hf(s7) << endl;*/


		//HashTable<string, int, StringHash> countHT;
		HashTable<string, int> countHT;

		for (auto& str : arr)
		{
			auto ret = countHT.Find(str);
			if (ret)
			{
				ret->_kv.second++;
			}
			else
			{
				countHT.Insert(make_pair(str, 1));
			}
		}

		// 对应类型配一个仿函数,仿函数对象实现把key对象转换成映射的整数
		//HashTable<Date, int, DateHash> countHT;
		//HashTable<Student, int, StudentHash> countHT;


		HashTable<string, int> copy(countHT);
	}
}

4. 开散列(哈希桶/拉链法)

数据不存在表中,表里面存储一个链表指针,冲突的数据以链表形式挂起来。

4.1 开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中 。

image-20220910220642912

image-20220910220650585

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

4.2 开散列的实现

image-20220910221814640

如果我们像以前一样开一个新表,把旧表的节点重新插入新表,这种方法效率是很低下的。因为新表插入得重新newnode节点,重新计算每一个节点的映射位置,还得释放旧表的节点,这是不好的。

优化:

我们没有必要去创建新的节点,可以直接把旧表的节点拿过来,但是不能像以前一样去调用insert(势必会开新节点)。简单来说就是把拷贝节点优化为转移节点。

但是析构函数还是得留下。最后一次析构得用到。

// 把仿函数放在公共区域

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


// 特化就不用传仿函数了
// 不能取地址:两个对象值相同地址不同
template<>
struct DefaultHash<string>
{
	size_t operator()(const string& key)
	{
		// BKDR:131能有效避免哈希冲突
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;
		}

		return hash;
	}
};

namespace Bucket
{
	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 HashFunc = DefaultHash<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}

				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}

			HashFunc hf;

			// 负载因子 == 1 扩容
			if (_tables.size() == _n)
			{
				// 扩容,有缺陷,可以再优化,大家下去可以思考一下
				/*size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newHT;
				newHT._tables.resize(newSize, nullptr);

				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						newHT.Insert(cur->_kv);
						cur = cur->_next;
					}
				}

				newHT._tables.swap(_tables);*/

				size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}

				newTable.swap(_tables);
			}

			size_t hashi = hf(kv.first);
			hashi %= _tables.size();

			// 头插到对应的桶即可
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n;

			return true;
		}

		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}

			HashFunc hf;
			size_t hashi = hf(key);
			//size_t hashi = HashFunc()(key);

			hashi %= _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return false;
			}

			HashFunc hf;
			size_t hashi = hf(key);
			hashi %= _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;

			//size_t hashi = key;
			//hashi %= _tables.size();
			//Node* cur = _tables[hashi];
			//while (cur)
			//{
			//	if (cur->_kv.first == key)
			//	{
			//		if (cur->_next == nullptr)
			//		{
			//			cur->_kv = _tables[hashi]->_kv;
			//			Node* first = _tables[hashi];
			//			_tables[hashi] = first->_next;
			//			delete first;
			//		}
			//		else
			//		{
			//			//....
			//		}

			//		return true;
			//	}

			//	prev = cur;
			//	cur = cur->_next;
			//}

			//return false;
		}

	private:
		// 指针数组
		vector<Node*> _tables;
		size_t _n = 0;
	};

	void TestHT1()
	{
		int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
		//HashTable<int, int, DefaultHash<int>> ht;
		HashTable<int, int> ht;

		if (ht.Find(10))
		{
			cout << "找到了10" << endl;
		}

		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Erase(20);
		ht.Erase(10);
		ht.Erase(30);
		ht.Erase(50);


		// 测试扩容
		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(5, 5));
		ht.Insert(make_pair(15, 15));
		ht.Insert(make_pair(25, 15));
		ht.Insert(make_pair(35, 15));
		ht.Insert(make_pair(45, 15));
	}

	void TestHT2()
	{
		int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		// 需要自己实现拷贝构造,完成链表桶深拷贝
		//HashTable<int, int> copy(ht);
	}

	void TestHT3()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

		//HashTable<string, int, StringHash> countHT;
		HashTable<string, int> countHT;

		for (auto& str : arr)
		{
			auto ret = countHT.Find(str);
			if (ret)
			{
				ret->_kv.second++;
			}
			else
			{
				countHT.Insert(make_pair(str, 1));
			}
		}
	}
}

4.3 开散列的删除

除了我们上面写的单链表的删除方式,我们还有另外一种方式,有点类似二叉搜索树的替换法。缺陷是最后一个节点没法找人替换,我们可以把第一个节点的值给给它,然后头删。效率没有本质的提升,只是提供另一个思路。

image-20220911130056895

bool Erase(const K& key)
{
		if (_tables.size() == 0)
		{
				return false;
		}

		HashFunc hf;
		size_t hashi = hf(key);
		hashi %= _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
						prev->_next = cur->_next;
				}

				delete cur;
					
				return true;
			}

			prev = cur;
			cur = cur->_next;
		}

		return false;
}

	private:
		// 指针数组
		vector<Node*> _tables;
		size_t _n = 0;
	};

后一个节点没法找人替换,我们可以把第一个节点的值给给它,然后头删。效率没有本质的提升,只是提供另一个思路。

[外链图片转存中…(img-Is2BHC9n-1662947354925)]

bool Erase(const K& key)
{
		if (_tables.size() == 0)
		{
				return false;
		}

		HashFunc hf;
		size_t hashi = hf(key);
		hashi %= _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
						prev->_next = cur->_next;
				}

				delete cur;
					
				return true;
			}

			prev = cur;
			cur = cur->_next;
		}

		return false;
}

	private:
		// 指针数组
		vector<Node*> _tables;
		size_t _n = 0;
	};
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 11:43:26  更:2022-09-13 11:44:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 17:03:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码