【C++】模拟实现哈希(闭散列和开散列两种方式)

news2024/10/7 18:21:16

哈希

  • 前言
  • 正式开始
    • map、set 与 unordered_map、unordered_set 的不同
      • 遍历结果不同
      • 查找速度不同
    • 哈希
      • 闭散列
        • 概念介绍
        • 模拟实现
        • 字符串等自定义类型找位置
        • 字符串哈希算法
        • 二次探测
      • 开散列
        • 概念介绍
        • 模拟实现
        • 存储自定义类型
        • 哈希表大小设置为素数

在这里插入图片描述

前言

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,例如map和set。即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不是很理想。

最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器(unordered_map、unordered_set、unordered_multimap和unordered_multiset),这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本篇中只对unordered_map和unordered_set进行介绍,并对其底层进行模拟实现,下一篇用本篇的模拟实现来封装unordered_map和unordered_set。

正式开始

STL中给的unordered_map和unordered_set用法可以说和map和set一样。我就不过多介绍了,如果对于map和set的使用不了解的同学,可以看看这篇:【C++】STL map和set用法基本介绍。

map、set 与 unordered_map、unordered_set 的不同

我就直接用set来进行对比了。

遍历结果不同

在这里插入图片描述

查找速度不同

下面代码为比较二者增删查的速度。

void test_op()
{
	int n = 10000;
	vector<int> v;
	v.reserve(n); // 先开到n,等会将产生的数据放到v中

	srand(time(0));
	for (int i = 0; i < n; ++i)
	{
		v.push_back(rand() + i);  // 重复少
		//v.push_back(rand());  // 重复多
	}

	// set的插入时间
	size_t begin1 = clock();
	set<int> s;
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();

	// unordered_set插入的时间
	size_t begin2 = clock();
	unordered_set<int> us;
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();

	cout << "总共插入的数据个数:" << s.size() << endl << endl;

	cout << "set insert:" << end1 - begin1 << endl;
	cout << "unordered_set insert:" << end2 - begin2 << endl << endl;

	// set的查找时间
	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();

	// unordered_set查找的时间
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
	cout << "set find:" << end3 - begin3 << endl;
	cout << "unordered_set find:" << end4 - begin4 << endl << endl;

	// set的删除时间
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();

	// unordered_set删除的时间
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();

	cout << "set erase:" << end5 - begin5 << endl;
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}

不断改变n的大小来改变插入元素的个数。下面的测试都是在debug版本下的:

n == 10000
在这里插入图片描述

因为随机数可能会产生重复的数,所以去重后的数据就会少一点。

n == 100000
在这里插入图片描述

n == 1000000
在这里插入图片描述

n == 10000000
在这里插入图片描述

可以看到,哈希还是很快的。

开始讲底层。

哈希

不知各位接触过计数排序没有。计数排序中就用到了哈希的思想。通过下标直接查找到某个数据。

这里给出1、3、0、12、5、14、2、4、7、6、9、8、11、10、15、13这个数,通过顺序表存储,我想要在O(1)的时间复杂度下查找任意一个数,各位想想有什么好办法?

可以直接通过下标产生对应的映射,什么意思呢?
就是下标0位置存放的数可以直接存储0,下标1位置存放的数可以直接存储1,下标2存储2,下标3存储3,下标4存储4,下标5存储5……一直到下标15。

那么假如顺序表名称为v,我们用v[5]就可以直接找到5,用v[13]就可以直接找到13……等等。找的时候根本不需要像红黑树那样不断对比才能找到,这样就是O(1)的时间复杂度。

那么哈希(也可叫散列)就是这样的思想,让每一个数据与其位置建立映射关系,通过位置直接查找出来所需的数据,这样查找起来就会非常的快。

但是有一个问题,就是当上面数据分布不是那么均匀(大的大,小的小)时该怎么办呢?

比如:3、7、19、25、36、300、7000
我们如果光对这7个数开7000个int,怕是有点不值得。得改一改思路。

我们可以将数进行取模运算,得到的余数即放到对应的顺序表中,比如这里把顺序表开到10个int。上面各个数对10进行取模就分别得到了3、7、9、5、6、0、0。

这样取模的方式就叫做除留余数法,是一种哈希函数(哈希函数传一个值能够产生对应的下标,哈希函数不止这里的除留余数法,还有其他的,不过除留余数法更常用,对于其他方法感兴趣的同学可以自己查查)。

那么存储下来就是这样:
在这里插入图片描述
虽然说解决了,但是还出现了一个问题,就是300和7000占用了同一个位置。

这个问题就叫做哈希冲突,也叫哈希碰撞。
有两个方法可以解决这个问题。分别为 闭散列 和 开散列。

闭散列 ===》 开放地址法。
开散列 ===》 拉链法/哈希桶。

那么我来挨个介绍,并进行模拟实现。

闭散列

概念介绍

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

就拿上面的例子来说,本来是这样的:
在这里插入图片描述

用闭散列的方式就是这样的:
在这里插入图片描述
将7000放到算出来的0下标的下一个位置1下标处。

如果再来一个20的话,余数还是0,此时还是继续往后寻找空位置,也就是2:
在这里插入图片描述

那么再来一个40呢?
余数还是0,被占用了,1、2、3也被占了,那就放到4处:
在这里插入图片描述

这里光一个0下标位置就几乎占了一半的下标了。

当空间快满的时候就需要进行扩容,这里可以搞一个负载因子来决定扩容时机。
负载因子 = 实际存储数据个数 / 当前顺序表的大小。这样的话负载因子取值范围就是[0, 1]。
我们可以控制当负载因子为0.7时就进行扩容。

对于负载因子:
在这里插入图片描述

在这里插入图片描述

接着上面的,如果我们此时查找20的话,先算出对应下标0,下标0处不是20,继续往后找,为7000,继续找,为20,此时即找到。

但如果找一个不存在的数呢?比如说66,余数为6,下标6不是,继续下标7,不是下标8找到空,此时就可以说明不存在了,因为我们存放的时候如果位置被占了,就会沿着继续往后找,直到找到一个空位置放进去就行。此时我们找一个数,位置被占了,沿着找,到了空就是没找到。

但是如果20被删除了呢?
找40,中间20是空的,就会出现找不到的情况。
我们可以将每一个位置设置一个标志位,EMPTY表示这个位置是空的,曾经没有数据。EXIST表示这个位置中有数据。DELETE表示这个位置没数据,但曾经是有数据的,只是被删除了,我们在查找的时候就不能跳过这个位置。其中EMPTY和DELETE能够插入数据。当想要删除数据的时候,只需要将标志位置为DELETE就行了。

如果说数据一直插入删除导致所有的位置的标志位都变为了DELETE。这样的话查找时就要先找到初始位置,然后从一个位置往后找到顺序表的末尾,再循环到顺序表的最开始找,直到又重新找到原始位置。但一般是不会出现这种情况的,因为

可以看到,这个方法并不是那么好,但是我们还是要学一学的。

下面我们就模拟实现一下。

模拟实现

基本框架:
在这里插入图片描述

然后写一个插入:
在这里插入图片描述

Find函数:
在这里插入图片描述

测试一下:
在这里插入图片描述

再填一个扩一下容:
在这里插入图片描述

然后再来写一下删除:
在这里插入图片描述

再写一个遍历打印的:
在这里插入图片描述

测试:
在这里插入图片描述

字符串等自定义类型找位置

还有问题。
库中是可以存放字符串的。而我们这里实现的可不行。
在这里插入图片描述

因为字符串可没有取模的功能。

想要改改的话,就得搞仿函数。当传一个string的时候,能够返回一个数就行。
那么上面的代码就要改改。在CloseHash的模版参数中添加一个参数hash用来传仿函数,然后在用到%的地方将pair的first套上仿函数。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

对于像int、float、double、long等等这样的类型,直接返回其本身强转的值就够了:
在这里插入图片描述

而对于我们的string需要再写一个仿函数:
在这里插入图片描述
但是模版有特化,我们还可以这样写:
在这里插入图片描述

如果是第一种方式的话,就要这样用:
在这里插入图片描述

如果是第二种方式的话,就不需要传第三个了。
测试:
在这里插入图片描述

这样的话,自定义类型再多传一个模版参数就行了。

字符串哈希算法

还有问题。
字符串冲突的概率还是比数字大不少的,比如说 “abcd” “bcad” “aadd” 还有 “eat” "ate"等等。
在这里插入图片描述

那么怎么解决呢?
有一篇博客详细介绍了:字符串哈希算法

里面有一种方法:
在这里插入图片描述
库中就用的是这个,我们也用:
在这里插入图片描述
这样产生的数重复就会少一点,而且相同字符串每次产生的树都是相同的。

二次探测

上面就是对于闭散列进行的简单实现。实现的时候当数据重复了,就从余数下标位置开始找,一个一个的找空位置,这种一个一个找的方式就叫做线性探测。还有种查找空位置的方式叫做二次探测,可不要听名字就觉得是两个两个位置的找,是平方平方的找。

先说一下线性查找的优缺点:
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
线性查找是 h a s h i = h a s h i + x ( x 取 1 , 2 , 3 … … ) hashi = hashi + x(x取1,2,3……) hashi=hashi+xx1,2,3……

再来说二次探测。查找的方式稍微变了点: h a s h i = h a s h i + x 2 hashi = hashi + x^2 hashi=hashi+x2,x取值同上。
那么这样找的话每次加的就是 1、4、9、16……

比如说下面的数据:
在这里插入图片描述

其中插入一个44,hash(44) = 4,4下标处有数据,4加上1的平方为5,5再加2的平方为9,9加上3的平方为18,再模上10得8,此时8处为空,那么就把44放到8下标处。

在这里插入图片描述

那么我们也可模拟实现一下二次探测,主要是改一下插入就行:
在这里插入图片描述

测试:
在这里插入图片描述

闭散列就讲到这里,没什么太难的地方,下面说更为重要的开散列。

开散列

概念介绍

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

上面的专业术语看起来比较模糊,看图:
在这里插入图片描述
仍然是上面的几个数,不过插入发生哈希冲突的时候只需要搞成链表,把冲突的数据挂到链表上就行了。对于闭散列来说冲突时不会占用其他数据的位置了,而且这样的话查找起来会更加方便,更快。

大概的意思就讲完了,下面来模拟实现。

模拟实现

首先,开散列中存放的是链表,那么用该用八种链表中的哪种呢?

单链表就行了,因为现实中就算有哈希冲突也不会很多的,等会我们模拟实现完后存放一下随机数各位就知道了,每个哈希桶的长度大多都是1,极个别的才是2、3,4往上的可以说几乎就没有。

那么存放单链表的话,就要写出链表的节点:
在这里插入图片描述

然后再写哈希表的框架:
在这里插入图片描述

还是插入:
在这里插入图片描述

查找
在这里插入图片描述

测试:
在这里插入图片描述

再来写删除:
在这里插入图片描述

测试:
在这里插入图片描述

存储自定义类型

和上面闭散列同样的问题,当前实现的开散列的方式没法存储string等自定义类型,所以还
要搞仿函数。

仿函数和上面闭散列中用到的一模一样,这里就不给了。

加上模版参数:
在这里插入图片描述

用到取模的地方套上仿函数:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

测试:
在这里插入图片描述

哈希表大小设置为素数

哈希表大小设置为素数能够减少哈希冲突。这句话是大佬说的,人家有科学依据,而且STL库中也是这么干的。

虽然我不知道为啥,但是咱们照做就行。

搞素数的函数如下:

inline size_t __stl_next_prime(size_t n)
{
	static const size_t __stl_num_primes = 28;
	static const size_t __stl_prime_list[__stl_num_primes] =
	{
	// 这里找的每一位素数都是按照前一位的二倍附近的素数去找的
		53, 97, 193, 389, 769,
		1543, 3079, 6151, 12289, 24593,
		49157, 98317, 196613, 393241, 786433,
		1572869, 3145739, 6291469, 12582917, 25165843,
		50331653, 100663319, 201326611, 402653189, 805306457,
		1610612741, 3221225473, 4294967291
	};

	for (size_t i = 0; i < __stl_num_primes; ++i)
	{
		if (__stl_prime_list[i] > n)
		{
			return __stl_prime_list[i];
		}
	}

	return -1;
}

扩容的时候用一下就行了。
在这里插入图片描述

到这里开散列和闭散列就讲的差不多了,我们来看看插入随机数每个链有多长。

如下几个接口:

// 表的长度
size_t TablesSize()
{
	return _tables.size();
}

// 桶的个数
size_t BucketNum()
{
	size_t num = 0;
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		if (_tables[i])
		{
			++num;
		}
	}

	return num;
}

// 最长桶的长度
size_t MaxBucketLenth()
{
	size_t maxLen = 0;
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		size_t len = 0;
		Node* cur = _tables[i];
		while (cur)
		{
			++len;
			cur = cur->_next;
		}

		//if (len > 0)
			//printf("[%d]号桶长度:%d\n", i, len);

		if (len > maxLen)
		{
			maxLen = len;
		}
	}

	return maxLen;
}

size_t Size()
{
	return _size;
}

测试代码:

void TestHT3()
{

	int n = 10000;
	vector<int> v;
	v.reserve(n);
	srand(time(0));
	for (int i = 0; i < n; ++i)
	{
		//v.push_back(i);
		v.push_back(rand() + i);  // 重复少
		//v.push_back(rand());  // 重复多
	}

	size_t begin1 = clock();
	HashTable<int, int> ht;
	for (auto e : v)
	{
		ht.Insert(make_pair(e, e));
	}
	size_t end1 = clock();

	cout << "数据个数:" << ht.Size() << endl;
	cout << "表的长度:" << ht.TablesSize() << endl;
	cout << "桶的个数:" << ht.BucketNum() << endl;
	cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
	cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
	cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
}

然后测试:

n == 10000
在这里插入图片描述

n == 100000
在这里插入图片描述

n == 1000000
在这里插入图片描述

n == 10000000
在这里插入图片描述

结果也是显而易见了。桶长度一般都是1,2的都少,3、4就更不用说了。
所以说哈希冲突不会那么多的。

上面的接口,STL库中也提供了。
在这里插入图片描述
像bucket_count就是桶个数,max_bucket_count就是最多能有多少个桶,bucket就是第几个桶的大小,bucket是返回某个关键字所在哪一个桶中。

在这里插入图片描述
load_factor就是负载因子。rehash和reserve就是扩容的东西。

最后写一下析构:
在这里插入图片描述

这篇就到这里,下一篇讲解用本篇的代码封装unordered_map和unordered_set。

本篇的所有代码如下:

#pragma once

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

template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t res = 0;
		for (auto c : str)
		{
			res += c;
			res *= 131;
		}
		return res;
	}
};

/*struct HashFuncString
{
	size_t operator()(const string& str)
	{
		size_t res = 0;
		for (auto c : str)
			res += c;

		return res;
	}
};*/

namespace FangZhang_CloseHash
{
	

	// 位置的状态
	enum State
	{
		EMPTY, // 位置为空
		EXIST, // 存在数据
		DELETE // 数据被删除
	};

	// 顺序表中存放的数据 此处是k/v模型
	// 等到讲哈希的封装的时候会改
	template<class K, class V>
	struct HashData
	{
		HashData(const pair<K, V>& kv = make_pair(K(), V()))
			:_kv(kv)
			, _state(EMPTY)
		{}

		pair<K, V> _kv;
		State _state;
	};

	// 闭散列
	template<class K, class V, class Hash = HashFunc<K>>
	class CloseHash
	{
		typedef HashData<K, V> Data;
	public:
		bool Insert(const pair<K, V>& kv)
		{
			// 去重
			if (Find(kv.first))
				return false;

			// 扩容                         _size / _tables.size()就是负载因子
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
			{
				// 新搞一个哈希表
				CloseHash newCH;
				// 新空间大小
				size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				// 给新的哈希表开空间
				newCH._tables.resize(newSize);

				// 扩容后,将原表中的数据重新映射到新表中
				// 改变原来的映射关系,重新映射
				// 能够将原表中重复占位的数减少一些
				for (int i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._state == EXIST)
						newCH.Insert(_tables[i]._kv);
				}

				// 将新表中的数据换到原表中
				_tables.swap(newCH._tables);
			}

			// 正式插入
			//Hash hash;
			//size_t hashi = hash(kv.first) % _tables.size();
			//while (_tables[hashi]._state == EXIST)
			//{ // DELETE 和 EMPTY都可以插入

			//	// 先寻找插入位置
			//	++hashi;

			//	// 循环找空位置
			//	hashi %= _tables.size();
			//}

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

			// 正式插入
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();
			int i = 0;
			while (_tables[hashi]._state == EXIST)
			{
				++i;
				hashi += i * i;
				hashi %= _tables.size();
			}

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

			return true;
		}

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

			Hash hash;
			// 先找位置
			size_t hashi = hash(key) % _tables.size();
			// 记录初始位置
			size_t start = hashi;

			// 标志位为delete或tmpty都要继续找
			while (_tables[hashi]._state != EMPTY)
			{
				// 如果找到了那个数并且标志位不为delete就返回对应的地址
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
					return &_tables[hashi];

				++hashi;
				// 循环找
				hashi %= _tables.size();

				// 绕了一圈都没找到
				if (hashi == start)
					break;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Data* pd = Find(key);
			if (!pd)
			{
				return false;
			}
			else
			{
				pd->_state = DELETE;
				--_size;
				return true;
			}
		}

		void Print()
		{
			for (int i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i]._state == EXIST)
				{
					printf("[%d::%d] ", i, _tables[i]._kv.first);
				}
				else
				{
					printf("[%d::*] ", i);
				}
			}
		}

	private:
		vector<Data> _tables;
		size_t _size;
	};

	void testCH1()
	{
		/*CloseHash<int, int> ch;
		int arr[] = { 3,7,19,25,36,300,7000,22 };
		for (auto i : arr)
		{
			ch.Insert(make_pair(i, i));
		}
		ch.Print();


		ch.Insert(make_pair(2, 2));
		ch.Insert(make_pair(95, 95));
		ch.Insert(make_pair(20, 20));

		ch.Erase(300);
		ch.Print();

		HashData<int, int>* pd = ch.Find(7000);
		cout << pd->_kv.first << endl;
		pd = ch.Find(20);
		cout << pd->_kv.first << endl;
		pd = ch.Find(22);
		cout << pd->_kv.first << endl;
		pd = ch.Find(300);
		cout << pd->_kv.first << endl;*/

		int a[] = { 1, 9, 4, 5, 6, 7};
		CloseHash<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(44,44));

		ht.Print();

		ht.Erase(4);
		cout << ht.Find(44)->_kv.first << endl;
		cout << ht.Find(4) << endl;
		ht.Print();

		ht.Insert(make_pair(-2, -2));
		ht.Print();

		cout << ht.Find(-2)->_kv.first << endl;
	}

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

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

	}


	void TestCH3()
	{
		HashFunc<string> hash;
		cout << hash("abcd") << endl;
		cout << hash("bcad") << endl;
		cout << hash("eat") << endl;
		cout << hash("ate") << endl;
		cout << hash("abcd") << endl;
		cout << hash("aadd") << endl << endl;

		cout << hash("abcd") << endl;
		cout << hash("bcad") << endl;
		cout << hash("eat") << endl;
		cout << hash("ate") << endl;
		cout << hash("abcd") << endl;
		cout << hash("aadd") << endl << endl;
	}
}

namespace FangZhang_OpenHash
{
	template<class K, class V>
	struct HashNode
	{
		HashNode(const pair<K, V>& kv = make_pair(K(), V()))
			:_kv(kv)
			, _next(nullptr)
		{}

		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;

	public:
		bool Insert(const pair<K, V>& kv)
		{
			// 去重
			if (Find(kv.first))
			{
				return false;
			}
			
			// 扩容
			// 此处扩容就不需要让负载因子为0.7了
			// 直接_size == _tables.size()就行
			// 因为不会出现占用其他数据位置的情况
			if (_size == _tables.size())
			{
				size_t newSize = __stl_next_prime(_size);
				// 这里可以利用前面开辟好的节点
				// 所以就不需要像闭散列那样再整一个哈希表了
				// 复用Insert的话开销比较大,还要重新给节点开空间
				// 所以直接给一个vector利用前面开的节点就好
				vector<Node*> newTables;
				newTables.resize(newSize);
				for (auto& node : _tables)
				{
					Node* cur = node;
					Hash hash;
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}

					node = nullptr;
				}

				_tables.swap(newTables);
			}

			// 真正插入
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();

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

			++_size;

			return true;
		}

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

			Hash hash;
			size_t hashi = hash(key) % _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 (!Find(key))
			{
				return false;
			}

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur && cur->_kv.first != key)
			{
				prev = cur;
				cur = cur->_next;
			}

			if (prev)
			{
				prev->_next = cur->_next;
			}
			else
			{
				_tables[hashi] = cur->_next;
			}
				
			delete cur;

			return true;
		}

		// 表的长度
		size_t TablesSize()
		{
			return _tables.size();
		}

		// 桶的个数
		size_t BucketNum()
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					++num;
				}
			}

			return num;
		}

		// 最长桶的长度
		size_t MaxBucketLenth()
		{
			size_t maxLen = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _tables[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}

				//if (len > 0)
					//printf("[%d]号桶长度:%d\n", i, len);

				if (len > maxLen)
				{
					maxLen = len;
				}
			}

			return maxLen;
		}

		size_t Size()
		{
			return _size;
		}

	private:
		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				// 这里找的每一位素数都是按照前一位的二倍附近的素数去找的
					53, 97, 193, 389, 769,
					1543, 3079, 6151, 12289, 24593,
					49157, 98317, 196613, 393241, 786433,
					1572869, 3145739, 6291469, 12582917, 25165843,
					50331653, 100663319, 201326611, 402653189, 805306457,
					1610612741, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

	private:
		vector<Node*> _tables;
		size_t _size = 0;
	};

	void test_HB1()
	{
		int a[] = { 1, 11, 4, 15, 26, 7, 44, 55, 99, 78, 32, 23, 30, 13};
		HashTable<int, int> ht;
		for (auto i : a)
		{
			ht.Insert(make_pair(i, i));
		}

		cout << ht.Erase(4) << endl;
		cout << ht.Erase(44) << endl;
		cout << ht.Erase(4) << endl;
		cout << ht.Erase(15) << endl;
	}

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

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

		cout << endl;
	}

	void TestHT3()
	{
		int n = 19000000;
		vector<int> v;
		v.reserve(n);
		srand(time(0));
		for (int i = 0; i < n; ++i)
		{
			//v.push_back(i);
			v.push_back(rand() + i);  // 重复少
			//v.push_back(rand());  // 重复多
		}

		size_t begin1 = clock();
		HashTable<int, int> ht;
		for (auto e : v)
		{
			ht.Insert(make_pair(e, e));
		}
		size_t end1 = clock();

		cout << "数据个数:" << ht.Size() << endl;
		cout << "表的长度:" << ht.TablesSize() << endl;
		cout << "桶的个数:" << ht.BucketNum() << endl;
		cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
		cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
		cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
	}
}

到此结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/904022.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文学习——FOLEY SOUND SYNTHESIS AT THE DCASE 2023 CHALLENGE(声音生成介绍)

文章目录 引言正文AbstractIntroduction问题 2 Problem And Task Definition3. Official Dataset And Baseline第一部分问题 4. Evaluation问题 4.1 Step 1&#xff1a;Objective Evaluation问题 4.2 Step 2: Subjective Evaluation问题 4.3 Execution&#xff08;非重点&#…

实验一 ubuntu 网络环境配置

ubuntu 网络环境配置 【实验目的】 掌握 ubuntu 下网络配置的基本方法&#xff0c;能够通过有线网络连通 ubuntu 和开发板 【实验环境】 ubuntu 14.04 发行版FS4412 实验平台 【注意事项】 实验步骤中以“$”开头的命令表示在 ubuntu 环境下执行&#xff0c;以“#”开头的…

华为OD机试 - ABR 车路协同场景 - (Java 2023 B卷 100分)

目录 专栏导读一、题目描述1、问题2、条件3、原型 二、输入描述三、输出描述四、Java算法源码五、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&am…

七夕特辑(一)浪漫表白方式 用神经网络生成一首情诗

目录 一、准备工作二、用神经网络生成一首诗&#xff0c;代码说明 牛郎织女相会&#xff0c;七夕祝福要送来。祝福天下有情人&#xff0c;终成眷属永相伴。 七夕是中国传统的情人节&#xff0c;也是恋人们表达爱意的好时机。在这个特别的日子里&#xff0c;送上温馨的祝福&…

idea创建javaweb项目,jboss下没有web application

看看下图这个地方有没有web application

mybatis入门环境搭建及CRUD

一、MyBatis介绍 二、MyBatis环境搭建 创建一个maven项目&#xff0c;名为mybatis01&#xff0c;如下&#xff1a; 2.1 pom.xml修改 代码如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.o…

Java-抽象类和接口(下)

接口使用实例 给对象数组排序 两个学生对象的大小关系怎么确定? 需要我们额外指定. 这里需要用到Comparable 接口 在Comparable 接口内部有一个compareTo 的方法&#xff0c;我们需要实现它 在下图中&#xff0c;我们需要将o强制转换为Student 之后调用Arrays.sort(array)即…

电商项目part04 微服务拆分

微服务架构拆分 微服务介绍 英文:https://martinfowler.com/articles/microservices.html 中文:http://blog.cuicc.com/blog/2015/07/22/microservices 微服务拆分时机 如下场景是否需要进行微服务拆分&#xff1f; 代码维护困难&#xff0c;几百人同时开发一个模块&…

01 背包算法

描述 王强决定把年终奖用于购物&#xff0c;他把想买的物品分为两类&#xff1a;主件与附件&#xff0c;附件是从属于某个主件的&#xff0c;下表就是一些主件与附件的例子&#xff1a; 主件附件电脑打印机&#xff0c;扫描仪书柜图书书桌台灯&#xff0c;文具工作椅无 如果…

漏洞指北-VulFocus靶场专栏-中级02

漏洞指北-VulFocus靶场专栏-中级02 中级005 &#x1f338;thinkphp lang 命令执行&#xff08;thinkphp:6.0.12&#xff09;&#x1f338;step1&#xff1a;burp suite 抓包 修改请求头step2 修改成功&#xff0c;访问shell.php 中级006 &#x1f338;Metabase geojson任意文件…

Linux内核源码分析-内存管理

Linux内核内存布局 64位Linux系统一般使用48位表示虚拟地址空间&#xff0c;45位表示物理地址。通过命令&#xff1a;cat /proc/cpuinfo。查看Linux内核位数和proc文件系统输出系统软硬件信息如下&#xff1a; x86_64架构体系内核分布情况 通过 cat /proc/meminfo 输出系统架…

ruoyi-vue-pro yudao 项目报表设计器 积木报表模块启用及相关SQL脚本

目前ruoyi-vue-pro 项目虽然开源&#xff0c;但是report模块被屏蔽了&#xff0c;查看文档却要收费 199元&#xff08;知识星球&#xff09;&#xff0c;价格有点太高了吧。 分享下如何启用 report 模块&#xff0c;顺便贴上sql相关脚本。 一、启用模块 修改根目录 pom.xml …

Laravel 框架模型的定义 模型的增删改 批量赋值和软删除 ⑧

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; THINK PHP &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

docker 安装 Wordpress 用lnmp搭建出现的故障

第一个故障就是mysql出现的故障了 你起mysql镜像是这么起的导致pid号用不了 docker run --namemysql -d --privileged --device-write-bps /dev/sda:10M -v /usr/local/mysql --net mynetwork --ip 172.20.0.20 mysql:lnmp 解决方法 docker run --namemysql -d --privilege…

【ARM-Linux】项目,语音刷抖音项目

文章目录 所需器材装备操作SU-03T语音模块配置代码&#xff08;没有用wiring库&#xff0c;自己实现串口通信&#xff09;结束 所需器材 可以百度了解以下器材 orangepi-zero2全志开发板 su-03T语音识别模块 USB-TTL模块 一个安卓手机 一根可以传输的数据线 装备操作 安…

【Python数据挖掘】应用toad包中的KS_bucket函数统计好坏样本率、KS值

大数据时代的到来&#xff0c;使得很多工作都需要进行数据挖掘&#xff0c;从而发现更多有利的规律&#xff0c;或规避风险&#xff0c;或发现商业价值。比如在支付领域&#xff0c;通过挖掘商户的交易数据&#xff0c;分析商户是否有欺诈、盗刷、赌博、套现等风险。对于有风险…

孤注一掷——基于文心Ernie-3.0大模型的影评情感分析

孤注一掷——基于文心Ernie-3.0大模型的影评情感分析 文章目录 孤注一掷——基于文心Ernie-3.0大模型的影评情感分析写在前面一、数据直观可视化1.1 各评价所占人数1.2 词云可视化 二、数据处理2.1 清洗数据2.2 划分数据集2.3 加载数据2.4 展示数据 三、RNIE 3.0文心大模型3.1 …

自己实现 SpringMVC 底层机制 系列之--实现任务阶段 3- 从 web.xml动态获取 wyxspringmvc.xml

&#x1f600;前言 自己实现 SpringMVC 底层机制 系列之–实现任务阶段 3- 从 web.xml动态获取 wyxspringmvc.xml &#x1f3e0;个人主页&#xff1a;尘觉主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是尘觉&#xff0c;希望我的文章可以帮助到大家&#xff…

如何根据蛋白质序列找到蛋白质ID

多数数据集中有蛋白质序列但不存储蛋白质ID&#xff0c;这使得PDB文件获取困难&#xff0c;如何找到蛋白质序列对应的ID&#xff0c;参考以下&#xff08;还没有找到批处理方法&#xff0c;如果有知道的小伙伴评论区留言&#xff09;&#xff1a; 1. 进入官方网站&#xff1a;…

java 用协程 实现 简单下订单功能

java 用协程有几种方式&#xff0c;本文是是基于kotlin的协程库实现。 kotlin 协程原理 Kotlin 的协程&#xff08;Coroutines&#xff09;是一种在 Kotlin 语言中实现异步编程的轻量级工具。它可以实现更简洁和可读性更高的异步代码&#xff0c;并且不需要显式地使用回调函数…