map 和 set 也是 c++ 中两个很重要的关联式容器,其底层结构都为搜索二叉树,set属于 k 模型, map 属于 kv 模型
序列式容器:vector/list/deque/… 单纯为了存储数据
关联式容器:map/set/… 数据之间存在某种关联关系(不只是存储数据)
1. set
还是在 cplusplus 中 set 的部分进行学习
这里我们主要针对和之前容器不相同的部分进行学习
1.1. insert
三种方式
第一种,插入一个值,放在树中合适位置
第二种,指定位置插入值,不建议使用,可能会出现问题
第三种,插入一段迭代器区间,特殊情况下使用
insert 还是按照之前学的容器写代码
#include<iostream>
#include<set>
using namespace std;
void test_set1()
{
set<int> s;
s.insert(5);
s.insert(6);
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(8);
s.insert(4);
set<int>::iterator it = s.begin();
while(it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
test_set1();
return 0;
}
在输入无序数据的情况下,最后输出的结果是升序的。
我们再试试一组数据
void test_set1()
{
set<int> s;
s.insert(1);
s.insert(2);
s.insert(1);
s.insert(3);
s.insert(1);
s.insert(2);
set<int>::iterator it =s.begin();
while(it != s.end())
{
cout << *it << " ";
}
cout << endl;
}
这组数据输入后,我们可以看见,同一个数据只插入了一次,这也是之前二叉搜索树的特性,一般情况下,同一个数据只能插入一个。
如果需要插入的位置已经有值,按照我们的理解,应该会返回一个 false,我们需要用 bool 接受
但是接受时这里就报错了
我们再看这里的返回值,有bool,但不止是 bool,返回的是一个 pair
1.2. 键值对
这里我们先简单了解一下用法,后面学习慢慢体会
pair是一个类,其 中分别含有 first, second 成员。
一般用于需要两个参数的地方。
SGU-STL 中关于键值对的定义
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)
{}
};
这里我们的 bool 是作为 pair 中的第二个参数传入的,所以我们要这样写
pair<set<int>::iterator, bool> ret = s.insert(2);
cout << ret.second << endl;
我们直接输出 返回的pair类型中的第二个参数second即可,这里可以看见,插入失败,返回 false,输出0。
另一个参数是 set< int >::iterator 类型,返回的是个迭代器,如果插入成功,就会返回 插入位置的迭代器。
虽然这里在定义的时候写的只有 iterator
pair<iterator, bool> insert(const value_type& val);
因为这个函数是在类中实现的,直接写迭代器知道是哪个类的,但是在使用的时候要写清楚,是什么类型。
当然,偷懒的话可以写 auto
auto ret = s.insert(7);
但是这样写就不好分清楚第一模版参数和第二模版参数的类型,时间中不太建议用 auto。
set 支持范围for,只要支持迭代器就支持范围for
1.3. erase,find
erase 也支持3种传入方式
- 删除某个迭代器位置的值
- 删除某个值
- 删除一段迭代器区间
void test_set2()
{
set<int> s;
s.insert(1);
s.insert(2);
s.insert(6);
s.insert(3);
s.insert(5);
s.insert(9);
s.insert(8);
s.insert(7);
set<int>::iterator it1 = s.begin();
while(it1 != s.end())
{
cout << *it << " ";
++it1;
}
cout << endl;
set<int>::iterator find = s.find(3);
s.erase(find);
it1 = s.begin();
while(it1 != s.end())
{
cout << *it1 << " ";
++it1;
}
}
这里我们直接使用 find(),找到 3 这个位置的迭代器,然后对这个位置删除。
如果删除迭代器节点时,节点不存在会发生什么?
当我们删除 30 时
这里崩了
我们看看 find 有什么特性
这里我们可以看见,find 在未找到的情况下,会返回 end() , 直接删除这个位置肯定会有问题。
如果通过删除值进行删除
s.erase(30);
我们看到,没删也没崩。
这里 erase 返回的是一个 size_type 类型
在成员类型这里,我们可以看见,size_type 就是 size_t 类型。
前面是 void 类型,传入迭代器直接删除该位置,但是这里是 size_t 类型,会删除查找到的节点,至于这个返回类型有什么用,且看下回分解。
1.4. lower_bound && upper_bound
这两个函数的作用是取区间
void test_set3()
{
std::set<int> myset;
std::set<int>::iterator itlow, itup;
for(int i = 1; i < 10; i ++)
{
myset.insert(i * 10);
}
itlow = myset.lower_bound(30);
itup = myset.upper_bound(60);
myset.erase(itlow, itup);
std::cout << "myset contains:" ;
for(std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
{
std::cout << ' ' << *it;
std::cout << '\n';
}
}
我们看到,这里输出结果是 10, 20。70, 80, 90。
lower_bound 返回的是 <= val 的迭代器位置
upper_bound 返回的是 > val 的迭代器位置
如果我们给 这两个函数传入不存在的值呢?
myset.lower_bound(25);
我们看到还是这个结果。
这里记住,返回的区间是 左闭右开。
up 返回的是开区间, low 返回的是闭区间
1.5. equal_range
这个函数返回的是一段区间,这个返回类型是由一个 pair 返回的。这个区间的特点是,区间内的都是相同数据
void test_set4()
{
std::set<int> myset;
for(int i = 1; i <= 5; i++)
{
myset.insert(i * 10);
}
std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
ret = myset.equal_range(30);
std::cout << "the lower bound points to:" << (*ret).first << '\n';
std::cout << "the upper bound points to:" << (*ret).second << '\n';
}
注:这里也是 左闭右开 的范围
first 是 >= val 的值的迭代器
second 是 < val 的值的迭代器
1.6. count
count 也算比较熟的了,返回的是某个值的个数。
但是因为这里 set 每个值只能存在一个,所以 count() 只能是 1 或是 0
因此 count 可以用来判断某个值在不在 set 中
void test_set4()
{
//...
if(s.count(3))
{
cout << "3 在" << endl;
}
else
{
cout << "3 不在" << endl;
}
}
比如这里,直接使用 count() 判断 3 在不在 set 内
这里还要注意,set 中的值是不支持修改的
set<int>::iterator it1 = s.begin();
while(it1 != s.end())
{
*it1 = 20;
//...
}
插入的值,是有 const 修饰的,我们不能直接修改。
2. multiset
上面我们说,set 这个搜索二叉树,不支持相同数据的插入,所以在 set 的头文件中,还有一个支持多个相同元素插入的 set----multiset
变异的搜索二叉树,允许多个相同的值进入同一棵树中,同时,它和上面的 set 大部分函数相同,不过有些函数用法不太相同。
这里我们插入相同的数据试试
void test_set1()
{
multiset<int> s;
s.insert(1);
s.insert(2);
s.insert(1);
s.insert(4);
s.insert(1);
s.insert(1);
s.insert(7);
multiset<int>::iterator it = s.begin();
while(it != s.end())
{
cout <<*it << " ";
it++;
}
cout << endl;
}
换成 multiset,这里就能存相同的数据,而且因为 multiset 和 set 函数基本一致,只需要稍微修改一下类型 multiset 就能使用 set 的代码。
这里节点的插入,节点值相同时,插入在相同节点的左边或者右边都可以。
如果都插入在右边,按理来说会形成这样的结构,虽然不合理,但是后面我们会学习 AVL树,红黑树等,会进行旋转改变结构,形成下面的结构
所以这里我们暂时理解,插左边或者右边是没有区别的。
如果我们再插入一个5
就会插入在这里,相同的数据不一定在一起,只要想通数据在中序在能一起遍历即可。
2.1. equal_range
set 中,它会返回一段相同数据的区间,但是因为 set 中每个数据只存在一个,所以这个函数 对set 没什么意义。
但是这里对 multiset 就不一样了,我们可以直接把相同数据所在的区间取出来,然后进行操作。
这里,我们使用的是 erase 来验证。
auto ret = s.equal_range(1);
s.erase(ret.first, ret.second);
it = s.begin();
while(it != begin())
{
cout << *it << " ";
++it;
}
cout << endl;
这里 ret 的类型比较长,可能不好记,理解万岁
std::pair<std::multiset<int>::const_iterator, std::multiset<int>::const_iterator> ret;
这样就能通过获取迭代器区间来进行删除操作了
2.2. erase
其实这里,我们使用 equal_range 来 erase 意义并不大。
s.erase(1);
如果我们不按照区间删除,直接在 erase 传入 1
我们看到,这里也实现了把 1 全部删除的操作。
前面在 set 中学习 erase,我们直到,erase 的返回值是 size_t 类型,这个 size_t 的值在这里就有意义了
szie_t n = s.erase(1);
cout << n << endl;
这里的 返回值 n,就是删除数据的个数。
3. map
map 也有两个,一个是map,一个是 multimap
map 属于 kv 型的数据结构
之前我们解决 kv 型的问题需要自己先实现这个结构,但是这里我们直接使用 map 容器即可。
map 和 set 底层都是 搜索二叉树,所以大部分函数都类似。
3.1. insert
这里我们要注意,这里返回值是 pair 类型,传入的类型却只有一个,我们知道 kv 模型应该需要插入两个参数 key 和 value 的,但是这里为什么只有一个值?
这里我们可以看见, value_type 也是 pair 类型,pair 中的两个模版参数是 key_type 和 mapped_type。
如果我们想要插入数据,我们就要这样写
map<string, string> dict;
dict.insert(pair<string, string>("sort","排序"));
这里需要注意,传入的 key 是 const 修饰的,而 value 是没有 const 修饰的,这里一般要求只能修改 value, 不能对 key 进行修改。
但是在模版中,模版参数不同,类型就应该不同。
pair<string, string>;
pair<const key_value, mapped_type>;
两个类型不一样,但是为什么能通过呢?
2.2. map 中的键值对
pair 虽然东西不大,但是内容齐全,构造,析构,赋值运算符重载都有
这里的构造函数,有无参构造,带参构造,拷贝构造。
我们这里是直接调用 pair 的拷贝构造函数
string 能拷贝构造 const string 类型,所以这里能直接使用。
除了 string 去初始化 const string,只要是类型相近的,这里都可以使用
dict.insert(pair<const char*, const char*>("sort", "排序"));
只要是符合初始化 const string 类型的类型,都能使用。
除了在 insert 中使用匿名对象调用拷贝构造,我们还有其他方法
make_pair
这里,能直接返回一个 pair 的匿名对象。
这里还有个很方便的地方,这里的 make_pair 使用了函数模版,这里的好处是能自动推演类型
dict.insert(make_pair("left", "左"));
这里我们输入了 left, 左,make_pair 接受到参数后自动推演类型,就不需要我们自己去写传入类型了。
dict.insert<pair<const char*, const char*>("sort", "排序"));
dict.insert(pair<string,string>("sort,"排序");
dict.insert(make_pair("left","左"));
使用迭代器访问数据,可以这样写
map<string, string>::iterator it = dict.begin();
while(it != dict.end())
{
cout << (*it).first << " ";
out << (*it).second << " ";
++it;
}
于是我们能输出下面的结果。
当 map 中的类型为自定义类型时,可以使用 -> 符号
显示调用
it.operator->()->first;
因为 map 可以使用迭代器,所以也支持范围 for
for(auto kv : dict)
{
cout <<kv.first << " " << kv.second;
}
但是这里要注意,这里最好使用引用,因为这里实现的 map 中的 pair 含有两个 string,如果直接拷贝过去,就会进行两次深拷贝,这样消耗太大。
所以这里最好传入引用,且在不修改值的情况下,最好传入 const
for(const auto& kv : dict)
但是如果不加 const,我们直接传入引用,那不就能对 树 中的值直接进行修改?
for(auto& kv : dict)
{
kv.first += 'x';
kv.second += 'x';
cout << kv.first << " " << kv.second;
}
但是这里修改 key 的时候,这里报错了。
因为 map 中的 pair 第一个参数本身就是 const 修饰过的,不能随意改变,而 second 本身没有const 修饰,是可以修改的。
key 是树中查找的线索,key被随意修改,树的结构很可能会被破坏。
但是 value 是无所谓的,一般不参与查找。
2.3. operator[]
首先我们想要实现 map 统计次数
void test_map2()
{
string arr = { "苹果", "西瓜", "苹果", "西瓜", "苹果" , "西瓜", "苹果" , "西瓜", "苹果" ,"香蕉" };
map<string, int> countMap;
for(auto& str : arr)
{
auto ret = countMap.find(str);
if(ret == countMaap.end())
{
countMap.insert(make_pair(str,1);
}
else
{
ret->second++;
}
}
}
这里我们先遍历 arr, 未找到相同的字符串则先插入字符串,找到字符串就对该位置的 value++;
但是这样玩太麻烦了,这里就要注意 operator[] 这个操作
void test_map2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果" , "西瓜", "苹果" , "西瓜", "苹果" ,"香蕉" };
map<string, int> countMap;
for(auto& str, arr)
{
countMap[str]++;
}
for(auto& kv : countMap)
{
cout << kv.first << " " << kv.second << endl;
}
}
只需要这么一句代码就能结束战斗。
这里可能不好理解
我们一步一步剖析
make_pair(k, mapped_type())
这步是创建匿名对象
this->insert( );
这步是往对象中插入数据,需要分情况
insert() 插入成功会返回 插入成功节点的 pair
插入失败会返回 重复值 位置的 pair。
我们需要的是这里的 iterator,所以要指明,要 pair 中的第一个元素
(Pair).first;
此时的 iterator 就是插入/重复位置的迭代器,所以对迭代器解引用后,使用其中的 second
这里的 iterator 指向的就是 pair 类型。
*(iterator).second;
下面的函数可能好理解一点
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(key, V());
return ret.first->second;
}
学会使用 [] 的操作符,我们现在就能直接用了
void test_map1()
{
map<string, string> dict;
dict.insert(pair<const char*, const char*>("sort", "排序"));
dict.insert(pair<string, string>("right", "右"));
dict.insert(make_pair("left", "左"));
dict["erase"];
cout << dict["erase"] << endl;
dict["erase"] = "删除";
cout << dict["erase"] << endl;
dict["test"] = "测试";
dict["left"] = "左边,剩余";
map<string, string>::iterator it = dict.begin();
while(it != dict.end())
{
cout << it.operator->()->first << " ";
cout << (*it).second << endl;
++it;
}
for(auto& kv : dict)
{
cout << kv.first << " " << kv.second;
}
}