这几天在刷树相关的习题,今天来整体的总结一下关于树的题目。
首先给出了树的节点定义:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
树
Esay
1. 相同的树
100. 相同的树
我觉得对于树相关的习题,使用递归往往很有好处,可以只全局的考虑左子树和右子树的条件,然后进行合理的递归。对于相同的树这个题目,只需要判断两个树根节点相同,左子树右子树分别相同就可以决定两个树是相同的。
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null)return true;
if (p == null || q == null) return false;
if (p.val != q.val) return false;
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}
}
当然可以使用非递归,维护一个队列,按照层序遍历的思路来比较节点的相同。
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(p);
queue.add(q);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if (node1 == null && node2 == null) continue;
if (node1 == null || node2 == null) return false;
if (node1.val != node2.val) return false;
queue.add(node1.left);
queue.add(node2.left);
queue.add(node1.right);
queue.add(node2.right);
}
return true;
}
}
2.对称二叉树
101. 对称二叉树
和上面的思路一样,只是变成了对比左节点的右孩子和右节点的左孩子是否相等,连代码都只是简单改了改:
递归:
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return Symmetric(root.left,root.right);
}
public boolean Symmetric(TreeNode left, TreeNode right) {
if(left == null && right == null) return true;
if (left == null || right == null)return false;
if(left.val != right.val) return false;
return Symmetric(left.left,right.right) &&
Symmetric(left.right,right.left);
}
}
迭代方法:
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode>queue = new LinkedList<>();
queue.add(root);
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if(node1 == null && node2 == null) continue;
if(node1 == null || node2 == null) return false;
if(node1.val != node2.val) return false;
queue.add(node1.left);
queue.add(node2.right);
queue.add(node1.right);
queue.add(node2.left);
}
return true;
}
}
3. 二叉树最大深度
第一种同样是递归,每个节点的深度都是自己这个节点的1 在加上左子树右子树中的最大深度。
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}else{
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return 1 + Math.max(left,right);
}
}
}
第二种使用广度优先遍历,每到一层,层数加一,将该层的节点都加入队列
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
// bfs
Queue<TreeNode> queue = new LinkedList<>();
int depth = 0;
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode temp = queue.poll();
if (temp.left != null) {
queue.add(temp.left);
}
if (temp.right != null) {
queue.add(temp.right);
}
}
}
return depth;
}
}
4.二叉树的层序遍历 II
107. 二叉树的层次遍历 II
和层序遍历类似,只是由于是自底向上的打印,所以可以采用头插的方法。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root){
LinkedList<List<Integer>> ans = new LinkedList<>();
if (root == null)
return ans;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(! queue.isEmpty()){
List<Integer> list = new LinkedList<>();
int size = queue.size();
for (int i = 0; i < size; i ++){
TreeNode tmp = queue.poll();
list.add(tmp.val);
if (tmp.left != null) {
queue.add(tmp.left);
}
if (tmp.right != null) {
queue.add(tmp.right);
}
}
ans.addFirst(list);
}
return ans;
}
}
5.将有序数组转化为二叉搜索树
108. 将有序数组转换为二叉搜索树
这里需要注意的就是二叉搜索树的中序遍历是有序的! 本题中还说明了这棵树高度平衡,所以可以利用这两个个性质,得到根节点就是数组最中间的数(高度平衡)然后使用递归的方式分别构建左右子树,最后与根节点连接。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return nums == null ? null : buildTree(nums,0,nums.length - 1);
}
private TreeNode buildTree(int[] nums, int left, int right) {
if(left > right) {
return null;
}
int mid = left + (right - left) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildTree(nums,left,mid - 1);
root.right = buildTree(nums,mid + 1,right);
return root;
}
}
6.平衡二叉树
110. 平衡二叉树
还是递归,首先明确对于一棵树是平衡二叉树需要满足:
- 左子树和右子树的高度差步大于1
- 左子树也是一棵平衡二叉树
- 右子树也是一颗平衡二叉树
找到条件后,就是写递归,从分析中可以看出需要一个求高度的方法,所以我们再提供一个方法求高度。
class Solution {
private int height(TreeNode root) {
if (root == null) {
return -1;
}
return 1 + Math.max(height(root.left), height(root.right));
}
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
return Math.abs(height(root.left) - height(root.right)) < 2
&& isBalanced(root.left)
&& isBalanced(root.right);
}
}
7.路径总和
112. 路径总和
对这道题也是进行递归,要想返回true,就要符合:
- 节点的Val相加为给定的值
- 左子树为空
- 右子树为空
于是,同样写出递归方法,每次的传入的值是sum值减去走过的节点的值
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
return isTarget(root,sum);
}
private boolean isTarget(TreeNode root, int target){
if(root == null) return false;
if(root.val == target && root.left == null && root.right == null) {
return true;
}
return isTarget(root.left,target-root.val) ||
isTarget(root.right,target-root.val);
}
}
8. 二叉树的公共祖先
235. 二叉搜索树的最近公共祖先
首先二叉搜索树的特征就是左子树永远小于根节点,右子树永远大于根节点
接着这道题同样递归解决,从根节点开始遍历,逐步缩小范围:
- p 和 q 的值如果都大于根节点的值,说明都在根节点的右子树上,以右子树为根遍历
- p 和 q 的值如果都小于根节点的值,说明都在根节点的左子树上,以左子树为根遍历
- 都不符合,那只有当前根节点就是它们的祖先
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
int parentVal = root.val;
int pVal = p.val;
int qVal = q.val;
if (pVal > parentVal && qVal > parentVal) {
return lowestCommonAncestor(root.right, p, q);
} else if (pVal < parentVal && qVal < parentVal) {
return lowestCommonAncestor(root.left, p, q);
} else {
return root;
}
}
}
9. 二叉树的直径
543. 二叉树的直径
定义一个全局变量来存放节点之间的最大路径,遍历每一个节点作为根节点,那么最长的路径就是当前的节点的左子树的最大深度与右子树最大深度之和。比较该节点的最大路径值和原有最大路径值,选择最大的作为答案。
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
if(root == null) return 0;
depth(root);
return max;
}
public int depth(TreeNode root){
if(root == null){
return 0;
}
int left = depth(root.left);
int right = depth(root.right);
max = (left + right) > max ? (left + right) : max;
return 1 + Math.max(left,right);
}
}
10. 二叉树的所有路径
257. 二叉树的所有路径
本题要求求出根节点到叶节点的路径,很明显是一种深度优先遍历的方式,我们可以使用递归来解决:
- 当前节点不是叶子节点 —— 将当前节点加到路径中,并递归调用它的孩子节点
- 当前节点是叶子节点 —— 将当前节点加到路径中,得到的路径加到答案中
class Solution {
public void construct_paths(TreeNode root, String path, LinkedList<String> paths) {
if (root != null) {
path += Integer.toString(root.val);
if ((root.left == null) && (root.right == null)) // 当前节点是叶子节点
paths.add(path); // 把路径加入到答案中
else {
path += "->"; // 当前节点不是叶子节点,继续递归遍历
construct_paths(root.left, path, paths);
construct_paths(root.right, path, paths);
}
}
}
public List<String> binaryTreePaths(TreeNode root) {
LinkedList<String> paths = new LinkedList();
construct_paths(root, "", paths);
return paths;
}
}
以上就是自己筛选的一些LeetCode上树这一专题的简单题,大家还可以自己去多刷些,循序渐进。
Medium
1. 树的先序非递归遍历
对于树的三种遍历方式的递归写法,相信大多数人已经没问题,那对于非递归的写法,也是面试的常考点。先来看看先序非递归遍历。
我们模拟递归栈的效果,先将根节点入栈,然后只要栈不为空,就重复取出栈顶元素并将其左右子树入栈。
public static void preOrderNoRecur(Node head) {
if(head != null) {
Stack<Node> s = new Stack<Node>();
s.push(head); //压入根节点
while(!s.isEmpty()) {
head = s.pop(); //弹出节点
System.out.print(head.data+" ");
if(head.right != null) { //压入右孩子
s.push(head.right);
}
if(head.left != null) { //压入左孩子
s.push(head.left);
}
}
}
}
2.树的中序非递归遍历
中序遍历相对于先序会有一些复杂,不过也好理解,首先就是入栈根节点,接着不断压入左节点,直到找到最左孩子也就是中序遍历序列的第一个节点。接着逐个出栈打印(此时就相当于遍历上一个节点的根节点)接着将这个节点的右孩子入栈,重复这个步骤直至栈为空。
public static void inOrderNoRecur(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.data+" ");
head = head.right;
}
}
}
3.树的后序非递归遍历
树的非递归后续遍历更加复杂,它需要先遍历左子树再遍历右子树,最后遍历根节点。因为在遍历左子节点的过程我们也要记录下根节点,要通过这个根节点访问到右子树。
这里定义了 h 节点表示上一次打印的节点,c 表示每次的栈顶元素。
对于每一个栈顶的节点就有:
- 如果此节点的左孩子和右孩子都不是上一次打印过的节点,那说明下一次就应该继续将左孩子入栈(左孩子不为空的情况下)
- 如果此节点的左孩子是上一次打印过的,那就证明此节点的右孩子应该是下一个被入栈的节点,所以将右孩子入栈(右孩子不为空的情况下)
- 否则就是该节点的左右孩子都为空,或者就是该节点的左右孩子都被打印过了,此时出栈该节点并打印
按照这个流程就是一次非递归的后续遍历。
public void postorderNoRecur(Node head) {
Node h = head;
if (head != null) {
Stack<Node> stack = new Stack<Node>();
stack.push(head);
Node c = null;
while (! stack.isEmpty()) {
c = stack.peek();
if (c.left != null && h != c.left && h != c.right) {
stack.push(c.left);
} else if (c.right != null && h != c.right) {
stack.push(c.right);
} else {
System.out.print(stack.pop().val + " ");
h = c;
}
}
}
}
4. 不同的二叉搜索树
96. 不同的二叉搜索树
对于本题,只是需要返回可组成树的个数,事实上,对于每个数量的节点组成的不同二叉树的个数都是相同的。比如:1个节点有一种,两个节点有两种,三个节点有五种,所以本题我们可以使用动态规划的方法(如果不熟悉动态规划可以看——> 动态规划:适合新手的动态规划入门常见题目详解)
对于本题,可以将树分为左右两部分考虑:
对于每个数量,先将节点分为左右两部分(在分的时候就有一层循环就是内(j)循环),每个数量的节点可以组成的数量是左子树的钟类和右子树钟类的乘积,而且每次的分的方式得到的节点数量的总和就是dp的值。
在下面代码中,dp[i] 表示 数量为 i 时可以有的组合方式;dp[j] 表示分出的左子树的组成数量;dp[i - j + 1]相应就是右子树的数量(i 表示总节点数,j 表示分出的左节点个数,i - j - 1 就是右节点个数(-1 是减去了根节点))
class Solution {
public int numTrees(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i ++) {
for(int j = 0; j < i; j ++){
dp[i] += (dp[j] * dp[i - j - 1]);
}
}
return dp[n];
}
}
5. 不同的二叉搜索树 II
95. 不同的二叉搜索树 II
上一题的升级版,不是只要求 找出个数,而是直接返回所有的组合方式。
当然,我们还是使用递归,接着来考虑递归怎么写。
与上一题相似,还是将树分为左子树和右子树来构建。所以需要一个构建树的一个方法,这个方法的参数就是给定的树的数据区域。
在这个方法中再次递归产生左右子树,然后每次都将得到的左右子树的各种可能性都串起来。
class Solution {
public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new LinkedList<TreeNode>();
}
return buildTree(1, n);
}
private List<TreeNode> buildTree(int start,int end){
List<TreeNode> list = new LinkedList<>();
if (start > end){
list.add(null);
return list;
}
for(int i = start; i <= end; i ++){ //每次以i为root 分成左右两半
List<TreeNode> leftList = buildTree(start,i - 1); //得到左子树的所有可能情况
List<TreeNode> rightList = buildTree(i + 1, end); // 得到右子树的所有可能情况
//以i为根节点将左右子树合成一棵树
for(TreeNode l : leftList){
for(TreeNode r : rightList){
TreeNode tmp = new TreeNode(i);
tmp.left = l;
tmp.right = r;
list.add(tmp);
}
}
}
return list;
}
}
6.验证二叉搜索树
98.验证二叉搜索树
首先,题目上说明白了验证的三个条件,这种左子树右子树都是同样要求的很适合使用递归方法来解决。
对于根节点,它一定是左子树中的最大值,所以在验证左子树是否符合条件的时候,选用根节点的值作为upper(lower的值无所谓)。而对于右子树,它一定是右子树中的最小值,所以在验证右子树是否符合条件时,只需要选用根节点的值作为lower(upper的值无所谓)。
初始时,将root传入,upper和lower都为null,此后,只要upper不为null,说明传入了最大值,也就是判断的就是左子树是否符合条件,只需要当前节点的值小于upper就继续判断,否则就一定不是搜索树。反之亦然,对于lower不为null的情况,判断的就是右子树。
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, null, null);
}
public boolean helper(TreeNode node, Integer lower, Integer upper) {
if (node == null) return true;
int val = node.val;
if (lower != null && val <= lower) return false;
if (upper != null && val >= upper) return false;
if (! helper(node.right, val, upper)) return false;
if (! helper(node.left, lower, val)) return false;
return true;
}
}
迭代方式也可以做,采用中序遍历的思路。由于二叉搜索树的中序遍历总是有序的,所以我们可以先求得中序遍历的结果,在进行判断是否有序。
当然其实只需要证明每一个遍历到的节点一定比下一个遍历到的节点小就可以了。也就是本次遍历的一定比上一次的要大。
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
double inorder = -Double.MAX_VALUE;
while(!stack.isEmpty() || root != null) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if(root.val <= inorder) return false; //本次节点如果比上一次小,就false
inorder = root.val; //更新inorder的值便于下一次比较
root = root.right;
}
return true;
}
}
7.二叉树的层序遍历
102. 二叉树的层次遍历
和前面的题目有相似之处,使用Queue辅助完成层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
if (root == null) return ans;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
int level = 0;
while(!queue.isEmpty()) {
List<Integer> list = new ArrayList<>();
int size = queue.size();
for(int i = 0; i < size; i++) {
TreeNode tmp = queue.poll();
list.add(tmp.val);
if(tmp.left != null) {
queue.add(tmp.left);
}
if(tmp.right != null) {
queue.add(tmp.right);
}
}
ans.add(new ArrayList(list));
}
return ans;
}
}
8.二叉树的锯齿形层次遍历
103. 二叉树的锯齿形层次遍历
这个相对于直接的层序遍历要复杂些,因为使用锯齿型遍历,本层先遍历的节点的孩子节点在下一层要后遍历到,这符合我们所说的栈的先进后出的原理,所以我们采用两个栈来实现:每一层都在其中一层遍历所有节点而另一层则加入这一层的所有孩子节点,直到两个栈都为空。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
if (root == null) {
return list;
}
//栈1来存储右节点到左节点的顺序
Stack<TreeNode> stack1 = new Stack<>();
//栈2来存储左节点到右节点的顺序
Stack<TreeNode> stack2 = new Stack<>();
//根节点入栈
stack1.push(root);
//每次循环中,都是一个栈为空,一个栈不为空,结束的条件两个都为空
while (!stack1.isEmpty() || !stack2.isEmpty()) {
List<Integer> subList = new ArrayList<>(); // 存储这一个层的数据
TreeNode cur = null;
if (!stack1.isEmpty()) { //栈1不为空,则栈2此时为空,需要用栈2来存储从下一层从左到右的顺序
while (!stack1.isEmpty()) { //遍历栈1中所有元素,即当前层的所有元素
cur = stack1.pop();
subList.add(cur.val); //存储当前层所有元素
if (cur.left != null) { //左节点不为空加入下一层
stack2.push(cur.left);
}
if (cur.right != null) { //右节点不为空加入下一层
stack2.push(cur.right);
}
}
list.add(subList);
}else {//栈2不为空,则栈1此时为空,需要用栈1来存储从下一层从右到左的顺序
while (!stack2.isEmpty()) {
cur = stack2.pop();
subList.add(cur.val);
if (cur.right != null) {//右节点不为空加入下一层
stack1.push(cur.right);
}
if (cur.left != null) { //左节点不为空加入下一层
stack1.push(cur.left);
}
}
list.add(subList);
}
}
return list;
}
}
9.从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树
每次以先序遍历中第一个节点为根节点,在中序遍历中找到改节点的位置,以该点的位置就将整个区间划分为左右子树。然后将根节点和左右子树串联,继续递归
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length == 0 || inorder.length == 0) return null;
TreeNode root = new TreeNode(preorder[0]);
int i; //先序遍历的第一个节点在找到中序遍历中的位置
for (i = 0; i < inorder.length; i ++) {
if (inorder[i] == root.val) {
break;
}
}
//按这个位置将先序中序遍历分为左右子树继续遍历
int[] leftPreorder = Arrays.copyOfRange(preorder,1,i+1); //i+1 不包含
int[] rightPreorder = Arrays.copyOfRange(preorder,i+1,preorder.length);
int[] leftInorder = Arrays.copyOfRange(inorder,0,i);
int[] rightInorder = Arrays.copyOfRange(inorder,i+1,inorder.length);
root.left = buildTree(leftPreorder,leftInorder);
root.right = buildTree(rightPreorder,rightInorder);
return root;
}
}
10. 从中序与后序遍历序列构造二叉树
106.从中序与后序遍历序列构造二叉树
同上一题思路相似。后序遍历的最后一个节点就是整个树得根节点,
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
if(inorder.length==0) return null;
TreeNode root = new TreeNode(postorder[postorder.length-1]);
int i;
for (i = 0; i < inorder.length; i ++) {
if (inorder[i] == root.val) {
break;
}
}
int[] leftInorder = Arrays.copyOfRange(inorder,0,i);
int[] rightInorder = Arrays.copyOfRange(inorder,i+1,inorder.length);
int[] leftPostorder = Arrays.copyOfRange(postorder,0,i);
int[] rightPostorder = Arrays.copyOfRange(postorder,i,postorder.length-1);
root.left = buildTree(leftInorder,leftPostorder);
root.right = buildTree(rightInorder,rightPostorder);
return root;
}
}