写在前头的总结
下面的先序中序后序遍历以及二叉树的带权路径的解法基本可以囊括绝大多数与二叉树相关的问题,为了方便记忆,在写的时候都用了两个嵌套的while循环,省得到时候再跟while里套if else的记混~
首先二叉树的先序、中序、后序遍历在 这篇博客 里写的很清楚,尤其是后续遍历,思路很巧妙:后序遍历的非递归版本是最有技巧性的,难度相对高一点,但其实是可以转换成前序遍历的。后序遍历的顺序是左右中,因此只需要按中右左遍历,再reverse一下即可。 当然大部分玩家实现的方式都是两个栈如下:
import java.util.Stack;
public class BinaryTree1 {
public Node init() {//注意必须逆序建立,先建立子节点,再逆序往上建立,因为非叶子结点会使用到下面的节点,而初始化是按顺序初始化的,不逆序建立会报错
Node J = new Node(8, null, null);
Node H = new Node(4, null, null);
Node G = new Node(2, null, null);
Node F = new Node(7, null, J);
Node E = new Node(5, H, null);
Node D = new Node(1, null, G);
Node C = new Node(9, F, null);
Node B = new Node(3, D, E);
Node A = new Node(6, B, C);
return A; //返回根节点
}
public void printNode(Node node){
System.out.print(node.getData());
}
public void theFirstTraversal_Stack(Node root) { //先序遍历
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || stack.size() > 0) { //将所有左孩子压栈
while(node != null) { //压栈之前先访问
printNode(node);
stack.push(node);
node = node.getLeftNode();
}
/*
重要的两行:pop()后,最近的一个被访问了左子树的节点被弹出栈,
然后将当前操作的节点设置为刚刚被弹出栈的节点的右子节点。
*/
node = stack.pop();
node = node.getRightNode();
}
}
public void theInOrderTraversal_Stack(Node root) { //中序遍历
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || stack.size() > 0) {
while (node != null) {
stack.push(node);
node = node.getLeftNode();
}
node = stack.pop();
printNode(node);
node = node.getRightNode();
}
}
public void thePostOrderTraversal_Stack(Node root) { //后序遍历
Stack<Node> stack = new Stack<>();
Stack<Node> output = new Stack<>();//构造一个中间栈来存储逆后序遍历的结果
Node node = root;
while (node != null || stack.size() > 0) {
if (node != null) {
output.push(node);
stack.push(node);
node = node.getRightNode();
} else {
node = stack.pop();
node = node.getLeftNode();
}
}
System.out.println(output.size());
while (output.size() > 0) {
printNode(output.pop());
}
}
public static void main(String[] args) {
BinaryTree1 tree = new BinaryTree1();
Node root = tree.init();
System.out.println("先序遍历");
tree.theFirstTraversal_Stack(root);
System.out.println("");
System.out.println("中序遍历");
tree.theInOrderTraversal_Stack(root);
System.out.println("");
System.out.println("后序遍历");
tree.thePostOrderTraversal_Stack(root);
System.out.println("");
}
class Node{
private int data;
private Node leftNode;
private Node rightNode;
public Node(int data, Node leftNode, Node rightNode){
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
public int getData() {
return data;
}
public Node getLeftNode() {
return leftNode;
}
public Node getRightNode() {
return rightNode;
}
}
}
二叉树的带权路径(这个题目我几个星期前被度小满金融面试官问到了...)的计算也可以用非递归的方式来实现,但是有一点复杂、难想,找了很久才在LeetCode上找了一个大佬的思路。首先该题目是,给定一个二叉树和一个整数,求该二叉树的跟到任一叶子节点的路径和为该整数的所有组合,比如:
看到这个题目第一反应必然是二叉树的遍历。这个题目用递归来做并不太难,非递归的话就有一点难想了,主要是因为需要把之前遍历过的节点保存下来,在由子节点回到父节点的时候要记得把子节点的路径值减掉。如果使用上面链接里的博客的做法的话,会丢失前面的访问信息,导致根本不知道访问到的是哪一层。
正确的解法同样是像上面先序遍历一样使用两个while循环,但是具体的部分会有差异,最精髓的地方在于:设置一个prev,在某节点遍历完毕返回上层节点之前把pre设置为自己,这样上层节点就会先检查自己的右节点是不是pre,以判断本节点是不是也可以返回了;另外就是在循环的最后cur = null,跳过下一轮大循环开头的小循环,避免重复访问。具体请看下面代码中的注释:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
public class PathSumNoRecursive {
class TreeNode {
int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int x) { val = x; }
}
public List<List<Integer>> pathSum(TreeNode root, int target) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
LinkedList<Integer> list = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root, prev = null;
int sum = 0;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
// 左子节点入栈。同时把当前节点路径值加进sum和list中
stack.push(cur);
sum += cur.val;
list.add(cur.val);
cur = cur.left;
}
cur = stack.peek(); // 重要,先peek再根据信息判断是否还要继续在栈里,当不需要的时候再pop
if (cur.left == null && cur.right == null && sum == target){
//关键。add一个根据list而新生成的LinkedList,这样还可以继续用list
res.add(new LinkedList<>(list));
}
if (cur.right != null && cur.right != prev) {
// 右节点不为空且没有被访问过
/*
这里并不需要令pre = cur,因为pre的作用是在
退回上一层节点时判断当前节点的右子节点是否为pre,即是否已访问过。
*/
cur = cur.right;
}else {
// 左右都为空 或者 虽然右节点不为空但是已被访问过了
prev = stack.pop();
sum -= prev.val; //返回时把自己的路径值减掉
list.removeLast(); // 返回时把自己在链表中的位置删除
/*
* 下一行很关键,在下一次的大循环中跳过小循环,是为了
* 避免对已经访问过左子节点的节点进行重新访问。
*/
cur = null;
}
}
return res;
}
}
该方法同样可以计算树的深度(只要把所有节点的路径值设为1,就可以在遍历结束后获得最大深度了~)。查了百度的绝大多数的人都是用层次遍历实现的使用广度优先策略计算树的深度,而本篇文章里的方法是深度优先的~
二叉树的最大路径和:求二叉树中从任意一个节点到任意一个节点的路径和的最大值,路径最少要包含一个节点。
递归解决,设置一个类的成员变量ret,用于在递归中保有全局的信息。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private int ret = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
/**
对于任意一个节点, 如果最大和路径包含该节点, 那么只可能是两种情况:
1. 其左右子树中所构成的和路径值较大的那个加上该节点的值后向父节点回溯构成最大路径
2. 左右子树都在最大路径中, 加上该节点的值构成了最终的最大路径
**/
getMax(root);
return ret;
}
/**
getMax的返回值代表从root的左子树中的某节点或从root右子树中的某节点出发,到达root节点,
所经过的路径值的最大值。
在每一次的递归过程中都对全局变量ret进行判断,保证获取到最大的路径值。
**/
public int getMax(TreeNode root) {
if (root == null) return 0;
int left = Math.max(getMax(root.left), 0);
int right = Math.max(getMax(root.right), 0);
ret = Math.max(ret, root.val + left + right);
return Math.max(left, right) + root.val;
}
}