目录
序列式容器关联式容器:
- 我们已经接触过STL中的部分容器,比如:vector、list、deque等,这些容器统称为序列式容器,因为其底
层为线性序列的数据结构,里面存储的是元素本身 - 关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在
数据检索时比序列式容器效率更高
键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,
value表示与key对应的信息
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)
{}
};
树形结构的关联式容器
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式
容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)
作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器
map
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
- 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值
key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,
为其取别名称为pair - map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行
直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列) - map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
总结:
- map中的的元素是键值对,map中的key是唯一的,并且不能修改
- 默认按照小于的方式对key进行比较
- map中的元素如果用迭代器去遍历,可以得到一个有序的序列
- map的底层为平衡搜索树(红黑树),查找效率比较高
- 与operator[]类似的操作at()(该函数不常用)函数,都是通过key找到与key
对应的value然后返回其引用,不同的是:当key不存在时,operator[]用默认value与key构造键值对
然后插入,返回该默认value,at()函数直接抛异常 - count()返回key为x的键值在map中的个数,注意map中key是唯一的,
因此该函数的返回值要么为0,要么为1,因此也可以用该函数来检测一个key是否在map中
#include<iostream>
#include<map>
#include<string>
#include<vector>
using namespace std;
int main()
{
map<int, string> m;
//四种插入方式:
m.insert(map<int, string>::value_type(1,"one"));
m.insert(make_pair(2,"two"));
m.insert(pair<int,string>(3,"three"));
m[4] = "four";//不能用在mulitmap中,因为包含重复key,所以没重载[]
cout << m.size() << endl;
//迭代器
map<int, string>::const_iterator it;
it = m.find(4);
cout <<it->first<<' '<<it->second << endl;
if (m.erase(4) > 0)
cout << m.size() << endl;
//失效
cout << it->first << ' ' << it->second << endl;
return 0;
}
multimap
- 存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的
- 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的
- multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历
multimap中的元素可以得到关于key有序的序列 - multimap在底层用二叉搜索树(红黑树)来实现
- multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的。
- multimap中没有重载operator[]操作(因为key可以重复,不能确定)
set
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素
不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。 - 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序
- set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直
接迭代
- 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但
在底层实际存放的是由<value, value>构成的键值对 - set中插入元素时,只需要插入value即可,不需要构造键值对。
- set中的元素不可以重复(因此可以使用set进行去重)。
- 使用set的迭代器遍历set中的元素,可以得到有序序列
- set中查找某个元素,时间复杂度为:
#include<iostream>
#include<set>
#include<string>
#include<vector>
using namespace std;
//使用set容器的目的是为了去重+排序+快速搜索
//map,set都默认由小到大
int main()
{
set<int> s;
s.insert(1);
s.insert(9);
s.insert(4);
s.insert(7);
s.insert(1);//去重
set<int>::iterator it;
s.find(4);//找不到返回s.end()
s.erase(9);
for (it = s.begin();it != s.end();it++)
{
cout << *it << endl;
}
return 0;
}
#include<iostream>
#include<set>
using namespace std;
struct big
{
bool operator()(const int &a, const int &b)
{
if (a > b)
return true;
else
return false;
}
};
int main()
{
set<int,big> s;
s.insert(2);
s.insert(4);
s.insert(1);
s.insert(5);
set<int>::iterator it;
for (it = s.begin();it != s.end();it++)
{
cout << *it << endl;
}
return 0;
}
multiset
- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的,与set的区别是,multiset中的元素可以重复,set是中value是唯
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,
因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素
总是const的),但可以从容器中插入或删除。 - multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时
会得到一个有序序列
底层结构
map/multimap/set/multiset 底层都是红黑树实现的
二叉搜索树
- 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
查找:
插入:
1:如果树为空直接插入
2:如果不为空,按照二叉树性质查找,插入新的节点、
删除:
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
- a. 要删除的结点无孩子结点
- b. 要删除的结点只有左孩子结点
- c. 要删除的结点只有右孩子结点
- d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
- 情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
- 情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
- 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题
性能:
- 插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
- 最差情况下,二叉搜索树退化为单支树,其平均比较次数:
- 如果退化为单支,二叉搜索数的性能就失去了
AVL树
但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当
于在顺序表中搜索元素,效率低下。因此,当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之
差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
- 如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
,搜索时
间复杂度
验证
- 1. 验证其为二叉搜索树如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
- 2. 验证其为平衡树
- 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
- 节点的平衡因子是否计算
节点定义:
template<class T>
struct AVLTreeNode
{
AVLTreeNode(const T& data)
: _pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
, _data(data)
, _bf(0)
{}
AVLTreeNode<T>* _pLeft; // 该节点的左孩子
AVLTreeNode<T>* _pRight; // 该节点的右孩子
AVLTreeNode<T>* _pParent; // 该节点的双亲
T _data;
int _bf; // 该节点的平衡因子
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入
过程可以分为两步
- 先按照二叉搜索树的规则将节点插入到AVL树中
- . 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了
AVL树的平衡性
pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
- 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
- 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
- 1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整成0,此时满
- 足
- AVL树的性质,插入成功
- 2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更新成正负1,
- 此
- 时以pParent为根的树的高度增加,需要继续向上更新
- 3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理
AVL树的旋转
3. 新节点插入较高左子树的右侧---左右:先左单旋再右
4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
总结:
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
- 当pSubR的平衡因子为1时,执行左单旋
- 当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
- 当pSubL的平衡因子为-1是,执行右单旋
- 当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
红黑树
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过
对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩
倍,因而是接近平衡的。
- 1. 每个结点不是红色就是黑色
- 2. 根节点是黑色的
- 3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,
均包含相同数目的黑色结点5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 1. 按照二叉搜索的树规则插入新节点
- 2. 检测新节点插入后,红黑树的性质是否造到破坏
- 因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不
- 需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,
- 此时需要对红黑树分情况来讨论:
- 约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
STL红黑树总结
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是,红黑树不追求绝对平衡,其
只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构
中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
begin()与end()
STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后,可以得到一
个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点
(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪块?能否给成nullptr呢?答案是行不通
的,因为通过前面的学习知道,对end()位置的迭代器进行operator--操作,必须要能找最后一个元
素,此处就不行,因此最好的方式是将end()放在头结点的位置: