数据结构与算法(六)-二叉树与树
1.树的基本概念
术语包括:
术语 | 描述 |
---|---|
根节点 | |
子节点 | |
叶子节点 | 度为0的节点 |
边 | |
路径 | |
节点高度 | 深度-1 |
层级 | 根节点的层数为1,依次递增 |
深度 | 节点的最大层数 |
度 | 节点所也有的子树的个数 |
下面通过几个图解释树的几个比较重要的概念:
边、根节点、叶子节点
路径
节点高度
叶子节点高度为0
2. 二叉树
2.1 二叉树的定义
- 二叉树是n(n>=0)个节点的有限集,它或为空树(n=0),或有一个根节点和两棵分别称为左子树和右子树的互不相交的二叉树构成。二叉树特点:
- 二叉树每个节点最多有两棵子树,即二叉树每个节点的度不大于2;
- 左右子树不能颠倒;
- 二叉树是递归结构,在二叉树的定义中又用到了二叉树的概念;
- 二叉树可以是空树。
2.2 二叉树的分类
满二叉树
满二叉树满足两个特性:
- 所有的几点都包含两个子节点;
- 所有的叶子节点的Height或者Level都相等。
完全二叉树
- 完全二叉树是除了最后一层都是满的(都有两个子节点),并且最后一层的节点是从左往右排列的。
- 完全二叉树,通俗点说就是节点按层从左往右排列。如果最后一层排满了就是满二叉树,没有满则是完全二叉树。
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
- 完全二叉树可以使用数组表示,这也是为什么它必须要从左往右排列的原因。
完满二叉树
每个节点都有两个子节点。也就是说它比满二叉树少了一个条件。
2.3 二叉树的性质
这些性质在某些情况下及其有助于解题
- 在二叉树的第
i
层上至多有2^(i-1)
个节点(i>=1); - 深度为k的二叉树至多有
2^k-1
个节点(k>=1); - 对任何一棵二叉树T,如果其叶节点数为
n0
,度为2
的节点数为n2
,则n0=n2+1
; - 具有
n
个节点的完全二叉树的深度为Math.ceil(long2(n+1))
; - 对含n个节点的完全二叉树从上到下、从左至右进行1值n的连续编号,则完全二叉树中任意一个编号为i的节点:
- 节点
i
的左子节点编号为2*i
,右子节点编号为2*i+1
; - 若
i=1
,则该节点是二叉树的根,无双亲,否则,编号为i/2
的节点为其双亲节点; - 若
2*i>n
,则该节点无左子节点,否则,编号为2*i
的节点为其左子节点; - 若
2*i+1>n
,则该节点无右子节点,否则,编号为2*i+1
的节点为其右子节点。
- 节点
2.4 二叉树的存储结构
二叉树的链式存储
存储空间是离散的
二叉树的顺序存储
Java语言数组下标是从0开始的,而由于二叉树的特殊性,其对数组的下标要求从下标1开始(符合计算公式)
开辟一块连续的内存空间存储数据,在Java中基于数组存储二叉树的顺序结构,依赖于一个公式得出各个节点存储的位置。由上图的树结构,进行顺序存储有:
2.5 二叉树遍历
深度优先遍历:先序、中序、后序遍历都属于深度优先遍历。先序、中序、后序是针对与根获取次序来命名
宽度优先遍历:层次遍历属于宽度优先遍历。
先序遍历
先处理根节点,在遍历左子树,后遍历右子树(根-左-右)。Java代码描述如下:
public void preOrder(BinNode<T> root){
if(root != null){
visit(root);
preOreder(root.left);
preOreder(root.right);
}
}
中序遍历
先遍历左子树,再处理根节点,后遍历右子树(左-根-右)。Java代码描述如下:
public void inOrder(BinNode<T> root){
if(root != null){
inOrder(root.left);
visit(root);
inOrder(root.right);
}
}
后序遍历
先遍历左子树,再遍历右子树,后处理根节点(左-右-根)。Java代码描述如下:
public void postOrder(BinNode<T> root){
if(root != null){
postOrder(root.left);
postOrder(root.right);
visit(root);
}
}
层次遍历
实战题目:
https://leetcode-cn.com/problems/xu-lie-hua-er-cha-shu-lcof/
对二叉树从上到下、从左到右依次处理每一个节点。使用队列存储当前节点的子节点,先进先出,先操作。伪代码:
队列初始化Q
将二叉树的根节点插入队列Q;
while(Q非空){
取队首元素,将其赋值给p;
处理节点p;
将p的左、右子节点插入队列Q;
}
根据已知的遍历序列恢复二叉树
1.已知先序序列和中序序列,可以唯一确定二叉树;
2.已知后序序列和中序序列,可以唯一确定二叉树;
3.已知先序序列和后序序列,不能唯一确定二叉树.
例如,给出前序遍历 preorder = [3,9,20,15,7]
,中序遍历 inorder = [9,3,15,20,7]
,返回如下的二叉树:
3
/ \
9 20
/ \
15 7
**递归解析:**对一颗树而言,前序遍历的形式总是[ 根节点 | 左子树 | 右子树 ]
;中序遍历[ 左子树 | 根节点 | 右子树 ]
.这里采用分治算法,考虑通过递归对所有子树进行划分。分治算法解析:
-
递推参数:根节点在前序遍历的索引 root 、子树在中序遍历的左边界 left 、子树在中序遍历的右边界 right
-
终止条件:当 left > right ,代表已经越过叶节点,此时返回 null ;
-
递推工作:
-
建立根节点 node : 节点值为
preorder[root]
; -
划分左右子树: 查找根节点在中序遍历 inorder 中的索引
index
; -
构建左右子树: 开启左右子树递归,这里的 左右子树之间根节点的关系 是最重要的!
根节点在先序序列中的索引 中序遍历左边界 中序遍历右边界 左子树 roo+1 left index-1 右子树 root+(index-left)+1
(根节点索引+左子树长度+1)index+1 right 下面探讨
右子树的根节点在先序列中的索引
如何获取:在先序遍历中[ 根节点 | 左子树 | 右子树 ]
,那么右子树
的根节点也就是右子树中第一个元素,现在获取到左子树长度即可获取到右子树根节点索引.而左子树长度就要借助中序序列了,index
是中序序列中对于根节点的所有,left
是中序序列中对于树的左边界,那么左子树的长度自然就等于index - left
了.
-
-
返回值:node,作为上一层递归中根节点的左/右子树。
/**
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int[] preorder;
//为了提升效率,使用哈希表 map 存储中序遍历的值与索引的映射,查找操作的时间复杂度为 O(1)
Map<Integer,Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
//使得在下面的recur()方法也能访问到preorder
this.preorder = preorder;
for(int i=0;i<inorder.length;i++)
//存储对应的数值和下标,数值存储在key处,这是hashmap的惯用法
map.put(inorder[i],i);
return recur(0,0,inorder.length-1);
}
/**
* root 先序序列中的根节点索引
* left 中序序列中的左边界
* right 中序序列中的右边界
*/
public TreeNode recur(int root,int left,int right){
if(left > right) return null;//越过叶节点
TreeNode node = new TreeNode(preorder[root]);
int index = map.get(preorder[root]);//index根节点在前序序列中的所有值
node.left = recur(root+1,left,index-1);//恢复左子树
node.right = recur(root-left+index+1,index+1,right);//恢复右子树
return node;
}
}