【数据结构】哈希表(散列表)

目录

1、unordered系列关联式容器

2、哈希概念

3、哈希函数

3.1 直接定址法

3.2 除留余数法

4、哈希冲突

4.1 闭散列(开放定址法)

4.1.1 线性探测

4.1.2 二次探测

4.1.3 线性探测代码实现

插入

搜索

删除

对于不可以取模的类型

4.2 开散列(哈希桶/拉链法)

插入

搜索

删除

对于不可取模的类型


1、unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到O(logN),即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,其底层结构是使用哈希表实现的。

这四个容器是unordered_set、unordered_map、unordered_multiset和unordered_multimap。这四个容器后序会有详细介绍,这里就简单介绍一下unordered_set和set的区别,为了后面更好地介绍哈希表

unoredered_set和set百分之90的内容都是相同的,不同点有:

1. 对key的要求不同

set:支持比较大小(自定义类型自己写比较函数)

unordered_set:支持key转化为整型 + 比较相等

2. set遍历有序,unordered_set遍历无序

3. set是双向迭代器,unordered_set是单向迭代器

4. 性能差异(插入、删除、搜索)

set:O(logN)             unordered_set:O(1)

2、哈希概念

哈希/散列是一种思想,指的是值与值建立映射关系

哈希表/散列表是根据哈希思想设计出来的数据结构,是将值与存储位置建立映射关系

我们使用一道题来引入哈希表

这道题是只包含小写字母的,所以我们可以想到,创建一个大小为26的整型数组,用一个下标代表一个字符,下标 = 字符 - ‘a’,然后遍历一遍字符串s,统计出每个字符出现的个数,完成之后,再遍历一次字符串s,如果遍历到的字符对应的下标的值是1,就代表这个字符再字符串中只出现了1次,也就是结果

class Solution {
public:int firstUniqChar(string s) {// 统计每个字符出现的次数int count[26] = {0};int size = s.size();for(int i = 0; i < size; ++i)count[s[i] - 'a'] += 1;// 按照字符次序从前往后找只出现一次的字符for(int i = 0; i < size; ++i)if(1 == count[s[i] - 'a'])return i;return -1;}
};

这就是哈希思想,那个数组就是哈希表。将字符串中可能出现的字符与数组的下标(存储位置)建立起了映射关系。当搜索一个值出现的次数时count[s[i] - 'a'],时间复杂度就是O(1)。但与真正的哈希表也是有些区别,真正的哈希表是将值(在这里也就是字符)放到映射的存储位置,而这里是将值出现的次数放到映射的存储位置。

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

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

当向该结构中:

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

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

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

若要将值与存储位置建立映射关系,就需要使用哈希函数

3、哈希函数

将值与存储位置建立关系的函数,称为哈希函数

常用的哈希函数有多种,这里仅介绍两种

3.1 直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

用key的值映射一个绝对位置或相对位置

优点:简单、均匀、没有冲突

缺点:只适合范围集中的数据

使用场景:适合查找比较小且连续的情况
上面的题目使用的就是这种方法,地址 = 字符 - 'a',之所以适用这种方法,是因为26个字母是连着的,是范围集中的数据,加入有一组数据是10,20,15,35,66,16,10000,如果使用直接定址法,需要开一个10000大小的数组,并且里面的数据十分分散,显然是不适用的

3.2 除留余数法

key模表的大小N后,映射到表中的一个位置

hashi = key % N,N是表的大小

假设有一组数据10,20,15,35,66,16,10000,表的大小是10,会发现,用10和20模表的大小,结果都是0,那是将10和20都放到下标为0的位置吗?这种情况称为哈希冲突

4、哈希冲突

哈希冲突就是指不同值,取模以后映射到相同位置,也称为哈希碰撞

解决哈希冲突的两种常用方式是:闭散列和开散列

4.1 闭散列(开放定址法)

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

闭散列就是按规则去其他位置找一个空位置存储,也就是根据这个规则去寻找“下一个”位置,寻找下一个位置的方法有两种,线性探测和二次探测 

4.1.1 线性探测

线性探测就是若映射到的位置已经被占了,则从这个位置开始,往下寻找空的位置,如何将数据放入空位置中。但这种方法容易造成一片拥堵,因为这个值占用了别人的位置,后面插入的值又要往后占位置

当前位置 + 1 ,若还被占了,当前位置+2,...,一直到空位置

4.1.2 二次探测

二次探测与线性探测是类似的,都是当映射到的位置已经被占了,当前位置 + 1^2 ,若还被占了,当前位置 + 2^2,...,一直到空位置

在前面,我们有提到set和unordered_set对key的要求不同, unordered_set要求key可以比较相等,正是在这里体现了这个作用。并且支持将key转为整型就是为了能够取模

在代码实现,我们只实现线性探测,因为两者是类似的

4.1.3 线性探测代码实现

我们前面只是了解了线性探测的插入操作,接下来我们了解一下线性探测的查找和删除操作

查找16:先去16映射到的位置查看,比较这个位置存储的值与16是否相等,若不想等,则继续往后查找,若到了空,则表中没有这个数据。像这里,16映射到的位置存放的是35,不是16,所以继续向后查找,一直到下标为8的时候找到16。假设要查找的是26,则26映射到的位置是35,继续向后查找,会发现,一直到下标为9没有存放值的位置都没有找到26,所以表中没有26

删除66:先查找66,若66存在,则删除

此时如果再查找16,会发现表中有16,但是找不到。所以,表中存数据时,每个位置不应该只存数据,还应该存放这个数据的状态。数据的状态有3种:存在、不存在、删除。此时也需要修改一下上面的查找和删除逻辑。查找除了要比较值是否相同,并且还需要这个值的状态是存在,才算找到,找不到时,是从映射的位置一直向后找,直到状态为空,才算找不到,如果遇到状态是删除的,仍然需要向后找。删除可以不改变这个结点的数据,只需要将这个结点的状态改成删除即可

搜索、插入、删除也不是完全只需要1次,可能需要几次,但仍然是常数时间,所以时间复杂度是O(1)

enum State
{EXIST,EMPTY,DELETE
};
template<class K,class V>
struct HashDate // 哈希表是这个类型的数组
{pair<K, V> _kv;State _state = EMPTY; // 默认状态为空
};
template<class K,class V>
class HashTable
{
public:HashTable(){_tables.resize(10);}
private:vector<HashDate<K, V>> _tables;
};
插入

首先要注意,取模时,数组的大小需要根据size()计算,不能用capacity来计算,因为是需要将数据放进算出的位置的,若是用capacity,operator[]会报错

bool Insert(const pair<K, V>& kv)
{size_t hashi = kv.first % _tables.size();// 找到空或删除的位置将数据往里面放while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size(); // 防止++后超过数组范围}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;return true;
}

此时当哈希表满了时,会出现死循环,所以,需要对哈希表进行扩容操作

哈希表需要在什么时候进行扩容?如何扩容呢?

哈希表的载荷因子定义为;a = 填入表中的数据个数 / 哈希表的长度

a是哈希表装满程度的标志因子,由于表长是定值,a与"填入表中的数据个数"成正比,所以,a越大,表明填入表中的元素越多,产生冲突的可能性越大,反之,a越小,表明填入表中的元素越少,产生冲突的可能性越小。实际上,哈希表的平均查找长度是载荷因子a的函数,只是不同的处理冲突的方法有不同的函数。

对于开放定址法,载荷因子是特别重要的因素,应严格限制载0.7~0.8以下,当载荷因子过大,就要扩容。所以,哈希表永远不会满,当快满时就扩容了,无需担心上面的死循环

扩容时,不能直接对这个vector进行resize,因为扩容后,原来冲突的数据,现在可能不冲突了

10,20,10000原本3个都是冲突的,现在只有20和10000冲突了,66和16也是同理

扩容方法:

方法一:当满了,创建一个vector,然后遍历原来的哈希表,将这些数据插入到新开的vector中,最后再swap两个vector。这种方法不好,因为需要重复写插入函数中的代码

方法二:创建一个哈希表对象,然后遍历原来的哈希表,通过新创建的哈希表对象调用Insert函数,将原来哈希表中的值一个一个插入,这样就能完成代码的复用

bool Insert(const pair<K, V>& kv)
{// 扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}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++;return true;
}
搜索

Find不能只比较key是否相同,因为在删除操作中,删除数据时,只会将其状态修改为删除,并不会将数据真的删除,所以除了比较key相同,还需要状态是存在才是真的找到了

HashDate<K, V>* 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;
}

这时候我们可以对插入进行修改,使之不能放入重复元素

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;// 扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}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++;return true;
}
删除
bool Erase(const K& key)
{HashDate<K, V>* ret = Find(key);if (ret == nullptr)return false;ret->_state = DELETE;return true;
}

不需要写析构、拷贝构造和赋值运算符重载,因为成员变量是vector和内置类型

对于不可以取模的类型

我们可以使用下面的代码对我们上面实现的哈希表进行测试

void test_hash1()
{HashTable<int, int> hash;int a[] = { 10,20,15,35,66,16,10000 };for (auto e : a){hash.Insert({ e,e });}hash.Insert({ 9,9 });hash.Insert({ 3,3 });hash.Erase(66);
}

此时建立的哈希表的key是整型,是可以取模的。那若是存储的key是string等不可取模的类型,又该怎么办呢?

此时就需要用到仿函数了,对于int、size_t、double、float、long、long long、char等可以直接取模的类型,就使用仿函数的缺省值,对于string等无法取模的类型,就自己写仿函数,然后定义哈希表时传过去

template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;// 扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V> newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash hs;size_t hashi = hs(kv.first) % _tables.size();// 找到空或删除的位置将数据往里面放while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size(); // 防止++后超过数组范围}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret == nullptr)return false;ret->_state = DELETE;return true;}HashDate<K, V>* Find(const K& key){Hash hs;size_t hashi = hs(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;}
private:vector<HashDate<K, V>> _tables;size_t _n = 0; // 表中存储数据个数
};

可以看到,对于可以直接取模的类型,是先转化为size_t,再取模。对于string等不能取模的类型也是类似。此时是完成了两层映射,先用string去映射整型,再用整型去映射出对于的存储位置。注意,在这两层映射中,每一层都可能会产生冲突

那string要如何映射为整型呢?

此时就涉及到了字符串哈希算法。

可不可以取地址,将地址转为整型呢?不可以,因为两个相同的字符串的地址会不同,这样映射成的整型也是不同的

应当将string中的每个字母的ASCII相加,但是这样太容易发生冲突了,因为像abcd和dcba映射为整型时就会产生冲突,为了减少冲突,没加一个值时,可以对目前的和乘一个固定的数

struct StringHashFunc
{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash *= 31;hash += e;}return hash;}
};
void test_hash2()
{HashTable<string, string, StringHashFunc> hash;hash.Insert({ "left","左边" });hash.Insert({ "right","右边" });
}

在我们使用unordered_map时,会发现当存储的key是string时,并不需要自己写仿函数,这是因为string很常用,所以STL底层对string进行了特化。我们实现时可以对其进行模板的特化

namespace open_address // 开放定址法
{enum State{EXIST,EMPTY,DELETE};template<class K, class V>struct HashDate // 哈希表是这个类型的数组{pair<K, V> _kv;State _state = EMPTY; // 默认状态为空};template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};template<> // 对string类型进行特化struct HashFunc<string>{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash *= 31;hash += e;}return hash;}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;// 扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash> newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash hs;size_t hashi = hs(kv.first) % _tables.size();// 找到空或删除的位置将数据往里面放while (_tables[hashi]._state == EXIST){++hashi;hashi %= _tables.size(); // 防止++后超过数组范围}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret == nullptr)return false;ret->_state = DELETE;return true;}HashDate<K, V>* Find(const K& key){Hash hs;size_t hashi = hs(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;}private:vector<HashDate<K, V>> _tables;size_t _n = 0; // 表中存储数据个数};
}

实际上,解决哈希冲突的闭散列方式,无论是线性探测还是二次探测都不是太好,因为本质都是一种零和博弈

4.2 开散列(哈希桶/拉链法)

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

这里的哈希表就是一个指针数组,下面挂着的就是链表,所以结点除了要存储值,还要存储下一个结点的地址。每一个链表称为一个哈希桶

namespace hash_bucket
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}};template<class K, class V>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(10, nullptr);}private:vector<Node*> _tables;size_t _n = 0; // 表中存储数据个数};
}
插入

插入首先计算出插入值的插入位置,然后往插入位置的链表中插入该值。因为冲突的这些值的先后顺序是任意的,所以插入一个值时既可以头插,也可以尾插,我们这里采用的是头插

bool Insert(const pair<K, V>& kv)
{size_t hashi = kv.first % _tables.size();// 头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}

 使用哈希桶扩容时与前面有些不同,是当载荷因子等于1时才进行扩容

bool Insert(const pair<K, V>& kv)
{size_t hashi = kv.first % _tables.size();// 负载因子==1,扩容if (_n == _tables.size()){HashTable<K, V> newHT;newHT._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];// 遍历整个哈希表,当桶不为空时,让cur遍历这个桶,然后插入,直到cur为空,说明这个桶已经完了while (cur){newHT.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newHT._tables);}// 头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}

在前面,完美看到了哈希表底层就是一个vector<Node*>,是一个指针数组,下面挂着的那些链表是我们new出来的,所以也需要我们自己释放,所以是需要自己实现析构函数的

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

此时的插入操作中的扩容是不好的,因为我们没有利用表中原有的结点,而是重新创建结点,原来的结点还会delete掉,倘若扩容前有10000个结点,那么会进行10000次new和delete,这样消耗是比较大的。所以我们不应该调用Insert,因为一旦调用Insert就一定会有new和delete。此时应该创建一个vector<Node*>,并且复用原来的结点,即用上面提到的方法一

bool Insert(const pair<K, V>& kv)
{size_t hashi = kv.first % _tables.size();// 负载因子==1,扩容if (_n == _tables.size()){vector<Node*> newtables(_tables.size() * 2, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 旧表中的结点,挪动到新表重新映射的位置size_t hashi = cur->_kv.first % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}// 原来的桶取完后要置空_tables[i] = nullptr;}_tables.swap(newtables);}// 头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}
搜索
Node* Find(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key)return cur;cur = cur->_next;}return nullptr;
}

为了防止冗余,可以修改插入

bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;Hash hs;size_t hashi = hs(kv.first) % _tables.size();// 负载因子==1,扩容if (_n == _tables.size()){vector<Node*> newtables(_tables.size() * 2, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 旧表中的结点,挪动到新表重新映射的位置size_t hashi = hs(cur->_kv.first) % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}// 原来的桶取完后要置空_tables[i] = nullptr;}_tables.swap(newtables);}// 头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;
}
删除

删除操作,记录当前遍历到的结点cur和cur的前一个结点prev,删除时让prev的_next指向cur的下一个即可,然后释放cur,但若cur是头结点需要特殊考虑,因为头结点没有前一个结点,此时直接让表里的指针指向cur的下一个

bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr)_tables[hashi] = cur->_next;elseprev->_next = cur->_next;delete cur;return true;}prev = cur;--_n;cur = cur->_next;}return false;
}
对于不可取模的类型

与开放定址法是类似的

namespace hash_bucket // 哈希桶
{template<class K,class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}};template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};template<> // 对string类型进行特化struct HashFunc<string>{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash *= 31;hash += e;}return hash;}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(10, nullptr);}~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;Hash hs;size_t hashi = hs(kv.first) % _tables.size();// 负载因子==1,扩容if (_n == _tables.size()){//HashTable<K, V> newHT;//newHT._tables.resize(_tables.size() * 2);//for (size_t i = 0; i < _tables.size(); i++)//{//	Node* cur = _tables[i];//	// 遍历整个哈希表,当桶不为空时,让cur遍历这个桶,然后插入,直到cur为空,说明这个桶已经完了//	while (cur)//	{//		newHT.Insert(cur->_kv);//		cur = cur->_next;//	}//}//_tables.swap(newHT._tables);vector<Node*> newtables(_tables.size() * 2, nullptr);for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 旧表中的结点,挪动到新表重新映射的位置size_t hashi = hs(cur->_kv.first) % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}// 原来的桶取完后要置空_tables[i] = nullptr;}_tables.swap(newtables);}// 头插Node* newNode = new Node(kv);newNode->_next = _tables[hashi];_tables[hashi] = newNode;++_n;return true;}Node* Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key)return cur;cur = cur->_next;}return nullptr;}bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr)_tables[hashi] = cur->_next;elseprev->_next = cur->_next;delete cur;return true;}prev = cur;--_n;cur = cur->_next;}return false;}private:vector<Node*> _tables;size_t _n = 0; // 表中存储数据个数};
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【pyhton】Python中zip用法详细解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

在WordPress上启用reCAPTCHA的指南

随着网络安全问题的日益严重&#xff0c;网站管理员必须采取措施保护自己的网站免受恶意攻击。对于WordPress用户来说&#xff0c;可以通过启用谷歌的reCAPTCHA功能来增强网站的安全性。本文将介绍两种在WordPress上启用reCAPTCHA的方法&#xff1a;使用插件和手动添加代码。 一…

白盒测试基础与实践:Python示例及流程图设计

文章目录 前言一、白盒测试是什么&#xff1f;主要特点常用方法优点缺点 二、白盒测试常用技术语句覆盖判定覆盖条件覆盖判定/条件覆盖条件组合覆盖路径覆盖 三、程序流程图设计四、测试用例设计1. 基本路径法2. 语句覆盖3. 判断覆盖4. 条件覆盖5. 判断/条件覆盖6. 条件组合覆盖…

两个好消息,你先听哪个?

1.第五大数据、人工智能与软件工程国际研讨会&#xff08;ICBASE 2024)成功申请IEEE出版&#xff0c;上线IEEE官网&#xff0c;欢迎投稿参会&#xff01;&#xff01;&#xff01; &#x1f4e3;IEEE独立出版&#xff0c;设置优秀评选 &#x1f525;院士加盟&#xff0c;中外高…

一个私有化的中文笔记工具个人知识库,极空间Docker部署中文版『Trilium Notes』

一个私有化的中文笔记工具&个人知识库&#xff0c;极空间Docker部署中文版『Trilium Notes』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 最近被很多小伙伴问到NAS上的笔记工具&#xff0c;虽说之前也出过Memos&#xff0c;刚开始用起来还不错&#xff0c;但是用了一段时间…

(vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束

(vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束 需求&#xff1a;按勾选的顺序给后端传值 难点&#xff1a;在 Element UI 的 el-cascader 组件中&#xff0c;默认的行为是根据数据的层级结构来显示选项&#xff0c;用户的选择也会基于这种层级结构&#xff0c;el-…

文件解析漏洞—IIS解析漏洞—IIS7.X

在IIS7.0和IIS7.5版本下也存在解析漏洞&#xff0c;在默认Fast-CGI开启状况下&#xff0c;在一个文件路径/xx.jpg后面加上/xx.php会将 “/xx.jpg/xx.php” 解析为 php 文件 利用条件 php.ini里的cgi.fix_pathinfo1 开启IIS7在Fast-CGI运行模式下 在 phpstudy2018 根目录创建…

红酒与夜晚:享受静谧的品酒时光

当夜幕低垂&#xff0c;星光点点&#xff0c;世界仿佛进入了一个宁静而神秘的领域。在这样的夜晚&#xff0c;与一瓶定制红酒洒派红酒&#xff08;Bold & Generous&#xff09;相伴&#xff0c;便是一场令人陶醉的品酒之旅&#xff0c;让人在静谧中感受生活的美好。 一、夜…

《BiFormer: Vision Transformer with Bi-Level Routing Attention》CVPR2023

摘要 这篇论文提出了一种新型的视觉Transformer&#xff0c;名为BiFormer&#xff0c;它采用了双层路由注意力&#xff08;Bi-Level Routing Attention, BRA&#xff09;机制。注意力机制是视觉变换器的核心构建模块&#xff0c;能够捕获数据中的长期依赖性。然而&#xff0c;…

java远程调试

java远程调试 idea2024创一个Spring Web项目springdemo1 使用maven-assembly-plugin插件打包成JAR文件 pom.xml参考如下 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi&quo…

离线安装MeterSphere遇到的问题

1.安装步骤&#xff0c;参考官方文档 在线安装 - MeterSphere 文档 2.安装完成以后&#xff0c;docker ps查看有很多服务一直处于重启状态&#xff0c;查看容器日志docker logs ID,发现所有一直处于重启状态的容器都是因为服务无法创建日志目录和文件。一直处于重启的服务如…

NAND行业回归盈利:AI与云存储需求驱动

市场概览 根据Yole Group于2024年6月25日发布的市场报告&#xff0c;经过五个季度的亏损之后&#xff0c;NAND闪存行业在2024年第一季度&#xff08;1Q24&#xff09;实现了盈利回归。这一转变主要得益于企业级固态硬盘&#xff08;SSD&#xff09;领域的强劲需求增长&#xf…

画图像解方程系列-FPI

不是所有方程都能求出精确解。 解方程 sinx(x) cos(x)&#xff0c;求x&#xff0c;在区间&#xff08;0&#xff0c;1&#xff09;范围内。 正常解法&#xff1a; 两边除以cosx得到tanx 1 解的x Π/4&#xff0c;使用计算机计算得到&#xff1a;0.7853981633974483096156…

CSP-J 复赛 模拟题

1.生产计划&#xff1a; 样例 #1 样例输入 #1 2 4 5 6 12 1 3 6 15 8 1 3 100 3 200 4 300 6 100 样例输出 #1 YES NO 2.分组和为3&#xff1a; 样 例 # 1 样 例 输 入 # 1 5 1 1 1 2 1 样 例 输 出 # 1 2 样 例 # 2 样 例 输 入 # 2 7 2 2 1 1 2 1 1 样 例 输 出 # …

Jenkins保姆笔记(1)——基于Java8的Jenkins安装部署

前言 记录分享下Jenkins的相关干货知识。分2-3篇来介绍Jenkins的安装部署以及使用。还是和以前一样&#xff0c;文章不介绍较多概念和细节&#xff0c;多介绍实践过程&#xff0c;以战代练&#xff0c;来供大家学习和理解Jenkins 概念 Jenkins是一个开源的自动化服务器&…

【过题记录】 8.2 hddx

飞行棋 关于这一题 我在考场上手莫了n2和n3的情况 发现一点规律&#xff0c;大力猜想蒙了一个结论 结果蒙对了… 关于正确做法&#xff0c;发现零号点和其他几个点是不一样的。 因为对于0而言&#xff0c;他没有赠送的情况(只要摇到n就直接胜利) 因此0和其他点要分开讨论 对于…

【中项】系统集成项目管理工程师-第7章 软硬件系统集成-7.2基础设施集成

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

【xss-labs-master】靶场通关详解!-----持续更新

XSS基础概念&#xff1a; 跨站脚本攻击XSS(Cross Site Scripting)&#xff0c;为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆&#xff0c;故将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码&#xff0c;当用户浏览该页之时&#xff0c;嵌入其…

《技术人求职之道》之面试准备篇:不打无准备之仗,优秀技术人的面试前准备

摘要 本文为求职者提供面试前的全面准备策略,旨在提升面试成功几率并减轻面试前的焦虑和不自信。文章首先强调准备求职资料的重要性,包括简历、寸照、学历证明等,并建议提前准备以避免入职时的尴尬。接着,讨论对应聘公司进行调研的必要性,包括了解公司业务和技术需求,以…

《从U-Net到Transformer:深度模型在医学图像分割中的应用综述》论文阅读

网络首发地址&#xff1a;https://link.cnki.net/urlid/51.1307.tp.20231026.1648.002 摘要&#xff1a; U-Net以卷积神经网络&#xff08;CNN&#xff09;为主干&#xff0c;其易于优化促使在医学图像分割领域的发展&#xff0c; 但只擅长获取局部特征&#xff0c;缺乏长期相…