【C++】-- 哈希(上万字详细配图配代码从执行一步步讲解)

news2024/10/6 16:31:52

目录

哈希

常见哈希函数

除留余数法

哈希冲突

哈希冲突解决

闭散列 

a、线性探测

插入

查找

删除

线性探测的实现代码

b、二次探测

二次探测的实现

开散列

开散列实现

插入

查找

删除

析构函数

代码汇总


哈希

常见哈希函数

  • 直接定址法 -- (常用) -- 不存在哈希冲突
        取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
        优点:简单、均匀
        缺点:需要事先知道关键字的分布情况
         使用场景:适合查找比较小且连续的情况(常见:计数排序)
  • 除留余数法 -- (常用) -- 存在哈希冲突,重点解决哈希冲突
        设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 (本文重点)
  • 平方取中法 -- (了解) -- 存在哈希冲突,只能适用于整数
        假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
        再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
         平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
  • 折叠法 -- (了解) -- 不存在哈希冲突,只能适用于整数
        折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。
         折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。
  • 随机数法 -- (了解)
        选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。
        随机数法通常应用于关键字长度不等时。
  • 数学分析法 -- (了解)
        设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:

        假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法。
        数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况。

除留余数法

        哈希基于映射,值跟存储位置建立关联映射关系。以我们就学过计数排序,实现的原理是:
利用映射,取最大值到最小值的大小建立数组通过唯一的对应关系,利用映射关系计数排序,正是因为此计数排序也有巨大的缺陷,由于数组大小取决最大值与最小值,如果遇见:3 7 19 300 70000,仅仅5个数据就要创建70000个空间,而哈希利用
除留余数法建立关联映射关系:

        设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
以p == 10为例:

哈希冲突

        不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

哈希冲突解决

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

  • 闭散列 -- 开放定址法
    • a、线性探测
    • b、二次探测
  • 开散列 -- 拉链法/哈希桶(此方法更好,也是库中所用的)

闭散列 

a、线性探测

        如,下列场景,现在需要插入元素44,先通过哈希函数计算哈希地址,为4(44%10 = 4),因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突:

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

插入

  • 通过哈希函数获取待插入元素在哈希表中的位置。
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素。

         从此可以看出闭散列的线性探测是不好的,容易数据之间过于的占用位置,造成互相影响,但是其也是有效的方式。(冲突越多效率越低)

查找

  • 根据插入的规则,如果数据存在。即,数据一定在:该哈希地址位置,或者在地址位置后连续不为空的序列里,否则无该值。

删除

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

为什么以伪删除法来删除一个元素?

        如果我们不用伪删除法来删除一个元素:

伪删除法:加一个状态标志位

// 哈希表每个空间给个标记

// EMPTY此位置空, EXIST 此位置已经有元素, DELETE 元素已经删除
enum State EMPTY , EXIST , DELETE  };

        即:删除的时候并不是标为空,而是标为删除。这样查找的时候看EMPTYEMPTY是无该数据插入的是时候看是DELETE还是EMPTY,是DELETE更改数据,是EMPTY填补数据。

线性探测的实现代码

        由于key值需要进行%数求哈希地址为,遇见能强转为size_t的还好,但是如果遇见的是string之类不能强转的就会出现问题,所以我们需要利用仿函数来解决此类问题,对于能强转的我们提供成默认的仿函数,并利用特化提供string类型的,其余不能强转的同理。

        字符串哈希算法

#include<vector>
#include<utility>
#include<iostream>
#include<string>
using namespace std;

enum State
{
	EMPTY,  //没有元素
	EXIST,  //存在元素
	DELETE  //该元素已被删除
};

// 单位数据
template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

// 仿函数为了防止出现Key是string的存在,因为string不能直接%,其余不能直接%同理
//能强转的
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};


//由于string不能强转为size_t,需要显示写仿函数,特化
template<>
struct HashFunc<string>
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}

		return val;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	Hash hash;
	//插入
	bool Insert(const pair<K, V>& kv)
	{
		// 防止已有该值
		if (Find(kv.first))
			return false;

		//哈希表的扩容
		if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 散列表的载荷因子:a = 填入表中的元素个数 / 散列表的长度。(此处7为规定的载荷因子)
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;

			// size变大,即%数变大,哈希地址改变,需要重新insert。
			HashTable<K, V> newHT;
			newHT._tables.resize(newSize);
			for (auto e : _tables)
			{
				if (e._state == EXIST)
					newHT.Insert(e._kv);
			}
			_tables.swap(newHT._tables);
		}

		size_t hashi = hash(kv.first) % _tables.size(); //此处%的是size是因为底层为vector实现,而其opertor[]的范围规定为size

		// 哈希表是通过:size大小的列表的如同头与尾相接,以循环寻找插入位置。
		// (由于引入了载荷因子,列表不可能满)
		while (_tables[hashi]._state != EMPTY)
		{
			hashi++;
			hashi %= _tables.size();
		}
		// 插入数据
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		_size++;

		return true;
	}

	//查找
	HashData<K, V>* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}

		size_t state = hash(key) % _tables.size();
		size_t hashi = state;
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				return &_tables[hashi];

			hashi++;
			hashi %= _tables.size();

			if (hashi == state) // 防止出现边删除边插入而导致的,不超过载荷因子下而存满(概率极低)
				break;
		}
		return nullptr;
	}

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

	//打印存在的数据
	void Print()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i]._state == EXIST)
				cout << "[" << i << "]:" << _tables[i]._kv.first << "  ";
		}
	}

private:
	vector<HashData<K, V>> _tables;
	size_t _size = 0;  //存储多少个有效数据
};


void TestHash()
{
	HashTable<int, int> h;
	int array[] = { 1, 5, 8, 33, 77, 36, 86, 1, 8, 5, 8, 2, 5 };
	for (auto e : array)
	{
		h.Insert(make_pair(e, e));
	}

	h.Erase(1);
	h.Erase(6);
	cout << h.Find(1) << endl;
	cout << h.Find(8) << endl;
	h.Print();
}

void TestHashString()
{
	HashTable<string, int> h;
	string array[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	for (auto ch : array)
		h.Insert(make_pair(ch, 1));

	h.Print();
}

int main()
{
	TestHash();
	TestHashString();
	return 0;
}
哈希表线性探测进行扩容方式

        如 a = 0.7即:填入表中的元素个数只能是散列表长度的70%。当大于就扩容。

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

b、二次探测

二次探测的实现

        二次探测的实现与线性探测的实现的核心是极为相似的,也就是对于哈希地址取值方式的改变。

#include<string>
#include<vector>
#include<iostream>
using namespace std;
enum State
{
	EMPTY,
	EXIST,
	DELETE
};

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

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

// 特化
template<>
struct HashFunc<string>
{
	// BKDR
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}
		return val;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
			return false;

		// 负载因子到了就扩容
		if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 扩容
		{
			size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V, Hash> newHT;
			newHT._tables.resize(newSize);
			// 旧表的数据映射到新表
			for (auto e : _tables)
			{ 
				if (e._state == EXIST)
					newHT.Insert(e._kv);
			}

			_tables.swap(newHT._tables);
		}

		Hash hash;
		size_t start = hash(kv.first) % _tables.size();
		size_t i = 0;
		size_t hashi = start;
		// 二次探测
		while (_tables[hashi]._state == EXIST)
		{
			++i;
			hashi = start + i * i;
			hashi %= _tables.size();
		}

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

		return true;
	}

	HashData<K, V>* Find(const K& key)
	{
		if (_tables.size() == 0)
			return nullptr;

		Hash hash;
		size_t start = hash(key) % _tables.size();
		size_t hashi = start;
		int i = 0;
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				return &_tables[hashi];

			i++;
			hashi = start + i * i;
			hashi %= _tables.size();

			if (hashi == start)
				break;
		}
		return nullptr;
	}

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

	void Print()
	{
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			cout << "[" << i << "]:" << _tables[i]._kv.first << "  ";
		}
		cout << endl;
	}

private:
	vector<HashData<K, V>> _tables;
	size_t _size = 0; //存储的数据个数
};

void TestTable()
{
	HashTable<int, int> h;
	int array[] = { 1,6,545,876,235,8765,41 };
	for (auto e : array)
	{
		h.Insert(make_pair(e, e));
	}

	cout << h.Find(545) << endl;
	h.Erase(545);
	cout << h.Find(545) << endl;

	h.Print();
}

int main()
{
	TestTable();
	return 0;
}

        总的来说闭散列是不好的,可以说是被淘汰的,但是意识一种值得学习的是思维。其作为哈希实现的一种思维结构,闭散列以这种开放定址法,总是会由于冲突而去占用别的位置。于是便有为解决冲突的开散列的桶式结构 —— 拉链法/哈希桶。

开散列

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

        从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。(1、11、21冲突。2、12冲突), 每一个单链表各自称为一个桶

开散列实现

        对于增删查,皆与单链表的实现相识。本质上就是多个单链表放入数组中。

        由于存储的数据可能是string之类,无法直接强转为size_t的类型,所以我们需要使用仿函数实现:

namespace cr
{
	// 哈希桶中的单链表的节点
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _data;
		HashNode<K , V>* _next;

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

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

	// 特化
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& key)
		{
			size_t count = 0;
			for (auto ch : key)
			{
				count *= 131;
				count += ch;
			}
			return count;
		}
	};

	// 哈希桶的封装实现
	template<class K, class V, class Hash = HashFunc<K>>
	class HashBucket
	{
	private:
		typedef HashNode<K, V> Node;
	public:
    
    //……

	private:
		vector<Node*> _tables; // 哈希表
		size_t _size = 0; // 有效数据个数
	};
}

插入

        由于是单链表,并且哈希桶并未要求对数据进行排序,所以表中的每一个单链表,进行尾插还是头插都是可以的,此处选择头插

        对于空间的扩容,根据相关实验素数的效率更高,于是哈希桶的空间开辟是根据素数数组开辟的:

// 提取空间开辟的大小(素数)
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
	};
	//4294967291个int类型已经是16G,大小已经足够。

	for (size_t i = 0; i < __stl_num_primes; ++i)
	{
		if (__stl_prime_list[i] > n)
			return __stl_prime_list[i];
	}
	return -1;
}
// 插入
bool Insert(const pair<K, V>& key)
{
	//查重
	if (Find(key.first))
		return false;

	Hash hash;
	// 扩容
	if (_size == _tables.size())
	{
		vector<Node*> newTables;
		newTables.resize(__stl_next_prime(_size), nullptr);
		// 将旧表数据移动到新表
		for (size_t i = 0; i < _tables.size(); ++i)
		{
			// 将旧表的每个哈希桶的单链表节点移动到新表上
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;

				size_t hashi = hash(key.first) % newTables.size(); // 在新表上的哈希地址

				// 在新表的哈希地址上头插
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;

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

	// 找到哈希地址并进行头插
	size_t hashi = hash(key.first) % _tables.size();
	Node* cur = new Node(key);
	cur->_next = _tables[hashi];
	_tables[hashi] = cur;
	++_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->_data.first == key)
			return cur;
		cur = cur->_next;
	}
	return nullptr;
}

删除

        根据哈希地址寻找后,直接按照单链表的删除方式即可。

// 删除
bool Erase(const K& key)
{
	if (_tables.size() == 0) // 未存入数据
		return false;

	Hash hash;
	size_t hashi = hash(key) % _tables.size();
	Node* cur = _tables[hashi];
	Node* prev = nullptr;
	while (cur)
	{
		if (cur->_data.first == key) // 找到需删除的数据
		{
			if (prev == nullptr) // 删除的是桶的第一个数据
				_tables[hashi] = cur->_next;
			else
				prev->_next = cur->_next;
			delete cur;
			--_size;

			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

析构函数

        由于是单链表,是new的空间,所以需要写析构函数,对一个一个节点释放。 

~HashBucket()
{
	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;
	}
}

代码汇总

#include<iostream>
#include<utility>
#include<vector>
#include<time.h>
using namespace std;

namespace cr
{
	// 哈希桶中的单链表的节点
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _data;
		HashNode<K , V>* _next;

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

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

	// 特化
	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& key)
		{
			size_t count = 0;
			for (auto ch : key)
			{
				count *= 131;
				count += ch;
			}
			return count;
		}
	};

	// 哈希桶的封装实现
	template<class K, class V, class Hash = HashFunc<K>>
	class HashBucket
	{
	private:
		typedef HashNode<K, V> Node;
	public:
		~HashBucket()
		{
			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;
			}
		}

		// 提取空间开辟的大小(素数)
		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
			};
			//4294967291个int类型已经是16G,大小已经足够。

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

		// 插入
		bool Insert(const pair<K, V>& key)
		{
			//查重
			if (Find(key.first))
			{
				return false;
			}

			Hash hash;
			// 扩容
			if (_size == _tables.size())
			{
				vector<Node*> newTables;
				newTables.resize(__stl_next_prime(_size), nullptr);
				// 将旧表数据移动到新表
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					// 将旧表的每个哈希桶的单链表节点移动到新表上
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash(key.first) % newTables.size(); // 在新表上的哈希地址

						// 在新表的哈希地址上头插
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

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

			// 找到哈希地址并进行头插
			size_t hashi = hash(key.first) % _tables.size();
			Node* cur = new Node(key);
			cur->_next = _tables[hashi];
			_tables[hashi] = cur;
			++_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->_data.first == key)
					return cur;
				cur = cur->_next;
			}
			return nullptr;
		}

		// 删除
		bool Erase(const K& key)
		{
			if (_tables.size() == 0) // 未存入数据
				return false;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_data.first == key) // 找到需删除的数据
				{
					if (prev == nullptr) // 删除的是桶的第一个数据
						_tables[hashi] = cur->_next;
					else
						prev->_next = cur->_next;
					delete cur;
					--_size;
					
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		// 有效数据个数
		size_t Size()
		{
			return _size;
		}

		// 表的长度
		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 > maxLen)
					maxLen = len;
			}
			return maxLen;
		}
	private:
		vector<Node*> _tables; // 哈希表
		size_t _size = 0; // 有效数据个数
	};

	void TestHT()
	{
		int n = 19000000;
		vector<int> v;
		v.reserve(n);
		srand(time(0));

		//rand()所提供的数最多位3万多,所以在远大于3万会大量数重复
		for (int i = 0; i < n; ++i)
		{
			//v.push_back(i);
			v.push_back(rand() + i);  // 重复少
			//v.push_back(rand());  // 重复多
		}

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

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

int main()
{
	cr::TestHT();
	return 0;
}

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

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

相关文章

2022 年博客总结

时间过的飞快&#xff0c;孩子也快4岁了&#xff0c;1号带孩子去玩雪&#xff0c;发生了一件有趣的事&#xff0c;发个视频。 带孩子玩雪我拉着闺女&#xff0c;闺女拉着儿子&#xff0c;忽略了力的作用&#xff0c;我以为只有我在使劲&#xff0c;实际上闺女需要需要更大的力拉…

java8新特性——函数式编程

文章目录1.函数式编程思想1.1概念1.2函数式编程的思想2.Lambda表达式2.1概述2.2核心原则2.3基本格式2.4Lambda表达式练习2.5省略规则3.Stream流3.1概述3.2案例准备3.3Steam流操作案例3.3.1需求3.3.2实现3.4Stream常用操作3.4.1创建stream流方式3.4.2中间操作3.4.2.1filter3.4.2…

SpringMVC 底层机制的简易实现

SpringMVC 底层机制的简易实现项目基础配置 xml 文件开发指南开发步骤1.初始化数据2.中央控制器 - 分发请求3.开发者角度4.视图解析器开发总结项目基础 <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId&g…

【Python基础】常用数据结构及处理

1. KeyValue dict.clear() 删除字典内所有元素dict.copy()返回一个字典的浅复制[dict.fromkeys(seq, val])创建一个新字典&#xff0c;以序列 seq 中元素做字典的键&#xff0c;val 为字典所有键对应的初始值 dict.get(key, defaultNone) 返回指定键的值&#xff0c;如果值不在…

prompt模型详解之文本生成

prompt在生成方面的应用从两个方面进行介绍&#xff1a; 评估手段 具体任务 评估手段 生成任务的评估手段主要分为四种类型&#xff1a; 1). 基于N-gram匹配 2). 基于编辑距离 3). 基于词向量 4). 基于可学习方式。 本小节主要介绍BARTSCORE&#xff0c;其使用prompt方…

Python杂题

目录 一、前言 二、例题1——修剪灌木 三、例题2—— 付账问题 四、例题3——最少砝码 五、例题四——矩形拼接 六、例题五——蜂巢 一、前言 竞赛题有很多不需要什么算法的题目&#xff0c;只要学过编程语言就能做&#xff0c;其考核思维、逻辑、编码能力。而这种题有“…

【算法题解】 8. K 个一组翻转链表

文章目录题目解题思路代码实现复杂度分析题目 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍&#xff0c;那么请将最后剩余的节点保持…

Python中编码(encode)解码(decode)讲解

嗨害大家好鸭&#xff01;我是小熊猫~ 这次也是给大家带来一点干货~ 所用素材:点击此处跳转文末名片获取 一、python3中str与unicode 在python3中&#xff0c;字符串有两种形式&#xff1a;str和bytes&#xff0c;两者区别如下&#xff1a; unicode string(str类型)&#xf…

【经验】关于区分cin、getline、cin.getline三种字符串输入的区别

cin 既可以输入char[]数组&#xff0c;也可以输入string类型&#xff0c;输入会被空格打断 cin对char进行输入 #include<bits/stdc.h> using namespace std; int main(){char ch[50];cin>>ch;cout<<strlen(ch)<<endl;for(int i0;i<strlen(ch);i){…

1.移动机器人发展现状

移动机器人主要应用场景&#xff1a; 场景1.仓储机器人(AGV自动导引运输车)&#xff1a;电商企业用户下单后机器人可以实现自动分拣和发货。需要多个传感器配合 2.自动驾驶领域(AMR自主移动机器人):车辆避让行人、导航等 热点研究领域&#xff1a; 环境感知和建模、人机交互…

2022简要总结和2023行动指南

在这辞旧迎接之际&#xff0c;心存感恩&#xff0c;放眼未来。 祝宝妈妈宝&#xff0c;幸福快乐&#xff1b; 祝国泰民安&#xff0c;政通人和。 祝百融云创系&#xff0c;生意兴隆&#xff1b; 祝公司老板们&#xff0c;大展宏图&#xff1b; 祝同事同行er&#xff0c;身…

Java算法_LeetCode:旋转数组

旋转数组 给你一个数组&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,…

Fastsapi的小疑问

1. Fastapi中的get和post区别是什么&#xff1f; 答&#xff1a;get参数传输暴露在外&#xff0c;post隐式传输 GET参数获取&#xff1a;获取一个URL后面带?param11&param22这种形式。 特点&#xff1a;URL上直接编辑传输&#xff0c;方便快捷&#xff0c;但是信息暴露在…

【nowcoder】笔试强训Day16

目录 一、选择题 二、编程题 2.1扑克牌大小 2.2完全数计算 一、选择题 1.在关系型是数据库中&#xff0c;有两个不同的事务同时操作数据库中同一表的同一行&#xff0c;不会引起冲突的是&#xff1a; A. 其中一个DELETE操作&#xff0c;一个是SELECT操作 B. 其中两个都是…

植物大战僵尸:代码实现无限阳光

通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移&#xff0c;从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据&#xff0c;最后我们将通过使用C语言编写通用辅助实现简单的无限阳光辅助&#xff0c;在教程开始之前我们先来说一下为什么会有动态地址与基址…

光缆单盘检测与光缆线路测试需使用双窗口吗?

1 引言 光缆线路和宽带接入工程中&#xff0c;通常会涉及光缆单盘检测与光缆线路的测试工作&#xff0c;光缆线路测试包括&#xff1a;中继段测试、用户光缆测试等。这些测试条目&#xff0c;有的只需采用测试仪表的1个波长进行测试&#xff0c;即单窗口测试&#xff0c;有的则…

代码随想录算法训练营第2天 977. 有序数组的平方、209. 长度最小的子数组

代码随想录算法训练营第2天| 977. 有序数组的平方、209. 长度最小的子数组 有序数组的平方 力扣题目链接(opens new window) 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 数组其实是有…

C 语法--编译相关

1&#xff0c; 单下划线和双下划线 #pragma #pragma 用于指示编译器完成一些特定的动作。#pragma 所定义的很多指示字是编译器特有的&#xff0c;在不同的编译器间是不可移植的 #pragma section APP_VERSION //__far const unsigned long version 0x01010101; __far const un…

redis缓存淘汰策略-基于LinkedHashMap实现LRU算法

redis缓存淘汰策略-LRU算法&#xff08;最近最少使用&#xff09; LRU是Least Recently Used的缩写&#xff0c;即最近最少使用&#xff0c;是一种常用的页面置换算法&#xff0c; 选择最近最久未使用的数据予以淘汰。 1&#xff0c;所谓缓存&#xff0c; 必须要有读写两个操作…

【叨叨与总结】2022年总结

如果我记得没错&#xff0c;这个记录时间的软件应该是在6月份或者7月份才开始用的&#xff0c;大概记录的时间有半年。个人觉得还是不错的&#xff0c;下面还是简单的总结一下。   首先睡眠时长是有一定保障的&#xff0c;甚至有好多时候还睡了9、10个小时&#xff0c;当然这…