STL中的set容器

推荐使用电脑浏览器浏览
前言:本文介绍了 set 的使用方法和增删查功能中常见的函数声明。

1. 序列式容器和关联式容器

  string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构线性序列的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下值,它依旧是序列式容器。序列式容器中的元素时按它们在容器中的存储位置来顺序保存和访问的。

  关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构。两个位置有紧密的关联关系,交换一下两个位置的关键值,它的存储结构就被破坏了。关联容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。

  一个小例子来对比两个容器之间的差异:当有一个新元素插入时,序列容器一般都是直接插入,容器内变化不大,可能存在元素整体往后挪(vector这样的整片空间连续的),也可能不会发生元素整体往后挪(list这样单独为结点申请空间的)。而关联容器里有一个规则在约束着插入功能,要求插入的元素在插入容器后,能保证容器是稳定且继续保持某种关系的状态,如map默认使用红黑树,默认关键值为左小右大,那么插入的元素也要根据这个关键值的规则进行插入。

红黑树是一棵平衡二叉搜索树set是key搜索场景的结构,map是key/value搜索场景的结构。


2. set类的介绍

set类的声明

  • set 的声明如上,T就是 set 底层关键字的类型。
  • set 默认要求 T 支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。
  • set 底层储存数据的内存是空间配置器申请的,如果有特殊需求可以自己实现内存池,传给第三个参数。
  • 一般情况下,我们都不需要传后两个模版参数,传第一个模版参数是最常用的。
  • set 底层是用红黑树实现的,增删查效率为 O ( log ⁡ N ) O(\log N) O(logN),迭代器遍历走的是搜索树的中序遍历,所以是有序的。
template <	class T,					// set::key_type/value_type
			class Compare = less<T>,	// set::key_compare/value_compare
			class Alloc = allocator<T>	// set::allocator_type
			> class set;

3. set的构造和迭代器

  set 的构造关注以下几种接口即可:
set的构造函数

3.1. 无参默认构造

// empty(1) 无参默认构造
explicit set(const key_compare& comp = key_compare(),
			const allocator_type& alloc = allocator_type());

选择直接 set<T> 变量名来创建一个 set 对象即可。下面展示直接创建的代码演示:
set的无参构造

#include <iostream>
#include <set>
using namespace std;

int main() {
	// 创建一个 关键值类型为 int 的set对象
	set<int> emptySet;
	emptySet.insert(1);
	emptySet.insert(4);
	emptySet.insert(3);
	emptySet.insert(2);

	for (auto e : emptySet) {
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

3.2. 迭代器区间构造

// range(2) 迭代器区间构造
template <class InputIterator>
set(InputIterator first, InputIterator lase,
	const key_compare& comp = key_compare(),
	const allocator_type& = allocator_typr());

这里要求是传入的迭代器类型为单向迭代器即可(当然双向或者随机迭代器也可以)。下面还是使用代码演示迭代器区间构造 set 对象:
set使用迭代器区间构造
代码部分:

#include <iostream>
#include <set>
#include <vector>
using namespace std;

int main() {
	// 创建一个其他容器,注意有重复元素在这个 vec 中
	vector<int> vec = { 3, 4, 3, 6, 1, 2, 7, 4 };

	// 使用vector的迭代器范围来构造set
	// set会自动去重和排序
	set<int> rangeSet(vec.begin(), vec.end());

	cout << "原始vector: ";
	for (auto e : vec) {
		cout << e << " ";
	}
	cout << endl;

	cout << "构造的set: ";
	for (auto e : rangeSet) {
		cout << e << " ";
	}
	cout << endl; 
	return 0;
}

3.3. 拷贝构造

// copy (3) 拷贝构造
set (const set& x);

通过一个同关键值类型的 set 来作为参数来拷贝构造一个新的 set 对象。下面是代码演示:
set的拷贝构造
代码部分:

#include <iostream>
#include <set>
#include <vector>
using namespace std;

void Print(const set<string>& original_Set, const set<string>& copied_Set) {
	cout << "原始Set: ";
	for (const auto& fruit : original_Set) {
		cout << fruit << " ";
	}
	cout << endl;

	cout << "拷贝的Set: ";
	for (const auto& fruit : copied_Set) {
		cout << fruit << " ";
	}
	cout << endl;
}
int main() {
	// 原始set
	set<string> original_Set = { "Apple", "Banana", "Orange" };

	// 使用拷贝构造函数
	set<string> copied_Set(original_Set);

	// 调用打印函数查看 原始Set 和 拷贝Set
	Print(original_Set, copied_Set);

	// 修改拷贝的Set, 不会影响原始的Set,说明是深拷贝
	copied_Set.insert("Grape");
	cout << "\n\n添加Grape后:" << endl;
	// 调用打印函数查看,对拷贝Set新增元素后,两个Set的变化
	Print(original_Set, copied_Set);

	return 0;
}

3.4 初始化列表构造

// initailizer lis (5) innitializer 列表构造
set (initializer_list<value_type> il,
	const key_compare& comp = key_compare(),
	const allocator_type& alloc = allocator_type());

通过初始化列表来创建 set 对象。下面通过代码演示:
Set的初始化列表构造
代码部分:

#include <iostream>
#include <set>
using namespace std;

int main() {
	// 使用初始化列表直接构造Set
	set<int> initList_Set = { 9, 2, 2, 9, 5, 3, 1 };
	cout << "使用初始化列表构造的Set: ";
	for (auto e : initList_Set) {
		cout << e << " ";
	}
	cout << endl << endl;

	// 也可以直接使用 花括号
	set<char> charSet{ 'h', 'e','l','l','o' };
	cout << "字符Set: ";
	for (char ch : charSet) {
		cout << ch << " ";
	}
	cout << endl;

	return 0;
}

3.5. set的迭代器

set的迭代器
由上图我们可以知道set的迭代器是双向迭代器。

itetator -> a bidirectional iterator to const value_type

想要获取正向迭代器:

使用下面这两个函数:
iterator begin();
iterator end();

获取反向迭代器:

使用下面这两个函数:
reverse_iterator rbegin();
reverse_iterator rend();

4. set 的增删查

set的成员变量
由上图知道,set 从实现来看,它可以被视为一个键和值相同的特殊映射(map)。虽然我们通常说 set 值存储键(key)。

4.1 set 的插入

这里只讲解下面这三种类型的插入。
set的插入

  • pair<iterator, bool> insert (const value_type& val);返回一个类型为 pair<iterator, bool> 的值。pair 是由两个成员变量组成的新的数据结构,第一个参数 iterator 是第一个成员,可以通过使用.first来取出;第二个参数 bool 是第二个成员,可以通过使用 .second来取出。
    • 插入成功:返回pair<指向新元素的迭代器,true>
    • 插入失败:返回pair<指向已存在等价元素的迭代器, false>
      可以看出无论插入是否成功,返回的迭代器指向的新元素值都为val。
  • template <class InputIterator>
      void insert (InputIterator first, InputIterator last);
    根据迭代器区间插入,已经在容器中存在的值不会插入。知道一下这个用法。
  • void insert (initailizer_list<value_type> il);
    根据初始化列表插入,已经在容器中存在的值不会插入。知道一下这个用法。

下面通过代码来演示这三种用法:
图一展示单个元素的插入:
set的第一种插入方法

图二展示迭代器区间的插入:
set的第二种插入方法

图三展示初始化列表的插入:
set的第三种插入方法
总代码:

#include <iostream>
#include <set>
#include <vector>
using namespace std;

void Test1() {
	cout << "1. 单个数据插入演示:" << endl;
	set<int> mySet = { 1, 5, 3, 7 };
	cout << "初始集合:";
	for (auto num : mySet) cout << num << " ";
	cout << endl << endl;

	// 插入新元素(成功示例)
	auto result1 = mySet.insert(2);
	cout << "插入 2:" << (result1.second ? "成功" : "失败");
	cout << ", 迭代器指向:" << *result1.first << endl;

	// 插入已存在元素(失败示例)
	auto result2 = mySet.insert(3);	// 3已存在
	cout << "插入 3:" << (result2.second ? "成功" : "失败");
	cout << ", 迭代器指向:" << *result2.first << endl;

	//再插入一个新元素
	auto result3 = mySet.insert(9);	// 3已存在
	cout << "插入 9:" << (result3.second ? "成功" : "失败");
	cout << ", 迭代器指向:" << *result3.first << endl;

	cout << "当前集合:";
	for (auto num : mySet) cout << num << " ";
	cout << endl << endl;
}

void Test2() {
	cout << "2. 迭代区间插入演示: " << endl;
	set<int> mySet = { 1, 5, 3, 7, 9 };
	cout << "插入前集合的内容:";
	for (auto num : mySet) cout << num << " ";
	cout << endl;

	// 准备一个vector, 包含新元素和已存在元素
	vector<int> vec = { 10, 12, 3, 9 };

	mySet.insert(vec.begin(), vec.end());	// 插入整个vector

	cout << "插入后集合:";
	for (auto num : mySet) cout << num << " ";
	cout << endl << endl;
}

void Test3() {
	cout << "3. 初始化列表插入演示: " << endl;
	set<int> mySet = { 1, 5, 3, 7, 9 };
	cout << "插入前集合的内容:";
	for (auto num : mySet) cout << num << " ";
	cout << endl;

	cout << "插入{ 4, 6, 8, 3, 5 } (3和5已存在)" << endl;
	mySet.insert({ 4, 6, 8, 3, 5 });

	cout << "插入后集合:";
	for (auto num : mySet) cout << num << " ";
	cout << endl << endl;
}

int main() {
	Test1();
	cout << endl;

	Test2();
	cout << endl;

	Test3();
	cout << endl;

	return 0;
}

4.2. set 的查找

set 的方法里面可以有两种方式来达到查找的目的:

  1. iterator find (const value_type& val);查找val,返回val所在的迭代器,没有找到返回end()
  2. size_type count (const value_type& val) const;查找val,返回 val 的个数。在 set 中val 的个数只能是01,因为 set 是一个去重的集合。如果使用 multiset 的话,val 的个数可能就不止是 1

通过下面的代码来演示这两个函数的使用方式:

#include <iostream>
#include <set>
using namespace std;

int main() {
	set<string> fruitSet = { "Apple", "Banana", "Orange", "Grape" };

	cout << "当前水果集合:";
	for (const auto& fruit : fruitSet)	cout << fruit << " ";
	cout << endl << endl;

	// find()函数示例
	cout << "1. find() 函数演示:" << endl;
	// 查找存在的水果
	string target1 = "Orange";
	auto it = fruitSet.find(target1);
	if (it != fruitSet.end()) {
		cout << "找到了 " << target1 << ", 迭代器指向:" << *it << endl;
	}
	else {
		cout << "没有找到 " << target1 << endl;
	}

	// 查找不存在的水果
	string target2 = "mango";
	it = fruitSet.find(target2);
	if (it != fruitSet.end()) {
		cout << "找到了 " << target2 << ", 迭代器指向:" << *it << endl;
	}
	else {
		cout << "没有找到 " << target2 << endl;
	}
	cout << endl;


	// 2.count() 函数演示
	cout << "2. count() 函数演示:" << endl;
	// 统计存在的水果
	string target3 = "Banana";
	size_t count = fruitSet.count(target3);
	cout << target3 << " 在集合中的个数:" << count << endl;

	// 统计不存在的水果
	string target4 = "Watermelon";
	count = fruitSet.count(target4);
	cout << target4 << " 在集合中的个数:" << count << endl;
	cout << endl;

	return 0;
}
/*
运行结果:
当前水果集合:Apple Banana Grape Orange

1. find() 函数演示:
找到了 Orange, 迭代器指向:Orange
没有找到 mango

2. count() 函数演示:
Banana 在集合中的个数:1
Watermelon 在集合中的个数:0
*/

总结一下:

  • find()count()的时间复杂度相同,都是 O ( log ⁡ N ) O(\log N) O(logN)。都可以用来查看元素是否存在于集合中。如果不存在于集合中,在 find() 里的标志是返回 end() 的迭代器;在 count() 里的标志是返回 数字0。
  • 只需要知道元素是否存在,更推荐使用count(),在条件判断中直接使用if (set.count(value)) 即可。
  • 需要找到元素的迭代器进行操作,或者需要删除找到的元素,使用 find()
  • 注意区分算法库中也有一个find()函数,但是算法库中的find()是暴力搜索,通过遍历迭代器来查找元素,时间复杂度为 O ( N ) O(N) O(N),而 set 中自定义的 find() 是运用了红黑树的规则来进行快速查找的。

4.3. set 的元素删除

介绍三个删除函数的声明:

  1. size_type erase (const value_type& val);删除 val,val 不存在返回0 ,存在返回1。如果是 multiset,val 存在返回的值不一定是 1,可能是一个比 1 更大的数字。
  2. iterator erase (const_iterator position);删除一个迭代器位置的值。通过 set::find() 函数来找到某个 val 对应的迭代器,将迭代器传入 erase 函数中进行删除。函数的返回值是一个迭代器,指向下一个元素。
    使用方法:
    auto it = set.find(val);
    it = set.erase(it);
    这样 it 就会得到下一个元素的迭代器(也可以说是更新),可以这样子循环删除。
  3. iterator erase (const_iterator first, const_iterator last);删除一段迭代器区间的值。这段迭代器区间是左闭右开的范围,且要求左边的迭代器不能比右边的迭代器大(逻辑上不能小于,比如是保存数字的set,就应该是[40,80),而不是[80,40)这样子)。正常来说我们不能直接得到这么一段要求内的迭代器区间,除非操作的人很熟悉这个 set 对象的内容,所以后面还会介绍得到某段区间的函数。函数的返回值是一个指向最后一个被删除元素之后位置的迭代器,通常就是 last 参数指向的位置。函数内部会自动调整后返回迭代器,因为一次删除操作可能使之前迭代器失效。

补充:两个可以得到某段区间的函数,可以用来搭配最后一种删除函数来使用:

  • iterator lower_bound (const value_type& val) const;返回大于等于 val 位置的迭代器。
  • iterator upper_bound(const value_type& val) const;返回大于 val 位置的迭代器。
  • 迭代器区间内,first 采用 lower_bound, last 采用 upper_bound 是左闭右闭区间;而 first 采用 lower_bound ,last 也采用 lower_bound 是左闭右开区间。

下面的代码逐一演示上面三个删除函数的情景:

#include <iostream>
#include <set>
using namespace std;

void Print(set<int>& Set) {
	for (const auto& num : Set)
		cout << num << " ";
	cout << endl << "大小:" << Set.size() << endl << endl;
}

int main() {
	set<int> numSet = {10, 20, 30, 40, 50, 60, 70, 80,90};
	cout << "初始集合:";
	Print(numSet);

	// 1.删除特定值 - size_type erase (const value_type& val)
	cout << "1. 删除特定值" << endl;
	//	删除存在的值
	size_t result1 = numSet.erase(10);
	cout << "删除10:返回值 = " << result1 << " (1表示成功)" << endl;
	// 删除不存在的值
	size_t result2 = numSet.erase(25);
	cout << "删除25:返回值 = " << result2 << " (0表示失败)" << endl;
	
	cout << "删除操作后的集合:";
	Print(numSet);

	// 2.删除一个迭代器位置的值 - iterator erase (const_iterator position)
	cout << "2. 删除迭代器位置的值" << endl;
	//	查找要删除的元素
	auto it = numSet.find(50);
	if (it != numSet.end())	{// 如果结果不是 end(),说明找到了	
		cout << "找到元素:" << *it << endl;
		// 删除该迭代器指向的元素
		auto return_it = numSet.erase(it);

		cout << "删除后返回值指向:";	// 检验函数返回值的结果
		if (return_it != numSet.end()) {
			cout << *return_it << endl;	// 指向下一个元素
		}
		else {
			cout << "end()" << endl;
		}
	}

	cout << "删除50后的集合:";
	Print(numSet);

	// 3. 删除迭代器区间的值 - iterator erase (const_iterator first, const_iterator last)
	cout << "3. 删除迭代器区间的值:" << endl;

	// 想要删除35 - 85之间的值
	// 想要左闭右闭,使用lower_bound和upper_bound,
	// 如果要左闭右开,则使用两个lower_bound
	// lower_bound是大于等于, upper_bound是大于
	cout << "删除 区间 35-85 之间的值:";
	auto first_it = numSet.lower_bound(35);
	auto last_it = numSet.upper_bound(85);
	if (first_it != numSet.end()) {
		cout << "删除区间 [" << *first_it << ", " << *last_it << ")" << endl;
		// 删除区间
		auto return_it = numSet.erase(first_it, last_it);
		cout << "删除后返回值指向:";
		if (return_it != numSet.end()) {
			cout << *return_it << endl;	// 指向90
		}
		else {
			cout << "end()" << endl;
		}
	}
	cout << "删除区间 35-85 之间的值后的集合:";
	Print(numSet);

	return 0;
}
/*
初始集合:10 20 30 40 50 60 70 80 90
大小:9

1. 删除特定值
删除10:返回值 = 1 (1表示成功)
删除25:返回值 = 0 (0表示失败)
删除操作后的集合:20 30 40 50 60 70 80 90
大小:8

2. 删除迭代器位置的值
找到元素:50
删除后返回值指向:60
删除50后的集合:20 30 40 60 70 80 90
大小:7

3. 删除迭代器区间的值:
删除 区间 35-85 之间的值:删除区间 [40, 90)
删除后返回值指向:90
删除区间 35-85 之间的值后的集合:20 30 90
大小:3
*/

删除操作的结果图:
删除操作的运行结果


5. multiset 和 set 的差异

  multiset 和 set 的使用基本完全类似,主要区别点在于 multiset 支持值冗余,那么 insert/find/count/erase都围绕着支持值冗余有些差异。
  可以说,set 能对一段数据进行去重和排序,而 multiset 仅仅是对这一段数据进行排序。

  • 当数据中的 x 存在多个时,set 只保留一个 x,而 multiset 会全部保留下来。
  • 当数据中的 x 存在多个时,find()只会查找中序的第一个 x,此时通过遍历这个迭代器,可以得到剩下的 x。
  • 当数据中的 x 存在多个时,count()会返回 x 的实际个数,不再只是 0 和 1。
  • 当数据中的 x 存在多个时,erase()把 x 作为参数传递后会删除掉所有的 x。

希望对大家有帮助! 😃

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值