set 认识及使用
序列式容器和关联式容器
之前学过的STL中string、vector、list、deque、array、forward_list等容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换⼀下,他依旧是序列式容器。
关联式容器 与序列式容器不同的是 关联式容器逻辑结构通常是非线性结构 两个位置有紧密的关联关系 交换⼀下他的存储结构就被破坏了 关联式容器map/set unordered_map/ unordered_set
set
set其实就是上节二叉搜索树实现的key问题 不过底层是二叉搜索树优化后的红黑树
第一个参数就是要里面存的类型 第二个参数就是之前的仿函数方式 默认支持小于比较 也就是左树都小于根节点的值 右树大于根节点值 第三个参数是空间配置器的内容 这里先不管
第二第三个参数都有缺省值 一般情况下也只需要传第一个参数就可以满足我们的需求
set迭代器和之前的list一样是双向迭代器 支持++ --等的功能 所以需要用到++ --的一些算法set就是可以支持的
迭代器方式来打印是按照中序遍历的方式来进行的
如下 第二个个参数不传 迭代器打印默认为升序 当显示传第二个参数的为greater<int>时候 就按照逆序打印了
set里面不允许插入重复的数据 当插入的数据在set里面已经有了的话 重复的数据不会插进去 但是也不会报错
所以我们使用set这个容器插入一些数据的时候 就会完成去重+升序排序的功能 另外set容器是不支持改的功能(因为底层的结构 改数据会破坏底层结构)
也支持在定义的时候再{}里面一下插入很多值
这是之前学习过的c+11的initializer_list 这也是一种模版类型 可以支持在创建时候在{}里面写值对它初始化 那么在insert里面写一个支持initializer_list类型的构造函数(和insert下面的第四个类型)后就可以支持这样插入了
到时候传的{}里面的值会先隐式转换为initialiter_list 然后再拷贝构造传实参过去进行构造 这样的方式其实底层就是一个一个插数据 同样也不能插入重复的数据 这里的insert的返回值pair先不用管
也可以存string类型 会从第一位开始按照ASCII的方式比较
erase这里有三种方式
先看第二种方式 删除掉一个值后会返回size_t类型 其实就是删除成功返回1删除失败(里面没有)返回0 那为什么不用bool类型呢
这是为了和multiset统一 multisrt可以支持重复数据的插入删除查找 所以删除的时候删除一个值可以同时删除多个位置的值
第三种删除迭代器区间范围内的值后会返回此时传的第二个参数位置的迭代器
第一种方式就是删除一个迭代器位置的值 如下set是按照默认的方式是左树小右树大的方式 那么begin迭代器最左边节点就是最小值位置 那么用这种方式删除begin位置的值就是在删除了最小值
在创建对象的时候 模版第二个参数传greater<int>这样左树的值都大于根节点值 右树值都小于根节点的值 这样begin指向的位置就是最左边节点的最大值 删除begin位置的节点也就是删除了最大值
迭代器失效问题
这里的迭代器失效问题有两种
上一篇学习了二叉搜索树的erase时候我们知道删除的方式有两种 1.如果删除节点只有一个孩子节点或者没有孩子节点的话会采用直接删除的方式(下图左) 2.如果有两个孩子节点 采取先用一个值和这个节点交换了 然后删除(下图右)
这两种情况都会导致迭代器失效 第二种虽然迭代器指向的节点没有释放但是迭代器的意义改变了 也算迭代器失效 对于vs下这两种情况都会直接报错
stl中set的erase在删除后会返回下一个位置的迭代器 所以在使用之后可以让迭代器进行更新
find会返回要找的值位置的迭代器 如果没有找到就会返回end位置的迭代器
如下 如果没有找到的话 最终会返回end位置迭代器打印没找到
它的查找是从根节点开始查找 判断一次之后就会到下一个节点 时间复杂度O(logN)
swap会直接交换两个set对象的根节点 clear就是把所有节点释放 emplace这里可以先看成和insert一样 比insert的效率更高
count会返回容器里面要找的值的个数 这里返回值为size_t 和erase第二个使用一样同样是要和multiset保持一致
也可以用count来判断一个值是否存在 之前的find还需要接收返回的迭代器 然后判断是不是end count就可以直接通过返回值是不是1判断值存不存在
upper_bound 会返回大于val第一个位置的迭代器
lower_bound不是返回小于val第一个位置的迭代器 是返回大于等于val第一个位置的迭代器
那么为什么这么设置呢
如下 我们先插入了10 20 30 40 50 60 70 80 90然后我们想删除[30,50]范围内的值 对于左区间30我们用lower_bound会返回30位置的迭代器 itlow对于右区间50我们用upper_bounde会返回60位置的迭代器itup
而删除我们需要通过erase的第三个方式传迭代器区间的方式 对于第二个参数相当于end一样是删除位置的后一个位置 所以不会删除这个位置 这时候的itlow和itup刚好就是满足我们期望的
所以我们想删除哪个区间内的值只需要只需要让用lower_bound返回左区间的位置迭代器 用upper_bound返回右区间位置的迭代器 然后用erase传这两个迭代器就可以满足我们需求了
set<int> s1 ;
for (int i = 1; i < 10; i++)s1.insert(i * 10); // 插入10 20 30 40 50 60 70 80 90for (auto e : s1)
{cout << e << " ";
}
cout << endl;
//要删除[30,50] 范围内的值
auto itlow = s1.lower_bound(30); // 返回 >= 30的迭代器也就是30这个位置的迭代器
auto itup = s1.upper_bound(50); // 返回 > 50的迭代器 也就是60位置的迭代器
s1.erase(itlow, itup);
for (auto e : s1)
{cout << e << " ";
}
cout << endl;
multiset
multiset和set的区别就是可以支持重复数据的增删插
基本的功能和set都差不多 count会返回这个值的个数
在删除和查找上有些差异 有多个相同数的话 find会找中序遍历的第一个数 比如里面有很多的5 在找到5之后会继续判断此时位置的左树里面有没有5了 直到找到一个此时位置的左树里面没有5了 此时的位置才是find要找的
我们知道迭代器是按照中序遍历的方式 找到中序遍历的第一个有助于继续++遍历 如下找到5后迭代器更新可以继续到下一个位置一直把所有5都删掉
当然要删除里面的一个值不需要这么麻烦 直接erase(x)就会把里面所有x的值都删掉