二叉树
本文所讨论的二叉树节点定义如下:
Class Node {
public int value;
public Node left;
public Node right;
public Node(int value) {
this.value = value;
}
}
二叉树的递归序与三种基础遍历方式
递归序
首先考虑以下代码:
public static void f(Node head) {
// 第1次访问当前节点
if (head == null) {
return;
}
f(head.left);
// 第2次访问当前节点
f(head.right);
// 第3次访问当前节点
}
对于如下的一颗简单二叉树,根据代码中的访问顺序输出节点值(即每次访问到都输出一次)得到它的递归序为:[1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1]。
三种遍历树的方式
根据前面的递归序,我们可以选择在某个特定的访问输出节点值,这就有了三种遍历方式。
先序(前序)遍历
先序遍历即在第1次访问到节点时就输出节点值,后两次不做操作,根据递归序,我们可以知道这棵树的先序遍历为:[1,2,4,4,4,2,5,5,5,2,1,3,6,6,6,3,7,7,7,3,1],即[1,2,4,5,3,6,7]。可以看出,先序遍历的输出顺序是父节点->左子节点->右子节点。
递归实现:
public static void preOrderRecur(Node head) {
// 先序遍历
// 中序遍历和后序遍历只是改变了打印位置,后文将不再赘述
System.out.print(head.value + " ");
if (head == null) {
return;
}
preOrderRecur(head.left);
preOrderRecur(head.right);
}
非递归实现:
// 利用栈,方式如下
// 1 先把根节点压入栈
// 2 弹出栈顶节点v并输出节点值
// 3 如果v.right不为null,压入栈(注意是先压右节点,因为栈先进后出)
// 4 如果v.left不为null,将其压入栈
// 5 重复2-4直至栈为空
public static void preOrderUnRecur(Node head) {
// 先序遍历
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
}
中序遍历
同理,我们在第2次访问到节点时再输出节点值,剩余两次不做操作,得到的就是中序遍历结果,这颗树的中序遍历为:[4,2,5,1,6,3,7],输出顺序为:左子节点->父节点->右子节点。
非递归实现
// 1 将树的整个左边界从上到下压入栈
// 2 弹出栈顶节点v,输出节点值
// 3 如果v.right不为空,将以v.right为根节点的子树的整个左边界从上到下压入栈
// 4 重复2-3直至栈空
// 原理:每次将整个左边界压入栈,则出栈时一定是 左 头 的顺序
// 而在这个过程中又将右子树的整个左边界压入栈了,得到的顺序是:
// 左 头 右(拆分成新的 左 头 右(...))
public static void midOrderUnRecur(Node head) {
if (head != null) {
Stack<Node> s = new Stack<Node>();
while (!s.isEmpty() || head != null) {
if (head != null) {
// 压入左边界
s.push(head);
head = head.left;
} else {
// 输出当前子树根节点的值,切换到右子树
head = s.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
}
后序遍历
还是一样,后序遍历:[4,5,2,6,7,3,1],输出顺序为:左子节点->右子节点->父节点。
非递归实现
// 分别用两个栈s1和s2,用于输出倒序节点,方法如下
// 1 将头节点压入s1
// 2 弹出栈顶节点v,压入s2
// 3 如果v.left不为null,压入s1
// 4 如果v.right不为null,压入s1
// 重复2-4直至s1为空
// 重复弹出s2的栈顶节点并输出节点值直至s2为空
// s1中节点的弹出顺序是 头 右 左,这也是节点在s2的入栈顺序,则s2中节点的弹出顺序是 左 右 头,即后序遍历
public static void backOrderUnRecur(Node head) {
if (head != null) {
Stack<Node> s1 = new Stack<Node>();
Stack<Node> s2 = new Stack<Node>();
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();
s2.push(head);
if (head.left != null) {
s1.push(head.left);
}
if (head.right != null) {
s1.push(head.right);
}
}
while (!s2.isEmpty()) {
System.out.print(s2.pop().value + " ");
}
}
}
深度优先遍历 DFS 和广度优先遍历 BFS
DFS
前文提到的先序遍历其实就是深度优先遍历,在此不再赘述。
BFS
广度优先遍历即一层层地遍历树,实现如下:
public static void BFS(Node head) {
if (head == null) {
return null;
}
Queue<Node> queue = new LinkedList<>();
queue.add(head);
while (!queue.isEmpty()) {
Node cur = queue.poll();
System.out.println(cur.value);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
BFS引申题:求二叉树的最大宽度
此题可以转换为树的层序遍历问题,即把树的每一层作为一个整体单独输出,只需对BFS的代码稍作修改即可,实现如下:
public static void BFS(Node head) {
if (head == null) {
return null;
}
Queue<Node> queue = new LinkedList<>();
queue.add(head);
// 记录最大宽度
int maxWidth = -1;
while (!queue.isEmpty()) {
// 当前层的宽度
int size = queue.size();
maxWidth = Math.max(size, maxWidth);
// 每次遍历一层
for (int i = 0; i < size; ++i) {
Node cur = queue.poll();
System.out.println(cur.value);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
}
经典例题
1.如何判断一棵二叉树是否是搜索二叉树?
搜索二叉树:对于任意一个节点,它的左子树的节点值均比其小,右子树的节点值均比其大,不考虑重复值,下图是一个示例。
判断方式:中序遍历——搜索二叉树的中序遍历结果一定是非降序的。代码实现:
// 记录中序遍历中上一个访问的节点值
public static int preValue = Intefer.MIN_VALUE;
public static boolean testBST(Node head) {
if (head == null) {
return true;
}
// 判断左子树是否是搜索二叉树
boolean isLeftBst = testBST(head.left);
if (!isLeftBst) {
return false;
}
// 判断当前节点是否满足条件
if (head.value <= preValue) {
return false;
} else {
// 更新节点值
preValue = head.value;
}
// 判断右子树是否是搜索二叉树
return testBST(head.right);
}
2.如何判断一个二叉树是否是完全二叉树?
完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。下图是一个示例。
判断方法,考虑以下两个条件:
- 对于某个节点,如果它的右孩子不为空,则左孩子一定不能为空;
- 在不违背条件1的情况下,在BFS中如果遇到了第一个左右子节点不全的节点,那么后序遍历到的一定均为叶子节点。
如果违背了以上任意一个条件,则树不是完全二叉树,代码实现如下:
private static boolean isComplete(TreeNode root) {
if (root == null) {
return true;
}
boolean hasFullNode = true; // 标记之前的节点是否都有两个子节点
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode left = node.left;
TreeNode right = node.right;
// 如果当前节点没有左子节点,但是有右子节点,直接返回false
if (left == null && right != null) {
return false;
}
// 如果之前的节点已经没有两个子节点了,但是当前节点仍然有左/右子节点,也返回false
if ((left != null || right != null) && !hasFullNode) {
return false;
}
if (right == null) { // 遇到第一个没有右子节点的节点
hasFullNode = false; // 之后的节点不应该有两个子节点
}
if (left != null) {
queue.offer(left);
}
if (right != null) {
queue.offer(right);
}
}
return true;
}
3.如何判断一棵二叉树是否是满二叉树?
满二叉树:叶子节点均在最底层且除叶子节点外所有节点的左右子节点均不为空。
判断方法的代码实现如下:
public boolean isFullTree(TreeNode root) {
//如果根节点为null,则为空树,也就是满二叉树
if(root==null) {
return true;
}
//如果左右子树均为null,则为叶子结点,也就是满二叉树
if(root.left==null && root.right==null) {
return true;
}
//如果左右子树均不为null,则说明当前结点为非叶子结点,则根据定义可以判断为满二叉树
if(root.left!=null && root.right!=null) {
return isFullTree(root.left) && isFullTree(root.right);
}
//如果左右子树有一个为null,则不为满二叉树
return false;
}
4.如何判断一棵二叉树是否是平衡二叉树?
平衡二叉树:对于任何一颗子树,其左树与右数的高度差不大于1。
递归实现:
// 递归中需要从子树获取两个信息:子树是否是平衡二叉树以及子树的高度
// 把这两个信息封装为一个返回信息类
public class BalanceResult {
public boolean isBalanced;
public int height;
public BalanceResult(boolean isBalanced, int height) {
this.isBalanced = isBalanced;
this.height = height;
}
}
// 递归执行
public static BalanceResult process(Node x) {
if (x == null) {
// 基线条件:空子树是平衡二叉树,直接返回直接返回true
return new BalanceResult(true, 0);
}
// 获取左右子树的信息
BalanceResult leftResult = process(x.left);
BalanceResult rightResult = process(x..right);
// 计算当前节点的高度
int height = Math.max(leftResult.height, rightResult.height) + 1;
// 判断以当前节点为根节点的子树是否是平衡二叉树
boolean isBalanced = leftResult.isBalanced && rightResult.isBalanced && Math.abs(leftResult.height - rightResult.height) < 2;
// 封装结果并返回
return new BalanceResult(isBalanced, height);
}
// 入口
public static boolean testBBT(Node head) {
return process(head).isBalanced;
}
二叉树的递归套路(树型DP通用解法)
以判断二叉树是否是搜索二叉树为例,首先考虑判断某个节点时需要从左右子树获取什么信息,在这里是需要:1.左子树是否是搜索二叉树;2.左子树的最大值;3.右子树是否是搜索二叉树;4.右子树的最小值。那么我们可以把:1.是否是搜索二叉树、2.最小值、3.最大值 封装为一个返回类,每次递归时返回相应结果即可。实现代码如下:
// 返回结果类
public static ReturnData {
public boolean isBST;
public int min;
public int max;
public ReturnData(boolean is, int min, int max) {
this.isBST = is;
this.min = min;
this.max = max;
}
}
// 递归运行
public static ReturnData process(Node x) {
if (x == null) {
// 当前节点为空时,最大最小值不好定义,直接返回空
return null;
}
ReturnData leftData = process(x.left);
ReturnData rightData = process(x.right);
boolean isBST = true;
int min = x.value;
int max = x.value;
if (leftData != null) {
// 左子树不为空,更新当前子树的最小值
min = Math.min(min, leftData.min);
}
if (rightData != null) {
// 右子树不为空,更新当前子树的最大值
max = Math.max(max, rightData.max);
}
// 左子树不是搜索二叉树或左子树的最大值不小于当前节点,则树不是搜索二叉树
if (leftData != null && (!leftData.isBST || leftData.max >= x.value)) {
isBST = false;
}
// 右子树不是搜索二叉树或右子树的最小值不大于当前节点,则树不是搜索二叉树
if (rightData != null && (!rightData.isBST || rightData.min <= x.value)) {
isBST = false;
}
return new ReturnData(isBST, min, max);
}
// 入口
public static boolean testBST(Node head) {
return process(head).isBST;
}
同理可以得出判断满二叉树的实现代码:
public static class Info {
public int height;
public int nodes;
public Info(int h, int n) {
height = h;
nodes = n;
}
}
public static Info process(Node x) {
if (x == null) {
return new Info(0, 0);
}
Info leftData = process(x.left);
Info rightData = process(x.right);
int height = Math.max(leftData.height, rightData.height) + 1;
int nodes = leftData.nodes + rightData.nodes + 1;
return new Info(height, nodes);
}
public static boolean testFBT(Node head) {
Info data = process(head);
if (data.nodes == (1 << data.height - 1));
}
练习题
给定二叉树的头节点 head、两个二叉树的节点 node1 和 node2,找到它们的最低公共祖先节点
代码实现如下:
public static Node lowestAncestor(Node head, Node o1, Node o2) {
if (head == null || head == o1 || head == o2) {
// 基线条件,根节点为空(空树)或o1、o2之一为根节点,直接返回
return head;
}
// 在左子树中寻找o1和o2的最低公共祖先
Node left = lowestAncestor(head.left, o1, o2);
// 在右子树中寻找o1和o2的最低公共祖先
Node right = lowestAncestor(head.right, o1, o2);
if (left != null && right != null) {
// 当前节点为根节点的子树中同时存在o1和o2,它就是要找的最低公共祖先,直接返回(一路往上传)
return head;
}
// 否则返回不为空的那边(两种情况:1.某一边已经找到最低公共祖先,另一边一定是null;2.只找到了o1和o2中的一个)
return left != null ? left : right;
}
在二叉树中找到一个节点的后继节点
现有一种新的二叉树节点类型如下:
public class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node (int val) {
value = val;
}
}
请找出给定节点 o 的后继节点(后继节点定义:中序遍历结果列表中该节点的后一个节点)。
思路:
考虑以下两种情况:
- o有右子树,则它的后继节点是它左子树中的最左叶节点;
- o无右子树,则向父节点遍历,直到遍历到的某个节点是其父节点的左节点,这个父节点就是o的后继节点。(特殊情况,整棵树的最右叶子节点不存在后继节点,它的后继节点为空)
代码实现如下:
public static Node getSuccessorNode(Node node) {
if (node == null) {
return node;
}
if (node.right != null) {
// 第一种情况
return getLeftMost(node.right);
} else {
// 第二种情况
Node parent = node.parent;
while (parent.left != null && parent.left != node) {
node = parent;
parent = node.parent;
}
return parent;
}
}
public static Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
二叉树的序列化和反序列化
序列化:内存里的树转变成字符串形式;
反序列化:将字符串转变成内存里的树。
通过先序遍历方式序列化和反序列化二叉树
序列化
在这里考虑用如下所示方式将树转为字符串:
{value}_{value}_....
节点值之间用下划线隔开,空节点用"#"表示。
代码实现如下:
public static String serialByPre(Node head) {
if (head == null) {
return "#_";
}
String res = head.value + "_";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}
反序列化
代码实现如下:
public static Node reconByPreString(String preStr) {
// 所有节点值
String[] values = preStr.split("_");
Queue<String> queue = new LinkedList<String>();
for (int i = 0; i < values.length; ++i) {
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
public static Node reconPreOrder(Queue<String> queue) {
String value = queue.poll();
if ("#".equals(value)) {
return null;
}
Node head = new Node(Integer.valueOf(value));
head.left = reconPreOrder(queue);
head.right = reconPreOrder(queue);
return head;
}