深入理解二叉树——从结构和递归原理到实战
目录
一、引言
二、树的基本概念
2.1 树的定义和特点
2.2 常见术语
2.3 树的表示方法
三、二叉树的核心概念
3.1 二叉树的定义
3.2 两种特殊的二叉树
满二叉树
完全二叉树
3.3 二叉树的性质
3.3.1 练习
1. 某一叉树共有399个结点,其中有199个度为2的结点,则该二叉树中的叶子结点数为( )
2. 在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
3. 一个具有767个节点的完全二叉树,其叶子节点个数为( )
4. 一棵完全二叉树的节点数为531个,那么这棵树的高度为( )
四、二叉树的存储结构
4.1 顺序存储
4.2 链式存储
五、二叉树的基本操作
5.1 二叉树的遍历
先序遍历
中序遍历
后序遍历
层序遍历
5.2 其他基本操作
5.2.1 获取树中节点的个数
5.2.2 获取叶子节点的个数
5.2.3 获取第k层节点的个数
5.2.4 获取二叉树的高度
5.2.5 检测二叉树中是否含有指定值为val的元素
六、二叉树相关面试题
6.1 判断两棵树是否相同
6.2 判断一棵二叉树是不是另一棵树的子树
6.3 翻转一棵二叉树
6.4 轴对称二叉树
6.5 平衡二叉树
6.6 将二叉搜索树转换成有序的双向链表并输出中序遍历的结果
6.7 由字符串构建二叉树并遍历
6.8 根据二叉树创建字符串
6.9 判断一棵二叉树是不是完全二叉树
6.10 找二叉树两个指定节点的最近公共祖先
6.11 根据前序和中序遍历构建二叉树
6.12 根据中序和后序遍历构建二叉树
6.13 非递归实现三种二叉树遍历方法
先序遍历
中序遍历
后序遍历
6.14 对二叉树进行层序遍历并且把每一层以一组的形式打印,返回值是二维链表
一、引言
二叉树是计算机科学中最基础且重要的数据结构之一,不仅是许多高级数据结构(如AVL树、红黑树、堆等)的基础,也是面试中频繁考察的知识点。本文将系统性地介绍二叉树的核心概念、特性、操作方式以及常见面试题,帮助读者从零开始构建对二叉树的完整理解。
二、树的基本概念
2.1 树的定义和特点
树是一种非线性的数据结构,由 n(n>=0)个有限节点组成的一个具有层次关系的集合。
它看起来像一个倒挂的树,根在上叶子朝下。
树具有以下特点:
-
树是递归定义的,子树之间不能有交集
-
除了根节点外,每个节点有且仅有一个父节点
-
一棵 N 个结点的树共有 N-1 条边
2.2 常见术语
-
结点的度:一个结点含有子树的个数
-
树的度:一棵树中所有结点度的最大值
-
叶子结点/终端结点:度为 0 的结点
-
双亲结点/父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点
-
孩子结点/子结点:一个结点含有的子树的根结点
-
根结点:没有双亲结点的结点
-
结点的层次:从根开始定义,根为第 1 层,依次类推
-
树的高度/深度:树中结点的最大层次
-
兄弟结点:具有相同父结点的结点
-
堂兄弟结点:双亲在同一层的结点
-
结点的祖先:从根到该结点所经分支上的所有结点
-
子孙:以某结点为根的子树中任一结点
2.3 树的表示方法
树有多种表示方法:孩子表示法、孩子双亲表示法、孩子兄弟表示法,其中最常用的是孩子兄弟表示法。
本文采用孩子表示法:
class TreeNode {public char val;public TreeNode left; // 左孩子public TreeNode right; // 右孩子
}
三、二叉树的核心概念
3.1 二叉树的定义
二叉树是结点的一个有限集合,该集合要么为空要么由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
特点是:
-
二叉树中不存在度大于2的结点,即每一个节点都有两棵子树
-
二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
3.2 两种特殊的二叉树
满二叉树
一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。
也就是说,如果一棵二叉树的层数为 K,且结点总数是 2ᴷ-1 ,则它就是满二叉树。
满二叉树的树形如下:
图中的二叉树层数是3,结点总数为 7 = 2³ - 1,满足要求,这棵树就是满二叉树。
完全二叉树
完全二叉树是由满二叉树引出的。
对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 0 至 n-1 的结点一一对应时,称之为完全二叉树。
即满足节点数按照从上到下从左到右的顺序依次编号,如图:
如果树形是下图,就不是完全二叉树:
注意:满二叉树是一种特殊的完全二叉树。
3.3 二叉树的性质
-
若规定根结点的层数为1,则一棵非空二叉树的第 i 层上最多有 2ⁱ⁻¹ 个结点 (i>0)
-
若规定只有根结点的二叉树的深度为1,则深度为 K 的二叉树的最大结点数是2ᴷ - 1(K>0)
-
对任何一棵二叉树,如果其叶结点个数为n₀,度为 2 的非叶结点个数为n₂,则有n₀ = n₂ + 1
-
具有 n 个结点的完全二叉树的深度 k 为 log₂(n+1) 上取整
-
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则:
-
若 i>0,双亲序号:(i-1)/2;i=0,i为根结点编号,无双亲结点
-
若 2i+1<n,左孩子序号:2i+1,否则无左孩子
-
若 2i+2<n,右孩子序号:2i+2,否则无右孩子
-
3.3.1 练习
1. 某一叉树共有399个结点,其中有199个度为2的结点,则该二叉树中的叶子结点数为( )
答案:B.200
解析:根据性质3,n₀ = n₂ + 1 = 199 + 1 = 200
2. 在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
答案:A.n
解析:
- 完全二叉树中,若结点数为偶数,则度为1的结点个数 n₁ 为1
- 节点总数为 2n = n₀ + n₁ + n₂,又由性质三有n₀ = n₂ + 1,得到式子 2n = n₀ + n₁ + n₀ - 1 (其中n₁ 为1) ,得到 2n = n₀ + 1 + n₀ - 1 => 2n = 2*n₀
- 化简可得 n₀ = n,故选A
3. 一个具有767个节点的完全二叉树,其叶子节点个数为( )
答案:B.384
解析:
- 设叶子结点数为n₀,度为1的结点数为n₁,度为2的结点数为n₂。则n₀+n₁+n₂=767
- 又有 n₀=n₂+1,代入得2n₂+1+n₁=767
- 该完全二叉树结点数为奇数个,则度为1的结点个数 n₁ 为0
- 故 2n₂ + 1 + 0 =767,即 766 = 2*(n₀ - 1),化简的 n₀ = 384,故选B
4. 一棵完全二叉树的节点数为531个,那么这棵树的高度为( )
答案:B.10
解析:根据性质4,高度k=⌈log₂(531+1)⌉=⌈log₂532⌉。2⁹=512<532<1024=2¹⁰,所以k=10
四、二叉树的存储结构
4.1 顺序存储
对于完全二叉树,可以使用数组来表示,且不会浪费空间。
顺序存储使用数组来存储二叉树,特别适用于完全二叉树。
-
根节点存储在索引0处
-
对于索引为 i 的节点:
-
左孩子节点索引:2*i+1
-
右孩子节点索引:2*i+2
-
父节点索引:(i-1)/2
-
4.2 链式存储
链式存储通过节点之间的引用关系来表示二叉树,常见的表示方式是孩子表示法:
class TreeNode {public int val; // 数据域public TreeNode left; // 左孩子的引用public TreeNode right; // 右孩子的引用
}
五、二叉树的基本操作
5.1 二叉树的遍历
遍历是二叉树最重要的操作之一,是按照某种规则依次访问二叉树中所有节点的过程。
二叉树常用的遍历方法有四种:
- 先序遍历
- 中序遍历
- 后序遍历
- 层序遍历
先序遍历
访问顺序:根节点 → 左子树 → 右子树
public void preOrder(TreeNode root) {if (root == null) return;System.out.print(root.val + " "); // 访问根节点preOrder(root.left); // 遍历左子树preOrder(root.right); // 遍历右子树
}
中序遍历
访问顺序:左子树 → 根节点 → 右子树
public void inOrder(TreeNode root) {if (root == null) return;inOrder(root.left); // 遍历左子树System.out.print(root.val + " "); // 访问根节点inOrder(root.right); // 遍历右子树
}
后序遍历
访问顺序:左子树 → 右子树 → 根节点
public void postOrder(TreeNode root) {if (root == null) return;postOrder(root.left); // 遍历左子树postOrder(root.right); // 遍历右子树System.out.print(root.val + " "); // 访问根节点
}
层序遍历
从上到下、从左到右逐层访问节点
实现思路如下:
- 使用队列
- 若该二叉树的根节点不是空节点就入队列
- 然后当队列不为空时,定义一个引用cur让它等于弹出的队头元素,弹出后打印cur的val
- 若cur所指的节点的左右孩子都不为空,就分别入队该节点的左右孩子节点
- 入队完节点的左右孩子节点后,就再弹出队头元素给cur并打印
- 当队列为空时,层序遍历也就完成了
public void levelOrder(TreeNode root) {Queue<TreeNode> queue = new LinkedList<>();if (root == null) return;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.2 其他基本操作
5.2.1 获取树中节点的个数
这里提供两个思路:
- 逐层遍历,定义一个变量进行计数
- 将整个二叉树节点个数问题拆解成子问题:左子树的节点个数 + 右子树的节点个数 + 1(根节点)
我们实现第二种思路如下:
- 我们使用递归来计数,前面说过,二叉树是递归定义的,每一个节点都是自身子树的根节点,因此我们把每一个节点都看成一棵子树
- 子树有三个元素:根、子树的左子树和子树的右子树,我们只需要一直递归左右子树再加上1(根)即可
- 当递归到空树(即根节点为空)时,就回溯,那么这样每一棵子树都会遍历到
具体实现如下:
public int size(TreeNode root) {if (root == null) return 0;// 递归计算return size(root.left) + size(root.right) + 1;
}
5.2.2 获取叶子节点的个数
这里同样提供两种思路:
- 遍历二叉树计数
- 拆解成子问题:整棵树的叶子节点个数 = 左子树的叶子节点数 + 右子树的叶子节点数
我们这里采用第二种思路(即子问题)来实现:
public int leafSize(TreeNode root) {if (root == null) return 0;if (root.left==null && root.right==null) // 叶子节点return 1;return leafSize(root.left) + leafSize(root.right);
}
5.2.3 获取第k层节点的个数
我们使用递归的方法来求:
- 我们从根节点开始查找左右子树,每一次查找都传入参数k-1
- 当k等于1时,就表示到了我们的目标层,此时返回1
- 返回到根结点时,结果为左右子树的返回值之和
如图:
具体实现如下:
public int KthLevelSize(TreeNode root, int k) {if (root == null) return 0;// 当到达目标层时回溯if (k == 1) return 1;// 返回左右子树结果之和return KthLevelSize(root.left,k-1) + KthLevelSize(root.right,k-1);
}
5.2.4 获取二叉树的高度
二叉树的高度根据概念应该是左右子树高度的最大值再加上1,通过递归得到每一个根的左右子树的高度然后求最大值再加1即可:
具体实现如下:
public int getHeight(TreeNode root) {// 当节点为空时,回溯if (root == null) return 0;// 求出每个根结点左右子树的高度int leftHeight = getHeight(root.left);int rightHeight = getHeight(root.right);// 返回左右子树最大高度+1return Math.max(leftHeight,rightHeight) + 1;
}
5.2.5 检测二叉树中是否含有指定值为val的元素
算法实现思路:
- 先检查根结点的值,若根结点的值就是对应值val,就返回根结点
- 再分别检查左右子树的值,若左右子树的返回值不为空,就返回该节点;若为空,就返回空
具体实现如下:
public TreeNode findVal(TreeNode root, char val) {if (root == null) return null;// 若根节点的值是对应值val,就返回根结点if (root.val == val) return root;// 若根结点不是,检查左右子树的返回值TreeNode leftTree = findVal(root.left,val);if (leftTree != null) return leftTree;TreeNode rightTree = findVal(root.right,val);if (rightTree != null) return rightTree;// 若都不是,返回nullreturn null;
}
六、二叉树相关面试题
6.1 判断两棵树是否相同
题目链接
算法实现思路:
- 同时遍历两棵二叉树(根结点分别是p和q),判断每个子树是否相等
- 先判断结构是否一样:
若p或者q其中一个为空,则结构不一样
若p和q都是空,则认为两棵树结构一样且都是空树,此时直接返回true
若p和q结果相同且都不为空,则认为两棵树结构相同,接下来判断值是否相同 - 若值相同,接下来通过递归判断当前节点的左右子树是否相同
具体实现如下:
public boolean isSame(TreeNode p,TreeNode q) {// 判断两棵树结构是否相同if (p!=null && q==null|| p==null && q!=null)return false;// 若结果相同且两棵树根结点都为空,返回trueif (p==null && q==null)return true;// 若结构相同且都不为空,判断值是否相同if (p.val != q.val)return false;// 若结构相同值也相同,递归判断左右子树是否相同return isSame(p.left,q.left) && isSame(p.right,q.right);
}
时间复杂度(p树有m个节点,q树有n个节点):O(min(m,n))
分析:节点个数不同的话结构就不同,同时遍历两棵树,一旦遇到结构不同就直接返回false并结束程序,这取决于哪棵树的节点个数更少,因此复杂度是两棵树节点个数的最小值
6.2 判断一棵二叉树是不是另一棵树的子树
题目链接
算法实现思路:
- 判断subRoot树是不是root树的子树时,从root树的根结点开始判断,将树的根节点逐一与subRoot树进行比较是否相等,再比较左子树和右子树,root树的每一个节点都按照这个顺序进行判断
- 共有三种情况:
- 情况一、subRoot树的结构与root树的结构相等且值也相等,但是subRoot树缺少后代节点,这种情况返回false;
- 情况二、subRoot树和root树的结构和值都相等,这种情况返回true;
- 情况三、subRoot树和root树结构和值从根节点开始就完全一样(或者subRoot树和root树为双胞胎),这种情况也是返回true
具体实现如下:
public boolean isSubtree(TreeNode root,TreeNode subRoot) {// 若root树为空,subRoot树肯定不是root树的一个子树if (root == null)return false;// 若两颗二叉树结构相同值也相同,就返回trueif (isSame(root,subRoot))return true;// 若root树的左子树与subRoot相同,返回trueif (isSubtree(root.left,subRoot))return true;// 若root树的右子树与subRoot相同,返回trueif (isSubtree(root.right,subRoot))return true;// 若根、左右子树都不相同,返回falsereturn false;
}
时间复杂度:O(r*s):root树共有r个节点,subroot树共有s个节点
6.3 翻转一棵二叉树
题目链接
算法实现思路:
- 前序遍历二叉树,把每一个节点都遍历到,把每一个节点的左右孩子地址交换
- 首先判断根节点是否为空,然后判断节点的左右孩子是否为空
- 若都不为空,就将左孩子和右孩子的地址交换
- 交换完毕后,再依次递归左子树和右子树
- 最后返回节点即可
具体实现如下:
public TreeNode invertTree(TreeNode root) {if (root == null) return null;if (root.left==null && root.right==null)return root;// 交换左右孩子节点的地址TreeNode temp = root.left;root.left = root.right;root.right = temp;// 依次递归遍历左子树和右子树invertTree(root.left);invertTree(root.right);return root;
}
6.4 轴对称二叉树
题目链接
算法实现思路:
- 判断一棵二叉树是否轴对称,即判断二叉树根节点的左子树和右子树是否轴对称并且值也对称
- 判断左子树的左孩子节点是否与右子树的右孩子节点相等(并且值是否相等),同时要满足左子树的右孩子节点与右子树的左孩子节点结构和值是否相等。若两者都相等,则认为该树是一棵轴对称二叉树
- 判断方法为判断结构和值是否相等 (即判断二叉树是否相等)
具体实现如下:
public boolean isSymmetric(TreeNode root) {if (root == null)return true;return isSymmetricChild(root.left,root.right);
}public boolean isSymmetricChild(TreeNode leftTree, TreeNode rightTree) {// 结构不相同if ((leftTree!=null && rightTree==null)|| (leftTree==null && rightTree!=null)) {return false;}// 结构相同但是都为空if (leftTree==null && rightTree==null) {return true;}// 结构相同但值不相同if (leftTree.val != rightTree.val) {return false;}// 结构相同且值也相同return isSymmetricChild(leftTree.right,rightTree.left)&& isSymmetricChild(leftTree.left,rightTree.right);
}
6.5 平衡二叉树
题目链接
平衡二叉树是一种左右子树的高度差不超过1的二叉树
算法实现思路
- 前序遍历整棵二叉树,求出每个节点的左右子树的高度,再求它们的高度差绝对值h,判断h是否小于等于1,若是则返回true,否(h>=2 )则返回false
- 如果二叉树根节点是平衡的并且根节点的左子树和右子树都是平衡的,则认为整棵二叉树都是平衡的
具体实现如下:
public boolean isBalance(TreeNode root) {if (root == null) return true;// 获取左右子树的高度并求出差值int leftHeight = getHeight(root.left);int rightHeight = getHeight(root.right);int h = Math.abs(leftHeight - rightHeight);return (h <= 1) && isBalance(root.left)&& isBalance(root.right);
}public int getHeight(TreeNode root) {if (root == null) return 0;int leftHeight = getHeight(root.left);int rightHeight = getHeight(root.right);return Math.max(leftHeight,rightHeight) + 1;
}
时间复杂度:O(N²)
分析:重复大量求节点的高度,导致效率减慢
解决方法:在每次求高度时判断左右子树高度的差值绝对值是否满足<2,若满足就返回左右子树最大值+1,否则就返回-1
优化后的算法:
public boolean isBalancedTree(TreeNode root) {if (root == null)return true;if (getHeightOfTree(root) > 0)return true;elsereturn false;
}public int getHeightOfTree(TreeNode root) {if (root == null)return 0;int leftH = getHeightOfTree(root.left);// 若左子树的高度为负数,整棵树不可能是平衡二叉树,返回-1if (leftH < 0)return -1;int rightH = getHeightOfTree(root.right);// 当右子树的高度不为负数且左右子树高度差不超过1,认为是平衡二叉树if (rightH >= 0 && (Math.abs(leftH-rightH) <= 1))return Math.max(leftH,rightH) + 1;elsereturn -1;
}
时间复杂度:O(N)
6.6 将二叉搜索树转换成有序的双向链表并输出中序遍历的结果
题目链接
二叉搜索树:每个左孩子结点都比子树根节点小,右孩子节点都比子树根节点大。
中序遍历得到的结果是有序的。
算法实现思路:
- 中序遍历二叉树,定义一个引用prev记录每一个子树根节点的前一个结点,用于改变left和right的指向
- 当遍历到最底层的孩子节点的时候,此时的root指向的就是该节点,让该节点的left指向prev,并且让prev指向该节点,由于此时prev初始为空,故不对prev的right进行改变
- 当回溯到倒数第二层的时候,此时root指向的是上一个节点的双亲结点,root的left依然指向prev(此时prev指向双亲结点),这时候就要使prev的right指向当前根节点了,然后再让prev往后走指向root
- 依次回溯改变每一个节点的left和right
- 当二叉树排序完成后,让二叉树原来的根节点往左走并判断什么时候节点的left为空,就停下,该节点就是排序后的的首节点,返回该节点即可
具体实现如下:
TreeNode prev = null;
public TreeNode Convert(TreeNode pRootOfTree) {if (pRootOfTree == null)return pRootOfTree;ConvertChildNode(pRootOfTree);if (pRootOfTree.left != null)pRootOfTree = pRootOfTree.left;return pRootOfTree;
}public void ConvertChildNode(TreeNode root) {// 中序遍历if (root == null)return;ConvertChildNode(root.left);root.left = prev;if (prev != null)prev.right = root;prev = root;ConvertChildNode(root.right);
}
6.7 由字符串构建二叉树并遍历
题目链接
将输入的字符串(前序遍历)转为一棵二叉树,然后中序遍历该二叉树并输出结果
算法实现思路:
- 根据前序遍历的顺序遍历字符串,若不为#则实例化成节点
- 然后再通过递归构建左子树和右子树;否则继续遍历,最后返回节点
- 注意遍历时的下标变量不能是局部的
具体实现如下:
public static int i = 0;
// 构建二叉树
public static TreeNode createTreeByStr(String str) {if (str == null)return null;TreeNode root = null;if (str.charAt(i) != '#') {// 实例化成结点root = new TreeNode(str.charAt(i++));// 构建左右子树root.left = createTreeByStr(str);root.right = createTreeByStr(str);} else {i++;}return root;
}// 中序遍历
public void inOrder(TreeNode root) {if (root == null) return;inOrder(root.left); // 遍历左子树System.out.print(root.val + " "); // 访问根节点inOrder(root.right); // 遍历右子树
}
6.8 根据二叉树创建字符串
题目链接
算法实现思路:
- 首先判断root是否为空,若不为空就把值添加到字符串
- 遍历左子树时,若左子树不为空,就添加一个左括号。然后再递归该节点的左子树,等递归完后回退后,添加右括号。若左子树为空且右子树不为空,就添加一个括号“()”
- 然后遍历右子树,若右子树不为空,添加左括号、递归右子树再添加右括号;若为空,直接返回
具体实现如下:
public String tree2str(TreeNode root) {StringBuilder stringBuilder = new StringBuilder();tree2strChild(root,stringBuilder);return stringBuilder.toString();
}public void tree2strChild(TreeNode root, StringBuilder stringBuilder) {if (root == null)return;// 添加根节点的值stringBuilder.append(root.val);if (root.left != null) {stringBuilder.append("(");tree2strChild(root.left,stringBuilder); // 递归左子树stringBuilder.append(")");} else {if (root.right != null)stringBuilder.append("()");elsereturn;}if (root.right != null) {stringBuilder.append("(");tree2strChild(root.right,stringBuilder); // 递归右子树stringBuilder.append(")");} else {return;}
}
6.9 判断一棵二叉树是不是完全二叉树
题目链接
算法实现思路:
- 使用队列,若二叉树的根结点不为空,就入队列
- 定义一个引用cur让它等于弹出的队头元素,若队列不为空且cur拿到的不为空节点,就持续循环弹出元素
- 分别入队该节点的左右孩子节点,然后再弹出
- 当cur拿到的节点为空时,跳出循环并检查队列中的节点是否都是空节点,若是则认为该二叉树是完全二叉树,否则不是完全二叉树
具体实现如下:
public boolean isCompleteTree (TreeNode root) {if (root == null)return true;Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {// 让cur拿到队头元素TreeNode cur = queue.poll();if (cur != null) {// 队头元素不为空时入队该节点的左右子树queue.offer(cur.left);queue.offer(cur.right);} else {// 若队头元素为空就结束循环break;}}// 检查队列中的元素是否都是空结点while (!queue.isEmpty()) {if (queue.peek() != null) {return false;}queue.poll();}return true;
}
6.10 找二叉树两个指定节点的最近公共祖先
题目链接
算法实现思路:
- 先判断p或者q是否是根结点root;若不是再判断是不是分别在左右子树中;然后判断是不是都在左子树或者都在右子树中
- 共有四种情况
- 若根结点是空就返回空
- 情况一:判断如果p是根结点或者q是根结点,公共祖先就是根结点,直接返回该节点
- 接着递归左子树和右子树,并接收返回值。
- 情况二:此时若leftTree和rightTree都不为空,最近的公共祖先就是它们的根节点,返回该根节点即可
- 情况三若leftTree不为空,说明左子树中有p和q,公共祖先是左子树的根结点,返回该节点即可;相反(即情况四)同理
具体实现如下:
public TreeNode lowestCommonAncestor(TreeNode root,TreeNode p, TreeNode q) {// 若根结点是空就返回空if (root == null)return null;// 如果p是根结点或者q是根结点,公共祖先就是根结点if (p==root || q==root)return root;TreeNode leftTree = lowestCommonAncestor(root.left,p,q);TreeNode rightTree = lowestCommonAncestor(root.right,p,q);// 若leftTree和rightTree都不为空,最近的公共祖先就是它们的根节点if (leftTree!=null && rightTree!=null)return root;else if (leftTree != null)return leftTree;elsereturn rightTree;
}
思路二:
可以改成两个节点的相遇点,使用两个栈,分别存储从根结点到两个指定节点路径上的节点;如果没有找到指定节点就出栈。然后找两个栈的公共节点,即先看哪个栈长,让长的弹出lenA-lenB个元素再两个栈一起弹出元素,当两个栈顶元素相同时,该节点就是公共祖先节点
算法实现步骤:
写一个找到指定节点路径的所有节点的方法(getPath),先把路径节点都存储起来
- 若跟节点不为空,就把根结点压入栈并且判断根结点是否是指定节点之一,若是返回true
- 然后递归根结点的左子树和右子树,并判断返回值是否为true(true代表找到了,否则代表未找到),若没找到,就弹出该节点,表示该节点不是指定节点路径上的节点,然后返回false
然后找两个路径的交点即可
- 先求两个栈的大小,然后让长的先弹出差值数个元素
- 然后在两个栈都不为空的情况下让两个栈的栈顶元素相比较是否相等,若相等,返回该栈顶元素;否则让两个栈都一起弹出栈顶元素。
- 若以上都没有返回,认为没有找到祖先节点,返回null
public TreeNode lowestCommonAncestor2(TreeNode root,TreeNode p, TreeNode q) {if (root == null)return null;// 构建两个存储从根节点到指定节点的路径的栈Stack<TreeNode> stackP = new Stack<>();Stack<TreeNode> stackQ = new Stack<>();getPath(root,p,stackP);getPath(root,q,stackQ);// 获取两个栈的长度int sizeP = stackP.size();int sizeQ = stackQ.size();if (sizeP > sizeQ) {int size = sizeP - sizeQ;while (size-- != 0)stackP.pop();} else {int size = sizeQ - sizeP;// 让长的栈先弹出差值数个元素while (size-- != 0)stackQ.pop();}// 找到两个栈的交点while (!stackP.isEmpty()&& !stackQ.isEmpty()) {// 两个栈的栈顶元素相同,该节点就是公共祖先if (stackP.peek() == stackQ.peek())return stackP.pop();else {// 否则就继续同时弹出元素stackP.pop();stackQ.pop();}}// 若找不到就返回nullreturn null;
}// 用于构建存储路径的栈的方法
public boolean getPath(TreeNode root, TreeNode node, Stack<TreeNode> stack) {if (root == null)return false;// 若要找到节点是根节点,就直接入栈根节点stack.push(root);if (node == root)return true;// 递归查找左右子树boolean ret = getPath(root.left,node,stack);if (ret)return true;ret = getPath(root.right,node,stack);if (ret)return true;// 若没有找到指定节点,说明树中不存在stack.pop();return false;
}
6.11 根据前序和中序遍历构建二叉树
题目链接
根据中序遍历和前/后序遍历结果推出二叉树的过程如图:
算法实现思路:
- 根据先序遍历找到根节点
- 在中序遍历结果中找到根的位置
- 再根据中序遍历递归找到左右子树的范围,然后确定左子树和右子树
算法实现步骤:
- 由于仅根据两个遍历的数组不能通过递归构建二叉树,我们写一个有下标参数的方法,它的参数分别是:先序遍历数组,先序遍历的根节点下标,中序遍历数组,中序遍历第一个节点的下标,中序遍历最后节点的下标
- 首先 根据先序遍历数组和其根节点下标创建二叉树的根节点
- 为了在中序遍历数组中找到根节点,我们再写一个能够根据传入的范围找对应值的方法,参数传入 中序遍历数组、查找的范围(第一个和最后一个的下标)、要查找的值
- 然后递归构建该根节点的左子树和右子树,传入对应子树范围的参数即可
- 当全部构建完成后,返回根节点
- 注意,当下标不合法时(如第一个下标大于最后下标),表示节点没有子树,返回空即可
- 注意,由于我们需要全程使用先序遍历确定子树根节点,所以遍历先序数组的下标应设置为全局变量,因而在写方法时,应把参数列表中的先序数组根节点下标删去
具体实现如下:
public int preIndex;public TreeNode buildTreeByPreAndInOrder(char[] preorder, char[] inorder) {return buildTreeChildPAI(preorder,inorder,0,inorder.length-1);
}public TreeNode buildTreeChildPAI(char[] preorder, char[] inorder,int inBegin, int inEnd) {// 这种情况表示该节点没有子树if (inBegin > inEnd)return null;// 先构建根节点TreeNode root = new TreeNode(preorder[preIndex]);// 再在中序数组中根据范围找到根节点的下标位置int rootIndex = findVal(inorder,inBegin,inEnd,preorder[preIndex]);preIndex++;// 然后构建左子树和右子树root.left = buildTreeChildPAI(preorder,inorder,inBegin,rootIndex-1);root.right = buildTreeChildPAI(preorder,inorder,rootIndex+1,inEnd);// 当全部构建完成,返回根节点return root;
}
6.12 根据中序和后序遍历构建二叉树
题目链接
算法实现思路:
- 根据后序遍历找到根节点
- 在中序遍历结果中找到根的位置
- 再根据中序遍历递归找到左右子树的范围,然后确定左子树和右子树
- 需要注意:按照后序遍历的顺序从后往前创建树,即先创建根,再创建右子树,最后创建左子树
算法实现步骤:
- 根据后序数组找到根节点并构建
- 然后在中序数组中找到根节点的对应下标
- 先递归构建右子树然后递归左子树
- 最后返回根节点即可
具体实现如下:
public int postIndex;public TreeNode buildTreeByInAndPostOrder(char[] inorder, char[] postorder) {postIndex = postorder.length-1;return buildTreeChildIAP(inorder,postorder,0,inorder.length-1);
}public TreeNode buildTreeChildIAP(char[] inorder, char[] postorder,int inBegin, int inEnd) {// 这种情况表示该节点没有子树if (inBegin > inEnd)return null;// 先构建根节点TreeNode root = new TreeNode(postorder[postIndex]);// 再在中序数组中根据范围找到根节点的下标位置int rootIndex = findVal(inorder,inBegin,inEnd,postorder[postIndex]);postIndex--;// 然后构建右子树和左子树root.right = buildTreeChildIAP(inorder,postorder,rootIndex+1,inEnd);root.left = buildTreeChildIAP(inorder,postorder,inBegin,rootIndex-1);// 当全部构建完成,返回根节点return root;
}
6.13 非递归实现三种二叉树遍历方法
算法实现思路:
- 用一个栈存储节点,然后让引用cur遍历二叉树左子树并打印(中、后序不打印)
- 当cur拿到的值为空,弹出栈顶元素然后让cur遍历节点的右子树(中序思路:在遍历右子树之前打印节点值)
- 若右子树为空就再弹出元素(后序思路:先获取栈顶元素,并看看右子树:若右子树为空再打印并弹出);若不为空就遍历右子树(后序思路:当右子树不为空时,遍历右子树。但是:会出现死循环;解决方法:让引用prev指向已经打印过的节点,当右子树为空或者右节点已被打印过,就打印并弹出栈顶元素,然后让prev指向该节点)
- 当栈为空时,遍历就完成了
先序遍历
算法实现步骤:
- 遍历二叉树,用一个引用遍历二叉树节点,若cur当前节点不为空,入栈并打印。然后让cur一直往左遍历并入栈
- 当cur所拿到的为空时,结束遍历并弹出栈顶元素,然后根据该元素遍历右子树,若不为空就入栈并打印
- 但是若栈顶元素的右子树不为空时该如何入栈呢?总不能再写一遍前面已写的代码吧?我们想到用一个外循环,其条件是栈不为空就循环,又因为第一次根节点入栈前栈就是空的导致无法进入循环:因此加上cur的值不为空这个条件,两个条件是或的关系,满足其中之一即可进入循环
具体实现如下:
public void preOrderNoRe(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;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;}
}
中序遍历
public void inOrderNoRe(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;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;}
}
后序遍历
public void postOrderNoRe(TreeNode root) {Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;TreeNode prev = null;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;}}
}
6.14 对二叉树进行层序遍历并且把每一层以一组的形式打印,返回值是二维数组
题目链接
算法实现思路:
- 在上一个算法的基础上:弹出队头元素之前先统计一下队列中有多少个元素size并申请一个空链表,然后接下来的步骤在size次循环中操作。每次循环都重新申请一个链表并将节点放入连表,然后入队左右孩子节点,size自减1
- 当队列中元素都弹出,size为0,就停止操作,检查队列中的元素个数并赋值给size
- 当队列为空,层序遍历完成
具体实现如下:
public List<List<Character>> levelOrder(TreeNode root) {List<List<Character>> list = new ArrayList<>();if (root == null)return list;Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {int size = queue.size();List<Character> ret = new ArrayList<>();while (size-- != 0) {TreeNode cur = queue.poll();ret.add(cur.val);if (cur.left != null) {queue.offer(cur.left);}if (cur.right != null) {queue.offer(cur.right);}}list.add(ret);}return list;
}
至此,本文任务已达成,希望读者能够掌握好二叉树的知识并灵活运用 ~
完