目录
1. 关联式容器
2. 键值对
3. 树形结构的关联式容器
3.1 set
3.1.2 set的使用
3.2 map
3.2.1 map的介绍
3.2.2 map的使用
3.3 multiset
3.3.1 multiset的介绍
3.3.2 multiset的使用
3.4 multimap
3.4.1 multimap的介绍
3.4.2 multimap的使用
4.红黑树模拟实现STL中的map与set
4.1红黑树的节点定义
4.2 红黑树的迭代器
4.3改造红黑树
4.4 map的模拟实现
4.5 set的模拟实现
1. 关联式容器
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、 forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面 存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是结构的 键值对,在数据检索时比序列式容器效率更高。
2. 键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代 表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然 有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应 该单词,在词典中就可以找到与其对应的中文含义。
3. 树形结构的关联式容器
根据应用场景的不桶,STL总共实现了两种不同结构的关联式容器:树型结构与哈希结构。树型结 构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使 用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一 个容器。
3.1 set
3.1.1 set的介绍
1. set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。 set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行 排序。
4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但set允许根据顺序(begin到end)对 子集进行直接迭代。
5. set在底层是用二叉搜索树(红黑树)实现的。
注意:
1. 与map/multimap不同,map/multimap中存储的是真正的键值对,set中只放 value,但在底层实际存放的是由构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重)。
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set中的元素默认按照小于来比较
6. set中查找某个元素,时间复杂度为:$log_2 n$
7. set中的元素不允许修改(为什么? 因为底层的红黑树是一个升序结构,如果擅自修改节点的值,可能会破坏树结构)
8. set中的底层使用二叉搜索树(红黑树)来实现。
3.1.2 set的使用
1. set的模板参数列表
T: set中存放元素的类型,实际在底层存储的键值对。
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
2. set的构造
3. set的迭代器
4. set的容量
5. set修改操作
6. set的使用举例
#include <set>
void TestSet()
{// 用数组array中的元素构造setint array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,
6, 8, 0 };set<int> s(array, array+sizeof(array)/sizeof(array[0]));cout << s.size() << endl;// 正向打印set中的元素,从打印结果中可以看出:set可去重for (auto& e : s)cout << e << " ";cout << endl;// 使用迭代器逆向打印set中的元素for (auto it = s.rbegin(); it != s.rend(); ++it)cout << *it << " ";cout << endl;// set中值为3的元素出现了几次cout << s.count(3) << endl;
}
3.2 map
3.2.1 map的介绍
1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元 素。
2. 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联 的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类 型value_type绑定在一起,为其取别名称为pair
3. 在内部,map中的元素总是按照键值key进行比较排序的。
4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序 对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
3.2.2 map的使用
1. map的模板参数说明
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比 较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户 自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的 空间配置器 注意:
在使用map时,需要包含头文件。
2. map的构造
3. map的迭代器
4. map的容量与元素访问
问题:当key不在map中时,通过operator获取对应value时会发生什么问题?将键值对插入;
注意:在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过 key找到与key对应的value然后返回其引用,不同的是:当key不存在时,operator[]用默认 value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。
5. map中元素的修改
【总结】
1. map中的的元素是键值对
2. map中的key是唯一的,并且不能修改
3. 默认按照小于的方式对key进行比较
4. map中的元素如果用迭代器去遍历,可以得到一个有序的序列
5. map的底层为平衡搜索树(红黑树),查找效率比较高$O(log_2 N)$
6. 支持[]操作符,operator[]中实际进行插入查找。
3.3 multiset
3.3.1 multiset的介绍
1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是组成 的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器 中进行修改(因为元素总是const的),但可以从容器中插入或删除。
3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则 进行排序。
4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭 代器遍历时会得到一个有序序列。
5. multiset底层结构为二叉搜索树(红黑树)。
注意:
1. multiset中在底层中存储的是的键值对
2. multiset的插入接口中只需要插入即可
3. 与set的区别是,multiset中的元素可以重复,set中value是唯一的
4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
5. multiset中的元素不能修改
6. 在multiset中找某个元素,时间复杂度为$O(log_2 N)$
7. multiset的作用:可以对元素进行排序
3.3.2 multiset的使用
此处只简单演示set与multiset的不同,其他接口接口与set相同,同学们可参考set。
#include <set>
void TestSet()
{int array[] = { 2, 2, 3, 9, 6, 0, 5, 8, 4, 7 };// 注意:multiset在底层实际存储的是<int, int>的键值对multiset<int> s(array, array + sizeof(array)/sizeof(array[0]));for (auto& e : s)cout << e << " ";cout << endl;return 0;
}
3.4 multimap
3.4.1 multimap的介绍
1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中多个键值对之间的key是可以重复的。
2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内 容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起, value_type是组合key和value的键值对: typedef pair value_type;
3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对 key进行排序的。
4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代 器直接遍历multimap中的元素可以得到关于key有序的序列。
5. multimap在底层用二叉搜索树(红黑树)来实现。
注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以 重复的。
3.4.2 multimap的使用
multimap中的接口可以参考map,功能都是类似的。
注意:
1. multimap中的key是可以重复的。
2. multimap中的元素默认将key按照小于来比较
3. multimap中没有重载operator[]操作(为什么?)。
multimap允许存储多个相同键的元素,这使得operator[]的行为变得不明确。当使用operator[]访问一个存在多个相同键的multimap时,应该返回哪个值?是第一个匹配的值、最后一个匹配的值,还是所有匹配的值?这种歧义使得operator[]在multimap中的实现变得复杂且不符合直观。
4. 使用时与map包含的头文件相同:
4.红黑树模拟实现STL中的map与set
虽然之前已经写过红黑树的模拟实现,但在这里会有所不同。
4.1红黑树的节点定义
enum Color
{Red,Black
};
template<class T>
struct RBTreeNode
{T _data;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Color _col;RBTreeNode(const T& key):_data(key), _left(nullptr), _right(nullptr), _parent(nullptr), _col(Red){}
};
注意这里只用了T这一个模版参数,因为一个T既可以存储单一类型也可以存储pair类型,因此K模型和KV模型都可以支持!
4.2 红黑树的迭代器
迭代器的好处是可以方便遍历,是数据结构的底层实现与用户透明。如果想要给红黑树增加迭代 器,需要考虑以前问题:
begin()与end() STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后, 可以得到一个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位 置,end()放在最大节点(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪块? 这里我们将end处给上nullptr,当要执行end--的时候,我们要确保能回到最大节点处。
下面给出红黑树的迭代器封装,思路体现在注释里:
//红黑树的迭代器封装,其实就是对某个节点指针的封装;
//begin位于最小的节点,end位于最大节点的下一个位置
//红黑树的end迭代器,我们设该节点为空指针,且由于我们要满足end--能回到最大元素的位置,就要用到根节点_root
template<class T, class Ref, class Ptr>
struct Iterator
{using Node = RBTreeNode<T>;using Self = Iterator<T, Ref, Ptr>;Node* _node;Node* _root;Iterator(Node* node, Node* root):_node(node),_root(root){}//引用迭代器,直接返回数据data的引用Ref operator*(){return _node->_data;}//it->,返回的是数据data的指针,编译器会优化一次->操作//对于set,->可以直接返回数据,或返回类对象的成员变量//对于map,存储的是pair类型,可以直接用it->first,it->secondPtr operator->(){return &_node->_data;}bool operator==(const Self& it){return _node->_data == it._node->_data;}bool operator!=(const Self& it){return _node != it._node;}//按照顺序返回下一个数据的节点Self& operator++(){//如果当前节点的右侧还有节点,就找出右侧的最小节点if (_node->_right){Node* rightmin = _node->_right;while (rightmin->_left){rightmin = rightmin->_left;}_node = rightmin;}//否则,找出第一个左指针指向当前节点的节点,如果遇到父节点为空了,说明已经到根节点了,// 也就是最初的cur为最大节点,_node最后就应该指向空指针(end),符合迭代器思想else{Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_right == cur){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}//按照顺序返回上一个数据的节点Self& operator--(){//这种情况是从end--,应该回到最大节点处if (_node == nullptr){Node* rightmax = _root;while (rightmax->_right){rightmax = rightmax->_right;}_node = rightmax;}//如果当前节点的左侧还有节点,就找出左侧的最大节点else if (_node->_left){Node* leftmax = _node->_left;while (leftmax->_right){leftmax = leftmax->_right;}_node = leftmax;}//否则,找出第一个右指针指向当前节点的节点,如果遇到父节点为空了,说明已经到根节点了,// 也就是最初的cur为最小节点,_node最后就应该指向空指针(end),符合迭代器思想else{Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_left == cur){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}
};
4.3改造红黑树
因为关联式容器中存储的是的键值对:
T是存储的数据类型,可能是单一类型,也可能是pair类型,因此我们无法确定T的键值类型以及如何取键值,
KeyofT是取键值的方式,Compare是比较逻辑
//因为T是存储的数据类型,可能是单一类型,也可能是pair类型,因此我们无法确定T的键值类型以及如何取键值,
// KeyofT是取键值的方式,Compare是比较逻辑
template<class K, class T, class KeyofT,class Compare = myless<K>>
class RBTree
{
public:using Node = RBTreeNode<T>;using iterator = Iterator<T, T&, T*>;using const_iterator = Iterator<T, const T&, const T*>;iterator begin(){Node* leftmin = _root;while (leftmin && leftmin->_left){leftmin = leftmin->_left;}return iterator(leftmin, _root);}iterator end(){return iterator(nullptr, _root);}const_iterator begin() const{Node* leftmin = _root;while (leftmin && leftmin->_left){leftmin = leftmin->_left;}return const_iterator(leftmin, _root);}const_iterator end() const{return const_iterator(nullptr, _root);}RBTree() = default;RBTree(const RBTree<K, T, KeyofT, Compare>& t){_root = Copy(t._root);}Node* Copy(Node* root){if (root == nullptr){return nullptr;}Node* newroot = new Node(root->_data);newroot->_parent = root->_parent;newroot->_color = root->_color;newroot->_left = Copy(root->_left);newroot->_right = Copy(root->_right);return newroot;}RBTree<K, T, KeyofT,Compare >& operator=(RBTree t){swap(_root, t._root);return *this;}~RBTree(){Destroy(_root);_root = nullptr;}void Destroy(Node* root){if (root == nullptr){return;}Destroy(root->_left);Destroy(root->_right);delete root;}void inorder(){Inorder(_root);}void Inorder(Node* root){if (root == nullptr){return;}Inorder(root->_left);cout << root->_data.first << ' ' << root->_data.second << endl;Inorder(root->_right);}iterator find(const K& key){Compare comp;KeyofT key;Node* cur = _root;while (cur){if (comp(key(cur->_data), key)){cur = cur->_right;}else if (!comp(key(cur->_data), key)){cur = cur->_left;}else{return iterator(cur, _root);}}return end();}pair<iterator,bool> insert( T& data){Compare comp;KeyofT key;if (_root == nullptr){_root = new Node(data);_root->_color = Black;return make_pair(iterator(_root,_root),true);}Node* cur = _root;Node* parent = nullptr;while (cur){if (comp(key(cur->_data), key(data))){parent = cur;cur = cur->_right;}else if (!comp(key(cur->_data), key(data))){parent = cur;cur = cur->_left;}else{return make_pair(iterator(cur, _root), false);}}cur = new Node(data);if (comp(key(parent->_data), key(data))){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//进行颜色调整while (parent && parent->_color == Red){Node* grand = parent->_parent;Node* uncle = nullptr;if (parent == grand->_left){uncle = grand->_right;if (uncle && uncle->_color == Red){parent->_color = uncle->_color = Black;grand->_color = Red;cur = grand;parent = cur->_parent;}else{if (cur == parent->_left){RotateR(grand);grand->_color = Red;parent->_color = Black;break;}else{RotateL(parent);RotateR(grand);cur->_color = Black;grand->_color = Red;break;}}}else{uncle = grand->_left;if (uncle && uncle->_color == Red){parent->_color = uncle->_color = Black;grand->_color = Red;cur = grand;parent = cur->_parent;}else{if (cur == parent->_right){RotateL(grand);grand->_color = Red;parent->_color = Black;break;}else{RotateR(parent);RotateL(grand);cur->_color = Black;grand->_color = Red;break;}}}}_root->_color = Black;return make_pair(iterator(cur, _root), true);}
private:void RotateR(Node* parent){Node* cur = parent->_left;Node* cr = cur->_right;parent->_left = cr;if (cr){cr->_parent = parent;}Node* pparent = parent->_parent;cur->_right = parent;parent->_parent = cur;if (pparent == nullptr){_root = cur;cur->_parent = nullptr;}else{if (pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}}void RotateL(Node* parent){Node* cur = parent->_right;Node* cr = cur->_left;parent->_right = cr;if (cr){cr->_parent = parent;}Node* pparent = parent->_parent;cur->_left = parent;parent->_parent = cur;if (pparent == nullptr){_root = cur;cur->_parent = nullptr;}else{if (pparent->_left == parent){pparent->_left = cur;}else{pparent->_right = cur;}cur->_parent = pparent;}}
private:Node* _root = nullptr;
};
4.4 map的模拟实现
map的底层结构就是红黑树,因此在map中直接封装一棵红黑树,然后将其接口包装下即可
#include "RBTree.hpp"template<class K,class V>
class Map
{
public:class KeyofT{public:const K& operator()(const pair<K,V>& x){return x.first;}};using iterator = typename RBTree<K, pair<const K,V>, KeyofT>::iterator;using const_iterator = typename RBTree<K, pair<const K, V>, KeyofT>::const_iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}const_iterator begin()const{return _t.begin();}const_iterator end()const{return _t.end();}iterator find(const K& x){return _t.find(x);}pair<iterator, bool> insert( pair<const K, V> x){return _t.insert(x);}V& operator[](const K& key){pair<iterator, bool> ret = insert({ key,K() });auto it = ret.first;return it->second;}
private:RBTree<K, pair<const K, V>, KeyofT> _t;
};
4.5 set的模拟实现
set的底层为红黑树,因此只需在set内部封装一棵红黑树,即可将该容器实现出来(具体实现可参 考map)。
#include "RBTree.hpp"template<class K>
class Set
{
public:class KeyofT{public:const K& operator()(const K& x){return x;}};using iterator = typename RBTree<K,const K, KeyofT>::iterator;using const_iterator = typename RBTree<K, const K, KeyofT>::const_iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}const_iterator begin()const{return _t.begin();}const_iterator end()const{return _t.end();}iterator find(const K& x){return _t.find(x);}pair<iterator, bool> insert(const K& x){return _t.insert(x);}
private:RBTree<K, const K, KeyofT> _t;
};