文章目录
序列式容器与关联式容器
我们之前已经接触过STL中的部分容器,比如:vector、list、deque等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
而关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key,value>结构的键值对,在数据检索时比序列式容器效率更高。
树形结构的关联式容器和哈希结构的关联式容器
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。
树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。
哈希结构的关联式容器为unordered_map 和unordered_set,这两个容器底层都是使用哈希结构映射的,而元素不是有序的。
Map与Set的简单介绍
set的简单介绍
-------》set文档介绍链接
- set是按照一定次序存储元素的容器
- 在set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- set容器通过key访问单个元素的速度O(log2N)通常比unordered_set容器(O(1))慢,但它们允许根据顺序对子集进行直接迭代(即可以实现迭代器用来迭代遍历且有序)。
- set在底层是用二叉搜索树(红黑树)实现的。
- set中的元素不可以重复(因此可以使用set进行去重)。
map的简单介绍
------》map的文档链接
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
- 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair value_type;
- 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。(重点)
- map底层也是平衡二叉搜索树(红黑树))。
注意:
- 与set/multiset不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对(这四个容器底层都是用的一个红黑树模板)。
- multiset和multimap中的key值是可以重复的。(因为用的同一棵树,所以能不能重复是由底层的红黑树插入操作里的一个bool值标识的)
- multimap和map的不同就是:map中的key是唯一的,而multimap中key是可以重复的。且multimap不可以重载operator[ ]操作。
红黑树
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍(即最长路径长度不高于最短路径长度的两倍)
,因而是接近平衡的。
红黑树的性质
1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
只要满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍的平衡性质。
红黑树是近似平衡,高度控制没有AVL树那么严格,增删查改的性能基本差不多。红黑树高度可能会高一些,但是它旋转的操作次数少些。所以性能更优。这也是map和set底层使用红黑树的原因。
红黑树节点的定义
//红黑树节点的颜色
enum Colour
{
RED, //红色
BLACK, //黑色
};
//红黑树节点的定义
template<class T> //节点里可以保存键值对(对应map)也可以只保存键值(对应set)
struct RBTreeNode
{
typedef RBTreeNode<T> Node;
Node* _left;
Node* _right;
Node* _parent;
T _data;
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)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
//根据搜索树的特点
// 1、如果右不为空,中序的下一个就是其右子树的最左节点
// 2、如果右为空或者已经访问完,表示_node所在的子树已经访问完成,则下一个节点在他的祖先中找
// 沿着路径往上找 我是它的左孩子的那个祖先节点
if (_node->_right != nullptr)
{
Node* subleft = _node->_right;
while (subleft->_left)
{
subleft = subleft->_left;
}
_node = subleft;
}
else //右边为空,按照搜索树的中序遍历则说明当前树都已经走完 ,需要回到以此子树的根节点为左子树的祖先节点
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && parent->_right == cur) //往上回退
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
self& operator--()
{
//减减就要找左边走,思路与加加相同,就是换个方向找即中序的逆序
//1.如果左不为空,就找其左子树的最右节点
if (_node->_left)
{
Node* subright = _node->_left;
while (subright->_right)
{
subright=subright->_right;
}
_node = subright;
}
else //2.要是左为空,就回退到以此子树为右子树的父亲节点
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
红黑树插入节点的控制
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索的树规则插入新节点
- 检测新节点插入后,红黑树的性质是否造到破坏,破坏后需要根据具体情况进行旋转着色处理。
注意前提:每个节点插入之前,树一定是符合红黑树性质的
约定: cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
根据具体情况选择具体的旋转着色处理:
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
-
情况一: cur为红,p为红,g为黑,u存在且为红
解决方式:将p,u改为黑,g改为红。
此处所看到的树,可能是一棵完整的树,也可能是一棵子树,又分情况谈论是否向上调整:
1.如果g是根,把g再变黑,结束;
2.如果g不是根,再看g的父亲的颜色:如果g的父亲是黑色,对上层没影响了,结束;如果g的父亲是红色,可能对其上面有影响,继续往上处理。 -
情况二: cur为红,p为红,g为黑,u不存在/u为黑
这时: u的情况有两种:
1.如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个数相同。
2.如果u节点存在,则在此情况二前提下其一定是黑色的,那么cur节点原来的颜色一定是黑色的,若要是其是红色的原因就是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色(为二次调整)。
且又根据插入位置的不同,可能在外侧插入(单旋),可能在内侧插入(双旋),而对应着不同的旋转着色。
外侧插入:
内侧插入:
pair<iterator, bool> Insert(const T& data)
{
//1.按照常规搜索树的规则插入节点
if (_root == nullptr) //树为空时
{
_root = new Node(data);
_root->_col = BLACK;//红黑树根节点为黑色节点
return make_pair(iterator(_root),true);
}
KeyofT koft;//仿函数对象,map和set底层都是用的红黑树,利用这个仿函数对象取键值,可以只封装一个底层结构供两者使用
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (koft(cur->_data) < koft(data)) //根据键值对的关键字来判断
{
parent = cur;
cur = cur->_right;
}
else if (koft(cur->_data) > koft(data))
{
parent = cur;
cur = cur->_left;
}
else
return make_pair(iterator(cur),false);//已有该键值
}
cur = new Node(data);
Node* returnnode = cur;
if (koft(cur->_data) < koft(parent->_data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//2. 检测新节点插入后,红黑树的性质是否造到破坏,
// 若满足性质直接退出,否则对红黑树分情况进行旋转着色处理,
/*因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不
需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
*/
//插入红节点,他的父亲是红色的,可以推断他的祖父存在且一定为黑色。关键看叔叔。
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)//在左子树
{
Node* uncle = grandfather->_right;
// 情况一: cur为红,p为红,g为黑,u存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续向上调整
cur = grandfather;
parent = grandfather->_parent;
}
else
{
// 情况二:cur为红,p为红,g为黑,u不存在 / u为黑
//这里其实又分为四个不同情况,即单旋的两种(情况二)和双旋的两种(情况三)
/*说明: u的情况有两种
1.如果u节点不存在, 则cur -定是新插入节点,因为如果cur不是新插入节点,
则cur和p - 定有一个节点的颜色是色,就不满足性质4 : 每条路径黑色节点个数相同。
2.如果u节点存在,则其 - -定是黑色的,那么cur节点原来的颜色-定是黑色的,
现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色(即是由下面调整上来的)。*/
//先考虑双旋的情况,因为双旋情况都是会进一步调整转换成单旋来处理的
if (cur == parent->_right)//内侧插入,则要先单旋一次
{
RotateL(parent);
swap(parent, cur);
}
//单旋的逻辑,可能是由上面双旋旋转一次后的逻辑(即cur节点不是新增节点),也可能是新增节点引起的简单单旋逻辑
//对grandfather进行一次右旋
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
}
else if (parent == grandfather->_right) //与上面逻辑相对,在右子树
{
Node* uncle = grandfather->_left;
if (uncle&& uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
}
else
{
if (cur == parent->_left)//内侧插入
{
RotateR(parent);
swap(cur, parent);
}
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
}
}
_root->_col = BLACK;//确保根节点为黑色的性质
return make_pair(iterator(returnnode),true);
}
旋转的代码逻辑与之前AVL旋转是一样的逻辑:AVL树的旋转
红黑树的迭代器
红黑树是支持迭代遍历的,其迭代器和链表迭代器封装是差不多的,加上利用搜索树的特性来决定如何移动。
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)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
self& operator++()
{
//根据搜索树的特点
// 1、如果右不为空,中序的下一个就是其右子树的最左节点
// 2、如果右为空或者已经访问完,表示_node所在的子树已经访问完成,则下一个节点在他的祖先中找
// 沿着路径往上找 我是它的左孩子的那个祖先节点
if (_node->_right != nullptr)
{
Node* subleft = _node->_right;
while (subleft->_left)
{
subleft = subleft->_left;
}
_node = subleft;
}
else //右边为空,按照搜索树的中序遍历则说明当前树都已经走完 ,需要回到以此子树的根节点为左子树的祖先节点
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && parent->_right == cur) //往上回退
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
self& operator--()
{
//减减就要找左边走,思路与加加相同,就是换个方向找即中序的逆序
//1.如果左不为空,就找其左子树的最右节点
if (_node->_left)
{
Node* subright = _node->_left;
while (subright->_right)
{
subright=subright->_right;
}
_node = subright;
}
else //2.要是左为空,就回退到以此子树为右子树的父亲节点
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
红黑树的检测
//对于红黑树的检测,由性质决定
/*1.根节点为黑色
2.对于每一个结点,从该节点到叶子节点的所有路径中(子树路径),黑色节点个数相等
3.没有连续的红色节点
另外,也要检测看看中序是不是有序*/
bool IsRBTree()
{
Node* cur = _root;
if (cur == nullptr)// 空树也是红黑树
{
return true;
}
// 检测根节点是否满足情况,即根节点必须为黑色
if (cur->_col != BLACK)
{
cout << "违反红黑树性质二:根节点必须为黑色" << endl;
return false;
}
// 获取任意一条路径中黑色节点的个数,与其他路劲对比黑色节点个数
size_t blackcount = 0;
Node* tmp = _root;
while (tmp)
{
if (tmp->_col == BLACK)
{
blackcount++;
}
tmp = tmp->_left;
}
// 检测是否满足红黑树的性质,k用来记录每条路径中黑色节点的个数,一一对比
size_t k = 0;
return _IsRBTree(_root, k, blcakcount);
}
bool _IsRBTree(Node* proot, size_t k, size_t blackcount) //以此根为起点的每条路径的黑节点个数判断以及是否有连续的红节点
{
//走到null之后,判断k和black是否相等
if (proot == nullptr)
{
if (k != blackcount)
{
cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
return false
}
return true;
}
//统计此路径黑色节点个数
if (proot->_col == BLACK)
{
k++;
}
// 检测当前节点与其双亲是否都为红色
Node* parent = proot->_parent;
if ( parent &&parent->_col == RED && proot->_col == RED)
{
cout << "违反性质三:不存在连续的红色节点" << endl;
return false;
}
return _IsRBTree(proot->_left, k, blackcount) && _IsRBTree(proot->_right, k, blackcount);
}
set的简单模拟(插入操作和迭代器遍历)
#pragma once
#include "RBTree.h"
namespace mytest
{
template<class T>
class set
{
struct KeyofT //取键值的仿函数类
{
const T& operator()(const T& data)
{
return data;//因为set只有一个键值,所以传参的data就是一个键值
}
};
public:
typedef typename RBTree<T, T, KeyofT>::iterator iterator;
iterator begin()
{
return _rbtree.begin();
}
iterator end()
{
return _rbtree.end();
}
pair<iterator, bool> Insert(const T& T)
{
return _rbtree.Insert(T);
}
private:
RBTree<T, T, KeyofT> _rbtree;
};
void test_set()
{
set<int> s;
s.Insert(3);
s.Insert(4);
s.Insert(1);
s.Insert(2);
s.Insert(5);
set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
}
map的简单模拟(插入操作和迭代器遍历)
#pragma once
#include "RBTree.h"
namespace mytest
{
template<class T,class V>
class map
{
struct KeyofT //取键值的仿函数类
{
const T& operator()(const pair<T,V>& data)
{
return data.first;
}
};
public:
typedef typename RBTree<T, pair<T, V>, KeyofT> ::iterator iterator;
iterator begin()
{
return _rbtree.begin();
}
iterator end()
{
return _rbtree.end();
}
pair<iterator, bool> Insert(const pair<T, V>& data)
{
return _rbtree.Insert(data);
}
//在实现[]运算符时,内部其实是调用了一次insert插入操作.由于是键值对类型所以我们对于pair结构体的第二个数据,
//直接调用这个数据类型的构造函数,创建一个匿名对象即V()为0。
V& operator[](const T& key)
{
pair<iterator, bool> ret = _rbtree.Insert(make_pair(key,V())); // V()便是0
return ret.first->second;
}
private:
RBTree<T, pair<T,V>, KeyofT> _rbtree;
};
void test_map()
{
/*map<int, int> m;
m.Insert(make_pair(1, 1));
m.Insert(make_pair(3, 3));
m.Insert(make_pair(10, 10));
m.Insert(make_pair(5, 5));
m.Insert(make_pair(6, 6));
map<int, int>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (auto kv : m)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;*/
string strs[] = { "西瓜", "樱桃", "西瓜", "苹果", "西瓜", "西瓜", "西瓜", "苹果" };
map<string, int> countMap;
for (const auto& str : strs)
{
// 1、如果水果不在map中,则operator[]会插入pair<str, 0>, 返回映射对象(次数)的引用进行了++。
// 2、如果水边在map中,则operator[]返回水果对应的映射对象(次数)的引用,对它++。
countMap[str]++;
}
for (auto kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
}
其实map和set底层就只是一颗红黑树,其他接口也都是直接调用红黑树的操作接口:
// Capacity
size_t size()const{ return _rbtree.Size();}
bool empty()const{ return _rbtree.Empty();
// modify
void clear(){ _rbtree.Clear();}
iterator find(const K& key){ return _rbtree.Find(key);}