STL标准模板库 set容器

文章目录

  • 迭代器
    • 迭代器的五大分类
    • 迭代器系列帮手函数一览
  • set容器
    • 打印任意 STL 容器的printer.h
    • set与vector
      • set 和 vector 的区别
      • set 和 vector 迭代器的共同点
      • set 和 vector 迭代器的不同点
    • set 的排序
      • set 的排序:string 会按“字典序”来排
      • set 的排序:自定义排序函数
      • set 的妙用:排序
    • set运算符 + - +=
      • std::next 等价于 +
      • std::advance 等价于 +=
      • next 和 advance 同样支持负数
      • std::distance 等价于 -
    • set 中插入元素
      • pair 的定义
      • 使用 C++17 的结构化绑定来拆解 pair
      • 插入重复数据
      • insert 的第一个返回值:指向插入/现有元素的迭代器
      • insert 的第二个返回值:表示插入是否成功
    • set 中查询元素
      • 在 set 中查询元素是否存在 法2
      • 在 set 中查询元素是否存在 法3
    • set 中删除元素
      • set 中删除指定元素
      • set 中删除指定范围的元素
      • 从 set 中删除指定范围的元素(错误)
      • lower_bound 和 upper_bound 函数
      • 从 set 中删除指定范围的元素(正确)
      • 清空 set 所有元素
    • set 的遍历
        • C 语言怎样遍历数组 1
        • C 语言怎样遍历数组 2
        • C 语言怎样遍历数组 3
        • C 语言指针到 C++ 迭代器(4)
      • set 的遍历 for each
        • set 的大小(元素个数)
      • set 的遍历 迭代器加强
    • set 和其他容器之间的转换
  • set 的不去重版本:multiset
    • 查找 multiset 中的等值区间 1(用 upper_bound 和 lower_bound 函数)
    • 查找 multiset 中的等值区间 2(equal_range获得等值区间)
    • 查找 multiset 中的等值区间 3 (1和2的不同)
    • 删除 multiset 中的等值区间
    • 查找 multiset 中的等值(equal_range 返回的等值区间,求长度)
    • 求 multiset 中的等值元素个数
    • multiset 也有 find 函数
  • C++11 新增:unordered_set 容器
  • 总结
    • 不同版本的 set 容器比较
    • set 增删改查操作总结
    • multiset 增删改查操作总结
    • set 系列成员函数总结
  • 查找方面各容器适合的领域

迭代器

迭代器的五大分类

在这里插入图片描述
包含关系:前向迭代器>双向迭代器>随机访问迭代器
这意味着如果一个STL模板函数(比如std::find)要求迭代器是前向迭代器即可,那么也可以给他随机访问迭代器,因为前向迭代器是随机访问迭代器的子集。
例如,vector 和 list 都可以调用 std::find(set 则直接提供了 find 作为成员函数)


参考链接:https://www.cplusplus.com/reference/iterator/istream_iterator
https://en.cppreference.com/w/cpp/iterator/random_access_iterator

迭代器系列帮手函数一览

在这里插入图片描述
参考链接:https://en.cppreference.com/w/cpp/iterator/distance

set容器

打印任意 STL 容器的printer.h

#pragma once
#include <iostream>
#include <utility>
#include <type_traits>
namespace std {
template <class T, class = const char *>
struct __printer_test_c_str {using type = void;
};template <class T>
struct __printer_test_c_str<T, decltype(std::declval<T>().c_str())> {};template <class T, int = 0, int = 0, int = 0,class = decltype(std::declval<std::ostream &>() << *++std::declval<T>().begin()),class = decltype(std::declval<T>().begin() != std::declval<T>().end()),class = typename __printer_test_c_str<T>::type>
std::ostream &operator<<(std::ostream &os, T const &v) {os << '{';auto it = v.begin();if (it != v.end()) {os << *it;for (++it; it != v.end(); ++it)os << ',' << *it;}os << '}';return os;
}
}

set与vector

set 和 vector 的区别

相同:都是能存储一连串数据的容器。
区别1:set会自动给其中的元素从小到大排序,而vector会保持插入时的顺序。
区别2:set会把重复的元素去除,只保留一个,即去重。
区别3:vector中的元素在内存中是连续的,可以高效地按索引随机访问,set则不行。
区别4:set中的元素可以高效地按值查找,而 vector 则低效。
#include <vector>
#include <set>
#include "printer.h"using namespace std;int main() {vector<int> a = {9, 8, 5, 2, 1, 1};cout << "vector=" << a << endl;set<int> b = {9, 8, 5, 2, 1, 1};cout << "set=" << b << endl;return 0;
}	

在这里插入图片描述

set 和 vector 迭代器的共同点

vector 具有 begin() 和 end() 两个成员函数,他们分别返回指向数组头部元素和尾部再之后一格元素的迭代器对象。vector 作为连续数组,他的迭代器基本等效于指针。
set 也有 begin() 和 end() 函数,他返回的迭代器对象重载了 * 来访问指向的地址。

int main() {vector<int> a = { 1, 2, 3, 4, 5, 6, 7 };vector<int>::iterator a_it = a.begin();cout << "vector[0]=" << *a_it << endl;set<int> b = { 1, 2, 3, 4, 5, 6, 7 };set<int>::iterator b_it = b.begin();cout << "set[0]=" << *b_it << endl;return 0;
}

在这里插入图片描述

set 和 vector 迭代器的不同点

set 的迭代器对象也重载了 ++ 为红黑树的遍历。
vector 提供了 + 和 += 的重载,而 set 没有。这是因为 vector 中的元素在内存中是连续的,可以随机访问。而 set 是不连续的,所以不能随机访问,只能顺序访问。
所以这里调用 b.begin() + 3,就出错了。

int main() {vector<int> a = {1, 2, 3, 4, 5, 6, 7};vector<int>::iterator a_it = a.begin() + 3;cout << "vector[3]=" << *a_it << endl;set<int> b = {1, 2, 3, 4, 5, 6, 7};set<int>::iterator b_it = b.begin() + 3;cout << "set[3]=" << *b_it << endl;return 0;
}

在这里插入图片描述


set 的排序

set 的排序:string 会按“字典序”来排

set 会从小到大排序,对 int 来说就是数值的大小比较。那么对字符串类型 string 要怎么排序呢?

其实 string 类定义了运算符重载 <,他会按字典序比较两个字符串。所谓字典序就是优先比较两者第一个字符(按 ASCII 码比较),如果相等则继续比较下一个,不相等则直接以这个比较的结果返回。如果比到末尾都相等且字符串长度一样,则视为相等。

int main() {vector<string> a = {"arch", "any", "zero", "Linux"};cout << "vector=" << a << endl;set<string> b = {"arch", "any", "zero", "Linux"};cout << "set=" << b << endl;return 0;
}

在这里插入图片描述
警告:千万别用 set<char *> 做字符串集合。这样只会按字符串指针的地址去判断相等,而不是所指向字符串的内容。

set 的排序:自定义排序函数

set 作为模板类,其实有两个模板参数:set<T, CompT>
第一个 T 是容器内元素的类型,例如 int 或 string 等。
第二个 CompT 定义了你想要的比较函子,set 内部会调用这个函数来决定怎么排序。
如果 CompT 不指定,默认会直接用运算符 < 来比较。
这里我们定义个 MyComp 作为比较函子,和默认的一样用 < 来比较,所以没有变化。

struct MyComp {bool operator()(string const &a, string const &b) const {return a < b;}
};int main() {set<string, MyComp> b = {"arch", "any", "zero", "Linux"};cout << "set=" << b << endl;return 0;
}

在这里插入图片描述


这里我们把比较函子 MyComp 定义成只比较字符串第一个字符 a[0] < b[0]。
神奇的一幕发生了,“any” 不见了!为什么?因为去重!
为什么 set 会把 “arch” 和 “any” 视为相等的元素?明明内容都不一样?

struct MyComp {bool operator()(string const &a, string const &b) const {return a[0] < b[0];}
};int main() {set<string, MyComp> b = {"arch", "any", "zero", "Linux"};cout << "set=" << b << endl;return 0;
}

在这里插入图片描述

首先搞懂 set 内部是怎么确定两个元素 a 和 b 相等的:
!(a < b) && !(b < a)
也就是说他 set 内部没有用到 == 运算符,而是调用了两次比较函子来判断的。
逻辑是:若 a 不小于 b 且 b 不小于 a,则视为 a 等于 b,所以这就是为什么 set 只需要一个比较函子,不需要相等函子的原因。

所以我们这里写了 a[0] < b[0] 就相当于让相等条件变成了 a[0] == b[0]。也就是说只要第一个字符相等就视为字符串相等,所以 “arch” 和 “any” 会被视为相等的元素,从而被 set 给去重了!
扩展知识:其实,map<K, T> 无非就是个只比较 K 无视 T 的 set<pair<K, T>>,顺手还加了一些方便的函数,比如 [] 和 at。

set 的妙用:排序

vector 转成 set 会让元素自动排序和去重。
我们其实可以利用这一点,把 vector 转成 set 再转回 vector,这样就实现去重了。

template <class ForwardIt>
void assign(ForwardIt beg, ForwardIt end);
int main() {vector<int> arr = { 9, 8, 5, 2, 1, 1 };cout << "原始数组:" << arr << endl;set<int> b(arr.begin(), arr.end());cout << "结果集合:" << b << endl;return 0;
}

在这里插入图片描述

set运算符 + - +=

set 迭代器没有重载 + 运算符,因为他不是随机迭代器。
那如果确实需要让 set 迭代器向前移动 3 格怎么办?
可以调用三次 ++ 运算,实现和 + 3 同样的效果。
vector 迭代器的 + n 复杂度是 O(1)。而 set 迭代器模拟出来的 + n 复杂度为 O(n)。虽然低效,但至少可以用了。

int main() {vector<int> a = { 1, 2, 3, 4, 5, 6, 7 };vector<int>::iterator a_it = a.begin();a_it = std::next(a_it, 3);  // 会调用 a_it + 3cout << "vector[3]=" << *a_it << endl;set<int> b = { 1, 2, 3, 4, 5, 6, 7 };set<int>::iterator b_it = b.begin();b_it = std::next(b_it, 3);  // 会调用三次 ++b_it/* ++b_it;++b_it;++b_it;    */cout << "set[3]=" << *b_it << endl;return 0;
}

在这里插入图片描述

std::next 等价于 +

但是这样手写三个 ++ 太麻烦了,而且是就地操作,会改变迭代器本身。
因此标准库提供了 std::next 函数,他的内部实现相当于这样:

auto next(auto it, int n =1){if(it is random_access){return it+n;}while(n--)++it;return it;
}

他会自动判断迭代器是否支持 + 运算,如果不支持,会改为比较低效的调用 n 次 ++。

int main() {vector<int> a = {1, 2, 3, 4, 5, 6, 7};vector<int>::iterator a_it = a.begin();a_it = std::next(a_it, 3);  // 会调用 a_it += 3cout << "vector[3]=" << *a_it << endl;set<int> b = {1, 2, 3, 4, 5, 6, 7};set<int>::iterator b_it = b.begin();b_it = std::next(b_it, 3);  // 会调用三次 ++b_itcout << "set[3]=" << *b_it << endl;return 0;
}

在这里插入图片描述

std::advance 等价于 +=

刚刚的 std::next 会返回自增后迭代器。
std::advance 会就地自增作为引用传入的迭代器,他同样会判断是否支持 += 来决定采用哪一种实现。

void advance(auto &it, int n){if(it is random_access){it += n;} else {while(n--)++it;}
}

区别:advance 就地修改迭代器,没有返回值;next 修改迭代器后返回,不会改变原迭代器。
advance 相当于 +=,next 相当于 +。

int main() {vector<int> a = {1, 2, 3, 4, 5, 6, 7};vector<int>::iterator a_it = a.begin();std::advance(a_it, 3);  // 会调用 a_it += 3cout << "vector[3]=" << *a_it << endl;set<int> b = {1, 2, 3, 4, 5, 6, 7};set<int>::iterator b_it = b.begin();std::advance(b_it, 3);  // 会调用三次 ++b_itcout << "set[3]=" << *b_it << endl;return 0;
}

在这里插入图片描述

next 和 advance 同样支持负数

next 的第二个参数 n 通常是正数,表示向前走的距离。
如果迭代器类型是双向迭代器。next 的第二个参数 n 还可以是负数,这时他会让迭代器往前走一段距离,例如:
std::next(it, -3) 相当于 it - 3。
还可以用另一个专门的函数 std::prev(it, 3) 也相当于 it - 3。

auto prev(auto it,int n=1){return next(it,-n);
}
auto next(auto it, int n =1){if(it is random_access){return it+n;}else{if(it is bidrectional && n<0){while(n++)--it;}while(n--)++it;return it;}
}
void advance(auto &it, int n){if(it is random_access){it += n;} else {if(it is bidrectional && n<0){while(n++)--it;}while(n--)++it;}
}

std::distance 等价于 -

std::distance 会求出两个迭代器之间的距离(差值)。
std::distance(it1, it2) 相当于 it2 - it1,注意顺序和 - 相反。
注意:distance 要求 it1 < it2。

ptrdiff_t distaance(auto it1,auto it2){if(it1 and it2 is random_access){return it2-it1;}else{ptrdiff_t n =0; //计数器while(it1 != it2){  //当it1 不等于it2 一直++it1 等于时 n就是他们的长度++it1;++n;}return n;}	
}
int main() {vector<int> a = {1, 2, 3, 4, 5, 6, 7};vector<int>::iterator a_it1 = a.begin();vector<int>::iterator a_it2 = a.end();int a_size = std::distance(a_it1, a_it2);    // 会调用 a_it2 - a_it1cout << "vector size = " << a_size << endl;set<int> b = {1, 2, 3, 4, 5, 6, 7};set<int>::iterator b_it1 = b.begin();set<int>::iterator b_it2 = b.end();int b_size = std::distance(b_it1, b_it2);    // 会调用 ++b_it1 直到等于 b_it2cout << "set size = " << b_size << endl;return 0;
}

在这里插入图片描述

set 中插入元素

可以通过调用 insert 往 set 中添加一个元素。
用户无需关心插入的位置,例如插入元素 3 时,set 会自动插入到 2 和 4 之间,从而使元素总是从小到大排列。

pair<iterator, bool> insert(int val);
int main() {set<int> b = {1, 4, 2, 1};cout << "插入之前: " << b << endl;b.insert(3);cout << "插入之后: " << b << endl;return 0;
}

在这里插入图片描述

pair 的定义

pair 类似于 python 里的元组,不过固定只能有两个元素,自从 C++11 引入了能支持任意多元素的 tuple 以来,就没 pair 什么事了……但是为了兼容 pair 还是继续存在着。pair 是个模板类,根据尖括号里你给定的类型来替换这里的 _T1 和 _T2。例如 pair<iterator, bool> 就会变成:

struct pair {iterator first;bool second;
};
template<typename _T1,Ttypename_T2>struct pair:private __pair_base<_T1,_T2>{typedef _T1 first_type;typedef _T2 second_type;_T1 first;_T2 second;}

使用 C++17 的结构化绑定来拆解 pair

C++17 提供了结构化绑定(structual binding)的语法,可以取出一个 POD 结构体的所有成员,pair 也不例外。

auto [ok, it] = b.insert(3);
等价于
auto tmp = b.insert(3);
auto ok = tmp.first;
auto it = tmp.second;
int main() {set<int> b = {1, 4, 2, 1};auto [ok1, it1] = b.insert(3);cout << "插入3成功:" << ok1 << endl;cout << "3所在的位置:" << *it1 << endl;auto [ok2, it2] = b.insert(3);cout << "再次插入3成功:" << ok2 << endl;cout << "3所在的位置:" << *it2 << endl;return 0;
}

在这里插入图片描述

插入重复数据

因为 set 具有自动去重的功能,如果插入的元素已经在 set 中存在,则不会完成插入。
例如往集合 {1,2,4} 中插入 4 则什么也不会发生,因为 4 已经在集合中了。

pair<iterator, bool> insert(int val);
int main() {set<int> b = {1, 4, 2, 1};cout << "插入之前: " << b << endl;b.insert(4);cout << "插入之后: " << b << endl;return 0;
}

在这里插入图片描述

insert 的第一个返回值:指向插入/现有元素的迭代器

其中第一个返回值是一个迭代器,分两种情况讨论。
当向 set 容器添加元素成功时,该迭代器指向 set 容器新添加的元素,bool 类型的值为 true;
如果添加失败,即证明原 set 容器中已存有相同的元素,此时返回的迭代器就指向容器中相同的此元素,同时 bool 类型的值为 false。

pair<iterator, bool> insert(int val);
int main() {set<int> b = { 1, 4, 2, 1 };auto res1 = b.insert(3);cout << "插入3成功:" << res1.second << endl;cout << "3所在的位置:" << *res1.first << endl;auto res2 = b.insert(3);cout << "再次插入3成功:" << res2.second << endl;cout << "3所在的位置:" << *res2.first << endl;return 0;
}

在这里插入图片描述

insert 的第二个返回值:表示插入是否成功

insert 函数的返回值是一个 pair 类型,也就是说他同时返回了两个值。其中第二个返回值是 bool 类型,指示了插入是否成功。
若元素在 set 容器中已存有相同的元素,则插入失败,这个 bool 值为 false;如果元素在 set 中不存在,则插入成功,这个 bool 值为 true。

pair<iterator, bool> insert(int val);
int main() {set<int> b = { 1, 4, 2, 1 };auto res4 = b.insert(4);cout << "插入4成功:" << res4.second << endl;auto res3 = b.insert(3);cout << "插入3成功:" << res3.second << endl;return 0;
}

在这里插入图片描述

set 中查询元素

set 有一个 find 函数。只需给定一个参数,他会寻找 set 中与之相等的元素。
如果找到,则返回指向找到元素的迭代器。
如果找不到,则返回 end() 迭代器。
刚刚说过,end() 指向的是 set 的尾部再之后一格元素,他指向的是一个不存在的地址,不可能有任何元素在那里!因此 end() 常被标准库用作一个标记,来表示找不到的情况。Python 中的 find 找不到元素时会返回 -1 来表示,也是这个思想。
在这里插入图片描述

iterator find(int const &val) const;	
int main() {set<int> b = { 1, 4, 2, 1 };auto it = b.find(2);cout << "2所在位置:" << *it << endl;cout << "比2小的数:" << *prev(it) << endl;cout << "比2大的数:" << *next(it) << endl;return 0;
}

在这里插入图片描述

在 set 中查询元素是否存在 法2

因此,可以用这个写法:

set.find(x) != set.end()

来判断集合 set 中是否存在元素 x。
这是个固定的写法,虽然要调用两个函数看起来好像挺麻烦,但是大家都在用。

iterator find(int const &val) const;
iterator end() const;
int main() {set<int> b = { 1, 4, 2, 1 };if (b.find(2) != b.end()) {cout << "集合中存在2" << endl;}else cout << "集合中没有2" << endl;if (b.find(8) != b.end()) {cout << "集合中存在8" << endl;}else cout << "集合中没有8" << endl;   return 0;
}

在这里插入图片描述

在这里插入图片描述

在 set 中查询元素是否存在 法3

set.count(x) != 0
count 返回的是一个 int 类型,表示集合中相等元素的个数。

不是说 set 具有去重的功能,不会有重复的元素吗?为什么标准库让 count 计算个数而不是直接返回 bool…因为他们考虑到接口的泛用性,毕竟 multiset 就不去重。对于能去重的 set,count 只可能返回 0 或 1。
个数为 0 就说明集合中没有该元素。 个数为 1 就说明集合中存在该元素。

因为 int 类型能隐式转换为 bool,所以 != 0 可以省略不写。
size_t count(int const &val) const;
int main() {set<int> b = { 1, 4, 2, 1 };if (b.count(2)) {cout << "集合中存在2" << endl;}else cout << "集合中没有2" << endl;if (b.count(8)) {cout << "集合中存在8" << endl;}else cout << "集合中没有8" << endl;    return 0;
}

在这里插入图片描述

set 中删除元素

set.erase(x) 可以删除集合中值为 x 的元素。
erase 返回一个整数,表示被他删除元素的个数。
个数为 0 就说明集合中没有该元素,删除失败。
个数为 1 就说明集合中存在该元素,删除成功。
这里的“个数”和 count 的情况很像,因为 set 中不会有重复的元素,所以 erase 只可能返回 0 或 1,表示是否删除成功。

size_t erase(int const &val);
int main() {set<int> b = { 1, 2, 3, 4, 5 };cout << "删之前:" << b << endl;int num = b.erase(4);cout << "删之后:" << b << endl;cout << "删了 " << num << " 个元素" << endl;return 0;
}

在这里插入图片描述

set 中删除指定元素

erase 还支持迭代器作为参数。
set.erase(it) 可以删除集合位于 it 处的元素。用法举例:
set.erase(set.find(x)) 会删除集合中值为 x 的元素,和 set.erase(x) 等价。
set.erase(set.begin()) 会删除集合中最小的元素(因为 set 具有自动排序的特性,排在最前面的元素一定是最小的那个)
set.erase(std::prev(set.end())) 会删除集合中最大的元素(因为自动排序的特性,排在最后面的元素一定是最大的那个)

iterator erase(iterator pos);
int main() {set<int> b = { 1, 2, 3, 4, 5 };cout << "原始集合:" << b << endl;b.erase(b.find(4));cout << "删除元素4:" << b << endl;b.erase(b.begin());cout << "删最小元素:" << b << endl;b.erase(std::prev(b.end()));cout << "删最大元素:" << b << endl;return 0;
}

在这里插入图片描述

在这里插入图片描述

set 中删除指定范围的元素

erase 还支持输入两个迭代器作为参数。
set.erase(beg, end) 可以删除集合中从 beg 到 end 之间的元素,包含 beg,不包含 end。也就是说他是个前开后闭区间 (beg, end],毕竟这是标准库一贯的作风。
注意:beg 必须在 end 之前,否则崩溃。

用法举例:a.erase(a.find(2), a.find(4));

会删除 set 中所有满足 2 ≤ x<4 的元素(因为 set 有自动排序的特性,所有元素都从小到大连续排列,所以删除 2 迭代器和 4 迭代器之间的元素其实就是删除 2 ≤ x<4 的元素)

iterator erase(iterator first, iterator last);
int main() {set<int> b = { 1, 2, 3, 4, 5 };cout << "原始集合:" << b << endl;b.erase(b.find(2), b.find(4));cout << "删除[2,4)之间的元素:" << b << endl;return 0;
}

在这里插入图片描述
在这里插入图片描述

从 set 中删除指定范围的元素(错误)

刚刚说的:a.erase(a.find(2), a.find(4));
这种写法有一个很大的问题:如果集合中没有 2 怎么办?那么 find(2) 因为找不到就会返回 end(),而 find(4) 成功找到返回了指向 4 的迭代器。这样一来 find(2) 的迭代器也就是 end() 其实在 find(4) 之后,违背了刚刚说的“beg 必须在 end 之前”这一规则,会导致标准库崩溃!
a.erase(a.find(2), a.find(4));
会删除 set 中所有满足 2 ≤ x<4 的元素
前提是 2 和 4 这两个元素在集合中存在!

iterator find(int const &val) const;
iterator erase(iterator first, iterator last);
int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;b.erase(b.find(2), b.find(4));cout << "删除[2,4)之间的元素:" << b << endl;return 0;
}

在这里插入图片描述

lower_bound 和 upper_bound 函数

find(x) 是找第一个等于 x 的元素。
lower_bound(x) 找第一个大于等于 x 的元素。
upper_bound(x) 找第一个大于 x 的元素。
他们找不到时都会返回 end()。
find 的条件更加严格(必须相等才算找到),lower_bound 和 upper_bound 就比较宽松。
所以如果集合中有 2:
lower_bound(2) 会返回指向 2 的迭代器。
upper_bound(2) 也会返回指向 3 的迭代器。
find(2) 会返回指向 2 的迭代器。

iterator find(int const &val) const;
iterator lower_bound(int const &val) const;
iterator upper_bound(int const &val) const;
static void check(bool success) {if (!success) throw;cout << "通过测试" << endl;
}
int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;check(b.find(2) == b.end());check(b.lower_bound(2) == b.find(3));check(b.upper_bound(2) == b.find(3));return 0;
}

在这里插入图片描述
在这里插入图片描述

从 set 中删除指定范围的元素(正确)

a.erase(a.lower_bound(2), a.upper_bound(4));

会删除 set 中所有满足 2 ≤ x ≤ 4 的元素
注意这里变成 [2, 4] 两边都是闭区间了!因为 upper_bound 会返回指向大于 x 的元素,也就是等于 x 的元素再之后一个元素,所以刚好抵消了标准库的前开后闭区间效果……
右边的运行结果也可以看到他把 4 也删了。
lower_bound(x) 找第一个大于等于 x 的元素。
upper_bound(x) 找第一个大于 x 的元素。

iterator find(int const &val) const;
iterator lower_bound(int const &val) const;
iterator upper_bound(int const &val) const;
iterator erase(iterator first, iterator last);
int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;b.erase(b.lower_bound(2), b.upper_bound(4));cout << "删除[2,4]之间的元素:" << b << endl;return 0;
}

在这里插入图片描述

在这里插入图片描述

清空 set 所有元素

如图,清空 set 有三种方式。
最常用的是调用 clear 函数。
这和 vector 的 clear 函数名字是一样的,方便记忆。

void clear() noexcept;
set &operator=(initializer_list<int> lst);
int main() {set<int> b = { 1, 2, 3, 4, 5 };cout << "原始集合:" << b << endl;b.clear();// b = {};// b.erase(b.begin(), b.end());cout << "清空后集合:" << b << endl;return 0;
}

在这里插入图片描述

set 的遍历

遍历方法和ector 的一样

int main() {set<int> b = {0, 1, 3, 4, 5};cout << "原始集合:" << b << endl;for (auto it = b.begin(); it != b.end(); ++it) {int value = *it;cout << value << endl;}return 0;
}

在这里插入图片描述

C 语言怎样遍历数组 1

int arr[n];
for (int i = 0; i < n; i++) {int value = arr[i];
}
循环的范围是 [0, n)。
因为这里 arr[i] 等价于 *(arr + i),所以……

C 语言怎样遍历数组 2

int arr[n];
for (int *p = arr; p < arr + n; p++) {int value = *p;
}
索性用 arr + i 作为迭代的变量,避免一次加法的开销。
循环的范围变成 [arr, arr + n)

C 语言怎样遍历数组 3

int arr[n];
//for (int *p = arr; p != arr + n; p++) {
for (int *p = arr; p != arr + n; ++p) {int value = *p;
}
n 总是大于 0 的。p 的初值 arr 总是小于末值 arr + n,
所以把 p < arr + n 改成 p != arr + n 是一样的,还高效一点。
建议用前置 ++ 运算符,前置比较高效且符合逻辑。

C 语言指针到 C++ 迭代器(4)

C++ 的迭代器模式也就呼之欲出了:
set<int> arr;
for (set<int>::iterator p = arr.begin(); p != arr.end(); ++p) {int value = *p;
}
begin 和 end 返回了迭代器类,这个类具有运算符重载,使得他能模仿指针的行为,
从而尽可能在不同容器之间重用算法(例如 std::find 和 std::reverse),
而不必修改算法的代码本身,是 STL 库解耦思想的体现。

set 的遍历 for each

基于范围的 for 循环(range-based for loop)。
for (类型 变量名 : 可迭代对象)

int main() {set<int> b = {0, 1, 3, 4, 5};cout << "原始集合:" << b << endl;for (int value: b) {cout << value << endl;}return 0;
}

在这里插入图片描述

set 的大小(元素个数)

和 vector 一样,set 也有个 size() 函数查询其中元素个数。

size_t size() const noexcept;
int main() {set<int> b = { 1, 2, 3, 4, 5 };cout << "原始集合:" << b << endl;cout << "元素个数:" << b.size() << endl;return 0;
}

在这里插入图片描述

set 的遍历 迭代器加强

基于范围的 for 循环只是一个简写,他会遍历整个区间 [begin, end)。有时写完整版会有更大的自由度
也就是说这里的 begin 和 end 可以替换为其他位置的迭代器(如 find/lower_bound/upper_bound)	
int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;for (auto it = b.lower_bound(2); it != b.upper_bound(4); ++it) {int value = *it;cout << value << endl;}return 0;
}

在这里插入图片描述

在这里插入图片描述

set 和其他容器之间的转换

vector 的构造函数也能接受两个前向迭代器作为参数,set 的迭代器符合这个要求。
所以可以把 set 中的一个区间(2 ≤ x ≤ 4)拷贝到 vector 中去。
相当于过滤出所有 2 ≤ x ≤ 4 的元素了。

template <class ForwardIt>
explicit vector(ForwardIt beg, ForwardIt end);

没错,vector 的构造函数可以接受任何前向迭代器。
不一定是 vector 自己的迭代器哦,任何前向迭代器!
而 set 是双向迭代器,覆盖了前向迭代器,满足要求。

int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;vector<int> arr(b.lower_bound(2), b.upper_bound(4));cout << "结果数组:" << arr << endl;return 0;
}

在这里插入图片描述

如果是 vector(b.begin(), b.end()) 那就毫无保留地把 set 的全部元素都拷贝进 vector 了,这是最常用的。

int main() {set<int> b = { 0, 1, 3, 4, 5 };cout << "原始集合:" << b << endl;vector<int> arr(b.begin(), b.end());cout << "结果数组:" << arr << endl;return 0;
}

在这里插入图片描述

也可以反过来,把 vector 转成 set。
强制转换到 vector 容器:vector(b.begin(), b.end())
强制转换到 set 容器:set(b.begin(), b.end())

template <class ForwardIt>
explicit set(ForwardIt beg, ForwardIt end);
int main() {vector<int> b = {0, 1, 3, 4, 5};cout << "原始数组:" << b << endl;set<int> arr(b.begin(), b.end());cout << "结果集合:" << arr << endl;return 0;
}

在这里插入图片描述

set 的不去重版本:multiset

set 具有自动排序,自动去重,能高效地查询的特点。其中去重和数学的集合很像。
还有一种不会去重的版本,那就是 multiset,他允许重复的元素,但仍保留自动排序,能高效地查询的特点。
特点:因为 multiset 不会去重,但又自动排序,所以所有相等的元素都会紧挨着,如 {1, 2, 2, 4, 6}。

#include <set>
#include "printer.h"
using namespace std;int main() {set<int> a = { 1, 1, 2, 2, 3 };multiset<int> b = { 1, 1, 2, 2, 3 };cout << "set: " << a << endl;cout << "multiset: " << b << endl;return 0;
}

在这里插入图片描述

查找 multiset 中的等值区间 1(用 upper_bound 和 lower_bound 函数)

刚刚说了 multiset 里相等的元素都是紧挨着排列的。
所以可以用 upper_bound 和 lower_bound 函数获取所有相等值的区间。
[lower_bound, upper_bound)

iterator lower_bound(int const &val) const;
iterator upper_bound(int const &val) const;
int main() {multiset<int> b = { 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;b.erase(b.lower_bound(2), b.upper_bound(2));cout << "删除2以后:" << b << endl;return 0;
}

在这里插入图片描述
在这里插入图片描述

查找 multiset 中的等值区间 2(equal_range获得等值区间)

对于 lower_bound 和 upper_bound 的参数相同的情况,可以用 equal_range 一次性求出两个边界,获得等值区间,更高效。

pair<iterator, iterator> equal_range(int const &val) const;
int main() {multiset<int> b = { 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;auto r = b.equal_range(2);b.erase(r.first, r.second);cout << "删除2以后:" << b << endl;return 0;
}

在这里插入图片描述

查找 multiset 中的等值区间 3 (1和2的不同)

equal_range(等值区间)和调用两次 lower_bound( >= 起点)upper_bound(大于起点)的不同:
当指定的值找不到时,equal_range 返回两个 end() 迭代器,代表空区间。
lower/upper_bound 却会正常返回指向大于等于/大于指定值的迭代器。
原因:equal_range 的用途都是返回一个用来遍历的区间,两个迭代器是一起用的,不会单独用。所以为了高效,找不到等值元素会直接返回空区间。

pair<iterator, iterator> equal_range(int const &val) const;
int main() {multiset<int> b = { 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;auto r = b.equal_range(6);//当将bool值输出到流中时,它会被转换为整数0或1。//但是,当使用boolalpha标志后,它将被转换为字符串"true"或"false"。cout << boolalpha; cout << (r.first == b.end()) << endl;cout << (r.second == b.end()) << endl;return 0;
}

在这里插入图片描述

删除 multiset 中的等值区间

erase 只有一个参数的版本,会把所有等于 2 的元素删除。
例如:b.erase(2) 等价于b.erase(b.lower_bound(2), b.upper_bound(2));

iterator erase(int const &val) const;
int main() {multiset<int> b = { 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;b.erase(2);cout << "删除2以后:" << b << endl;return 0;
}

在这里插入图片描述

查找 multiset 中的等值(equal_range 返回的等值区间,求长度)

equal_range 返回的等值区间,可以求长度,也可以遍历。
对 multiset 而言遍历似乎没什么用,反正都是一堆相等的元素。
求长度也没什么用,可以用 count 替代,总之就是非常尴尬。
但之后说到 multimap 的时候这个函数就会很有用了。

pair<iterator, iterator> equal_range(int const &val) const;
int main() {multiset<int> b = { 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;auto r = b.equal_range(2);size_t n = std::distance(r.first, r.second);cout << "等于2的元素个数:" << n << endl;for (auto it = r.first; it != r.second; ++it) {int value = *it;cout << value << endl;}return 0;
}

在这里插入图片描述

求 multiset 中的等值元素个数

count(x) 返回 multiset 中等于 x 的元素个数(如果找不到则返回 0)。
刚刚说 set(具有去重功能)的 count 只会返回 0 或 1。
而 multiset(没有去重功能)的 count 可以返回任何 ≥ 0 的数。

size_t count(int const &val) const;
int main() {multiset<int> b = { 1, 1, 1, 2, 2, 3 };b.size();cout << "原始集合:" << b << endl;cout << "等于2的元素个数:" << b.count(2) << endl;cout << "等于1的元素个数:" << b.count(1) << endl;return 0;
}

在这里插入图片描述

multiset 也有 find 函数

multiset 允许多个重复的元素存在,那么 find 会返回哪一个?第一个!
find(x) 会返回第一个等于 x 的元素的迭代器。找不到也是返回 end()。

int main() {multiset<int> b = { 1, 1, 1, 2, 2, 3 };cout << "原始集合:" << b << endl;cout << boolalpha;cout << "集合中存在2:" << (b.find(2) != b.end()) << endl;cout << "集合中存在1:" << (b.find(1) != b.end()) << endl;cout << "第一个1在头部:" << (b.find(1) == b.begin()) << endl;return 0;
}

在这里插入图片描述

C++11 新增:unordered_set 容器

set 会让元素从小到大排序。
而 unordered_set 不会排序,里面的元素都是完全随机的顺序,和插入的顺序也不一样。虽然你可能注意到这里的刚好和插入的顺序相反?巧合而已,具体怎么顺序是和 glibc 实现有关的。
set 基于红黑树实现,相当于二分查找树,unordered_set 基于散列哈希表实现,正是哈希函数导致了随机的顺序。

#include <set>
#include <unordered_set>
#include "printer.h"
using namespace std;
int main() {set<int> a = {1, 4, 2, 8, 5, 7};unordered_set<int> b = {1, 4, 2, 8, 5, 7};cout << "set: " << a << endl;cout << "unordered_set: " << b << endl;return 0;
}

在这里插入图片描述

总结

不同版本的 set 容器比较

在这里插入图片描述
在这里插入图片描述

set 增删改查操作总结

在这里插入图片描述

multiset 增删改查操作总结

在这里插入图片描述

set 系列成员函数总结

在这里插入图片描述

查找方面各容器适合的领域

vector 适合:按索引查找。通过运算符 []。
set 适合:按值相等查找,按值大于/小于查找。分别通过函数 find、lower_bound、upper_bound。
unordered_set 只适合:按值相等查找,通过函数 find。

unordered_set 的性能在数据量足够大(>1000)时,平均查找时间比 set 短,但不保证稳定。
个人推荐使用久经沙场的 set,数据量小时更高效。

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

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

相关文章

云计算UPS监控,怎么办?

在大型数据机房中&#xff0c;UPS系统扮演着关键的角色&#xff0c;为计算机和网络设备提供可靠的电力备份。由于数据机房的规模庞大且关键性强&#xff0c;监控UPS系统的可靠性和效率至关重要。 UPS监控可以提供实时的电池状态、负载信息、电网电压等监测数据&#xff0c;并能…

代码随想录打卡

这里写目录标题 1.数组部分1.1二分查找1.2移除元素1.3 有序数组的平方1.4长度最小的子数组1.5螺旋矩阵II 2. 链表部分2.1移除链表元素2.2设计链表2.3反转链表2.4两两交换相邻的节点2.5删除链表的倒数第n个节点2.6环形链表II2.7链表相交 3.哈希表 1.数组部分 1.1二分查找 class…

我司的短信接口被刷了

如何发现的 成本分摊系统&#xff0c;将成本分摊给业务部门时&#xff0c;业务部门对账&#xff0c;发现某一类型的短信用量上涨了100多倍 排查调用来源时&#xff0c;发现来源为C端用户&#xff0c;由于调用量异常高&#xff0c;业务反馈近期无活动&#xff0c;因此怀疑被刷…

服务器数据库中了360后缀勒索病毒怎么办,如何预防勒索病毒攻击?

随着网络技术的不断发展&#xff0c;企业的计算机服务器也受到了网络安全威胁&#xff0c;近日&#xff0c;很多企业的服务器被360后缀勒索病毒攻击&#xff0c;导致企业的数据库中的许多重要数据被加密&#xff0c;无法正常读取打开。360后缀勒索病毒数据BeijingCrypt勒索病毒…

请求响应-日期时间参数的接受

日期参数 由于从前端发送的请求中&#xff0c;日期的格式可能各不相同&#xff0c;使用DateTimeFormat注解完成日期参数格式的转换具体关键代码如下&#xff1a; 在postman中发出对应请求携带对应参数结果如下&#xff1a; 参数名称要与方法中的形参名称一致&#xff0c;免得…

【Python】PyCharm中调用另一个文件的函数或类

&#x1f389;欢迎来到Python专栏~PyCharm中调用另一个文件的函数或类 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;Python学习专栏 文章作者技术和水平有限&#xff0c;如果文中出现错误&…

【IMX6ULL驱动开发学习】18.中断下半部(tasklet、工作队列、中断线程化)

下图表述了Linux内核的中断处理机制&#xff0c;为了在中断执行时间尽量短和中断处理需完成的工作尽量大之间找到一 个平衡点&#xff0c; Linux将中断处理程序分解为两个半部&#xff1a; 顶半部&#xff08;Top Half&#xff09; 和底半部&#xff08;Bottom Half&#xff09…

C语言a---b

C语言的编译遵循贪心读法&#xff0c;也就是说&#xff0c;对于有歧义的符号&#xff0c;编译器会一直读取&#xff0c;直到它的意思完结&#xff1b; a---b&#xff0c;是a-- -b还是a- --b&#xff0c;根据贪心法则&#xff0c;读到第二个减号&#xff0c;意思完结&#xff0c…

你知道mp3转换器怎么用吗?分享在线音频转换mp3怎么弄

飒飒&#xff1a;嘿&#xff0c;你有没有想过如何将在线音频转换为mp3格式&#xff1f; 潇潇&#xff1a;是的&#xff0c;我确实有过这个需求。在网上找到了一些工具和方法&#xff0c;可以帮助我们完成这个任务。 飒飒&#xff1a;那太好了&#xff01;你能告诉我一些详细的…

【新版系统架构】系统架构设计师教程全篇知识点提炼

第一章-绪论 架构的定义&#xff1a; 1、架构体现在组件中的一个系统的基本组织、彼此的关系和环境的关系及指导它的设计和发展的原则 2、系统是组织起来完成某一特定功能或一组功能的组件集 3、环境或者上下文决定了对这个系统的开发、运作、政策以及会对系统造成其他影响的…

开放式耳机哪个好?开放式耳机选购推荐

相比入耳式耳机&#xff0c;近几年流行的开放式耳机似乎更受大众欢迎&#xff0c;不入耳的设计&#xff0c;佩戴稳固舒适不容易掉落&#xff0c;也不伤耳&#xff0c;不仅能够提升幸福感还能听到周围环境声&#xff0c;避免在长期佩戴后舒适度下降的问题&#xff0c;对于产生的…

nginx日志分析,实时可视化工具goaccess

一款可以实时分析NGINX访问日志&#xff0c;并且支持可视化的软件 GoAccess - Visual Web Log Analyzer github如下&#xff1a;GitHub - allinurl/goaccess: GoAccess is a real-time web log analyzer and interactive viewer that runs in a terminal in *nix systems or th…