一.树的定义
我们先来看看现实生活中树的概念.下面就是我们现实中树的定义如下:
无论多高多大的树,那也是从小到大、由根到叶、一点点成长起来的.俗话说十
年树木、 年树人,可一棵大树又何止是十年这样容易一一哈哈,说到哪里去了,我
们现在不是在上生物谍,而是要讲 种新的数据结构 一树。
二.树的概念和树的表现形式
2.1 树的概念
这里来解释树的概念,究竟什么是树呢?用文字来描述
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
图片如下:
树形结构中,子树之间不能有交集,否则就不是树形结构.
- 结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6
- 树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6
- 叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I…等节点为叶结点
- 双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
- 孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
- 根结点:一棵树中,没有双亲结点的结点;如上图:A
- 结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推
- 树的高度或深度:树中结点的最大层次; 如上图:树的高度为4 树的
- 非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G…等节点为分支结点
- 兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
- 堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
- 结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
- 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
- 森林:由m(m>=0)棵互不相交的树组成的集合称为森林
2.2 树的表现形式
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,实际中树有很多种表示方式,如:双亲表示法,
孩子表示法、孩子双亲表示法、孩子兄弟表示法等等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
static class TreeNode {
public char val;//数据域
public TreeNode left;//左孩子的引用
public TreeNode right;//右孩子的引用
public TreeNode(char val) {
this.val = val;
}
}
三.二叉树简介
3.1 二叉树的定义:
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
- 二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
对于任意的二叉树都是由以下几种情况复合而成的:
3.2 二叉树与其他树结构的比较
这是两种特殊的二叉树
满二叉树
满二叉树是一种特殊的二叉树,它的每个节点都有0个或2个子节点,且所有叶节点都在同一层上。满二叉树的层数为h,节点总数为2^h-1。满二叉树是一种特殊的完全二叉树.
满二叉树的特点是结构非常规整,每层节点数都是满的,因此也叫做“完美二叉树”。
完全二叉树
完全二叉树是一种二叉树,除了最后一层,其它层的节点数都是满的,最后一层的节点都靠左排列。
完全二叉树的特点是结构也比较规整,最后一层的节点可能不满,但是一定是从左到右依次排列的。
四.二叉树的性质与表示
4.1 二叉树的性质:
性质1:
在一棵二叉树中,第i层上最多有2^(i-1)个节点。
性质2:
在一棵二叉树中,深度为k,最多有2^k-1个节点.
性质3
在一棵二叉树中,如果叶子节点数为n0,度数为2的节点数为n2,则n0=n2+1。
下面列举一个场景,大家还是看图片
终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n为度是1的结点数。则树T结点总数n=no+n1+n2。
性质4:
性质5:
下面是一个例子.
4.2 二叉树的链式存储结构与顺序存储结构的比较。
顺序存储
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,井且结点的存储位
置,也就是数组的下标要能体现结点之间的逻辑关系,比如~亲与孩 的关系,左右
兄弟的关系等。
大家可以来看一下,完全二叉树的存储结构
将这棵树,放到数组中,如下:
另外对于一 般的二叉树,尽管层序编号不能反映逻辑关系,但也可以进行编号,只不过,把不存在的结点设置为 ^ 而已.
具体过程如下:
将这棵树,放到数组中,如下:
链式存储
既然顺序存储适用性不强,我们就要考虑链式存储结构。 二叉树每个结点最多有
两个孩子,所以为官设计一个数据域和两个指针域 较自然的想法, 我们称这样的
链表叫做二叉链表.
结构示意图,如图所示:
五. 二叉树的基本操作
5.1 二叉树的定义
对于java来说我们采用的是内部类去实现.
代码如下:
public class TestBinaryTree {
static class TreeNode {
public char val;//数据域
public TreeNode left;//左孩子的引用
public TreeNode right;//右孩子的引用
public TreeNode(char val) {
this.val = val;
}
}
}
至于为什么采用内部类的方式,我总结出来以下原因:
可以直接访问外部类的成员,简化内部类的定义。
至于为什么这么说,你看我下面的代码构思之后,就明白了.
5.2 二叉树的创建
这里我们简单粗暴一点,就直接手动创建即可,具体代码思路如下:
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;
}
构造出来的二叉树如下:
5.2 前序遍历:根-左-右。
前序遍历的规则是先遍历根节点,再遍历左节点,然后遍历右节点,具体过程如下:
至于我们代码上的思路:
自然是采用递归,在采用递归之前,我们首先要明白递归的三个要点:
- 确定递归函数的参数和返回值
- 确定终⽌条件
- 确定单层递归的逻辑
我对这三个问题的回答:
- 参数:当前遍历的节点root
- 终止条件:root为空,直接返回
- 单层递归逻辑:
- 先访问root节点
- 递归遍历root的左子树
- 递归遍历root的右子树
代码思路如下:
public void preOrder(TreeNode root) {
if(root == null) {
return;
}
System.out.print(root.val+" ");
preOrder(root.left);
preOrder(root.right);
}
5.3 中序遍历:左-根-右。
中序遍历的规则是先遍历左节点,再遍历根节点,最后遍历右节点.
我们还是来看看下图的操作:
还是来回答这三个问题:
- 参数:当前遍历的节点root
- 终止条件:root为空,直接返回
- 单层递归逻辑:
- 递归遍历root的左子树
- 访问root节点
- 递归遍历root的右子树
代码如下:
public void inOrder(TreeNode root) {
if(root == null) {
return;
}
inOrder(root.left);
System.out.print(root.val+" ");
inOrder(root.right);
}
5.4 后序遍历:左-右-根。
后序遍历的规则是先遍历左节点,再遍历右节点,最后遍历根节点
看具体图示遍历过程
回答这三个基本的问题:
- 参数:当前遍历的节点root
- 终止条件:root为空,直接返回
- 单层递归逻辑:
- 递归遍历root的左子树
- 递归遍历root的右子树
- 访问root节点
代码如下:
public void postOrder(TreeNode root) {
if(root == null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val+" ");
}
5.5 层次遍历:逐层遍历结点。
二叉树的层次遍历顾名思义按照次序依次遍历,具体怎么个遍历法,大家看动图操作
咋说呢,我们要手动去实现层次遍历,我们要借助另一个数据结构,就是队列,队列的特性大家都知道,就是先进先出.我们要利用这个特性帮我们完成层次遍历,我下面会给出层次遍历的思路:
- 使用队列存储每一层的节点。
- 首先将根节点入队。
- 如果队列为空,则遍历结束。否则从队列中取出一个节点,访问之。
- 如果该节点有左子节点,则左子节点入队;如果有右子节点,则右子节点入队。
- 重复步骤3和4,直到队列为空。
具体代码如下:
public void levelOrder(TreeNode root) {
if(root == null) {
return;
}
Queue<TreeNode> 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);
}
}
5.6 三种遍历方式的非递归实现。
这里针对的遍历的非递归实现是前序遍历,中序遍历,后序遍历.
接下来我们将对这三种方式展开讨论
前序遍历(非递归)
我接下来给出前序遍历的思路:
我们要利用一个数据结构栈的先进后出特性,来实现这个前序遍历.
具体思路如下:
- 定义栈stack和指针cur初始指向根节点。
- 如果cur不为空,则访问节点cur,并将cur指向其左子节点,push到栈中。
- 如果cur为空且栈不为空,则弹出一个节点,并将cur指向其右子节点。
- 重复2和3直到cur为空且栈为空。
具体操作步骤
具体代码如下:
public void preOrderNor(TreeNode root) {
if(root == null) {
return;
}
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
System.out.print(cur.val + " ");
cur = cur.left;
}
TreeNode top = stack.pop();
cur = top.right;
}
System.out.println();
}
中序遍历(非递归)
中序非递归的思路如下:
- 定义栈stack和指针cur,初始指向根节点。
- 如果cur不为空,则push到栈中,并cur指向其左子节点。
- 如果cur为空且栈不为空,则弹出一个节点,访问它,并将cur指向其右子节点。
- 重复2和3直到cur为空且栈为空。
具体代码思路如下:
public void inOrderNor(TreeNode root) {
if(root == null) {
return;
}
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
System.out.print(top.val + " ");
cur = top.right;
}
System.out.println();
}
后序遍历(非递归)
后序遍历非递思路如下:
- 定义栈stack,指针cur和prev。cur初始指向根节点,prev初始为空。
- 如果cur不为空,则一直向左子树遍历,并将节点压入栈中。
- 如果cur为空,stack不为空,则判断栈顶节点top:
- 如果top的右子树为空,或右子树已被访问过(prev指向它),则访问该节点,从stack中弹出,并将prev指向它。
- 否则,令cur指向top的右子树,接着向左子树遍历。
- 重复步骤3,直到cur为空且stack为空,遍历结束。
具体的代码如下:
public void postOrderNor(TreeNode root) {
if(root == null) {
return;
}
TreeNode cur = root;
TreeNode prev = null;
Deque<TreeNode> stack = new ArrayDeque<>();
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if(top.right == null || top.right == prev) {
System.out.print(top.val+" ");
stack.pop();
prev = top;
}else {
cur = top.right;
}
}
System.out.println();
}
5.7 获取叶子节点的个数
获取叶子节点的思路如下:
子问题思路:
什么是叶子节点?
左节点和右节点都为null就为叶子节点.
具体思路:
- 如果当前节点的左子树和右子树都为空,则当前节点即为叶子节点,将其加入结果列表。
- 如果当前节点的左子树不为空,则递归遍历左子树,获取左子树的叶子节点。
- 如果当前节点的右子树不为空,则递归遍历右子树,获取右子树的叶子节点。
- 合并左子树和右子树得到的叶子节点,得到当前二叉树的全部叶子节点。
- 不断递归,直到遍历完整棵二叉树,最终得到二叉树全部叶子节点
代码如下:
public int getLeafNodeCount(TreeNode root) {
if(root == null) {
return 0;
}
if(root.left == null && root.right == null){
return 1;
}
int leftSize = getLeafNodeCount(root.left);
int rightSize = getLeafNodeCount(root.right);
return leftSize+rightSize;
}
5.8 获取第K层节点的个数
具体思路如下:
- 如果K == 1,则返回根节点。因为根节点是二叉树的第1层。
- 如果当前节点的左子树或右子树不为空,且K > 1,则递归遍历左子树和右子树,获取各自第K-1层的节点个数。
- 当前层的节点个数 = 左子树第K-1层节点个数 + 右子树第K-1层节点个数。
- 递归终止条件是K < 1,此时返回0。
具体实现代码如下:
代码如下:
public int getKLevelNodeCount(TreeNode root,int k) {
if(root == null) {
return 0;
}
if(k == 1) {
return 1;
}
int leftSize = getKLevelNodeCount(root.left,k-1);
int rightSize = getKLevelNodeCount(root.right,k-1);
return leftSize+rightSize;
}
5.9 获取二叉树的高度
具体思路如下:
- 如果树为空,高度为0。
- 如果左子树高度和右子树高度都不为0,则取左子树高度和右子树高度的最大值,加1作为当前树的高度。
- 如果只有左子树或右子树不为空,则取非空子树的高度加1作为当前树的高度。
- 不断递归,直到遍历完所有节点,得到整棵二叉树的高度。
public int getHeight(TreeNode root) {
if(root == null) {
return 0;
}
return (getHeight(root.left) > getHeight(root.right)) ?
(getHeight(root.left)+1):(getHeight(root.right)+1);
}
5.10 检测值为value的元素是否存在
具体思路入下:
- 如果树为空,则返回False。
- 如果当前节点的值等于value,则返回True。
- 如果当前节点的值不等于value,则递归遍历左子树和右子树,检测是否存在值为value的节点。
- 如果左子树或右子树递归返回True,则返回True,否则返回False。
- 不断递归,直到遍历完整棵二叉树,如果任一子树返回True,则返回True,否则返回False。
TreeNode find(TreeNode root, int val) {
if(root == null) {
return null;
}
if(root.val == val) {
return root;
}
TreeNode leftTree = find(root.left,val);
if(leftTree != null) {
return leftTree;
}
TreeNode rightTree = find(root.right,val);
if(rightTree != null) {
return rightTree;
}
return null;//没有找到
}
5.11 判断一棵树是不是完全二叉树
什么是完全二叉树,大家可以先来看看下面的图示:
完全二叉树只有两种情况,情况一:就是满二叉树,
情况二:最后一层叶子节点没有满。
知道了什么是完全二叉树之后,我们需要去判断一课树是不是完全二叉树,具体的思路如下:
- 空树视为完全二叉树,直接返回True。
- 使用一个队列存储二叉树节点,先入队根节点。
- 循环队列,如果当前节点不为空,则将其左右子节点入队。
- 如果遇到第一个为空的节点,则继续循环队列。
- 如果循环队列过程中,再遇到不为空的节点,则说明不是完全二叉树,返回False。
- 循环结束后,如果队列为空,说明是完全二叉树,返回True。
boolean isCompleteTree(TreeNode root){
if(root == null) {
return true;
}
Queue<TreeNode> 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 tmp = queue.poll();
if(tmp != null) {
return false;
}
}
return true;
}
六.二叉树的实际题目
1. 检查两颗树是否相同。
OJ链接
大概就是
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
首先我们先来划分不相同和相同的一个情况
具体思路如下:
- 如果只有一棵树为空,则两棵树不相同,返回False。
- 如果两棵树都为空,则相同,返回True。
- 如果两棵树的根节点值不相同,则不相同,返回False。
- 如果根节点相同,则递归检查两棵树的左子树是否相同。
- 如果左子树相同,则递归检查两棵树的右子树是否相同。
- 只有当左右子树也都相同时,则两棵树才相同,返回True。
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null || p != null && q == null) {
return false;
}
//走到这里 要么都是空 要么都不是空
if(p == null && q == null) {
return true;
}
if(p.val != q.val) {
return false;
}
//p q都不空 且 值一样
return isSameTree(p.left,q.left)
&& isSameTree(p.right,q.right);
}
2. 另一颗树的子树。
具体来说
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。
目前判断是不是另一颗的子树有以下三种情况
具体思路如下:
- 判断是不是相同的树
- 判断是不是root的左子树
- 判断是不是root的右子树
代码如下:
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null || p != null && q == null) {
return false;
}
//走到这里 要么都是空 要么都不是空
if(p == null && q == null) {
return true;
}
if(p.val != q.val) {
return false;
}
//p q都不空 且 值一样
return isSameTree(p.left,q.left)
&& isSameTree(p.right,q.right);
}
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if(root == null || subRoot == null) {
return false;
}
if(isSameTree(root,subRoot)) return true;
if(isSubtree(root.left,subRoot)) return true;
if(isSubtree(root.right,subRoot)) return true;
return false;
}
3. 翻转二叉树。
先带大家看一下具体的翻转过程究竟是什么样子的
具体思路:
- 如果根节点为空,直接返回。
- 交换根节点的左右子树。
- 递归翻转左子树。
- 递归翻转右子树。
- 返回翻转后的根节点。
具体代码实现如下:
具体代码如下:
public TreeNode invertTree(TreeNode root) {
if(root == null) {
return null;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
4. 判断一颗二叉树是否是平衡二叉树。
OJ链接
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。
具体下面的图示:
具体解决思路:
1.先求左边高度
2.再求右边高度
3.求高度之差,进行比较绝对值绝对不超过1
具体代码:
public boolean isBalanced(TreeNode root) {
if(root == null) {
return true;
}
int leftH = maxDepth(root.left);
int rightH = maxDepth(root.right);
return Math.abs(leftH-rightH) < 2
&& isBalanced(root.left)
&& isBalanced(root.right);
}
//求高度
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return (leftHeight > rightHeight) ?
(leftHeight+1):(rightHeight+1);
}
}
5. 对称二叉树。
OJ链接
对称二叉树的结构如下:
我们的解题思路就是:
1.左子树的左树和右子树的右树是否相等
2.左子树的右树和右子树的左树是否相等
3.重复上面的俩个操作.
代码如下:
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return isSymmetricChild(root.left,root.right);
}
public boolean isSymmetricChild(TreeNode leftTree,TreeNode rightTree) {
//情况1
if(leftTree != null && rightTree == null || leftTree == null && rightTree != null) {
return false;
}
//情况2
if(leftTree == null && rightTree == null) {
return true;
}
//走到这里说明都不为空了
if(leftTree.val != rightTree.val) {
return false;
}
return isSymmetricChild(leftTree.left,rightTree.right)
&& isSymmetricChild(leftTree.right,rightTree.left);
}
6. 二叉树的构建及遍历。
OJ链接
这道题的意思如下:
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
我们用具体的步骤去演示现在的内容.
具体的代码思路:
- 使用Scanner读取输入的字符串,每次读取一行。
- 根据输入的字符串构建二叉树,使用createTree递归构建。遇到#跳过,否则创建节点并递归构建左右子树。
- 中序遍历构建好的二叉树,输出节点的值。
- 重复步骤1-3,读取下一行输入并处理,直到没有更多输入。
class TreeNode {
char val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(char val) {
this.val = val;
}
}
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNextLine()) {
String str = scan.nextLine();
TreeNode root = createTree(str);
inOrder(root);
}
}
public static int i = 0;
public static TreeNode createTree(String str) {
TreeNode root = null;
if (str.charAt(i) != '#') {
root = new TreeNode(str.charAt(i));
i++;
root.left = createTree(str);
root.right = createTree(str);
} else {
i++;
}
return root;
}
public static void inOrder(TreeNode root) {
if (root == null) {
return;
}
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
}
7. 二叉树的分层遍历 。
在介绍这个题目之前,我们还要了解一下,什么是二叉树的分层遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如图所示,遍历的顺序为:ABCDEFGHI。
接下来我们先来实现最简单的分层遍历,再根据这个简单的分层遍历去思考在线oj的题目
具体思路如下:
- 根节点入队
- 输出队头节点,并将其非空子节点入队
- 队头出队,重复步骤2
- 直到队列为空,遍历结束
动画效果如下:
具体代码如下:
public void levelOrder(TreeNode root) {
if(root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
//1.根节点入队
queue.offer(root);
while (!queue.isEmpty()) {
//2.队头出队
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);
}
}
}
看了上面的题解之后,我们来解决上面的在线oj.
具体思路:
- 创建一个队列queue和一个列表list来存储结果。
- 首先将根节点root加入队列。
- 然后进入一个循环,循环次数取决于队列的大小。
- 在每次循环中,从队列中弹出一个节点,并将其值添加到临时列表tmp中。
- 然后将该节点的左右子节点(如果存在)加入队列。
- 循环完成后,将临时列表tmp添加到结果列表list中。
- 重复步骤3-6,直到队列为空。
- 返回结果列表list。
具体代码:
public List<List<Integer>> levelOrder2(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
if(root == null) {
return list;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
while (size != 0) {
TreeNode cur = queue.poll();
//System.out.print(cur.val + " ");
//tmp.add(cur.val);
size--;
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
list.add(tmp);
}
return list;
}
8. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。
先来对问题进行一定的分析.
目前公共祖先出现的情况我们分为以下三种情况.
OJ链接
具体思路:
- 首先判断当前根节点root是否是p或q其中的一个,如果是则直接返回root。
- 然后递归地查找左子树和右子树。
- 如果左子树返回的节点和右子树返回的节点都不为空,说明p和q分别在左子树和右子树,所以当前root是最近公共祖先,返回root。
- 如果左子树返回的节点不为空,则返回左子树返回的节点。
- 如果右子树返回的节点不为空,则返回右子树返回的节点。
- 如果左右子树都返回空,则说明当前树中不存在p和q,返回null。
- 通过这样的递归,如果p和q的最近公共祖先节点较深,则会先找到更深的那个,否则会直接在判断根节点的时候返回根节点。
具体代码:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) {
return null;
}
//先判断根节点 是不是其中的一个
if(root == p || root == q) {
return root;
}
TreeNode leftRet = lowestCommonAncestor(root.left,p,q);
TreeNode rightRet = lowestCommonAncestor(root.right,p,q);
if(leftRet != null && rightRet != null) {
return root;
}else if(leftRet != null) {
return leftRet;
}else if( rightRet != null ){
return rightRet;
}
return null;
}
具体对代码的细节做出一些解释
9. 根据一棵树的前序遍历与中序遍历构造二叉树。
OJ链接
在写代码之前,我们先来看看,具体我们怎么通过分析得到构造的二叉树.
代码具体思路:
- 在前序遍历找到根节点
- 在中序遍历中找到根节点root的位置,则root的左子树位于中序遍历中root的左边,右子树位于右边。
- 递归地构造左子树和右子树。
具体的代码如下:
class Solution {
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public int i = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
return buildTreeChild(preorder,inorder,0,inorder.length-1);
}
public TreeNode buildTreeChild(int[] preorder, int[] inorder,
int inbegin,int inend) {
if(inbegin > inend) {
return null;
}
TreeNode root = new TreeNode(preorder[i]);
//找到当前根,在中序遍历的位置
int rootIndex = findIndex(inorder,inbegin,inend,preorder[i]);
i++;
root.left = buildTreeChild(preorder,inorder,inbegin,rootIndex-1);
root.right = buildTreeChild(preorder,inorder,rootIndex+1,inend);
return root;
}
private int findIndex( int[] inorder,int inbegin,int inend, int key) {
for(int i = inbegin;i <= inend; i++) {
if(inorder[i] == key) {
return i;
}
}
return -1;
}
10. 根据一棵树的中序遍历与后序遍历构造二叉树。
OJ链接
在写代码之前,我们先来看看,具体我们怎么通过分析得到构造的二叉树.
具体思路:
- 在后序遍历找到根节点
- 在中序遍历中找到根节点root的位置,则root的左子树位于中序遍历中root的左边,右子树位于右边。
- 递归地构造左子树和右子树。
具体代码:
class Solution {
public int i = 0;
public TreeNode buildTree(int[] inorder, int[] postorder) {
i = postorder.length-1;
return buildTreeChild(postorder,inorder,0,inorder.length-1);
}
public TreeNode buildTreeChild(int[] postorder, int[] inorder,
int inbegin,int inend) {
if(inbegin > inend) {
return null;
}
TreeNode root = new TreeNode(postorder[i]);
//找到当前根,在中序遍历的位置
int rootIndex = findIndex(inorder,inbegin,inend,postorder[i]);
i--;
root.right = buildTreeChild(postorder,inorder,rootIndex+1,inend);
root.left = buildTreeChild(postorder,inorder,inbegin,rootIndex-1);
return root;
}
private int findIndex( int[] inorder,int inbegin,int inend, int key) {
for(int i = inbegin;i <= inend; i++) {
if(inorder[i] == key) {
return i;
}
}
return -1;
}
}
}
11. 二叉树创建字符串。
OJ链接
具体分析过程
我们这里
具体思路:
- 首先使用StringBuilder来构建字符串。
- 使用递归的方法前序遍历二叉树的每个节点。
- 如果当前节点t为空,则直接返回。
- 如果当前节点t不为空,则添加对应的"(“和”)",以及节点的值t.val。
- 如果当前节点的左子节点t.left不为空,则递归遍历左子树,并添加对应的"(“和”)"。
- 如果左子节点为空而右子节点不为空,则添加"()"表示左子树为空。
- 如果当前节点的右子节点t.right为空,则直接返回。
- 如果右子节点不为空,则递归遍历右子树,并添加对应的"(“和”)"。
- 递归遍历完成后,通过StringBuilder.toString()得到最终的字符串。
具体代码:
public String tree2str(TreeNode root) {
if(root == null ){
return null;
}
StringBuilder stringBuilder=new StringBuilder();
tree2strChilde(root,stringBuilder);
return stringBuilder.toString();
}
public void tree2strChilde(TreeNode t,StringBuilder stringBuilder) {
if(t == null) {
return;
}
stringBuilder.append(t.val);
if(t.left != null) {
stringBuilder.append("(");
tree2strChilde(t.left,stringBuilder);
stringBuilder.append(")");
}else {
//左边为空了
if(t.right != null) {
//右边不为空
stringBuilder.append("()");
}else {
//右边为空
return ;
}
}
if(t.right == null) {
return;
}else {
stringBuilder.append("(");
tree2strChilde(t.right,stringBuilder);
stringBuilder.append(")");
}
}
五. 总结与扩展
树是一种非线性数据结构,由节点和边组成。每个节点最多只有一个父节点,但可以有多个子节点。二叉树是树的一种特殊形式,每个节点最多只有两个子节点。树和二叉树在算法和数据结构中有广泛的应用,例如搜索、排序、数据库、编译器等。它们也可以用于解决各种实际问题,如计算机网络、生物学、图形学等。
扩展:
树和二叉树是计算机科学中非常重要的基础数据结构,它们有许多扩展形式和应用。例如:
平衡树:平衡树是一种特殊的二叉树,它可以保证树的高度始终在一个较小的范围内,从而提高了树的效率。常见的平衡树包括AVL树、红黑树等。
B树和B+树:B树和B+树是一种多叉树,它们被广泛应用于文件系统和数据库中,可以快速地进行插入、删除和查找等操作。
Trie树:Trie树是一种多叉树,它用于存储字符串集合,可以快速地进行字符串匹配和前缀搜索等操作。
堆:堆是一种特殊的树形数据结构,它满足堆序性质,可以用于排序和优先级队列等场景。常见的堆包括最大堆和最小堆等。
赫夫曼树:赫夫曼树是一种特殊的二叉树,它被广泛应用于数据压缩中,可以将数据压缩成更小的体积。
除此之外,树和二叉树还有许多其他的应用,如图形学中的空间分区、计算机网络中的路由算法、生物学中的系统演化等。因此,学习树和二叉树对于理解计算机科学中的许多问题具有重要意义。
期待我们后续的学习,小伙伴们!