前言
该笔记是个人学习过程中所记录:
- 1-9节部分主要记录各类经典的树的一些定义、性质和简单的使用,是阅读《大话数据结构》所记录的;
- 10-11节部分记录了多路查找树和红黑树的定义、性质和使用(内容较多),是通过查阅相关资料、博客等记录。
树
1 树的定义
n>0
时,根结点唯一m>0
时,子树的个数没有限制,但它们一定不相交(即一个子结点不会同时连接到两个父结点)
其中,n指结点个数,m指根结点的子树个数
结点分类
- 度(
degree
):结点拥有的子树数- 度为0的结点:称为叶结点()
Leaf
)或终端结点 - 度不为0的结点:称为分支结点或非终端结点
- 除根结点外的分支结点,也称为内部结点
- 度为0的结点:称为叶结点()
- 树的度:树内各结点的度的最大值
树的其他相关概念
- 结点的层次:从根开始,根为第一层
- 树的深度或高度:树中结点的最大层次
- 森林: m ( m ≥ 0 ) m(m≥0) m(m≥0)棵互不相交的树的集合
2 二叉树的定义
- 二叉树:由一个根节点和其左子树、右子树组成的
二叉树的特点
- 每个结点最多有两棵子树(度最多为2)
- 左子树、右子树是有顺序的
- 二叉树的五种基本形态
- 空二叉树
- 只有一个根结点
- 根节点只有左子树
- 根节点只有右子树
- 根节点既有左子树又有右子树
特殊二叉树
-
斜树:所有结点都只有左子树或者右子树
- 每一层只有一个结点,结点的个数 = 二叉树的深度
-
满二叉树:⼆叉树只有度为0的结点和度为2的结点,并且度为0的结点在同⼀层上,即:
- 所有分支结点都存在左子树和右子树(非叶结点的度为2)
- 叶结点都在同一层(最下一层)
- 深度为
k
,有 2 k − 1 2^k-1 2k−1个结点 - 同样深度的二叉树中,满二叉树结点最多、叶结点最多
-
完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最⼤
值,并且最下⾯⼀层的节点都集中在该层最左边的若⼲位置
- 叶结点只出现在最下两层
- 若结点度为1,,则该结点只有左孩子
- 同样结点数的二叉树,完全二叉树的深度最小
3 二叉树的性质
-
在二叉树的第
i
层上至多有 2 i − 1 2^{i-1} 2i−1个结点(i≥1
) -
深度为
k
的二叉树,至多有 2 k − 1 2^k-1 2k−1个结点(k≥1
) -
对任一二叉树,叶结点个数为 n 0 n_0 n0,度为2的结点个数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
- 数的总结点 n = n 0 + n 1 + n 2 n = n_0+n_1+n_2 n=n0+n1+n2, n 1 n_1 n1为度为1的结点
-
有
n
个结点的完全二叉树,深度为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1 ,[]为向下取整
4 二叉树的存储结构
-
顺序存储
- 一般二叉树,按层序编号,没有元素的位置空着(浪费空间)
- 适用于完全二叉树
-
二叉链表
- 一个数据域,两个指针域(指向左、右孩)
- 如有必要可添加一个指向父节点的指针域(三叉链表)
5 遍历二叉树
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
层序遍历:从上往下逐层遍历,每层 从左到右逐个访问结点
//遍历算法
//1.前序
void PreOrderTraverse(BiTree T) {
if(T == NULL)
return;
cout << T->data << endl; //访问C结点,或进行其其它操作
PreOrderTraverse(T->left);
PreOrderTraverse(T->right);
}
//2.中序
void PreOrderTraverse(BiTree T) {
if(T == NULL)
return;
PreOrderTraverse(T->left);
cout << T->data << endl; //访问C结点,或进行其其它操作
PreOrderTraverse(T->right);
}
//3.后序
void PreOrderTraverse(BiTree T) {
if(T == NULL)
return;
PreOrderTraverse(T->left);
PreOrderTraverse(T->right);
cout << T->data << endl; //访问C结点,或进行其其它操作
}
- 已知二叉树的
前序和中序遍历序列
或后序和中序遍历序列
可唯一确定一棵二叉树 - 但已知
前序和后序遍历序列
,不能唯一确定一棵二叉树
6 二叉树的建立
利用递归建立,类似于递归遍历
- 约定其子结点为空时,输入“#”
//如输入 AB#D##C##
void CreateBiTree(BiTree* T)
{
char ch; //假设结点存储的为字符
cin >> ch;
if(ch == '#')
*T = NULL;
else
{
T = new BiTree;
T->data = ch; //生成结点
CreateBiTree(T->left); //递归生成其左右子树
CreateBiTree(T->right);
}
}
7 线索二叉树
利用那些没有左右结点的指针域,将它们指向前驱或者后继
- 指向前驱和后继的指针称为线索
- 加上线索的二叉链表称为线索链表
- 相应的二叉树就称为
线索二叉树
线索化:对二叉树以某种次序遍历使其变为线索二叉树的过程
- 具体实现过程参考《大话数据结构》P191-194
8 二叉排序树
二叉排序树(Binary Sort Tree
),又称二叉查找树、二叉搜索树(BST)。它是一棵空树或是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上的值均小于根节点的值
- 若它的右子树不空,则右子树上的值均大于根节点的值
- 它的左、右子树也分别为 - 二叉排序树
先创建查找的函数,再利用查找函数,创建插入、构建二叉树的函数
删除则较复杂:用删除节点的前驱节点替换掉要删除的节点,再对子树进行调整
总结:
- 二叉排序树的插入和删除性能较好;查找性能则不一定,取决于树的结构(即是否平衡)
- 因此希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,均为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1([]-向下取整),那么查找的时间也为 O ( l o g n ) O(logn) O(logn),极端的斜树为 O ( n ) O(n) O(n)
故引出平衡二叉树(AVL)
9 平衡二叉树(AVL)
平衡二叉树(AVL)是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。
- 它是一棵空树
- 或它的左右子树都是平衡二叉树,且左右子树深度之差不超过1
最小不平衡子树:距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树
9.1 构建
平衡二叉树的构建思想:
-
在构建二叉排序树的过程中,插入一个节点时,检查是否因插入破坏平衡性,若是,则找出
最小不平衡子树
,再对其调整 -
当
最小不平衡子树
的根节点与它的子节点符号(BF平衡因子)不统一时,不能直接进行旋转
10 多路查找树
当数据量大时,内存不足以处理,需要减少内存取外存的次数,打破每一个节点只存储一个元素的性质,引入多路查找树
多路查找树(定义):其每一个节点的孩子树可以多于两个,且每一个节点处可以存储多个元素;且它是查找树(元素间存在某种特定的排序)
- 根据每个节点存储元素个数
- 以及它的孩子数多少
- 分为
2-3树
、2-3-4树
、B树
、B+树
10.1 2-3树
2-3树:其每一个节点都有两个孩子(称它为2节点)或三个孩子(称它为3节点)
- 2节点:包含一个元素和两个孩子(或没有孩子),且其元素排序与
二叉排序树
类似- 左子树小于当前元素,右子树大于当前元素
- 3节点:包含一小一大两个元素和三个孩子(或没有孩子)
- 若有孩子,则左子树小于当前较小的元素
- 右子树大于当前较大的元素
- 中间子树介于当前两个元素之间
- 且2-3树的所有叶子节点在同一层,如下图为典型的2-3树
注意:2-3树复杂的地方在于新节点的插入和已有节点的删除(因为每个节点都可能是2节点或3节点,且保证所有叶子在同一层)
1. 插入
分三种情况:
- 对于一个空树,插入一个2节点即可
- 插入节点到一个2节点的叶子上;由于其本身只有一个元素,因此只需要将其升级为3节点即可(根据大小决定谁在左/右)
- 往3节点中插入一个新元素,因为3节点已是最大容量,只能将其拆分,并将3个元素之一往上移动一层(复杂情况也在此)
(1)情况一:插入位置的父节点原来为2节点;插入元素5,拆分当前3节点(6、7),并将中间元素上移到父节点构成3节点
(2)情况二:插入位置的父节点原来为3节点,爷爷节点为2节点;则拆分当前节点和父节点为2节点,并将爷爷节点构建为3节点
(3)情况三:相对于第二种情况,再往上一层还是三节点,此时需要拆分的节点更多,情况更复杂
注意:若拆分的节点是根节点,则树的高度会增加
2. 删除
2-3树的删除也分为三种情况,与插入相反。
- 删除一个位于3节点上的元素,直接将3节点变为2节点即可
- 删除的元素位于一个2节点上,不能直接删除,否则会不满足2-3树的定义,需要分四种情况:
(1)情况一:此节点的父节点为2节点,且拥有一个3节点孩子;则删除当前节点、拆分3节点,并做左旋或右旋
(2)情况二:此节点的父节点是2节点,它的右孩子也为2节点;此时只能将比右孩子稍大的节点拿下来凑成3节点,再从比其稍大的3节点拆分一个元素补充其原来位置,再进行情况一的左旋
(3)情况三:此节点的父节点是一个3节点;此时拆分父节点,删除当前元素,并将父节点其中一个元素和一个原来的子节点合并为3节点
(4)情况四:当前树为满二叉树,此时删除任一个节点都会不满足2-3树的定义,因此考虑减少层数
- 删除的元素位于非叶子的分支节点;此时常考虑按中序遍历得到此元素的前驱或后继元素来补位
(1)删除节点为2节点
(2)删除节点为3节点
10.2 2-3-4树
2-3-4树是2-3树的扩展,它包括了4节点的使用:
- 4节点包含小中大三个元素和四个孩子(或没有孩子);若有孩子,满足:
- 左子树元素小于4节点的小元素
- 第二子树元素大于4节点的小元素、小于4节点的中元素
- 第三子树元素大于4节点的中元素、小于4节点的大元素
- 右子树元素大于4节点的大元素
- 2-3-4树与2-3树的插入、删除情况相似
10.3 B树(B-树)
B树:是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例;节点最大的孩子数目称为B树的阶(order
)
- 2-3树是3阶B树、2-3-4树是4阶B树
- 平衡:左边和右边均匀
- 多路:二叉树是二路,B树查找时有多条路(即父节点有多个子节点)
一个m阶B树具有如下属性:
- 若根节点不是叶结点,则其至少有两棵子树
- 每一个非根的分支节点都有
k-1
个元素和k
个孩子,每个叶子节点n
都有k-1
个元素,其中 [ m / 2 ] ≤ k ≤ m [m/2] ≤ k ≤ m [m/2]≤k≤m —— [ m / 2 ] [m/2] [m/2]向上取整 - 所有叶子节点位于同一层次
关键:
- 在B树上查找的过程是一个顺时针查找节点和在节点中查找关键字的交叉过程;
- B树的插入和删除,与2-3树和2-3-4树类似,只不过阶数可能很大;
- 由于B树每节点可以具有比二叉树多得多的元素,所有与二叉树的操作不同,它减少了必须访问节点和数据块的数量,从而提高了性能;可以说,B树的数据结构就是为内外存的数据交互准备的。
用途:
- 使用B-tree结构可以显著减少定位记录时所经历的中间过程,从而加快存取速度。这个数据结构一般用于数据库的索引,综合效率较高。
10.4 B+树
树结构都可以通过中序遍历来顺序查找树中的元素,这一切都是在内存中运行的。
- 在B树中, 往返于每个节点间,意味着在硬盘的页面间进行多次访问;且每次经过节点遍历时,都会对节点中的元素进行一次遍历,这很糟糕;为解决此问题,引出B+树
B+树是应文件系统所需而出的一种B树的变形树:
- 出现在分支节点中的元素会被当作它们在该分支节点位置的中序后继者(即被当作叶子节点的后继者)中再次列出;
- 另外,每个叶子节点都会保存一个指向后一叶子节点的指针
一棵m阶的B+树和m阶的B树差异在于:
- 有n棵子树的节点包含n个关键字
- 所有的叶节点包含关键字的全部信息,及指向这些关键字记录的指针,叶子节点本身依关键字大小 自小到大顺序链接;
- 所有分支节点可以看成索引,节点中仅含有其子树中最大(或最小)关键字。
该数据结构的好处:
- 若随机查找,从根节点出发,与B树的查找方式相同,只不过即使在分支节点找到了待查找的关键字,它只是用来索引的,需要到达包含此关键字的终端节点
- 需要从小到大顺序查找时:可从最左侧的叶节点开始,不经过分支节点,延着指向下一叶子的指针就可遍历所有关键字
- B+树的结构特别适合带范围的查找:从根节点出发,找到范围的较小边界,再延叶节点按顺序遍历
- B+树的插入、删除过程与B树类似,只不过都是在叶节点上进行的
11 红黑树
红黑树(Red Black Tree
)是一种自平衡的二叉搜索树(红黑树从根到叶子的最长路径不会超过最短路径的2倍)
- 红黑树不追求完全平衡,追求的是一种大致平衡,以达到在 插入、查找、删除 的时间复杂度最好情况和最坏情况都维持在 O ( l o g N ) O(logN) O(logN)
- 红黑树的特殊:在插入、删除节点时,会自平衡
性质:
-
节点是红色或黑色;
-
根节点是黑色;
-
每个叶子节点(
nullptr
)都是黑色的空节点(NIL节点); -
每个红色节点的两个子节点都是黑色的;(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(这个数为黑高度)。
推论:
- 若一个节点存在一个黑色子节点,那么该节点肯定有两个子节点;
- 一个黑色节点的两个子节点不一定都是红色节点;
- 不能同时存在两个直接相连的红色节点;
下图中这棵树,就是一颗典型的红黑树:
插入或删除节点时,可能会打破红黑树的规则,需要进行调整;
调整方式:
- 变色 —— 2.3.4节点的转换
- 旋转(左旋和右旋)—— 调平
11.1 红黑树由2-3-4树转化
2-3-4树中的2节点
对应着红黑树中的黑色节点,而2-3-4树中的非2节点
是以红节点+黑节点的方式存在,红节点的意义是与黑色父节点结合,表达着2-3-4树中的3,4节点(此处理解成红色链接也行,很多书中会说是由黑色节点指出的红色链接,链接指向的节点颜色为红)。
由2-3-4树到红黑树的节点转换:
- 2节点直接转化为黑色节点;
- 4节点被强制要求转化为一个黑父带着左右两个红色儿子。
11.2 左倾红黑树到2-3树的转化
- 红黑树其实就是对概念模型2-3树(或者2-3-4树)的一种实现;
- 算法导论中给出的是红黑树基于2-3-4树实现,其中4节点要求平衡(即4节点必须用黑色父亲和左右两个红色儿子表示,红色儿子不能出现在同一边);
- 算法4中给出的红黑树是基于2-3树实现,而且这种实现的红黑树十分特殊,它要求3节点只能用左倾表示,大大的减少了红黑树调整过程的复杂性。
11.3 左倾红黑树的插入
插入的节点必须为红色:即插入后和其父节点(黑节点)构成2-3树中的3节点或临时4节点;因为红色节点的意义是与父节点进行关联
分为三种情况:
-
情况一:待插入元素比黑父大,插在了黑父的右边,而黑父左边是红色儿子。这种情况会导致在红黑树中出现右倾红节点
- 该情况对应:2-3树中出现临时4节点
- 在2-3树中就是将该节点分裂,左右元素各自形成一个2节点,中间元素上升到上层和父节点结合
- 对应红黑树中的操作为:将原为红色的左右子节点染黑(左右分裂),将黑父染红(即上升结合)
-
情况二:待插入元素比红父小,且红父自身就是左倾。其实就是红父和待插入元素同时靠在了左边,形成了连续的红节点
- 由于插入的为红节点,故不会破坏黑色完美平衡,但要注意在旋转和染色过程中继续保持这种平衡
- 首先:对红父的父亲进行一次右旋,但仍没有解决连续红色的问题
- 然后:将旋转后的后父节点和黑子节点换色(消除连续红色,并维持黑色平衡)
- 最后:已转换为情况1,按1处理
-
情况三:待插入元素比红父大,且红父自身就是左倾
- 即插入后形成右倾的红色连续节点
- 将红父进行一次左旋,就转换为情况二
总结:左倾红黑树对于左倾的限制的好处:因为在原树符合红黑树定义的情况下,如果父亲是红的,那么它一定左倾,同时也不用考虑可能存在的右倾兄弟(如果有,那说明原树不满足红黑树定义)。
11.4 左倾红黑树的删除
利用二叉查找树删除的思想:删除某个节点时,选择前驱或后继节点元素来替代它,转而删除前驱/后继节点(此处选择后继节点)
删除过程:
-
从根节点开始一直往下,每次都保证当前的节点是2-3树中的非2节点,如果当前节点已经是非2节点,那么继续往下;如果当前节点是2节点,那么根据兄弟节点的状况来进行调整:
- 若兄弟为2节点,则从父节点借一个元素,然后本身、兄弟、借的元素构成临时4节点
- 若兄弟为非2节点,则兄弟上升一个元素到父节点,同时父节点下降一个元素到当前节点,使当前节点成为3节点
-
这样做的目的:走到待删除节点时,该节点一定是个非2节点,直接删除元素即可
修复过程:
-
由于调整过程出现了右倾红色节点,所有需要修复,沿着搜索方向向上回溯
-
若当前节点有右倾红节点,则进行一次左旋+换色、
-
依据此思路,一直回溯到根节点,则消灭所有右倾的红节点
11.5 总结
- 此处只记录了左倾红黑树,普通红黑树只是多了一些情况
- 插入阶段与左倾红黑树比较相似
- 删除阶段:对于删除黑色节点带来的黑色平衡破坏,按照“某条路径欠黑,所以要想办法补充一个黑色节点”的思路思考即可