C++ 哈希使用与底层原理

news2024/11/26 16:40:03

哈希的概念

       哈希是一种建立映射的思想,我们尝试用的数据结构是哈希表 ,又称「散列表」,其通过建立键 key 与值 value 之间的映射,实现高效的元素查询。具 体而言,我们向哈希表输入一个键 key ,则可以在 𝑂(1) 时间内获取对应的值 value 。早期的C++的STL中没有hash表,在C++11中,STL又提供了4个 unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是 其底层结构不同。其中unordered_set就是一种哈希表。关于C++函数的查询可以点击下面链接。unordered_set - C++ Reference (cplusplus.com)

       大家可以看下如下视频,可以对哈希有个大致的印象。最后再来看博客中具体的代码实现。哈希究竟代表什么?哈希表和哈希函数的核心原理_哔哩哔哩_bilibili

        可以说哈希的核心就是建立key 与值 value之间的映射关系。下面我们可以看到题目来体会这种关系。

        387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

        我们可以分两步解决,第一次遍历字符串统计每个出现的次数,第二次遍历找次数为1的字符下标,没找到就返回-1。如下述代码。

class Solution {
public:
    int firstUniqChar(string s) 
    {
        int check[26]={0};

        //先遍历一遍统计次数
        for(int i=0; i<s.size(); i++)
        {
            check[s[i]-'a']++;
        }

         //找次数为1的元素
        for(int i=0; i<s.size(); i++)
        {
            if(check[s[i]-'a']==1)
                return i;
        }

        return -1;
    }
};

        可以看到上述代码中我们没有给每次字符建立一个count,int counta=0; int countb=0;......等,这显然是十分麻烦的,我们知道字符按照ASCII值存储在内存中,我们就可以建立256大小的数组check,当遍历到字符a的时候,就让check[97]++,上述就建立了如下映射关系。

        但通过题目我们知道字符串只含有小写字母,相当于数组前97都没用,于是我们就可以进一步优化映射关系如下。

        他们之间的关系也是十分容易的,只需要将原本check[s]++改为check[s-97]++即可,这样就可以将小写字母与数组下标建立映射关系。这样我们查询数组中一个元素只需要O(1)的时间复杂度,效率十分高。

       在C++中哈希表命名为unordered_set和unordered_map,如果大家之前了解过set与map,那么unordered_set和unordered_map使用与前者几乎一样,只不过底层结构有差别罢了,set与map底层一般为红黑树,unordered_set和unordered_map实现有多种方法,下面我们来一一实现。

实现哈希

              在上述的问题中,我们的元素个数值有26个,在使用哈希算法(ch-97)后的位置也比较近,可以直接开辟26大小的数组即可,但在实际的问题中我们遇到的数字可能并不集中,比较分散且数比较大,如下图。

        此时数组a最大值为33465,我们有必要为5个数开辟大小为33465的数组么?结果是显然不能的,先不说这么大内存可不可以向堆区申请出来,为了5个数开辟这么的大空间,浪费了太多空间,就是丢了西瓜捡了芝麻,大可不必。

        我们有更合适哈希算法取模( 除留余数法)。例如674%10=4,就是674在数组中映射的下标。10为数组的大小。33465%10=5,就在数组下标为5的地方存储33465,这样就不必开辟太大的内存了。

        我们按照上述方法依次向上插入数据,结果到最后一个时,遇到如下问题

        我i们在插入最后一个数的时候,发先下标为5的地方已经有了数字,显然我们不能直接将下标为5的数字改为33465,这样会造成数据的丢失,而上述情况我们称之为哈希冲突,也叫哈希碰撞。

        解决哈希冲突的方法有许多,一种时改进哈希算法,例如将上述数组的大小改为100,就不会出现哈希冲突了,但这只是权宜之计,当数据量不断变大的时候,一定会出现哈希冲突。我们更多的采用开放地址法(开散列)或者封闭地址法(闭散列)。

闭散列

        闭散列的核心思想,十分简单。当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。例如上图的33465就可以存在下标为7的地方。

        

        还有一个问题就是我们数组的大小为10,不断插入数据一定会插满,我们就需要扩容。并且在插入个数n越大的时候,插入效率就越低,我们于是引入一个概念,负载因子a=插入元素个数/数组大小。当a大于0.7的时候就进行扩容。将数组的大小翻倍。

        到此我们就基本了解了哈希表的一种原理,接下来我们就可以着手实现了。

        首先根据上述我们可以先搭建一个大框架如下。(定义在一个头文件中,后序使用只需包含头文件即可)

#pragma once
#include<iostream>
#include<vector>
using namespace std;


template<class V> //Value 存储的数据类型
class HashTable
{


private:
	vector<V> _table;
	size_t _n;//用于计算负载因子
};

        对于哈希表我们常用的就是插入,删除,查找,外加些迭代器,接下来我们一一实现。

插入

        我们向一个数组中插入数据,核心思想就是先找到映射后的下标hashi,然后判断当前位置是否为空,为空就插入,否则就找下一个空位。

        但在这里我们遇到个问题,假设我们插入的数据时int,我们怎么判断当前位置是否可以插入?

        例如下图,数组元素初始化为0,我们或许认为可以在下标为6的地方插入16,

        但如果时下图该如何判断呢?我们可以在下标为0的地方插入0么?我们无法判断数组小标为0位置的元素是初始化时造成的还是我们后序插入造成的,所以上述的框架要修改,将vector每个元素修改为结构体,结构体中存该位置的状态。

        修改后如下。

//枚举出当前状态
enum State
{
	EXIST,
	EMPTY,
	DELETE
};
template<class V>
struct date
{
	//默认初始化为空
	date()
		:_status(EMPTY),
		_date(V())
	{

	}

	State _status;
	V _date;
};


template<class V> //Value 存储的数据类型
class HashTable
{
public:
//构造函数,初始化_n与_table
HashTable(int n=10)
	:_n(0),
	_table(n)
{
}

private:
	vector<date<V>> _table;
	size_t _n;//用于计算负载因子
};

        插入函数有两个要点

        1.当前位置存在插入元素的时候,就向下一个位置,直到遇到合法的位置。

        2.数组扩容

bool insert(const V& key)
{
	resize(_n + 1);

	size_t hashi = key % _table.size();

	//当前位置已经插入元素,向后寻找合法位置
	while (_table[hashi]._status == EXIST)
	{
		hashi++;
		hashi %= _table.size();//防止越界
	}
	
	
	//当前位置为空或者向后寻找的新位置
	_table[hashi]._date = key;
	_table[hashi]._status = EXIST;
	_n++;

	return true;
}

        当我们写完上述代码后,可以将扩容函数单独封装在resize函数中。如下代码,再将原来插入函数开头加一句resize(_n + 1);即可。

	bool resize(int n)
	{
		//乘10解决除法小数问题
		if (10 * n / _table.size() < 7)
		{
			return true;
		}
		//需要扩容
		HashTable<V> hs(2 * _table.size());
		//下面代码十分巧妙
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._status == EXIST)
			{
				//调用新对象的插入函数,复用代码
				//对于新hs对象,他的大小为原来两倍一定不会扩容,只会执行插入
				hs.insert(_table[i]._date);
			}
		}
		//将新hs对象_table与原来交换
		_table.swap(hs._table);
		return true;
	}

        上述代码最巧妙的是利用已经写好的插入函数,将新旧对象的_table交换,从而旧对象完成了扩容加插入的操作。

测试

        为了方便我们可以在HashTable类中加入如下代码,打印数组中元素

void CoutArr()
{
	for (int i = 0; i < _table.size(); i++)
	{
		if (_table[i]._status == EXIST)
		{
			cout <<"下标" << i << ":" << _table[i]._date << " ";
			cout << endl;
		}
		else
		{
			cout << "下标" << i << ":空";
			cout << endl;
		}
	}
	cout << endl;
	cout << endl;
}

        测试代码

void test1()
{
	HashTable<int> ht;

	int a[] = { 1,5,6,4,8,9,10 };

	for (auto e : a)
		ht.insert(e);

	ht.CoutArr();
}

        运行结果如下,当我们插入第七个元素10的时候会发生扩容,我们可以通过调试更好观察

        如下插入10元素的时候,如下连续图

        最后在正常插入即可。

        到此我们插入函数基本就结束了。

插入扩展

        我们刚才面对当前位置已经存在元素采取的方式是依次向后加一寻找空位置,称之为线性探测,他每次探测的位置可以如下图表示

        除了线性探测外,还有二次探测,他每次的位置是原位置加上i的平方,如下图。相对来说二叉探测比线性探测效率更好,可以减少数据集群在某个值附近的情况。

        当然除了上述两种方法还有很多寻找空位置的方法,在这里就不一一阐述了。

删除

        接下来我们实现删除功能,首先按照插入的思路先找到映射后的下标hashi,然后判断当前位置是否为目标值,否则就找下一个空位。直到找到目标值或者遇到EMPTY位置。

        这里有个细节就是我们把当前位置的状态分为三种,空,删除,存在,而不是简单的空和存在两种情况,这主要是为了解决下面这种特殊的情况。如下图插入。

        此时还没有什么问题,但当我们删除12元素后,_table[2]的状态该为什么呢?假如只有两种状态空和存在,我们只能标记为空,如下图,此时我们再删除22,就会发现一个问题,存不存在22?

        按照哈希函数的映射关系22应该在下标为2的地方,现在的现实是22的下标为3,在插入22时由于原先下标2的地方有元素,故22的下标向后移动,面临这种情况,我们只能把整个数组遍历一遍来确定存不存在22.不能遇到空位置就停止。

        当然上述的方法也能解决问题,但是删除的效率太低了,最坏的情况下要遍历一遍数组确定存不存在。我们有更好的解法,那就是增加一个状态删除。

        还是上面相同的问题,只不过此时格子的状态增加了一种。如下图。

        此时22通过哈希函数映射的下标为2,2的位置不为空,说明可能在后面,继续访问下一个位置,遇到22删除,并将状态标为DELETE。

        假如我们在上图的情况删除62,首先通过哈希函数映射的下标为2,2的位置不为空,说明可能在后面,继续访问下一个位置,存在元素但不为62,继续向下寻找,直到遇到下标为5的位置,状态为空,此时还需要往下遍历么?结果是不需要的,62不可能在后面,后面如果有插入哈希函数映射的下标为2的元素,那么下个位置的状态一定为存在或者删除,相同映射是连续的不可能出现跳跃的情况。

        相较于第一种做法,显然第二种做法更加的高效。

        基于上述的分析我们就可以完成删除函数了。


	bool erase(const V& key)
	{
		size_t hashi = key % _table.size();

		//当前位置是否为目标值,否就向后寻找
		while(_table[hashi]._status != EMPTY)
		{
			
			if(_table[hashi]._date == key)
			{
				//更改状态
				_table[hashi]._status = DELETE;
				return true;
			}

			hashi++;
			hashi %= _table.size();//防止越界
		}

		return false;
	}

删除测试

        

void test2()
{
	HashTable<int> ht;

	//int a[] = { 1,5,6,4,8,9,10 };
	int a[] = { 1,5,6,4,8,9,10,2};

	for (auto e : a)
		ht.insert(e);

	ht.erase(10);
	ht.CoutArr();

	ht.erase(8);
	ht.CoutArr();
}

        运行结果如下,符合预测,当然可以多测几组这里就不过多赘述了。

查找

        查找的核心思想和删除几乎一样,只不过删除要改变结点状态,查找不用。首先按照插入的思路先找到映射后的下标hashi,然后判断当前位置是否为目标值,否则就找下一个空位。直到找到目标值或者遇到EMPTY位置结束。

bool find(const V& key)
{
	size_t hashi = key % _table.size();

	while (_table[hashi]._status != EMPTY)
	{

		if (_table[hashi]._date == key)
		{
			return true;
		}

		hashi++;
		hashi %= _table.size();//防止越界
	}

	return false;
}

泛型模板

        我们通常建立的关系不只有整数到整数,还有字符到整数,甚至一些自定义类型到整数,当面对字符串的时候,我们在直接用key % _table.size()就会报错,字符串类也不支持取模操作,此时我们就需要用到仿函数。

        在STL提供的模板参数里面第二个就是自定义Hash函数

        于是我们可以在最开始的模板加上Hash类,接着就是实现默认Hash类

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

        这样对于一般的类型我们就可以应对了,此时在将原来key封装一层函数即可。如下图

        修改完后的代码如下。

#pragma once
#include<iostream>
#include<vector>
using namespace std;

//枚举出当前状态
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

template<class V>
struct date
{
	//默认初始化为空
	date()
		:_status(EMPTY),
		_date(V())
	{

	}

	State _status;
	V _date;
};


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




template<class V,class Hash=Hash<V>> //Value 存储的数据类型
class HashTable
{
public:
	//构造函数,初始化_n与_table
	HashTable(int n=10)
		:_n(0),
		_table(n)
	{
	}

	bool insert(const V& key)
	{
		Hash hs;
		resize(_n + 1);

		size_t hashi = hs(key) % _table.size();

		//当前位置已经插入元素,向后寻找合法位置
		while (_table[hashi]._status == EXIST)
		{
			hashi++;
			hashi %= _table.size();//防止越界
		}
		
		
		//当前位置为空或者向后寻找的新位置
		_table[hashi]._date = key;
		_table[hashi]._status = EXIST;
		_n++;

		return true;
	}

	bool resize(int n)
	{

		//乘10解决除法小数问题
		if (10 * n / _table.size() < 7)
		{
			return true;
		}
		//需要扩容
		HashTable<V> hs(2 * _table.size());
		//下面代码十分巧妙
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._status == EXIST)
			{
				//调用新对象的插入函数,复用代码
				//对于新hs对象,他的大小为原来两倍一定不会扩容,只会执行插入
				hs.insert(_table[i]._date);
			}
		}
		//将新hs对象_table与原来交换
		_table.swap(hs._table);
		return true;
	}

	bool erase(const V& key)
	{

		Hash hs;
		size_t hashi = hs(key) % _table.size();

		//当前位置是否为目标值,否就向后寻找
		while(_table[hashi]._status != EMPTY)
		{
			
			if(_table[hashi]._date == key)
			{
				//更改状态
				_table[hashi]._status = DELETE;
				return true;
			}

			hashi++;
			hashi %= _table.size();//防止越界
		}

		return false;
	}

	bool find(const V& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _table.size();

		while (_table[hashi]._status != EMPTY)
		{

			if (_table[hashi]._date == key)
			{
				return true;
			}

			hashi++;
			hashi %= _table.size();//防止越界
		}

		return false;
	}

	void CoutArr()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._status == EXIST)
			{
				cout <<"下标" << i << ":" << _table[i]._date << " ";
				cout << endl;
			}
			else
			{
				cout << "下标" << i << ":空";
				cout << endl;
			}
		}
		cout << endl;
		cout << endl;
	}
private:
	vector<date<V>> _table;
	size_t _n;//用于计算负载因子
};

        上述我们只是完成了基础的类型,对于特殊的类型强制转换显然是不可以的,我们可以在类实例化的始后传递第二个模板参数解决,如下测试代码。

struct HashString
{
	size_t operator()(const string & s)
	{
		size_t ret = 0;

		for (auto e : s)
			ret += e;

		return ret;
	}
};

void test3()
{
	HashTable<string, HashString> ht;

	ht.insert("avdvvdava");
	ht.insert("bddd");
	ht.insert("assa");

	ht.CoutArr();
}

        当编译时会报错,如下图。

       报错是当前行访问了私有成员,这十分奇怪,之前的测试用例都没出现这种报错。我们回顾下类的知识,私有成员只能在同类里面访问,不支持外部访问。

        按照报错看会一脸懵,同类调用怎么成了非法访问,我们来看下当前函数的类型。

        HashTable<V> hs(2 * _table.size());第二个参数采用默认,实际类型为HashTable<V,Hash<V>>

        当前调用insert类类型为HashTable<V,HashString>。

        相信大家看到这就明白了,他们的类型不一样!!将hs修改为如下即可,不采用默认,采用传进来的类,这样就可以保持完全相同了。

        

HashTable<V,Hash> hs(2 * _table.size());

       

        修改后运行结果如下,结果是正确的。

  迭代器

        当前遍历的迭代器可以复用底层vector的迭代器,两者效果都是一样的。

        由于哈希表++操作与Vector操作不同,我们可以采用创建类的方式,在类中将原来迭代器的++重载达到我们想要的效果

        结果如下。

        这些代码大家可以自己先写写,实现的核心思想不难,不过细节十分多!!一定要多加尝试。

template<class V,class Self>
struct  HashIterator
{
	typedef HashIterator<V, Self> Iterator;
	Self _t;
	typedef  decltype(_t.begin()) VectorIterator;
	VectorIterator _it;

	HashIterator(VectorIterator it, Self t)
		:_it(it),
		_t(t)
	{

	}

	bool operator != (HashIterator<V,Self>& ht )
	{
		return _it != ht._it;
	}

	Iterator operator++()
	{
		_it++;
		//向后寻找存在元素
		while (_it!=_t.end() && _it->_status != EXIST)
			_it++;
		return *this;
	}

	V& operator*()
	{
		return _it->_date;
	}

	V* operator->()
	{
		return &(_it->_date);
	}
};

        在主类中我们也只需要假如begin与end函数即可

template<class V,class Hash=Hash<V>> //Value 存储的数据类型
class HashTable
{
	typedef HashIterator<V,vector<date<V>>&> iterator;
public:

	iterator begin()
{
	auto it = _table.begin();
	while (it != _table.end() && it->_status != EXIST)
		it++;
	return iterator(it,_table);
}

iterator end()
{
	return iterator(_table.end(),_table);
}
}

        范围for底层用的就是迭代器,我们接着就可以用范围for来检测迭代器是否正常

        测试代码

void test4()
{
	HashTable<int> ht;
	int a[] = { 1,5,6,4,8,9,10,2,6,45 };

	for (auto e : a)
		ht.insert(e);
	ht.CoutArr();


	for (auto e : ht)
		cout << e << " ";
	cout << endl;

}

        运行结果如下图是没问题的。

 到目前位置源码

头文件

#pragma once
#include<iostream>
#include<vector>
using namespace std;

//枚举出当前状态
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

template<class V>
struct date
{
	//默认初始化为空
	date()
		:_status(EMPTY),
		_date(V())
	{

	}

	State _status;
	V _date;
};


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

template<class V,class Self>
struct  HashIterator
{
	//注意顺序,使用类型前要声明
	typedef HashIterator<V, Self> Iterator;
	Self _t;
	typedef  decltype(_t.begin()) VectorIterator;
	VectorIterator _it;

	HashIterator(VectorIterator it, Self t)
		:_it(it),
		_t(t)
	{

	}

	bool operator != (HashIterator<V,Self>& ht )
	{
		return _it != ht._it;
	}

	Iterator operator++()
	{
		_it++;
		//向后寻找存在元素
		while (_it!=_t.end() && _it->_status != EXIST)
			_it++;
		return *this;
	}

	V& operator*()
	{
		return _it->_date;
	}

	V* operator->()
	{
		return &(_it->_date);
	}
};


template<class V,class Hash=Hash<V>> //Value 存储的数据类型
class HashTable
{
	typedef HashIterator<V,vector<date<V>>&> iterator;
public:

	iterator begin()
	{
		auto it = _table.begin();
		while (it != _table.end() && it->_status != EXIST)
			it++;
		return iterator(it,_table);
	}

	iterator end()
	{
		return iterator(_table.end(),_table);
	}


	//构造函数,初始化_n与_table
	HashTable(int n=10)
		:_n(0),
		_table(n)
	{
	}

	bool insert(const V& key)
	{
		Hash hs;
		resize(_n + 1);

		size_t hashi = hs(key) % _table.size();

		//当前位置已经插入元素,向后寻找合法位置
		while (_table[hashi]._status == EXIST)
		{
			hashi++;
			hashi %= _table.size();//防止越界
		}
		
		
		//当前位置为空或者向后寻找的新位置
		_table[hashi]._date = key;
		_table[hashi]._status = EXIST;
		_n++;

		return true;
	}

	bool resize(int n)
	{

		//乘10解决除法小数问题
		if (10 * n / _table.size() < 7)
		{
			return true;
		}
		//需要扩容
		HashTable<V,Hash> hs(2 * _table.size());
		//下面代码十分巧妙
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._status == EXIST)
			{
				//调用新对象的插入函数,复用代码
				//对于新hs对象,他的大小为原来两倍一定不会扩容,只会执行插入
				hs.insert(_table[i]._date);
			}
		}
		//将新hs对象_table与原来交换
		_table.swap(hs._table);

		return true;
	}

	bool erase(const V& key)
	{

		Hash hs;
		size_t hashi = hs(key) % _table.size();

		//当前位置是否为目标值,否就向后寻找
		while(_table[hashi]._status != EMPTY)
		{
			
			if(_table[hashi]._date == key)
			{
				//更改状态
				_table[hashi]._status = DELETE;
				return true;
			}

			hashi++;
			hashi %= _table.size();//防止越界
		}

		return false;
	}

	bool find(const V& key)
	{
		Hash hs;
		size_t hashi = hs(key) % _table.size();

		while (_table[hashi]._status != EMPTY)
		{

			if (_table[hashi]._date == key)
			{
				return true;
			}

			hashi++;
			hashi %= _table.size();//防止越界
		}

		return false;
	}

	void CoutArr()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._status == EXIST)
			{
				cout <<"下标" << i << ":" << _table[i]._date << " ";
				cout << endl;
			}
			else
			{
				cout << "下标" << i << ":空";
				cout << endl;
			}
		}
		cout << endl;
		cout << endl;
	}

private:
	vector<date<V>> _table;
	size_t _n;//用于计算负载因子
};

源文件

#include"OpenHash.h"

void test1()
{
	HashTable<int> ht;

	//int a[] = { 1,5,6,4,8,9,10 };
	int a[] = { 1,5,6,4,8,9,10,2,6,45 };

	for (auto e : a)
		ht.insert(e);

	ht.CoutArr();
}

void test2()
{
	HashTable<int> ht;

	//int a[] = { 1,5,6,4,8,9,10 };
	int a[] = { 1,5,6,4,8,9,10,2};

	for (auto e : a)
		ht.insert(e);

	ht.erase(10);
	ht.CoutArr();

	ht.erase(8);
	ht.CoutArr();
}

struct HashString
{
	size_t operator()(const string & s)
	{
		size_t ret = 0;

		for (auto e : s)
			ret += e;

		return ret;
	}
};

void test3()
{
	HashTable<string, HashString> ht;

	ht.insert("avdvvdava");
	ht.insert("bddd");
	ht.insert("assa");

	ht.CoutArr();
}

void test4()
{
	HashTable<int> ht;
	int a[] = { 1,5,6,4,8,9,10,2,6,45 };

	for (auto e : a)
		ht.insert(e);
	ht.CoutArr();


	for (auto e : ht)
		cout << e << " ";
	cout << endl;

}
int main()
{
	test4();


	return 0;
}

开散列

        之前我们实现的是闭散列,当遇到哈希映射相同的时候就在列表中向后寻找下一个“空位置”,与之相反的还有一种做法,把原来每个结点换位链表,当遇到哈希映射相同的情况,就在对应哈希位置头插入结点,如下图。

        

        可以看出开散列与闭散列最主要的区别就是当出现哈希冲突时,采取的方法不一样,其余的都十分相似。

        首先我们可以定义出链表的结构体如下。

template<class V>
struct HashNode
{
	//采用缺省参数,可以在调用时简化操作
	HashNode(const V& val=V(), HashNode* next = nullptr)
		:_val(val),
		_next(next)
	{

	}

	V _val;
	HashNode* _next;
};

        对于HashTable的主体如下,通过闭散列的实现,我们可以提前写上Hash类,适配特殊类型。

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

template<class V, class Hash = Hash<V>> //Value 存储的数据类型
class HashTable
{
	typedef HashNode<V> Node;
public:
    //默认构造函数
HashTable(int n=10)
	:_n(0),
	_table(n)
{

}

private:
	vector<Node*> _table;
	size_t _n;//用于计算负载因子
};

插入

        接下来我们实现插入,开散列的插入与闭散列插入核心几乎一样,先通过哈希函数找到下标,在头插对应下标位置,加上判断扩容即可。

        插入就如下这么简单。

bool insert(const V& val)
{
	resize(_n + 1);
	//哈希算法
	Hash hs;

	size_t hashi = hs(val) % _table.size();
	Node* cur = new Node(val, _table[hashi]);
	_table[hashi] = cur;

	_n++;

	return true;
}

       

        

        扩容操作,我们可以像之前一样复用插入代码,最后交换vector即可。

bool resize(size_t n)
{
	//开散列负载因子可以达到0.9~1
	if (n < _table.size())
	{
		return true;
	}
	else
	{
		HashTable<V, Hash> ht(2*_table.size());

		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			//不为空遍历插入
			while (cur)
			{
				ht.insert(cur->_val);
				cur = cur->_next;
			}
		}
		//交换
		_table.swap(ht._table);
	}

}

测试

        同理为了测试方便,我们可以写个打印数组函数如下

	void CoutArr()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			if (cur == nullptr)
				cout << "第" << i << "个:空" << endl;
			else
			{
				cout << "第" << i << "个:";
				while (cur)
				{
					cout << cur->_val << " ";
					cur = cur->_next;
				}
                cout << endl;
			}
			
		}
		cout << endl;
		cout << endl;
	}

        测试代码

void test1()
{
	HashTable<int> ht;

	//int a[] = { 1,5,6,4,8,9,10 };
	int a[] = { 1,5,6,4,8,9,10,2,45,99 };

	for (auto e : a)
		ht.insert(e);

	ht.CoutArr();
}

        结果如下,符合预期。

  删除

        接下来实现删除操作,核心思想就是找到哈希映射的小白,遍历一遍找有无该元素,有删除,无返回false。

        

	bool erase(const V& val)
	{
		//哈希算法
		Hash hs;

		size_t hashi = hs(val) % _table.size();
		Node* cur = _table[hashi];
		Node* prev = nullptr;
		//不为空遍历寻找
		while (cur)
		{
			if (cur->_val == val)
			{
				if (cur == _table[hashi])
				{
					//头节点特殊处理
					delete cur;
					_table[hashi] = nullptr;
				}
				else
				{
					prev->_next = cur->_next;
					delete cur;
				}
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}

		return false;
	}

        

测试代码

void test2()
{
	HashTable<int> ht;

	int a[] = { 1,5,6,4,8,9,10,2,45,99 ,25,15};

	for (auto e : a)
		ht.insert(e);

	ht.erase(10);
	ht.CoutArr();

	ht.erase(5);
	ht.CoutArr();

	ht.erase(15);
	ht.CoutArr();
}

        我们还可以通过调试窗口观察,结果是一样的

查找

        查找的思想与删除异曲同工,先找到下标,遍历寻找。

bool find(const V& val)
{
	//哈希算法
	Hash hs;

	size_t hashi = hs(val) % _table.size();
	Node* cur = _table[hashi];
	//不为空遍历寻找
	while (cur)
	{
		if (cur->_val == val)
		{
			return true;
		}
		cur = cur->_next;
	}

	return false;
}

        在这里我们可以将erase完善些,我们实现的是不允许相同元素插入的版本,就可以在insert加上检测哈希表是否已有该元素,有则返回false。

bool insert(const V& val)
{
	resize(_n + 1);
	//已经存在则不插入
	if (find(val))
		return false;
	//哈希算法
	Hash hs;

	size_t hashi = hs(val) % _table.size();
	Node* cur = new Node(val, _table[hashi]);
	_table[hashi] = cur;

	_n++;

	return true;
}

        到这里核心就结束了,迭代器模块我们可以借鉴闭散表

迭代器

        由于单独的依靠指针++无法达到我们预期的结果,我们就可以将迭代器封装为一个类,进行操作符重载完成我们自定义的要求。

        在与原来的类中加上如下代码。

template<class V, class Hash = Hash<V>> //Value 存储的数据类型
class HashTable
{
	typedef HashNode<V> Node;
	typedef HashIterator<V, vector<Node*>&,Hash> iterator;
public:

	iterator begin()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i])
			{
				return iterator(_table[i], _table);
			}
		}

		return iterator(nullptr, _table);
	}

	iterator end()
	{
		return iterator(nullptr, _table);
	}
}

        迭代器主体,代码实现思想不难,但加上模板后有些绕,可以尝试多写几次。

template<class V, class Self,class Hash>
struct  HashIterator
{
	typedef HashNode<V> Node;
	typedef HashIterator<V, Self,Hash> Iterator;

	HashIterator(Node* cur, Self t)
		:_cur(cur),
		_table(t)
	{

	}

	bool operator != (Iterator& ht)
	{
		return _cur != ht._cur;
	}

	Iterator operator++()
	{

		//哈希算法
		Hash hs;
		size_t hashi = hs(_cur->_val) % _table.size();
		_cur = _cur->_next;

		if (_cur == nullptr)
		{
			//当前链表结束,跳转到下一个位置
			hashi++;
			for (int i = hashi; i < _table.size(); i++)
			{
				if (_table[i])
				{
					_cur = _table[i];
					break;
				}
			}
		}

		return *this;
	}


	//返回有效数据引用
	V& operator*()
	{
		return _cur->_val;
	}

	//返回有效数据地址
	V* operator->()
	{
		return &(_cur->_val);
	}

	Node* _cur;
	Self _table;
};

测试代码

        同理我们可以用如下代码测试迭代器。范围for底层就是用的迭代器。

void test4()
{
	HashTable<int> ht;
	int a[] = { 1,5,6,4,8,9,10,2,6,45 };

	for (auto e : a)
		ht.insert(e);
	ht.CoutArr();


	for (auto e : ht)
		cout << e << " ";
	cout << endl;

}

        运行结果如下

源码如下

头文件

#pragma once
#include<iostream>
#include<vector>
using namespace std;

template<class V>
struct HashNode
{
	//采用缺省参数,可以在调用时简化操作
	HashNode(const V& val=V(), HashNode* next = nullptr)
		:_val(val),
		_next(next)
	{

	}

	V _val;
	HashNode* _next;
};

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

template<class V, class Self,class Hash>
struct  HashIterator
{
	typedef HashNode<V> Node;
	typedef HashIterator<V, Self,Hash> Iterator;

	HashIterator(Node* cur, Self t)
		:_cur(cur),
		_table(t)
	{

	}

	bool operator != (Iterator& ht)
	{
		return _cur != ht._cur;
	}

	Iterator operator++()
	{

		//哈希算法
		Hash hs;
		size_t hashi = hs(_cur->_val) % _table.size();
		_cur = _cur->_next;

		if (_cur == nullptr)
		{
			//当前链表结束,跳转到下一个位置
			hashi++;
			for (int i = hashi; i < _table.size(); i++)
			{
				if (_table[i])
				{
					_cur = _table[i];
					break;
				}
			}
		}

		return *this;
	}


	//返回有效数据引用
	V& operator*()
	{
		return _cur->_val;
	}

	//返回有效数据地址
	V* operator->()
	{
		return &(_cur->_val);
	}

	Node* _cur;
	Self _table;
};

template<class V, class Hash = Hash<V>> //Value 存储的数据类型
class HashTable
{
	typedef HashNode<V> Node;
	typedef HashIterator<V, vector<Node*>&,Hash> iterator;
public:

	iterator begin()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i])
			{
				return iterator(_table[i], _table);
			}
		}

		return iterator(nullptr, _table);
	}

	iterator end()
	{
		return iterator(nullptr, _table);
	}

	//默认构造函数
	HashTable(int n=10)
		:_n(0),
		_table(n)
	{

	}


	bool insert(const V& val)
	{
		resize(_n + 1);
		//已经存在则不插入
		if (find(val))
			return false;
		//哈希算法
		Hash hs;

		size_t hashi = hs(val) % _table.size();
		Node* cur = new Node(val, _table[hashi]);
		_table[hashi] = cur;

		_n++;

		return true;
	}

	bool resize(size_t n)
	{
		//开散列负载因子可以达到0.9~1
		if (n < _table.size())
		{
			return true;
		}
		else
		{
			HashTable<V, Hash> ht(2*_table.size());

			for (int i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				//不为空遍历插入
				while (cur)
				{
					ht.insert(cur->_val);
					cur = cur->_next;
				}
			}
			//交换
			_table.swap(ht._table);
		}

	}

	bool erase(const V& val)
	{
		//哈希算法
		Hash hs;

		size_t hashi = hs(val) % _table.size();
		Node* cur = _table[hashi];
		Node* prev = nullptr;
		//不为空遍历寻找
		while (cur)
		{
			if (cur->_val == val)
			{
				if (cur == _table[hashi])
				{
					//头节点特殊处理
					delete cur;
					_table[hashi] = nullptr;
				}
				else
				{
					prev->_next = cur->_next;
					delete cur;
				}
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->_next;
		}

		return false;
	}


	bool find(const V& val)
	{
		//哈希算法
		Hash hs;

		size_t hashi = hs(val) % _table.size();
		Node* cur = _table[hashi];
		//不为空遍历寻找
		while (cur)
		{
			if (cur->_val == val)
			{
				return true;
			}
			cur = cur->_next;
		}

		return false;
	}
	void CoutArr()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			if (cur == nullptr)
				cout << "第" << i << "个:空" << endl;
			else
			{
				cout << "第" << i << "个:";
				while (cur)
				{
					cout << cur->_val << " ";
					cur = cur->_next;
				}
				cout << endl;
			}
			
		}
		cout << endl;
		cout << endl;
	}
private:
	vector<Node*> _table;
	size_t _n;//用于计算负载因子
};

源文件

#include"CloseHash.h"
void test1()
{
	HashTable<int> ht;

	//int a[] = { 1,5,6,4,8,9,10 };
	int a[] = { 1,5,6,4,8,9,10,2,45,99 };

	for (auto e : a)
		ht.insert(e);

	ht.CoutArr();
}

void test2()
{
	HashTable<int> ht;

	int a[] = { 1,5,6,4,8,9,10,2,45,99 ,25,15};

	for (auto e : a)
		ht.insert(e);

	ht.erase(10);
	ht.CoutArr();

	ht.erase(5);
	ht.CoutArr();

	ht.erase(15);
	ht.CoutArr();
}

void test4()
{
	HashTable<int> ht;
	int a[] = { 1,5,6,4,8,9,10,2,6,45 };

	for (auto e : a)
		ht.insert(e);
	ht.CoutArr();


	for (auto e : ht)
		cout << e << " ";
	cout << endl;

}
int main()
{
	test4();


	return 0;
}

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

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

相关文章

Docker③_VMware虚拟机和Docker的备份与恢复

目录 1. VMware虚拟机的快照备份 1.1 VMware本机的快照备份 1.2 VMware快照备份到另一电脑 2. Docker知识点 2.1 Docker镜像和容器的关系 2.2 Docker的存储卷 2.3 Docker命令简介 2.4 删除Anylink镜像 3. Docker备份和恢复 3.1 确定要回滚的容器和版本 3.2 备份当前…

【C语言-扫雷游戏】mineweeper【未完成】

编程小白如何成为大神&#xff1f;大学新生的最佳入门攻略 编程已成为当代大学生的必备技能&#xff0c;但面对众多编程语言和学习资源&#xff0c;新生们常常感到迷茫。如何选择适合自己的编程语言&#xff1f;如何制定有效的学习计划&#xff1f;如何避免常见的学习陷阱&…

psychopy stroop 实验设计

斯特鲁stroop实验就是色词一致/不一致实验。 设计步骤如下&#xff1a; 1. 先去设置中将Input改为PsychToolbox&#xff0c; 2. 然后左上角File-New新建一个 3. 右键trial&#xff0c;rename改名 改成自己想要的名字即可&#xff0c;比如欢迎界面welcome。 4. 接下来添加提示语…

老阳推荐的temu选品师项目能不能做成?

在不断变化的电商领域&#xff0c;temU选品师项目作为一种新兴职业&#xff0c;受到了越来越多的关注。老阳的推荐使得这一项目引起了不少人的兴趣&#xff0c;那么&#xff0c;temU选品师项目究竟能否成功呢?让我们从一个新的角度来探讨这一问题。 新兴市场的机遇与挑战 temU…

C语言 ——— 写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串

目录 题目要求 代码思路 代码实现 题目要求 写一个函数&#xff0c;判断一个字符串是否为另外一个字符串旋转之后的字符串 例如 s1 "AABCD" &#xff1b;s2 "BCDAA" &#xff0c;返回1 s1 "AABcd" &#xff1b;s2 "BCDAA" …

免费分享一套SpringBoot+Vue仓库(进销存)管理系统【论文+源码+SQL脚本】,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue仓库(进销存)管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue仓库(进销存)管理系统 Java毕业设计_哔哩哔哩_bilibili 项目介绍 该系统的设计初衷是解决传统仓库管理中存…

高可用集群KEEPALIVED

文章目录 高可用集群KEEPALIVEDVRRPkeepalived 部署环境准备 开启通信功能设置独立日志设置独立子配置文件抢占模式和非抢占模式延迟抢占非抢占模式 单播模式邮箱邮件通知脚本 双主架构实现ipvs的高可用性lvs-dr VRRP Script实现HAProxy高可用 文章相关连接如下&#xff1a; 如…

力扣 | 动态规划 | 在字符串的应用 | 最长回文子串、最长回文子序列、单词拆分、编辑距离

文章目录 1.最长回文子串2.最长回文子序列3.单词拆分4.编辑距离5. 共同点和思路6. 各个问题的思路和扩展1. 最长回文子串2. 最长回文子序列3. 单词拆分4. 编辑距离 在解答字符串动态规划的应用时&#xff0c;我们需要非常注意一个问题&#xff1a;   有时候我们定义 d p [ i …

MoonBit 周报 Vol.53:新增高级循环语法、引入字符串插值、MoonBit AI 支持代码解释!

weekly 2024-08-05 MoonBit更新 添加了基于 Iter 和 Iter2 类型的 for .. in 循环支持&#xff1a; fn main {for x in [ 1, 2, 3 ] {println(x)}for k, v in { "x": 1, "y": 2 } {println("\{k} > \{v}")} }for 与 in 之间可以使用 1&…

事务及事务的控制.特性

一.事务 1.事务定义 ​ 逻辑上多个DML操作形成的一个整体,多个DML操作要么全都执行成功,要么全都执行失败,如果是DDL的操作会自动提交事务. 2.事务的控制 命令 start transaction开启事务rollback 回滚commit提交事务 mysql默认自动提交事务,每个DML执行完毕后,直接提交事…

【Linux】编译器gcc/g++ 、程序翻译过程、动静态库

目录 1.gcc/g Linux编译器1.1. gcc与g的安装1.2. gcc与g用法1.2.1.gcc用法1.2.2. g用法 1.3. 程序翻译的过程1.3.1. 前提知识&#xff1a;1.3.2. 预处理&#xff08;语言种类不变&#xff09;条件编译用途&#xff1a; 1.3.3. 编译&#xff08;生成汇编语言&#xff09;1.3.4. …

性能测试工具之JMeter

JMeter Apache JMeter应用程序是开源软件,是一个100%纯Java应用程序,旨在负载测试功能行为和衡量性能。它最初是为测试Web应用程序而设计的,但后来扩展到其他测试功能。 JMeter是一个免费、开源、跨平台的性能测试工具,于20世纪90年代后期面世。这是一个成熟、健全且具有…

‘Task‘ object is not callable ERROR

pycharm 调试异步的代码报错 TypeError: ‘Task‘ object is not callable ERROR: Exception in callback &#xff1c;Task 解决方法&#xff1a;点击菜单栏帮助-查找操作-注册表&#xff0c;在注册表中搜索python.debug.asyncio.repl禁用即可

卷大模型,还是卷应用?一次看明白

自从ChatGPT横空出世以来&#xff0c;中美之间围绕大模型的科技竞争愈演愈烈&#xff0c;也渐渐分化出两条差异化发展路线&#xff1a;一派侧重将AI能力投入应用场景&#xff0c;另一派则侧重让基础模型能力更强。于是&#xff0c;“卷应用”还是“卷大模型”成为中国许多新入场…

phpmailer如何配置SSL以发送安全电子邮件?

phpmailer支持哪些邮件附件类型&#xff1f;如何使用phpmailer&#xff1f; PHPMailer作为一个广泛使用的PHP邮件发送类库&#xff0c;支持通过SSL发送加密邮件&#xff0c;以确保信息在传输过程中的安全性。AokSend将探讨如何配置PHPMailer以使用SSL发送安全的电子邮件。 Ph…

M12防水分线盒双通道PNP型8路预铸线缆半导体制造设备

M12防水分线盒双通道PNP型8路预铸线缆&#xff0c;作为工业自动化设备中关键的组件之一&#xff0c;在提升系统稳定性与可靠性方面发挥着至关重要的作用。以钡铼技术DB系列为例&#xff0c;这款8路M12双通道预铸线缆分线盒在设计和材料选择上均体现了工业级的高标准&#xff0c…

解决方案:如何在虚拟机中扩展 Windows 11 C 盘,绕过恢复分区的限制!

第2章 背景 不管是虚拟化还是物理机&#xff0c;我们安装Windows 11 时候经常遇到无法直接扩容盘&#xff0c;我们简单介绍下Windows中C盘组成。 ### 恢复分区的目的 恢复分区是 Windows 操作系统安装过程中创建的一个特殊分区&#xff0c;主要用于以下目的&#xff1a; 1. …

如何修复 VLC 无法播放 MKV 问题

VLC 是一个媒体播放器&#xff0c;您可以用它来播放各种格式的音频或视频文件。但有时&#xff0c;您无法在 VLC 媒体播放器上运行 MKV 文件。问题可能是黑屏、没有声音、视频断断续续或错误消息&#xff08;例如“无法识别的编解码器&#xff1a;VLC 无法识别音频或视频编解码…

自助创建 1Panel 应用

自助创建 1Panel 应用 前言 1Panel 作为一款开源的 Linux 服务器运维管理面板&#xff0c;其优质的 应用商店 想必也是很多人喜爱它的原因&#xff0c;除了官方的 应用列表 &#xff0c;开源社区内也涌现出了许多优质的第三方应用商店资源&#xff0c;比如 okxlin/appstore 等…

【MySQL】什么是索引?了解索引的底层原理

索引的概念 索引是一种用于提高数据库查询效率的数据结构。它类似于书籍的目录&#xff0c;通过快速定位数据的方式&#xff0c;减少了数据检索的时间。索引在数据库表中可以被看作是一个指向数据的指针&#xff0c;它们存储了列的值及其对应行的位置&#xff0c;从而使得数据…