目录
1.树的基本概念
2.二叉树的概念与性质
3.有关二叉树的基本实现
4.部分oj题目解析
树与我们之前所学的链式结构或者顺序结构都有所不同,树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看 起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
1.有一个特殊的结点,称为根结点,根结点没有前驱结点
2.除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
3.树是递归定义的。
比如下面的就是一种树
然后我们介绍一些树的重要概念:
结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6
树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6
叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶结点
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
根结点:一棵树中,没有双亲结点的结点;如上图:A
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推
树的深度:树中结点的最大层次; 如上图:树的高度为4
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
从上图可以看出:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
当然我们还有两种特殊的二叉树:
1.满二叉树:如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵 二叉树的层数为K,且结点总数是,则它就是满二叉树。
2. 完全二叉树: 完全二叉树是由满二叉树而引出来的。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完 全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
注意区分完全二叉树与非完全二叉树
这些性质对于我们做题非常重要,请务必牢记!
1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有 (i>0)个结点
2. 若规定只有根结点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 (k>=0)
3. 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
4. 具有n个结点的完全二叉树的深度k为 上取整
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i 的结点有:
若i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
若2i+1
若2i+2
那说了那么多让我们看看下面几道题
这题我们应该可以想到n0=n2+1这个公式,则答案为199+1=200,选B
这题大家乍一看可能会没什么思路,但实际上还是考察的对几个公式的应用
首先在完全二叉树中有2n个节点(偶数),则我们可以知道n1的数量是1,由我们的性质
n0=n2+1以及我们二叉树的总节点2n=n0+n1+n2,,由这三个式子我们可以得到n0=n,
所以这题选A
这题我们很容易想到是用这个公式k=(向上取整),带入数据我们可以得到选B
在构建二叉树前,我们先介绍一些二叉树是如何储存的
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
接下来我们尝试着手创建一棵二叉树
static class TreeNode {
public char val;
public TreeNode left;//左孩子的引用
public TreeNode right;//右孩子的引用
public TreeNode(char val) {
this.val = val;
}
}
public TreeNode createTree() {//暴力创建一棵二叉树
TreeNode A = new TreeNode('A');
TreeNode B = new TreeNode('B');
TreeNode C = new TreeNode('C');
TreeNode D = new TreeNode('D');
TreeNode E = new TreeNode('E');
TreeNode F = new TreeNode('F');
TreeNode G = new TreeNode('G');
TreeNode H = new TreeNode('H');
A.left = B;
A.right = C;
B.left = D;
B.right = E;
C.left = F;
C.right = G;
E.right = H;
return A;
}
当然以上创建方法并非是正规的创建方法,这里为了大家方便理解我们的后续操作所以采用了这种暴力的方法去建树,在之后我们会以一道例题的形式来讲解如何以递归的方式建树
谈到二叉树我们就免不了要谈到他的四种遍历方式
前序遍历(Preorder Traversal )——访问根结点--->根的左子树--->根的右子树。
中序遍历(Inorder Traversal)——根的左子树--->根节点--->根的右子树。
后序遍历(Postorder Traversal)——根的左子树--->根的右子树--->根节点。
层序遍历(LeveLOrderTraverse)——从根节点出发,从左到右依上至下去访问
我们以下图为例讲解
比如我们去模拟一下他的前序遍历如下图
红色代表递归的过程,绿色代表回溯,所以我们不难得到
前序遍历 : 1 2 3 4 5 6
中序遍历:3 2 1 5 4 6
后序遍历:3 1 5 6 4 1
层序遍历:1 2 3 4 5 6
(剩下三种参考前序遍历也可得出,给大家一个思考空间自己画一下图)
接下来是我们的代码实现
public void preOrder(TreeNode root){//前序遍历
if(root==null) return ;
System.out.println(root.val+" ");
preOrder(root.left);
preOrder(root.right);
}
public void inOrder(TreeNode root){//中序遍历
if(root==null) return ;
preOrder(root.left);
System.out.println(root.val+" ");
preOrder(root.right);
}
public void postOrder(TreeNode root){//后序遍历
if(root==null) return ;
preOrder(root.left);
preOrder(root.right);
System.out.println(root.val+" ");
}
关于遍历的代码我们需要注意判断root是否为空,因为在递归的过程中我们会一直递归到树的最深处,此时root会为空,为了防止继续遍历导致空指针异常我们需要及时返回一个null代表本次递归结束返回。
之后我们可以发现三种遍历方式的代码其实只有输出的位置不同,这也正好和我们遍历的顺序不同有关。
有关层序遍历我们单独拿出来讲,因为它与其他三种遍历方式的代码实现有所差异
void levelOrder(TreeNode root) {
if(root == null) return;
Queue queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
System.out.print(cur.val+" ");
if(cur.left != null) {
queue.offer(cur.left);
}
if(cur.right != null) {
queue.offer(cur.right);
}
}
}
可以看到,为了实现层序遍历我们需要使用Queue队列容器去实现,因为层序遍历的方式正好是由上至下从左到右的顺序来实现的,所以我们可以得到思路:首先如果该树的root不为空则先把他加入到队列,一个循环,如果队列不为空则不断将队首元素取出,如果队首元素的左儿子或者右儿子不为空则把他加入队列,之后不断重复该过程即可
接下来我们实现一些二叉树的基本操作:
1. 获取树中节点的个数
我们只需要按任意方式遍历一遍二叉树即可
int treeSize(TreeNode root){//获取树节点数
if(root==null) return 0;
return treeSize(root.left)+treeSize(root.right)+1;
}
2.获取叶子节点个数
和获取全部节点的个数类似,不过需要多一个判断,当该节点的左儿子和右儿子均为空时才返回1
int getLeafNodeCount(TreeNode root){//获取叶子节点数
if(root == null) return 0;
if(root.left == null && root.right == null) return 1;
return getLeafNodeCount(root.left)+getLeafNodeCount(root.right);
}
3.获取第k层节点个数
从根节点开始遍历,每次递归时k-1,当k=1时返回
int getKLevelNodeCount(TreeNode root,int k){//获取第k层节点数
if(root == null) return 0;
if(k == 1) return 1;
return getKLevelNodeCount(root.left,k-1)+getKLevelNodeCount(root.right,k-1);
}
4.获取二叉树高度
每次递归左子树和右子树,返回较大值+1
int getHeight(TreeNode root){//获取二叉树高度
if(root==null) return 0;
return Math.max(getHeight(root.left),getHeight(root.right))+1;
}
5.查找某节点
TreeNode find(TreeNode root,int val){//查找某节点
if(root == null) return null;
if(root.val == val) return root;//找到后返回
TreeNode ret=find(root.left,val);
if(ret!=null){//找到后返回
return ret;
}
ret=find(root.right,val);
if(ret!=null){//找到后返回
return ret;
}
return null;
}
这题的题意乍一看可能有点绕,但实际上就是让你去判断一棵树是否为完全二叉树
思路:
这题如果是初见的话的确有一些难度,我们可以用层序遍历的方式去遍历它,只要当前节点不为空,就将它弹出队列,并把它的左右节点加入队列(无论空不空),当当前节点为空或者队列为空时结束循环,最后遍历队列,如果队列元素均为null说明是完全二叉树,反之则不是(为什么会有这个结论大家可以尝试去手动模拟一下)
AC代码:
class Solution {
public boolean isCompleteTree(TreeNode root) {
if(root==null) return false;
Queue queue=new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
TreeNode cur=queue.poll();
if(cur!=null){
queue.offer(cur.left);
queue.offer(cur.right);
}
else{
break;
}
}
while(!queue.isEmpty()){
TreeNode cur = queue.peek();
if(cur == null) {
queue.poll();
}else {
return false;
}
}
return true;
}
}
题目说了那么多其实就是让你判断一棵树是不是另一棵树的子树(当两棵树相同时也算)
思路:
首先我们需要写一个判断两棵树是否相等的方法,然后递归去判断这棵树,左子树,右子树是否与另一棵数相同
AC代码:
class Solution {
public boolean isSubtree(TreeNode s, TreeNode t) {
if(s==null) return false;//防止空指针异常(为空了还没匹配说明不符合)
return isSameTree(s,t) || isSubtree(s.left,t) || isSubtree(s.right,t);
}
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==q&&q==null){
return true;
}
if(p!=null&&q!=null&&p.val==q.val){
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
else{
return false;
}
}
}
这题就是让你根据前序遍历与中序遍历去还原一棵二叉树 ,虽然题目非常简单,但是这题却有着非常深刻的教育意义,同时他也是一道非常经典的面试题。
在做这题之前我们还需要一些前置知识,如何根据前序遍历和中序遍历去还原一棵二叉树
我们知道前序遍历的顺序是 根--->左子树--->右子树
中序遍历 左子树--->根--->右子树
然后我们看到下面这道题来感受一下
根据前序遍历的顺序,我们可以知道前序遍历的第一个节点一定为根节点E
根据中序遍历的顺序,我们以根节点E作为分界线,E的左边是他的左子树,右边为右子树
这样一颗二叉树的大致结构就已经出现的,接下来我们只要不断重复这个过程便能还原这棵二叉树了,大家发现这个过程是不是不断把一个大问题分成若干个性质相同的小问题,所以实际上可以运用递归去解决
那我也给大家把这棵树画出来
那现在我们已经拥有了前置知识,那这题我们也可以用相同的方式去思考
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。
这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
当我们理解了思路之后自然要解决的就是递归时如何利用下标来划分左右子树的问题,这里借用一下官方的图,可以说已经是非常清楚了大家可以自己去理解一下,但是有一个细节问题我们可以优化一下,就是我们在中序遍历去寻找根节点时,朴素做法时去遍历一遍,时间复杂度为O(n),但是我们可以利用哈希表去做一个映射达到O(1)的查找,在java中我们可以利用Map容器来实现
AC代码:
class Solution {
Map hm = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0;ipr) return null;
int k=hm.get(pre[pl]);
TreeNode u=new TreeNode(pre[pl]);
u.left=dfs(pre,in,pl+1,pl+k-il,il,k-1);
u.right=dfs(pre,in,pl+k-il+1,pr,k+1,ir);
return u;
}
}