⭐️今天我要给大家介绍两个新的容器,它们都是关联式容器——map和set,我会先介绍它们的使用方法,然后带大家用上一篇博客中的红黑树封装出map和set。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code
目录
🌏关联式容器
关联式容器也是用来存储数据的,与序列式容器(如vector、list等)不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。今天要介绍的的四种容器是树形关联式容器:map、set、multimap和multiset。它们的底层都是用红黑树来实现的,容器中的元素是一个有序的序列。
🌏键值对
键值对: 用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。
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)
{}
};
一般的两种方式创建键值对对象:
第一种: pair<T1, T2>(x, y) 使用构造函数的方式构造一个匿名对象
第二种: make_pair(x, y) 是一个函数模板,其中返回的是一个pair的匿名对象
实例演示:
void test()
{
// pair<T1, T2>(T1(), T2()) 通过构造函数构造一个匿名对象
// make_pair(T1() , T2()) 是一个模板函数,返回的是pair的匿名对象,用起来更方便
pair<int, int>(1, 1);
make_pair(1, 1);
}
🌏set
🌲set的介绍
总结几点:
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
- set在底层是用红黑树实现的。
🌲set的使用
🍯set的几个构造函数
- 构造函数: set (const Compare& comp = Compare(), const Allocator& =Allocator() ); 构造空的set容器
- 拷贝构造: set (const set& x);
🍯set的迭代器
和之前几个容器一样,有正向迭代器和反向迭代器,还有const迭代器。这里用法也和之前的类似,不过多介绍。下面会给大家演示。
🍯set的大小和容量
- empty: 判断set是否为空
- size: 返回set中元素的个数
🍯set的插入和删除
- insert: pair<iterator,bool> insert (const value_type& val); 插入元素,返回值是键值对,其中如果第二个参数为true,那么第一个参数是插入元素的迭代器的位置,为false的话,第一个参数就是已经存在元素的迭代器的位置
- erase: void erase (iterator position); 删除position位置的元素
🍯非成员函数
这里只介绍find一个。
find 查找某个元素。这里find的时间复杂度为O(logN),比算法中的find(时间复杂是O(N))更高效,所以set容器一般室友自己的find进行查找。
实例演示:
实例1 插入、删除、查找和迭代器遍历
void test_set1()
{
set<int> s;
s.insert(5);
s.insert(1);
s.insert(6);
s.insert(3);
s.insert(6);
s.insert(s.begin(), 10);
set<int>::iterator pos = s.find(15);// 底层是搜索二叉树,时间复杂度是O(logN)
// set<int>::iterator pos = find(s.begin(), s.end(), 3);// 遍历查找,时间复杂度是O(N)
if (pos != s.end())
{
// cout << *pos << endl;
s.erase(pos);// 没有会报错
}
//s.erase(1); // 没找到不会报错
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
代码运行结果如下:
实例2 下面是对算法中的find和set中的find进行效率比较的小测试
void test_set2()
{
srand((size_t)time(nullptr));
set<int> s;
for (size_t i = 0; i < 10000; ++i)
{
s.insert(rand());
}
cout << "个数:" << s.size() << endl;
int begin1 = clock();
for (auto e : s)
{
s.find(e);
}
int end1 = clock();
int begin2 = clock();
for (auto e : s)
{
find(s.begin(), s.end(), e);
}
int end2 = clock();
cout << "用时1:" << end1 - begin1 << "ms" << endl;
cout << "用时2:" << end2 - begin2 << "ms" << endl;
}
代码运行结果如下:
🌏map
🌲map的介绍
总结以下几点:
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
- 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
- map支持下标访问符,支持operator[],即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
🌲map的用法
🍯map的几个构造函数
- 构造函数: map() 构造一个空的map容器
- 拷贝构造: map(const map& m);
🍯map的迭代器
和set是类似的,不过多介绍,后面有实例演示。
🍯大小和容量
- empty 判断绒是否为空
- size 返回容器中元素个数
🍯插入和删除
- insert: pair<iterator,bool> insert (const value_type& x ); 返回的是一个键值对,和set的原理一样
- erase: void erase (iterator position); 在pos删除元素
🍯operator[](重点)
operator[]函数的定义如下:
mapped_type& operator[] (const key_type& k)
{
return (*((this->insert(make_pair(k,mapped_type()))).first)).second;
}
其中,mapped_type是KV模型中V的类型,也就是返回value值得引用。我们可以对这个value进行修改。
分析:((this->insert(make_pair(k,mapped_type()))).first这是一个迭代器,迭代器指向键值对中的第二个元素就是value。所以operato[]的底层是用到了插入,同时可以对value进行修改和访问。
总结: operator[]的三个用处:插入、修改和访问。
实例演示:
实例1 用map统计水果个数,以下用了3种方式,同时还对operator的几种作用进行了说明
void test_map2()
{
map<string, int> countMap;
string fruitArray[] = { "西瓜","桃子","香蕉","桃子","苹果","西瓜", "香蕉","苹果", "香蕉","西瓜","桃子", "西瓜", "西瓜","桃子",
"桃子", "桃子", "西瓜","桃子","香蕉","桃子","苹果","西瓜" };
// 方法一
//for (auto& e : fruitArray)
//{
// map<string, int>::iterator ret = countMap.find(e);
// if (ret != countMap.end())// 找到了,说明容器里有,第二个参数加1即可
// {
// ++ret->second;
// }
// else
// {
// // 没有就插入,第二个参数记为1
// countMap.insert(make_pair(e, 1));
// }
//}
// 方法二
//for (auto& e : fruitArray)
//{
//
// // countMap无此元素,pair的第一个参数返回新的迭代器,第二个参数返回true
// // countMap有此元素,pair的第一个参数返回旧的迭代器,第二个参数返回false
// pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(e, 1));
// // 插入失败,只需要++value即可
// if (ret.second == false)
// {
// ++ret.first->second;
// }
//}
// 方法三
for (auto& e : fruitArray)
{
// mapped_type& operator[] (const key_type& k) ;
// mapped_type& operator[] (const key_type& k) { return (*((this->insert(make_pair(k,mapped_type()))).first)).second; }
// ((this->insert(make_pair(k,mapped_type()))).first 迭代器
// (*( (this->insert(make_pair(k,mapped_type()))).first )).second 返回value的值的引用 operator[]的原型
countMap[e]++;// 有插入、查找和修改的功能 返回value的值的引用
}
countMap["梨子"];// 插入
countMap["梨子"] = 5;// 修改
cout << countMap["梨子"] << endl;// 查找 一般不会用 operator[] 来进行查找,因为没找到会进行插入
countMap["哈密瓜"] = 3;// 插入+修改
for (auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
}
代码运行结果如下:
实例2 测试map的插入、删除和迭代器的使用
void test_map1()
{
map<int, int> m;
// 键值对
// pair<T1, T2>(T1(), T2()) 通过构造函数构造一个匿名对象
// make_pair(T1() , T2()) 是一个模板函数,返回的是pair的匿名对象,用起来更方便
//m.insert(pair<int, int>(1, 1));
m.insert(make_pair(1, 1));
m.insert(pair<int, int>(2, 2));
m.insert(pair<int, int>(3, 3));
m.insert(pair<int, int>(4, 4));
map<int, int>::iterator it = m.begin();
while (it != m.end())
{
// *it 返回 值得引用
cout << (*it).first << ":" << (*it).second << endl;
// it-> 返回 值的地址 -> 解引用访问两个元素
// cout << it->first << ":" << it->second << endl;
++it;
}
// e是自定义类型,传引用防止有拷贝构造发生
for (auto& e : m)
{
cout << e.first << ":" << e.second << endl;
}
}
代码运行结果如下:
🌏multiset
🌲介绍
总结几点:
- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 底层是红黑树,和set的特点基本类似,只是multiset可以存放多个相同的值。
🌲用法
与set的接口基本相似,直接上演示。
实例演示:
void test_multiset()
{
multiset<int> ms;
// multiset 和 set 的接口基本一致,multiset可以插入重复的
ms.insert(1);
ms.insert(5);
ms.insert(3);
ms.insert(2);
ms.insert(3);
multiset<int>::iterator pos = ms.find(3);// 返回的是第一个3
cout << *pos << endl;
++pos;
cout << *pos << endl;
++pos;
cout << *pos << endl;
++pos;
for (auto e : ms)
{
cout << e << " ";
}
cout << endl;
}
代码运行结果如下:
🌏multimap
🌲介绍
总结几点:
- multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的。
- 底层也是红黑树,和map的性质基本类似
🌲用法
实例演示:
void test_multimap()
{
// multimap 和 map 的区别:可以有不同的key
// 不支持operator[] 因为有多个key时,不知道返回哪个key对应的value的引用
multimap<int, int> mm;
mm.insert(make_pair(1, 1));
mm.insert(make_pair(1, 2));
mm.insert(make_pair(1, 3));
mm.insert(make_pair(2, 1));
mm.insert(make_pair(2, 2));
for (auto& e : mm)
{
cout << e.first << ":" << e.second << endl;
}
}
代码运行结果如下:
🌏用一颗红黑树封装出map和set
🌲对红黑树进行改造
这里是我上一篇关于红黑树的博客——红黑树
这里红黑树完整代码——红黑树完整代码
大概框架:
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
private:
Node* _root = nullptr;
};
这里的红黑树是一个KV模型,我们要用这个红黑树同时封装出map和set两个容器,直接使用这棵红黑树显然是不行的,set属于是K模型的容器,我们要做怎样的改造才能够同时封装出这两个容器呢?
这里我们参考STL源码的处理方式,下面是源码的部分截图:
可以看出这里,红黑树的第一个类模板参数和之前是一样的,但是第二个参数value和之前是不一样的,这里的直接把value存放在节点里面,通过map和set构造红黑树可以看出value存的是pair<K, V>或K,对于map而言,value存的是pair<K, V>;对于set而言,value存的是K。所以这里的红黑树暂时可以这样改造:
template<class K, class T>
class RBTree
{
typedef RBTreeNode<T> Node;// 根据T的类型判断是map还是set 可能是pair<K, V>或K
public:
private:
Node* _root = nullptr;
};
同时,我们还会发现,上面的红黑树的类模板中有第三个参数是什么呢?
为了获取value中的key值,我们可以让map和set各自传一个仿函数过来,以便获得各自的key值。
两个仿函数如下:
template<class K, class V>
class map
{
struct MAPOFV
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
};
template<class K>
class set
{
struct SETOFV
{
const K& operator()(const K& key)
{
return key;
}
};
};
第四个类模板参数是一个空间配置器,这里也不实现了,我们实现主体内容即可。后面会介绍空间配置器相关内容。
迭代器的实现
其中operato++就是通过非递归中序遍历的方式走一遍红黑树,走到空就结束
template<class T, class Ptr, class Ref>
struct __rbtree_iterator
{
typedef __rbtree_iterator<T, Ptr, Ref> Self;
typedef RBTreeNode<T> Node;
Node* _node;
__rbtree_iterator(Node* node)
:_node(node)
{}
// 返回值(data)的地址
Ptr operator->()
{
return &_node->_data;
}
// 返回值(data)的引用
Ref operator*()
{
return _node->_data;
}
Self& operator++()
{
// 1.先判断右子树是否为空,不为空就去右子树找最左节点
// 2.右子树为空,去找孩子是其左孩子的祖先
Node* cur = _node;
if (cur->_right)
{
cur = cur->_right;
while (cur->_left)
{
cur = cur->_left;
}
}
else
{
Node* parent = cur->_parent;
while (parent && parent->_right == cur)
{
cur = parent;
parent = parent->_parent;
}
cur = parent;
}
_node = cur;
return *this;
}
Self& operator--()
{
// 1.先判断左子树是否为空,不为空就去左子树找最右节点
// 2.右子树为空,去找孩子是其右孩子的祖先
Node* cur = _node;
if (cur->_left)
{
cur = cur->_left;
while (cur->_right)
{
cur = cur->_right;
}
}
else
{
Node* parent = cur->_parent;
while (parent && parent->_left == cur)
{
cur = parent;
parent = parent->_parent;
}
cur = parent;
}
_node = cur;
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
};
红黑树修改后的代码: (留下了主体内容,有些部分删去了,不然有点长,可以去我的gitee上面看)
#pragma once
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
enum Color
{
RED,
BLACK
};
template<class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
T _data;
Color _color;
RBTreeNode(const T& data, Color color = RED)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _color(color)
{}
};
template<class T, class Ptr, class Ref>
struct __rbtree_iterator
{
typedef __rbtree_iterator<T, Ptr, Ref> Self;
typedef RBTreeNode<T> Node;
Node* _node;
__rbtree_iterator(Node* node)
:_node(node)
{}
// 返回值(data)的地址
Ptr operator->()
{
return &_node->_data;
}
// 返回值(data)的引用
Ref operator*()
{
return _node->_data;
}
Self& operator++()
{
// 1.先判断右子树是否为空,不为空就去右子树找最左节点
// 2.右子树为空,去找孩子是其左孩子的祖先
Node* cur = _node;
if (cur->_right)
{
cur = cur->_right;
while (cur->_left)
{
cur = cur->_left;
}
}
else
{
Node* parent = cur->_parent;
while (parent && parent->_right == cur)
{
cur = parent;
parent = parent->_parent;
}
cur = parent;
}
_node = cur;
return *this;
}
Self& operator--()
{
// 1.先判断左子树是否为空,不为空就去左子树找最右节点
// 2.右子树为空,去找孩子是其右孩子的祖先
Node* cur = _node;
if (cur->_left)
{
cur = cur->_left;
while (cur->_right)
{
cur = cur->_right;
}
}
else
{
Node* parent = cur->_parent;
while (parent && parent->_left == cur)
{
cur = parent;
parent = parent->_parent;
}
cur = parent;
}
_node = cur;
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
};
// KOFV是一个仿函数,返回的是对应类型的值 map返回pair中的key set也返回key
template<class K, class T, class KOFV>
class RBTree
{
typedef RBTreeNode<T> Node;// 根据T的类型判断是map还是set 可能是pair<K, V>或K
public:
typedef __rbtree_iterator<T, T*, T&> iterator;
typedef __rbtree_iterator<T, const T*, const T&> const_iterator;
iterator begin()
{
Node* cur = _root;
while (cur && cur->_left)
{
cur = cur->_left;
}
return iterator(cur);
}
iterator end()
{
return iterator(nullptr);
}
iterator begin() const
{
Node* cur = _root;
while (cur && cur->_left)
{
cur = cur->_left;
}
return const_iterator(cur);
}
iterator end() const
{
return const_iterator(nullptr);
}
pair<iterator, bool> Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data, BLACK);// 根节点默认给黑
return make_pair(iterator(_root), true);
}
Node* cur = _root;
Node* parent = nullptr;
KOFV kofv;
while (cur)
{
parent = cur;
if (kofv(data) < kofv(cur->_data))
cur = cur->_left;
else if (kofv(data) > kofv(cur->_data))
cur = cur->_right;
else
return make_pair(iterator(cur), false);;
}
// 节点默认给红节点,带来的影响更小
// 给黑节点的话会影响 每条路径的黑节点相同这条规则
cur = new Node(data);
Node* newnode = cur;
if (kofv(cur->_data) < kofv(parent->_data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
while (parent && parent->_color == RED)
{
Node* grandfather = parent->_parent;
// 左边
if (grandfather->_left == parent)
{
// 红黑色的条件关键看叔叔
Node* uncle = grandfather->_right;
// u存在且为红
if (uncle && uncle->_color == RED)
{
// 调整 p和u改成黑,g改成红
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
// 迭代 向上调整
cur = grandfather;
parent = cur->_parent;
}
else// u存在为黑/u不存在
{
// 折线用一个左单旋处理 1.p左单旋 2.g右单旋 3.把cur改成黑,g改成红 cur p g 三个是一条折线
if (cur == parent->_right)
{
RotateL(parent);
swap(parent, cur);
}
// 直线 cur p g 把p改成黑,g改成红
// 右单旋 有可能是第三种情况
RotateR(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
}
// uncle在左边
else
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_color == RED)
{
parent->_color = uncle->_color = BLACK;
grandfather->_color = RED;
// 迭代 向上调整
cur = grandfather;
parent = cur->_parent;
}
else
{
// 折线用一个右单旋处理 g p cur g变红p边黑
if (cur == parent->_left)
{
RotateR(parent);
swap(parent, cur);
}
// 直线 g p cur 把p改成黑,g改成红
// 左单旋 有可能是第三种情况
RotateL(grandfather);
parent->_color = BLACK;
grandfather->_color = RED;
}
}
}
_root->_color = BLACK;
return make_pair(iterator(newnode), true);
}
bool Erase(const K& key)
{
// 如果树为空,删除失败
if (_root == nullptr)
return false;
KOFV kofv;
Node* parent = nullptr;
Node* cur = _root;
Node* delNode = nullptr;
Node* delNodeParent = nullptr;
while (cur)
{
// 小于往左边走
if (key < kofv(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else if (key > kofv(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else
{
// 删除...
}
}
}
iterator Find(const K& key)
{
if (_root == nullptr)
return iterator(nullptr);
KOFV kofv;
Node* cur = _root;
while (cur)
{
// 小于往左走
if (key < kofv(cur->_data))
{
cur = cur->_left;
}
// 大于往右走
else if (key > kofv(cur->_data))
{
cur = cur->_right;
}
else
{
// 找到了
return iterator(cur);
}
}
return iterator(nullptr);
}
private:
Node* _root = nullptr;
};
总结(改造的几个点):
- 把类模板参数的value存放K或pair<K, V>
- 第三个类模板参数是仿函数,可以获取第二个类模板参数中的key
- 增加了迭代器,重载了operator[],具有STL中map中的operator[]一样的特性
🌲封装map和set
template<class K, class V>
class map
{
struct MAPOFV
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
typedef RBTree<K, pair<K, V>, MAPOFV> RBTree;
public:
// typename 告诉编译器这只是一个名字,暂时不用堆模板进行实例化
typedef typename RBTree::iterator iterator;
typedef typename RBTree::const_iterator const_iterator;
iterator begin()
{
return _rbt.begin();
}
iterator end()
{
return _rbt.end();
}
const_iterator begin() const
{
return _rbt.begin();
}
const_iterator end() const
{
return _rbt.end();
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _rbt.Insert(kv);
}
bool erase(const K& key)
{
return _rbt.Erase(key);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
RBTree _rbt;
};
---------------------------------------------------------------------------
template<class K>
class set
{
struct SETOFV
{
const K& operator()(const K& key)
{
return key;
}
};
typedef RBTree<K, K, SETOFV> RBTree;
public:
// typename 告诉编译器这只是一个名字,暂时不用堆模板进行实例化
typedef typename RBTree::iterator iterator;
typedef typename RBTree::const_iterator const_iterator;
iterator begin()
{
return _rbt.begin();
}
iterator end()
{
return _rbt.end();
}
const_iterator begin() const
{
return _rbt.begin();
}
const_iterator end() const
{
return _rbt.end();
}
pair<iterator, bool> insert(const K& key)
{
return _rbt.Insert(key);
}
bool erase(const K& key)
{
return _rbt.Erase(key);
}
private:
RBTree _rbt;
};
测试map:
void test_map()
{
map<string, int> countMap;
string strArr[] = { "香蕉","水蜜桃","西瓜","苹果","香蕉" ,"西瓜","香蕉" ,"苹果","西瓜","苹果","苹果","香蕉" ,"水蜜桃" };
for (auto& e : strArr)
{
countMap[e]++;
}
countMap["芒果"] = 10;
countMap.erase("水蜜桃");
countMap.erase("西瓜");
countMap.erase("芒果");
/*countMap.erase("香蕉");
countMap.erase("香蕉");
countMap.erase("苹果");*/
for (auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
}
代码运行结果如下:
测试set:
void test_set()
{
set<int> s;
int arr[] = { 1,3,2,3,4,5,1,5,5,8,3,3,2 };
for (auto e : arr)
{
s.insert(e);
}
for (auto& e : s)
{
cout << e << endl;
}
}
代码运行结果如下:
🌐总结
map和set的内容就介绍到这,这一块相对还是比较复杂的,但是理解了就不会觉得很复杂,其中用到的东西都很巧妙,体现了泛型编程的特性。今天的内容就先介绍到这,喜欢的话,欢迎点赞支持~