STL容器之unordered_set类

文章目录

  • STL容器之unordered_set类
    • 1、unordered系列关联式容器
    • 2、unordered_set
      • 2.1、unordered_set介绍
      • 2.2、unordered_set的使用
        • 2.2.1、unordered_set的常见构造
        • 2.2.2、unordered_set的迭代器
        • 2.2.3、unordered_set的容量
        • 2.2.4、unordered_set的增删查
        • 2.2.5、unordered_set的桶操作
    • 3、unordered_multiset
    • 4、unordered_set的底层结构
      • 4.1、哈希概念
      • 4.2、哈希冲突
      • 4.3、哈希函数
      • 4.4、解决哈希冲突
        • 4.4.1、开放地址法(Open Addressing)
        • 4.4.2、链表法(Separate Chaining)
    • 5、unordered_set的模拟实现
      • 5.1、改造哈希表(链表法)
      • 5.2、MyUnordered_Set类的构成

img

STL容器之unordered_set类

1、unordered系列关联式容器

前面在讲map和set有提到过关联式容器,即使用键值对即< Key , Value >的形式来存储和访问数据,按照键值进行组织和查找。在数据检索时比序列式容器效率更高。常见的unordered系列关联式容器包括:

  1. unordered_map: 哈希表实现的键值对集合,查找速度比map快,但是不保证元素的顺序。

  2. unordered_set: 哈希表实现的关键字集合,查找速度比set快,但是不保证元素的顺序。

注意:map和set的结构是树形结构的关联式容器,用的是红黑树作为底层结构。unordered_map和unordered_set的结构是哈希结构的关联式容器,用的是哈希表作为底层结构。


2、unordered_set

2.1、unordered_set介绍

unordered_set的文档介绍

  1. unordered_set是以不特定顺序存储独特元素的容器,并允许根据其值快速检索单个元素。

  2. 在unordered_set中,元素的值同时是其键,唯一标识它。键是不可变的,因此,unordered_set中的元素不能在容器中修改 – 不过,它们可以插入和删除。

  3. 在内部,unordered_set中的元素没有按任何特定顺序排序,而是根据其哈希值组织成桶,以便通过其值直接快速访问单个元素(平均时间复杂度恒定)。

  4. unordered_set容器比set容器更快地通过其键访问单个元素,但是它通常在遍历元素子集的范围迭代方面效率较低。

  5. 容器中的迭代器至少是前向iterators。


2.2、unordered_set的使用

2.2.1、unordered_set的常见构造
(constructor )函数名称接口说明
unordered_set ( size_type n = /* see below */, const hasher& hf = hasher(), const key_equal& eql = key_equal(), const allocator_type& alloc = allocator_type() )构造空的unordered_set
unordered_set ( InputIterator first, InputIterator last, size_type n = /* see below */, const hasher& hf = hasher(), const key_equal& eql = key_equal(), const allocator_type& alloc = allocator_type() )用[first,last)迭代区间构造unordered_set
unordered_set ( const unordered_set& ust )unordered_set的拷贝构造
void test_us1() {unordered_set<int> us;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {us.insert(e);}for (auto e: us) {cout << e << " ";}cout << endl;cout << "=======================" << endl;unordered_set<int> us1(us.begin(), us.end());for (auto e: us1) {cout << e << " ";}cout << endl;cout << "=======================" << endl;unordered_set<int> us2(us1);for (auto e: us2) {cout << e << " ";}cout << endl;}

2.2.2、unordered_set的迭代器
函数名称接口说明
begin()+end()获取第一个元素位置的iterator和获取最后一个元素的后一个位置的iterator
cbegin()+cend()获取第一个元素位置的const_iterator和获取最后一个元素的后一个位置的const_iterator
void test_us2() {unordered_set<int> us;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {us.insert(e);}unordered_set<int>::iterator it = us.begin();while (it != us.end()) {cout << *it << " ";++it;}cout << endl;cout << "=======================" << endl;unordered_set<int>::const_iterator it1 = us.cbegin();while (it1 != us.cend()) {cout << *it1 << " ";++it1;}cout << endl;
}

2.2.3、unordered_set的容量
函数名称接口说明
empty判断当前unordered_set是否为空
size获取unordered_set的元素个数
void test_us3() {unordered_set<int> us;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {us.insert(e);}cout << us.empty() << endl;cout << us.size() << endl;us.clear();cout << us.empty() << endl;cout << us.size() << endl;
}

2.2.4、unordered_set的增删查
函数名称接口说明
insert在unordered_set中插入元素x,实际上插入的是<x,x>键值对,如果插入成功,返回<x插入位置,true>,插入失败说明unordered_set中已经有x,返回<x在unordered_set的位置,false>
erase删除unordered_set中pos位置的元素,或者删除值为val的元素
swap交换两个unordered_set的元素
clear将unordered_set的元素置空
find返回unordered_set中值为x的元素位置
count返回unordered_set中值为x的元素个数
void test_us4() {unordered_set<int> us;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {us.insert(e);}for (auto e: us) {cout << e << " ";}cout << endl;cout << "========================" << endl;us.erase(1);us.erase(2);auto pos = us.find(11);us.erase(pos);for (auto e: us) {cout << e << " ";}cout << endl;cout << "========================" << endl;unordered_set<int> us1;us1.insert(100);us1.swap(us);cout << "us:";for (auto e: us) {cout << e << " ";}cout << endl;cout << "us1:";for (auto e: us1) {cout << e << " ";}cout << endl;cout << "========================" << endl;cout << us1.count(5) << endl;cout << us1.count(1) << endl;cout << "========================" << endl;us1.clear();cout << "us:";for (auto e: us) {cout << e << " ";}cout << endl;cout << "us1:";for (auto e: us1) {cout << e << " ";}cout << endl;}

2.2.5、unordered_set的桶操作
函数名称接口说明
bucket_count返回unordered_set中的桶的个数
bucket_size返回第n个桶中元素的个数
bucket返回元素k在哪个桶
void test_us5() {unordered_set<int> us;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {us.insert(e);}cout << us.bucket_count() << endl;cout << us.bucket_size(1) << endl;cout << us.bucket(11) << endl;
}

3、unordered_multiset

这个其实就和set和multiset一样,就是比unordered_set多了一个可以存重复值的特性。

演示一下:

void test_ums1() {unordered_multiset<int> ums;int a[]{1, 2, 5, 7, 2, 3, 5, 8, 2, 11, 9};for (auto e: a) {ums.insert(e);}for (auto e: ums) {cout << e << " ";}cout << endl;
}

4、unordered_set的底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

4.1、哈希概念

哈希(Hash)是一种常见的数据结构和算法,用于快速地将数据映射到固定大小的索引值,通常是一个整数哈希函数将输入数据映射到索引值,这使得可以快速地在数据结构中查找、插入或删除数据,而无需遍历整个数据结构

哈希是一种将任意长度的输入数据通过哈希函数转换为固定长度的输出数据的过程。哈希函数将输入数据映射到一个称为哈希值或散列值的固定大小的值。哈希函数通常设计为能够尽可能均匀地将不同的输入映射到不同的输出,以减少哈希碰撞的可能性。

例如:

问题:按上述映射,插入10会出现什么问题?

答:因为10%8=2,而2的位置已经被占了,那么我们就要想办法解决这个问题。常见有两个方案:

方案一:使用开放定址法,从2的位置向后找到一个没有使用的位置插入。

方案二:使用拉链法(哈希桶),让数组每个位置存的都是一个结点的指针,当出现映射到同一个位置时,直接进行头插。


4.2、哈希冲突

哈希冲突是指两个不同的输入数据经过哈希函数计算后,得到了相同的哈希值。由于哈希函数将不同的输入映射到有限的输出空间,而输入空间是无限的,所以在实际应用中,哈希冲突是不可避免的

解决哈希冲突的常见方法包括:

  1. 链表法(Separate Chaining): 将具有相同哈希值的数据存储在同一个位置上,形成链表或其他数据结构。这样,当发生哈希冲突时,可以将冲突的数据连接在一起,不会丢失数据。
  2. 开放地址法(Open Addressing): 当发生哈希冲突时,通过探测序列(如线性探测、二次探测、双重哈希等)在哈希表中寻找另一个可用的位置来存储冲突的数据。

4.3、哈希函数

哈希函数是将输入数据映射到哈希值的函数。一个好的哈希函数应该尽可能地均匀地将输入映射到输出,以减少哈希冲突的发生。哈希函数的选择取决于应用的需求和数据的特性。

常见的哈希函数的构造方法:

  1. 直接定址法(常用):取关键字或关键字的某个线性函数值为哈希地址。即 H(key)=key 或 H(key)=a*key+b,其中a和b为常数。这种方法简单直观,但仅适用于关键字分布基本均匀的情况。若关键字的分布极不均匀,则哈希表中有的区域会很拥挤,而有的区域则可能空闲很多。
  2. 除留余数法(常用):选取关键字除以某个整数p的余数作为哈希地址。哈希函数的一般形式为:Hash(key)=key % p。这种方法的关键在于选择合适的p,通常要求p小于等于哈希表长度m,并且接近m。
  3. 数字分析法:取关键字中某些取值较分散的数字位作为哈希地址。这种方法适合于所有关键字已知的情况,分析数字分布的情况,选择某些位作为哈希地址,尽量避免冲突。
  4. 平方取中法:取关键字平方的中间几位作为哈希地址。具体取多少位视实际要求而定。
  5. 折叠法:将关键字分割成位数相同的几段,然后将这些段叠加求和作为哈希地址。段的位数取决于哈希地址的位数,由实际需要而定。
  6. 乘法哈希法:将输入乘以一个常数,再取结果的小数部分或整数部分作为哈希值。这种方法能够在一定程度上消除冲突,并且适用于输入数据分布不均匀的情况。

4.4、解决哈希冲突

4.4.1、开放地址法(Open Addressing)

开放地址法(Open Addressing): 当发生哈希冲突时,通过探测序列(如线性探测、二次探测、双重哈希等)在哈希表中寻找另一个可用的位置来存储冲突的数据。

以下是开放定址法的一些常见方法:

  1. 线性探测(Linear Probing)
  • 当发生碰撞时,依次检查下一个槽位,直到找到一个空槽位。
  • 探测序列公式为:h(k,i) = (h′(k)+i) mod m其中 h′(k)初始哈希函数的值,i 是探测的步长, m 是哈希表的大小。
  • 线性探测可能会产生一系列聚集现象,导致槽位的线性排列。
  1. 二次探测(Quadratic Probing)
  • 当发生碰撞时,通过一个二次探测序列来寻找下一个槽位。
  • 探测序列公式为:h(k,i)=(h′(k)+c1⋅i+c2⋅i^2) mod m,其中 c1 和 c2 是常数,m 是哈希表的大小。
  • 二次探测在碰撞发生后,会以二次的步长来搜索下一个空槽位,有助于减少线性探测的聚集现象。
  1. 双重哈希(Double Hashing)
  • 使用两个哈希函数来计算探测序列的步长。
  • 探测序列公式为:h(k,i)=(h1(k)+i⋅h2(k)) mod m其中 h1(k) 和 h2(k) 是两个不同的哈希函数,m 是哈希表的大小。
  • 双重哈希通常能够更好地减少聚集现象,并且对于大部分数据集合,具有较好的性能。

这里我们就只介绍线性探测法:

就像前面我们介绍的哈希概念中,当插入10的时候发生冲突了,怎么解决?我们从位置2顺序向后找到一个空位置填入就行。

这里我们需要考虑到一个问题!元素删除:当一个元素删除过后,我们直接给他删了吗?实际上是不行的,因为如果直接删除了,也就是这个位置置空了,但是它后面位置的值又可能也是之前发生冲突探测过来的值,那么下次找这些值就找不到了!所以我们的解决办法是给数组的每个位置添加一个标志位:存在(EXIST)、删除(DELETE)、空(EMPTY)。这样就在删除的时候,只需要把这个位置的标志设置为DELETE就行,即只要这个位置标志不是EMPTY就可以继续向后探测!

因此有每个元素的结构体:

enum State {EMPTY,EXIST,DELETE
};template<class K, class V>
struct HashData {pair<K, V> _kv;State _s;HashData(const pair<K, V> &kv = pair<K, V>()) : _kv(kv), _s(EMPTY) {}
};

哈希表:这里给了一个成员变量_n,用来记录当前哈希表中的元素个数

template<class K, class V>
class HashTable_OpenAddr {typedef HashData<K, V> Node;
public:HashTable_OpenAddr(size_t size = 10) {_tables.resize(size);}Node *Find(const K &key) {int hashi = key % _tables.size();while (_tables[hashi]._s == EXIST) {if (_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) {// 装填因子为0.7,扩容size_t newSize = 2 * _tables.size();
//            _tables.resize(newSize);// 把原数据重新放到新空间中HashTable_OpenAddr<K, V,HashMode> newHashTable(newSize);for (auto &e: _tables) {if (e._s == EXIST) {newHashTable.Insert(e._kv);}}_tables.swap(newHashTable._tables);}int hashi = kv.first % _tables.size();while (_tables[hashi]._s == EXIST) {hashi++;hashi %= _tables.size(); // 可能从尾部到头部了}// 现在hashi的位置是空的或者是删除了的_tables[hashi]._kv = kv;_tables[hashi]._s = EXIST;++_n;return true;}bool Erase(const K &key) {Node *ret = Find(key);if (ret) {--_n;ret->_s = DELETE;return true;} else {return false;}}private:vector<Node> _tables;size_t _n = 0;
};

这里还会有一个问题:如果我们插入的值不是int类型呢?比如string类型,那么key就是string,直接插入将会报错。

我们可以设计一个仿函数,将string类型的key映射位size_t类型的值,来用来映射位置。

// 默认的取key的用来哈希的值
template<class K>
struct HashNodeFunc {size_t operator()(const K &key) {return (size_t) key;}
};//特化string类型的key的用来哈希的值
template<>
struct HashNodeFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {e *= 131;hash += e;}return hash;}
};

这里string的特化函数,用的是BKDR方法(有大佬测试过这个方法取的值引起冲突的概率最小),其他的方法在这个网站字符串的Hash函数

因此我们的代码变为:

enum State {EMPTY,EXIST,DELETE
};// 哈希表 --- 开放定址法
template<class K, class V>
struct HashData {pair<K, V> _kv;State _s;HashData(const pair<K, V> &kv = pair<K, V>()) : _kv(kv), _s(EMPTY) {}
};template<class K>
struct HashNodeFunc {size_t operator()(const K &key) {return (size_t) key;}
};template<>
struct HashNodeFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {e *= 131;hash += e;}return hash;}
};template<class K, class V, class HashMode = HashNodeFunc<K> >
class HashTable_OpenAddr {typedef HashData<K, V> Node;
public:HashTable_OpenAddr(size_t size = 10) {_tables.resize(size);}Node *Find(const K &key) {HashMode hs;int hashi = hs(key) % _tables.size();while (_tables[hashi]._s == EXIST) {if (_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; // 存在就不插入了HashMode hs;int hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._s == EXIST) {hashi++;hashi %= _tables.size(); // 可能从尾部到头部了}// 现在hashi的位置是空的或者是删除了的_tables[hashi]._kv = kv;_tables[hashi]._s = EXIST;++_n;return true;}bool Erase(const K &key) {Node *ret = Find(key);if (ret) {--_n;ret->_s = DELETE;return true;} else {return false;}}private:vector<Node> _tables;size_t _n = 0;
};

如果我们需要插入自定义类型,比如Date类型的元素,我们可以自己写一个哈希函数来传入模版参数。

如下,包含测试:

class Date {
public:Date(int year = 1999, int month = 11, int day = 14) : _year(year), _month(month), _day(day) {}int getYear() const {return _year;}int getMonth() const {return _month;}int getDay() const {return _day;}bool operator==(const Date& d) {return _year == d._year && _month == d._month && _day == d._day;}private:int _year;int _month;int _day;
};struct HashNodeDate{size_t operator()(const Date &d) {size_t hash = 0;hash += d.getYear() * 131;hash += d.getMonth() * 131;hash += d.getDay() * 131;return hash;}
};void test_Hash_Table4() {Date *d[] = {new Date(), new Date(1998, 1, 1), new Date(2000, 12, 11)};HashTable_OpenAddr<Date, Date, HashNodeDate> hs;for (auto e: d) {hs.Insert(make_pair(*e, *e));}hs.Erase(*new Date(1998, 1, 1));
}

当哈希表什么时候进行扩容?怎么扩容?

这里涉及到一个概念:装填因子。

装填因子 = 存入哈希表的元素个数/哈希表长。

对于开放定址法,装填因子是一个特别重要的元素,应该严格控制在0.7~0.8之间。

所以我们这里在装填因子为0.7的时候进行扩容,2倍扩容。

扩容方式是遍历旧表,状态为EXIST的元素插入到新表,按新表的映射方式进行插入。

代码如下:

if (_n * 10 / _tables.size() >= 7) {// 装填因子为0.7,扩容size_t newSize = 2 * _tables.size();
//            _tables.resize(newSize);// 把原数据重新放到新空间中HashTable_OpenAddr<K, V,HashMode> newHashTable(newSize);for (auto &e: _tables) {if (e._s == EXIST) {newHashTable.Insert(e._kv);}}_tables.swap(newHashTable._tables);
}

因此开放定址法的哈希表的整体代码为:

enum State {EMPTY,EXIST,DELETE
};// 哈希表 --- 开放定址法
template<class K, class V>
struct HashData {pair<K, V> _kv;State _s;HashData(const pair<K, V> &kv = pair<K, V>()) : _kv(kv), _s(EMPTY) {}
};template<class K>
struct HashNodeFunc {size_t operator()(const K &key) {return (size_t) key;}
};template<>
struct HashNodeFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {e *= 131;hash += e;}return hash;}
};template<class K, class V, class HashMode = HashNodeFunc<K> >
class HashTable_OpenAddr {typedef HashData<K, V> Node;
public:HashTable_OpenAddr(size_t size = 10) {_tables.resize(size);}Node *Find(const K &key) {HashMode hs;int hashi = hs(key) % _tables.size();while (_tables[hashi]._s == EXIST) {if (_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) {// 装填因子为0.7,扩容size_t newSize = 2 * _tables.size();
//            _tables.resize(newSize);// 把原数据重新放到新空间中HashTable_OpenAddr<K, V,HashMode> newHashTable(newSize);for (auto &e: _tables) {if (e._s == EXIST) {newHashTable.Insert(e._kv);}}_tables.swap(newHashTable._tables);}HashMode hs;int hashi = hs(kv.first) % _tables.size();while (_tables[hashi]._s == EXIST) {hashi++;hashi %= _tables.size(); // 可能从尾部到头部了}// 现在hashi的位置是空的或者是删除了的_tables[hashi]._kv = kv;_tables[hashi]._s = EXIST;++_n;return true;}bool Erase(const K &key) {Node *ret = Find(key);if (ret) {--_n;ret->_s = DELETE;return true;} else {return false;}}private:vector<Node> _tables;size_t _n = 0;
};

4.4.2、链表法(Separate Chaining)

链表法(Separate Chaining): 将具有相同哈希值的数据存储在同一个位置上,形成链表或其他数据结构。这样,当发生哈希冲突时,可以将冲突的数据连接在一起,不会丢失数据。

这里我们数组中存的元素必然是一个指针!如果偷懒的话,其实可以在vector中存一个list,即vector<list<pair<K,V>>> _tables。但是我们秉承着低耦合和高内聚的理念,我们还是自己写一个结构体用来存每个结点的数据。

存结点数据的结构体:

template<class K, class V>
struct HashNode {typedef HashNode<K, V> Node;Node *_next;pair<K, V> _kv;HashNode(const pair<K, V> &kv) : _kv(kv), _next(nullptr) {}};

**哈希表:**仿函数还是要写的,和开放定址法一样!

template<class K, class V, class Hash = HashFunc<K>>
class HashTable {typedef HashNode<K, V> Node;
public:HashTable(size_t size = 10) {_tables.resize(size, nullptr);// 各结点初始化为空}Node *Find(const K &key) {Hash hs;// 先算出映射到哪里int 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;Node *ret = Find(key);if (ret) {int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];Node *prev = nullptr;while (cur) {if (cur->_kv.first == key) {if (prev == nullptr)_tables[hashi] = cur->_next; // 要删除的就是表中的结点,那么将它的下一个结点放到表中elseprev->_next = cur->_next; // 要删除的是表中的结点的后继中的某个delete cur; // 删除该结点并退出循环break;}prev = cur;cur = cur->_next;}--_n;return true;} else {return false;}}bool Insert(const pair<K, V> &kv) {Hash hs;if (Find(kv.first))return false;// 找到了// 先算出映射到哪里int hashi = hs(kv.first) % _tables.size();Node *cur = _tables[hashi];Node *newnode = new Node(kv);if (cur) {// 当前表中的结点不为空,则头插并链接newnode->_next = cur;_tables[hashi] = newnode;} else {// 当前表中的结点为空,则新结点直接放到这个表中_tables[hashi] = newnode;}++_n;return true;}private:vector<Node *> _tables;size_t _n = 0;
};

考虑一下哈希表的扩容?考虑极端情况下,也就是很多元素进行插入,但是我们这里初始的哈希表的长度为10,那么插入很多元素后会导致每个位置(我们这里叫桶)的结点下面的链表的长度很长,下次查找的时候需要遍历链表,很花时间!

官方的扩容条件是在装填因子为1的时候进行扩容!

那么这里是怎么进行扩容呢?

我们可以直接遍历旧表,将旧表的元素按新表的映射方式全部插入到新表中,并把旧表释放。

if (_n == _tables.size()) {// 扩容vector<Node *> newTables;newTables.resize(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); ++i) {Node *cur = _tables[i];while (cur) {Node *next = cur->_next; //记录下一个结点位置,防止找不到下一个结点// 映射到新表位置int hashi = hs(cur->_kv.first) % newTables.size();// 插入到新表位置Node *newcur = newTables[hashi];if (newcur) {// 当前表中的结点不为空,则头插并链接cur->_next = newcur;newTables[hashi] = cur;} else {// 当前表中的结点为空,则新结点直接放到这个表中newTables[hashi] = cur;cur->_next = nullptr;// 新插入的cur的_next不一定是空}cur = next;// 继续往下找}}_tables.swap(newTables);
}

因此链表法哈希表的整体代码为:

// 哈希表 --- 拉链法/哈希桶
template<class K, class V>
struct HashNode {typedef HashNode<K, V> Node;Node *_next;pair<K, V> _kv;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<>
struct HashFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {hash += e;e *= 131;}return hash;}
};template<class K, class V, class Hash = HashFunc<K>>
class HashTable {typedef HashNode<K, V> Node;
public:~HashTable() {for (int i = 0; i < _tables.size(); ++i) {_tables[i] = nullptr;}}HashTable(size_t size = 10) {_tables.resize(size, nullptr);// 各结点初始化为空}Node *Find(const K &key) {Hash hs;// 先算出映射到哪里int 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;Node *ret = Find(key);if (ret) {int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];Node *prev = nullptr;while (cur) {if (cur->_kv.first == key) {if (prev == nullptr)_tables[hashi] = cur->_next; // 要删除的就是表中的结点,那么将它的下一个结点放到表中elseprev->_next = cur->_next; // 要删除的是表中的结点的后继中的某个delete cur; // 删除该结点并退出循环break;}prev = cur;cur = cur->_next;}--_n;return true;} else {return false;}}bool Insert(const pair<K, V> &kv) {Hash hs;if (Find(kv.first))return false;// 找到了if (_n == _tables.size()) {// 扩容vector<Node *> newTables;newTables.resize(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); ++i) {Node *cur = _tables[i];while (cur) {Node *next = cur->_next; //记录下一个结点位置,防止找不到下一个结点// 映射到新表位置int hashi = hs(cur->_kv.first) % newTables.size();// 插入到新表位置Node *newcur = newTables[hashi];if (newcur) {// 当前表中的结点不为空,则头插并链接cur->_next = newcur;newTables[hashi] = cur;} else {// 当前表中的结点为空,则新结点直接放到这个表中newTables[hashi] = cur;cur->_next = nullptr;// 新插入的cur的_next不一定是空}cur = next;// 继续往下找}}_tables.swap(newTables);}// 先算出映射到哪里int hashi = hs(kv.first) % _tables.size();Node *cur = _tables[hashi];Node *newnode = new Node(kv);if (cur) {// 当前表中的结点不为空,则头插并链接newnode->_next = cur;_tables[hashi] = newnode;} else {// 当前表中的结点为空,则新结点直接放到这个表中_tables[hashi] = newnode;}++_n;return true;}private:vector<Node *> _tables;size_t _n = 0;
};

我们开放定址法和链表法用的扩容都是两倍扩容,但是官方用的是使用素数扩容,也就是每次扩容后哈希表的长度都是素数,据说这样会让元素更加分散。但其实并没有哈哈哈,Java底层就是2倍扩容。那么怎么做到差不多2倍扩容,每次扩容后哈希表长度都是素数呢?

size_t GetNextPrime(size_t prime) {const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (; i < PRIMECOUNT; ++i) {if (primeList[i] > prime)return primeList[i];}return primeList[i];
}

5、unordered_set的模拟实现

以下我们自己模拟实现的时候,unordered_set的模拟实现我们使用MyUnordered_Set类用于区分,unordered_map的模拟实现我们使用MyUnordered_Map类用于区分

5.1、改造哈希表(链表法)

结点的结构体需要修改一下,之前我们存的是pair<K,V>,但MyUnordered_Set是存K的,因此我们模版参数设置为一个参数T,MyUnordered_Set调用就是K,MyUnordered_Map调用就是pair<K,V>

template<class T>
struct HashNode {typedef HashNode<T> Node;Node *_next;T _data;HashNode(const T &data) : _data(data), _next(nullptr) {}};

哈希表需要增加一个模版参数:取Key的仿函数,因为MyUnordered_Set插入的是K,但是MyUnordered_Map插入的是pair<K,V>,我们需要从pair<K,V>中取出K。因此插入也要改成bool Insert(const T &data){}

这个仿函数分别放在MyUnordered_SetMyUnordered_Map的类中,用来传给哈希表。

MyUnordered_Set类中:

struct MapKeyOfT {K operator()(const pair<K, V> &kv) {return kv.first;}
};

MyUnordered_Map类中:

struct MapKeyOfT {K operator()(const pair<K, V> &kv) {return kv.first;}};

因此当前改造后的哈希表为:

template<class T>
struct HashNode {typedef HashNode<T> Node;Node *_next;T _data;HashNode(const T &data) : _data(data), _next(nullptr) {}};template<class K>
struct HashFunc {size_t operator()(const K &key) {return (size_t) key;}
};template<>
struct HashFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {hash += e;e *= 131;}return hash;}
};template<class K, class T, class KeyOfT, class Hash>
class HashTable {typedef HashNode<T> Node;
public:~HashTable() {for (int i = 0; i < _tables.size(); ++i) {_tables[i] = nullptr;}}HashTable(size_t size = 10) {_tables.resize(size, nullptr);// 各结点初始化为空}Node* Find(const K &key) {Hash hs;KeyOfT kot;// 先算出映射到哪里int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];while (cur) {if (kot(cur->_data) == key) {return cur;}cur = cur->_next;}return nullptr; // 没找到}bool Erase(const K &key) {Hash hs;KeyOfT kot;iterator ret = Find(key);if (ret != end()) {int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];Node *prev = nullptr;while (cur) {if (kot(cur->_data) == key) {if (prev == nullptr)_tables[hashi] = cur->_next; // 要删除的就是表中的结点,那么将它的下一个结点放到表中elseprev->_next = cur->_next; // 要删除的是表中的结点的后继中的某个delete cur; // 删除该结点并退出循环break;}prev = cur;cur = cur->_next;}--_n;return true;} else {return false;}}bool Insert(const T &data) {Hash hs;KeyOfT kot;if (Find(kot(data)))return false;// 找到了if (_n == _tables.size()) {// 扩容vector<Node *> newTables;newTables.resize(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); ++i) {Node *cur = _tables[i];while (cur) {Node *next = cur->_next; //记录下一个结点位置,防止找不到下一个结点// 映射到新表位置int hashi = hs(kot(cur->_data)) % newTables.size();// 插入到新表位置Node *newcur = newTables[hashi];if (newcur) {// 当前表中的结点不为空,则头插并链接cur->_next = newcur;newTables[hashi] = cur;} else {// 当前表中的结点为空,则新结点直接放到这个表中newTables[hashi] = cur;cur->_next = nullptr;// 新插入的cur的_next不一定是空}cur = next;// 继续往下找}}_tables.swap(newTables);}// 先算出映射到哪里int hashi = hs(kot(data)) % _tables.size();Node *cur = _tables[hashi];Node *newnode = new Node(data);if (cur) {// 当前表中的结点不为空,则头插并链接newnode->_next = cur;_tables[hashi] = newnode;} else {// 当前表中的结点为空,则新结点直接放到这个表中_tables[hashi] = newnode;}++_n;return true;}private:vector<Node *> _tables;size_t _n = 0;
};

还需要增加迭代器,用来遍历哈希表。

// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct __HTIterator {typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef __HTIterator<K, T, KeyOfT, Hash> Self;Node *_node;HT *_ht;__HTIterator(Node *node, HT *ht) : _node(node), _ht(ht) {}T &operator*() {return _node->_data;}T *operator->() {return &_node->_data;}Self &operator++() {if (_node->_next) {// 当前结点还有后继结点_node = _node->_next;} else {// 当前桶走完了KeyOfT kot;Hash hs;// 先算出当前在哪个哈希桶int hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;// 走到下一个桶while (hashi < _ht->_tables.size()) {if (_ht->_tables[hashi]) {//  这个桶有结点_node = _ht->_tables[hashi];break;} else++hashi;// 这个桶没有有结点}if (hashi == _ht->_tables.size()) {// 走到最后了,没找到下一个位置_node = nullptr;}}return *this;}bool operator!=(const Self &s) {return s._node != _node;}
};

改造后的哈希表(终极版):这里的插入的返回值由bool变为了pair<iterator,bool>,和官方保持一致。

template<class T>
struct HashNode {typedef HashNode<T> Node;Node *_next;T _data;HashNode(const T &data) : _data(data), _next(nullptr) {}};template<class K>
struct HashFunc {size_t operator()(const K &key) {return (size_t) key;}
};template<>
struct HashFunc<string> {size_t operator()(const string &str) {size_t hash = 0;for (auto e: str) {hash += e;e *= 131;}return hash;}
};// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct __HTIterator {typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef __HTIterator<K, T, KeyOfT, Hash> Self;Node *_node;HT *_ht;__HTIterator(Node *node, HT *ht) : _node(node), _ht(ht) {}T &operator*() {return _node->_data;}T *operator->() {return &_node->_data;}Self &operator++() {if (_node->_next) {// 当前结点还有后继结点_node = _node->_next;} else {// 当前桶走完了KeyOfT kot;Hash hs;// 先算出当前在哪个哈希桶int hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;// 走到下一个桶while (hashi < _ht->_tables.size()) {if (_ht->_tables[hashi]) {//  这个桶有结点_node = _ht->_tables[hashi];break;} else++hashi;// 这个桶没有有结点}if (hashi == _ht->_tables.size()) {// 走到最后了,没找到下一个位置_node = nullptr;}}return *this;}bool operator!=(const Self &s) {return s._node != _node;}
};template<class K, class T, class KeyOfT, class Hash>
class HashTable {friend __HTIterator<K, T, KeyOfT, Hash>;// 为了能访问_tablestypedef HashNode<T> Node;
public:typedef __HTIterator<K, T, KeyOfT, Hash> iterator;iterator begin() {for (int i = 0; i < _tables.size(); ++i) {if (_tables[i])return iterator(_tables[i], this);// 从左找到第一个哈希桶的第一个结点}return end();}iterator end() {return iterator(nullptr, this);}~HashTable() {for (int i = 0; i < _tables.size(); ++i) {_tables[i] = nullptr;}}HashTable(size_t size = 10) {_tables.resize(size, nullptr);// 各结点初始化为空}iterator Find(const K &key) {Hash hs;KeyOfT kot;// 先算出映射到哪里int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];while (cur) {if (kot(cur->_data) == key) {return iterator(cur, this);}cur = cur->_next;}return iterator(nullptr, this); // 没找到}bool Erase(const K &key) {Hash hs;KeyOfT kot;iterator ret = Find(key);if (ret != end()) {int hashi = hs(key) % _tables.size();Node *cur = _tables[hashi];Node *prev = nullptr;while (cur) {if (kot(cur->_data) == key) {if (prev == nullptr)_tables[hashi] = cur->_next; // 要删除的就是表中的结点,那么将它的下一个结点放到表中elseprev->_next = cur->_next; // 要删除的是表中的结点的后继中的某个delete cur; // 删除该结点并退出循环break;}prev = cur;cur = cur->_next;}--_n;return true;} else {return false;}}pair<iterator, bool> Insert(const T &data) {Hash hs;KeyOfT kot;iterator it = Find(kot(data));if (it != end())return make_pair(it, false);// 找到了if (_n == _tables.size()) {// 扩容vector<Node *> newTables;newTables.resize(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); ++i) {Node *cur = _tables[i];while (cur) {Node *next = cur->_next; //记录下一个结点位置,防止找不到下一个结点// 映射到新表位置int hashi = hs(kot(cur->_data)) % newTables.size();// 插入到新表位置Node *newcur = newTables[hashi];if (newcur) {// 当前表中的结点不为空,则头插并链接cur->_next = newcur;newTables[hashi] = cur;} else {// 当前表中的结点为空,则新结点直接放到这个表中newTables[hashi] = cur;cur->_next = nullptr;// 新插入的cur的_next不一定是空}cur = next;// 继续往下找}}_tables.swap(newTables);}// 先算出映射到哪里int hashi = hs(kot(data)) % _tables.size();Node *cur = _tables[hashi];Node *newnode = new Node(data);if (cur) {// 当前表中的结点不为空,则头插并链接newnode->_next = cur;_tables[hashi] = newnode;} else {// 当前表中的结点为空,则新结点直接放到这个表中_tables[hashi] = newnode;}++_n;return make_pair(iterator(_tables[hashi], this), true);}private:vector<Node *> _tables;size_t _n = 0;
};

5.2、MyUnordered_Set类的构成

其实就是套了一层哈希表,多了一个SetKeyOfT用来传给哈希表的模版参数。

#include "HashTable_Bucket.h"template<class K, class Hash = HashFunc<K>>
class MyUnordered_Set {struct SetKeyOfT {K operator()(const K &key) {return key;}};public:typedef typename HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;iterator begin() {return _hs.begin();}iterator end() {return _hs.end();}pair<iterator,bool> insert(const K &key) {return _hs.Insert(key);}bool erase(const K &key) {return _hs.Erase(key);}iterator find(const K &key) {return _hs.Find(key);}private:HashTable<K, const K, SetKeyOfT, Hash> _hs;
};

OKOK,C++ STL容器之unordered_set类就到这里。如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

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

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

相关文章

考研数学|武忠祥各阶段用书搭配及分享

看到有人问武忠祥老师&#xff0c;不请自来 武忠祥老师&#xff0c;绝对的宝藏老师&#xff0c;我在考研强化阶段的时候听过他的强化课程&#xff0c;听完之后&#xff0c;很多问题都想通了&#xff0c;所以&#xff0c;如果有人想问武忠祥老师行不行&#xff0c;那我就一个字…

短剧在线搜索PHP网站源码

源码简介 短剧在线搜索PHP网站源码&#xff0c;自带本地数据库500数据&#xff0c;共有6000短剧视频&#xff0c;与短剧猫一样。 搭建环境 PHP 7.3 Mysql 5.6 安装教程 1.上传源码到网站目录中 2.修改【admin.php】中&#xff0c; $username ‘后台登录账号’; $passwor…

(2022级)成都工业学院数据库原理及应用实验一:CASE工具概念数据模型建模

写在前面 1、基于2022级软件工程/计算机科学与技术实验指导书 2、代码仅提供参考 3、如果代码不满足你的要求&#xff0c;请寻求其他的途径 运行环境 window11家庭版 PowerDesigner 16.1 实验要求 某医院一个门诊部排班管理子系统涉及如下信息&#xff1a; 若干科室&a…

【.Net】Polly

文章目录 概述服务熔断、服务降级、服务限流、流量削峰、错峰、服务雪崩Polly的基本使用超时策略悲观策略乐观策略 重试策略请求异常响应异常 降级策略熔断策略与策略包裹&#xff08;多种策略组合&#xff09; 参考 概述 Polly是一个被.NET基金会支持认可的框架&#xff0c;同…

通过 Cookie、Redis共享Session 和 Spring 拦截器技术,实现对用户登录状态的持有和清理(四)

本篇内容对应 “2.5 开发登录、退出功能” 小节 “4.7 优化登陆模块” 小节 2.6 显示登录信息 2.7 账号设置 2.8 检查登录状态 登录功能的流程是什么&#xff1f; UUID为什么不会重复&#xff1f; 因为UUID是基于mac物理地址、时间戳、随机数等信息生成。因此UUID居于极高的唯…

【鸿蒙开发】ArkTS和组件

1. 初识ArkTS语言 ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript生态基础上做了进一步扩展&#xff0c;继承了TS的所有特性。 当前&#xff0c;ArkTS在TS的基础上主要扩展了如下能力&#xff1a; 基本语法&#xff1a;ArkTS定义了声明式UI描述、自…

Java事件处理机制

一、介绍 java事件处理是采取“委派事件模型”。当事件发生时&#xff0c;产生事件的对象&#xff0c;会把此“信息”传递给"事件的监听者"处理&#xff0c;这里所说的"信息"实际上就是java.awt.event事件类库里某个类所创建的对象&#xff0c;把它称为&q…

2024年AI带来的革命性变革与创新

大家好&#xff01;相信大家对于AI&#xff08;人工智能&#xff09;的发展已经有了一定的了解&#xff0c;但你是否意识到&#xff0c;到了2024年&#xff0c;AI已经变得如此强大和普及&#xff0c;带来了我们从未想象过的便利和创新呢&#xff1f;让我们一起来看看AI在这个时…

基于springboot+vue+Mysql的职称评审管理系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

4.9日总结

1.MySQL概述 1.数据库基本概念&#xff1a;存储数据的仓库&#xff0c;数据是有组织的进行存储 2.数据库管理系统&#xff1a;操纵和管理数据库的大型软件 3.SQL&#xff1a;操作关系型数据库的编程语言&#xff0c;定义了一套操作型数据库统一标准 2.MySQL数据库 关系型数…

数据结构---顺序表实现

目录 1.顺序表 2.动态顺序表的实现 &#xff08;4&#xff09;顺序表初始化 &#xff08;5&#xff09;顺序表销毁 &#xff08;6&#xff09;顺序表的插入 a.尾插 b.头插 &#xff08;7&#xff09;顺序表的删除 a.尾删 b.头删 &#xff08;8&#xff09;指定位置之…

GitHub 仓库 (repository) Watch - Star - Fork - Follow

GitHub 仓库 [repository] Watch - Star - Fork - Follow References 眼睛图标旁边写着 Watch 字样。点击这个按钮就可以 Watch 该仓库&#xff0c;今后该仓库的更新信息会显示在用户的公开活动中。Star 旁边的数字表示给这个仓库添加 Star 的人数。这个数越高&#xff0c;代表…

HuggingFists-如何复用流程

在使用HuggingFists进行业务流程编写时&#xff0c;很可能会发现多个流程间拥有相同的流程片段。按照通常的思考&#xff0c;如果能将这些相同的片段封装为一个可复用的子流程&#xff0c;在各个主流程中引用使用&#xff0c;就可以避免每次重复编写该片段&#xff0c;省去不必…

泰迪智能科技高职人工智能专业人才培养方案

人工智能行业近年来得到了快速发展&#xff0c;全球科技公司都在竞相投入人工智能的研发&#xff0c;从硅谷到北京&#xff0c;都在人工智能上取得了显著的进步。人工智能已经从学术研究转变为影响制造业、医疗保健、交通运输和零售等多个行业的关键因素。我国政策的积极推动下…

Windows下docker-compose部署DolphinScheduler

参照&#xff1a;快速上手 - Docker部署(Docker) - 《Apache DolphinScheduler v3.1.0 使用手册》 - 书栈网 BookStack 下载源文件 地址&#xff1a;https://dolphinscheduler.apache.org/zh-cn/download/3.2.1 解压到指定目录&#xff0c;进入apache-dolphinscheduler-xxx-…

4.2.4 理解路由器数据包过程

1、实验目的 通过本实验可以掌握&#xff1a; 了解IP路由原理了解数据包封装和解封装的概念了解路由器路由和交换过程 2、实验拓扑 观察路由器路由数据包过程的实验拓扑如图4-3所示&#xff0c;设备接口地址信息如表4-2所示。 图4-3 观察路由器路由数据包过程的实验拓扑 本…

H.265视频直播点播录像EasyPlayer.js流媒体播放器用户常见问题及解答

EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;无须安装任何插件&#xff0c;起播快、延迟低、兼容性强&#xff0c;使用非常便捷。 今天我们来汇总下用户常见的几个问题及解答。 1、EasyPlayer.js播放多路H.265视…

More than one argument (#0 and left as delegating for Creator) 报错解决

"stack_trace": "c.f.j.d.e.InvalidDefinitionException:Invalid type definition for type `com.xx.xx.x.x.x

软件设计—接口安全设计规范

1.token授权机制 2.https传输加密 3.接口调用防滥用 4.日志审计里监控 5.开发测试环境隔离&#xff0c;脱敏处理 6.数据库运维监控审计 软件项目相关全套精华资料包获取方式①&#xff1a;点我获取 获取方式②&#xff1a;本文末个人名片直接获取。

Golang | Leetcode Golang题解之第13题罗马数字转整数

题目&#xff1a; 题解&#xff1a; var symbolValues map[byte]int{I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000}func romanToInt(s string) (ans int) {n : len(s)for i : range s {value : symbolValues[s[i]]if i < n-1 && value < symbolValues[s…