C++ STL之map/set

一.关联式容器与序列式容器

1.概念

先前我们已经介绍了大部分的序列式容器,例如string、vector、list、deque、array,forward_list。

他们被称作序列式容器,是因为他们在逻辑上是线性结构,并且任意两个数据没有必要性联系,即使发生交换或任意删除也依然是序列式容器。序列式容器中的元素是按照他们存储位置来顺序存储和访问的。接下来学习的关联式容器,他们在逻辑上通常不是线性的,并且两个位置的数据具有强关联性,随意更改他们会导致关联关系被破坏。最主要的关联式容器有map/set系和unordered_map/unordered_set。

二.set系列的使用

set是一种关联式容器,用于存放唯一的,有序的键(key)的容器。它的一些雏形可以从上一节的二叉搜索树中看出来,因为他的底层就是二叉搜索树的改良版本——红黑树。

1.set类的介绍

• set的声明如下,T就是set底层关键字的类型

• set默认要求T⽀持⼩于⽐较,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现仿函数传给第⼆个模版参数

• set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参

数。

• ⼀般情况下,我们都不需要传后两个模版参数。

• set底层是⽤红⿊树实现,增删查效率是 ,迭代器遍历是⾛的搜索树的中序,所以是有序的。

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;

2.set的构造与迭代器

set支持正向和反向迭代,并且因为其底层为二叉搜索树,所以迭代器走的是中序遍历,并且默认为升序。set的iterator和const_iterator都不支持对数据的修改,因为这会破坏底层整个二叉搜索树的平衡。

值得注意的是,这里的set也同样支持先前提到的initializer_list的初始化方法。

// 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();

构造和去重的测试

int main()
{
	// 去重+升序排序
	//set<int> s;
	set<int, greater<int>> s;
	s.insert(5);
	s.insert(2);
	s.insert(7);
	s.insert(5);
	s.insert(7);
	s.insert(3);

	//set<int>::iterator it = s.begin();
	auto it = s.begin();
	while (it != s.end())
	{
		// error C3892: “it”: 不能给常量赋值
		//*it = 1;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	s.insert({ 2,8,3,9,2 });
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

	// void insert (initializer_list<value_type> il);
	set<string> strset = { "sort", "insert", "add" };
	//set<string> strset({ "sort", "insert", "add" });
	// 
	// 遍历string比较ascll码大小顺序遍历的
	for (auto& e : strset)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

3.set的增删查

需要注意的是,set和map的迭代器也会产生迭代器失效的问题,这个解决方法在之前学习链表的时候已经给出,在删除当前结点后将迭代器更新为下一个位置即可。

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;

增删查的测试

需要注意的是,算法库中的find函数是线性查找,本质上是单纯用for循环从迭代器初始位置开始查找,而set本身的find大家都清楚,查找树的高度次,原理和算法库的find是不同的。

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;
	}
	else
	{
		cout << x << "删除成功!" << endl;
	}*/

	cin >> x;
	auto pos = s.find(x);
	if (pos != s.end())
	{
		// pos失效
		s.erase(pos);
		//cout << *pos << endl;
	}
	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;
}

lower_bound和upper_bound:用于区间的精确取值,lower_bound(x)用于返回一个大于等于该值的最近的一个结点的迭代器,upper_bound(y)用于返回一个大于y的最近一个结点的迭代器。对于用户来说这两个方法就十分有价值,对于删除一个set或者map中某个迭代器区间的值就变得很方便,即使你不知道这两个端点值是否在这个容器中。还有一个重点,在C++中我们谈论迭代器及迭代器区间都是按左闭右开的规则进行的。

#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 << " ";
} c
out << endl;
return 0;
}

4.mutiset和set的区别

mutiset和set接口的使用基本类似,区别在于mutiset支持重复的数据,而这个区别会体现在insert,find和erase的逻辑上。

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;

	//pos = s.find(x);
	//while (pos != s.end() && *pos == x)
	//{
	//	pos = s.erase(pos);
	//}
	//cout << endl;
	s.erase(x);

	it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	return 0;
}

三.map系列的使用

1.map类的介绍

map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是 O(logN) ,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。
 

template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > //
map::allocator_type
> class map;

2.pair类型

1. 核心概念:键值对 (Key-Value Pair)

std::map 是一个关联容器,它存储的元素是由 键 (Key) 和 值 (Value) 组成的配对。每个键在map中都是唯一的,并用于自动排序和快速查找其关联的值。

std::pair 是一个标准库模板类,其唯一目的就是将两个值组合成一个单一对象。这两个值被称为 first 和 second。对于 std::map 来说,这是存储键值对的完美载体。

2. std::pair 在 std::map 中的角色

在 std::map<K, V> 中:

  • std::pair 的 first 成员是 常量键 (const Key) 类型。因为键是map排序和唯一性的依据,一旦插入,就不能被修改,否则会破坏map的内部结构(通常是红黑树)。

  • std::pair 的 second 成员是 映射值 (Mapped Value) 类型。这个值可以被修改。

因此,std::map<K, V> 中每个元素的真实类型是 std::pair<const K, V>

3.pair类型

typedef pair<const Key, T> value_type;
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)
{}
template<class U, class V>
pair (const pair<U,V>& pr): first(pr.first), second(pr.second)
{}
};
template <class T1,class T2>
inline pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}

4.创建和插入pair

在map中插入元素,需要提供一个pair对象

方法一:使用 std::make_pair (C++11之前常用)
#include <iostream>
#include <map>
#include <utility> // 包含 std::pair 和 std::make_pair 的定义

int main() {
    std::map<int, std::string> myMap;

    // 创建pair并插入
    myMap.insert(std::make_pair(1, "Apple"));
    myMap.insert(std::make_pair(2, "Banana"));

    // ...
}
方法二:使用花括号初始化 (C++11起)

C++11引入了统一的初始化语法,使得创建pair更加简洁。

// 直接使用花括号创建pair
myMap.insert({3, "Cherry"}); // 最简洁的现代写法
方法三:使用 emplace (C++11起,更高效)

emplace 方法允许你直接传递用于构造键和值的参数,它会在map内部直接构造 pair,避免创建临时对象,通常更高效。

myMap.emplace(6, "Fig"); // 直接在map内部构造 pair<const int, std::string>(6, "Fig")
map中的pair类型

按照我们日常简单的认知,map是存储键值对(Key-Value)的容器。在这里的名称定义略有不同,typedef时的Key即键,T即值,而这又分别对应pair结构体中的T1参数,T2参数,也分别对应first变量和second变量。也就是说,当我们用迭代遍历map对象时,它会返回一个类型为pair的迭代器,若要访问其中的键或者值,需要通过访问first或者second得到。如下所示:

pair<string, string> kv1("first", "第一个");
dict.insert(kv1);

dict.insert(pair<string, string>("second", "第二个"));

dict.insert(make_pair("sort", "排序"));

// C++11
dict.insert({ "auto", "自动的" });

// 插入时只看key,value不相等不会更新
dict.insert({ "auto", "自动的xxxx" });

map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
	// 可以修改value,不支持修改key
	//it->first += 'x';
	it->second += 'x';

	//cout << (*it).first <<":"<< (*it).second<< endl;
	cout << it->first << ":" << it->second << endl;
	//cout << it.operator->()->first << ":" << it.operator->()->second << endl;
	++it;
}
cout << endl;

除此之外,我们也可以顺便了解到pair设计为一个结构体的用意了。因为C++不支持同时返回多个对象,对于map来说键值对封装到一个pair类型中恰好可以满足需求,需要访问时分别解引用即可。

3.map的构造

map的⽀持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,map⽀持修改value数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。

// empty (1) ⽆参默认构造
explicit map (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// range (2) 迭代器区间构造
template <class InputIterator>
map (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
// copy (3) 拷⻉构造
map (const map& x);
// initializer list (5) initializer 列表构造
map (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();

构造和插入的测试,可以看到在插入时就是插入的pair类型,有多种插入方法。

#include<map>

int main()
{
	//map<string, string> dict;
	map<string, string> dict = { {"left", "左边"}, {"right", "右边"}, {"insert", "插入"},{ "string", "字符串" } };

	//pair<string, string> kv1("first", "第一个");
	//map<string, string> dict = {kv1, pair<string, string>("second", "第二个")};

	pair<string, string> kv1("first", "第一个");
	dict.insert(kv1);

	dict.insert(pair<string, string>("second", "第二个"));

	dict.insert(make_pair("sort", "排序"));

	// C++11
	dict.insert({ "auto", "自动的" });

	// 插入时只看key,value不相等不会更新
	dict.insert({ "auto", "自动的xxxx" });

	map<string, string>::iterator it = dict.begin();
	while (it != dict.end())
	{
		// 可以修改value,不支持修改key
		//it->first += 'x';
		it->second += 'x';

		//cout << (*it).first <<":"<< (*it).second<< endl;
		cout << it->first << ":" << it->second << endl;
		//cout << it.operator->()->first << ":" << it.operator->()->second << endl;
		++it;
	}
	cout << endl;

	return 0;
}

4.map的增删查

map增接⼝,插⼊的pair键值对数据,跟set所有不同,但是查和删的接⼝只⽤关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代还可以修改value
 

Member types
key_type -> The first template parameter (Key)
mapped_type -> The second template parameter (T)
value_type -> pair<const key_type,mapped_type>
// 单个数据插⼊,如果已经key存在则插⼊失败,key存在相等value不相等也会插⼊失败
pair<iterator,bool> insert (const value_type& val);
// 列表插⼊,已经在容器中存在的值不会插⼊
void insert (initializer_list<value_type> il);
// 迭代器区间插⼊,已经在容器中存在的值不会插⼊
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
// 查找k,返回k所在的迭代器,没有找到返回end()
iterator find (const key_type& k);
// 查找k,返回k的个数
size_type count (const key_type& k) const;
// 删除⼀个迭代器位置的值
iterator erase (const_iterator position);
// 删除k,k存在返回0,存在返回1
size_type erase (const key_type& k);
// 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
// 返回⼤于等k位置的迭代器
iterator lower_bound (const key_type& k);
// 返回⼤于k位置的迭代器
const_iterator lower_bound (const key_type& k) const;

5.map的数据修改

前⾯我提到map⽀持修改mapped_type 数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。

map第⼀个⽀持修改的⽅式时通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map还有⼀个⾮常重要的修改接⼝operator[],但是operator[]不仅仅⽀持修改,还⽀持插⼊数据和查找数据,所以他是⼀个多功能复合接⼝

需要注意从内部实现⻆度,map这⾥把我们传统说的value值,给的是T类型,typedef为mapped_type。⽽value_type是红⿊树结点中存储的pair键值对值。⽇常使⽤我们还是习惯将这⾥的T映射值叫做value

Member types
key_type -> The first template parameter (Key)
mapped_type -> The second template parameter (T)
value_type -> pair<const key_type,mapped_type>


// 查找k,返回k所在的迭代器,没有找到返回end(),如果找到了通过iterator可以修改key对应的
mapped_type值
iterator find (const key_type& k);
// ⽂档中对insert返回值的说明
// The single element versions (1) return a pair, with its member pair::first
set to an iterator pointing to either the newly inserted element or to the
element with an equivalent key in the map. The pair::second element in the pair
is set to true if a new element was inserted or false if an equivalent key
already existed.
// insert插⼊⼀个pair<key, T>对象
// 1、如果key已经在map中,插⼊失败,则返回⼀个pair<iterator,bool>对象,返回pair对象
first是key所在结点的迭代器,second是false
// 2、如果key不在在map中,插⼊成功,则返回⼀个pair<iterator,bool>对象,返回pair对象
first是新插⼊key所在结点的迭代器,second是true
// 也就是说⽆论插⼊成功还是失败,返回pair<iterator,bool>对象的first都会指向key所在的迭
代器
// 那么也就意味着insert插⼊失败时充当了查找的功能,正是因为这⼀点,insert可以⽤来实现
operator[]
// 需要注意的是这⾥有两个pair,不要混淆了,⼀个是map底层红⿊树节点中存的pair<key, T>,另
⼀个是insert返回值pair<iterator,bool>
pair<iterator,bool> insert (const value_type& val);
mapped_type& operator[] (const key_type& k);
// operator的内部实现
mapped_type& operator[] (const key_type& k)
{
// 1、如果k不在map中,insert会插⼊k和mapped_type默认值,同时[]返回结点中存储
mapped_type值的引⽤,那么我们可以通过引⽤修改返映射值。所以[]具备了插⼊+修改功能
// 2、如果k在map中,insert会插⼊失败,但是insert返回pair对象的first是指向key结点的
迭代器,返回值同时[]返回结点中存储mapped_type值的引⽤,所以[]具备了查找+修改的功能
pair<iterator, bool> ret = insert({ k, mapped_type() });
iterator it = ret.first;
return it->second;
}

详解operator[]的功能:这里我们用统计单词次数作为详解。可以看到统计单词的次数逻辑也很简单,若当前单词count中不存在,则将该单词插入,并且将次数修改为1。若当前单词已存在,则令当前单词迭代器的second(次数)++。

map<string, string> dict = { {"left", "左边"}, {"right", "右边"}, {"insert", "插入"},{ "string", "字符串" } };
map<string, int> count;
for (auto& e : dict) {
	auto ret=dict.find(e);
	if (ret == dict.end()) {
		count.insert({ e,1 });
	}
	else {
        ret->second++;
	}
}
return 0;

其实上面的代码还可以这样写:

	for (auto& e : dict) {
		count[e]++;
}

这就体现出operator[]的三重作用:key不存在,插入;key不存在,插入+修改;key存在,修改。我们可以看看operatorp[]的内部实现来说明这个问题。首先先看operator的返回值,mapped_type即“键值对”中的那个,也就是说,直接从外部看operator[]有查找的功能,能查到当前单词在map中的个数。再看内部,用了一个insert函数,并且insert的返回值pos同样也是一个pair类型,注意观察这个返回类型,是<iterator,bool>,bool类型大家都清楚,是返回插入成功还是失败,而这个iterator就是map的迭代器了,而这个iterator同样也是一个<value_type,mapped_type>类型的pair。那么insert什么呢?也就是说,当前查找的对象map中不存在时,调用这里的insert({k,mapped_type()}),就直接插入这个key,至于value就使用缺省值。这是他的插入功能。再看最后的返回,pos是一个pair类型,pos的first是迭代器iterator,iterator的second是次数,恰好对的上operator[]的返回值。

map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
// key不存在->插⼊ {"insert", string()}
dict["insert"];
// 插⼊+修改
dict["left"] = "左边";
// 修改
dict["left"] = "左边、剩余";
// key存在->查找
cout << dict["left"] << endl;

//内部原理
mapped_type& operator[](const key_type& k) {
   // return (*(insert(value_type(k, mapped_type())).first)).second;
	
	pair<iterator, bool> pos = insert({k,mapped_type()});
	iterator it = pos.first;
	return it->second;
	
}

6.mutimap与map的差异

multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。其次就是multimap不⽀持[],因为⽀持key冗余,[]就只能⽀持插⼊了,不能⽀持修改
 

四.set的map在算法题中的简单应用

1.找出两个数组的交集

题目给了我们两个无序的数组,并找出交集,只要让他们有序并且用双指针问题就会变得简单。这里考虑使用set的好处有两个,一个是可以排序,另一个是可以去重。

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());

        vector<int> v1;
        auto t1=s1.begin();
        auto t2=s2.begin();

        while(t1!=s1.end()&&t2!=s2.end()){
            if(*t1<*t2){
                t1++;
            }
            else if(*t2<*t1){
                t2++;
            }
            else{
                v1.push_back(*t1);
                t1++;
                t2++;
            }
        }
        return v1;
    }
};

2.环形链表

在学习C语言时就做过这道题,当时的解法是通过数学关系解决的。而这里的解决方法就更简单了。我们创建一个ListNode*类型的set,通过遍历链表,将链表结点依次插入set中。若不带环就直接结束,若带环,由于每次插入的结点的地址唯一,再次判断入环结点是否在set中就会检查到,由此可以实现环形链表及其入环结点的判断。(这里主要运用了set中的count函数,判断当前元素set中是否存在)

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*> s1;
        ListNode* cur=head;
        while(cur){
            if(s1.count(cur)){
                return cur;
            }else{
                s1.insert(cur);
                cur=cur->next;
            }
        }
        return nullptr;
    }
};

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值