参考文章:https://www.cnblogs.com/lfalex0831/p/9698249.html
参考文章:https://www.cnblogs.com/utank/p/4256133.html
参考文章:https://blog.csdn.net/qq_42730750/article/details/108285846
一、介绍
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。也就是说二叉树是每个结点最多有两个子树的数据结构。
二叉树的基本形态
- 空二叉树
- 只有一个根结点的二叉树
- 只有左子树;
- 只有右子树
- 完全二叉树
如下图所示
二叉树相关术语
- 节点的度:一个节点含有的子树的个数称为该节点的度;
- 叶节点:也叫终端节点,即度为0的节点;
- 分支节点:度不为0的节点;
- 父节点:也叫双亲节点,若一个节点含有子节点,则这个节点称为其子节点的父节点,例如:B 结点是 A 结点的孩子,则A结点是B结点的双亲;
- 子节点:也叫孩子节点,一个节点含有的子树的根节点称为该节点的子节点;
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;
- 树的度:一棵树中,最大的节点的度称为树的度;
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;
- 节点的祖先:从根到该节点所经分支上的所有节点;
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
- 路径和路径长度:从结点n1到nk的路径为一个结点序列n1,n2,...,nk。ni是ni+1的父结点。路径所包含边的个数为路径的长度;
【二叉树的几个性质】
- 一个二叉树第i层的最大结点数为:2i-1,i≥1;
- 深度为k的二叉树有最大结点总数为:2k-1,k≥1;
- 对任何非空二叉树T,若n0表示叶结点的个数,n2是度为2的非叶结点个数,那么两者满足关系:n0=n2+1;
假设:叶结点个数为n0;度为1的结点个数为n1;度为2的结点个数为n2。
则二叉树的总边数N=2*n2+n1;总结点数N′=n0+n1+n2;
因N+1=N′,所以2*n2+n1+1=n0+n1+n2;得n0=n2+1。
二、二叉树特殊类型
1、斜二叉树
斜二叉树:只有左子节点或只有右子节点的二叉树称为斜二叉树;
- 度为1;
- 只有左子节点或右子节点;
2、满二叉树
满二叉树(完美二叉树):除最后一层无任何子节点外,每一层上的所有结点都有两个子结点;
- 叶子结点只能在最后一层;
- 非叶子节点的结点的度为2;
3、完全二叉树
完全二叉树:有n个结点的二叉树,对树中的结点从上至下、从左到右顺序进行编号,编号为i(1≤i≤n)结点与满二叉树中编号为i结点在二叉树中的位置相同
完全二叉树的顺序存储结构特点:
- 根结点的序号为1;
- 结点(序号为i)的左孩子结点的序号是:2 * i,若2*i > n,则没有左孩子;
- 结点(序号为i)的右孩子结点的序号是:2 * i + 1,若2*i+1 > n,则没有右孩子;
注意:满二叉树一定是完全二叉树,而完全二叉树不一定是满二叉树;
4、线索二叉树
对于二叉树,无论是何种,叶子节点左右两边都是空链域,切空链域的个数还很多。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。
那么怎么避免空叶子节点的空间浪费呢?
而线索二叉树是在上面null的位置放入遍历时的前驱结点和后继结点,如下图:
上图只是其中一种线索,黑线代表前驱,红线代表后继。
线索:利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。
实际就是为了解决无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题。
为了避免混淆,尚需改变结点结构,增加两个标志域。
lchild LTag data(数据) RTag rchild 其中: LTag=0 lchild域指示结点的左孩子 LTag=1 lchild域指示结点的前驱 RTag=0 rchild域指示结点的右孩子 RTag=1 rchild域指示结点的后继
例如:
代码实现:
二叉线索链表节点的定义如下:
class ThreadNode(object): def __init__(self, data='#'): self.data = data self.lchild = None self.rchild = None self.ltag = 0 self.rtag = 0
以某种次序遍历将二叉树变为线索二叉树的过程称为线索化。
线索化的实质就是当二叉树中某节点不存在左孩子或右孩子时,将其lchild域或rchild域指向该节点的前驱或后继。
普通二叉树
改成线索二叉树
以这棵二叉树为例,来看一下线索二叉树是如何构造的
1.先序遍历线索二叉树
上述二叉树的先序遍历为:A B D E C F ABDECFABDECF
上面这棵就是先序线索二叉树,是通过先序遍历构造的,根据原二叉树的二叉链表可知,空指针(即度为1的节点或叶子节点的孩子指针域)得到了有效利用。
先序线索二叉树寻找节点的后继的过程如下:
(1) 如果该节点有左孩子,则左孩子就是该节点的后继;
(2) 如果该节点无左孩子但有右孩子,则右孩子就是该节点的后继;
(3) 如果该节点即无左孩子又无右孩子(即叶子节点),则右链域指向的节点就是该节点的后继。
2.后序线索二叉树
上述二叉树的后序遍历为:D E B F C A
后序线索二叉树寻找节点的后继的过程如下:
(1) 如果该节点是二叉树的根,则该节点的后继为空;
(2) 如果该节点是其双亲的右孩子,或者是其双亲的左孩子且双亲没有子树,则该节点的后继即为双亲;
(3) 如果该节点是其双亲的左孩子,且其双亲有右子树,则该节点的后继为双亲的右子树上按后序遍历得到的第一个节点。
先序线索二叉树、后序线索二叉树寻找节点的前驱也都复杂,这里也就不介绍了,不常用,最常用的是下面要介绍的中序线索二叉树。
3.中序线索二叉树
上述二叉树的后序遍历为:D B E A F C
中序线索二叉树的建立如下:
令指针PreNode指向刚刚访问过的节点,指针RootNode指向正在访问的节点,即PreNode指向RootNode的前驱。在中序遍历过程中,先判断RootNode是否有左孩子,若没有左孩子就将它的lchild指向PreNode;然后再判断PreNode是否有右孩子,若没有右孩子就将它的rchild指向RootNode。
为了方便,可以在二叉树的线索链表上添加一个头节点,令其lchild域的指针指向二叉树的根节点,其rchild域的指针指向中序遍历时访问的最后一个节点(即最右边的那个节点);然后再令二叉树中序序列中的第一个节点(即二叉树最左边的那个节点)的lchild域指针和最后一个节点的rchild域指针均指向头节点。没错,这是二叉树的双向线索链表。
三、二叉树的遍历
二叉树的遍历是指从根节点出发,按照某种次序依次访问二叉树中所有节点,使得每个节点被访问依次且仅被访问一次。
二叉树的遍历次序不同于线性结构,线性结构最多也就是分为顺序、循环、双向等简单的遍历方式。
而树不存在唯一的后继节点,在访问一个节点后,下一个被访问的节点面临着不同的选择,所以我们需要规范遍历方式
三种遍历方法:
- 前序遍历:根-左-右
- 中序遍历:左-根-右
- 后序遍历:左-右-根
记忆点:前中后指的是根节点
1、前序遍历
定义:先访问根节点,然后访问左子树,再访问右子树;
按照定义遍历的顺序遍历结果为:A B D H I E J C F K G
代码实现:
# 前序遍历 # 递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution: def preorderTraversal(self, root: TreeNode) -> List[int]: res = [] def tree(node): if not node: return res.append(node.val) tree(node.left) tree(node.right) tree(root) return res # 迭代 class Solution: """ 它先将根节点 cur 和所有的左孩子入栈并加入结果中,直至 cur 为空,用一个 while 循环实现: 然后,每弹出一个栈顶元素 tmp,就到达它的右孩子,再将这个节点当作 cur 重新按上面的步骤来一遍,直至栈为空。这里又需要一个 while 循环。 """ def preorderTraversal(self, root: TreeNode) -> List[int]: if not root: return [] cur, stack, res = root, [], [] while cur or stack: while cur: # 根节点和左孩子入栈 res.append(cur.val) stack.append(cur) cur = cur.left tmp = stack.pop() # 每弹出一个元素,就到达右孩子 cur = tmp.right return res
2、中序遍历
定义:先访问左子树,再访问根节点,最后访问右子树;
按照定义遍历的顺序遍历结果为:H D I B E J A F K C G
代码实现:
# 中序遍历 # 递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution: def inorderTraversal(self, root: TreeNode) -> List[int]: res = [] def tree(node): if not node: return tree(node.left) res.append(node.val) tree(node.right) tree(root) return res # 迭代 class Solution: """ 和前序遍历的代码完全相同,只是在出栈的时候才将节点 tmp 的值加入到结果中。 """ def inorderTraversal(self, root: TreeNode) -> List[int]: if not root: return [] cur, stack, res = root, [], [] while cur or stack: while cur: # 根节点和左孩子入栈 stack.append(cur) cur = cur.left tmp = stack.pop() res.append(cur.val) # 出栈再加入结果 cur = tmp.right return res
3、后序遍历
定义:先访问左子树,再访问右子树,最后访问根节点;
按照定义遍历的顺序遍历结果为:H I D J E B K F G C A
代码实现:
# 后续遍历 # 递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Solution: def postorderTraversal(self, root: TreeNode) -> List[int]: res = [] def tree(node): if not node: return tree(node.left) tree(node.right) res.append(node.val) tree(root) return res # 迭代 class Solution: """ 继续按照上面的思想,这次我们反着思考,节点 cur 先到达最右端的叶子节点并将路径上的节点入栈; 然后每次从栈中弹出一个元素后,cur 到达它的左孩子,并将左孩子看作 cur 继续执行上面的步骤。 最后将结果反向输出即可。参考代码如下: """ def postorderTraversal(self, root: TreeNode) -> List[int]: if not root: return [] cur, stack, res = root, [], [] while cur or stack: while cur: # 先达最右端 res.append(cur.val) stack.append(cur) cur = cur.right tmp = stack.pop() cur = tmp.left return res[::-1]
4、层次遍历
定义:逐层的从根节点开始,每层从左至右遍历;
按照定义遍历的顺序遍历结果为:A B C D E F G H I J K
代码实现:
四、平衡树
平衡树(Balance Tree,BT) 指的是,任意节点的子树的高度差都小于等于1。常见的符合平衡树的有,B树(多路平衡搜索树)、AVL树(二叉平衡搜索树)等。
平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
参考文章:https://www.cnblogs.com/suimeng/p/4560056.html
1、平衡二叉树-AVL Tree
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
AVL树首先是一种二叉查找树(也可以叫二叉排序树)。二叉查找树是这么定义的,为空或具有以下性质:
- 若它的左子树不空,则左子树上所有的点的值均小于其根节点
- 若它的右子树不空,则右子树上所有的点的值均大于其根节点
- 它的左右子树分别为二叉查找树
AVL 树是具有以下性质的二叉查找树:
- 左子树和右子树的高度差(平衡因子)不能超过 1
- 左子树和右子树都是 AVL树
例如,将一个数组{1,2,3,4}依次插入树的时候,形成了图1的情况。有建立树与没建立树对于数据的增删查改已经没有了任何帮助,反而增添了维护的成本。而只有建立的树如图2,才能够最大地体现二叉树的优点。
在上述的例子中,图2就是一棵平衡二叉树。科学家们提出平衡二叉树,就是为了让树的查找性能得到最大的体现(至少我是这样理解的,欢迎批评改正)。下面进入今天的正题,平衡二叉树。
平衡二叉树的定义(特性):
- 可以是空树。
- 假如不是空树,任何一个节点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
平衡之意,如天平,即两边的分量大约相同。如定义,假如一棵树的左右子树的高度之差超过1,如左子树的树高为2,右子树的树高为0,子树树高差的绝对值为2就打破了这个平衡。如依次插入1,2,3三个节点(如下图)后,根节点的右子树树高减去左子树树高为2,树就失去了平衡。
那么在建立树的过程中,我们如何知道左右子树的高度差呢?在这里我们采用了平衡因子进行记录。
平衡因子:以某个节点为根,这个节点的左子树的高度减去右子树的高度。由平衡二叉树的定义可知,平衡因子的取值为0,1,-1的都是平衡二叉树,分别对应着左右子树等高,左子树比较高,右子树比较高。如下图
说到这里,我们已经可以大概知道平衡二叉树的结构定义需要什么内容了,数据成员,平衡因子,以及左右分支。
如何判断二叉树是否为平衡树
前置知识:二叉树的深度
# 节点定义如下 # Definition for a binary tree node. class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None n3 = TreeNode(3) n9 = TreeNode(9) n20 = TreeNode(20) n15 = TreeNode(15) n7 = TreeNode(7) n3.left = n9 n3.right = n20 n20.left = n15 n20.right = n7 n3 = TreeNode(3) n9 = TreeNode(9) n20 = TreeNode(20) n15 = TreeNode(15) n7 = TreeNode(7) n3.left = n9 n3.right = n20 n20.left = n15 n20.right = n7
Solution().maxDepth(n3)
# 方法一:每到一个节点,深度就+1 class Solution: def maxDepth(self, root: TreeNode) -> int: return self.get_dep(root, 1) def get_dep(self, node, depth): if not node: return depth - 1 return max(self.get_dep(node.left,depth+1),self.get_dep(node.right,depth+1)) # 方法二: class Solution: def maxDepth(self, root: TreeNode) -> int: if root is None: return 0 else: left_depth = self.maxDepth(root.left) right_depth = self.maxDepth(root.right) return max(left_depth,right_depth)+1
算法解析:
终止条件: 当 root 为空,说明已越过叶节点,因此返回 深度 0 。
递推工作: 本质上是对树做后序遍历,从叶子节点一层层向上加。
计算节点 root 的 左子树的深度 ,即调用 maxDepth(root.left);
计算节点 root 的 右子树的深度 ,即调用 maxDepth(root.right);
返回值: 返回 此树的深度 ,即 max(maxDepth(root.left), maxDepth(root.right)) + 1。