一、关联式容器
1、什么是关联式容器?
关联式容器也是用来存储数据的,与序列式容器(vector、list【底层是线性序列的数据结构】)不同的是,其存储的是<key,value>类型的键值对,数据检索时比序列式容器更加高效;
2、什么时键值对?
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value代表与key对应的信息;
STL一共实现了两种不同结构的管理式容器:树形结构和哈希结构;
二、树形结构关联式容器
以下四种容器的共同特点是:使用红黑树(平衡搜索树)作为底层结果,容器中的元素是一个有序序列;
1、map
(1)map是关联式容器,按照key值的大小比较排序,来存储由键值key和value组成的键值对;
(2)key在map中是用于排序的唯一标识元素,值value存在与对应键值相关的地方,且键值和value的类型可能不一样,但是可以用value_type(pair)将其连接在一起;
(3)map通过键值访问单个元素比unorder_map要慢(因为unorder_map是乱序的,支持直接根据键值查找对应value的情况;),但是map允许顺序对元素进行迭代;
(4)map支持下标访问,在[]内放入key即可访问对应的value;注意:当key值不存在时,会用默认的value和当前不存在的key构造键值对然后插入到map中,而用at()会直接抛异常;
(5)一个函数 size_type count(const key_type&x)const;
:返回key值为x的键值在map中的个数,因为map中键值的个数是唯一的,所以可以用来检测map中存不存在这个键值;
2、set
(1)set的value就是它的key,可以在容器中进行插入和删除但是不能在容器中修改。
(2)set内部同样按照严格顺序进行排序;
(3) set通过key访问元素的速度比unordered_set慢,但是允许顺序迭代;
3、multimap
(1) multimap和map基本相同,不过,multimap中的键值是可以重复的;
(2)multimap中的元素默认是按照key小于进行排序的;
(3)multimap没有重载
注:
1)set虽然是只存放value但是底层实际存储的还是key-value键值对。
2)set中插入元素不需要构造键值对,可以直接插入;
3)set元素不可以重复;
4)set中查找某个元素,时间复杂度为:log(以2为低)n;
5)set中的元素不允许修改,因为键就是值,一旦修改就要重新比较,重新排序;
4、multiset
与set不同点:
(1)multiset其中元素可以重复;
(2)multiset::count(key)的返回值可能大于1。(因为插入了多个关键值)
(3)multiset::size()的返回值是多重集合的势(cardinality),即multiset中元素的个数,而不是值的个数。比如,{1, 1, 2}的size是3,而不是2。
(4)multiset::erase(key)会将对应的key全部删掉,所以对{1, 1, 2}调用erase(1)之后,它就变成了{2}。
三、树形结构
上述容器的共同点是:其底层都是按照二叉搜索树实现的
1、二叉搜索树(也叫二叉排序树)
二叉搜索树可以为空树;
(1)特点:
a)左子树不为空的情况下,左子树上所有节点的值都小于根节点;
b)右子树不为空的情况下,右子树上所有结点的值都大于根节点;
c)其左右子树分别也为二叉搜索树;
(2)二叉搜索树的操作
a)查找
若根节点不为空:
若根节点key==查找的key 返回true;
若跟接待key>查找的key ,前往左子树进行查找;
若根节点key<查找的key ,前往右子树进行查找;
b)插入
树为空,则直接插入;
树不为空,按照二叉搜索树的性质查找插入位置,插入新节点;
(从根节店判断,是插入根节点的右子树还是左子树,并依次进行下去直到找到合适的位置);
c)删除
查找元素是否在二叉搜索树中,不存在则返回,否则:
- 要删除的结点无孩子结点,直接删除即可;
- 要删除的结点只有左孩子,删除掉该结点,并且让已删除节点的父节点成为该左孩子的父节点;
- 要删除的结点只有右孩子,删除掉该结点,并且让已删除节点的父节点成为该右孩子的父节点;
- 要删除的结点即有右孩子又有左孩子:
1、找到该节点的右子树中的最左孩子(也就是右子树中序遍历的第一个节点);
或者找到左子树的最右子树;
2、把它的值和要删除的节点的值进行交换;
3、然后删除这个节点即相当于把我们想删除的节点删除了,返回true;
举例:
a、左子树没有右孩子
直接让左孩子继承自己的右孩子和父亲
b、左子树有右孩子
一路向右,找到最后的一个右孩子,然后将这个孩子的左子树挂在它父亲的右子树上,然后让它继承要删除节点的人际关系(左右子树和父亲)当要删除的节点是根节点时,不用继承父亲关系,但要修改根节点指向。
if (cur->m_left && cur->m_right)
//当这个数的左右子树都不为空时
{
//把cur赋给pre2;把cur的左子树赋给cur
TreeNode<T> * cur2 = cur->m_left;
TreeNode<T> * pre2 = cur;
//找到cur的左子树的最右子树继承cur的位置
if (cur2->m_right)//如果右子树存在
{
for (; cur2->m_right; pre2 = cur2, cur2 = cur2->m_right);
//找到最右的子树来继承cur
pre2->m_right = cur2->m_left;
//如果cur2有左子树就把他给cur2父节点的右子树,没有的话就给一个空,不影响
cur2->m_left = cur->m_left;//继承cur的左子树
}
cur2->m_right = cur->m_right;//(当它的左子树没有右子树时)直接继承它的右子树
if (cur == pre)
{
m_root = cur2;
//如果删除的是根节点,则继承cur的左右孩子关系后直接把cur2赋给m_root
}
else
{
if (cur->m_data < pre->m_data)
//继承cur的父亲关系
{
pre->m_left = cur2;
}
else
{
pre->m_right = cur2;
}
}
delete cur;//删除cur
}
(4)二叉搜索树性能分析
a)插入和删除操作必须先进行查找,查找效率代表了其性能;
b)最优情况下,二叉树搜索树为完全二叉树,平均比较次数为:log(以2为底)N;
最查情况下,二叉搜索树为单支,平均比较次数为N/2;
2、AVL树
1、AVL树的特点:(AVL树也可以为空树)
(1)每个结点的左右子树高度之差(平衡因子)的绝对值不超过1(-1/0/1);
(2)它的左右子树都是AVL树;
注:若一棵二叉搜索树是平衡的,那么它就是AVL树,其搜索的时间复杂度为log(以二为底的n)。
2、AVL树的操作
(1)AVL树结点的定义
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; // 该节点的平衡因子
};
(2)AVL树的插入
a)按照二叉搜索树的方式插入新节点;
b)调整结点的平衡因子;
(即对树进行部分的旋转)
(3)AVL树的旋转(四种情况)
a)左单旋(较高右子树的右侧插入)
b)右单旋(较高左子树的左侧插入)
c)左右双旋(较高左子树的右插入)
d)右左双旋(较高右子树的左侧插入)
(4)AVL树旋转总结:
以root为根的子树不平衡,即root的平衡因子为2或者-2;(右子树 树高减去左子树树高)
1、root的平衡因子为2,说明root的右子树比左子树高;
设root的右子树根为Rroot,则:
当Rroot的平衡因子为-1时,执行右左双旋;
当Rroot的平衡因子为1时,执行左旋;
2、root的平衡因子为-2时,说明root的左子树比右子树高;
设root的右子树根为Lroot;
当Lroot的平衡因子为1时,执行右左双旋;
当Lroot的平衡因子为-1时,执行右旋;
(5)如何验证是否为AVL树呢?
a)验证其是否为二叉搜索树(中序遍历为一个有序的序列)
b)验证其为平衡树
每个结点的子树高度差不超过1;(同时要注意平衡因子的计算是否正确)
(6)AVL树的性能
AVL树是一颗绝对平衡的二叉搜索树;要求每个节点的左右子树高度差绝对值不超过1,以保证AVL树的查询时高效(即时间复杂度为log(以2为底)的N)。但是因为要维护其平衡,在插入和删除的过程中会需要不断地进行旋转,效率比较低;
代码:https://github.com/Wilingpz/sunny/tree/master/11.25%20AVL%E6%A0%91