序列式容器和关联式容器
序列式容器
前⾯已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器
关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。本章节讲解set是key搜索场景的结构,
一、set的概念
set的声明如下,T就是set底层关键字的类型
- 在 C++ 中,set是一种关联容器。它是按照特定顺序存储唯一元素的容器。
这些元素在set中是有序的(默认按照小于(<)关系排序),并且每个元素只能出现一次。 - set默认要求T⽀持小于比较,如果不⽀持或者想按自己的需求⾛可以自行实现仿函数传给第⼆个模
版参数 - set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
- ⼀般情况下,我们都不需要传后两个模版参数。
- set底层是用红⿊树实现,增删查效率是O(logN) ,迭代器遍历是⾛的搜索树的中序,所以是有序的。O(logN)
- 通常set底层是基于红黑树(Red - Black Tree)实现的。
- 红黑树是一种自平衡二叉查找树,这使得set具有以下特性:查找、插入和删除操作的时间复杂度在平均和最坏情况下都是对数时间O(log n),其中n是set中的元素个数。
二、set&multiset的使用
set的构造和迭代器
set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
// empty (1) ⽆参默认构造
explicit set(const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// range (2) 迭代器区间构造
template <class InputIterator>
set(InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type & = allocator_type());
// copy (3) 拷⻉构造
set(const set& x);
// initializer list (5) initializer 列表构造
set(initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// 迭代器是⼀个双向迭代器
iterator->a bidirectional iterator to const value_type
// 正向迭代器
iterator begin();
iterator end();
// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
set的增删查
Member types
key_type->The first template parameter(T)
value_type->The first template parameter(T)
// 单个数据插⼊,如果已经存在则插⼊失败
pair<iterator, bool> insert(const value_type& val);
// 列表插⼊,已经在容器中存在的值不会插⼊
void insert(initializer_list<value_type> il);
// 迭代器区间插⼊,已经在容器中存在的值不会插⼊
template <class InputIterator>
void insert(InputIterator first, InputIterator last);
// 查找val,返回val所在的迭代器,没有找到返回end()
iterator find(const value_type& val);
// 查找val,返回Val的个数
size_type count(const value_type& val) const;
// 删除⼀个迭代器位置的值
iterator erase(const_iterator position);
// 删除val,val不存在返回0,存在返回1
size_type erase(const value_type& val);
// 删除⼀段迭代器区间的值
iterator erase(const_iterator first, const_iterator last);
// 返回⼤于等val位置的迭代器
iterator lower_bound(const value_type& val) const;
// 返回⼤于val位置的迭代器
iterator upper_bound(const value_type& val) const;
insert和迭代器遍历使用
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 去重+升序排序
set<int> s;
// 去重+降序排序(给⼀个⼤于的仿函数)
//set<int, greater<int>> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
//set<int>::iterator it = s.begin();
auto it = s.begin();
while (it != s.end())
{
// error C3892: “it”: 不能给常量赋值
// *it = 1;
cout << *it << " ";
++it;
}
cout << endl;
// 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败
s.insert({ 2,8,3,9 });
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
set<string> strset = { "sort", "insert", "add" };
// 遍历string⽐较ascll码⼤⼩顺序遍历的
for (auto& e : strset)
{
cout << e << " ";
}
cout << endl;
}
这段代码展示了set容器在去重、排序以及插入元素等方面的特性,对于理解 C++ 中set容器的基本用法很有帮助。同时,需要注意在遍历过程中不能修改set中的元素,否则会引发编译错误,如尝试对常量赋值*it = 1;会导致编译错误error C3892: “it”: 不能给常量赋值。
find和erase使用
find
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<int> s = { 4,2,7,2,8,5,9 };
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 删除最⼩值
s.erase(s.begin());
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 直接删除x
int x;
cin >> x;
int num = s.erase(x);
if (num == 0)
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 直接查找在利⽤迭代器删除x
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
else
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 算法库的查找 O(N)
auto pos1 = find(s.begin(), s.end(), x);
// set⾃⾝实现的查找 O(logN)
auto pos2 = s.find(x);
// 利⽤count间接实现快速查找
cin >> x;
if (s.count(x))
{
cout << x << "在!" << endl;
}
else
{
cout << x << "不存在!" << endl;
}
return 0;
}
这段代码全面地展示了对set容器的各种操作和不同查找方法的性能差异,有助于更好地理解和掌握 C++ 中set容器的使用。同时,通过对比算法库中的查找和set自身的查找方法,强调了在特定场景下选择合适方法的重要性。
erase
#include<iostream>
#include<set>
using namespace std;
int main()
{
std::set<int> myset;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
// 实现查找到的[itlow,itup)包含[30, 60]区间
// 返回 >= 30
auto itlow = myset.lower_bound(30);
// 返回 > 60
auto itup = myset.upper_bound(60);
// 删除这段区间的值
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
return 0;
}
这段代码有效地演示了如何利用set容器的特定函数来实现区间查找和删除操作,对于需要对set中的元素进行范围操作的场景非常有用。同时,也展示了lower_bound和upper_bound函数在处理有序集合时的强大功能。
三、set&multiset的差异
1.元素的唯一性
set:
- 在set容器中,每个元素都是唯一的。如果尝试插入一个已经存在于set中的元素,该操作将不会改变set的内容。例如,若有一个set,已经包含元素5,再次插入5时,set中仍然只有一个5。
multiset:
- 而multiset允许存在重复的元素。同样对于multiset,如果已经有一个5,再插入5,multiset中将有两个5。
元素的排序性
2.两者相同点:
- set和multiset中的元素都是有序的。默认情况下,它们按照元素的小于(<)关系进行排序。例如,对于存储整数的set或multiset,元素会按照从小到大的顺序排列。
不同点(主要体现在操作上): - 在set中,由于元素的唯一性,一些操作(如查找、删除特定元素)相对简单,因为每个元素只有一个实例。而在multiset中,由于存在重复元素,操作会稍有不同。例如,当删除multiset中的一个元素时,需要考虑是删除所有的该元素实例还是只删除一个实例。
3.迭代器操作的区别
set:
- 由于set中元素唯一,迭代器在遍历set时,每个元素只会被访问一次。例如,使用迭代器遍历set时,不会出现相同的整数被多次迭代到的情况。
multiset:
- 在multiset中,由于存在重复元素,迭代器可能会多次指向相同值的不同实例。例如,在一个包含多个5的multiset中,迭代器在遍历过程中会多次遇到值为5的元素。
4.适用场景
set:
- 适用于需要存储一组唯一且有序的元素的情况。比如存储一个班级学生的学号,每个学号应该是唯一的,并且可能需要按照学号顺序进行一些操作(如查找某个学号是否存在等)。
multiset:
- 适合于允许元素重复且需要保持有序性的场景。例如,统计一个班级学生的考试成绩,可能会有多个学生得到相同的分数,此时使用multiset来存储分数就比较合适,可以方便地进行一些与分数分布相关的操作,如找出某个分数出现的次数等。
multiset和set的使⽤基本完全类似,主要区别点在于multiset⽀持值冗余,那么insert/find/count/erase都围绕着⽀持值冗余有所差异,具体参看下⾯的样例代码。
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 相⽐set不同的是,multiset是排序,但是不去重
multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
cout << endl;
// 相⽐set不同的是,count会返回x的实际个数
cout << s.count(x) << endl;
// 相⽐set不同的是,erase给值时会删除所有的x
s.erase(x);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
return 0;
}
四、set的练习
两个数组的交集
给定两个数组 nums1 和 nums2 ,返回它们的交集 。输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序 。
思路:
1.创建两个set容器:
- 将两个输入数组nums1和nums2分别转换为set容器,这样可以利用set自动去重和快速查找的特性。
- 假设set1存储nums1的元素,set2存储nums2的元素。这个步骤的时间复杂度取决于数组的长度,为O(nlogn)和O(mlogm),其中n是nums1的长度,m是nums2的长度。
2.寻找交集:
- 创建一个新的set容器用于存储交集结果。
- 遍历较小的那个set容器中的元素,对于每个元素,在另一个set中使用find函数进行查找。
- 如果找到,则将该元素插入到结果set中。这个步骤的时间复杂度为O(min(n,m)),因为最多遍历较小的那个set。
3.将交集结果转换为数组:
- 如果需要将交集以数组形式返回,可以将结果set中的元素逐个复制到一个新的数组中。这个步骤的时间复杂度取决于交集的大小,为O(K),其中k是交集的元素个数。
总的时间复杂度为O(nlogn+mlogm+min(n,m)),其中n和m分别是两个输入数组的长度。空间复杂度取决于交集的大小以及存储两个set所需的空间,为O(n+m)。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
// 因为set遍历是有序的,有序值,依次⽐较
// ⼩的++,相等的就是交集
vector<int> ret;
auto it1 = s1.begin();
auto it2 = s2.begin();
while (it1 != s1.end() && it2 != s2.end())
{
if (*it1 < *it2)
{
it1++;
}
else if (*it1 > *it2)
{
it2++;
}
else
{
ret.push_back(*it1);
it1++;
it2++;
}
}
return ret;
}
};
环形链表
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路:
1.创建一个set容器:
- 创建一个set用于存储遍历过的节点指针。
2.遍历链表:
- 从链表的头节点head开始遍历链表。
- 对于每个节点,检查它的指针是否已经在set中。
- 如果不在,将该节点的指针插入到set中,然后继续遍历下一个节点。
- 如果当前节点的指针已经在set中,说明找到了环形链表的入口节点,返回该节点。
这个算法的时间复杂度是O(n),其中n是链表的节点个数。因为在最坏的情况下,需要遍历整个链表。空间复杂度也是 (O (n),因为可能需要将所有的节点指针都存储在set 中。
总结起来,利用set的特性可以快速判断一个节点是否已经被遍历过,从而找到环形链表的入口节点。但这种方法需要额外的空间来存储节点指针。
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
set<ListNode*> s;
ListNode* cur = head;
while (cur)
{
auto ret = s.insert(cur);
if (ret.second == false)
return cur;
cur = cur->next;
}
return nullptr;
}
};
五、总结
特点
- 元素唯一性:set中的元素是唯一的,不允许重复。在插入元素时,如果尝试插入已存在的元素,set不会进行任何改变。
- 有序性:默认情况下,set中的元素按照升序排列。可以通过自定义比较函数来改变排序规则。
常用操作
- 创建与初始化:
std::set mySet; 创建一个空的存储int类型元素的set。
std::set anotherSet = {3, 1, 2}; 创建并初始化一个包含特定元素的set。 - 插入元素:
mySet.insert(5); 插入单个元素。插入操作的平均时间复杂度为O(logn),其中n是set中元素的个数。
删除元素:
mySet.erase(5); 删除特定值的元素。
auto it = mySet.begin(); mySet.erase(it); 通过迭代器删除特定位置的元素。删除操作的平均时间复杂度为O(logn)。 - 查找元素:
auto it = mySet.find(3); 如果元素存在,返回指向该元素的迭代器;如果不存在,返回等于mySet.end()的迭代器。查找操作的平均时间复杂度为O(logn)。
应用场景
- 当需要存储不重复的元素集合,并且希望能够快速查找、插入和删除元素时,set是一个很好的选择。
- 可以用于去除重复元素,例如从一个包含重复元素的数组中提取出唯一的元素集合。
- 在需要对元素进行排序且不允许重复的情况下,set可以自动完成排序和去重的工作。