【C++修炼之路】22.哈希

news2024/11/24 15:30:01

在这里插入图片描述
每一个不曾起舞的日子都是对生命的辜负

哈希

  • 一.哈希概念及性质
    • 1.1 哈希概念
    • 1.2 哈希冲突
    • 1.3 哈希函数
  • 二.哈希冲突解决
    • 2.1 闭散列/开放定址法
    • 2.2 开散列/哈希桶
  • 三.开放定址法代码
    • 3.1 插入Insert
    • 3.2 查找Find
    • 3.3 删除Erase
    • 3.4 映射的改良&完整代码
  • 四.开散列代码
    • 4.1 插入Insert
    • 4.2 查找Find
    • 4.3 删除Erase
    • 4.4 完整代码&统计水果次数
  • 五.扩容机制

一.哈希概念及性质

1.1 哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素

    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

  • 搜索元素

    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)


例如:数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

image-20230221140003931

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

1.2 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==
Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?

1.3 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见哈希函数

1. 直接定址法–(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

一旦出现类似于这种数组:[1,2,3,100,10000],此时就会浪费很多比毕业的空间,因此一旦出现这种情况,我们采用下面这种方法:

2. 除留余数法–(常用)

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

因此,对于上面的数组来说,我们就可以采用%10的方式,将其固定在一定大小的范围之内,从而降低空间上的浪费。但此时,就会发生上面所说的哈希冲突,即对于100,10000,这两个数字%10之后都是0,因此一旦先将100放在了0的位置,那么10000就会发生哈希冲突。

二.哈希冲突解决

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

2.1 闭散列/开放定址法

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

1. 线性探测

比如1.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

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

image-20230221142456535

  • 删除

    采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。即我们可以采用标记的方式将没有插入过的地方标记为EMPTY,存在数据的地方标记位EXIST

  • 查找

    那如果想要具体查找某一个元素,就可以从指定映射的地方直接找,我们知道解决哈希冲突时会往后插入,因此我们会继续往后找,如果找了一圈没有找到,那一定有一个位置代表终止位置,即EMPTY的位置,但是如果将之前的元素删除并标记EMPTY的话,那这个位置之后实际上还有数据可以查找,这样也就不能用这个表示终止位置了,为了能够满足终止位置,我们再设定一个状态,将存在的数据并删除的位置标记位DELETE,这样就可以通过EMPTY来终止查找了。

接下来直接看三.开放定址法代码中的代码实现->

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


2.二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

image-20230222134147248

对于1.1中如果要插入44,产生冲突,使用解决后的情况为:

image-20230222134407711

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

为了让映射的元素不互相影响,开散列/哈希桶的方式也就与此诞生:

2.2 开散列/哈希桶

1. 开散列概念

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

数组为指针数组:

image-20230222134638088

image-20230222134646011

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。C++库中就是用的这种方式。

指针数组也需要一定的扩容,不然空间为10,而有1000个数据,每个桶就需要挂100个数据,事实上比单链表好不了多少。所以在扩容时同样需要负载因子,不过对于演示来说,负载因子控制在1就好了。相比开放地址法,哈希桶的方式能让负载因子上升到很高的比例。

三.开放定址法代码

将除成员函数之外封装放在这里,这样设计是为了更具观赏性。

HashTable.h

enum State
{
	EMPTY,
	EXIST,
	DELETE,
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;//默认给一个缺省值
};


template<class K, class V>
class HaskTable
{
	typedef HashData<K, V> Data;
public:
    //插入、查找、删除功能
private:
	vector<Data> _tables;
	size_t _n;//表中存储的有效数据的个数
};

直接使用vector会更加便捷,这也是一种很好的手段。

3.1 插入Insert

对于插入来说,将采用线性探测法,即如果发生冲突,就往后++遍历找到空的位置,那此时同样会发生一个问题,如果空间不够了,需要扩容,对于哈希来说,并不能等到空间满了才扩容,一旦到达某个程度,就会因哈希冲突的不断发生造成效率的低下,此时为了解决这种问题,就有了负载因子/载荷因子,即 负载因子 = 表中有效数据的个数/表的大小

  • 负载因子越小,冲突概率越小,消耗空间越多。
  • 负载因子越大,冲突概率越大,空间的利用率越高。

可以看出,负载因子的出现正是以空间换时间的做法。一般将负载因子控制在0.7左右的样子,超过了这个值就需要扩容了。


对于扩容的过程,实际上随着空间大小的改变,取模%的大小也发生变化,这就造成数据原表的的位置可能与新表的位置寅映射关系的改变而变得不一样,事实上这并非是坏事,或许还会一定程度的减少哈希冲突。

因此一般的写法就是在写出一个 vector<Data> newTable,然后重复下面的逻辑,但是有一个更好的现代写法,重新定义一个类的对象也就是新的哈希表,将旧值通过类的方法重新Insert到新表中,最后这两个表的值进行交换,这样就不用重复写冲突的逻辑了。

注意:是新对象调用的Insert,和递归无关。

当然,一开始会出现除0错误,通过在内部重新写构造函数,直接resize()一个非0的数,就可以避免这个问题了。

那看看类中Insert的代码吧:结点之类的代码没有写,以免代码看起来太多,在代码案例开始已经写过。

template<class K, class V>
class HashTable
{
	typedef HashData<K, V> Data;
public:
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}

	bool Insert(const pair<K, V>& kv)
	{
		//大于标定负载因子,就需要扩容
		if (_n * 10 / _tables.size() > 7)//这样处理了小数为0
		{
			旧表数据,重新计算,映射到新表
			//vector<Data> newTable;
			//newTable.resize(_tables.size() * 2);
			//for()
			
			//现代写法
			HashTable<K, V> newHT;
			newHT._tables.resize(_tables.size() * 2);
			for (auto& e : _tables)
			{
				newHT.Insert(e._kv);
			}

			_tables.swap(newHT._tables);
		}

		//因为需要满足vector的operator[],因此%size而不是capacity
		size_t hashi = kv.first % _tables.size();
		while (_tables[hashi]._state == EXIST)
		{
			//线性探测
			++hashi;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
        
        //如果发现插入的值重复就应该返回false,但由于这里还没有讲解find函数,因此直接返回true也没什么问题,后续将会有完整代码
        return true;
	}
    
private:
	vector<Data> _tables;
	size_t _n = 0;//表中存储的有效数据的个数
};

测试一下:

void TestHT1()
{
	HashTable<int, int> ht;
	int a[] = { 18, 8, 7, 27, 57, 3, 38 };
	for (auto& e : a)
	{
		ht.Insert(make_pair(e, e));
	}
}

image-20230221182926665

image-20230221184046771

3.2 查找Find

Data* Find(const K& key)
{
    size_t hashi = key % _tables.size();
    while (_tables[hashi]._state != EMPTY)
    {
        if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
        {
            return &_tables[hashi];
        }
        ++hashi;
        hashi %= _tables.size();
    }
    return nullptr;
}

返回类型为Data*可以更好的删除,下面看看为什么:

3.3 删除Erase

bool Erase(const K& key)
{
    Data* ret = Find(key);
    if (ret)
    {
        ret->_state = DELETE;
        --_n;
        return true;
    }
    return false;
}

可以发现,通过Find的返回值就可以将删除数据的_state变为DELETE,事实上这是一种伪删除的方式,不过用的恰到好处。

3.4 映射的改良&完整代码

仍然拿出统计水果出现的次数为背景引出问题:

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "苹果", "西瓜" , "苹果", "苹果",
		"西瓜","苹果","香蕉", "苹果","香蕉" };
	HashTable<string, int> countHT;
	for (auto& e : arr)
	{
		HashData<string, int>* ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.Insert(make_pair(e, 1));
		}
	}

}

对于这段代码来说,看不出有什么问题,那试着编译一下:image-20230221222756990

可以发现,我们之前解决哈希冲突的方式为线性探测中的除留余数法,这种方式无法对字符串进行取模,因此出现错误。那字符串转整形怎么转?并且还是汉字,汉字实际上就是由多个字母构成的。

解决方式->仿函数

通过仿函数的方式就可以将类型在映射时将string类型成功转换。在所有取模的地方都加上仿函数对象,就可以通过我们自定义的映射方式解决,即:

enum State
{
	EMPTY,
	EXIST,
	DELETE,
};

//仿函数:解决s映射问题,完全没有关联的类型不能随便转,这个不能string转整形,因此还需要写一个
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//这个是针对string类型的仿函数
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		//BKDR
		size_t hash = 0;
		for (auto& ch : key)
		{
			hash *= 131;
			hash += ch;
		}
	    return hash;//这样映射不易产生哈希冲突
	}
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;//默认给一个缺省值
};


template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashData<K, V> Data;
public:
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}

	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))//已经有了,就不需要插入了
		{
			return false;
		}
		//大于标定负载因子,就需要扩容
		if (_n * 10 / _tables.size() > 7)//这样处理了小数为0
		{
			旧表数据,重新计算,映射到新表
			//vector<Data> newTable;
			//newTable.resize(_tables.size() * 2);
			//for()

			//现代写法
			HashTable<K, V, Hash> newHT;//定义对象多了一个仿函数参数
			newHT._tables.resize(_tables.size() * 2);
			for (auto& e : _tables)
			{
				newHT.Insert(e._kv);
			}

			_tables.swap(newHT._tables);
		}

		Hash hf;//取模的地方,用定义的仿函数对象封装解决
		size_t hashi = hf(kv.first) % _tables.size();//因为需要满足vector的operator[],因此%size而不是capacity
		while (_tables[hashi]._state == EXIST)
		{
			//线性探测
			++hashi;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;

		return true;
	}

	Data* Find(const K& key)
	{
		Hash hf;//仿函数:把key转化成可以取模的整形
		size_t hashi = hf(key) % _tables.size();
		while (_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];
			}
			++hashi;
			hashi %= _tables.size();
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		Data* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}

private:
	vector<Data> _tables;
	size_t _n = 0;//表中存储的有效数据的个数
};

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "苹果", "西瓜" , "苹果", "苹果",
		"西瓜","苹果","香蕉", "苹果","香蕉" };
	HashTable<string, int, HashFunc<string>> countHT;
	for (auto& e : arr)
	{
		HashData<string, int>* ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.Insert(make_pair(e, 1));
		}
	}
}

image-20230221230659065

如果映射的是vector<string>甚至是vector<vector<string>>,或者是一个利他的类映射成整形,同样需要配套一个仿函数,这就体现了仿函数的灵活比较。

因此对于unordered_map,通过观察同样发现,其就利用了哈希的仿函数进行映射,在使用unordered_map时,我们一般传入两个参数,第三个有缺省值,对于string类型等还有模板的特化,因此在调用库中的unordered_map也无需自己设计仿函数。image-20230222125624866

此外:对于map和unordered_map除了底层的区别,还有就是map是比较的方式找值,而unordered_map是通过指定的算法将传入的数据转成整形再映射。

对于我们设计的Hash表,实际上也不需要写默认的六大成员函数,因为vector作为自定义类型会调用自己内置的析构,对于size_t这种内置类型也不用处理。

四.开散列代码

将除成员函数之外封装放在这里,这样设计同样是为了更具观赏性。

template<class K, class V>
struct HashNode
{
	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:
	//插入、查找、删除

private:
	vector<Node*> _tables;//指针数组
	size_t _n;
};

4.1 插入Insert

插入时由于是链表,所以头插无疑是最好的方式。此外,对于指针数组来说,如果达到一定的限度,同样需要扩容,负载因子可以根据所需的数量从而控制在一定范围内,在这里负载因子以1为例。

与闭散列不同的是,因为是链表结构,默认生成的析构函数不能将空间全部释放,所以我们需要自己写一个析构函数将链表节点的空间释放。

开散列扩容的问题

对于哈希桶这种结构,扩容意味着重新开辟空间将旧表数据映射到新表,需要注意的是,不能直接一串一串的复制,因为由于新表的空间变大,因此取模时的映射关系也会变话,直接成串复制会导致映射关系发生错误进而在Find时找不到对应位置。所以还是要像正常扩容一样,把旧表的数据都遍历一遍映射拷贝到新表。与闭散列一样的扩容方式:

// 负载因子控制在1,超过就扩容
if (_tables.size() == _n)
{
    HashTable<K, V, Hash> newHT;
    newHT._tables.resize(_tables.size() * 2);
    for (auto cur : _tables)
    {
        while (cur)
        {
            newHT.Insert(cur->_kv);
            cur = cur->_next;
        }
    }

    _tables.swap(newHT._tables);
}

对于这种扩容方式,实际上很浪费空间,因其在插入过程中都是在拷贝节点,在一定程度上浪费了空间,并且在析构时会析构两次,又是一次性能的缺失。本着能优化尽量优化的思想,事实上,我们可以将旧表中的结点头插到新表指定的映射位置,这样就不需要拷贝创建新节点,但这样需要注意的是:要将旧表的每一个元素:_tables[i]清理掉或者都置nullptr,防止同一块空间析构两次造成浅拷贝。因此,改善之后的扩容及插入代码如下:

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
		return false;

	// 负载因子控制在1,超过就扩容
	if (_tables.size() == _n)
	{
		vector<Node*> newTables;
		newTables.resize(2 * _tables.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()(cur->_kv.first) % newTables.size();
				//头插到新表
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;

				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

	size_t hashi = Hash()(kv.first) % _tables.size();
	// 头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;

	return true;
}

4.2 查找Find

和开放定址法一样:

Node* Find(const K& key)
{
    size_t hashi = Hash()(key) % _tables.size();
    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            return cur;
        }
        else
        {
            cur = cur->_next;
        }
    }
    return nullptr;
}

4.3 删除Erase

bool Erase(const K& key)
{
    size_t hashi = Hash()(key) % _tables.size();
    Node* prev = nullptr;
    Node* cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr)
            {
                _tables[hashi] = cur->_next;
            }
            else
            {
                prev->_next = cur->_next;
            }
            delete cur;
            --_n;
            return true;
        }
        else
        {
            prev = cur;
            cur = cur->_next;
        }
    }
    return false;
}

4.4 完整代码&统计水果次数

//仿函数:解决s映射问题,完全没有关联的类型不能随便转,这个不能string转整形,因此还需要写一个
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)
	{
		//BKDR-Hash
		size_t hash = 0;
		for (auto& ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;//这样映射不易产生哈希冲突
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	HashTable()
		:_n(0)
	{
		_tables.resize(10);
	}
	~HashTable()
	{
		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;
		}
	}

	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
			return false;

		// 负载因子控制在1,超过就扩容
		if (_tables.size() == _n)
		{
			vector<Node*> newTables;
			newTables.resize(2 * _tables.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()(cur->_kv.first) % newTables.size();
					//头插到新表
					cur->_next = newTables[hashi];
					newTables[hashi] = cur;

					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		size_t hashi = Hash()(kv.first) % _tables.size();
		// 头插
		Node* newnode = new Node(kv);
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;
		++_n;

		return true;
	}
	Node* Find(const K& key)
	{
		size_t hashi = Hash()(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				return cur;
			}
			else
			{
				cur = cur->_next;
			}
		}
		return nullptr;
	}
	bool Erase(const K& key)
	{
		size_t hashi = Hash()(key) % _tables.size();
		Node* prev = nullptr;
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (cur->_kv.first == key)
			{
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
				delete cur;
				--_n;
				return true;
			}
			else
			{
				prev = cur;
				cur = cur->_next;
			}
		}
		return false;
	}
private:
	vector<Node*> _tables; //指针数组
	size_t _n = 0;
};

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "苹果", "西瓜" , "苹果", "苹果",
		"西瓜","苹果","香蕉", "苹果","香蕉" };
	//HashTable<string, int, HashFuncString> countHT;
	HashTable<string, int, HashFunc<string>> countHT;
	for (auto& e : arr)
	{
		auto ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.Insert(make_pair(e, 1));
		}
	}
}

image-20230225180120092

五.扩容机制

对于哈希来说,源码中采用了哈希容量为奇数,这样或许可以在取模的时候更加的分散,缓解冲突但并不能完全解决,极端场景仍然没有办法。下面改善一下扩容机制,当然这种方式也是可有可无的。

if (_tables.size() == _n)//扩容
{
    vector<Node*> newTables;
    //newTables.resize(2 * _tables.size(), nullptr);
    newTables.resize(__stl_next_prime(_tables.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()(kot(cur->_data)) % newTables.size();
            // 头插到新表
            cur->_next = newTables[hashi];
            newTables[hashi] = cur;

            cur = next;
        }

        _tables[i] = nullptr;
    }

    _tables.swap(newTables);
			
}

inline unsigned long __stl_next_prime(unsigned long n)
{
    static const int __stl_num_primes = 28;
    static const unsigned long __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 (int i = 0; i < __stl_num_primes; ++i)
    {
        if (__stl_prime_list[i] > n)
        {
            return __stl_prime_list[i];
        }
    }

    return __stl_prime_list[__stl_num_primes - 1];
}

当然,一旦数据过于驳杂,哈希桶挂的单链表改成红黑树是一个很好的解决方式。

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

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

相关文章

Linux之init.d、rc.d文件夹说明

备注&#xff1a;Ubuntu没有rc.d文件夹&#xff0c;原因看问题四 Linux的几个重要文件 rc.d&#xff0c;init.d文件夹的说明 今天在研究mysql的安装的时候&#xff0c;最后一步要创建一个软连接&#xff0c;使得mysql服务可以自启动&#xff0c;代码如下&#xff1a; ln -s…

扒系统CR8记录

目录 终极改造目标 过程记录 参考 为了将一套在线安装的系统&#xff0c;在不了解其架构、各模块细节的基础上&#xff0c;进行扒弄清楚&#xff0c;作以下记录。 终极改造目标 最终的目标&#xff0c;就是只通过CreMedia8_20230207.tar.gz解压 install 就把业务包安装了&…

CorelDRAW2023新功能有哪些?最新版cdr下载安装教程

使用 CorelDRAW2023&#xff0c;随时随都能进行设计创作。在 Windows或Mac上使用专为此平台设计的直观界面&#xff0c;以自己的风格尽情自由创作。同全球数百万信赖CorelDRAW Graphics Suite 的艺术家、设计者及小型企业主一样&#xff0c;大胆展现真我&#xff0c;创作出众的…

IAP初探

IAP(In-Application Programming)在应用编程&#xff0c;浅显易懂&#xff0c;按照字面意思即是在程序不关闭情况下&#xff0c;对应用进行再次写入程序&#xff0c;对程序的写入需要传输数据&#xff0c;而传输数据的前提是通信&#xff0c; IAP对代码进行更新可以简要分为以…

【ElasticSearch系列-01】初识以及安装elasticSearch

elasticSearch入门和安装一&#xff0c;elasticSearch入门1&#xff0c;什么是elasticSearch2&#xff0c;elasticSearch的底层优点2.1&#xff0c;全文检索2.2&#xff0c;倒排索引2.2.1&#xff0c;正排索引2.2.2&#xff0c;倒排索引2.2.3&#xff0c;倒排索引解决的问题2.2…

Linux | 2. 用户管理

如有错误&#xff0c;恳请指出。 1. 设置文件权限 权限设置如下&#xff1a; root表示文件所有者&#xff0c;stud1表示文件所属组。其他用户无法访问。更改指令是chown。 更改目录文件所属组&#xff1a;chown .lab lossfound/更改目录文件所有者&#xff1a;chown lab loss…

mac安装 Termius

1.下载安装包 链接: https://pan.baidu.com/s/1f5xmvYnVehCkMUD291SbsA?pwdy43k 提取码: y43k 2.打开系统偏好设置 -> 安全性与隐私 -> 通用&#xff0c;勾选“任何来源” 显示文件损坏的情况下执行下面操作 3.打开terminal终端 3.1 输入&#xff1a;sudo spctl --m…

windows10使用wsl2安装docker

配环境很麻烦&#xff0c;想利用docker的镜像环境跑一下代码整个安装过程的原理是&#xff1a;windows使用docker&#xff0c;必须先安装一个linux虚拟机&#xff0c;才可运行docker&#xff0c;而采用wsl2安装虚拟机是目前最好的方法第一步 windows安装wsl2控制面板->程序-…

数据结构-考研难点代码突破(C++实现树型查找 - B树插入与遍历,B+树基本概念)

数据结构&#xff08;C&#xff09;[B树&#xff08;B-树&#xff09;插入与中序遍历&#xff0c;效率分析]、B树、B*树、B树系列应用 文章目录1. B树B树的插入与删除流程2. B树&#xff08;MySQL&#xff09;3. B树与B树对比4. C实现B树插入&#xff0c;中序遍历1. B树 B树类…

面试题(基础篇)

1、你是怎样理解OOP面向对象的面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征&#xff1a;&#xff08;1&#xff09;继承&#xff1a;继承是从已有类得到继承信息创建新类的过程&#xff08;2&#xff09;封装&#xff1a;通常认为封装是把数据和操作数据的方法…

vue中的$forceUpdate()、$set()

$forceUpdate() 迫使vue实例重新&#xff08;rander&#xff09;渲染虚拟dom&#xff0c;注意并不是重新加载组件。 结合vue的生命周期&#xff0c;调用 $forceupdate 后只会触发beforeupdate和updated这两个钩子函数&#xff0c;不会触发其他的钩子函数。它仅仅影响实例本身和…

作为一名Android车载工程师,需要具备哪些能力?

安卓开发在近几年的就业环境大家肯定都听说过&#xff0c;许多Android开发程序员都找不到自己满意的工作&#xff0c;于是纷纷另谋出路… 如今&#xff0c;随着Android汽车开发的兴起&#xff0c;很多Android开发者想转行做Android车载开发。然而&#xff0c;Android车载开发不…

深入理解border以及应用

深入border属性以及应用&#x1f44f;&#x1f44f; border这个属性在开发过程中很常用&#xff0c;常常用它来作为边界的。但是大家真的了解border吗&#xff1f;以及它的形状是什么样子的。 我们先来看这样一段代码&#xff1a;&#x1f44f; <!--* Author: syk 185901…

如何为三星active2手表安装自己DIY的表盘

一、步骤介绍 Step 1. 下载Galaxy watch studio&#xff1b; Step 2. 按照up主“隔壁张师傅2022”的文章进行安装。 二、安装流程简单说明&#xff1a; ① 电脑端官网下载并安装Galaxy Watch Designer或者Galaxy Watch Studio程序。 ② 关闭手表蓝牙连接&#xff0c;并打开调…

Spring中最常用的11个扩展点

前言我们一说到spring&#xff0c;可能第一个想到的是 IOC&#xff08;控制反转&#xff09; 和 AOP&#xff08;面向切面编程&#xff09;。没错&#xff0c;它们是spring的基石&#xff0c;得益于它们的优秀设计&#xff0c;使得spring能够从众多优秀框架中脱颖而出。除此之外…

【源码解析】SpringBoot的源码深入分析

SpringBoot源码分析 主流程 SpringBoot项目的组成是需要引入SpringBoot需要的依赖&#xff0c;另外启动类上添加SpringBootApplication&#xff0c;主要是标明该类是启动类和实现自动装配&#xff0c;自动装配的原理详细可见&#xff0c;SpringBoot自动装配的实现原理。那么m…

Docker基本介绍

最近需要将项目做成一个web应用并部署到多台服务器上&#xff0c;于是就简单学习了一下docker&#xff0c;做一下小小的记录。 1、简单介绍一下docker 我们经常遇到这样一个问题&#xff0c;自己写的代码在自己的电脑上运行的很流畅&#xff0c;在其他人电脑上就各种bug&…

Linux学习--常用命令vi/vim

linux平台的文本编辑器 vi/vim的使用 vi windows的记事本 vim Windows的notepad 基本上vi/vim共分为三种模式&#xff0c;命令模式(Command mode)&#xff0c;输入模式(Insert mode)&#xff0c;底线命令模式(Last line mode) vim使用流程 1、下载vim yum install vim …

【并发编程学习篇】深入理解CyclicBarrier

一、CyclicBarrier介绍 字面意思回环栅栏&#xff08;循环屏障&#xff09;&#xff0c;通过它可以实现让一组线程等待至某个状态&#xff08;屏障点&#xff09;之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后&#xff0c;CyclicBarrier可以被重用。 和Count…

动态规划:leetcode 70.爬楼梯、322.零钱兑换、279.完全平方数

leetcode 70.爬楼梯leetcode 322.零钱兑换leetcode 279.完全平方数leetcode 70.爬楼梯假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f;注意&#xff1a;给定 n 是一个正整数。示例 1&#xff1a; 输入…