刚结束完链表,二叉树这可不就来了~链表和二叉树都是面试的常考题,在做题之前大家一定要理清楚这两种数据结构的特性噢
1.二叉树的中序遍历(难度等级:简单)
首先大家得知道前,中,后序都分别是什么含义。其次,我个人觉得中序遍历是三种遍历方式中较为难写的一个(当然,递归法都一样哈哈...)。但是面试过程中,面试官可能会让大家用迭代法写,所以这里就给出迭代法的代码啦~
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
list<Integer> res = new ArrayList<>();
Stack<Integer> stack = new Stack<>();
while(!stack.isEmpty || root==null){
while(root != null){
stack.push(root);
root = root.left;
}
root = root.pop();
res.add(root.val);
root = root.right;
}
}
}
2.二叉树的最大深度(难度等级:简单)
这道题递归和迭代法倒是可以讲讲了。递归法又叫深度优先遍历,本质还是分成算左子树和右子树的高度来计算的。迭代法又叫做广度优先遍历,其实就是层序遍历,一层一层地遍历二叉树。
递归法:
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
} else {
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}
}
}
迭代法,需要有变量来表示当前层是否遍历完:
class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int height = 0;
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
while(!deque.isEmpty()){
int size = deque.size();
while(size > 0){
TreeNode node = deque.poll();
if(node.left != null){
deque.offer(node.left);
}
if(node.right != null){
deque.offer(node.right);
}
size--;
}
height++;
}
return height;
}
}
3.翻转二叉树(难度等级:简单)
这道题我最先想到的就是,用深度优先搜索+栈就能实现(毕竟栈就是先进后出嘛),但是这种做法又带来了额外的空间开销。
没想到,官方给出的递归法几行代码就搞定了。其本质思想还是 从根节点出发,直到找到叶子节点,从叶子节点开始翻转左右子树,这样慢慢回溯到根节点,整颗二叉树就也构建好了!
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return null;
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.right = left;
root.left = right;
return root;
}
}
4.对称二叉树(难度等级:简单)
这道题很适合递归法。轴对称,也就是根节点的左右子节点值相等,左右子树关于轴是对称的。可以看到这个思路中是需要2个节点的,所以递归法还需要自己构造一个函数,输入即为左右子树的根节点。
class Solution {
TreeNode temp = new TreeNode();
public boolean isSymmetric(TreeNode root) {
return middle(root, root);
}
public boolean middle(TreeNode l, TreeNode r){
if(l == null && r == null){
return true;
}
if(l == null || r == null){
return false;
}
//轴对称的含义不要写错了!
boolean left = middle(l.left, r.right);
boolean right = middle(r.left, l.right);
return l.val==r.val && left && right;
}
}
5.二叉树的直径(难度等级:简单)
这道题初步思考一下,我就想到直径不就等同于根节点左右子树的最大深度之和嘛
OK写完代码一看,测试用例都通过了,提交报错了...再看失败的测试用例,奥~原来直径的路径不一定是经过根节点的呀
那怎么办呢?那只能对树上的每一个节点都按照上面的方式计算一下直径了,最后去个最大值不就出结果了嘛!在我的原始代码上改动了一下,快乐OC啦
class Solution {
int res;
public int diameterOfBinaryTree(TreeNode root) {
if(root==null || (root.left == null && root.right==null)){
return 0;
}
res = 1;
findHeight(root);
return res;
}
public int findHeight(TreeNode root){
if(root==null) return 0;
int left = findHeight(root.left);
int right = findHeight(root.right);
res = Math.max(res, left+right);
return Math.max(left, right)+1;
}
}
6.二叉树的层序遍历(难度等级:中等)
层序遍历是经典问题啦,关键就在于怎么记录当前层是否遍历完毕,以及怎么知道下一层节点是什么?
这里把迭代法(广度优先)和递归法(深度优先)的答案都给出:
迭代法(注意刚开始就判断根节点是否为null)
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
List<Integer> output = new ArrayList<>();
while(output.size() < size){
TreeNode temp = queue.poll();
if(temp.left != null){
queue.offer(temp.left);
}
if(temp.right != null){
queue.offer(temp.right);
}
output.add(temp.val);
}
res.add(output);
}
return res;
}
}
递归法(用level值来维护层级遍历关系)
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
preBST(root, 0);
return res;
}
public void preBST(TreeNode root, int level){
if(root == null) return;
if(res.size()==level) res.add(new ArrayList<Integer>());
res.get(level).add(root.val);
preBST(root.left, level+1);
preBST(root.right, level+1);
}
}
7.将有序数组转换为二叉搜索树(难度等级:简单)
可以观察到,有序数组不就是一颗二叉搜索树 中序遍历 的结果吗?但是对于构建一颗二叉搜索树,我们是要 前序遍历 的,即先构建中间节点,再去构建它的左右子节点。那么中间节点的值怎么确定呢?是不是就是等于有序数组的中值呀~而根据二叉搜索树的特性,中间节点的左子树的值必然小于中间节点(即对应有序数组的左半部分),右子树的值必然大于中间节点(即对于有序数组的右半部分)。如此一来,通过递归来构造二叉搜索树即可~
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return binaryBSY(nums, 0, nums.length-1);
}
public TreeNode binaryBSY(int[] nums, int left, int right){
if(left > right) return null;
int mid = (left+right)/2;
int value = nums[mid];
TreeNode root = new TreeNode(value);
root.left = binaryBSY(nums, left, mid-1);
root.right = binaryBSY(nums, mid+1, right);
return root;
}
}
8.验证二叉搜索树(难度等级:中等)
这道题也是考察二叉搜索树的性质的,即中序遍历的结果是个单调递增的有序数组。所以同样的,也有迭代和递归2种解法。中序遍历的写法大家应该都会,那么关键就是在代码中遍历到“中间节点”时,怎么比较它和上一个遍历节点的值呢?
我们可以定义一个类的成员变量来解决,它维护上一次遍历的节点的值。在实现中,我们只要判断这个类成员变量的值是否小于当前节点的值即可。
这里给出递归法的解:
class Solution {
// 递归
TreeNode max;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 左
boolean left = isValidBST(root.left);
if (!left) {
return false;
}
// 中
if (max != null && root.val <= max.val) {
return false;
}
max = root;
// 右
boolean right = isValidBST(root.right);
return right;
}
}
9.二叉搜索树中第 K 小的元素(难度等级:中等)
这道题一开始我一直在想怎么用递归解决(且不需要额外的容器),结果想了半天也没想出来答案...于是转而尝试迭代法,迭代法其实就很简单了,只要在遍历当前节点时加上判断条件即可。最后看官方题解发现还是迭代法简洁(虽然性能一般)。但是从应试角度来说,够用了!
class Solution {
int res = Integer.MAX_VALUE;
public int kthSmallest(TreeNode root, int k) {
Stack<TreeNode> stack = new Stack<>();
while(root != null || !stack.isEmpty()){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();
res = root.val;
k--;
if(k == 0) return res;
root = root.right;
}
return res;
}
}
10.二叉树的右视图(难度等级:中等)
这道题,一看就是需要层序遍历的,而且只需要在原层序遍历的代码上做一些修改就行!即当每层遍历到最后一个节点时,将该节点的值存入集合即可。我用迭代法做了~
class Solution {
public List<Integer> rightSideView(TreeNode root) {
if(root == null) return new ArrayList<>();
List<Integer> res = new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
while(!deque.isEmpty()){
int size = deque.size();
int num = 0;
while(num < size){
TreeNode temp = deque.poll();
num++;
if(temp.left != null){
deque.offer(temp.left);
}
if(temp.right != null){
deque.offer(temp.right);
}
if(num == size){
res.add(temp.val);
}
}
}
return res;
}
}
11.二叉树展开为链表(难度等级:中等)
这道题需要注意的是,题目描述的是“展开”,所以是在当前二叉树上操作,而不是自行构建一个新的链表!我的第一想法就是,用迭代法的前序遍历,其中处理中间节点的逻辑写成构建链表的逻辑即可。那么就需要在处理当前节点时,知道上一个遍历的节点。需要注意的是,根节点的处理方式,即可以构建一个虚拟节点,只有当其不等于null时才进行“展开”操作。
class Solution {
TreeNode prev = new TreeNode();
public void flatten(TreeNode root) {
if(root == null) return;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode temp = stack.pop();
//处理逻辑
if(prev != null){
prev.right = temp;
prev.left = null;
}
if(temp.right != null){
stack.push(temp.right);
}
if(temp.left != null){
stack.push(temp.left);
}
prev = temp;
}
return;
}
}
12.从前序与中序遍历序列构造二叉树(难度等级:中等)
根据前序遍历和中序遍历的性质,我们可以想到这样一个思路:
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。
这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
这不就是天然的递归嘛。but写代码的时候,我发现思路特别容易混乱。我觉得,最关键之处,就是要找到当前构造的根节点在前序遍历和后序遍历的位置。之后,我们不是要构造当前节点的左子树和右子树嘛,所以关键就是要找到左子树分别在前序遍历和中序遍历的左区间和右区间(右子树同理)。
class Solution {
Map<Integer, Integer> map = new HashMap<>(); //为了方便获取元素索引
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = inorder.length;
for(int i=0; i<n; i++){
map.put(inorder[i], i);
}
return myFindRoot(preorder, inorder, 0, n-1, 0, n-1);
}
public TreeNode myFindRoot(int[] preorder, int[] inorder, int preLeft, int preRight, int inLeft, int inRight){
if(preLeft > preRight){
return null;
}
int preRoot = preLeft; //当前根节点在preorder中的位置
int inRoot = map.get(preorder[preRoot]); //当前根节点在inorder中的索引位置
TreeNode root = new TreeNode(preorder[preRoot]); //创建当前根节点
int numLeft = inRoot - inLeft;
//构造左子树
root.left = myFindRoot(preorder, inorder, preRoot+1, preRoot+numLeft, inLeft, inRoot -1);
//构造右子树
root.right = myFindRoot(preorder, inorder, preRoot+1+numLeft, preRight, inRoot+1, inRight);
return root;
}
}
13.路径总和 III(难度等级:中等)
这道题我是真没思路啊,比之前做的路径总和1和2可难太多了。这道题我们关键要记住 前缀和 的思想,采用 前序遍历,并用一个HashMap来维护已经遍历过的前缀和以及该前缀和出现次数。在处理当前节点时,如果当前节点前缀和减去目标值,能在HashMap中找到该元素,证明这两个节点的路径必然等于目标
class Solution {
int sum = 0;
public int pathSum(TreeNode root, int targetSum) {
Map<Long, Integer> map = new HashMap<>(); //key为当前节点前缀和,value为当前前缀和在map中的数量
map.put(0L, 1);
return dfs(root, 0, targetSum, map);
}
//构造前缀和
public int dfs (TreeNode root, long cur, int targetSum, Map<Long, Integer> map){
if(root == null) return 0;
int sum = 0;
cur += root.val; //当前节点的前缀和
sum = map.getOrDefault(cur-targetSum, 0); //如果当前节点前缀和-某遍历过节点的前缀和==target,则找到一条路径
map.put(cur, map.getOrDefault(cur, 0)+1);
sum += dfs(root.left, cur, targetSum, map);
sum += dfs(root.right, cur, targetSum, map);
map.put(cur, map.getOrDefault(cur, 0)-1); //退出当前节点时,恢复现场
return sum;
}
}
14.二叉树的最近公共祖先(难度等级:中等)
这道题用递归就可以很好解答了~其实就是当前节点的左子树和右子树(或当前节点)包含p和q即可。那我们只需要递归地来判断p和q是否存在于子树中,因此要额外定义一个函数。需要注意的事,题目中明确提示了 一个节点也可以是它自己的祖先。
class Solution {
TreeNode ans = new TreeNode();
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
isExist(root, p, q);
return ans;
}
public boolean isExist(TreeNode root, TreeNode p, TreeNode q){
if(root == null) return false;
boolean left = isExist(root.left, p, q);
boolean right = isExist(root.right, p, q);
if((left&&right) || ((root.val==p.val || root.val==q.val)&&(left||right))){ //判断条件
ans = root;
}
return left || right || (root.val==p.val || root.val==q.val);
}
}
15.二叉树中的最大路径和(难度等级:困难)
这里给出我看到的一个比较好的解析,大家可以看一下逻辑是怎么捋的~
我们不要先去考虑整个递归的代码怎么去写,而是要明确一个递归的主体,就是这个递归的主体要怎么构造,然后再去想边界条件,返回值等等。
1、那么,首先我们可以假设走到了某个节点,现在要面临的问题是路径的最大值问题,显然对于这种问题,每遍历到一个节点,我们都要求出包含该节点在内的此时的最大路径,并且在之后的遍历中更新这个最大值。对于该节点来说,它的最大路径currpath就等于左右子树的最大路径加上本身的值,也就是currpath = left+right+node,val,但是有一个前提,我们要求的是最大路径,所以若是left或者right小于等于0了,那么我们就没有必要把这些值加上了,因为加上一个负数,会使得最大路径变小。这里的最大路径中的最其实就是一个限定条件,也就是我们常说的贪心算法,只取最大,最好,其余的直接丢弃。
2、好了,1中的主体我们已经明确了,但是还存在一个问题,那就是left和right具体应该怎么求,也就是left和right的递归形式。显然我们要把node.left和node.right再次传输到递归函数中,重复上述的操作。但如果到达了叶子节点,是不是需要往上一层返回了呢?那么返回值又是多少呢? 我们要明确left和right的基本含义,它们表示的是最大贡献,那么一个节点的最大贡献就等于node.val+max(left,right),这个节点本身选上,然后从它的左右子树中选择最大的那个加上。 对于叶子节点也是这样,但是叶子节点的左右子树都为空,所以加上0,哎,注意看,此时是不是边界条件也出来了,但节点为空时,返回0 。 好了,至此循环的主体,返回值,边界条件都定义好了,那么整个递归的代码是不是就水到渠成了。
代码写起来真的很简洁,所以最重要还是思想呀!
class Solution {
int max = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGet(root);
return max;
}
public int maxGet(TreeNode root){
if(root == null) return 0;
//若路径和已经小于0,则直接舍弃
int leftMax = Math.max(maxGet(root.left), 0); //左子树最大路径和
int rightMax = Math.max(maxGet(root.right), 0); //右子树最大路径和
int nowMax = leftMax + rightMax + root.val;
max = Math.max(nowMax, max);
return root.val + Math.max(leftMax, rightMax);
}
}
总结:
其实对于二叉树,关键就是要弄清楚采用递归还是迭代更方便?以及针对不同问题,要选择合适的遍历方式(前中后序OR层序遍历)。其实这块的难点,还是在于迭代法怎么写。