树和二叉树
定义和基本术语
树(Tree)是n(n>=0
)个结点的有限集。树形结构是一类(1:n)非线性数据结构。
如上图所示,每个结点都可以从根节点经过一个唯一的弧序列到达,此弧序列被称为路径,路径中的弧的个数称为路径长度。(结点也可写作节点)
- 根(Root)结点是没有父结点的结点;
- 结点的度(Degree):一个结点含有的子树的个数称为该结点的度;树的度:树中所有结点的度的最大值;
- 叶结点(Leaf):度为0的结点称为叶结点,也可以叫做终端结点;
- 分支结点:度不为0的结点称为分支结点,也可以叫做非终端结点;
- 孩子(Child):一个结点子树的根结点称为该结点的孩子结点;这个结点称为孩子结点的 双亲(父结点Parent);
- 兄弟(Sibling):同一双亲结点的孩子结点间互称兄弟结点;堂兄弟:双亲在同一层次的结点互为堂兄弟;
- 祖先:从根结点到该结点所经分支上的所有结点都是此结点的祖先; 子孙:以某结点为根的子树中的所有结点都是该结点的子孙;
- 结点的层次(Level):从根结点开始,根结点的层次为1,根的直接后继层次为2以此类推; 结点的层序编号:将树中的结点,按照从上层到下层,同层从左到右的次序排成一个线性序列,把他们编成连续的自然数;
- 树的高度(深度Depth):树中结点的最大层次;
- 森林(Forest):m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根结点,森林就变成一棵树;
二叉树
二叉树(Binary Tree)是至多有两个子结点的树,每一个子节点都区分左右且不能颠倒顺序,左边子节点称为左子结点,右边子结点称为右子结点。
满二叉树:一颗深度为k且有
2
k
−
1
2^k-1
2k−1个结点的二叉树称为满二叉树(每一层的结点都达到最大值)。
完全二叉树:叶子结点只可能在最下面的两侧上;对于任一结点,其右分支下的子孙最大层数为
l
l
l, 其左分支子孙最大层数必为
l
+
1
l+1
l+1。
二叉树性质
- 在二叉树的第i层上至多有2^(i-1)个结点(i≥1);
- 深度为k的二叉树的 最大节点数为 2 k − 1 2^k-1 2k−1个;
- 任何一棵二叉树T,如果终端点结点数为n0,度为2的结点数n2,则n0 = n2+1;
- 具有n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ + 1 ⌊log_2^n⌋+1 ⌊log2n⌋+1;
- 如果一棵树有n个结点按层序编号(从第1层到 ⌊ l o g 2 n ⌋ + 1 ⌊log_2^n⌋+1 ⌊log2n⌋+1层,每层从左到右),对于任意结点i,有:(1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)= ⌊ i / 2 ⌋ ⌊i/2⌋ ⌊i/2⌋;(2)如果2i>n,则结点i为叶子节点(没有左孩子);否则其左孩子LCHILD(i)=2i;(3)如果2i+1>n,则结点i无右孩子结点;否则其右孩子RCHILD(i)=2i+1。
二叉树存储
顺序存储结构
按照自上而下,从左到右给整个二叉树编号,(不是完全二叉树一律转为完全二叉树,即如果结点为空也要给编号),然后以编号为下标存入数组中。这种存储对于满二叉树和完全二叉树可以做到唯一复原,而且不会浪费空间,但如果不是满二叉树和完全二叉树将会浪费很大存储空间。
链式存储结构
用二叉链表从根节点开始存储,当然访问也要从根节点开始访问。
二叉树结点数据类型定义
typedef struct node *tree_pointer;
typedef struct node {
int data;
tree_pointer left_child;
tree_pointer right_child;
} node;
/**
* LeetCode: Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
二叉树的遍历
二叉树由根、左子树、右子树构成,定义为D、L、R的组合共有六种遍历方案:LDR、LRD、DLR、DRL、RDL、RLD,在限定先左后右后,有三种方案:DLR(先序遍历)、LDR(中序遍历)、LRD(后序遍历) 。——可以看的出,“先、中、后”是按照访问结点D先于子树还是后于子树出现。
层次遍历:逐层从左到右遍历整个二叉树。(可以利用队列存放子树的指针)
二叉树遍历的时间复杂度和空间复杂度都是
O
(
n
)
O(n)
O(n)。
遍历算法
一般遍历算法采用递归来做格外简单
void PreOrderTraverve(node *root)
{ // 先序遍历
if(root != NULL){
std::cout << root->data;
PreOrderTraverve(root->left_child);
PreOrderTraverve(root->right_child);
}
}
void InOrderTraverve(node *root)
{ // 中序遍历
if(root != NULL){
PreOrderTraverve(root->left_child);
std::cout << root->data;
PreOrderTraverve(root->right_child);
}
}
void PostOrderTraverve(node *root)
{
if(root != NULL){
PreOrderTraverve(root->left_child);
PreOrderTraverve(root->right_child);
std::cout << root->data;
}
}
还原二叉树
给定
先序+中序
or后序+中序
可以唯一确定一颗二叉树而 先序+后序 不能。
先序遍历的根节点在前,中序遍历根节点左边是左子树的结点,右边是右子树的结点,后续遍历的根节点在最后。
所以给定前序+中序,根据前序遍历特点拿出先序序列第一个结点(先序第一个结点是根节点),将中序序列分为两段(左子树)根(左子树)
,继续重复直到还原。给定中序+后序,根据后序遍历特点,拿出后序序列最后结点(后序最后一个结点是根节点),将中序分为两段(左子树)根(左子树)`,继续重复直到还原。
二叉搜索树 BST
二叉搜索树(Binary Search Tree),又名二叉排序树、有序二叉树、二叉查找树,是一颗空树或者具有下列特点的树:
- 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 任意节点的左、右子树也分别为二叉查找树。
- 没有键值相等的节点。
重要性质:二叉搜索树的中序遍历是一个有序数组。如不同形态的二叉查找树(a)中序遍历:
6、8、10、11、13、14、15、17
。
缺点
二叉查找树比普通的树查找更快,插入、删除、查找的时间复杂度为 O ( l o g 2 n ) O(log_2^n) O(log2n),但当用有序数列创建一颗二叉搜索树时有可能生成一个单分支的二叉树,退化为一种线性链表似的结构了,它的查找时间复杂度还是 O ( n ) O(n) O(n),所以出现了平衡二叉树。
BST的操作有构建、插入、查找、删除等,这里不做实现,但是会实现平衡二叉树。
二叉搜索树的实现可以参考: 二叉搜索树详解(C++实现)
平衡二叉树
定义
平衡二叉树(Balanced Binary Tree)又称为AVL树,它是二叉树搜索树的改进,使得二叉树尽量降低高度,保持高度平衡,减少树的平均搜索长度。它是具有以下特点的二叉搜索树:
- 它是一颗空树或它的左右子树高度差绝不能超过1;
- 左右子树也必须是平衡二叉树
——关于AVL,AVL是大学教授G.M. Adelson-Velsky 和 E.M. Landis 名称的缩写,他们提出的平衡二叉树的概念,为了纪念他们,将 平衡二叉树 称为 AVL树。
在AVL树中,任何结点的左右子树高度最大差别是1,为了度量高度差引入了下边的一个概念,平衡因子。
平衡因子 balance factor
平
衡
因
子
=
结
点
左
子
树
高
度
−
结
点
右
子
树
高
度
平衡因子=结点左子树高度-结点右子树高度
平衡因子=结点左子树高度−结点右子树高度
当然,平衡因子(bf)也可以是右子树高度减去左子树高低,为了统一我这里用左子树高度-右子树高低。
在AVL树中,平衡因子必须满足: 1 > = b f > = − 1 1>=bf >=-1 1>=bf>=−1, 否则就不是AVL树了。
由AVL树的定义,上图不是AVL树。
破坏平衡情况
在介绍AVL操作之前先来看看破坏平衡的四种情况。大致可以分为两种,内测和外侧,其中外侧包括左左型和右右型,内测包括左右型和右左型。
左左型——右旋
在平衡状态下,给一个结点(T)的左左孩子(X)插入子结点(M),导致结点(T)平衡因子不符合AVL树要求,这种情况是左左型,需要降低左子树的高度,进行右旋转。
右旋步骤:
- 以L为圆形,T向右旋转成为L的右孩子;(T>L,T可以作为L的右孩子)
- L原来的右孩子Y变为T的左孩子。 (T>Y>L,Y可以作为T的左孩子)
注意:旋转以平衡因子被破坏的节点作为T。
其实Y可以是虚结点,当Y是一个虚结点的时候,插入M之前整个二叉树也是平衡的。
右旋动图展示:
右右型——左旋
在平衡状态下,给一个结点(T)的右右孩子(X)插入子结点(M),导致结点(T)平衡因子不符合AVL树要求,这种情况是右右型,需要降低右子树的高度,进行左旋转。
很容易看出,右右型和左左型是对称的,它们同是在AVL的外侧结点(X)插入子结点导致失衡。
左旋步骤:
- 以R为圆形,T向左旋转成为R的左孩子;(T<R,T可以作为R的左孩子)
- R原来的左孩子Y变为T的右孩子。 (T<Y<R,Y可以作为T的右孩子)
左旋动图展示:
左旋和右旋对称,相同的,Y也可以是虚结点。
如果Y是虚结点,那么在插入新的结点M后,R的平衡因子bf=-2, T的平衡因子也是-2,但是我们旋转选择距离插入节点更远的根节点T来做旋转。
左右型——左旋+右旋
左右型如果仅仅使用右旋转是不平衡的。
需要经过一个左旋将内测高度降低、外侧高度增加,然后在进行右旋降低左子树高度即可。
步骤:
- 以L结点进行左旋,完成左旋后在把R作为T的左子结点;(N>R>M>L,所以M可以作为L的右孩子)
- 以T结点进行右旋。
插入的结点时M或者N,这里进行左旋的时候要注意是以T的左孩子结点L为根进行的;
左旋和右旋比较重要,先以L为根进行左旋,在以T为根进行右旋,如果看不清楚可以将左旋、右旋的图和上图进行比较,建议自己在纸上多画几次。
右左型——右旋+左旋
右左型和左右型对称,需要先以T的右孩子进行右旋,将右内测的高度降低、升高右外侧的高度,然后在以T为根进行左旋,完成平衡。
AVL操作
AVL树的的操作和BST的操作基本相同,只是在插入删除的时候AVL需要通过旋转保持树的平衡。
插入
插入的时候从根结点开始,小就往左走,大就往右走,(注意不能相等)直到插入到合适的位置,如果是BST到这里就结束了,但是AVL需要根据新节点的位置进行四种不同的旋转以保证可以达到平衡。
查找
查找是从根节点开始,小就往左走,大就往右走,如果相等就返回,如果到达叶子节点还没找到就查找失败。
注意:AVL树由于加了平衡,查找的复杂度 O ( l o g 2 n ) O(log_2^n) O(log2n),这是它同BST的优势所在。
删除
平衡二叉树结点的删除比较复杂,主要取决于删除了结点之后是否平衡,如果失衡还要做出相应的调整。
主要把删除的结点分为三类:
(1) 叶子节点 (度为0的结点)
(2) 只有左子树或只有右子树(即删除的结点是度为1的结点)
(3) 既有左子树又有右子树(度为2的节点)
(1)当删除的节点是叶子节点,则将节点删除,然后从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;如果中间过程发现失衡,则判断属于哪种类型的失衡(左左,左右,右左,右右),然后进行调整。
(2)删除的节点只有左子树或只有右子树,这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除,然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了,从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,如果中间过程发现失衡,则根据失衡的类型进行调整。
(3)删除的节点既有左子树又有右子树,这种情况又比上面这种多一步,就是中序遍历,找到待删除节点的前驱或者后驱都行,然后与待删除节点互换位置,然后把待删除的节点删掉,后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。
中序遍历
同二叉树的中序遍历,先访问左子树,然后访问根最后在访问右子树,一般调用递归比较简单。
二叉搜索树的中序遍历是升序排列的,所以格外重要。
AVL的实现可以参考:STL源码笔记(18)—平衡二叉树AVL(C++封装+模板)
参考:
说明:图片来自网络和以上博客,如有侵权请联系删除