不同于前面我们已经熟知的数据结构,二叉树这部分内容概念居多,且这些概念对我们解决后面的面试题有很大帮助;其次,在学习二叉树的过程中,会大量用到一种我们之前学习到的方法——递归。所以学习二叉树,要重点关注这两点内容。
目录
一:树型结构
1.1概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来就像一棵倒挂的树,根朝上,而叶朝下。
树具有以下的特点:
- 有一个特殊的结点,称为根结点,根结点没有前驱结点;
- 除根结点外,其余结点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树;
- 树是递归定义的;
- 树形结构中,子树之间不能有交集,即不能形成环。
一些重要概念:
- 结点的度:一个结点含有子树的个数称为该结点的度; 如上图:A的度为6
- 树的度:一棵树中,所有结点度的最大值称为树的度; 如上图:树的度为6
- 叶子结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等节点为叶子结点
- 双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:D是H的父结点
- 孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
- 根结点:一棵树中,没有双亲结点的结点;如上图:A
- 结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推
- 树的高度或深度:树中结点的最大层次; 如上图:树的高度为4
- 非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等节点为分支结点
- 兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点
- 堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点
- 结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先
- 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙
- 森林:由m(m>=0)棵互不相交的树组成的集合称为森林
以上蓝色背景处为需要重点关注的概念。
1.2树的表示形式(了解即可)
树结构相对线性表比较复杂,实际中树有很多种表示方式,如:双亲表示法,孩子表示法、孩子双亲表示法、孩子兄弟表示法等等。例如树的孩子兄弟表示法如下:
二:二叉树
2.1定义
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空;
- 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
如下图所示就是一棵二叉树:
注意:
1. 二叉树不存在度大于2的结点;
2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
2.2两种特殊的二叉树
1.满二叉树: 一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
2. 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从0至n-1的结点一一对应时称之为完全二叉树。注意:满二叉树是一种特殊的完全二叉树。
2.3二叉树的性质
2.4 二叉树的存储
二叉树的存储结构分为:顺序存储和类似于链表的链式存储。本课主要介绍链式存储。链式存储,又以孩子表示法和孩子双亲表示法居多。
//孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点(或父结点)
}
2.5二叉树的遍历
所谓二叉树的遍历,是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如:打印节点内容、节点内容加1等等。) 遍历是二叉树上最重要的操作之一,是在二叉树上进行其它运算之基础。
二叉树的遍历共有四种方式:即前序遍历、中序遍历、后序遍历和层序遍历。这里所谈的前中后,是指遍历根的顺序。对于三种遍历,大家可以参考以下文章,其中有大量的图解,非常形象。
进行二叉树的遍历,前提条件是创建一棵二叉树。为便于初学,在此先使用手动方式进行创建,即直接指定各个节点之间的关系;对于二叉树的结构,使用孩子表示法。具体代码如下:
public class BinaryTree {
static class TreeNode {
public char val;
public TreeNode left;//左孩子的引用
public TreeNode right;//右孩子的引用
public TreeNode(char val) {
this.val = val;
}
}
//public TreeNode root;
/**
* 创建一棵二叉树 返回这棵树的根节点
*
* @return
*/
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;
}
}
最终创建的二叉树如下图所示:
2.5.1前序遍历
(1).递归思路
// 前序遍历
//遍历思路:
public void preOrder(TreeNode root) {
if (root == null) return;
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
遍历思路:如果根结点为空,直接return;否则直接打印根节点;然后递归调用根节点的左,再递归调用根节点的右。对于上面创建的二叉树,运行结果如下:2
(2)子问题思路
参考oj题:
144. 二叉树的前序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-preorder-traversal/
//2.子问题思路:
public List<Character> preorderTraversal(TreeNode root) {
List<Character> ret = new ArrayList<>();
if (root == null) return ret;
ret.add(root.val);
List<Character> leftTree = preorderTraversal(root.left);
ret.addAll(leftTree);
List<Character> rightTree = preorderTraversal(root.right);
ret.addAll(rightTree);
return ret;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
List<Character> list= new ArrayList<>();
list = tree.preorderTraversal(root);
System.out.println("前序遍历的结果为:");
System.out.println(list);
}
}
这就是子问题思路求解前序遍历的典型例题。什么是子问题思路呢?先把根节点放到列表里,然后再放左,再放右。不同于遍历思路的是,每次访问完左树或右树,要接收一下它的返回值。就是把递归下去的值接收到当前层。
运行结果如下:
2.5.2中序遍历
(1)递归思路
// 中序遍历
void inOrder(TreeNode root) {
if (root == null) return;
inOrder(root.left);
System.out.print(root.val + " ");
inOrder(root.right);
}
运行结果如下:
(2)子问题思路
参考oj题:
94. 二叉树的中序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-inorder-traversal/参考前序遍历的思路即可,代码如下:
//2.子问题思路
public List<Character> inorderTraversal(TreeNode root) {
List<Character> ret = new ArrayList<>();
if (root == null) return ret;
List<Character> leftTree = inorderTraversal(root.left);
ret.addAll(leftTree);
ret.add(root.val);
List<Character> rightTree = inorderTraversal(root.right);
ret.addAll(rightTree);
return ret;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
List<Character> list= new ArrayList<>();
list = tree.inorderTraversal(root);
System.out.println("中序遍历的结果为:");
System.out.println(list);
}
}
运行结果如下:
2.5.3后序遍历
(1).递归思路
// 后序遍历
void postOrder(TreeNode root) {
if (root == null) return;
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val + " ");
}
(2)子问题思路
参考oj题:
145. 二叉树的后序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-postorder-traversal/ 代码如下:
//2.子问题思路
public List<Character> postorderTraversal(TreeNode root) {
List<Character> ret = new ArrayList<>();
if (root == null) return ret;
List<Character> leftTree = postorderTraversal(root.left);
ret.addAll(leftTree);
List<Character> rightTree = postorderTraversal(root.right);
ret.addAll(rightTree);
ret.add(root.val);
return ret;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
List<Character> list= new ArrayList<>();
list = tree.postorderTraversal(root);
System.out.println("后序遍历的结果为:");
System.out.println(list);
}
}
运行结果如下:
2.5.4层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
具体实现:
对于层序遍历的实现,我们要注意:
1.层序遍历需要借助队列;
2.步骤如下:
- 首先,创建一个队列;
- 将二叉树的根节点入队;
- 如果队列不为空,将队列中一个元素出队,同时创建一个新节点接受出队元素,并打印节点值;
- 判断新节点的左不为空,则将新节点的左边节点入队;
- 判断新节点的右不为空,则将新节点的右边节点入队。(判断顺序不能颠倒)
- 重复循环3、4、5步。
具体代码如下:
//层序遍历
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);
}
}
}
运行结果如下:
2.6二叉树的基本操作
首先,宏观地看下我们所需要掌握的操作,大致如下:
注意,以下所有的操作,均针对我们上文中手动创建的这棵二叉树,即:
2.6.1获取树中结点的个数
/**
* 获取树中结点的个数
*/
public static int count = 0;
//1.递归思路
int size(TreeNode root){
if(root == null) return 0;
count++;
size(root.left);
size(root.right);
return count;
}
//2.子问题思路
int size1(TreeNode root) {
if (root == null) return 0;
return size1(root.left) + size1(root.right) + 1;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
int count = tree.size(root);
int count1 = tree.size1(root);
System.out.println("二叉树结点个数为:"+count);
System.out.println("二叉树结点个数为:"+count1);
}
}
运行结果如下:
2.6.2获取叶子节点的个数
要解决这个问题,首先你要思考到——什么是叶子节点?就是左子树和右子树均为空的结点。有了这样的思考,解决这个问题就信手拈来了。
/**
* 获取叶子节点的个数
*/
//1.递归思路
public static int LeafNodeCount = 0;
int getLeafNodeCount(TreeNode root){
if(root == null) return 0;
if(root.left == null && root.right == null) LeafNodeCount++;
getLeafNodeCount(root.left);
getLeafNodeCount(root.right);
return LeafNodeCount;
}
//2.子问题思路
int getLeafNodeCount1(TreeNode root){
if(root == null) return 0;
if(root.left == null && root.right == null) return 1;
return getLeafNodeCount1(root.left) + getLeafNodeCount1(root.right);
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
int count = tree.getLeafNodeCount(root);
int count1 = tree.getLeafNodeCount1(root);
System.out.println("二叉树叶子结点个数为:"+count);
System.out.println("二叉树叶子结点个数为:"+count1);
}
}
运行结果如下:
2.6.3获取第K层节点的个数
相当于求root左树的第K-1层+root右树的第K-1层,依次类推。
如图所示,假设求第4层节点的个数,相当于以B,C为根节点时,求第3层节点的个数;
对于以B节点为根节点的这棵树来说,它第3层节点的个数又等于以D,E为根节点时第2层节点的个数。依此类推即可。
具体代码如下:
/**
* 获取第k层结点的个数
* @param root
* @return
*/
int getKLevelNodeCount(TreeNode root, int k) {
if (root == null) return 0;
if (k == 1) return 1;
return getKLevelNodeCount(root.left, k - 1) +
getKLevelNodeCount(root.right, k - 1);
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
int count = tree.getKLevelNodeCount(root,4);
System.out.println("第4层结点的个数为"+count);
}
}
运行结果如下:
2.6.4获取二叉树的高度
二叉树的高度,即等于左子树和右子树中最大的高度再加1。
/**
* // 获取二叉树的高度
*/
int getHeight(TreeNode root){
if (root == null) return 0;
return getHeight(root.left) > getHeight(root.right) ?
getHeight(root.left) + 1 : getHeight(root.right) + 1;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
int height = tree.getHeight(root);
System.out.println("这颗二叉树的高度为:"+height);
}
}
运行结果如下:
2.6.5获取值为val的元素
先来看一种错误的写法:
TreeNode find(TreeNode root, int val){
if(root == null) return null;
if(root.val == val) return root;
find(root.left,val);
find(root.right,val);
return null;
}
在这种情况下,当一条支路走完任没有找到值为val的元素时,就会return null,而不会继续遍历其余的结点。
例如在上图的二叉树中获取值为‘H’的元素,当遍历到值为‘D’的元素时,就已经会return null了,因此得到错误的结果。正确做法是保存每次遍历的结果,如果不为空再打印,如果全部遍历完任未找到值为val的元素,才返回null。正确做法如下:
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;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
binarytree.TreeNode findnode = tree.find(root,'H');
if(findnode == null){
System.out.println("未找到!");
}else
{
System.out.println("找到了!"+"值为 "+findnode.val);
}
}
}
运行结果如下:
2.6.6判断一棵树是不是完全二叉树
借助队列。
先将根结点放入队列中,判断当前队头元素不为空,就弹出队列中的一个结点,并将该节点的左和右入队;判断当前队头元素不为空,重复上述操作,直至队头元素为空。
如果此时队列中没有非空元素,说明这棵树就是一棵完全二叉树;否则,不是完全二叉树。
依据这个思路,具体代码如下:
/**
* 判断一棵树是不是完全二叉树
*/
boolean isCompleteTree(TreeNode root){
if(root == null) return false;
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 cur = queue.peek();
if(cur == null){
queue.poll();
}else{
return false;
}
}
return true;
}
public class Test {
public static void main(String[] args) {
binarytree tree = new binarytree();
binarytree.TreeNode root = binarytree.createTree();
boolean ret = tree.isCompleteTree(root);
if(ret == true){
System.out.println("这是一棵完全二叉树!");
}else{
System.out.println("这不是一棵完全二叉树!");
}
}
}
运行结果如下:
2.7二叉树相关oj题
2.7.1检查两棵二叉树是否相同
链接:100. 相同的树 - 力扣(LeetCode)https://leetcode.cn/problems/same-tree/
1.题目
给你两棵二叉树的根节点
p
和q
,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
2.题解
注意考虑所有可能的情况,两棵树相同,即两棵树的左子树和右子树均相同。
注意:该算法的时间复杂度为O(min(m,n))。
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null) return false;
if(p != null && q == null) return false;
if(p == null ) return true;
if(p.val != q.val) return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
2.7.2另一颗树的子树
链接:Loading Question... - 力扣(LeetCode)https://leetcode.cn/problems/subtree-of-another-tree/
1.题目
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
2.题解
可以利用1题中的代码,先判断这两棵树是否相同,然后依次判断subRoot是否等于root的左树,subRoot是否等于root的右树。
boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q != null) return false;
if(p != null && q == null) return false;
if(p == null ) return true;
if(p.val != q.val) return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == 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;
}
2.7.3二叉树的最大深度
链接:
104. 二叉树的最大深度 - 力扣(LeetCode)https://leetcode.cn/problems/maximum-depth-of-binary-tree/1.题目
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。
2.题解
就是左子树和右子树中最大的高度再加1。代码如下:
public int maxDepth(TreeNode root) {
if(root == null) return 0;
int leftH = maxDepth(root.left) ;
int rightH = maxDepth(root.right) ;
return leftH > rightH ? leftH + 1 : rightH + 1;
}
2.7.4平衡二叉树
链接:110. 平衡二叉树 - 力扣(LeetCode)https://leetcode.cn/problems/balanced-binary-tree/
1.题目
给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
2.题解
public class Solution {
//计算给定二叉树的高度
private int Height(TreeNode head) {
if(head == null) return 0;
return Math.max(Height(head.left),Height(head.right)) + 1;
}
public boolean IsBalanced_Solution(TreeNode root) {
if(root == null) return true;//空树
return Math.abs(Height(root.left)-Height(root.right)) <= 1 &&
IsBalanced_Solution(root.left) &&
IsBalanced_Solution(root.right);
}
}
2.7.5对称二叉树
链接:
101. 对称二叉树 - 力扣(LeetCode)https://leetcode.cn/problems/symmetric-tree/
1.题目
给你一个二叉树的根节点
root
, 检查它是否轴对称。
2.题解
这与2.7.2题有异曲同工之妙啊!具体代码如下:
public boolean isSymmetricChild(TreeNode leftTree,TreeNode rightTree) {
if (leftTree == null && rightTree != null) return false;
if (leftTree != null && rightTree == null) return false;
if(leftTree == null ) return true;
if( leftTree.val != rightTree.val) return false;
return isSymmetricChild(leftTree.left,rightTree.right) && isSymmetricChild(leftTree.right,rightTree.left);
}
public boolean isSymmetric(TreeNode root) {
return isSymmetricChild(root.left,root.right);
}
2.7.6二叉树的构建及遍历
链接:
编一个程序,读入用户输入的一串先序遍历字符串,根据此字符串建立一个二叉树(以指针方式存储)。 例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。建立起此二叉树以后,再对二叉树进行中序遍历,输出遍历结果。
2.题解
首先打好框架,包括包的导入,结点的定义,主函数的书写,字符串输入,用前序遍历的方式创建二叉树的逻辑,以及中序遍历输出。具体代码如下:
import java.util.*;
class TreeNode{
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val){
this.val = val;
}
}
public class Main{
public static TreeNode createTree(String str){
}
public static void inOrder(TreeNode root){
if(root == null) return;
inOrder(root.left);
System.out.print(root.val+" ");
inOrder(root.right);
}
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);
}
}
}
接下来,主要攻克用前序遍历方式创建二叉树。在创建过程中,如果遇到'#',说明为null,否则,就要new一个新的结点。按照根---左---右的顺序进行递归创建即可。具体代码如下:
import java.util.*;
class TreeNode{
public char val;
public TreeNode left;
public TreeNode right;
public TreeNode(char val){
this.val = val;
}
}
public class Main{
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);
}
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);
}
}
}
2.7.7二叉树的分层遍历
链接:102. 二叉树的层序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-level-order-traversal/
1.题目
2.题解
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<>();
if (root == null) return ret;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();//3
List<Integer> list = new ArrayList<>();
while (size != 0) {
TreeNode cur = queue.poll();
list.add(cur.val);
size--;//0
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ret.add(list);
}
return ret;
}
}
2.7.8二叉树的最近公共祖先
链接:
236. 二叉树的最近公共祖先 - 力扣(LeetCode)https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/1.题目:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
2.题解
思路一:如果这棵二叉树的表示方法是孩子双亲表示法,那么此时求最近公共祖先,可以被优化为求链表的交点!由这种思路,我们引申出解法一:
解法一:
1.使用两个栈,存储从根节点到指定节点的路径上的所有节点;
2.比较两个栈的大小,让栈中多的出差值个节点;
3.此时同时开始出栈,如果栈顶元素相同,那么此时这个值就是最近的公共祖先。
基于这种思路,代码如下:
private boolean getPath(TreeNode root,TreeNode node,Stack<TreeNode> stack){
if(root == null || node == null) return false;
stack.push(root);
if(root == node) return true;
boolean flg1 = getPath(root.left,node,stack);
if(flg1 == true) return true;
boolean flg2 = getPath(root.right,node,stack);
if(flg2 == true) return true;
stack.pop();
return false;
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
Stack<TreeNode> stack1 = new Stack<>();
getPath(root,p,stack1);
Stack<TreeNode> stack2 = new Stack<>();
getPath(root,q,stack2);
int size1 = stack1.size();
int size2 = stack2.size();
if(size1 > size2){
int s = size1 - size2;
while(s-- > 0){
stack1.pop();
}
}else{
int s = size2 - size1;
while(s-- > 0){
stack2.pop();
}
}
//此时两个栈元素就相同了
while(!stack1.empty() && !stack2.empty()){
if(stack1.peek() != stack2.peek()){
stack1.pop();
stack2.pop();
} else {
return stack1.peek();
}
}
return null;
}
思路二:如果这棵树是一棵二叉搜索树,那么怎么找最近公共祖先呢?注意二叉搜索树,是指具有下列性质的二: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。分析如下:
由这种思路,我们可以得到启发,进而引申出解法二:
1.p和q在异侧,此时的最近公共祖先是root;
2.p和q其中一个为root,此时的最近公共祖先是p或者q;
3.p和q在同侧,此时需要分别递归左侧或者右侧继续查找。
基于这种思路,代码如下:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(root == p || root == q) return root;
//走到这里,说明p和q都不是根节点,分别到左右子树中去找
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;
}else{
return null;
}
}
2.7.9二叉搜索树与双向链表
链接:
1.题目
2.题解
import java.util.*;
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
TreeNode prev = null;
public void ConvertChild(TreeNode pCur) {
if (pCur == null) return;
ConvertChild(pCur.left);
//System.out.println(pCur.val+" ");
pCur.left = prev;
if (prev != null) {
prev.right = pCur;
}
prev = pCur;
ConvertChild(pCur.right);
}
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) return null;
ConvertChild(pRootOfTree);
TreeNode head = pRootOfTree;
while (head.left != null) {
head = head.left;
}
return head;
}
}
因为最后要求返回链表的第一个节点,即二叉树左下角的那个节点,所以需要保护根节点,不让它发生移动,最后就可以根据根节点与左下角节点的性质,找到链表的第一个节点。
基于这种思路,我们设计了ConvertChild(pRootOfTree)函数,在这里实现更改节点关系的逻辑。
本题难度较大,重点要去理解核心的代码逻辑。
2.7.10从前序和中序遍历构造二叉树
链接:
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
2.题解
抓住前中序遍历的特点,结合分析,可以写出如下代码:
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public int preIndex = 0;
public TreeNode buildTreeChild(int[] preorder, int[] inorder,int inbegin,int inend) {
if(inbegin > inend) return null;//说明此时,没有左树或者右树
TreeNode root = new TreeNode(preorder[preIndex]);
//1、找到根节点在中序遍历当中的位置
int rootIndex = findInorderRootIndex(inorder,inbegin,inend,preorder[preIndex]);
preIndex++;
root.left = buildTreeChild(preorder,inorder,inbegin,rootIndex-1);
root.right = buildTreeChild(preorder,inorder,rootIndex+1,inend);
return root;
}
private int findInorderRootIndex(int[] inorder,int inbegin,int inend,int val) {
for(int i = inbegin;i <= inend;i++) {
if(inorder[i] == val) {
return i;
}
}
return -1;//没有val这个数据
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
return buildTreeChild(preorder,inorder,0,inorder.length-1);
}
}
1.需要注意的是,preIndex应该定义为全局变量,否则在回退时,preIndex的值也会跟着回退,实际上我们需要它不断前进。
2.在这里没有判断当rootIndex的值小于0时,应该进行什么操作。因为题目所给的必定是一棵二叉树正确的前序或中序遍历,所以其实不存在rootIndex < 0的情况。
2.7.11根据二叉树创建字符串
链接:606. 根据二叉树创建字符串 - 力扣(LeetCode)https://leetcode.cn/problems/construct-string-from-binary-tree/1.题目
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。
空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
2.题解
先进行简单的过程分析:
由此逻辑,写出具体代码:
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public void tree2strChild(TreeNode t, StringBuilder sb){
sb.append(t.val);
if (t.left != null) {
sb.append("(");
tree2strChild(t.left, sb);
sb.append(")");
} else {
if (t.right == null) {
return;
} else {
sb.append("()");
}
}
if (t.right == null) {
return;
} else {
sb.append("(");
tree2strChild(t.right, sb);
sb.append(")");
}
}
public String tree2str(TreeNode root) {
if (root == null) return null;
StringBuilder sb = new StringBuilder();
tree2strChild(root, sb);
return sb.toString();
}
}
注意:
2.7.12二叉树前序遍历的非递归实现
链接:
144. 二叉树的前序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-preorder-traversal/1.题目
给你二叉树的根节点
root
,返回它节点值的 前序 遍历。
2.题解
定义cur = root,一开始不断向左走,只要cur != null,就将当前节点入栈,并将节点的val值放入List中;
当cur == null时,说明当前栈顶节点的左已经走完了,此时弹出弹顶元素,并用节点top存储一下,再让cur = top.right,去找当前节点的右。
此时分为两种情况:如果右边节点为空,说明当前节点的左右子树都为空,判断栈是否为空,如果栈不为空,就继续将栈顶元素出栈;如果右边节点不为空,那么就将该节点入栈,并将节点的val值放入List中,显然这时就是在重复上面1的操作。
这说明,1、2、3的操作是一个大循环,而1的操作又是一个小循环。
基于这种思路,我们写出如下代码:
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list =new ArrayList<>();
if(root == null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.empty()){
while(cur != null){
stack.push(cur);
list.add(cur.val);
cur = cur.left;
}
TreeNode top = stack.pop();
cur = top.right;
}
return list;
}
}
2.7.13二叉树中序遍历的非递归实现
链接:
94. 二叉树的中序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-inorder-traversal/1.题目:
给你二叉树的根节点
root
,返回它节点值的 中序 遍历。
2.题解:
和前序遍历非常相似,只是在入栈时不会直接将节点val值加入队列,而是在遍历到最左端时,弹出节点同时将其加入队列中,其余代码均不变。
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list =new ArrayList<>();
if(root == null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.empty()){
while(cur != null){
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
list.add(top.val);
cur = top.right;
}
return list;
}
}
在这儿我要做一点补充,在非递归实现前序遍历和中序遍历时,为什么我们可以直接把栈顶元素pop出去呢?这是因为在这两种遍历下,顺序无非是根-左-右或左-根-右,当pop栈顶元素赋值给top节点后,只需要用cur存储top元素的右节点即可,此时我们已经处理完左和根了,所以不需要再保存左节点或根节点的信息。而在后序遍历中,顺序为左-右-根,此时不能直接把栈顶元素pop出去了,因为我拿到栈顶元素后,先得判断当前节点的右子树是否为空,再做打算,此时还需要保存根节点信息,因为这个是最后被加入List的。总而言之,这会是后序遍历与前、中序遍历的一个重大区别。
2.7.14二叉树后序遍历的非递归实现
1.链接
145. 二叉树的后序遍历 - 力扣(LeetCode)https://leetcode.cn/problems/binary-tree-postorder-traversal/2.题目
给你二叉树的根节点
root
,返回它节点值的 后序 遍历。
3.题解
基于前面的分析,我们写出第一版的代码,这个版本是存在问题的:
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if (root == null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.empty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null) {
stack.pop();
list.add(top.val);
} else {
cur = top.right;
}
}
return list;
}
在这个版本下,我们不直接将栈顶元素弹出,而是先通过peek获取栈顶元素,然后对右树进行是否为空进行判断。如果top.rihgt == null,弹出栈顶元素并加入List;否则,让cur向右走。
这个代码存在一定的问题。
如下图所示,当cur到达节点6时,此时栈顶元素为6,peek一下,top拿到的就是节点6;top.right == null成立,将节点6弹出并打印节点6;此时栈不为空,继续peek,top拿到的是节点7, top.right == null不成立,执行cur = top.right,此时cur又到达节点6,但是节点6已结被加入List了。如此反复,则成死循环。
所以我们定义一个prev,每当一个节点加入到List中,就用prev保存这个节点的信息,并在出栈时加上这个判断条件,即top.right == prev时,也要出栈,而不是让cur向右行进。所以改正之后的代码为:
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if (root == null) return list;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while (cur != null || !stack.empty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null || top.right == prev) {
stack.pop();
list.add(top.val);
prev = top;
} else {
cur = top.right;
}
}
return list;
}
}