1 二叉树的最大深度
计算二叉树的最大深度,即根节点到叶子节点的最大深度 ,常见的方法有两种:深度优先遍历
和广度优先遍历
。
深度优先遍历,是分别获取到左子树和右子树的深度,从而根据公式:max(左子树深度,右子树深度) + 1
获取二叉树的最大深度。
递归实现
java
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return Math.max(leftHeight,rightHeight) + 1;
}
其实通过递归求解二叉树的问题,很简单!记结论就行了:
- 问下左树,能提供哪些信息,例如高度是多少?
- 问下右树,能提供哪些信息,例如高度是多少?
- 最终返回问题的答案,例如二叉树的最大高度就是:
max(左子树深度,右子树深度) + 1
那么为什么能这么做,我们需要知道其中核心原理:遍历序。我们知道二叉树有3种遍历的方式,分别是前序、中序、后序,所谓遍历序就是遍历二叉树时,每个节点都会走3次。
按照上图中的二叉树,在遍历的过程是这样的:
java
head起始节点为5.
如果要遍历左树,首先会到节点2,然后再查节点2的左树发现为空,返回2,再检查2的右树发现为空,再返回2.
5 - 2 - 2 - 2 - 5
然后此时再转向右树,到节点1,如此反复,直到查到节点为空往上返回。
1 - 4 - 4 - 4 - 1 - 6 - 6 - 6 - 1 - 5
其实最终递归拿到的就是全部节点的深度,例如2因为左右节点均为空,因此深度为1;加上根节点,那么左子树的最大深度为2;
再看右边,节点4的深度为1,节点6的深度为1,因此节点1的深度为2,加上根节点,右子树的最大深度为3.
因此整个二叉树的最大深度为3.
整个的遍历过程:
java
5 - 2 - 2 - 2 - 5 - 1 - 4 - 4 - 4 - 1 - 6 - 6 - 6 - 1 - 5
//前序遍历:取第一个出现的数字 5- 2 - 1 - 4 - 6
//中序遍历:取第二个出现的数字 2 - 5 - 4 - 1 - 6
//后序遍历:取第三个出现的数字 2 - 4 - 6 - 1 - 5
当然在面试的时候,可能不会让我们这么简单地把题目做出来,因此需要使用非递归的方式,此时就需要使用栈来处理。
非递归实现
非递归实现,是广度优先遍历
的实现方案,目的就是一层一层遍历二叉树,直到叶子节点。
所以通过递归的方式,我们需要知道每一层的开始和结束,每层遍历结束之后,深度+1,通过这种方式也可以完成宽度优先遍历
,获取哪一层有最多的节点数。
那么如果只是查深度,只通过栈就可以完成,因为我们知道栈这种数据结构是先进后出:
java
//使用栈数据结构
Queue<TreeNode> stack = new LinkedList();
//先把头节点放进去
stack.offer(root);
此时栈内只有第一层的元素,那么我们只把这一层的元素全部弹出,但是不影响节点插入,所以需要使用while循环一直pop。
java
//开始一层一层遍历
while(!stack.isEmpty()){
//获取当前栈内元素个数
int size = stack.size();
while(size > 0){
//在while循环中,会弹出栈内全部的元素,也可以认为是这一层的元素
TreeNode node = stack.poll();
//判断是否存在左右节点
if(node.left != null){
stack.offer(node.left);
}
if(node.right != null){
stack.offer(node.right);
}
size--;
}
//记录层数
ans++;
}
此时stack的size为1,那么在while循环中,pop全部的节点,同时把pop出去的节点的左右节点入栈。
一层遍历完成之后,此时stack内部全部是二层的节点,而且stack不是空的,因此继续遍历把二层的全部节点均pop出去。
如此循环直到stack为空,此时所有的层级均遍历完成。
java
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
//使用栈数据结构
Queue<TreeNode> stack = new LinkedList();
//先把头节点放进去
stack.offer(root);
//记录层数
int ans = 0;
//开始一层一层遍历
while(!stack.isEmpty()){
//获取当前栈内元素个数
int size = stack.size();
while(size > 0){
//在while循环中,会弹出栈内全部的元素,也可以认为是这一层的元素
TreeNode node = stack.poll();
//判断是否存在左右节点
if(node.left != null){
stack.offer(node.left);
}
if(node.right != null){
stack.offer(node.right);
}
size--;
}
//记录层数
ans++;
}
return ans;
}
我们可以记住一点,任何二叉树的问题都可以通过深度优先遍历完成,像二叉树一致性
、二叉树对称性
等,可以通过深度优先遍历一层一层比较。
二叉树前序、中序、后序遍历
对于二叉树的前序、中序、后序遍历不在此多讲,实现的方式有很多:递归和非递归。通过递归实现非常简单:
java
private List<Integer> list = new ArrayList();
public List<Integer> inorderTraversal(TreeNode root) {
if(root != null){
inorderTraversal(root.left);
list.add(root.val);
inorderTraversal(root.right);
}
return list;
}
例如中序遍历,在往List中添加元素的时候,是在遍历整个左子树之后,同理,前序和后序遍历,就需要将添加元素的时机放在遍历前和遍历后。
递归很简单,我不在此赘述,面试的时候,可能没有这么简单,需要我们使用非递归的方式完成,那么我们需要记住公式即可。
非递归前序遍历
- 先将根节点入栈;
- 弹出节点立刻打印,判断当前弹出的节点是否存在右孩子,有就压栈;
- 随后再判断是否存在左孩子,有就压栈;
- 直到栈内没有节点,结束。
为什么先压右孩子,是因为栈属于先进后出的数据结构,保证每次弹出节点都是先处理左孩子,符合前序遍历中左右
的顺序。
java
private List<Integer> list = new ArrayList();
public List<Integer> preorderTraversal(TreeNode root) {
if(root == null){
return list;
}
Stack<TreeNode> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
list.add(node.val);
if(node.right != null){
stack.push(node.right);
}
if(node.left != null){
stack.push(node.left);
}
}
return list;
}
非递归后序遍历
非递归的后序遍历,其实也可以按照这样的方式,只不过稍微有些不同:
- 先将根节点入栈;
- 弹出节点立刻打印,将其保存在一个栈内,判断当前弹出的节点是否存在左孩子,有就压栈;
- 随后再判断是否存在右孩子,有就压栈;
- 直到栈内没有节点,结束。
此时遍历的过程,是头右左
的顺序,将其反转就是左右头
的顺序,满足后序遍历的顺序,这也是为什么需要额外一个堆栈的原因。
java
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList();
if(root == null){
return list;
}
Stack<TreeNode> stack = new Stack();
Stack<TreeNode> postorStack = new Stack();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
postorStack.push(node);
if(node.left != null){
stack.push(node.left);
}
if(node.right != null){
stack.push(node.right);
}
}
while(!postorStack.isEmpty()){
TreeNode node = postorStack.pop();
list.add(node.val);
}
return list;
}
非递归中序遍历
- 从根节点开始,从左孩子一直遍历,直到左孩子为空;
- 此时弹出节点就打印,同时转向当前节点的右孩子;
- 如果右孩子为空就继续弹出栈内的节点;如果不为空,那么就继续压栈当前节点的左孩子。
java
public void inorder(TreeNode root,List<Integer> list){
//需要一个栈
Stack<TreeNode> stack = new Stack();
while(!stack.isEmpty() || root != null){
if(root != null){
stack.push(root);
//左边一整条全部入栈
root = root.left;
}else{
//弹出节点就打印
TreeNode node = stack.pop();
list.add(node.val);
//然后就去右树找,如果右子树也为空,继续弹出
root = node.right;
}
}
}
构造二叉树节点的附属节点
前面我们是介绍了二叉树一些常见的算法,其实在遍历二叉树的时候,我们的思维不要只局限于在树上的节点做事情,在遍历二叉树的时候,其实是可以记录一些与题目相关的节点信息,相当于二叉树节点的一些附属信息,但又不在树上, 这时就会对我们解题有很大的帮助。
题目
给你二叉树的根节点 root
和一个表示目标和的整数 targetSum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum
。如果存在,返回 true
;否则,返回 false
。
叶子节点 是指没有子节点的节点。
题解
这道题的核心是要遍历到叶子节点,即便中间有几个节点和为 targetSum
也不行,所以我们在遍历二叉树的同时,需要构建每个节点的附属节点,如下图:
这道题附属节点就是沿着一条路径上的节点之和,例如遍历到节点5时,其附属节点sum = 5;然后判断当前节点是否存在左右节点,如果存在那么就加入到队列中;例如左孩子4,其附属节点sum = 5+4;如此遍历。
当遍历到叶子节点时,即当前节点的left和right为空,此时判断当前节点的附属节点的值,如果等于 targetSum
,那么直接return。
java
public boolean hasPathSum(TreeNode root, int targetSum) {
//一定是到叶子节点
if(root == null){
return false;
}
//两个队列,记录节点和总和
Queue<TreeNode> nodeStack = new LinkedList();
Queue<Integer> valStack = new LinkedList();
nodeStack.offer(root);
valStack.offer(root.val);
while(!nodeStack.isEmpty()){
TreeNode node = nodeStack.poll();
int sum = valStack.poll();
//记住得是叶子节点
if(node.left == null && node.right == null){
if(sum == targetSum){
return true;
}
}
if(node.left != null){
nodeStack.offer(node.left);
valStack.offer(sum + node.left.val);
}
if(node.right != null){
nodeStack.offer(node.right);
valStack.offer(sum + node.right.val);
}
}
return false;
}
题目
给你一个二叉树的根节点 root
,树中每个节点都存放有一个 0
到 9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3
表示数字123
。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
题解
其实这道题的核心在于不是简单的加减法,而是从根节点到叶子节点路径上所有的数字组合成一个整数,如何求这个整数?
java
1 -> 2 -> 3
//遍历到1
sum = 1
//遍历到2
sum = sum * 10 + 2 即 12
//遍历到3
sum = sum * 10 + 3 即 12 * 10 + 3 = 123
所以根据上述求和算法,这道题就很简单了,依然去构造每个节点的附属节点,碰到叶子节点即判断是否满足题意。
java
public int sumNumbers(TreeNode root) {
if(root == null){
return 0;
}
//首先需要广度遍历全部的节点
Queue<TreeNode> nodeStack = new LinkedList();
Queue<Integer> arrStack = new LinkedList();
nodeStack.offer(root);
arrStack.offer(root.val);
int sum = 0;
while(!nodeStack.isEmpty()){
TreeNode node = nodeStack.poll();
int res = arrStack.poll();
//查到了叶子节点
if(node.left == null && node.right == null){
sum += res;
continue;
}
if(node.left != null){
nodeStack.offer(node.left);
arrStack.offer(res * 10 + node.left.val);
}
if(node.right != null){
nodeStack.offer(node.right);
arrStack.offer(res * 10 + node.right.val);
}
}
return sum;
}
二叉树节点的公共祖先
求解二叉树两个节点的公共祖先是常见的面试问题,一般来说会有两种:最近或者最远的公共祖先。
例如求节点p = 6和q = 4的公共祖先,节点6的祖先节点为3、5、6,节点4的祖先节点为3、5、2、4,所以两者的公共祖先就是3、5,其中5为最近的公共祖先,3为最远的公共祖先。
因此求解这种题目的思路就比较明确了:
- 求节点p、q的路径;
- 求两条路径上公共节点,并根据题意取最近或者最远的节点。
那么如何寻找某个节点的路径,但凡涉及到路径的问题,均可以构造附属节点来完成。
java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null){
return null;
}
if(p == null || q == null){
return null;
}
List<TreeNode> p_path = getPath(root,p);
List<TreeNode> q_path = getPath(root,q);
//开始找最近节点
return getCommonAncestor(p_path,q_path);
}
private TreeNode getCommonAncestor(List<TreeNode> p,List<TreeNode> q){
TreeNode res = null;
if(p == null || q == null){
return res;
}
for(int i = 0;i<p.size() && i<q.size();i++){
if(p.get(i).val == q.get(i).val){
res = p.get(i);
}else {
break;
}
}
return res;
}
private List<TreeNode> getPath(TreeNode root,TreeNode target){
Queue<TreeNode> stack = new LinkedList();
//这里存储了每个节点的祖先节点
Queue<List<TreeNode>> path = new LinkedList();
//先把根节点压栈
stack.offer(root);
List<TreeNode> list = new ArrayList();
list.add(root);
path.offer(list);
if(root.val == target.val){
return list;
}
while(!stack.isEmpty()){
TreeNode node = stack.poll();
List<TreeNode> nodePath = path.poll();
if(node.val == target.val){
return nodePath;
}
if(node.left != null){
stack.offer(node.left);
List<TreeNode> leftPath = new ArrayList(nodePath);
leftPath.add(node.left);
path.offer(leftPath);
}
if(node.right != null){
stack.offer(node.right);
List<TreeNode> rightPath = new ArrayList(nodePath);
rightPath.add(node.right);
path.offer(rightPath);
}
}
//都没找到,就直接返回null
return null;
}
具体的思路是这样的,还有很大的优化空间,感兴趣的伙伴把优化的算法贴在评论区。