文章目录
- (*中等)222. 完全二叉树的节点个数
- (*简单)110. 平衡二叉树
- (*简单)257. 二叉树的所有路径
- (简单)404. 左叶子之和
- (简单)513. 找树左下角的值
- (简单)112. 路径总和
- 由第112题 二叉树 递归 小结
- (中等)113. 路径总和II
- (*中等)106. 从中序与后序遍历序列构造二叉树
- (中等)105. 从前序与中序遍历序列构造二叉树
- (中等)654. 最大二叉树
- (简单)617. 合并二叉树
- (简单)700. 二叉搜索树中的搜索
- (中等)98. 验证二叉搜索树
- (简单)530. 二叉搜索树的最小绝对差
(*中等)222. 完全二叉树的节点个数
采用前序遍历的方式遍历该二叉树,然后统计该二叉树的节点个数
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int count = 0;
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
preOrder(root);
return count;
}
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
count++;
preOrder(root.left);
preOrder(root.right);
}
}
这种方式比较简单,但是没有利用题目中所给的完全二叉树这一信息
官方思路:二分查找+位运算
对于任意二叉树,都可以通过广度优先搜索或深度优先搜索计算节点个数,时间复杂度和空间复杂度都是O(n),其中n是二叉树的节点个数。这道题规定了给出的是完全二叉树,因此可以利用完全二叉树的特性计算节点个数。
规定根节点位于第0层,完全二叉树的最大层数为h。根据完全二叉树的特性可知,完全二叉树的最左边节点一定位于最底层,因此从根节点出发,每次访问左子节点,直到遇到叶子节点,该叶子节点即为完全二叉树的最左边的节点,经过的路径长度即为最大层数h。
当0<=i<h时,第i层包含 2 i 2^i 2i个节点,最底层包含的节点数量最少为1,最多为 2 h 2^h 2h
当最底层包含一个节点时,完全二叉树的节点个数是 ∑ i = 0 h − 1 2 i + 1 = 2 h \sum_{i=0}^{h-1}2^i+1=2^h ∑i=0h−12i+1=2h
当底层包含 2 h 2^h 2h个节点时,完全二叉树的节点个数是 ∑ i = 0 h 2 i = 2 h + 1 − 1 \sum_{i=0}^{h}2^i=2^{h+1}-1 ∑i=0h2i=2h+1−1
因此对于最大层数为h的完全二叉树,节点个数一定在 [ 2 h , 2 h + 1 − 1 ] [2^h,2^{h+1}-1] [2h,2h+1−1]的范围内,可以在该范围内通过二分查找的方式得到完全二叉树的节点个数。
具体做法是,根据节点个数范围的上下界得到当前需要判断的节点个数k,如果第k个节点存在,则节点个数一定大于或等于 k,如果第k个节点不存在,则节点个数一定小于k,由此可以将查找的范围缩小一半,直到得到节点个数。
如何判断第k个节点是否存在呢?如果第k个节点位于第h层,则k的二进制表示包含h+1位,其中最高位是1,其余各位从高到低表示从根节点到第k个节点的路径,0表示移动到左子节点,1表示移动到右子节点。通过位运算得到第k个节点对应的路径,判断该路径对应的节点是否存在,即可判断第k个节点是否存在
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
//根节点位于第0层
int level = 0;
TreeNode node = root;
//while循环是为了找出该完全二叉树的最底层节点位于第几层
while (node.left != null) {
++level;
node = node.left;
}
//该完全二叉树的节点个数的范围就在[low,high]之间
int low = 1 << level;
int high = (1 << (level + 1)) - 1;
//使用二分法确定具体节点个数
while (low < high) {
int mid = (high - low + 1) / 2 + low;
//判断,以root为根节点的二叉树,第level层是否有值为mid的节点
if (exists(root, level, mid)) {
low = mid;
} else {
high = mid - 1;
}
}
return low;
}
private boolean exists(TreeNode root, int level, int mid) {
int bits = 1 << (level - 1);
TreeNode node = root;
while (node != null && bits > 0) {
//举个简单例子,如果该二叉树一共有12个节点
//那么第8个节点到第12个节点都在第3层(根节点在第0层)
//从根节点到第8个节点这一层的路径长度为3
//所以,bits初始化为1<<(level-1),针对这个例子,bits=4=(100)
//如果(mid&bits)==0,说明mid的二进制形式的左边第一位为0,所以node要移动到左节点的位置
if ((mid & bits) == 0) {
node = node.left;
} else {
node = node.right;
}
//把(100)这个二进制形式的1向右移动一位,变成(010)
bits >>= 1;
}
return node != null;
}
}
复杂度分析:
- 时间复杂度:O(logn * logn),其中n是完全二叉树的节点数
- 首先需要O(h)的时间得到完全二叉树的最大层数,其中h是完全二叉树的最大层数。使用二分查找确定节点个数时,需要查找的次数为O( l o g 2 h log2^h log2h) = O(h),每次查找需要遍历从根节点开始的一条长度为h的路径,需要O(h)的时间,因此二分查找的总时间复杂度是O(h^2)。
- 因此,总时间复杂度是O(h^2)。由于完全二叉树满足 2 h ≤ n ≤ 2 h + 1 2^h\le n\le 2^{h+1} 2h≤n≤2h+1,因此有O(h)=O(logn),O(h^2)=O(logn * logn)
- 空间复杂度:O(1)。只需要维护有限的额外空间。
另一种思路
明确完全二叉树和满二叉树的定义
如果该二叉树是一个满二叉树,且层数为h,则总节点数为2^h-1(根节点位于第1层)
对root节点的左右子树进行高度统计,分别记为leftHeight和rightHeight,有以下两种结果
- leftHeight==rightHeight,这说明,左子树一定是满二叉树,因为节点已经填充到右子树了, 左子树必定已经填满。所以左子树的节点总数可以直接得到, 2 l e f t H e i g h t − 1 2^{leftHeight}-1 2leftHeight−1,加上root节点,正好是 2 l e f t H e i g h t 2^{leftHeight} 2leftHeight。再对右子树进行递归统计。
- leftHeight != rightHeight,说明此时最后一层不满,但是倒数第二层已经满了,可以直接得到右子树的节点个数,同理,右子树的节点数加上root节点,总数是 2 r i g h t H e i g h t 2^{rightHeight} 2rightHeight。再对左子树进行递归查找。
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int left = countLevel(root.left);
int right = countLevel(root.right);
if (left == right) {
return countNodes(root.right) + (1 << left);
} else {
return countNodes(root.left) + (1 << right);
}
}
private int countLevel(TreeNode root) {
int level = 0;
while (root != null) {
level++;
root = root.left;
}
return level;
}
}
(*简单)110. 平衡二叉树
自顶向下的递归
这道题中的平衡二叉树的定义是:二叉树的每个节点的左右子树的高度差的绝对值不超过1,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当所有子树也是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。
需要使用getHeight函数计算任一节点的高度。
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(getDepth(root.left), getDepth(root.right)) + 1;
}
有了计算节点高度的函数,即可判断二叉树是否平衡。具体做法类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差不超过1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归过程。
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
int rightDepth = getHeight(root.right);
int leftDepth = getHeight(root.left);
return Math.abs(leftDepth - rightDepth) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}
}
复杂度分析:
- 时间复杂度:O(
n
2
n^2
n2),其中n是二叉树中的节点个数
- 最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是O(n)。
- 对于节点p,如果它的高度是d,则height( p )最多会被调用d次(即遍历到它的每一个祖先节点时)。对于平均情况,一棵树的高度h满足O(h)=O(logn),因为d<=h,所以总的时间复杂度为O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为O(n),此时总时间复杂度为O( n 2 n^2 n2)。
- 空间复杂度:O(n),其中n是二叉树的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过n。
自底向上的递归
方法一由于是自顶向下的递归,因此对于同一个节点,函数height会被重复调用,导致时间复杂度较高。如果使用自底向上的做法,则对于每个节点,函数height只会被调用一次。
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回-1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
class Solution {
public boolean isBalanced(TreeNode root) {
return getHeight(root) >= 0;
}
public int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
if (leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
return Math.max(leftHeight, rightHeight) + 1;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是O(n)。
- 空间复杂度:O(n),其中n是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过n。
(*简单)257. 二叉树的所有路径
方法一:深度优先搜索
- 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
- 如果当前节点是叶子节点,则在当前路径末尾添加该节点后,就可以得到一条从根节点到叶子节点的路径,将该路径加入到答案即可。
如此,当遍历完整棵二叉树以后,就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现。
- 如果当前节点不是叶子节点,添加到stringbuilder中的是该节点的值和->(箭头),然后继续遍历该节点的左子树和右子树
- 如果当前节点是叶子节点,只将该节点的值添加到stringbuilder中,然后向结果集paths中添加该路径
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<>();
constructPaths(root, "", paths);
return paths;
}
private void constructPaths(TreeNode root, String path, List<String> paths) {
if (root != null) {
StringBuilder sb = new StringBuilder(path);
sb.append(root.val);
//如果当前节点是叶子节点
if (root.left == null && root.right == null) {
//将路径添加到答案中
paths.add(sb.toString());
}else {
sb.append("->");
constructPaths(root.left,sb.toString(),paths);
constructPaths(root.right,sb.toString(),paths);
}
}
}
}
方法二:广度优先搜索
维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时,广度优先搜索结束。
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<>();
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<String> pathQueue = new LinkedList<>();
nodeQueue.add(root);
pathQueue.add(Integer.toString(root.val));
while (!nodeQueue.isEmpty()) {
TreeNode node = nodeQueue.poll();
String path = pathQueue.poll();
if (node.left == null && node.right == null) {
paths.add(path);
} else {
if (node.left != null) {
nodeQueue.add(node.left);
pathQueue.add(path + "->" + node.left.val);
}
if (node.right != null) {
nodeQueue.add(node.right);
pathQueue.add(path + "->" + node.right.val);
}
}
}
return paths;
}
}
不论是DFS还是BFS,都是把已经走过的路径存起来,在遍历到下一个节点时在原有的基础上添加,而不是在回溯的时候,修改原来路径的字符串。
(简单)404. 左叶子之和
使用前序遍历的非递归方式,在向栈中添加元素时,判断该节点是否是左叶子节点。用变量flag来标识该节点是不是二叉树的左子节点(有可能是叶子节点,也有可能是非叶子节点)
按照前序遍历的顺序,当某一节点的左子节点为空时,就需要从栈中弹出栈顶节点,并继续遍历该节点的右子节点,如果该右子节点就是叶子节点,不用flag标记遍历加以区分的话,是无法区分该节点是左叶子还是右叶子
import java.util.Stack;
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode node = root.left;
int sum = 0;
boolean flag = true;
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
if (flag && node.left == null && node.right == null) {
sum += node.val;
}
node = node.left;
flag = true;
}
node = stack.pop().right;
flag = false;
}
return sum;
}
}
一个节点为【左叶子】节点,当且仅当它是某个节点的左子节点,并且是一个叶子节点。因此,可以考虑对整棵树进行遍历,让遍历到节点node时,如果它的左子节点是一个叶子节点,那么就将它的左子节点的值累加计入答案。
遍历整棵树的方法有深度优先搜索和广度优先搜索。
其他思路,方法一,深度优先遍历
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
return dfs(root);
}
public int dfs(TreeNode node) {
int ans = 0;
if (node.left != null) {
ans += isLeaf(node.left) ? node.left.val : dfs(node.left);
}
if (node.right != null) {
ans += dfs(node.right);
}
return ans;
}
public boolean isLeaf(TreeNode node) {
return node.left == null && node.right == null;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是树中的节点个数。
- 空间复杂度:O(n),空间复杂度与深度优先搜索使用的栈的最大深度相关。在最坏的情况下,树呈现链式结构,深度为O(n),对应的空间复杂度也为O(n)。
其他思路,方法二:广度优先搜索
采用层序遍历的方式,在向队列中添加该节点的左子节点时,判断该左子节点是不是叶子节点,如果是,则向答案中累加该节点的值。在向该队列中添加该节点的右子节点时,判断该右子节点是不是叶子节点,如果是,则不用向队列中添加该节点(因为不可能存在左叶子了)。
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
int res = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node.left != null) {
if (isLeaf(node.left)) {
res += node.left.val;
} else {
queue.add(node.left);
}
}
if (node.right != null) {
if (!isLeaf(node.right)) {
queue.add(node.right);
}
}
}
return res;
}
public boolean isLeaf(TreeNode node) {
return node.left == null && node.right == null;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是数中的节点个数。
- 空间复杂度:O(n),空间复杂度与广度优先搜索使用的队列需要的容量相关。
(简单)513. 找树左下角的值
思路是广度优先搜索的思想,使用变量res记录每一层的最左边节点的值。
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int res = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (i == 0) {
res = node.val;
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return res;
}
}
其他思路,方法一:深度优先搜索
使用height记录遍历到的节点的高度,curVal记录高度在curHeight的最左节点的值。在深度优先搜索时,先搜索当前节点的左子节点,再搜索当前节点的右子节点,然后判断当前节点的高度height是否大于curHeight,如果是,那么将curVal设置为当前节点的值,curHeight设置为height。
因为是先遍历左子树,再遍历右子树,所以对同一高度的所有节点,最左节点肯定是先被遍历到的。
class Solution {
int curVal = 0;
int curHeight = -1;
public int findBottomLeftValue(TreeNode root) {
dfs(root, 0);
return curVal;
}
public void dfs(TreeNode node, int height) {
if (node == null) {
return;
}
dfs(node.left, height + 1);
dfs(node.right, height + 1);
if (height > curHeight) {
curHeight = height;
curVal = node.val;
}
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是二叉树的节点数目。需要遍历n个节点。
- 空间复杂度:O(n),递归栈需要O(n)的空间,实际上栈的最大空间是二叉树的高度,平均是O(logn),如果二叉树呈现出链表的形式,就是O(n)
其他思路,方法二:广度优先搜索
使用广度优先搜索遍历每一层的节点。在遍历一个节点时,需要把它的非空右子节点放入队列,然后再把它的非空左子节点放入队列,这样才能保证从右到左遍历每一层的节点。广度优先搜索遍历的最后一个节点的值就是最底层最左边节点的值。
class Solution {
public int findBottomLeftValue(TreeNode root) {
int ret = 0;
Queue<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode p = queue.poll();
if (p.right != null) {
queue.offer(p.right);
}
if (p.left != null) {
queue.offer(p.left);
}
ret = p.val;
}
return ret;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是二叉树的节点个数。
- 空间复杂度:O(n),如果二叉树是满二叉树,那么队列最多保存 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉
(简单)112. 路径总和
深度优先搜索,设置一个flag变量,如果该树中存在根节点到叶子节点的路径,使得这条路径上所有节点值相加等于目标和 targetSum,flag设置为true。
class Solution {
boolean flag = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
dfs(root, root.val, targetSum);
return flag;
}
public void dfs(TreeNode node, int sum, int target) {
if (flag) {
return;
}
if (node.left == null && node.right == null) {
if (sum == target) {
flag = true;
return;
}
}
if (node.left != null) {
dfs(node.left, sum + node.left.val, target);
}
if (!flag && node.right != null) {
dfs(node.right, sum + node.right.val, target);
}
}
}
注意到本题的要求是,询问是否有从【根节点】到某一【叶子节点】经过的路径上的节点之和等于目标和。核心思想是对树进行一次遍历,在遍历时记录从根节点到当前节点的路径和,以防重复计算。
注意,树中节点数目的取值范围是[0,5000]
其他思路,方法一:广度优先遍历
可以想到使用广度优先搜索的方式,使用两个队列,分别存储将要遍历的节点,以及根节点到这些节点的路径和。
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
Queue<TreeNode> queueNode = new LinkedList<>();
Queue<Integer> queueVal = new LinkedList<>();
queueNode.add(root);
queueVal.add(root.val);
while (!queueNode.isEmpty()) {
TreeNode node = queueNode.poll();
int sum = queueVal.poll();
if (node.left == null && node.right == null) {
if (sum == targetSum) {
return true;
}
continue;
}
if (node.left != null) {
queueNode.add(node.left);
queueVal.add(sum + node.left.val);
}
if (node.right != null) {
queueNode.add(node.right);
queueVal.add(sum + node.right.val);
}
}
return false;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是树的节点数。对每个节点访问一次。
- 空间复杂度:O(n),其中n是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
其他思路,方法二:递归
观察题目要求完成的函数,可以归纳出它的功能:询问是否存在从当前节点root到叶子节点的路径,满足其路径和为sum
假定从根节点到当前节点的值之和为val,可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为sum-val。
如果当前节点是叶子节点,就直接判断sum是否等于val即可(因为路径和已经确定,就是当前结点的值)
如果当前节点不是叶子节点,只需要递归地询问它的子节点是否满足条件。
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
}
复杂度分析:
- 时间复杂度:O(N),其中N是树的节点数。对每个节点访问一次。
- 空间复杂度:O(H),其中H是树的高度,空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为O(logN)。
由第112题 二叉树 递归 小结
可以使用深度优先遍历的方式来遍历二叉树
1. 确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
递归函数什么时候需要返回值?什么时候不需要返回值?
- 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(参考LeetCode113)
- 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。(参考LeetCode236)
- 如果需要搜索一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径,就要及时返回
2. 确定终止条件
不建议去累加,然后判断是否等于目标和,这样代码会比较麻烦,可以使用递减,让技术及初始就为目标和,然后每次减去遍历路径节点上的数值。如果count最后等于0,同时找到了叶子节点的话,说明找到了目标和。
3. 确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
(中等)113. 路径总和II
广度优先搜索,创建两个队列,一个队列记录节点,一个队列记录路径
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
Queue<TreeNode> nodeQueue = new LinkedList<>();
nodeQueue.add(root);
Queue<List<Integer>> resQueue = new LinkedList<>();
ArrayList<Integer> list = new ArrayList<>();
list.add(root.val);
resQueue.add(list);
while (!nodeQueue.isEmpty()) {
TreeNode curNode = nodeQueue.poll();
List<Integer> curList = resQueue.poll();
if (curNode.left == null && curNode.right == null) {
int sum = 0;
for (Integer i : curList) {
sum += i;
}
if (sum == targetSum) {
res.add(curList);
}
continue;
}
if (curNode.left != null) {
nodeQueue.add(curNode.left);
ArrayList<Integer> tmp = new ArrayList<>(curList);
tmp.add(curNode.left.val);
resQueue.add(tmp);
}
if (curNode.right != null) {
nodeQueue.add(curNode.right);
ArrayList<Integer> tmp = new ArrayList<>(curList);
tmp.add(curNode.right.val);
resQueue.add(tmp);
}
}
return res;
}
}
注意本题的要求是,找到所有满足从【根节点】到某个根节点经过的路径上的节点之和等于目标和的路径。核心思想是对树进行一次遍历,在遍历时记录从根节点到当前节点的路径和,以防止重复计算。
其他思路,方法一:深度优先搜索
可以采用深度优先搜索的方式,枚举每一条从根节点到叶子节点的路径。当我们遍历到叶子节点,且此时路径和恰为目标和时,就找到了一条满足条件的路径。
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
class Solution {
List<List<Integer>> res = new LinkedList<>();
Deque<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return res;
}
public void dfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
path.offerLast(root.val);
if (root.left == null && root.right == null && targetSum == root.val) {
res.add(new LinkedList<>(path));
}
dfs(root.left, targetSum - root.val);
dfs(root.right, targetSum - root.val);
path.pollLast();
}
}
复杂度分析:
- 时间复杂度:O( N 2 N^2 N2),其中N是树的节点数。在最坏的情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径数目为O(N),并且每一条路径的节点个数也为O(N),因此要将这些路径全部添加进答案中,时间复杂度为O( N 2 N^2 N2)。
- 空间复杂度:O(N),其中N是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
其他思路,方法二:广度优先搜索
也可以采用广度优先搜索来遍历这棵树,当遍历到叶子节点,且此时的路径恰为目标和时,就找到了一条满足条件的路径。
为了节省空间,使用哈希表记录树中的每一个节点的父节点。每次找到一个满足条件的节点,就从该节点出发不断向父节点迭代,即可还原出从根节点到当前节点的路径。
import java.util.*;
class Solution {
//结果列表,需要返回的
List<List<Integer>> res = new LinkedList<>();
//HashMap用来记录子节点和父节点的对应关系
Map<TreeNode, TreeNode> map = new HashMap<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
if (root == null) {
return res;
}
Queue<TreeNode> queueNode = new LinkedList<>();
Queue<Integer> queueSum = new LinkedList<>();
queueNode.add(root);
queueSum.add(root.val);
while (!queueNode.isEmpty()) {
TreeNode node = queueNode.poll();
int sum = queueSum.poll();
if (node.left == null && node.right == null) {
if (sum == targetSum) {
getPath(node);
}
continue;
}
if (node.left != null) {
map.put(node.left, node);
queueNode.add(node.left);
queueSum.add(sum + node.left.val);
}
if (node.right != null) {
map.put(node.right, node);
queueNode.add(node.right);
queueSum.add(sum + node.right.val);
}
}
return res;
}
public void getPath(TreeNode node) {
List<Integer> tmp = new LinkedList<>();
while (node != null) {
tmp.add(node.val);
node = map.get(node);
}
Collections.reverse(tmp);
res.add(new LinkedList<>(tmp));
}
}
复杂度分析:
- 时间复杂度:O( N 2 N^2 N2),其中N是树的节点数。在最坏的情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径数目为O(N),并且每一条路径的节点个数也为O(N),因此要将这些路径全部添加进答案中,时间复杂度为O( N 2 N^2 N2)。
- 空间复杂度:O(N),其中N是树的节点数。空间复杂度主要取决于哈希表和队列空间的开销,哈希表需要存储除根节点外的每个节点的父节点,队列中的元素个数不会超过树的节点数。
(*中等)106. 从中序与后序遍历序列构造二叉树
首先,解决这道问题需要明确,给定一棵二叉树,是如何对其进行中序遍历和后序遍历的
- 中序遍历:左、根、右 的顺序
- 后序遍历:左、右、根 的顺序
后序遍历的数组最后一个元素代表的即为根节点。知道这个性质后,可以利用已知的根节点信息在中序遍历的数组中找到根节点所在的下标,然后根据其中序遍历的数组分成左右两部分,左边部分即左子树,右边部分即右子树,针对每个部分可以用同样的的方法继续递归下去构造。
- 为了高效查找根节点元素在中序遍历数组中的下标,选择创建哈希表来存储中序序列,即建立一个(元素,下标)键值对的哈希表
- 定义递归函数helper(in_left,in_right)表示当前递归到中序序列中当前字数的左右边界,递归入口为helper(0,n-1)
- 如果in_left>in_right,说明子树为空,返回空节点
- 选择后序遍历的最后一个节点作为根节点
- 利用哈希表在O(1)内查询当前根节点在中序遍历中的下标为index。从in_left到index-1属于左子树,从index+1到in_right属于右子树
- 根据后序遍历逻辑,递归地创建右子树helper(index+1,in_right)和左子树helper(in_left,index-1)。注意!这里需要先创建右子树,再创建左子树的依赖关系。可以理解为再后序遍历的数组中整个数组是先存储左子树的节点,再存储右子树的节点,最后存储根节点,如果每次选择【后序遍历的最后一个节点】为根节点,则先被构造出来的应该为右子树。
- 返回根节点root
import java.util.HashMap;
import java.util.Map;
class Solution {
Map<Integer, Integer> map = new HashMap<>();
int postIdx;
int inorderLen;
int postorderLen;
int[] inorder;
int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
inorderLen = inorder.length;
postorderLen = postorder.length;
this.inorder = inorder;
this.postorder = postorder;
//从后序遍历的最后一个元素开始
postIdx = postorderLen - 1;
for (int i = 0; i < inorderLen; i++) {
map.put(inorder[i], i);
}
return build(0, inorderLen - 1);
}
public TreeNode build(int inLeft, int inRight) {
if (inLeft > inRight) {
return null;
}
//当前子树根节点的值
int rootVal = postorder[postIdx];
//获取该节点在中序遍历数组中的索引,将数组分为左、右两部分
int rootIdx = map.get(rootVal);
//后序遍历索引减一,构建右子树
postIdx--;
TreeNode rightNode = build(rootIdx + 1, inRight);
TreeNode leftNode = build(inLeft, rootIdx - 1);
return new TreeNode(rootVal, leftNode, rightNode);
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是树中的节点个数。
- 空间复杂度:O(n),需要O(n)的空间存储哈希表,以及O(h)(h是树的高度)的空间表示递归时栈空间,这里h<n,所以总空间复杂度是O(n)。
其他思路,方法二:迭代
因为我是先看了105题的迭代思路,依旧是照葫芦画瓢,在105题中是根据前序和中序遍历构建二叉树,前序遍历的顺序是根、左、右,中序遍历的顺序是左、根、右,首先使用栈先将左节点添加到栈中,直到栈顶元素与inorder数组中inorderIndex指针指向的值一样时,则说明该节点已经没有左子节点,那么就需要把栈中的元素弹出,移动inorderIndex指针,直到栈为空或者inorderIndex指向的值与栈顶元素不同时,那么该节点就是刚刚弹出节点的右儿子
106这一题,可以将中序遍历和后序遍历的数组反转一下
后序遍历反转后的顺序:根、右、左
中序遍历反转后的顺序:右、根、左
和给定前序遍历和中序遍历的解题思路完全一样,唯一不同的就是105题是先构建左子树,而106题是先构建右子树
import java.util.Stack;
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
reverseArray(inorder);
reverseArray(postorder);
TreeNode root = new TreeNode(postorder[0]);
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < postorder.length; i++) {
int postorderVal = postorder[i];
TreeNode node = stack.peek();
if (node.val != inorder[inorderIndex]) {
node.right = new TreeNode(postorderVal);
stack.push(node.right);
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
node = stack.pop();
inorderIndex++;
}
node.left = new TreeNode(postorderVal);
stack.push(node.left);
}
}
return root;
}
//数组反转
public void reverseArray(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
}
(中等)105. 从前序与中序遍历序列构造二叉树
参照上一题的模板,照葫芦画瓢
对于任意一棵树而言,前序遍历的形式总是:
[ 根节点,[左子树的前序遍历结果],[右子树的前序遍历结果]]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是:
[[左子树的中序遍历结果],根节点,[右子树的中序遍历结果]]
只要在中序遍历中定位到根节点,就可以知道左子树和右子树中的节点数目。
由于一棵子树的前序遍历和中序遍历的长度显然是相同的,因此可以对应到前序遍历的结果中,对上述形式中的所有的左右括号进行定位。
这样,就知道了左子树前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,就可以递归地构造出左子树和右子树,再将这两棵子树接到根节点的左右位置。
中序遍历对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,这样做的时间复杂度较高。可以考虑使用哈希表快速定位到根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示器载中序遍历中出现的位置。在构造二叉树的过程之前,可以对中序遍历的数组进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,就只需要O(1)的时间对根节点进行定位了。
import java.util.HashMap;
import java.util.Map;
class Solution {
int[] preorder;
int[] inorder;
int idx;
Map<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
this.inorder = inorder;
idx = 0;
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return build(0, inorder.length - 1);
}
public TreeNode build(int inLeft, int inRight) {
if (inLeft > inRight) {
return null;
}
int rootVal = preorder[idx];
idx++;
int rootIdx = map.get(rootVal);
TreeNode leftNode = build(inLeft, rootIdx - 1);
TreeNode rightNode = build(rootIdx + 1, inRight);
return new TreeNode(rootVal, leftNode, rightNode);
}
}
import java.util.HashMap;
import java.util.Map;
class Solution {
Map<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
for (int i = 0; i < n; i++) {
map.put(inorder[i], i);
}
return build(preorder, inorder, 0, n - 1, 0, n - 1);
}
public TreeNode build(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return null;
}
//前序遍历中,第一个节点就是根节点
int preorder_root_index = preorder_left;
//在中序遍历中定位根节点的位置
int inorder_root_index = map.get(preorder[preorder_root_index]);
//建立根节点
TreeNode root = new TreeNode(preorder[preorder_root_index]);
//得到左子树中的节点数目
int size_left_subtree = inorder_root_index - inorder_left;
//递归地构造左子树,并连接到根节点
//先序遍历中【从左边界+1开始的size_left_subtree】个元素对应了中序遍历中【从 左边界 到 根节点定位-1】的元素
root.left = build(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root_index - 1);
//递归地构造右子树,并连接到根节点
//先序遍历中【从 左边界+左子树节点数目+1 到 右边界】的元素就对应了中序遍历中【从 根节点定位+1 到 右边界】的元素
root.right = build(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root_index + 1, inorder_right);
return root;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是树中的节点个数
- 空间复杂度:O(n),除了返回答案需要的O(n)空间之外,还需要O(n)的空间存储哈希映射,以及O(h)(其中h是树的高度)的空间表示递归时的栈空间。这里h<n,所以总空间复杂度为O(n)。
迭代
对于前序遍历中的任意两个连续节点u和v,根据前序遍历的流程,可以知道u和v只有两种可能的关系:
- v是u的左儿子。这是因为在遍历到u之后,下一个遍历的节点就是u的左儿子,即v
- u没有左儿子,并且v是u或者u的某个祖先节点的右儿子。
- 如果u没有左儿子,那么下一个遍历的节点就是u的右儿子。
- 如果u没有右儿子,就向上回溯,直到遇到第一个有右儿子的节点( u a u_a ua),那么v就是 u a u_a ua的右儿子。
举例:
3
/ \
9 20
/ / \
8 15 7
/ \
5 10
/
4
preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]
作者:LeetCode-Solution
链接:https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/cong-qian-xu-yu-zhong-xu-bian-li-xu-lie-gou-zao-9/
来源:力扣(LeetCode)
用一个栈stack来维护【当前节点的所有还没有考虑过右儿子的祖先节点】,栈顶就是当前节点,只有在栈中的节点才可能连接一个新的右儿子。
也就是说这个栈记录的节点,前一个节点是后一个节点的父节点,后一个节点是前一个节点的左儿子
还需要一个指针index指向中序遍历的某个位置,初始值为0。index对应的节点是【当前节点不断往左走达到的最终节点】。
该指针的意思,举个例子说明
比如二叉树是一条链的形式,情况一,那么从栈中弹出的顺序和中序遍历的顺序是一样的。情况二,在弹出栈时,顺序是4,5,8,9,3,而中序遍历是4,5,10,8,9,3。在5和8之间多了一个10,那么10就一定是5的右儿子。
情况一:
3
/
9
/
8
/
5
/
4
情况二:
3
/
9
/
8
/
5
/ \
4 10
具体算法:
- 用一个栈和一个指针辅助进行二叉树的构造。初始时栈中存放了根节点(前序遍历的第一个节点),指针指向中序遍历的第一个节点
- 依次枚举前序遍历中除了第一个节点以外的每个节点。如果index恰好指向栈顶节点,那就不断地弹出栈顶结点,并向右移动index,并将当前节点作为最后一个弹出的节点的左儿子;如果index和栈顶节点不同,将当前节点作为栈顶节点的右儿子
- 无论是哪一种情况,最后都将当前的节点入栈
import java.util.Stack;
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
TreeNode root = new TreeNode(preorder[0]);
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.length; i++) {
int preorderVal = preorder[i];
TreeNode node = stack.peek();
if (node.val != inorder[inorderIndex]) {
//把节点放入栈中,放的都是node节点的左子节点
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
//当栈顶元素等于中序遍历中的元素值时,说明左子节点已经遍历完了
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
node = stack.pop();
inorderIndex++;
}
//当遇到栈顶元素与中序遍历中的元素不同时,中序遍历的指向的元素一定是右子节点,也可以使用inorder[inorderIndex]作为TreeNode构造器中的值,不过,可能会出现数组角标越界的情况,所以使用preorderVal是更好的选择
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n是树中的节点个数
- 空间复杂度:O(n),除去返回答案需要的O(n)空间之外,还需要O(h)(其中h是树的高度)的空间存储栈。这里h<n,所以最坏的情况下总空间复杂度是O(n)。
(中等)654. 最大二叉树
需要做三步工作
- 先要找到数组中最大的值和对应的下标,最大的值构造根节点,下标用来下一步分割数组
- 最大值所在的下标左区间 构造左子树,这里需要判断最大值的下标是否大于0,因为要保证左区间至少有一个数
- 最大值所在的下标右区间 构造右子树,这里需要判断最大值的下标是否小于nums.size()-1,确保右区间至少有一个数值
Q:什么时候递归函数前面加if,什么时候不加if?
A:其实就是不同代码风格的实现,一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下,终止条件也会相应的调整
class Solution {
int[] nums;
public TreeNode constructMaximumBinaryTree(int[] nums) {
this.nums = nums;
return build(0, nums.length - 1);
}
public TreeNode build(int left, int right) {
if (left > right) {
return null;
}
int maxVal = nums[left];
int maxIndex = left;
for (int i = left + 1; i <= right; i++) {
if (maxVal < nums[i]) {
maxVal = nums[i];
maxIndex = i;
}
}
TreeNode root = new TreeNode(maxVal);
root.left = build(left, maxIndex - 1);
root.right = build(maxIndex + 1, right);
return root;
}
}
复杂度分析:
- 时间复杂度:O( n 2 n^2 n2),其中n是数组nums的长度。在最坏情况下,数组严格递增或递减,需要递归n层,第 i ( 0 ≤ i < n 0\le i\lt n 0≤i<n)层需要遍历n-i个元素找出最大值,所以总的时间复杂度是O( n 2 n^2 n2)
- 空间复杂度:O(n),即为最坏情况下需要使用的栈空间
(简单)617. 合并二叉树
深度优先搜索
从根节点开始同时遍历两棵二叉树,并将对应的节点进行合并。
两棵二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
- 如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空
- 如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点
- 如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树对应结点的值之和,此时需要显性合并两个两个节点。
对一个节点进行合并后,还要对该节点的左右子树分别进行合并。
首先题目所给的需要完成的mergeTrees(TreeNode t1, TreeNode t2)函数,就是要合并两个根为t1和t2的二叉树,合并的过程,是创建一棵新的二叉树的过程
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) {
return root2;
}
if (root2 == null) {
return root1;
}
TreeNode merge = new TreeNode(root1.val + root2.val);
merge.left = mergeTrees(root1.left, root2.left);
merge.right = mergeTrees(root1.right, root2.right);
return merge;
}
}
复杂度分析:
- 时间复杂度:O(min(m,n)),其中m和n分别是两个二叉树的节点个数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会对该节点进行显性合并操作,因此被访问到的节点数不会超过较小的二叉树的节点数。
- 空间复杂度:O(min(m,n)),其中m和n分别是两个二叉树的节点个数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过最小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。
广度优先搜索
首先判断两个二叉树是否为空:
- 如果两个二叉树都为空,则合并后的二叉树也为空
- 如果只有一个二叉树为空,则合并后的二叉树为另一个非空的二叉树
- 如果两个二叉树都不为空,则首先计算合并后的根结点的值,然后从合并后的二叉树与两个原始二叉树的根节点开始广度优先搜索,从根节点开始同时遍历每个二叉树,并将对应的节点进行合并
使用三个队列分别存储合并后的二叉树的节点以及两个原始二叉树的节点。初始时,将每个二叉树的根节点分别加入相应的队列。每次从每个队列中取出一个节点,判断两个原始二叉树的节点的左右子节点是否为空。如果两个原始二叉树的当前节点中至少有一个节点的左子节点不为空,则合并后的二叉树的对应节点的左子节点也不为空。对于右子节点,同理。
如果合并后的二叉树的左子节点不为空,则需要根据两个原始二叉树的左子节点计算合并后的二叉树的左子节点以及整个左子树。考虑一下两种情况:
- 如果两个原始二叉树的左子节点不为空,则合并狗的二叉树的左子节点的值为两个原始二叉树的左子节点的值之和,将每个二叉树中的左子节点都加入相应的队列;
- 如果两个原始二叉树的左子节点有一个为空,即有一个原始二叉树的左子树为空,则合并后哦的二叉树的左子树即为另一个原始二叉树的左子树,此时也不需要对非空左子树继续遍历,因此不需要将左子节点加入队列
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) {
return root2;
}
if (root2 == null) {
return root1;
}
Queue<TreeNode> queue = new LinkedList<>();
Queue<TreeNode> queue1 = new LinkedList<>();
Queue<TreeNode> queue2 = new LinkedList<>();
queue1.add(root1);
queue2.add(root2);
TreeNode merge = new TreeNode(root1.val + root2.val);
queue.add(merge);
while (!queue1.isEmpty()) {
TreeNode node = queue.poll();
TreeNode node1 = queue1.poll();
TreeNode node2 = queue2.poll();
if (node1.left != null && node2.left != null) {
node.left = new TreeNode(node1.left.val + node2.left.val);
queue.add(node.left);
queue1.add(node1.left);
queue2.add(node2.left);
} else {
if (node1.left != null) {
node.left = node1.left;
} else if (node2.left != null) {
node.left = node2.left;
}
}
if (node1.right != null && node2.right != null) {
node.right = new TreeNode(node1.right.val + node2.right.val);
queue.add(node.right);
queue1.add(node1.right);
queue2.add(node2.right);
} else {
if (node1.right != null) {
node.right = node1.right;
} else if (node2.right != null) {
node.right = node2.right;
}
}
}
return merge;
}
}
复杂度分析:
- 时间复杂度:O(min(m,n)),其中m和n分别是两个二叉树的节点个数。对两个二叉树同时进行广度优先搜索,只有当两个二叉树中的对应节点都不为空时,才会访问到该节点,因此被访问到的节点数不会超过较小的二叉树的节点数。
- 空间复杂度:O(min(m,n)),其中m和n分别是两个二叉树的节点个数。O(min(m,n)),其中m和n分别是两个二叉树的节点个数。空间复杂度取决于队列中的元素个数,队列中的元素个数不会超过较小的二叉树的节点数。
(简单)700. 二叉搜索树中的搜索
递归
二叉搜索树满足如下性质:
- 左子树所有节点的元素值均小于根的元素值
- 右子树所有节点的元素值均大于根的元素值
据此可以得到如下算法:
- 若root为空,则返回空节点
- 若root.val==val,则返回root
- 若val>root.val,递归右子树
- 若val<root.val,递归左子树
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null) {
return null;
}
if (val == root.val) {
return root;
}
if (val > root.val) {
return searchBST(root.right, val);
} else {
return searchBST(root.left, val);
}
}
}
复杂度分析:
- 时间复杂度:O(N),其中N是二叉搜索树的节点数。最坏情况下二叉搜索树是一条链,且要找的元素比链末尾的元素值还要小(大),这种情况下我们需要递归N次。
- 空间复杂度:O(N),最坏情况下递归需要O(N)的栈空间。
其他思路,迭代
将上一种方法的递归改成迭代:
- 若root为空则跳出循环,并返回空节点
- 若val=root.val,则返回root
- 若val<root.val,将root置为root.left
- 若val>root.val,将root置为root.right
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
while (root != null) {
if (val == root.val) {
return root;
}
root = val > root.val ? root.right : root.left;
}
return null;
}
}
复杂度分析:
- 时间复杂度:O(N),其中N是二叉搜索树的节点数。最坏情况下二叉搜索树是一条链,且要找的元素比末尾的元素值还要小(大),这种情况下需要迭代N次。
- 空间复杂度:O(1)。没有使用额外的空间。
(中等)98. 验证二叉搜索树
先对该二叉树进行中序遍历,记录下中序遍历的结果,在检查该中序遍历序列是不是单调递增的,如果是,则返回true,如果不是,返回false。
import java.util.ArrayList;
import java.util.List;
class Solution {
List<Integer> list;
public boolean isValidBST(TreeNode root) {
list = new ArrayList<>();
inorder(root);
for (int i = 1; i < list.size(); i++) {
if (list.get(i - 1) >= list.get(i)) {
return false;
}
}
return true;
}
public void inorder(TreeNode root) {
if (root == null) {
return;
}
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
}
其他思路,递归
二叉搜索树的性质:如果该二叉树的左子树不为空,则左子树上所有节点的值均小于它的根节点的值;如果它的右子树不为空,则右子树上左右结点的值均大于它的根节点的值;它的左右子树也为二叉搜索树
设计一个递归函数helper(root,lower,upper)来递归判断,函数表示考虑以root为根的子树,判断子树中所有节点是否都在(l,r)的范围内 (注意是开区间) 。如果root节点的值不在(l,r)的范围内说明不满足条件直接返回,否则要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。
那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界upper改为root.val,即调用helper(root.left, lower, root.val),因为左子树里所有的节点的值均小于它的根节点的值。右子树同理。
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) {
return true;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
需要注意的是,isValidBST(TreeNode node, long lower, long upper),需要使用long类型。2147483647=2^31-1,是需要返回true,但是在代码中,如果形参的位置不改成int,那么会返回false。
较特殊测试用例:
[2147483647]
复杂度分析:
- 时间复杂度:O(n),其中n为二叉树的节点个数。在递归调用的时候二叉树每个节点最多被访问一次,因此时间复杂度为O(n)。
- 空间复杂度:O(n),其中n为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为n,递归最深达n层,故最坏情况下空间复杂度为O(n)
其他思路,方法二:中序遍历
二叉搜索树【中序遍历】得到的值构成的序列一定是升序的,所以,在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。如果均大于,说明这个序列是升序的,整棵树是二叉搜索树,否则不是。
import java.util.Stack;
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
Long inorder = Long.MIN_VALUE;
stack.push(root);
TreeNode node = root.left;
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
if (inorder >= node.val) {
return false;
}
inorder = (long) node.val;
node = node.right;
}
return true;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为O(n)。
- 空间复杂度:O(n),其中n为二叉树的节点个数。栈最多存储n个节点,因此,需要额外的O(n)的空间。
(简单)530. 二叉搜索树的最小绝对差
中序遍历该二叉搜索树,并把节点值按照中序遍历的顺序添加到ArrayList中,边添加边比较相邻两个元素的差值的绝对值
import java.util.ArrayList;
import java.util.Stack;
class Solution {
public int getMinimumDifference(TreeNode root) {
ArrayList<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode node = root.left;
int val = Integer.MAX_VALUE;
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
if (list.isEmpty()) {
list.add(node.val);
} else {
val = Math.min(val, Math.abs(list.get(list.size() - 1) - node.val));
list.add(node.val);
}
node = node.right;
}
return val;
}
}
其他思路,也是中序遍历
对于升序数组a求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值
其中n为数组a的长度。其他任意间隔距离大于等于2的下标对(i,j)的元素之差一定大于下标对(i,i+1)的元素之差,故不需要被考虑。
本题目要求二叉搜索树任意两节点差的绝对值的最小值,而我们知道二叉搜索树有个性质:二叉搜索树中序遍历得到的值序列是递增有序的,因此我们只要得到中序遍历后的之序列即能用相邻元素求差值的方法进行求解。
朴素的方法是经过一次中序遍历将值保存在一个数组中再进行遍历求解,也可以在中序遍历的过程中用pre变量保存前驱节点的值,这样既能边遍历边更新答案,不再需要显式创建数组来保存,需要注意的是pre的初始值需要设置成任意负数标记开头。
import java.util.Stack;
class Solution {
public int getMinimumDifference(TreeNode root) {
//用于记录中序遍历中当前节点的前一个节点的值
int preVal = -1;
//使用非递归的方式实现中序遍历
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
TreeNode node = root.left;
int val = Integer.MAX_VALUE;
while (!stack.isEmpty() || node != null) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
if (preVal != -1) {
val = Math.min(val, Math.abs(node.val - preVal));
}
preVal = node.val;
node = node.right;
}
return val;
}
}
下面是使用递归形式
class Solution {
int pre;
int ans;
public int getMinimumDifference(TreeNode root) {
ans = Integer.MAX_VALUE;
pre = -1;
dfs(root);
return ans;
}
public void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
if (pre != -1) {
ans = Math.min(ans, Math.abs(root.val - pre));
}
pre = root.val;
dfs(root.right);
}
}
复杂度分析:
- 时间复杂度:O(n),其中n为二叉搜索树节点的个数。每个节点在中序遍历中都会被访问一次且只会被访问一次,因此总时间复杂度为O(n)
- 空间复杂度:O(n),递归函数的空间复杂度取决于递归的栈深度,而栈深度在二叉搜索树为一条链的情况下会达到O(n)级别。