二叉树
1. 遍历二叉树
1.1 递归方式
递归遍历二叉树非常简单,而且三种遍历方式的代码也非常相同,是因为在这三种方式中,节点被访问的先后顺序是完全一致的。如下图所示
- 如果第一次访问就打印,则为先序遍历
- 如果第二次访问打印,则为中序遍历
- 如果第三次访问打印,则为后序遍历
//递归遍历二叉树
public class Solution_SearchRecursively {
public static class TreeNode{
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val = val;
}
}
//先序遍历
public static void preSearch(TreeNode root){
if(root == null){
return ;
}
System.out.print(root.val + " ");
preSearch(root.left);
preSearch(root.right);
}
//中序遍历
public static void midSearch(TreeNode root){
if(root == null){
return ;
}
midSearch(root.left);
System.out.print(root.val + " ");
midSearch(root.right);
}
//后序遍历
public static void postSearch(TreeNode root){
if(root == null){
return ;
}
postSearch(root.left);
postSearch(root.right);
System.out.print(root.val + " ");
}
}
1.2 非递归方式
二叉树的非递归方式遍历,需要借助栈来实现。
- 先序遍历:先将根节点入栈,【弹出栈顶元素,栈顶节点的右孩子入栈之后,再将左孩子入栈】,循环…
栈顶元素的弹出顺序即为先序遍历的结果。 - 后序遍历:先将根节点入栈A,【弹出A的栈顶元素T到栈B,栈顶节点T的左后,再将右孩子入栈】,循环…
最后,将栈B中的元素一次性弹出,弹出顺序即为后续遍历的结果。 - 中序遍历:先将根节点入栈,【从根节点一直向左,遇到左孩子即入栈,没有左孩子后,弹出栈顶元素,并将该栈顶的右孩子入栈】,循环…
import java.util.Stack;
//以非递归的方式遍历二叉树
public class Solution_SearchUnRecursive {
public static class TreeNode{
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val = val;
}
}
//先序非递归遍历二叉树
public static void preSearch(TreeNode root){
if(root != null){
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
stack.push(cur);
while(!stack.isEmpty()){
//先将右孩子入栈,后将左孩子入栈。
cur = stack.pop();
System.out.print(cur.val + " ");
if(cur.right != null){
stack.push(cur.right);
}
if(cur.left != null){
stack.push(cur.left);
}
}
System.out.println();
}
}
//中序非递归遍历二叉树
public static void midSearch(TreeNode root){
if(root != null){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode cur = root.left;
while(cur != null || !stack.isEmpty()){
if(cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
System.out.print(cur.val + " ");
cur = cur.right;
}
}
}
}
//后序非递归遍历二叉树
public static void postSearch(TreeNode root){
if(root != null){
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> stackPrint = new Stack<>();
TreeNode cur = root;
stack.push(cur);
while(!stack.isEmpty()){
cur = stack.pop();
stackPrint.push(cur);
if(cur.left != null){
stack.push(cur.left);
}
if(cur.right != null){
stack.push(cur.right);
}
}
while(!stackPrint.isEmpty()){
System.out.print(stackPrint.pop() + " ");
}
}
}
}
2. 中序后继
2.1 树节点结构中没有父指针
题目:设计一个算法,找出二叉搜索树中指定节点的“下一个”节点(也即中序后继)。
如果指定节点没有对应的“下一个”节点,则返回
null
。
步骤:添加一个辅助节点,用来记录中序遍历过程中弹出的节点,如果该节点的值与指定节点的值相等,则下一个要弹出的节点即为所求。
public static TreeNode inOrderSuccessor(TreeNode root, TreeNode p){
if(root != null){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode cur = root.left;
TreeNode last = null;
while(cur != null || !stack.isEmpty()){
if(cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
if(last != null && last.val == p.val){
return cur;
}
last = cur;
cur = cur.right;
}
}
}
return null;
}
2.2 树节点结构中有父指针
题目: 现在有一种新的二叉树节点类型如下:
public class TreeNode { public int value; public TreeNode left; public TreeNode right; public TreeNode parent; public TreeNode(int data) { this.value = data; } }
该结构比普通二叉树节点结构多了一个指向父节点的parent指针。假设有一棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向自己的父节点,头节点的parent指向null。
求指定节点的后继节点。
思路:
- 如果该指定节点有右子树,其后继节点为其右子树上的最左节点
- 如果该指定节点没有右子树,则找该指定节点的父节点,如果其是其父节点的右孩子,继续向上找父节点,直至该节点是某一父节点的左子树中的节点。符合这个条件的父节点为该指定节点的后继节点。
//找某个节点的后继,树节点有父指针
public class Solution_InOrderSuccessor2 {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode parent;
TreeNode(int x) {
val = x;
}
}
public static TreeNode inOrderSuccessor(TreeNode node){
if(node == null){
return null;
}
if(node.right != null){
//找右子树上的最左节点
node = node.right;
while(node.left != null){
node = node.left;
}
return node;
}else{
TreeNode parent = node.parent;
while(parent != null && parent.left != node){
node = parent;
parent = node.parent;
}
return parent;
}
}
}
3. 平衡二叉树
题目:判断一棵二叉树是否是平衡二叉树
平衡二叉树: 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
思路:平衡二叉树的定义可以有另外一种说法:二叉树中任一节点的左右子树的高度差都不超过1。
既然是任一节点,那就可以考虑用递归函数来判断是否为平衡二叉树。
在递归函数里,对每一个节点采取的操作都是相同的,操作之后每一个节点的返回值的类型都是一致的。
判断是否为平衡二叉树需要采取的操作:
- 需要判断根节点的左右子树是否都分别为平衡二叉树
- 如果都为平衡二叉树,还需要计算左右子树的高度,是否超过1。
于是递归函数的返回值就是以该节点为根节点的子树是否为平衡二叉树,以及该子树的高度。由于需要返回两个值,可以创建一个自定义的类型来封装这两个数据。
综上,递归函数就能按照上述分析过程写出来了。
public class Solution_IsBalance {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
public static boolean isBalance(TreeNode root){
return recursive(root).balance;
}
public static class Result{
boolean balance;
int height;
public Result(boolean balance, int height){
this.balance = balance;
this.height = height;
}
}
public static Result recursive(TreeNode root){
if(root == null){
return new Result(true, 0);
}
Result left = recursive(root.left);
if(!left.balance){
return new Result(false, 0);
}
Result right = recursive(root.right);
if(!right.balance){
return new Result(false, 0);
}
if(Math.abs(left.height - right.height) > 1){
return new Result(false, 0);
}else{
return new Result(true, Math.max(left.height, right.height) + 1);
}
}
}
4. 二叉树的递归套路
在一个递归过程中,我们只需要关心三个问题
- 整个递归的终止条件。
- 一级递归需要做什么?
- 返回给上一级的返回值是什么?
因此,也就有了我们解递归题的三部曲:
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?
5. 搜索二叉树
题目:判断一棵树是否为搜索二叉树
搜索二叉树:二叉树中,任一节点比起左子树中所有节点大,比起右子树所有节点小。
错解:使用二叉树的递归套路,只将当前节点的值与左右两个孩子的值比较。示例:[5,10,15,null,null,6,20]。
正解:搜索二叉树的中序遍历得到的序列为递增序列,可以使用非递归版本的中序遍历,当遍历到某个节点时,与上个节点比较。
//在非递归方式的中序遍历的代码基础上,做如下修改,如添加注释的代码行所示
public static boolean isValidBST(TreeNode root){
if(root != null){
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode cur = root.left;
TreeNode pre = null; //---添加语句:记录前一个弹出的节点---
while(cur != null || !stack.isEmpty()){
if(cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
if(pre != null && pre.val >= cur.val){//---添加if语句:与前一个节点比较---
return false; //---添加语句---
} //---添加语句---
pre = cur; //---添加修改pre的代码----
//System.out.print(cur.val + " "); //---删除语句---
cur = cur.right;
}
}
}
return true;
}
6. 完全二叉树
题目:判断一棵二叉树是否为完全二叉树
6.1 解法一:
思路:任何一个节点,其左子树比右子树的高度差要么等于1,要么为0。
根据以上思路,可以写出一个递归函数,函数体中按上述规则比较某节点的左右子树高度差,函数体返回某节点是否符合规则,并返回该节点的高度。
public static class ReturnData{
int height;
boolean isComplete;
public ReturnData(int height, boolean isComplete){
this.height = height;
this.isComplete = isComplete;
}
}
public static boolean isComplete(TreeNode root){
return process(root).isComplete;
}
public static ReturnData process(TreeNode root){
if(root == null){
return new ReturnData(0, true);
}
ReturnData leftData = process(root.left);
if(!leftData.isComplete){
return new ReturnData(-1, false);
}
ReturnData rightData = process(root.right);
if(!rightData.isComplete){
return new ReturnData(-1, false);
}
if(leftData.height < rightData.height || leftData.height - rightData.height > 1){
return new ReturnData(-1, false);
}
return new ReturnData(Math.max(leftData.height, rightData.height) + 1, true);
}
6.2 解法二
思路:二叉树逐层检查,当某节点遇到以下两种情况,直接返回false,遍历完之后没出现下述情况,返回true。
- 其左子树为空,且右子树不为空
- 某节点没有右子树,且其后续节点有孩子
逐层遍历,需要使用一个队列,先把头结点放进队列,当弹出某个节点是,如果其有孩子,入队。
public static boolean isCBT(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
TreeNode cur = null;
boolean leaf = false;//如果某节点没有右孩子,则其后的节点都为叶节点。
if(root != null){
queue.offer(root);
}
while(!queue.isEmpty()){
cur = queue.poll();
if(!leaf){
if(cur.left != null){//左子树不为空,左孩子入栈
queue.offer(cur.left);
}else if(cur.right != null){//某节点左子树为空,右子树不为空
return false;
}
if(cur.right != null){
queue.offer(cur.right);
}else{//如果该节点没有右孩子,则剩下的节点都没有孩子,即都为叶节点
leaf = true;
}
}else{//剩下的节点都为叶节点
if(cur.left != null || cur.right != null){
return false;
}
}
}
return true;
}
7. 完全二叉树的节点个数
题目:已知一棵完全二叉树,求其节点的个数。
要求:时间复杂度低于O(N),N为这棵二叉树节点个数。
思路:要求时间复杂度低于O(N),那就不能用遍历的方法
如果一棵二叉树为满二叉树,且高度为H,则其总结点个数为 2 H − 1 2^H-1 2H−1,第m层的节点个数为 2 m − 1 2^{m-1} 2m−1。
步骤:
- 先求二叉树的高度为H
- 先判断根节点的右子树的高度,如果为
H
−
1
H-1
H−1,则根节点左子树为满二叉树,根节点右子树不一定满。此时根节点的左子树上的节点数为
2
H
−
1
−
1
2^{H-1}-1
2H−1−1个,再加上1个根节点,假设根节点右子树节点个数为
countNodes(root.right)
,则总的节点数为 2 H − 1 2^{H-1} 2H−1+countNodes(root.right)
- 如果根节点的右子树的高度为
H
−
2
H-2
H−2,则右子树一定为满二叉树,左子树不一定满。此时根节点的左子树上的节点数为
2
H
−
2
−
1
2^{H-2}-1
2H−2−1个,再加上1个根节点,假设根节点左子树节点个数为
countNodes(root.left)
,则总的节点数为 2 H − 2 2^{H-2} 2H−2+countNodes(root.left)
- 如果右子树为空,则左子树只有一个节点,总结点数为2。
public class Solution_number_CBT {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
public static int countNodes(TreeNode root){
int result = 0;
if(root == null){
return result;
}
int height = getHeight(root);
if(root.right != null){
if(getHeight(root.right) == height - 1){
//右子树高度为height - 1,则左子树为一棵满二叉树,右子树不一定满
//总节点数为左子树的节点数(2^(height - 1) - 1) + 根节点 + 右子树节点数
result = (int) (countNodes(root.right) + Math.pow(2, height - 1));
}else{
//右子树为一棵满二叉树,左子树不一定满
//总节点数为右子树的节点数(2^(height - 2) - 1) + 根节点 + 左子树节点数
result = (int) (countNodes(root.left) + Math.pow(2, height - 2));
}
}else if(root.left != null){
//一棵完全二叉树,根节点右子树为空,则左子树只有一个节点,总节点数为2
result = 2;
}else{
result = 1;
}
return result;
}
//求二叉树的高度
public static int getHeight(TreeNode root){
int height = 0;
while(root != null){
height++;
root = root.left;
}
return height;
}
}
注:1 << n
与
2
n
2^n
2n等价。
8. 序列化与反序列化
8.1 序列化
按照先序遍历的方式进行序列化,空孩子用“#”表示,节点与节点之间用“_"分隔开。否则如果一个节点值为12
,也有节点值为1
,在序列化后的结果中...12...
不能确定中间的12
是一个节点还是两个节点。
如上,其序列化后为1_2_4_#_#_#_3_#_#_
代码
public static String serialize(TreeNode root){
if(root == null){
return "#_";
}
String res = root.val + "_";
res += serialize(root.left);
res += serialize(root.right);
return res;
}
8.2 反序列化
反序列化,需要先将序列化之后得到的字符串进行处理,然后将其中的每一个节点(包括空节点)存入一个队列;
然后由这个队列采用先序遍历的方式构造二叉树
//反序列化
public static TreeNode deserialize(String res){
String[] str = res.split("_");
Queue<String> queue = new LinkedList<>();
for(String s : str){
queue.offer(s);
}
return preOrder(queue);
}
//根据队列构造二叉树
public static TreeNode preOrder(Queue<String> queue){
if(!queue.isEmpty()){
String temp = queue.poll();
if(temp.equals("#")){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(temp));
root.left = preOrder( queue);
root.right = preOrder(queue);
return root;
}
return null;
}