红黑树和 STL —— set和map 【复习笔记】
1. 二叉搜索树
1.1 二叉搜索树的概念
相比较前文的堆,二叉搜索树并不需要必须是一棵完全二叉树
二叉搜索树(二叉排序树或二叉查找树,简称BST)是具有以下特性的二叉树:
1. 若它的左子树非空,则左子树上所有结点的值均小于它的根结点的值。
2. 若它的右子树非空,则右子树上所有结点的值均大于它的根结点的值。
3. 它的左、右子树也分别为二叉搜索树。
即左子树结点的值 < 根结点的值 < 右子树结点值(在二叉搜索树中,一般不存在值相同的两个结点)
所以,对二叉搜索树中序遍历,可以得到一个递增的有序序列
1.2 二叉搜索树的查找、插入和删除
查找:
二叉搜索树查找从根节点开始,逐层向下比较,先让给出的值与根结点比较,如果相等,就结束;如果小于根结点,就在根结点的左子树上查找;如果大于根结点,就在根结点的右子树上查找。递归进行,直到查找到叶子结点
因为查找是逐层的,所以查找的时间复杂度一般是O(log N),但是,树高最差会没有分支,变成2单链表,考虑最坏情况,所以时间复杂度为O(N)
插入:
二叉搜索树的插入操作要以查找操作为基础,查找过程中,树不存在给定的值时再插入,插入的结点一定是新的叶结点(查找失败时访问的最后一个结点的左孩子或右孩子)
因为插入和查找相绑定,所以时间复杂度为O(N)
对于插入,如果二叉搜索树为空树,要插入是所有结点的值都相同,插入顺序不同所构造的二叉搜索树也不同
删除:
二叉搜索树的删除操作也要以查找操作为基础,但对于删除操作,可能删除某个结点后,这棵树就不满足二叉搜索树的特性,所以删除有多种情况:
1. 删除的结点是叶子结点,直接删除
2. 删除的结点 m 只有一棵左子树或右子树,则让 m 的子树成为 m 父结点的子树,替代 m
3. 删除的结点 m 的左右子树都存在,有两种方法:
3.1 让 m 的直接后继(m 的右子树中最小的结点)替代 m ,然后删除这个直接后继
3.2 让 m 的直接前驱( m 的左子树中最大的结点)替代 m ,然后删除这个直接前驱
对于直接前驱和直接后继 :
将二叉搜索树中序遍历,会得到一个递增的有序序列。而 m 的直接后继,就是这个序列中 m 的后继,在树中就是 m 的右子树最左下的结点(该结点无左子树);m 的直接前驱,就是这个序列中 m 的前驱,在树中就是 m 的左子树最右下的结点(该结点无右子树)
而之后的删除直接前驱或直接后继,因为无右子树或无左子树,可以按照方法 1,2 进行删除
2. 平衡二叉树
2.1 平衡二叉树的概念
因为二叉搜索树在某些情况下会没有分支,退化为单链表,所以我们需要一些方法维持二叉搜索树的“ 平衡 “,而使用下面方法维持平衡的,就是平衡二叉树,平衡二叉树就是一种特殊的二叉搜索树(具有二叉搜索树的性质)
规定在插入和删除结点时,保证任意结点的左、右子树的高度差的绝对值不超过 1 ,这样的二叉搜索树被称为平衡二叉树(AVL树)
平衡因子:结点的左子树和右子树的高度差,平衡二叉树中每一个结点的平衡因子只可能是 -1、0、1
2.2 查找
和二叉搜索树的查找一样:从根节点开始,逐层向下比较,先让给出的值与根结点比较,如果相等,就结束;如果小于根结点,就在根结点的左子树上查找;如果大于根结点,就在根结点的右子树上查找。递归进行,直到查找到叶子结点
时间复杂度:因为平衡二叉树限制子树的树高差,所以不会出现二叉搜索树那样极端的情况,因此时间复杂度为 O(log N)
2.3 插入
2.3.1 插入的背景
先按照二叉搜索树的方法进行插入,但插入结点后,可能改变了左右子树的树高差,让平衡因子的绝对值大于 1 ,所以需要再调整这棵树的结构。
在调整结构前,我们要知道一个概念:最小不平衡子树
最小不平衡子树:在平衡二叉树中,以离插入节点最近且平衡因子绝对值大于 1 的节点为根的子树。( 当对平衡二叉树进行插入操作时,可能会导致树的平衡性被破坏,从插入节点开始向上查找,第一个出现平衡因子绝对值大于 1 的节点所对应的子树就是最小不平衡子树 )
对于插入操作,仅需调整最小不平衡子树,就可以让所以结点平衡:
这棵树原来就是平衡二叉树,如果插入一个结点后失衡,那失衡的平衡因子的绝对值只能是2
当最小平衡子树调整平衡后,这棵子树的高度减1,向上传递,就让整个路径的平衡因子向下靠近,树就平衡了
对于插入操作的时间复杂度:后续的调整仅需修改指针,所以最大的时间开销和查找一样,所以时间复杂度为O(log N)
2.3.2 插入的方法
最小不平衡子树的出现分为 4 种情况,所以调整方法也有 4 种情况(下面情况假设最小不平衡子树的根结点为 T,左孩子为 L,右孩子为 R,左孩子的右孩子为 LR,右孩子的左孩子为 RL)
2.3.2.1 LL型 - 右单旋
LL型:新结点插入到 T 结点的左孩子(L)的左子树(L)中,导致失衡
右单旋:将结点 T 以左孩子 L 为轴向右下旋转作为结点 L 的右子树的根结点
结点 L 的原右子树则作为结点 T 的左子树
结点 L 就代替 T 成为根节点
2.3.2.2 RR型 - 左单旋
RR型:新结点插入到 T 结点的右孩子 (R) 的右子树(R)中,导致失衡
左单旋:将结点 T 以右孩子 R 为轴向左下旋转作为结点 R 的左子树的根结点
结点 R 的原左子树则作为结点 T 的右子树
结点 R 就代替 T 成为根结点
2.3.2.3 LR型 - 左右双旋
左右双旋分为两步,先左旋,再右旋
LR型:新结点插入到 T 结点的左孩子(L)的右子树(R)中,导致失衡
左旋:将结点 L 以 LR 为轴向左下旋转作为 LR 的左子树的根节点
结点 LR 的原左子树则作为结点 L 的右子树
结点 LR 就替代 L 成为根结点
右旋:将结点 T 以 LR 为轴向右下旋转作为结点 LR 的右子树的根结点
结点 LR 的原右子树则作为结点 T 的左子树
结点 LR 就替代 T 成为根结点
2.3.2.4 RL型 - 右左双旋
右左双旋分为量步,先右旋,再左旋
RL型:新结点插入到 T 结点的右孩子(R)的左子树(L)中,导致失衡
右旋:将结点 R 以 RL 为轴向右下旋转作为 RL 的右子树的根结点
结点 RL 的原右子树则作为结点 R 的右子树
结点 RL 就替代 R 成为根结点
左旋:将结点 T 以 RL 为轴向左下旋转作为 RL 的左子树的根结点
结点 RL 的原左子树则作为结点 T 的右子树
结点 RL 就替代 T 成为根结点
2.4 删除
删除操作和二叉搜索树的删除操作相似,删除后的调整思想和插入操作后的调整思想相似,时间复杂度为O(log N)
步骤:
1. 用二叉搜索树的删除方法对结点 e 进行删除
2. 从结点 e 开始,向上找到最小不平衡子树(假设第一个不平衡结点为 T ,结点 T 的高度最高的孩子结点为 X,结点 X 的高度最高的孩子结点为 Y)
3. 对以 Y 为根的子树进行平衡调整,根据 Y、X、T的位置可同插入操作一样分为 4 种情况:
a. LL型 - 右单旋:X 是 T 的左孩子(L),Y 是 X 的左孩子(L)
b. RR型 - 左单旋:X 是 T 的右孩子(R),Y 是 X 的右孩子(R)
c. LR型 - 左右双旋:X 是 T 的左孩子(L),Y 是 X 的右孩子(R)
d. RL型 - 右左双旋:X 是 T 的右孩子(R),Y 是 X 的左孩子(L)
4. 调整之后,继续向上找下一个最小不平衡子树,重复 3 操作,直到找不到为止
( 因为删除操作不像插入操作一样,删除操作可能导致大量结点失衡,一次调整不能保证整棵树的结点是平衡的。所以需要不断调整,重复 3 过程)
3. 红黑树
3.1 红黑树的概念和性质
红黑树(简称RBT)也是一个二叉搜索树,它具有二叉搜索树的特性。它在二叉搜索树的基础上,让它的每个节点都有一个颜色属性,可以是红色或黑色。通过下面的各个结点的着色规则限制,确保没有一条路径会比其他路径长出 2 倍,所以红黑树是一棵接近平衡的二叉搜索树
着色规则:
1. 每个结点是红色或黑色
2. 根结点和叶子节点(这里的叶子结点不是常规树上的叶子结点,而是空结点(NULL)是黑色
3.每个红色结点的两个子结点都是黑色(任意一条路径上不会有连续的红色结点)
4. 任意一个结点,从该结点到其所有叶子结点的路径上,均包含相同数目的黑色结点
由于红黑树的着色规则,我们可以得到它的两个重要性质:
1. 从根结点到叶子结点的最长路径不会大于最短路径的 2 倍
2. 有 n 个结点的红黑树,高度 h <= 2log(n+1),即查找的时间复杂度为O(log N)
3.2 查找
红黑树的查找和二叉搜索树的查找一样:从根节点开始,逐层向下比较,先让给出的值与根结点比较,如果相等,就结束;如果小于根结点,就在根结点的左子树上查找;如果大于根结点,就在根结点的右子树上查找。递归进行,直到查找到叶子结点
时间复杂度为O(log N)
3.3 插入
3.3.1 插入步骤
1. 先按照二叉搜索树的插入方式进行插入,新插入的结点默认为红色
( 如果默认黑色,那么这条路径的黑色结点增多,要调整所有从根节点到叶子结点的路径,让树重新符合红黑树的特性。调整规模过于庞大
如果默认红色,那么只会破坏根结点不能为红色和不能出现连续的红色结点的规则,调整起来相对简单 )
2. 如果新插入结点破坏红黑树的规则,那么要分情况调整
3.3.2 调整方法
假设新插入的结点为 c(cur),父结点为 p(parent),父结点的父结点为 g(grandfather),父结点的兄弟结点为 u (uncle)
3.3.2.1 插入的是根结点
直接将结点颜色变为黑色
3.3.2.2 插入节点的叔叔结点是红色
父亲、叔叔、爷爷结点同时变色,将爷爷结点看成新插入的结点,继续向上判断
( p、u 变为黑色,g 变成红色,整个路径的黑色结点数目保持平衡,数量不变;但 g 变成红色,可能 g 是根结点,或 g 的父亲结点也是红色,违反规则,所以将 g 当成插入结点重新判断)
3.3.2.3 插入节点的叔叔结点是黑色
这种情况要分类讨论,根据插入结点、父结点和爷爷结点的位置旋转+变色
3.3.2.3.1 LL型 - 右单旋 + 父爷变色
LL型:新结点为 g 结点的左孩子 p (L)的左孩子(L)
1. 右旋父亲结点(将爷爷结点以父亲结点为轴向右下旋转)
2. 将父亲结点和爷爷结点进行变色
3.3.2.3.2 RR型 - 左单旋 + 父爷变色
RR型:新结点为 g 结点的右孩子 p(R)的右孩子(R)
1. 左旋父亲结点(将爷爷结点以父亲结点为轴向左下旋转)
2. 将父亲结点和爷爷结点进行变色
3.3.2.3.3 LR型 - 左右双旋 + 儿爷变色
LR型:新结点为 g 结点的左孩子 p(L)的右孩子(R)
1. 新结点先左旋,再右旋(先将 p 结点以新结点为轴向左下旋转,新结点代替 p 结点的位置;再将 g 结点以新结点为轴向右下旋转)
2. 将新结点和爷爷结点进行变色
3.3.2.3.4 RL型 - 右左双旋 + 儿爷变色
RL型:新结点为 g 结点的右孩子 p(R)的左孩子(L)
1. 新结点先右旋,再左旋(先将 p 结点以新结点为轴向右下旋转,新结点代替 p 结点的位置,再将 g 结点以新结点为轴向左下旋转)
2. 将新结点和爷爷结点进行变色
3.4 删除
删除操作较复杂,为控制篇幅,之后篇章再讨论
4. STL —— set和map
4.1 set/multiset
set 和 multiset 的区别:set 不能存相同元素,multiset 可以存相同元素,其他的使用方法完全一致
所以set可以用来给数据去重
#include<iostream>
#include<set>
using namespace std;
void test()
{
set<int> mp;
//insert:插入元素,时间复杂度O(log N)
for (int i = 5; i >= 1; i--)
{
mp.insert(i);
}
//begin/end,返回迭代器,可以用范围 for 遍历整个红黑树
//中序遍历 set,结果应该递增有序
for (auto x : mp)
{
cout << x << " ";
}
cout << endl;
//size:返回 set 中元素个数,时间复杂度O(1)
cout << mp.size() << endl;
//empty:判断 set 是否为空,时间复杂度O(1)
if (!mp.empty()) cout << "非空" << endl;
//erase:删除元素,时间复杂度O(log N)
mp.erase(5);
mp.erase(4);
for (auto x : mp)
{
cout << x << " ";
}
cout << endl;
//find:查找一个元素,返回迭代器,时间复杂度O(log N)
//cout:查询元素出现的次数,一般用来判断元素是否在红黑树中,时间复杂度O(log N)
//想查找元素是否在set中,我们一般使用count,而非find,因为find返回迭代器不方便使用
if (mp.count(1)) cout << "1" << endl;
if (mp.count(5)) cout << "5" << endl;
//lower_bound:大于等于变量的最小元素,返回迭代器
//upper_bound:大于变量的最小元素,返回迭代器
//时间复杂度都为O(log N)
auto x = mp.lower_bound(1);
auto y = mp.upper_bound(1);
cout << *x << " " << *y << endl;
return;
}
int main()
{
test();
return 0;
}
4.2 map/multimap
map 和 multimap 的区别:map 不能存相同的元素,multimap 可以存相同的元素,其他的使用方法完全一致
map 和 set 的区别:set 里存储一个单独的关键字;而 map 里面存一个 pair<key,value>(k-v模型),除了一个关键字,还有一个和关键字绑定的值,但比较是以 key 进行的
比如:<int,int>,可以统计数字出现的次数
<string,int>可以统计字符串出现的次数
#include<iostream>
using namespace std;
#include<map>
void print(map<string, int>& mp)
{
//begin/end:返回迭代器,可以用范围for遍历整个 map
for (auto& p : mp)
{
cout << p.first << " " << p.second << endl;
}
}
void test()
{
map<string, int> mp;
//insert:插入元素,要插入一个pair,时间复杂度O(log N)
mp.insert({ "lili",1 });
mp.insert({ "kiki",2 });
print(mp);
//operator[]可以让 map 像数组一样使用
cout << mp["lili"] << endl;
mp["lili"] = 3;
cout << mp["lili"] << endl;
//但operator[]可能会向 map 插入意料外的元素
//插入时,第一个关键字为[]里内容,第二个关键字为默认值
if (mp["wiwi"] == 3) cout << mp["wiwi"] << endl;
//可以发现本不想插入wiwi,只是比较一下,但插入进去了
print(mp);
//erase:删除,时间复杂度O(log N)
mp.erase("wiwi");
//find:查询一个元素,返回迭代器
//count:查询元素出现的次数,可以判断该元素是否在map中
//时间复杂度都是O(log N),一般使用count查询元素
if (mp.count("wiwi") && mp["wiwi"] == 0) cout << "yes" << endl;
print(mp);
//size:求map的大小,时间复杂度O(1)
//empty:判断map是否为空,时间复杂度O(1)
cout << mp.size() << endl;
if (!mp.empty()) cout << "非空" << endl;
//lower_bound:大于等于传入值的最小元素,返回迭代器
//upper_bound:大于传入值的最小元素,返回迭代器
//时间复杂度都是O(log N)
auto x = mp.lower_bound("kiki");
auto y = mp.upper_bound("kiki");
cout << (*x).first << " " << (*y).first << endl;
}
int main()
{
test();
return 0;
}