从零开始的c++之旅——map_set的使用

1.序列式容器和关联式容器
序列式容器:逻辑结构为线性序列的数据结构,两个位置之间没有紧密的关系,比如两者交换一下还是序列式的容器,例如string,vector,deque,array等。
关联式容器:逻辑结构是非线性的,两个位置之间有紧密的关联,若交换其中两者的顺序,存储结构就被破坏了,因为他其中的元素是按关键字来保存和访问的。例如map/set系列和unordered_map/unordered_set系列。
2.set系列的使用
2.1 set的模板参数
template < class T, // T就是关键字keyclass Compare = less<T>, // 传仿函数,用来控制容器的升序降序// 默认是升序排序class Alloc = allocator<T> // 空间配置器> class set;
set底层是通过红黑树实现的,因此他的增删查的时间复杂度 O(logN),迭代器的遍历为搜索树的中序遍历,遍历出来的是一个有序列表。
set的部分接口与stirng,vector较为类似,因此我们就挑一些重要的接口详细介绍。
2.2 set的构造
无参的默认构造
虽然默认构造有三个参数,但是第三个参数空间配置器我们目前阶段不涉及,默认的缺省参数即可,我们一般就是用第一个参数控制容器的存储类型和第二个参数控制容器的升降序。
explicit set (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
set<int, less<int>> myset;
迭代器区间构造
传一段迭代器区间构造set容器。
template <class InputIterator>set (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& = allocator_type());
set<int, less<int>> myset(it1, it2);
拷贝构造
set (const set& x);
列表构造
类似vector的,set可以直接传一个列表进行构造,这其中经过了隐式类型转换。
set (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
set<int> myset = { 1,5,2,8,3,2 };
2.3 set的迭代器
set的迭代器是双向迭代器,有了迭代器也就说明了set支持范围for的遍历。
int main()
{set<int> myset = { 1,5,2,8,3,2 };for (auto i : myset){cout << i << " ";}return 0;
}
2.4 set的增删查
set只支持增删查不支持改,set底层是红黑树,改了数据就破坏了里面的结构
Member types
key_type -> T
value_type -> 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;// 返回容器中有几个对印的数据(set容器一般就是0/1个)
int set::cout(set x);
2.5 insert和迭代器遍历使⽤样例
int main()
{// 去重+升序排序 set<int> s;// 去重+降序排序(给⼀个⼤于的仿函数) //set<int, greater<int>> s;s.insert(5);s.insert(2);s.insert(7);s.insert(5);//set<int>::iterator it = s.begin();auto it = s.begin();while (it != s.end()){// error C3892: “it”: 不能给常量赋值 // *it = 1;cout << *it << " ";++it;}cout << endl;// 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败 s.insert({ 2,8,3,9 });for (auto e : s){cout << e << " ";}cout << endl;set<string> strset = { "sort", "insert", "add" };// 遍历string⽐较ascll码⼤⼩顺序遍历的 for (auto& e : strset){cout << e << " ";}cout << endl;
}
2.6 find和erase使⽤样例
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;}for (auto e : s){cout << e << " ";}cout << endl;// 直接查找在利⽤迭代器删除x cin >> x;auto pos = s.find(x);if (pos != s.end()){s.erase(pos);}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;
}
2.7 multiset和set的区别
最主要的区别就是multiset支持值冗余
#include<iostream>
#include<set>
using namespace std;
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;// 相⽐set不同的是,erase给值时会删除所有的x s.erase(x);for (auto e : s){cout << e << " ";}cout << endl;return 0;
}
2.8 set的实际算法应用 
以往做这道环形链表的题时,我们需要使用快慢指针,最后判断两个指针是否相遇,但现在只需要一个指针,遍历这个链表依次吧链表的节点的值加入set容器中,每次加入set容器时先判断但钱节点的值是否已经在set容器中即可。
class Solution {
public:ListNode *detectCycle(ListNode *head) {set<ListNode*> s;ListNode* cur = head;while(cur){if(s.count(cur)return cur;elses.insert(cur);cur=cur->next;}return nullptr;}
};
3.map系列的使用
3.1 map的模板参数
template < class Key, // map::key_typeclass T, // map::mapped_typeclass Compare = less<Key>, // map::key_compareclass Alloc = allocator<pair<const Key,T> > // map::allocator_type > class map;
其中Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数。第四个参数与set相同,我们目前阶段不用管。
map底层是也是⽤红⿊树实 现,所以其增删查改效率是O(logN) ,往其中数据类型为pair类型,这个pair类型有“ key ” 和 “ value ”两个值(接下来一点我们会讲),插入到map中是按key值的大小排序的,插入逻辑与二叉树相同。
其迭代器遍历是⾛的中序,按key的值进行遍历的。
3.2 pair类型的介绍
map底层节点中插入的数据,是使用 pair<Key,T> 键值对来存储数据的。
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) );
}
我们使用map的最爽的插入方式就是走隐式类型转换
int main()
{map<int, string> mymap;mymap.insert({ 1, "+" });mymap.insert({ 4, "-" });mymap.insert({ 2, "*" });mymap.insert({ 3, "/" });
}
3.3 map的构造
map与set又比较类似,所以这里我们简略提一提
(1) ⽆参默认构造
explicit map(const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());(2) 迭代器区间构造
template <class InputIterator>
map(InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type & = allocator_type());(3) 拷⻉构造
map(const map& x);(4) initializer 列表构造
map(initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
3.4 map的迭代器
// 迭代器是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type// 正向迭代器
iterator begin();
iterator end();
// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
int main()
{map<int, string> mymap;mymap.insert({ 1, "+" });mymap.insert({ 4, "-" });mymap.insert({ 2, "*" });mymap.insert({ 3, "/" });for (auto i : mymap){cout << i.second << " ";}cout << endl;
}
3.5 map的增删查
map支持增删查,因为map的底层也是红黑树,改了数据就破坏了里面的结构
Member types
key_type -> Key
mapped_type -> 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;
3.6 map的数据修改
而由于map的底层存储的是我们所说的pair<const Key, T>类型的数据,我们可以发现key的值是带了const的,这是因为我们map的底层排序是按照ket值进行的排序,因此我们无法修改key值,但是我们可以修改其T的值。
我们可以记其理解为键名和键值,键名是无法改动的,而键值是可以修改的。
map中数据的第一种修改方式是通过迭代器遍历找到对应的数据进行修改。
但是map还有一种非常重要的修改数据的方式,也就是其重载的 opreator[ ] ,operator[ ]不仅仅⽀持修改,还⽀持插⼊数据和查找数 据,所以他是⼀个多功能复合接⼝。
operator[ ] 的实现是通过map的成员函数 insert 来实现的。
为什么要通过insert函数来实现呢,因为insert函数是插入一个pait< key , T >的对象,此时就有两种情况:
1. 如果key已经在map中,那么则插入失败,返回一个pair< iterator , bool >对象,iterator指的是map中已有的该对象的节点的迭代器,bool值为false,表示插入失败,因为map中已经存在对应的键值。
2. 如果key不在map中,插入成功,返回一个pair< iterator , bool >对象,iterator指的是map中若插入的该节点的迭代器,bool值为true,表示插成功,
也就是说物理插入成功还是失败,返回的pair类型其 iterator 都是key所在的迭代器位置,那么也就意味着插入失败的情况实际是在进行查找的概念,插入成功则是实现插入的功能,这也就实现了operator所需的多功能复合借口。
Member types
key_type -> Key
mapped_type -> T
value_type -> pair<const key_type,mapped_type>mapped_type& operator[] (const key_type& k)
{pair<iterator, bool> ret = insert({ k, mapped_type() });//插入成功,pair对应的为插入到节点的位置,实现了插入的功能//插入失败,pair对应的为已有的节点的位置,实现了查找功能//而无论是哪种功能,由于返回值是其 T类型的引用,因此支持了修改的功能iterator it = ret.first;return it->second;
}
3.7 构造遍历及增删查使⽤样例
#include<iostream>
#include<map>
using namespace std;
int main()
{map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插入"},{ "string", "字符串" } };auto it = dict.begin();while (it != dict.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;pair<string, string> kv1("first", "第一个");//多种插入方式dict.insert(kv1);dict.insert(pair<string, string>("second", "第二个"));dict.insert(make_pair("sort", "排序"));dict.insert({ "auto", "自动的" });//这一种用着最香dict.insert({ "left", "左边,剩余" });for (const auto& e : dict){cout << e.first << ":" << e.second << endl;}cout << endl;string str;while (cin >> str){auto ret = dict.find(str);if (ret != dict.end()){cout << "->" << ret->second << endl;}else{cout << "⽆此单词,请重新输⼊" << endl;}}return 0;
}
3.8 map的迭代器和[]功能样例
#include<iostream>
#include<vector>
#include<set>
#include <map>
using namespace std;int main()
{// 利⽤[]插⼊+修改功能,巧妙实现统计⽔果出现的次数 string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠","苹果", "⾹蕉", "苹果", "⾹蕉" };map<string, int> countMap;for (const auto& str : arr){// []先查找⽔果在不在map中 // 1、不在,说明⽔果第⼀次出现,则插⼊{⽔果, 0},同时返回次数的引⽤,// ++⼀下就变成1次了// 2、在,则返回⽔果对应的次数++ countMap[str]++;}for (const auto& e : countMap){cout << e.first << ":" << e.second << endl;}cout << endl;return 0;
}
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{map<string, string> dict;dict.insert(make_pair("sort", "排序"));// key不存在->插⼊ {"insert", string()} dict["insert"];// 插⼊+修改 dict["left"] = "左边";// 修改 dict["left"] = "左边、剩余";// key存在->查找 cout << dict["left"] << endl;return 0;
}
3.9 multimap和map的差异
类比multiset和set的区别,multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么 insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如 find时,有多个key,返回中序第⼀个。
其次就是multimap不⽀持[],因为⽀持key冗余, []就只能⽀持插⼊了,不能⽀持修改。