【散列】散列表HashTable线性探测法类模板的实现

线性探测法

一般说来,对于不使用分离链接的散列表来说,其装填因子应该低于 λ=0.5,这样的表叫作探测散列表(probing hash table)。

如上图所示,当插入关键字68时,h(68)=68%11=2,与23冲突,于是被放入到下一个空闲位置4;插入11时,h(11)=0,与55冲突,向后寻找并存入空闲位置5。。。。只要表足够大,总能找到一个空闲位置,但花费的时间是相当多的。

可以看出,插入和不成功查找需要相同次数的探测,且成功查找应该比不成功查找平均花费较少的时间。

平方探测法

平方探测法是消除线性探测中一次聚集问题的冲突解决办法 – 冲突函数为二次的探测方法,流行的选择是f(i)=i2

如果使用平方探测,且表的大小是素数,那么当表至少有一半是空的时候,总能够插入一个新的元素。

再散列

对于使用平方探测的开放定址散列法(open addressing hashing),若散列表装的太满,则操作的运行时间将消耗过长,且插入操作可能失败,这可能发生在有太多的删除和插入混杂的场合。
此时,一种解决办法是建立另外一个大约两倍大的表(而且使用一个相关的新散列函数),扫描整个原始散列表,计算每个(未删除的)元素的新散列值并将其插入到新表中。

再散列可以用平方探测以多种方法实现:

(1)只要表满到一半就再散列
(2)只有当插入失败时才再散列
(3)途中策略:当散列表到达某一特定的装填因子时进行再散列。

代码实现

#include<vector>
#include<string>
int nextPrime(int n);

//使用函数对象,让散列函数只用对象作为参数并返回适当的整形量
template<typename T>
class _hash
{
public:
	size_t operator()(const T& key) {
		size_t hashVal = 0;

		T tmp = key;
		while (tmp > 0) {
			hashVal = 37 * hashVal + tmp % 10;
			tmp /= 10;
		}
		return hashVal;
	}
};

template<>
class _hash<std::string>
{
public:
	size_t operator()(const std::string & key) {
		size_t hashVal = 0;

		for (char ch : key) 
			hashVal = 37 * hashVal + ch;

		return hashVal;
	}
};

//使用探测方法的散列表的类接口,包括内嵌的HashEntry类
template<typename HashedObj>
class HashTable {
public:
	explicit HashTable(int size = 101):array(nextPrime(size))
	{
		makeEmpty();
	}

	bool contains(const HashedObj& x)const {
		return isActive(findPos(x));
	}

	void makeEmpty() {
		currentSize = 0;
		for (auto& entry : array)
			entry.info = EMPTY;
	}

	bool insert(const HashedObj& x) {
		//将x作为active插入
		int currentPos = findPos(x);
		if (isActive(currentPos))
			return false;

		array[currentPos].element = x;
		array[currentPos].info = ACTIVE;

		//再散列
		if (++currentSize > array.size() / 2) //装填因子超过0.5,则表是满的
			rehash();
		return true;
	}

	bool insert(HashedObj&& x) {
		//将x作为active插入
		int currentPos = findPos(x);
		if (isActive(currentPos))
			return false;

		array[currentPos].element = std::move(x);
		array[currentPos].info = ACTIVE;

		//再散列
		if (++currentSize > array.size() / 2) //装填因子超过0.5,则表是满的
			rehash();
		return true;
	}

	bool remove(const HashedObj& x) {
		int currentPos = findPos(x);
		if (!isActive(currentPos))
			return false;
		array[currentPos].info = DELETED;
		return true;
	}

	enum EntryType{ACTIVE,EMPTY,DELETED};

private:
	struct HashEntry {
		HashedObj element;
		EntryType info;

		HashEntry(const HashedObj& e = HashedObj{}, EntryType i = EMPTY)
			:element(e), info{ i }{}
		HashEntry(HashedObj&& e, EntryType i = EMPTY)
			:element(e), info{ i }{}
	};

	std::vector<HashEntry>array;
	int currentSize;

	bool isActive(int currentPos)const {
		return array[currentPos].info == ACTIVE;
	}

	int findPos(const HashedObj& x)const {
		int offset = 1;
		int currentPos = myhash(x);

		/*平方探测快速方法,由平方消解函数的定义可知,f(i)=f(i-1)+2i-1,因此,下一个要尝试的
		* 单元离上一个被试过的单元有一段距离,而这个距离在连续探测中增2。
		*/
		while (array[currentPos].info != EMPTY && array[currentPos].element != x) {
			currentPos += offset; //计算第i次探测
			offset += 2;
			if (currentPos >= array.size()) //新的定位越过数组,需要将它拉回到数组范围内
				currentPos -= array.size();
		}
		return currentPos;
	}

	void rehash() {
		std::vector<HashEntry> oldArray = array;

		//创建新的两倍大小的空表
		array.resize(nextPrime(2 * oldArray.size()));
		for (auto& entry : array)
			entry.info = EMPTY;

		//复制整个表
		currentSize = 0;
		for (auto& entry : oldArray)
			if (entry.info == ACTIVE)
				insert(std::move(entry.element));
	}

	size_t myhash(const HashedObj& x)const {
		static _hash<HashedObj> hf;
		return hf(x) % array.size();
	}
};
/**
 * 判断素数
 */
bool isPrime(int n)
{
	if (n == 2 || n == 3)
		return true;

	if (n == 1 || n % 2 == 0)
		return false;

	for (int i = 3; i * i <= n; i += 2)
		if (n % i == 0)
			return false;

	return true;
}

/**
 * 返回比n大的下一个素数
 * 假设 n > 0.
 */
int nextPrime(int n)
{
	if (n % 2 == 0)
		++n;

	for (; !isPrime(n); n += 2)
		;

	return n;
}

posted @ 2022-10-18 08:47  aw11  阅读(126)  评论(0)    收藏  举报