Map和Set使用及模拟实现
1)引入
关联式容器与序列式容器:
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高
键值对:
用来表示具有一 一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息
pair
在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) {} };
make_pair
make_pair:参考 make_pair
template <class T1,class T2> pair<T1,T2> make_pair (T1 x, T2 y) { return ( pair<T1,T2>(x,y) ); }
2)树形结构的关联式容器(STL分为树形结构和哈希结构)
①set
注意
注意
:
- 元素不可重复
- 在Set中
key就是value,value也是key,set中只放value,但在底层实际存放的是由<value, value>构成的键值对
- 以
RBTree
作为底层容器不允许迭代器修改Set的key/value,底层将其设置为了const(修改会造成迭代器失效)
,但是可以删除- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则(
默认按照小于
)进行排序- set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代
- set中查找某个元素,时间复杂度为log2N
迭代器
与前面容器大同小异,参考:Set
迭代器遍历:for (std::set<int>::iterator it=myset.begin(); it!=myset.end(); ++it) std::cout << ' ' << *it;
构造 容量(略)
与前面容器大同小异,参考:Set
修改
函数 | 功能 |
---|---|
insert | 插入元素 |
erase | 删除元素 |
… | … |
关联式容器都是
使用insert/erase
进行操作而非push/pop
与前面容器大同小异,参考:Set
操作
函数 | 功能 |
---|---|
find | 获取元素的迭代器 |
count | 计算具有特定值的元素的个数 |
lower_bound | 将迭代器返回到下限 |
upper_bound | 将迭代器返回到上限 |
equal_range | 获取相等元素的范围 |
find()返回值
:如果找到元素,则返回该元素的迭代器,否则返回end()
count()
只能返回0或1
相当于库中的swap和容器swap区别:
参考
lower_bound 和 upper_bound 和 equal_range在从小到大排序数组中:
upper_bound(begin,end,num)
lower_bound(begin,end,num)
在从大到小排序数组中(重载:需要传一个比较函数):
upper_bound(begin,end,num, greater< type>())
lower_bound(begin,end,num, greater< type>())
②multiset
注意
注意:
- 与set的区别是,
multiset中的元素可以重复
,set是中value是唯一的- 其他与set一致
迭代器 构造 容量 修改(略)
参考:multiset
操作
函数 | 功能 |
---|---|
find | 拿到查找元素的迭代器 |
… | … |
由于允许重复 multiset的
find找到的是中序遍历的第一个元素
(底层为RBTree) 找到此元素以后,要继续看此元素的左孩子是不是同值元素,如果不是,就返回当前这个元素,如果是,继续取左边的同值元素,继续刚才的判断
验证
:
由于元素可以重复 这里用count可以计算个数
③map
注意
注意:
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素
- 在内部,map中的元素总是
按照键值key
进行比较排序的- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value
typedef pair<const key_type,mapped_type> value_type
- 底层是RBTree
构造 迭代器 容量 操作(略)
参考:map
修改
函数 | 功能 |
---|---|
insert | 插入元素 |
… | … |
pair<iterator,bool> insert (const value_type& val)
insert返回值:
- 如果插入成功,将pair<iterator,bool>中的iterator设为该插入元素的迭代器,bool设为true
- 如果插入失败说明已存在此元素,将pair<iterator,bool>中的iterator设为已存在元素的迭代器,bool设为false
参考:insert
元素访问
函数 | 功能 |
---|---|
operator[] | 访问元素 |
mapped_type& operator[] (const key_type& k) { pair<iterator, bool> ret = insert(make_pair(k, mapped_type())); return ret.first->second; }
使用
[]
相当于:(*((this->insert(make_pair(k,mapped_type()))).first)).second
参考insert返回值,很好理解,这里[]重载已经改变了[]的意思了
这里的mapped_type()是value的匿名对象(缺省)(相当于第一次给0
),如果value是string就会调用string的匿名对象,类比
返回值可以理解为:
- k不存在,插入默认构造函数生成缺省值的value的pair<K, V()>
- k存在,返回k对应的value值
例子:
map<string, string> dict; dict.insert(make_pair("sort", "排序")); dict["string"] = "字符串"; // 先插入("string", ""),再修改value dict["map"]; // 插入("map", "") dict["map"] = "地图,映射"; // 查找+修改
统计一个数组中每个字符串出现的次数(三种)
1.普通方法
string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" }; map<string, int> countMap; for (auto& e : str) { auto ret = countMap.find(e); if (ret != countMap.end()) { //(*ret).second++; ret->second++; // ret->->second++; } else { countMap.insert(make_pair(e, 1)); //countMap.insert(pair<string, int>(e, 1)); } }
2.使用insert
string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" }; map<string, int> countMap; for (auto& e : str) { //pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(e, 1)); auto ret = countMap.insert(make_pair(e, 1)); // 插入失败,表示e对应字符串已经在map中了,++次数 if (ret.second == false) ret.first->second++; }
3. 使用operator[]
string str[] = { "sort", "sort", "tree", "insert", "sort", "tree", "sort", "test", "sort" }; map<string, int> countMap; for (auto& e : str) countMap[e]++;
迭代打印结果都一样
③multimap
注意
注意:
- Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的
- 注意multimap没有[]重载
- 其他和map相似
3)题目
leetcode: 前K个高频单词
题目:
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字典顺序
排序。
分析:
- 容易想到用sort库函数进行对value,也就是次数,加入一个仿函数进行排序,但值得注意的是这里要求按照字典序(相对顺序不能改变),所以不稳定排序(
sort是快排,不稳定
)一律不能用,我们只能用冒泡,归并等稳定排序
思路1:
- 写一个稳定排序(较麻烦 略)
思路2:
- 上面讲的第三种统计次数,形成一个CountMap,
- 再创建一个SortMultiMap(用multimap是因为次数会可能相同),
不用默认的less<Key>,加上greater<Key>形成逆序
,(底层由于平衡树的旋转不会改变次数相同元素的相对顺序
),- 再压入ret前k个SortMultiMap中的元素
vector<string> topKFrequent(vector<string>& words, int k) { map<string, int> CountMap; for(auto& e: words) CountMap[e]++; multimap<int, string, greater<int>> SortMultiMap; for(auto& e : CountMap) { SortMultiMap.insert(make_pair(e.second, e.first)); } vector<string> ret; for(auto& e: SortMultiMap) { if(k==0) break; ret.push_back(e.second); k--; } return ret; }
4)模拟实现
set和map底层都是用红黑树封装的
在STL底层set的key_type是key,value_type也是key
,而map的value_type是pair< const Key T>
红黑树模板参数Value统一接收se和map的Value
我们模拟的结构如下图:
①迭代器实现
本质就是一个树节点指针:
Node* _node
template<class T, class Ref, class Ptr> class RBTreeIterator { public: typedef RBTreeNode<T> Node; typedef RBTreeIterator<T, Ref, Ptr> Self; Node* _node;
重载* ->
Ref operator*() { return _node->_data; }
注意使用的时候,编译器会省略一个
->
Ptr operator->() { return &_node->_data; }
重载++ - -
++it是找下一个比*it大的节点
- 如果it指向节点的右子树不为空间,下一个就+ +到右子树中序第一个节点,也就是右子树的最左节点
- 如果it指向节点右子树为空,下一个+ +要访问节点是,沿着it指向节点到根节点的路径中,孩子是父亲左的那个父亲节点
Self& operator++() { if (_node->_right != nullptr)//右子树不为空,找右子树最左边的那个节点 { Node* cur = _node->_right; while (cur && cur->_left) { cur = cur->_left; } _node = cur; } else//右子树为空,向上找 直到祖父节点的左节点是父节点 { Node* cur = _node; Node* parent = cur->_parent; while (parent && parent->_right == cur) { cur = parent; parent = parent->_parent; } _node = parent; } return *this; }
--it同理:略
②红黑树中的修改
节点
为了兼容map和set,pair<K,V>全部替换为T
template<class T> struct RBTreeNode { RBTreeNode<T>* _left; RBTreeNode<T>* _right; RBTreeNode<T>* _parent; Color _col; T _data; RBTreeNode(const T& data) :_left(nullptr) , _right(nullptr) , _parent(nullptr) , _col(RED) , _data(data) {} };
插入
在进行插入的时候,由于比较的是RBTree中的T,map传入的是pair<const K, V>,set传入的是K,不能进行比较,所以要在map和set的类里
分别实现一个仿函数
,同时传给RBTree的KetOfT
在map中:
//针对map和set T分别返回pair<K,V>中的K 或 直接返回K,这里是pair,返回比较的内容是pair的K struct MapKeyOfT { const K& operator()(const pair<const K,V>& kv) { return kv.first; } };
在set中:
struct SetKeyOfT { const K& operator()(const K& key) { return key; } };
先在insert函数里定义一个kot
KetOfT kot
在所有的节点大小比较时需要改为如下:
kot(cur->_data) < kot(data)
注意需要改变返回值,insert的返回值是一个
pair<iterator,bool>
加入begin()和end()函数
begin找最左边的那个节点
iterator begin() { Node* cur = _root; while (cur && cur->_left) { cur = cur->_left; } return iterator(cur); }
end就是nullptr
iterator end() { return iterator(nullptr); }
③map
结构:
template<class K, class V > class map { //针对map和set T分别返回pair<K,V>中的K 或 直接返回K,这里是pair,返回比较的内容是pair的K struct MapKeyOfT { const K& operator()(const pair<const K,V>& kv) { return kv.first; } }; public: //iterator private: RBTree<K, pair<const K, V>,MapKeyOfT> _t;//私有一个RBTree类型 };
迭代器
直接复用RBTreeIterator迭代器,需要使用typename防止实例化,这里只是typedef声明类型
//直接复用RBTreeIterator迭代器,需要使用typename防止实例化,这里只是typedef声明类型 typedef typename RBTree<K, pair<const K, V>,MapKeyOfT>::iterator iterator; iterator begin() { return _t.begin();//复用RBTreeIterator的begin() } iterator end()//复用RBTreeIterator的end() { return _t.end(); } pair<iterator, bool> insert(const pair<const K, V>& kv) { return _t.Insert(kv); }
重载[]
V& operator[](const K& key) { pair<iterator, bool> ret = _t.Insert( make_pair(key, V()) ); return ret.first->second; }
注意:
- V()构造函数,自动调用V的默认构造函数初始化,int就会初始化为0
- second是pair<iterator, bool>中的iterator里的value (ret.first还是指针 需要用->,*(ret.first)才能用.)