文章目录
- 一、深度优先遍历(DFS)
- 二、广度优先遍历(BFS)
- 三、100. 相同的树
- 四、101. 对称二叉树
- 五、104. 二叉树的最大深度
- 六、111. 二叉树的最小深度
- 七、112. 路径总和
- 八、113. 路径总和 II
- 九、257. 二叉树的所有路径
- 十、404. 左叶子之和
- 十一、110. 平衡二叉树
- 十二、108. 将有序数组转换为平衡二叉搜索树
- 十三、235. 二叉搜索树的最近公共祖先
- 十四、530. 二叉搜索树的最小绝对差
- 十五、124. 二叉树中的最大路径和
- 十六、面试题 04.12. 求和路径
- 十七、98. 验证二叉搜索树
- 十八、99. 恢复二叉搜索树
- 十九、102. 二叉树的层序遍历
- 二十、105. 从前序与中序遍历序列构造二叉树
- 二十一、106. 从中序与后序遍历序列构造二叉树
- 二十二、107. 二叉树的层序遍历 II
- 二十三、109. 有序链表转换二叉搜索树
- 二十四、129. 求根节点到叶节点数字之和
- 二十五、116. 填充每个节点的下一个右侧节点指针
- 二十六、173. 二叉搜索树迭代器
- 二十七、222. 完全二叉树的节点个数
之前说到,算法是有框架的,刷题可以从二叉树开始,
因为二叉树是最容易培养框架思维的,而且大部分算法技巧,本质上都是树的遍历问题。
/* 基本的二叉树节点 */
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;
}
}
一、深度优先遍历(DFS)
深度优先搜索(DFS):主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。
1.1、二叉树遍历框架(递归)
二叉树遍历框架,典型的非线性递归遍历结构(深度优先搜索)。
public static void recursionTraversal(TreeNode root) {
if (root == null) {
return;
}
//前序遍历
//System.out.println(root.getVal());
recursionTraversal(root.getLeft());
//中序遍历
//System.out.println(root.getVal());
recursionTraversal(root.getRight());
//后序遍历
System.out.println(root.getVal());
}
框架在这,做题时就试着用这个来解题。
1.2、二叉树遍历框架(迭代)
虽然递归遍历用的最多,但只会一种方式也不好,来看一下迭代的方式。
它还是深度优先遍历。
两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同,具体实现可以看下面的代码。
整体思路还是比较清晰的,使用栈来将要遍历的节点压栈,然后出栈后检查此节点是否还有未遍历的节点,有的话压栈,没有的话不断回溯(出栈)。
package com.wlw.binaryTree;
import com.wlw.base.TreeNode;
import java.util.Deque;
import java.util.LinkedList;
/**
* 二叉树的遍历:前序、中序、后序 (迭代)
* 先根次序:访问根结点,遍历左子树,遍历右子树。
* 中根次序:遍历左子树,访问根结点,遍历右子树。
* 后根次序:遍历左子树,遍历右子树,访问根结点。
*
* 栈的性质,先进后出,只需看下Java里对于他的方法有哪些
* Stack的入栈和出栈的操作:
* ①把元素压栈:push(E);
* ②把栈顶的元素“弹出”:pop(E);
* ③取栈顶元素但不弹出:peek(E)。
*
* 在Java中,我们用Deque可以实现Stack的功能:
* ①把元素压栈:push(E)/addFirst(E);
* ②把栈顶的元素“弹出”:pop(E)/removeFirst();
* ③取栈顶元素但不弹出:peek(E)/peekFirst()。
*
* Java的集合类并没有单独的Stack接口,因为有个遗留类名字就叫Stack,
* 出于兼容性考虑,所以没办法创建Stack接口,
* 只能用Deque接口来“模拟”一个Stack了。
* 当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,那样会破坏栈的本质。
*/
public class TraversalOfBinaryTree {
public static void main(String[] args) {
TreeNode treeNode1 = new TreeNode(1);
TreeNode treeNode2 = new TreeNode(2);
TreeNode treeNode3 = new TreeNode(3);
TreeNode treeNode4 = new TreeNode(4);
TreeNode treeNode5 = new TreeNode(5);
treeNode1.setLeft(treeNode2);
treeNode1.setRight(treeNode3);
treeNode2.setLeft(treeNode4);
treeNode2.setRight(treeNode5);
//preTraversal(treeNode1);
//midTraversal(treeNode1);
postorderTraversal(treeNode1);
}
// 前序遍历
/**
* 思路:一开始先将根节点入栈,确保栈中有个元素,不空,可以让循环正常结束
* 循环体:先出栈(弹出父节点),将右、左孩子结点一次入栈(因为栈是先进后出,要让左孩子先出。)
*/
public static void preTraversal(TreeNode root) {
//栈
Deque<TreeNode> stk = new LinkedList<TreeNode>();
//入栈
stk.push(root);
while (!stk.isEmpty()) {
//出栈
TreeNode node = stk.pop();
System.out.println(node.getVal());
//先将右孩子结点入栈,再左孩子结点入栈(因为栈是先进后出)
if (node.getRight() != null) {
stk.push(node.getRight());
}
if (node.getLeft() != null) {
stk.push(node.getLeft());
}
}
}
// 中序遍历
/**
* 思路:因为要首先打印左子树,所以在一个栈中先插入根节点,然后一直插入左子树,直到左子树为空,
* 开始打印(入栈循环体)。
* 这样栈顶元素一定是左节点,
* 然后出栈打印,
* 再将右子树按照之前的方法插入到栈中
*/
public static void midTraversal(TreeNode root) {
//栈
Deque<TreeNode> stk = new LinkedList<TreeNode>();
while (root != null || !stk.isEmpty()) {
//入栈循环体
while (root != null) {
stk.push(root);
root = root.getLeft();
}
//出栈
root = stk.pop();
System.out.println(root.getVal());
//切换右孩子
root = root.getRight();
}
}
// 后序遍历
/**
* 思路:和中序遍历一样因为要先打印左子树所以入栈循环体不变,
* 不同之处在于要先打印右子树,后打印根节点,
* 打印根节点的前提是判断上次打印的节点是右子树还是左子树,
* 如果是右子树,直接打印当前节点,
* 如果是左子树则需要进入右子树,之后再打印该父节点,
* 因此我们定义一个前驱节点来记录上次打印的节点,如果这节点不等于右子树则需要进入右子树
*/
public static void postorderTraversal(TreeNode root) {
//栈
Deque<TreeNode> stk = new LinkedList<TreeNode>();
TreeNode currentNode = root;
TreeNode preNode = null;
//入栈循环体
while (currentNode != null) {
stk.push(currentNode);
currentNode = currentNode.getLeft();
}
while (!stk.isEmpty()) {
//出栈
TreeNode node = stk.pop();
//当前节点有右孩子,并且上一个出栈的不是该节点的右孩子,就把当前节点和当前节点的右子树放进栈中
if (node.getRight() != null && node.getRight() != preNode) {
stk.push(node);
currentNode = node.getRight();
//入栈循环体
while (currentNode != null) {
stk.push(currentNode);
currentNode = currentNode.getLeft();
}
} else {
System.out.println(node.getVal());
preNode = node;
}
}
}
}
二、广度优先遍历(BFS)
广度优先遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。
也就是一层一层的去遍历,所以广度优先遍历也叫层序遍历。
深度优先遍历用的是栈,而广度优先遍历要用队列来实现。
先看一下Queue 中的方法
public interface Queue<E> extends Collection<E> {
//将指定的元素插入此队列(如果立即可行且不会违反容量限制),在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。
boolean add(E e)
//将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
boolean offer(E e)
//获取,但是不移除此队列的头。
E element()
//获取但不移除此队列的头;如果此队列为空,则返回 null。
E peek();
//获取并移除此队列的头,如果此队列为空,则返回 null。
E poll();
//获取并移除此队列的头。
E remove();
}
以二叉树为例
package com.wlw.binaryTree;
import com.wlw.base.TreeNode;
import java.util.LinkedList;
import java.util.Queue;
/**
* 广度优先遍历
*/
public class bfsTest {
public static void main(String[] args) {
TreeNode treeNode1 = new TreeNode(1);
TreeNode treeNode2 = new TreeNode(2);
TreeNode treeNode3 = new TreeNode(3);
TreeNode treeNode4 = new TreeNode(4);
TreeNode treeNode5 = new TreeNode(5);
treeNode1.setLeft(treeNode2);
treeNode1.setRight(treeNode3);
treeNode2.setLeft(treeNode4);
treeNode2.setRight(treeNode5);
bfs(treeNode1);
}
private static void bfs(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> stack = new LinkedList<>();
stack.add(root);
while (!stack.isEmpty()) {
TreeNode node = stack.poll();
System.out.println("value = " + node.getVal());
System.out.println("------------------");
if (node.getLeft() != null) {
stack.add(node.getLeft());
}
if (node.getRight() != null) {
stack.add(node.getRight());
}
}
}
}
/*
value = 1
------------------
value = 2
------------------
value = 3
------------------
value = 4
------------------
value = 5
------------------
*/
三、100. 相同的树
问题:给你两棵二叉树的根节点 p
和 q
,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
解决思路:这就是一个简单的遍历,在遍历过程中去判断节点是否相等。
第一种方法:递归遍历,与遍历一棵树不同的是,同时遍历两棵树
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
} else if (p == null || q == null) {
return false;
} else if (p.val != q.val) {
return false;
} else {
//同时遍历两棵树,比较两个节点是否相等
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
}
第二种方法:迭代遍历,维护一个栈,与遍历一棵树不同的是,同时入栈两个元素,然后出栈比较,再入栈两个左孩子,两个右孩子。
class Solution {
// 套用的是 迭代的前序遍历模板
public boolean isSameTree(TreeNode p, TreeNode q) {
Deque<TreeNode> stack = new LinkedList<TreeNode>();
//入栈:一次入两
stack.push(p);
stack.push(q);
while(!stack.isEmpty()) {
//一次出两
q = stack.pop();
p = stack.pop();
//比较
if (p == null && q == null) {
continue;
} else if (p == null || q == null) {
return false;
} else if (p.val != q.val) {
return false;
}
//入栈:一次入两
stack.push(p.left);
stack.push(q.left);
//入栈:一次入两
stack.push(p.right);
stack.push(q.right);
}
return true;
}
}
四、101. 对称二叉树
问题:给你一个二叉树的根节点 root
, 检查它是否轴对称。
解决思路:和 [100. 相同的树] 很像,最终都是检查两个节点是否一样,不同的是这次检查的是对称的两个节点。所以在遍历时,就要指定遍历的节点。
第一种方法:递归遍历
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
} else {
//递归的方式
return recursionHandle(root.left, root.right);
}
}
public boolean recursionHandle(TreeNode leftNode, TreeNode rightNode) {
if (leftNode == null && rightNode == null) {
return true;
} else if (leftNode == null || rightNode == null) {
return false;
} else if (leftNode.val != rightNode.val) {
return false;
} else {
//让左树的左孩子节点 与 右树的右孩子节点做对比
//让左树的右孩子节点 与 左树的右孩子节点做对比
return recursionHandle(leftNode.left, rightNode.right) && recursionHandle(leftNode.right, rightNode.left);
}
}
}
第二种方法:迭代遍历
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
} else {
//迭代的方式
return check(root.left, root.right);
}
}
// 套用的是 迭代的前序遍历模板
public boolean check(TreeNode leftNode, TreeNode rightNode) {
Deque<TreeNode> stack = new LinkedList<TreeNode>();
//入栈:一次入两
stack.push(leftNode);
stack.push(rightNode);
while(!stack.isEmpty()) {
//出栈:一次出两
rightNode = stack.pop();
leftNode = stack.pop();
if (leftNode == null && rightNode == null) {
continue;
} else if (leftNode == null || rightNode == null) {
return false;
} else if (leftNode.val != rightNode.val) {
return false;
}
//入栈:让左树的左孩子节点 与 右树的右孩子节点做对比
stack.push(leftNode.left);
stack.push(rightNode.right);
//入栈:让左树的右孩子节点 与 左树的右孩子节点做对比
stack.push(leftNode.right);
stack.push(rightNode.left);
}
return true;
}
}
五、104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
第一种:递归,深度优先遍历
思路:如果我们知道了左子树和右子树的最大深度 l和 r,那么该二叉树的最大深度即为max(l,r) + 1 ,而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
} else {
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
}
第二种:广度优先遍历
思路:我们也可以用「广度优先搜索」的方法来解决这道题目,但我们需要对其进行一些修改,此时我们广度优先搜索的队列里存放的是「当前层的所有节点」。每次拓展下一层的时候,不同于广度优先搜索的每次只从队列里拿出一个节点,我们需要将队列里的所有节点都拿出来进行拓展,这样能保证每次拓展完的时候队列里存放的是当前层的所有节点,即我们是一层一层地进行拓展,最后我们用一个变量depth 来维护拓展的次数,该二叉树的最大深度即为depth 。
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<TreeNode>();
deque.add(root);
int depth = 0;
while(!deque.isEmpty()) {
int size = deque.size();
for (int i = size; i > 0; i--) {
TreeNode node = deque.poll();
if (node.left != null) {
deque.add(node.left);
}
if (node.right != null) {
deque.add(node.right);
}
}
depth++;
}
return depth;
}
}
六、111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
第一种:递归,深度优先遍历DFS
思路:首先可以想到使用深度优先搜索的方法,遍历整棵树,记录最小深度。对于每一个非叶子节点,我们只需要分别计算其左右子树的最小叶子节点深度。这样就将一个大问题转化为了小问题,可以递归地解决该问题。
复杂度分析:
- 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
- 空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)。
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 计算左子树的深度
int leftDepth = minDepth(root.left);
// 计算右子树的深度
int rightDepth = minDepth(root.right);
// 如果左子树或右子树的深度不为 0,即存在一个子树,那么当前子树的最小深度就是该子树的深度+1
if (root.left == null || root.right == null) {
return leftDepth + rightDepth + 1;
} else {
// 如果左子树和右子树的深度都不为 0,即左右子树都存在,那么当前子树的最小深度就是它们较小值+1
return Math.min(leftDepth, rightDepth) + 1;
}
}
}
另外一种DFS
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
if (root.left == null && root.right == null) {
return 1;
}
int min_depth = Integer.MAX_VALUE;
if (root.left != null) {
min_depth = Math.min(minDepth(root.left), min_depth);
}
if (root.right != null) {
min_depth = Math.min(minDepth(root.right), min_depth);
}
return min_depth + 1;
}
}
第二种:BFS 广度优先遍历
思路:同样,我们可以想到使用广度优先搜索的方法,遍历整棵树。
当我们找到一个叶子节点时,直接返回这个叶子节点的深度。广度优先搜索的性质保证了最先搜索到的叶子节点的深度一定最小。
复杂度分析:
- 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
- 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
class Solution {
class QueueNode{
TreeNode node;
int depth;
public QueueNode(TreeNode node, int depth){
this.node = node;
this.depth = depth;
}
}
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<QueueNode> queue= new LinkedList<QueueNode>();
//入队列
queue.offer(new QueueNode(root, 1));
while (!queue.isEmpty()) {
//出队
QueueNode queueNode = queue.poll();
TreeNode node = queueNode.node;
int depth = queueNode.depth;
//如果叶子节点,结束,返回结果
if (node.left == null && node.right == null) {
return depth;
}
//左孩子入队
if (node.left != null) {
queue.offer(new QueueNode(node.left, depth + 1));
}
//右孩子入队
if (node.right != null) {
queue.offer(new QueueNode(node.right, depth + 1));
}
}
return 0;
}
}
七、112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点:是指没有子节点的节点。
第一种:递归,深度优先遍历DFS:
解题思路:此题看上去是要做加法,但是转换为做减法会更简单。我们去遍历路径,也就是遍历节点,每遍历到一个节点,就用目标值减去当前节点的值,直到遍历到叶子节点(也就是没孩子节点的节点),看当前做过减法后的目标值与该叶子节点的值是否相等,如果有相等的,返回true。
复杂度分析:
-
时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
-
空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)。
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);
}
}
第二种:迭代,广度优先遍历BFS:
解题思路:使用广度优先搜索的方式,记录从根节点到当前节点的路径和,以防止重复计算。这样我们使用两个队列,分别存储将要遍历的节点,以及根节点到这些节点的路径和即可。
可以做加法,也可以做减法。
复杂度分析:
- 时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
- 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
//使用两个队列
class Solution {
//做加法
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
Queue<TreeNode> treeQueue = new LinkedList<TreeNode>();
Queue<Integer> targetQueue = new LinkedList<Integer>();
//入队列
treeQueue.offer(root);
targetQueue.offer(root.val);
while (!treeQueue.isEmpty()) {
TreeNode node = treeQueue.poll();
int sum = targetQueue.poll();
//先去判断当前节点是否还有孩子节点,
//再去判断当前遍历过的节点之和 与 目标值
if (node.left == null && node.right == null) {
if (sum == targetSum) {
return true;
}
continue;
}
if (node.left != null) {
treeQueue.offer(node.left);
targetQueue.offer(sum + node.left.val);
}
if (node.right != null) {
treeQueue.offer(node.right);
targetQueue.offer(sum + node.right.val);
}
}
return false;
}
}
//只使用一个队列,创建一个新类
//做减法
class Solution {
class QueueNode{
TreeNode node;
int value;
public QueueNode(TreeNode node, int value) {
this.node = node;
this.value = value;
}
}
//做减法
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
Queue<QueueNode> queue = new LinkedList<QueueNode>();
//入队列
queue.offer(new QueueNode(root, targetSum));
while (!queue.isEmpty()) {
//出队列
QueueNode queueNode = queue.poll();
TreeNode node = queueNode.node;
int sum = queueNode.value;
//先去判断当前节点是否还有孩子节点,
//再去判断当前叶子节点的值与目标值
if (node.left == null && node.right == null) {
if (node.val == sum) {
return true;
}
continue;
}
if (node.left != null) {
queue.offer(new QueueNode(node.left, sum - node.val));
}
if (node.right != null) {
queue.offer(new QueueNode(node.right, sum - node.val));
}
}
return false;
}
}
八、113. 路径总和 II
题:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。
解题思路:与112题相似,在112题的基础上找出所有满足条件的路径,所以我们需要一个容器(这里用的集合)来存放遍历到叶子节点的路径。
第一种方法:深度优先遍历DFS,递归
复杂度分析:
- 时间复杂度:O(N^2),其中 N是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。
- 空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
class Solution {
//深度优先遍历DFS
//返回最终结果
List<List<Integer>> result = new ArrayList<List<Integer>>();
//记录遍历的路径
List<Integer> list = new ArrayList<Integer>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return result;
}
public void dfs(TreeNode root, int targetSum){
if (root == null) {
return;
}
list.add(root.val);
//判断是否 是叶子节点,是否满足题意中的条件
if (root.left == null && root.right == null && root.val == targetSum) {
result.add(new ArrayList(list));
}
targetSum = targetSum - root.val;
dfs(root.left, targetSum);
dfs(root.right, targetSum);
//把最后一个节点去掉,去遍历另外一条路径
list.remove(list.size() - 1);
}
}
九、257. 二叉树的所有路径
问题:给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。
解题思路:这道题就是遍历二叉树,本身不难,遍历到叶子节点返回路径即可,注意的是怎么来记录路径。
第一种:递归,深度优先遍历DFS
复杂度分析:
- 时间复杂度:O(N^ 2),其中 NN 表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N^2)。
- 空间复杂度:O(N^ 2),其中 N表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 N,此时每一层的 path 变量的空间代价的总和为O(N^ 2), 空间复杂度为 O(N^ 2)。最好情况下,当二叉树为平衡二叉树时,它的高度为logN,此时空间复杂度为 O((log N)^2)
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
dfs(root, "", result);
return result;
}
public void dfs (TreeNode root, String path,List<String> result) {
if (root == null) {
return;
}
StringBuffer pathSb = new StringBuffer(path);
pathSb.append(Integer.toString(root.val));
//到达叶子节点
if (root.left == null && root.right == null) {
result.add(pathSb.toString());
} else {
pathSb.append("->");
dfs(root.left, pathSb.toString(), result);
dfs(root.right, pathSb.toString(), result);
}
}
}
第二种:广度优先遍历BFS
复杂度分析:
- 时间复杂度:O(N^2),其中 N表示节点数目。分析同方法一。
- 空间复杂度:O(N^ 2),其中 N 表示节点数目。在最坏情况下,队列中会存在 N 个节点,保存字符串的队列中每个节点的最大长度为 NN,故空间复杂度为 O(N^ 2)。
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
Queue<String> strPath = new LinkedList<String>();
//入
queue.offer(root);
strPath.offer(Integer.toString(root.val));
while (!queue.isEmpty()) {
TreeNode node = queue.poll();//出
String path = strPath.poll();//出
//叶子节点
if (node.left == null && node.right == null) {
result.add(path);
} else {
if (node.left != null) {
queue.offer(node.left);
strPath.offer(new StringBuffer(path).append("->").append(node.left.val).toString());
}
if (node.right != null) {
queue.offer(node.right);
strPath.offer(new StringBuffer(path).append("->").append(node.right.val).toString());
}
}
}
return result;
}
}
十、404. 左叶子之和
题:给定二叉树的根节点 root ,返回所有左叶子之和。
解题思路:遍历二叉树即可,到“左叶子”节点,把这种节点的值加起来。
第一种方法:DFS深度优先遍历,递归
复杂度分析:
- 时间复杂度:O(n),其中 n 是树中的节点个数。
- 空间复杂度:O(n)。空间复杂度与深度优先搜索使用的栈的最大深度相关。在最坏的情况下,树呈现链式结构,深度为 O(n),对应的空间复杂度也为 O(n)。
class Solution {
int sum = 0;
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) {
return 0;
}
//左叶子节点的判断
if (root.left != null && root.left.left == null && root.left.right == null) {
sum += root.left.val;
}
sumOfLeftLeaves(root.left);
sumOfLeftLeaves(root.right);
return sum;
}
}
第二种方法:BFS 广度优先遍历
复杂度分析:
- 时间复杂度:O(n),其中 n 是树中的节点个数。
- 空间复杂度:O(n)。空间复杂度与广度优先搜索使用的队列需要的容量相关,为 O(n)。
class Solution {
// 重点在于怎么判断出是左叶子节点
// BFS
public int sumOfLeftLeaves(TreeNode root) {
// 对根结点进行判定
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
int sum = 0;
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node.left != null) {
//左孩子节点是叶子节点
if (node.left.left == null && node.left.right == null) {
sum = sum + node.left.val;
} else {
//此else判断也可以不加
queue.offer(node.left);
}
}
if (node.right != null) {
//右孩子不是叶子结点,继续循环
//此判断也可以不加
if (!(node.right.left == null && node.right.right == null)) {
queue.offer(node.right);
}
}
}
return sum;
}
}
十一、110. 平衡二叉树
题:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
解题思路:有两个要点:1、平衡二叉树的所有子树也是平衡二叉树,2、判断平衡二叉树的条件:每个节点 的左右两个子树的高度差的绝对值不超过 1
第一种解法:DFS 深度优先遍历,递归,自底向上.
自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 -1−1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
复杂度分析:
- 时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。
- 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
class Solution {
// 此题与 求二叉树的高度相似
//DFS 自底向上,套用框架,相当于后序遍历
public boolean isBalanced(TreeNode root) {
return dfs(root) >= 0;
}
public int dfs (TreeNode node) {
if (node == null) {
return 0;
}
//计算左子树的深度
int leftDepth = dfs(node.left);
//计算右子树的深度
int rightDepth = dfs(node.right);
//node节点的左右两个子树的高度差 > 1 ,不符合题意,返回-1
//左子树或者右子树的深度 为-1,也是不符合题意
if (Math.abs(leftDepth - rightDepth) > 1 || leftDepth == -1 || rightDepth ==-1) {
return -1;
} else {
//返回的是一个节点的最大深度
return Math.max(leftDepth, rightDepth) + 1;
}
}
}
第二种方法:DFS,自顶向下。
类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。由于是自顶向下递归,因此对于同一个节点,函数 height 会被重复调用,导致时间复杂度较高
复杂度分析:
- 时间复杂度:O(n^2),其中 n 是二叉树中的节点个数。
- 空间复杂度:O(n)O,其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
class Solution {
// 此题与 求二叉树的高度相似
//DFS 自顶向下
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
} else {
return Math.abs(height(root.left) - height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
}
//计算一个节点的深度
public int height (TreeNode node) {
if (node == null) {
return 0;
}
//计算左子树的深度
int leftDepth = height(node.left);
//计算右子树的深度
int rightDepth = height(node.right);
//返回的是一个节点的最大深度
return Math.max(leftDepth, rightDepth) + 1;
}
}
十二、108. 将有序数组转换为平衡二叉搜索树
题:给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
解题: 根节点选用升序数组中间数,这样就把数组分成了左右两部分,那根节点的左节点就是左部分数组的中间数,根节点的右节点就是右部分数组的中间数,依次递归
复杂度分析:
- 时间复杂度:O(n),其中 n 是数组的长度。每个数字只访问一次。
- 空间复杂度:O(logn),其中 n 是数组的长度。空间复杂度不考虑返回值,因此空间复杂度主要取决于递归栈的深度,递归栈的深度是 O(logn)。
class Solution {
//转成一个平衡二叉树
//根节点选用升序数组中间数,这样就把数组分成了左右两部分
//那根节点的左节点就是左部分数组的中间数,根节点的右节点就是右部分数组的中间数,依次递归
public TreeNode sortedArrayToBST(int[] nums) {
return recursion(nums, 0, nums.length - 1);
}
public TreeNode recursion(int[] nums, int left, int right) {
if (left > right) {
return null;
}
int mid = (left + right)/2;
TreeNode node = new TreeNode(nums[mid]);
node.left = recursion(nums, left, mid-1);
node.right = recursion(nums, mid+1, right);
return node;
}
}
十三、235. 二叉搜索树的最近公共祖先
题:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
解题:因为是二叉搜索树,是排好序的,所以我们可以根据qp与根节点的大小关系来找出答案,如果一个比根节点大,一个比根节点小,那根节点就是答案,不管qp是都大于根节点,还是都小于根节点,我们总能找到上面那个“根节点”。
第一种方法:递归,DFS,套用模板,同时遍历q,p,找到一个大于q,小于p的目标节点。
复杂度分析:
- 时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。
- 空间复杂度:O(1)。
class Solution {
//DFS,递归
//同时遍历q,p
//因为是二叉搜索树,是排好序的,如果q>root,p<root,那结果就是root;如果qp都<root,继续判断root.left;如果qp都>root,继续判断root.right。循环下去就可找到了
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}
return root;
}
}
//不用递归
class Solution {
//同时遍历q,p
//因为是二叉搜索树,是排好序的,如果q>root,p<root,那结果就是root;如果qp都<root,继续判断root.left;如果qp都>root,继续判断root.right。循环下去就可找到了
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode node = root;
while (true) {
if (p.val < node.val && q.val < node.val) {
node = node.left;
} else if (p.val > node.val && q.val > node.val) {
node = node.right;
} else {
break;
}
}
return node;
}
}
第二种方法:分别找到根节点到目标节点的路径集合,对比两个路径集合,有相同的则找到答案。
复杂度分析:
- 时间复杂度:O(n),其中 n 是给定的二叉搜索树中的节点个数。上述代码需要的时间与节点 p和 q 在树中的深度线性相关,而在最坏的情况下,树呈现链式结构,p 和 q 一个是树的唯一叶子结点,一个是该叶子结点的父节点,此时时间复杂度为O(n)。
- 空间复杂度:O(n),我们需要存储根节点到 p 和 q 的路径。和上面的分析方法相同,在最坏的情况下,路径的长度为O(n),因此需要 O(n) 的空间。
class Solution {
//找到根节点到目标节点的路径,
//对比两个路径集合,有相同的则找到答案
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
List<TreeNode> pPath = getPath(root, p);
List<TreeNode> qPath = getPath(root, q);
TreeNode result = null;
//对比是否有相同节点
for (int i = 0; i < pPath.size() && i < qPath.size(); i++) {
if (pPath.get(i) == qPath.get(i)) {
result = pPath.get(i);
} else {
break;
}
}
return result;
}
//找路径
public List<TreeNode> getPath(TreeNode root, TreeNode target) {
List<TreeNode> path = new ArrayList<TreeNode>();
TreeNode node = root;
while(target.val != node.val) {
path.add(node);
if (target.val < node.val) {
node = node.left;
} else {
node = node.right;
}
}
//到达目标节点
path.add(node);
return path;
}
}
十四、530. 二叉搜索树的最小绝对差
题:给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。差值是一个正数,其数值等于两值之差的绝对值。
解:因为是二叉搜索树,中序遍历出来的是一个升序序列,对一个升序数组,求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值;用一个全局变量来记录上一次遍历到的节点 与 当前节点 进行减法。用一个全局变量来记录最小差值,在遍历二叉树的过程中比较差值,取最小的。
复杂度分析:
- 时间复杂度:O(n),其中 n 为二叉搜索树节点的个数。每个节点在中序遍历中都会被访问一次且只会被访问一次,因此总时间复杂度为 O(n)。
- 空间复杂度:O(n)。递归函数的空间复杂度取决于递归的栈深度,而栈深度在二叉搜索树为一条链的情况下会达到 O(n) 级别。
class Solution {
// 因为是二叉搜索树,中序遍历出来的是一个升序序列,对一个升序数组,求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值
// 用一个全局变量来记录最小差值,在遍历二叉树的过程中比较差值,取最小的。
// 用一个变量来记录上一次遍历到的节点 与 当前节点 进行减法。
int result = Integer.MAX_VALUE;
TreeNode pre = null;
public int getMinimumDifference(TreeNode root) {
dfs(root);
return result;
}
public void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
if (pre == null) {
pre = root;
} else {
result = Math.min(result, root.val - pre.val);
pre = root;
}
dfs(root.right);
}
}
十五、124. 二叉树中的最大路径和
题:路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
解题思路:dfs,递归遍历,后序遍历,自底向上,我们就可以得到每个节点的最大路径和,维护一个全局变量存储最大路径和,在递归过程中更新该值,最后得到的值即为二叉树中的最大路径和。
还是套用模板。
复杂度分析:
- 时间复杂度:O(N),其中 N 是二叉树中的节点个数。对每个节点访问不超过 2次。
- 空间复杂度:O(N),其中 N 是二叉树中的节点个数。空间复杂度主要取决于递归调用层数,最大层数等于二叉树的高度,最坏情况下,二叉树的高度等于二叉树中的节点个数。
class Solution {
//记录当前最大路径和
int result = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfsMax(root);
return result;
}
//dfs 递归寻找一个节点的最大路径和:(根+左+右)
public int dfsMax(TreeNode root) {
if(root == null) {
return 0;
}
//返回当前节点的 左节点 的 最大值
int leftSum = Math.max(0, dfsMax(root.left));
//返回当前节点的 右节点 的 最大值
int rightSum = Math.max(0,dfsMax(root.right));
//当前节点的 最大路径和
int nodeSum = root.val + leftSum + rightSum;
//对比,当前 与 之前记录的最大路径和,选大的。
result = Math.max(result, nodeSum);
//返回当前节点的 与 左右两节点。之间 最大的路径和。
return Math.max(leftSum, rightSum) + root.val;
}
}
十六、面试题 04.12. 求和路径
题目链接
题:给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。
解:由题目最后一句话可知,此题要前序遍历,自上而下,但要是条件是:从根节点到叶子节点,那么此题就差不多和112. 路径总和 很是相似了,但是题意是从任意节点开始到任意节点结束。所以需要对每个节点都进行前序遍历。也就是双dfs。
class Solution {
int result = 0;
public int pathSum(TreeNode root, int sum) {
if(root == null) {
return result;
}
// 找结果
dfs(root, sum);
// 对左节点进行遍历
pathSum(root.left, sum);
// 对右节点进行遍历
pathSum(root.right, sum);
return result;
}
//对一个节点进行遍历
public void dfs(TreeNode root, int sum) {
if (root == null) {
return;
}
sum = sum - root.val;
if (sum == 0) {
result++;
}
dfs(root.left, sum);
dfs(root.right, sum);
}
}
十七、98. 验证二叉搜索树
题:给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
第一种方法:递归,前序遍历
解:这启示我们设计一个递归函数 helper(root, lower, upper) 来递归判断,函数表示考虑以 root 为根的子树,判断子树中所有节点的值是否都在 (l,r)的范围内(注意是开区间)。如果 root 节点的值 val 不在 (l,r) 的范围内说明不满足条件直接返回,否则我们要继续递归调用检查它的左右子树是否满足,如果都满足才说明这是一棵二叉搜索树。那么根据二叉搜索树的性质,在递归调用左子树时,我们需要把上界 upper 改为 root.val,即调用 helper(root.left, lower, root.val),因为左子树里所有节点的值均小于它的根节点的值。同理递归调用右子树时,我们需要把下界 lower 改为 root.val,即调用 helper(root.right, root.val, upper)。
复杂度分析:
- 时间复杂度 : O(n),其中 n为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
- 空间复杂度 : O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 OO(n) 。
class Solution {
public boolean isValidBST(TreeNode root) {
return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
// dfs 判断当前节点
public boolean dfs(TreeNode root, long small, long big){
if(root == null) {
return true;
}
// 判断值,
if (root.val <= small || root.val >= big) {
//不符合题意
return false;
}
//左节点是小的, ,右节点是大的
return dfs(root.left, small, root.val) && dfs(root.right, root.val, big);
}
}
第二种:递归,中序遍历
解:二叉搜索树,中序遍历是升序,所以可以在中序遍历中比较前面一个,都是递增的才是正确的
复杂度分析:
- 时间复杂度 : O(n),其中 n 为二叉树的节点个数。二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
- 空间复杂度 : O(n),其中 n 为二叉树的节点个数。栈最多存储 n 个节点,因此需要额外的 O(n) 的空间。
class Solution {
// 二叉搜索树,中序遍历是升序,所以可以在中序遍历中比较前面一个,都是递增的才是正确的
long pre = Long.MIN_VALUE;;
public boolean isValidBST(TreeNode root) {
if(root == null) {
return true;
}
//左子树
if (!isValidBST(root.left)) {
return false;
}
//和前一个节点值比较
if (pre >= root.val) {
return false;
}
pre = root.val;
//右子树
return isValidBST(root.right);
}
}
十八、99. 恢复二叉搜索树
题:给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
解: 二叉搜索树:中序遍历升序,题中说只有两个节点是错误的,中序遍历找出这两个,交换。
复杂度分析:
- 时间复杂度:最坏情况下(即待交换节点为二叉搜索树最右侧的叶子节点)我们需要遍历整棵树,时间复杂度为 O(N),其中 N 为二叉搜索树的节点个数。
- 空间复杂度:O(H),其中 H 为二叉搜索树的高度。中序遍历的时候栈的深度取决于二叉搜索树的高度。
class Solution {
//二叉搜索树:中序遍历升序,题中说只有两个节点是错误的,中序遍历找出这两个,交换
TreeNode first = null;
TreeNode last = null;
TreeNode pre = new TreeNode(Integer.MIN_VALUE);
public void recoverTree(TreeNode root) {
dfs(root);
if (first != null && last != null) {
int temp = first.val;
first.val = last.val;
last.val = temp;
}
}
// 中序遍历找两数
public void dfs(TreeNode root) {
if (root == null) {
return;
}
dfs(root.left);
//当前节点是要大于上一个节点的,小于说明顺序出错
//第一次判断成功时 first == pre,第二次判断成功时 last = root,还是因为是升序序列
if(root.val < pre.val) {
last = root;
if (first == null) {
first = pre;
}
}
pre = root;
dfs(root.right);
}
}
十九、102. 二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
解:在普通的层序遍历的基础上,增加一点,首先根元素入队
当队列不为空的时候,求当前队列的长度 s,依次从队列中取 s
个元素进行拓展,然后进入下一次迭代。这和求 (104. 二叉树的最大深度),用bfs求解差不多。
复杂度分析 ,记树上所有节点的个数为 n。:
- 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
- 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<List<Integer>>();
if(root == null) {
return result;
}
Queue<TreeNode> stack = new LinkedList<>();
stack.add(root);
while(!stack.isEmpty()) {
List<Integer> temp = new ArrayList<Integer>();
int size = stack.size();
for(int i = size; i > 0; i--) {
TreeNode node = stack.poll();
temp.add(node.val);
if (node.left != null) {
stack.add(node.left);
}
if(node.right != null) {
stack.add(node.right);
}
}
result.add(temp);
}
return result;
}
}
二十、105. 从前序与中序遍历序列构造二叉树
题:给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
解:对于任意一颗树而言,前序遍历的形式总是
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果]
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
复杂度分析:
- 时间复杂度:O(n),其中 n 是树中的节点个数。
- 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。
class Solution {
//key 是中序遍历中的值,value是对应的下标
private Map<Integer, Integer> map;
public TreeNode buildTree(int[] preorder, int[] inorder) {
int size = preorder.length;
map = new HashMap<Integer, Integer>();
for(int i = 0; i < size ; i++){
map.put(inorder[i], i);
}
return dfs(preorder, inorder, 0, size-1, 0, size-1);
}
public TreeNode dfs(int[] preorder, int[] inorder,
int preorderLeft, int preorderRight,
int inorderleft, int inorderRight){
if(preorderLeft > preorderRight){
return null;
}
//根节点
TreeNode root = new TreeNode(preorder[preorderLeft]);
//中序遍历中根节点的位置
int inorderRootLocation = map.get(preorder[preorderLeft]);
//左子树的范围,节点数组
int leftTree = inorderRootLocation - inorderleft;
//递归构建左子树,preorder左子树的范围,inorder左子树的范围
root.left = dfs(preorder, inorder,
preorderLeft + 1, preorderLeft + leftTree,
inorderleft, inorderRootLocation - 1);
//递归构建右子树,preorder右子树的范围,inorder右子树的范围
root.right = dfs(preorder, inorder,
preorderLeft + leftTree + 1, preorderRight,
inorderRootLocation + 1, inorderRight);
return root;
}
}
二十一、106. 从中序与后序遍历序列构造二叉树
题:给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
解:和105题可以说是完全相同,根据中序遍历、后序遍历的遍历规则来。关键点就在于递归时确定左右子树的边界,注意都是闭区间
复杂度分析:
- 时间复杂度:O(n),其中 n 是树中的节点个数。
- 空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。
class Solution {
//存放中序遍历,用来快速找到节点的下标
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
int size = inorder.length;
for(int i = 0; i < size; i++) {
map.put(inorder[i], i);
}
return dfs(inorder, postorder, 0, size-1, 0, size - 1);
}
public TreeNode dfs(int[] inorder, int[] postorder,
int inorderLeft, int inorderRight,
int postorderLeft, int postorderRight) {
if(postorderLeft > postorderRight) {
return null;
}
//根节点
TreeNode root = new TreeNode(postorder[postorderRight]);
//根节点在中序遍历中的位置
int inorderRootLocation = map.get(postorder[postorderRight]);
//右子树有几个元素
int rightTreeNum = inorderRight - inorderRootLocation;
//递归的设置左子树
root.left = dfs(inorder, postorder,
inorderLeft, inorderRootLocation - 1,
postorderLeft, postorderRight - rightTreeNum - 1);
//递归的设置右子树
root.right = dfs(inorder, postorder,
inorderRootLocation + 1, inorderRight,
postorderRight - rightTreeNum, postorderRight - 1);
return root;
}
}
二十二、107. 二叉树的层序遍历 II
题:给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
解:此题与 102. 二叉树的层序遍历 一摸一样,只不过结果集合从ArrayList变为LinkedList
复杂度分析:
- 时间复杂度:O(n),其中 n 是二叉树中的节点个数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是 O(1),因此总时间复杂度是 O(n)。
- 空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> result = new LinkedList<List<Integer>>();
if(root == null) {
return result;
}
//队列
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root); //入栈
while(!queue.isEmpty()) {
//过渡集合
List<Integer> temp = new ArrayList<Integer>();
int size = queue.size();
for(int i = 0; i < size; i++) {
TreeNode node = queue.poll();//出栈
temp.add(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
result.add(0, temp);
}
return result;
}
}
二十三、109. 有序链表转换二叉搜索树
题:给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定的有序链表: [-10, -3, 0, 5, 9],
一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树:
解:此题和 108. 将有序数组转换为平衡二叉搜索树 思路是基本一样的,所以有个方法就是先将这个有序链表转为有序数组,这样就和108题一摸一样了,就是会多耗费O(n)的空间,我们也可以不转化,直接找到中间元素,只不过单链表和数组不一样,不能直接通过下标来访问,单链表只能next来访问,所以我们的解法是:从头访问单链表+中序遍历(而108题是 直接访问中间节点+前序遍历),两个题也都体现了 “分治” 的思想。
复杂度分析:
-
时间复杂度O(n),其中 n 是链表的长度。设长度为 n 的链表构造二叉搜索树的时间为 T(n),递推式为 T(n)=2⋅T(n/2)+O(n),根据主定理,T(n)=O(nlogn)。
-
空间复杂度:O(logn),这里只计算除了返回答案之外的空间。平衡二叉树的高度为 O(logn),即为递归过程中栈的最大深度,也就是需要的空间。
class Solution {
ListNode tempNode;
public TreeNode sortedListToBST(ListNode head) {
tempNode = head;
int length = getLength(head);
return dfs(0, length - 1);
}
//求单链表长度
public int getLength(ListNode head){
int length = 0;
while(head != null) {
length = length + 1;
head = head.next;
}
return length;
}
//中序遍历构造二叉树
public TreeNode dfs(int left, int right) {
if(left > right) {
return null;
}
int mid = (left + right + 1) / 2;
TreeNode root = new TreeNode();
root.left = dfs(left, mid - 1);
root.val = tempNode.val;
tempNode = tempNode.next;
root.right = dfs(mid + 1, right);
return root;
}
}
二十四、129. 求根节点到叶节点数字之和
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
叶节点 是指没有子节点的节点。
输入:root = [1,2,3]
输出:25
解释:
从根到叶子节点路径 1->2 代表数字 12
从根到叶子节点路径 1->3 代表数字 13
因此,数字总和 = 12 + 13 = 25
解:第一点求根节点到叶子节点的路径,第二个求和,这是两个关键点,这两步,我们可以分开做,也可以放在一起做。
第一种方法:两步分开做。
时间复杂度为:O(N^ 2),深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对 path 变量进行拷贝构造,时间代价为 O(N),故时间复杂度为 O(N^2)。遍历求和,为O(N),综合起来为O(N ^ 2)。
空间复杂度:O(N^ 2)。
class Solution {
public int sumNumbers(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfs(root, "", result);
int sum = 0;
int size = result.size();
if (size == 0) {
return sum;
} else {
for(int i = 0; i < size; i++) {
sum = sum + result.get(i);
}
}
return sum;
}
public void dfs (TreeNode root, String path,List<Integer> result) {
if (root == null) {
return;
}
StringBuffer pathSb = new StringBuffer(path);
pathSb.append(Integer.toString(root.val));
//到达叶子节点
if (root.left == null && root.right == null) {
result.add(Integer.valueOf(pathSb.toString()));
} else {
dfs(root.left, pathSb.toString(), result);
dfs(root.right, pathSb.toString(), result);
}
}
第二种方法:两步一起做,一边求路径,一边求和。
时间复杂度:O(n),其中 n 是二叉树的节点个数。对每个节点访问一次。
空间复杂度:O(n),其中 n 是二叉树的节点个数。空间复杂度主要取决于递归调用的栈空间,递归栈的深度等于二叉树的高度,最坏情况下,二叉树的高度等于节点个数,空间复杂度为 O(n)。
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root, 0);
}
public int dfs (TreeNode root, int sumTemp) {
if (root == null) {
return 0;
}
//求根节点到目前节点的和
int sum = sumTemp * 10 + root.val;
//到达叶子节点
if (root.left == null && root.right == null) {
return sum;
} else {
return dfs(root.left,sum) + dfs(root.right, sum);
}
}
}
二十五、116. 填充每个节点的下一个右侧节点指针
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
解:此题应该很熟悉了,层序遍历,一次输出一层,所以出栈之前需要判断当前层有几个节点,然后再设置当前节点的右侧节点。
复杂度分析:
- 时间复杂度:O(N)。每个节点会被访问一次且只会被访问一次,即从队列中弹出,并建立 next 指针。
- 空间复杂度:O(N)。这是一棵完美二叉树,它的最后一个层级包含 N/2 个节点。广度优先遍历的复杂度取决于一个层级上的最大元素数量。这种情况下空间复杂度为 O(N)。
class Solution {
public Node connect(Node root) {
if(root == null) {
return null;
}
Queue<Node> queue = new LinkedList<Node>();
// 入栈
queue.offer(root);
while(!queue.isEmpty()) {
int size = queue.size();
for(int i = 0; i < size; i++) {
//出栈
Node node = queue.poll();
// 连接
if (i < size - 1) {
node.next = queue.peek();
}
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
}
//返回根节点
return root;
}
}
二十六、173. 二叉搜索树迭代器
173. 二叉搜索树迭代器
就是中序遍历。
第一种:dfs 递归,先把中序遍历的结果写出来
复杂度分析:
- 时间复杂度:初始化需要 O(n) 的时间,其中 n 为树中节点的数量。随后每次调用只需要 O(1) 的时间。
- 空间复杂度:O(n),因为需要保存中序遍历的全部结果。
class BSTIterator {
int index;
List<Integer> list;
public BSTIterator(TreeNode root) {
index = 0;
list = new ArrayList<Integer>();
// 递归实现中序遍历
dfs(root, list);
}
public int next() {
return list.get(index++);
}
public boolean hasNext() {
return index < list.size();
}
public void dfs(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
dfs(root.left, list);
list.add(root.val);
dfs(root.right, list);
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
第二种:迭代 中序遍历 (拆开的)
复杂度分析
- 时间复杂度:显然,初始化和调用hasNext() 都只需要 O(1) 的时间。每次调用 next() 函数最坏情况下需要 O(n) 的时间;但考虑到 n 次调用 next() 函数总共会遍历全部的 n 个节点,因此总的时间复杂度为O(n),因此单次调用平均下来的均摊复杂度为O(1)。
- 空间复杂度:O(n),其中 n 是二叉树的节点数量。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到O(n) 的级别。
class BSTIterator {
private TreeNode cur;
private Deque<TreeNode> stack;
public BSTIterator(TreeNode root) {
cur = root;
stack = new LinkedList<TreeNode>();
}
public int next() {
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
int result = cur.val;
cur = cur.right;
return result;
}
public boolean hasNext() {
return cur != null || !stack.isEmpty();
}
}
/**
* Your BSTIterator object will be instantiated and called as such:
* BSTIterator obj = new BSTIterator(root);
* int param_1 = obj.next();
* boolean param_2 = obj.hasNext();
*/
二十七、222. 完全二叉树的节点个数
222. 完全二叉树的节点个数 - 力扣(LeetCode) (leetcode-cn.com)
第一种,暴力求解,dfs递归
时间复杂度O(N),空间复杂度O(N)
class Solution {
//暴力求解
public int countNodes(TreeNode root) {
if(root == null) {
return 0;
}
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
第二种:利用完全二叉树特性。
回顾一下满二叉的节点个数怎么计算,如果满二叉树的层数为h,则总节点数为:2^h - 1.
那么我们来对 root 节点的左右子树进行高度统计,分别记为 left 和 right,有以下两种结果:
- left == right。这说明,左子树一定是满二叉树,因为节点已经填充到右子树了,左子树必定已经填满了。所以左子树的节点总数我们可以直接得到,是 2^left - 1,加上当前这个 root 节点,则正好是 2^left。再对右子树进行递归统计。
- left != right。说明此时最后一层不满,但倒数第二层已经满了,可以直接得到右子树的节点个数。同理,右子树节点 +root 节点,总数为 2^right。再对左子树进行递归查找。
时间复杂度:O(log^2 n)
空间复杂度:O(1)
class Solution {
//利用完全二叉树的特性
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
// 计算左右子树的高度
int left = countHeight(root.left);
int right = countHeight(root.right);
if (left == right) {
// 说明左侧为满二叉树,1 << left 为当前的节点 + 子树的节点的和
return countNodes(root.right) + (1 << left);
} else {
// 说明右侧为满二叉树
return countNodes(root.left) + (1 << right);
}
}
public int countHeight(TreeNode root) {
int height = 0;
while(root != null) {
height++;
root = root.left;
}
return height;
}
}