【C++】unordered_set 容器的最全解析(什么是unordered_set?unordered_set的常用接口有那些?)

2024-08-31 18:44

本文主要是介绍【C++】unordered_set 容器的最全解析(什么是unordered_set?unordered_set的常用接口有那些?),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、前言

 二、预备知识 

💢关联式容器💢 

💢键值对💢  

💢哈希结构的关联式容器💢  

三、unordered_set 详解  

🔥unordered_set 的介绍 

🔥unordered_set 的构造 

🔥unordered_set 的使用 

🥝 insert

🍇find 

🍍 erase

🍉size 

🍋empty 

🍓swap 

🍌count

🍈迭代器 

🔥set 和 unordered_set 的区别 

🍐性能测试 

四、常考面试题 

五、共勉


一、前言

【unordered_set】  是 STL 中的容器之一,不同于普通容器,它的查找速度极快,常用来存储各种经常被检索的数据,因为容器的底层是【哈希表。除此之外,还可以借助其特殊的性质,解决部分难题。

 二、预备知识 

 在正式学习 unordered_set 之前,首先要有一些预备知识,否则后面可能看不懂相关操作 

💢关联式容器💢 

在以往的 【STL 容器学习中,我们接触到的都是 序列式容器,比如 stringvectorlistdeque 等,序列式容器的特点就是 底层为线性序列的数据结构,就比如 list,其中的节点是 线性存储 的,一个节点存储一个元素,其中存储的元素都可序,但未必有序  

  • 关联式容器 则比较特殊,其中存储的是 <key, value> 的 键值对,这就意味着可以按照 键值大小 key 以某种特定的规则放置于适当的位置,关联式容器 没有首尾的概念,因此没有头插尾插等相关操作,本文中学习的 unordered_set 就属于 关联式容器 

注意: stackqueue 等适配器也属于序列式容器,因为他们的底层是 deque 等容器 


💢键值对💢  

键值对】是 一种用来表示具有一一对应关系的结构,该结构中一般只包含两个成员变量:key 和 value,前者表示 键值,后者表示 实值 关联式容器的实现离不开【键值对

因此在标准库中,专门提供了这种结构  pair 定义如下 : 

//SGI 版 STL 中的实现
template <class T1, class T2>
struct pair {typedef T1 first_type;typedef T2 second_type;T1 first;	T2 second;	pair() : first(T1()), second(T2()) {}pair(const T1& a, const T2& b) : first(a), second(b) {}#ifdef __STL_MEMBER_TEMPLATEStemplate <class U1, class U2>pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {}
#endif
};
  • pair 中的 first 表示 键值second 则表示 实值,在给 【关联式容器】 中插入数据时,可以构建 pair 对象 

 比如下面就构建了一个 键值 key 为 string实值 value 为 int 的匿名 键值对 pair 对象 

pair<string, int>("hehe", 123);
  • 可以将此匿名对象传入 关联式容器 中,当然这样写未免过于麻烦了,于是库中设计了一个函数模板 make_pair,可以根据传入的参数,去调用 pair 构建对象并返回
make_pair("hehe", 123);	//构建出的匿名对象与上面的一致

make_pair 的定义如下所示:  

template <class T1,class T2>
pair<T1,T2> make_pair (T1 x, T2 y)
{return ( pair<T1,T2>(x,y) );
}
  • 该函数实际会被编译器优化为 内联函数,因此不会造成过多消耗,可以放心使用 

💢哈希结构的关联式容器💢  

所以在 C++ 标准中,共提供了四种 哈希结构的关联式容器  

  • unordered_set
  • unordered_multiset
  • unordered_map
  • unordered_multimap

关于 树形结构的关联式容器 将在 二叉搜索树 中学习

树型结构与哈希结构的关联式容器功能都是一模一样的,不过 哈希结构查找比树型结构快得多 -> O(1)

注:

  • STL 中选择的树型结构为 红黑树 RB-Tree
  • 树型结构中的元素 中序遍历 后有序,而哈希结构中的元素无序

三、unordered_set 详解  

在C++98中,STL提供了底层为 红黑树结构 的一系列关联容器,在查询时效率可以达到 log(N)。但是在较差的情况下 ,需要比较红黑树的高度次,当树中节点非常多的时候,查询效率也会不理想达到 log(N)

  • 最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了 4 个 unordered系列的关联式容器。 

  •  接下来我们将会对,unordered_set 进行详细的介绍,其余的容器将会在后续的文章中讲述。

🔥unordered_set 的介绍 

unordered_ set存储 没有特定顺序的唯一元素的容器,允许基于它们的值快速检索单个元素。

  • unordered_ set 中,元素的值与唯一标识它的键同时存在。 键是不可变的,因此,unordered_ set 中的元素在容器中不能被修改,但是它们可以被插入和删除
  • 在内部,unordered_ set 中的元素不按任何特定顺序排序,而是根据它们的哈希值组织到桶中,以允许直接根据它们的值快速访问单个元素(平均平均时间复杂度恒定)。 
  • unordered_ set 容器在通过键访问单个元素时比 set容器更快,尽管它们在通过元素子集进行范围迭代时通常效率较低。 
  • 容器中的迭代器至少是前向迭代器。 


🔥unordered_set 的构造 

构造一个 unordered_ set 容器对象,根据使用的构造函数版本初始化其内容,我们主要掌握3种方式即可:

(1)构造一个某个类型的容器 

unordered_set<int> s1; // 构造int类型的空容器

(2)拷贝构造某个类型的容器 

unordered_set<int> us2(us1); // 拷贝构造同类型容器us1的复制品

(3)使用迭代器区间进行初始化构造 

  •  构造一个 unordered_set 对象,其中包含 【first ,last )中的每一个元素副本。
string str("helloworld");
unordered_set<char> us3(str.begin(), str.end()); // 构造string对象某段区间的复制品

🔥unordered_set 的使用 

 unordered_set 的成员函数主要分为:迭代器,容量操作,修改操作。

  • 需要注意的是,对于unordered_ set 而言,它存储的数据是无序的,并组它是一个单向迭代器。

  •  我这里举几个用的例子,帮助大家理解

🥝 insert

unordered_ set 中插入新元素。

  • 每个元素只有在它不等同于容器中已经存在的任何其他元素时才会被插入,也就是说unordered_ set 中的每个元素是唯一的。 

代码示例: 

void test_unordered()
{unordered_set<int> us1;// 插入元素us1.insert(4);us1.insert(5);us1.insert(2);us1.insert(2);us1.insert(1);us1.insert(3);us1.insert(3);// 遍历for (auto e : us1){cout << e << " ";}
}
  •  可以看到当插入重复元素时,重复元素 是去掉了的,并且没有进行排序。


🍇find 

在容器中搜索值为 k 的元素,如果找到它,则返回一个迭代器,否则返回unordered_ set::end (容器末端之 前的元素)的迭代器。 

代码示例: 

void test_unordered()
{unordered_set<int> us;// 插入元素us.insert(4);us.insert(5);us.insert(2);us.insert(2);us.insert(1);us.insert(3);us.insert(3);unordered_set<int>::iterator pos = us.find(3);if (pos != us.end()){cout << "3存在" << endl;}
}


🍍 erase

 从 unordered_ set 容器中移除单个元素或一组元素( [first,last) )。

  • 通过调用每个元素的析构函数,这有效地减少了容器的大小。 

(1)从容器中删除单个元素(搭配find使用) 

void test_unordered()
{unordered_set<int> us;// 插入元素us.insert(4);us.insert(5);us.insert(2);us.insert(2);us.insert(1);us.insert(3);us.insert(3);unordered_set<int>::iterator pos = us.find(3);if (pos != us.end()){us.erase(pos); // 删除元素3cout << "删除成功" << endl;}else{cout << "删除失败" << endl;}// 遍历for (auto e : us){cout << e << " ";}
}
  • 可以看到 3 已经被删除了 

(2)从容器中删除单个元素(直接传要删除的元素) 

void test_unordered()
{unordered_set<int> us;// 插入元素us.insert(4);us.insert(5);us.insert(2);us.insert(2);us.insert(1);us.insert(3);us.insert(3);us.erase(5); // 删除元素5// 遍历for (auto e : us){cout << e << " ";}
}
  •  可以看到 5 已经被删除了

那么它和第 1 种的区别是什么呢?

  • erase(x) :如果 x 存在就删除;如果不存在,不做任何改变
  • erase(pos):如果 x 存在就删除;如果不存在,此时 pos 位置指向 set::end 的迭代器,那么程序就会报错。

其实这种方法本质上可以理解为 erase 去调用了 迭代器find


🍉size 

返回 unordered_set 容器中的元素数量 

代码示例: 

void test_unordered()
{unordered_set<string> us;// 构造元素us = { "milk", "potatoes", "eggs" };cout << "size: " << us.size() << endl;// 插入元素us.insert("pineapple");cout << "size: " << us.size() << endl;// 插入重复元素us.insert("milk");cout << "size: " << us.size() << endl;
}


🍋empty 

 返回一个bool值,指示unordered_ set 容器是否为空,即其大小是否为0。

  • 这个函数不会以任何方式修改数组的内容。

 代码示例:

void test_unordered()
{// us1构造3个元素unordered_set<string> us1 = { "milk", "potatoes", "eggs" };// us2构造一个空容器unordered_set<string> us2;cout << "us1 " << (us1.empty() ? "is empty" : "is not empty") << endl;cout << "us2 " << (us2.empty() ? "is empty" : "is not empty") << endl;
}


🍓swap 

通过 ust 的内容交换容器的内容, ust 另一个包含相同类型元素的 unordered_ set 对象。大小可能不同。

  • 这个函数在容器之间交换指向数据的内部指针,而不实际对单个元素执行任何复制或移动,允许常量时间执行,无论大小如何。 

代码示例:

void test_unordered()
{unordered_set<string> us1 = { "iron","copper","oil" };unordered_set<string> us2 = { "wood","corn","milk" };// 交换容器的内容us1.swap(us2);// 遍历us1for (const string& x1 : us1){cout << x1 << " ";}cout << endl;// 遍历us2for (const string& x2 : us2){cout << x2 << " ";}
}


🍌count

在容器中搜索值为 k 的元素,并返回找到的元素数。

  • 因为 unordered_ set 容器不允许重复值,这意味着如果容器中存在具有该值的元素,则函数实际返回1,否则返回 0。

代码示例:

void test_unordered()
{unordered_set<string> us = { "hat", "umbrella", "suit" };// 容器中值为"hat"的元素个数cout << us.count("hat") << endl;// 容器中值为"red"的元素个数cout << us.count("red") << endl;
}


🍈迭代器 

 unordered_set 当中迭代器相关函数如下:

  注意: set 是双向迭代器,而unordered_ set 是单向迭代器


🔥set 和 unordered_set 的区别 

setunordered_set 是 C++ 标准模板库(STL)中的两种关联容器,它们都有存储唯一元素的特性,但它们在底层实现、元素存储顺序、查找和插入的性能上存在显著的区别。 

  • set使用自平衡二叉搜索树实现,元素是有序的,适合需要有序存储的场景,操作的时间复杂度是 O(log⁡n)。 
  • unordered_set使用哈希表实现,元素是无序的,适合只关心元素存在性而不关心顺序的场景,操作的时间复杂度在理想情况下是 O(1)。 

🍐性能测试 

 下面是性能测试代码,包含 大量重复、部分重复、完全有序 三组测试用例,分别从 插入、查找、删除 三个维度进行对比

  • 注:测试性能用的是 Release 版,这里的基础数据量为 100 w 
#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>using namespace std;int main()
{const size_t N = 1000000;unordered_set<int> us;set<int> s;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; ++i){//v.push_back(rand());	//大量重复//v.push_back(rand()+i);	//部分重复//v.push_back(i);	//完全有序}size_t begin1 = clock();for (auto e : v){s.insert(e);}size_t end1 = clock();cout << "set insert:" << end1 - begin1 << endl;size_t begin2 = clock();for (auto e : v){us.insert(e);}size_t end2 = clock();cout << "unordered_set insert:" << end2 - begin2 << endl;size_t begin3 = clock();for (auto e : v){s.find(e);}size_t end3 = clock();cout << "set find:" << end3 - begin3 << endl;size_t begin4 = clock();for (auto e : v){us.find(e);}size_t end4 = clock();cout << "unordered_set find:" << end4 - begin4 << endl << endl;cout << s.size() << endl;cout << us.size() << endl << endl;;size_t begin5 = clock();for (auto e : v){s.erase(e);}size_t end5 = clock();cout << "set erase:" << end5 - begin5 << endl;size_t begin6 = clock();for (auto e : v){us.erase(e);}size_t end6 = clock();cout << "unordered_set erase:" << end6 - begin6 << endl << endl;return 0;
}

插入大量重复数据 

插入数据 大量重复 ---- 结果:

  • 插入:哈希 比 红黑 快 88%
  • 查找:哈希 比 红黑 快 100%
  • 删除:哈希 比 红黑 快 37%

总的来说,在数据 随机 的情况下,哈希各方面都比红黑强,在数据 有序 的情况下,红黑更胜一筹

单就 查找 这一个方面来说:哈希 一骑绝尘,远远的将红黑甩在了身后


四、常考面试题 

题目:最长连续序列
链接:128. 最长连续序列 - 力扣(LeetCode)

题目分析: 

  • 对于数组中存在的连续序列,为了统计每个连续序列的长度,我们希望直接定位到每个连续序列的起点,从起点开始遍历每个连续序列,从而获得长度。 

那么如何获取到每个连续序列的起点呢,或者说什么样的数才是一个连续序列的起点?

  • 答案是这个数的前一个数不存在于数组中,因为我们需要能够快速判断当前数num的前一个数num - 1是否存在于数组中。

同时当我们定位到起点后,我们就要遍历这个连续序列,什么时候是终点呢?

  • 答案是当前数num的后一个数nunm + 1不存在于数组中,因此我们需要能够快速判断当前数num的后一个数num + 1是否存在于数组中。

为了实现上述需求,我们使用哈希表来记录数组中的所有数以实现对数值的快速查找

class Solution {
public:int longestConsecutive(vector<int>& nums) {// 记录最长连续序列的长度int res = 0;unordered_set<int> num_set(nums.begin(),nums.end());int seqlen;for(auto ch : num_set){// 如果当前的数是一个连续序列的起点,统计这个连续序列的长度// 如果 ch - 1 不在 num_set 中,则返回 true;如果 ch - 1 在 num_set 中,则返回 false。if(!num_set.count(ch-1)){seqlen = 1; // 连续序列的长度,初始为1while(num_set.count(++ch)){seqlen++; // 不断查找连续序列,直到num的下一个数不存在于数组中}res = max(res,seqlen);}}return res;} 
};

五、共勉

以下就是我对 【C++】unordered_set 容器 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新【C++】请持续关注我哦!!! 

这篇关于【C++】unordered_set 容器的最全解析(什么是unordered_set?unordered_set的常用接口有那些?)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1124701

相关文章

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

Golang HashMap实现原理解析

《GolangHashMap实现原理解析》HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持高效的插入、查找和删除操作,:本文主要介绍GolangH... 目录HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

Python使用getopt处理命令行参数示例解析(最佳实践)

《Python使用getopt处理命令行参数示例解析(最佳实践)》getopt模块是Python标准库中一个简单但强大的命令行参数处理工具,它特别适合那些需要快速实现基本命令行参数解析的场景,或者需要... 目录为什么需要处理命令行参数?getopt模块基础实际应用示例与其他参数处理方式的比较常见问http

Python利用ElementTree实现快速解析XML文件

《Python利用ElementTree实现快速解析XML文件》ElementTree是Python标准库的一部分,而且是Python标准库中用于解析和操作XML数据的模块,下面小编就来和大家详细讲讲... 目录一、XML文件解析到底有多重要二、ElementTree快速入门1. 加载XML的两种方式2.

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类