C++ 哈希底层原理

哈希的概念

       哈希是一种建立映射的思想,我们常用的数据结构是哈希表 ,又称「散列表」,其通过建立键 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]++即可,原来a映射下表为97,此时就变为0,这样就可以将小写字母与数组下标建立映射关系。这样我们查询数组中一个元素,例如‘b’,只需要访问check[b-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,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.数组扩容

        对于扩容操作我们可以单独写一个resize在最后实现,先实现第一个要点

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元素的时候,如下连续图

        最后在正常插入即可。到此我们插入函数基本就结束了。

插入扩展

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

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

        当然除了上述两种方法还有很多寻找空位置的方法,在这里就不一一阐述了。上述线性探测插入也可改为二次探测。

       

删除

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

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

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

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

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

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

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

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

        相较于第一种做法,显然第二种做法更加的高效,我们无需遍历一遍数组即可完成删除。

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


	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操作不同,我们可以采用创建类的方式,在类中将原来迭代器的++重载达到我们想要的效果。

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

        在主类中我们也只需要假如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);
}
}

        封装迭代器类,其中重载*得到的是想要处理的数据的引用,重载->得到的是想要处理的数据的地址。

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);
	}
};

       对于测试我们可以看当前类支不支持范围for,范围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();
}

        结果如下,符合预期。

  删除

        接下来实现删除操作,核心思想就是找到哈希映射的下标,遍历一遍当前hashi位置的链表,找有无该元素,有删除,无返回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;
}

        到这里就全部结束了,如果有错误欢迎在评论区指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值