1.map
在C++标准模板库(STL)中,std::map是一种非常实用且强大的容器,它提供了键值对的存储机制。这使得std::map成为处理具有唯一关键的关联数据的理想选择。
1.1 map的特性
1、键值对存储:std::map通过键值对的形式存储数据,其中每个键都是唯一的,并且与一个值相关联。
2、自动排序:std::map内部使用一种平衡二叉搜索树(通常是红黑树)来存储元素,这使得元素根据键自动排序。
3、元素唯一性:在std::map中,键必须是唯一的。如果尝试插入一个已经存在的键,插入操作将失败。
4、直接访问:可以使用键直接访问std::map中的元素,这提供了高效的查找能力。
5、灵活的元素操作:std::map提供了丰富的元素操作,包括插入、删除、查找等。
1.2 map的性能
1、插入操作:插入操作的时间复杂度为O(log n),其中n是std::map中元素的数量。这是因为需要在平衡二叉树中找到合适的位置来插入新元素。
2、查找操作:查找操作的时间复杂度也是O(log n),由于std::map的有序性,可以快速定位到任何键。
3、删除操作:删除操作的时间复杂度同样为O(log n),需要找到要删除的元素并在保持树平衡的同时移除它。
4、遍历操作:遍历std::map的时间复杂度为O(n),因为需要访问容器中的每个元素。
1.3 C++标准库中map的基本用法
#include<iostream>
#include<map>
using namespace std;
int main()
{
//创建一个map,键和值都是int类型
map<int, int> myMap;
//插入元素
myMap.insert(make_pair(1, 100));
myMap.insert({ 2,200 });
myMap[3] = 300;//使用下标操作符直接插入或修改
myMap.insert({ 4,400 });
//访问元素
cout << "Element with key 2:" << myMap[2] << endl;
//迭代元素
cout << "Iterating over elements:" << endl;
for (const auto& pair : myMap)
{
cout << pair.first << "=>" << pair.second << endl;
}
//查找元素
auto search = myMap.find(2);//查找键位2的元素
if (search != myMap.end())
{
cout << "Found element with key 2:" << search->second << endl;
}
else
{
cout << "Element with key 2 was not found." << endl;
}
//删除元素
myMap.erase(2);//删除键为2的元素
cout << "Element with key 2 erased." << endl;
//再次遍历,查看删除效果
cout << "iterating over elements after deletion:" << endl;
for (const auto& pair : myMap)
{
cout << pair.first << "=>" << pair.second << endl;
}
return 0;
}
1.4 map工作原理
std::map的内部实现通常基于红黑树,红黑树相关介绍可参考文章红黑树的实现,红黑树自身支持排序,且我们实现的红黑树支持插入键值对。
2.set
std::set是C++标准模板库(STL)中提供的有序关联容器之一。它基于红黑树(Red-Black-Tree)实现,用于存储唯一的元素,并按照元素的值进行排序。
2.1set的特性
1、唯一性:std::set中不允许存储重复的元素,每个元素都是唯一的。插入重复元素的操作会被忽略。
2、有序性:std::set中的元素是按照升序进行排序的。这种排序是通过红黑树的自平衡性质实现的,保证了插入、删除等操作的高效性。
3、插入元素:使用insert成员函数可以将元素插入到集合中,如果元素已经存在,则插入操作会被忽略。
2.2 C++标准库中set的基本用法
#include<iostream>
#include<set>
using namespace std;
int main()
{
//创建std::set对象
std::set<int>mySet;
//插入元素
mySet.insert(42);
mySet.insert(21);
mySet.insert(63);
mySet.insert(21);
//删除元素
mySet.erase(63);
//查找元素
auto it = mySet.find(42);
for (const auto& element : mySet)
{
cout << element << " ";
}
cout << endl;
return 0;
}
3.map和set类模板
在同时封装set和map时,面临的第一个问题是:两者的参数不匹配。
set只需要key,map则需要key和value。用红黑树同时封装出set和map时,set传给value的是一个value,map传给value的是一个pair,set和map传给红黑树的value决定了这棵树里面存的节点值类型。上层容器不同,底层红黑树的key和value也不同。
参考STL库中的解决方案:不论是k还是k和v,都看作是value_type,获取key值时再使用别的方法解决。如下图所示。其中rb_tree的参数3就是获取key的方式,也就是上文提到的解决办法,后文会有介绍。参数4是比较方式,参数5是空间配置器。
能否省略 参数1 key_type?
对于set来说,可以省略 参数1 key_type,因为冗余了。但是对于map来说,不能省略参数1。因为map中的函数参数类型为key_type,省略后就无法确定参数类型了,比如Find、Erase中都需要key_type这个类型。
在上层容器set中,K和T都代表Key,底层红黑树节点当中存储K和T都是一样的;map中,K代表键值Key,T代表由Key和Value构成的键值对pair,底层红黑树中只能存储T。所以红黑树为了满足同时支持set和map,节点当中存储T。这就需要对红黑树进行改动。
4.红黑树节点的定义
4.1 红黑树节点的修改
原来红黑树节点的定义:
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;//节点的左孩子
RBTreeNode<K, V>* _right;//节点的右孩子
RBTreeNode<K, V>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)
pair<K, V> _kv;//节点的值域
Colour _col;//节点的颜色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
, _col(RED)
{}
};
这里将红黑树节点中的K-V键值对pair<K,V>修改成类型T,T类型的_data是pair键值对还是单个的值,视情况而定。如果是map的需求,那么就是pair;如果是set的需求,那么就是一个K。如下所示:
template<class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;//节点的左孩子
RBTreeNode<T>* _right;//节点的右孩子
RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)
T _data;//节点的值域
Colour _col;//节点的颜色
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _col(RED)
{}
};
4.2 仿函数
(1)节点比较大小时存在的问题
往红黑树中插入节点时,需要比较节点的大小,我们知道map中的元素是pair类型的关键字-值(key-value)对,关键字起到索引的作用,值则表示与索引相关联的数据。而在set中每个元素只包含一个关键字。比如文章红黑树中实现的查找元素的函数Find,该函数的形参是一个pair类型的关键字-值对,使用关键字first来比较大小。map完全可以借助该红黑树来实现,但是set中的元素只包含一个关键字,故传入到底层的红黑树的Find函数中的参数不是pair类型,所以不能借助该红黑树来实现set。凡是涉及到获取关键字的地方都有这个问题,因为对于map和set来说传入形参中的_data是不确定的,对于这种不确定的类型,一般使用仿函数来解决。
//2、红黑树查找节点
Node* Find(const pair<K, V>& kv)
{
Node* cur = _root;
while (cur)
{
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
(2)解决不同类型的关键字获取的问题
现在可以研究stl库中rb_tree的参数3了,它是一个函数对象,可以传递仿函数,用来从不同的T中获取key值。
set和map有自己各自的仿函数,这样底层的红黑树就能更具仿函数分别获取set和map的关键字。
①map的仿函数
template<class K, class V>
class MyMap
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
bool Insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
private:
RBTree<K, pair<K, V>,MapKeyOfT> _t;
};
②set的仿函数
template<class K>
class MySet
{
struct SetKeyOfT
{
const K& operator()(const K& k)
{
return k;
}
};
public:
bool Insert(const K& k)
{
return _t.Insert(k);
}
private:
RBTree<K, K,SetKeyOfT> _t;
};
当我们得到不同的关键字的获取方式后,就可以更改红黑树中相应的代码了,比如查找函数。
//2、红黑树查找节点
Node* Find(const T& data)
{
KOfT koft;
Node* cur = _root;
while (cur)
{
if (koft(data) > koft(cur->_data))
{
cur = cur->_right;
}
else if (koft(data) < koft(cur->_data))
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
5.红黑树的迭代器
map和set迭代器的实现本质是红黑树迭代器的实现,迭代器的实现模板类型、模板类型引用、模板类型指针。将红黑树的节点再一次封装,构建一个单独的迭代器类。因为节点的模板参数有K和V,所以迭代器类也需要这两个参数。不同的迭代器传递不同的参数,额外增加Ref和Ptr的目的是为了让普通迭代器和const迭代器能使用同一个迭代器类。其中Ref和Ptr具体是什么类型,取决于调用方传递的参数。
template<class T,class Ref,class Ptr>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T> Self;
Node* _node;
__TreeIterator(Node* node)
:_node(node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
5.1 移动操作
红黑树的迭代器是一个双向迭代器,只支持++和--操作,树形结构的容器在进行遍历时,默认按中序遍历的顺序进行迭代器移动,因为这样遍历二叉搜索树后,结果为有序。如下图中的二叉树遍历的结果为:5 6 7 8 10 11 12 13 15。
++移动的思路:
1.判断当前节点的右子树是否存在,如果存在,则移动至右子树的最左节点;
2.如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点;
3.如果父亲为空,则下一个节点就是空。
具体过程如下:it在节点5位置时,++是走到它的父亲节点6;但是it在节点7的位置时,++是走到它的祖先节点8的位置。it指向的节点5的右为空,节点5的右为空,表明节点5已经被访问完了。由于节点5是其父亲节点的左子树,节点5访问完了,此时该访问节点5的父亲节点,即节点6。此时节点6的右不为空,此时要访问节点6的右子树节点7。节点7的左子树为空,接着访问节点7,下一步访问节点7的右子树,节点7的右子树为空,表明节点7访问完了,此时节点6也访问完了,节点6是其父亲节点的左子树,接着访问节点6的父亲节点,即节点8。…最后访问完之后,it指向NULL,则结束。
这里解释两个问题:
1、为什么右子树不为时,要访问右子树的最左节点?
因为此时是中序遍历,路径为左-根-右,如果右边路径存在,就要从它的最左节点开始访问。
2、为什么右子树为空时,要访问当前路径中孩子节点为左孩子的父亲节点?
因为孩子节点为右孩子的父亲节点已经被访问过了。
Self& operator++()
{
//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
//
if (_node->_right)
{
Node* subLeft = _node->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
_node = subLeft;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent&&cur==parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
6.map模拟实现
template<class K, class V>
class MyMap
{
struct MapKeyOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K,V>, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
pair<iterator,bool> Insert(const pair<const K, V>& kv)
{
return _t.Insert(kv);
}
//[]返回key对应的value值
V& operator[](const K& key)
{
//这里的插入操作可能会成功,也可能会失败,如果key已经存在则失败;
//无论是插入成功还是插入失败,都会返回节点对应的迭代器,所以就能拿到该节点对应的value
//V()是缺省值,如果插入的是int即为0;如果是string,那么就构造一个空字符串对象
pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>,MapKeyOfT> _t;
};
7.set模拟实现
template<class K>
class MySet
{
struct SetKeyOfT
{
const K& operator()(const K& k)
{
return k;
}
};
public:
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
pair<iterator, bool> Insert(const K& k)
{
return _t.Insert(k);
}
private:
RBTree<K, K,SetKeyOfT> _t;
};
8.RBTree完整代码
#include<iostream>
using namespace std;
enum Colour
{
BLACK,
RED,
};
template<class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;//节点的左孩子
RBTreeNode<T>* _right;//节点的右孩子
RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)
T _data;//节点的值域,_data可能是一个key值,也可能是一个K-V键值对
Colour _col;//节点的颜色
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _col(RED)
{}
};
template<class T, class Ref, class Ptr>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, Ref, Ptr> Self;
Node* _node;
//节点指针构造迭代器
__TreeIterator(Node* node)
:_node(node)
{}
//使用普通迭代器构造const迭代器的构造函数
__TreeIterator(const __TreeIterator<T, T&, T*>& it)
:_node(it._node)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Self& operator++()
{
//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
//
if (_node->_right)
{
Node* subLeft = _node->_right;
while (subLeft->_left)
{
subLeft = subLeft->_left;
}
_node = subLeft;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self& operator--()
{
}
};
template<class K, class T, class KOfT>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
typedef __TreeIterator<T, T&, T*> iterator;
typedef __TreeIterator<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);
}
//1、红黑树插入节点
pair<iterator, bool> Insert(const T& data)
{
//1、按二叉搜索树的规则插入节点
//如果二叉树为空,则将新插入的节点作为根节点
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;//红黑树的根节点为黑色
return make_pair(iterator(_root), true);
}
KOfT koft;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (koft(data) > koft(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else if (koft(data) < koft(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(iterator(cur), false);
}
}
cur = new Node(data);
Node* newnode = cur;
if (koft(cur->_data) > koft(parent->_data))
{
parent->_right = cur;
cur->_parent = parent;
}
else if (koft(cur->_data) < koft(parent->_data))
{
parent->_left = cur;
cur->_parent = parent;
}
//cur->_col = RED;
//这里默认新插入的节点是红色节点,为什么呢?
//因为插入新节点时,就涉及破坏规则2(红黑树中没有连续的红节点);还是规则3(红黑树每条路径都有相同数量的黑节点)。
//首先,插入红色节点时,不一定会破坏规则2(如果插入节点的父亲节点是黑色节点);即使破坏了规则2,新插入的节点是
//红色节点也只会影响一条路径。
//如果新插入的节点是黑色的,会影响二叉树的所有路径,因为红黑树的每条路径都要有相同数量的黑色节点。
//情况1:cur节点为红色、parent节点为红色、grandfather为黑色、uncle节点存在且为红色;
//情况2:uncle节点不存在
//情况3:uncle节点存在且为黑
while (parent && parent->_col == RED)
{
//cur节点的父亲节点parent是红色,此时parent节点不可能是根节点
//此时看cur节点的叔叔节点uncle的颜色。
Node* grandfather = parent->_parent;
if (grandfather->_left == parent)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//如果grandfather不是根节点,继续往上处理。
cur = grandfather;
parent = cur->_parent;
}
else
{
//情况3:双旋;先parent节点左旋,转换为情况2
if (parent->_right == cur)
{
RotateL(parent);
swap(parent, cur);
}
//情况2:情况2也可能是情况3进行左单旋之后,需要再进行右单旋
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
break;
}
}
else
{
Node* uncle = grandfather->_left;
//情况1:uncle存在、且为红
//情况2 or 情况3:uncle不存在 or uncle存在、且为黑
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//如果grandfather不是根节点,继续往上处理
cur = grandfather;
parent = cur->_parent;
}
else
{
//情况3
if (cur == parent->_left)
{
RotateR(parent);
swap(cur, parent);
}
//情况2
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
}
}
//最终将根节点变为黑色
_root->_col = BLACK;
return make_pair(iterator(newnode), true);
}
//2、红黑树查找节点
iterator Find(const T& data)
{
KOfT koft;
Node* cur = _root;
while (cur)
{
if (koft(data) > koft(cur->_data))
{
cur = cur->_right;
}
else if (koft(data) < koft(cur->_data))
{
cur = cur->_left;
}
else
{
return iterator(cur);
}
}
return iterator(nullptr);
}
//中序遍历
void InOrder()
{
_InOrder(_root);
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_InOrder(root->_right);
}
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* grandfather = parent->_parent;
parent->_parent = subR;
//如果原来parent是这棵树的根节点,左旋转完成后subR节点变成这棵树的根节点
if (grandfather == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == grandfather->_left)
{
grandfather->_left = subR;
}
else if (parent == grandfather->_right)
{
grandfather->_right = subR;
}
subR->_parent = grandfather;
}
}
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* grandfather = parent->_parent;
parent->_parent = subL;
if (grandfather == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (grandfather->_left == parent)
{
grandfather->_left = subL;
}
else if (grandfather->_right == parent)
{
grandfather->_right = subL;
}
subL->_parent = grandfather;
}
}
Node* _root = nullptr;
};
完整代码可参考:set/map的模拟实现。