一丶二叉树的遍历
1.二叉树遍历递归写法与递归序
了解过二叉树的朋友,最开始肯定是从二叉树的遍历开始的,二叉树遍历的递归写法想必大家都有所了解。
public static void process(TreeNode node) { if (node == null) { return; } //如果在这里打印 代表前序遍历 ----位置1 process(node.left); //如果在这里打印中序遍历 ----位置2 process(node.right); //如果在这里打印 后序遍历 ---位置3 }
process函数在不同的位置进行打印,就实现了不同的遍历顺序。
我们这里引入一个概念递归序
—— 递归函数到达节点的顺序
process函数的递归序列是什么呢
- 首先
process(1)
此时方法栈记为A,遍历节点1(可以理解为A栈的位置1) - 然后
process(1.left)
再开辟一个栈记为B 来到2(可以理解为B栈的位置1) - 接着
process(2.left)
为空 出栈 相当于来到了B栈的位置2 ,再次来到2 - 接着
process(2.right)
为空,出栈,来到B栈位置3,再次来到2 - 接着出栈,来到A栈位置2
- 然后
process(1.right)
再开辟一个栈记为C 来到3(可以理解为C栈的位置1) - 接着
process(3.left)
为空 出栈 相当于来到了C栈的位置2 ,再次来到3 - 接着
process(3.right)
为空,出栈,来到C栈位置3,再次来到3 - 最后出栈,来到A栈的位置3,来到1
递归序为 1,2,2,2,1,3,3,3,1
。可以看到每一个节点都将访问3次。
-
第一次访问的时候打印
1,2,3
——先序遍历 -
第二次访问的时候打印
2,1,3
——中序遍历 -
第三次访问的时候打印
2,3,1
——后序遍历
2.二叉树遍历非递归写法
下面讲解的二叉树遍历非递归写法,都针对下面这棵树
2.1 先序遍历
递归写法告诉我们,打印结果应该是1,2,4,5,3
。
对于节点2,我们需要先打印2,然后处理4,然后处理5。栈先进后出,如果我们入栈顺序是4,5 那么会先打印5然后打印4,将无法实现先序遍历,所有我们需要先入5后入4。
- 当前打印的节点记忆为cur
- 打印
- cur的右节点(如果存在)入栈,然后左节点(如果存在)入栈
- 弹出栈顶进行处理,循环往复
程序如下
public static void process1(TreeNode root) { if (root == null) { return; } Stack<TreeNode> stackMemory = new Stack<>(); stackMemory.push(root); while (!stackMemory.isEmpty()) { TreeNode temp = stackMemory.pop(); System.out.println(temp.val); if (temp.right != null) { stackMemory.push(temp.right); } if (temp.left != null) { stackMemory.push(temp.left); } } }
2.2 中序遍历
-
将树的左边界放入栈中
这时候栈中的内容是
(栈底)1->2->4(栈顶)
-
然后弹出节点cur进行打印
也就是打印4,如果cur具备右子树,那么将右子树的进行步骤一
-
循环往复直到栈为空
为什么这可以实现左->中->右
打印的中序遍历
首先假如当前节点是A,那么打印A的前提是,左子树打印完毕,在打印A的左子树的时候,我们会把A左子节点的右树入栈,这一保证了打印A之前,A的左子树被处理完毕,然后打印A
打印完A,如果A具备右子树,右子树会入栈,然后弹出,保证了打印完A后打印其右子树,从而实现左->中->右
打印的中序遍历
public static void process2(TreeNode root) { if (root == null) { return; } Stack<TreeNode> stackMemory = new Stack<>(); do { //首先左子树入栈 //1 while (root!=null){ stackMemory.push(root); root = root.left; } //来到这儿,说明左子树都入栈了 //弹出 if (!stackMemory.isEmpty()){ root = stackMemory.pop(); System.out.println(root.val); //赋值为右子树,右子树会到1的代码位置,如果右子树,那么右子树会进行打印 root = root.right; } }while (!stackMemory.isEmpty()||root!=null); }
2.3 后序遍历
后续遍历就是左->右->头
的顺序,那么只要我以头->左->右
的顺序将节点放入收集栈中,最后从收集栈中弹出的顺序,就是左->右->头
public static void process3(TreeNode r) { if (r == null) { return; } //辅助栈 Stack<TreeNode> help = new Stack<>(); //收集栈 Stack<TreeNode> collect = new Stack<>(); help.push(r); while (!help.isEmpty()) { TreeNode temp = help.pop(); collect.push(temp); if (temp.left != null) { help.push(temp.left); } if (temp.right != null) { help.push(temp.right); } } StringBuilder sb = new StringBuilder(); while (!collect.isEmpty()) { sb.append(collect.pop().val).append(","); } System.out.println(sb); }
3.二叉树宽度优先遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 (也是宽度优先遍历)即逐层地,从左到右访问所有节点)。
此树宽度优先遍历——[3],[9,20],[15,7]
宽度优先遍历可以使用队列实现,最开始将队列的头放入到队列中,然后当队列不为空的时候,拿出队列头cur,加入到结果集合中,然后如果当前cur的左儿子,右儿子中不为null的节点放入到队列中,循环往复
下面以LeetCode102为例子
public List<List<Integer>> levelOrder(TreeNode root) { //结果集合 List<List<Integer>> res = new ArrayList<>(); if (root == null) { return res; } //队列 LinkedList<TreeNode> queue = new LinkedList<>(); queue.addLast(root); //当前层的节点数量为1 int curLevelNum = 1; while (!queue.isEmpty()) { //存储当前层节点的值 List<Integer> curLevelNodeValList = new ArrayList<>(curLevelNum); //下一层节点的数量 int nextLevelNodeNum = 0; //遍历当前层 while (curLevelNum > 0) { TreeNode temp = queue.removeFirst(); curLevelNodeValList.add(temp.val); //处理左右儿子,只要不为null 那么加入并且下一次节点数量加1 if (temp.left != null) { queue.addLast(temp.left); nextLevelNodeNum++; } if (temp.right != null) { queue.addLast(temp.right); nextLevelNodeNum++; } //当前层减少 curLevelNum--; } //当前层结束了,到下一层 curLevelNum = nextLevelNodeNum; //存储结果 res.add(curLevelNodeValList); } return res; }
二丶树型DP
1.从一道题开始——判断一颗二叉树是否是搜索二叉树
1.1 中序遍历解题
可以断定我们可以使用中序遍历,然后在中序遍历的途中判断节点的值是满足升序即可
-
递归中序遍历判断是否二叉搜索树
public boolean isValidBST(TreeNode root) { if (root == null) { return true; } //第二个参数记录之前遍历遇到节点的最大值 //由于TreeNode 可能节点值为int 最小使用Long最小 return check(root, new AtomicLong(Long.MIN_VALUE)); } private boolean check(TreeNode node, AtomicLong preValue) { if (node == null) { return true; } //左树是否二叉搜索树 boolean isLeftBST = check(node.left, preValue); //左树不是 那么返回false if (!isLeftBST) { return false; } //当前节点的值 大于之前遇到的最大值 那么更改preValue if (node.val > preValue.get()) { preValue.set(node.val); } else { //不满足升序那么false return false; } //检查右树 return check(node.right, preValue); }
-
非递归中序遍历判断是否二叉搜索树
private boolean check(TreeNode root) { if (root == null) { return true; } //前面节点最大值,最开始为null Integer pre = null; Stack<TreeNode> stack = new Stack<>(); do { while (root != null) { stack.push(root); root = root.left; } if (!stack.isEmpty()) { root = stack.pop(); //满足升序那么更新pre if (pre == null || pre < root.val) { pre = root.val; } else { return false; } root = root.right; } } while (!stack.isEmpty() || root != null); return true; }
1.2 引入 —— 树形DP
如果当前位于root节点,我们可以获取root左子树的一些"信息"
,root右子树的一些信息,我们们要如何判断root为根的树是否是二叉搜索树:
-
root左子树,右子树必须都是二叉搜索树
-
root的值必须大于
左子树最大
,必须小于右子树最小
-
根据1和2 我们可以得到
"信息"
的结构static class Info { //当前子树的最小值 Integer min; //当前子树最大值 Integer max; //当前子树是否是二叉搜索树 boolean isBst; Info(Integer min, Integer max, boolean flag) { this.min = min; this.max = max; this.isBst = flag; } }
接下来的问题是,有了左右子树的信息,如何拼凑root自己的信息?如果不满足二叉搜索树的要求那么返回isBst为false,否则需要返回root这棵树的最大,最小——这些信息可以根据左子树和右子树的信息构造而来。代码如下
private Info process(TreeNode node) { //如果当前节点为null 那么返回null //为null 表示是空树 if (node == null) { return null; } //默认现在是二叉搜索树 boolean isBst = true; //左树最大,右树最小 二者是否bst ,从左右子树拿信息 Info leftInfo = process(node.left); Info rightInfo = process(node.right); //左树不为null 那么 维护isBst标识符 if (leftInfo != null) { isBst = leftInfo.isBst; } //右树不为null 那么 维护isBst标识符 if (rightInfo != null) { isBst = isBst && rightInfo.isBst; } //如果左数 或者右树 不为二叉搜索树 那么返回 if (!isBst){ return new Info(null,null,isBst); } //左右是bst,那么看是否满足二叉搜索树的条件 //左边最大 是否小于当前节点 if (leftInfo!=null && leftInfo.max >= node.val){ isBst = false; } //右边最小 是否小于当前节点 if (rightInfo!=null && rightInfo.min <= node.val){ isBst = false; } //如果不满足 那么返回 if (!isBst){ return new Info(null,null,isBst); } //说明node为根的树是bst //那么根据左右子树的信息返回node这课树的信息 Integer min = node.val; Integer max = node.val; if (leftInfo!=null){ min = leftInfo.min; } if (rightInfo!=null){ max = rightInfo.max; } return new Info(min, max, true); }
2. 树型DP题目套路
之所以称之为树型DP,是因为这个套路用于解决 树的问题。那么为什么叫DP,这是由于node节点的信息,来自左右子树的信息,类似于动态规划中的状态转移。
2.1树型DP可以解决什么问题
怎么理解:
对于1中判断是否二叉搜索树的问题,S规则就是以node为根的这棵树是否是二叉搜索树
最终整棵树是否二叉搜索树,是依赖于树中所有节点的——"最终答案一定在其中"
2.2 解题模板
3.题目练习
3.1 二叉树的最大深度
需要的信息只有树的高度,我们可以向左子树获取,高度然后获取右子树的高度,然后二叉高度取max加上1就是当前节点为根的树的高度
public int maxDepth(TreeNode root) { if(root == null){ return 0; } int leftH = maxDepth(root.left); int rightH = maxDepth(root.right); return Math.max(leftH,rightH)+1; }
3.2 判断一颗树是否二叉平衡树
- 需要什么信息:左右树的高度,左右树是否是平衡的
- 怎么根据左右构造当前树的信息:当前高度=max(左右高度)+1 ,当前是否平衡=左平衡右平衡且二者高度差不大于1
/*** * 是否是平衡二叉树 * @return */ public static boolean isAVL(TreeNode root) { return process(root).getKey(); } public static Pair<Boolean, Integer> process(TreeNode root) { //当前节点为null 那么是平衡二叉树 if (root == null) { return new Pair<>(true, 0); } //右树 Pair<Boolean, Integer> rightData = process(root.right); //左树 Pair<Boolean, Integer> leftData = process(root.left); //右树是否是平衡 boolean rTreeIsAVL = rightData.getKey(); //右树高度 int rHigh = rightData.getValue(); //左树是否平衡 boolean lTreeIsAVL = leftData.getKey(); //左树高度 int lHigh = rightData.getValue(); //当前树是平衡要求:左树平衡 右树平衡 且二者高度差小于1 boolean thisNodeIsAvl = rTreeIsAVL && lTreeIsAVL && Math.abs(rHigh - lHigh) < 2; //返回当前树的结果 高度树是左右高度最大+1 return new Pair<>(thisNodeIsAvl, Math.max(rHigh, lHigh) + 1); }
3.3 判断一棵树是否满二叉树
满二叉树 树的高度h和树节点数目n具备 n = 2的h次方 -1 的特性
- 需要左右树的高度,左右树的节点个数
- 怎么根据左右构造当前树的信息:当前高度=max(左高,右高)+1,当前节点个数=左个数+右个数+1
public static boolean isFullTree(TreeNode root) { Pair<Integer, Integer> rootRes = process(root); int height = rootRes.getKey(); int nodeNums = rootRes.getValue(); return nodeNums == Math.pow(2, height)-1; } //key 高度 v 节点个数 public static Pair<Integer, Integer> process(TreeNode node) { if (node == null) { return new Pair<>(0, 0); } Pair<Integer, Integer> rInfo = process(node.right); Pair<Integer, Integer> lInfo = process(node.left); int thisNodeHeight = Math.max(rInfo.getKey(), lInfo.getKey()) + 1; int thisNodeNum = rInfo.getValue() + lInfo.getValue() + 1; return new Pair<>(thisNodeHeight, thisNodeNum); }