deque与链表、栈、队列的关系
在Java中,我们用Deque可以实现Stack的功能,注意只调用push()/pop()/peek()方法,避免调用Deque的其他方法。
我们可以发现Deque是继承自Queue,而Stack是继承自Vector,这就比较奇怪了。
Vector是由数组实现的集合类,他包含了大量集合处理的方法。而Stack之所以继承Vector,是为了复用Vector中的方法,来实现进栈(push)、出栈(pop)等操作。这里就是Stack设计不好的地方,既然只是为了实现栈,不用链表来单独实现,而是为了复用简单的方法而迫使它继承Vector,Stack和Vector本来是毫无关系的。这使得Stack在基于数组实现上效率受影响,另外因为继承Vector类,Stack可以复用Vector大量方法,这使得Stack在设计上不严谨,例如Vector中的:
public void add(int index, E element) {
insertElementAt(element, index);
}
可以在指定位置添加元素,这与Stack 的设计理念相冲突(栈只能在栈顶添加或删除元素)。所以Java后来修正了这个不良好的设计,提出了用Deque代替Stack的建议。
当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。
最后,不要使用遗留类Stack。
回溯
思路
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
回溯的思路基本如下:当前局面下,我们有若干种选择,所以我们对每一种选择进行尝试。如果发现某种选择违反了某些限定条件,此时 return;如果尝试某种选择到了最后,发现该选择是正确解,那么就将其加入到解集中。
在这种思想下,我们需要清晰的找出三个要素:选择 (Options),限制 (Restraints),结束条件 (Termination)。
回溯算法框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
全排列问题
public class Test {
public static void main(String[] args) {
int[] nums = new int[]{1,2,3};
System.out.println(permute(nums));
}
private static List<List<Integer>> res = new LinkedList<>();
/**
* 输⼊⼀组不重复的数字,返回它们的全排列
* @param nums
* @return
*/
private static List<List<Integer>> permute(int[] nums){
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
/**
* 路径:记录在track中
* 选择列表:nums中不存在track的那些元素
* 结束条件:nums中的元素全都在track中出现
*/
private static void backtrack(int[] nums,LinkedList<Integer> track){
if(track.size() == nums.length) {
// res.add(track);
res.add(new LinkedList<Integer>(track));
return;
}
for (int i = 0; i < nums.length; i++) {
//排除不合法的选择,数字不重复
//全排列应该用访问数组来减少复杂度,这里为了方便展现模板,没有更换
if(track.contains(nums[i])) {
continue;
}
//做选择
track.add(nums[i]);
//进入下一层
backtrack(nums, track);
//取消选择
track.removeLast();
}
}
}
注意,添加路径时,不能写成res.add(track)
变量 path 所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。
在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。
至此,我们就通过全排列问题详解了回溯算法的底层原理。当然,这个算法解决全排列不是很高效,因为对链表使用 contains 方法需要 O(N)的时间 复杂度。有更好的方法通过交换元素达到目的,但是难理解⼀些,这里就不写了,有兴趣可以自行搜索⼀下。
但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是⽆法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度⼀般都很高。
递归与回溯的区别
递归是一种算法结构。递归会出现在子程序中,形式上表现为直接或间接的自己调用自己。典型的例子是阶乘,计算规律为:n!=n×(n−1)!
回溯是一种算法思想,它是用递归实现的。回溯的过程类似于穷举法,但回溯有“剪枝”功能,即自我判断过程。例如有求和问题,给定有 7 个元素的组合 [1, 2, 3, 4, 5, 6, 7],求加和为 7 的子集。累加计算中,选择 1+2+3+4 时,判断得到结果为 10 大于 7,那么后面的 5, 6, 7 就没有必要计算了。这种方法属于搜索过程中的优化,即“剪枝”功能。
用一个比较通俗的说法来解释递归和回溯:
- 我们在路上走着,前面是一个多岔路口,因为我们并不知道应该走哪条路,所以我们需要尝试。尝试的过程就是一个函数。
- 我们选择了一个方向,后来发现又有一个多岔路口,这时候又需要进行一次选择。所以我们需要在上一次尝试结果的基础上,再做一次尝试,即在函数内部再调用一次函数,这就是递归的过程。
- 这样重复了若干次之后,发现这次选择的这条路走不通,这时候我们知道我们上一个路口选错了,所以我们要回到上一个路口重新选择其他路,这就是回溯的思想
递归
重建二叉树
牛客
描述:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
- 以中序 + 后序为例
数组拷贝,复杂度太高
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
if (postorder.length == 0) return null;
int mid = postorder[postorder.length - 1];
TreeNode root = new TreeNode(mid);
if (postorder.length == 1) return root;
int i = 0;
for(;i < inorder.length; i++){
if(inorder[i] == mid) break;
}
root.left = buildTree(Arrays.copyOfRange(inorder,0,i), Arrays.copyOfRange(postorder,0,i));
root.right = buildTree(Arrays.copyOfRange(inorder,i+1,inorder.length),Arrays.copyOfRange(postorder,i,postorder.length-1));
return root;
}
}
class Solution {
HashMap<Integer, Integer> hash;
int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inLen = inorder.length;
int postLen = postorder.length;
if (inLen != postLen) {
throw new RuntimeException("输入错误");
}
this.postorder = postorder;
hash = new HashMap<>();
for (int i = 0; i < inLen; i++) {
hash.put(inorder[i], i);
}
return buildTree(0, inLen - 1, 0, postLen - 1);
}
/**
* 使用中序遍历序列 inorder 的子区间 [inLeft, inRight]
* 与后序遍历序列 postorder 的子区间 [postLeft, postRight] 构建二叉树
*
* @param inLeft 中序遍历序列的左边界
* @param inRight 中序遍历序列的右边界
* @param postLeft 后序遍历序列的左边界
* @param postRight 后序遍历序列的右边界
* @return 二叉树的根结点
*/
private TreeNode buildTree(int inLeft, int inRight, int postLeft, int postRight) {
if (inLeft > inRight || postLeft > postRight) {
return null;
}
int pivot = postorder[postRight];
int pivotIndex = hash.get(pivot);
TreeNode root = new TreeNode(pivot);
root.left = buildTree(inLeft, pivotIndex - 1, postLeft, postRight - inRight + pivotIndex - 1);
root.right = buildTree(pivotIndex + 1, inRight, postRight - inRight + pivotIndex, postRight - 1);
return root;
}
}
二叉搜索树的后序遍历序列
public class Solution {
public boolean VerifySquenceOfBST(int [] sequence) {
int len = sequence.length;
if(len == 0) return false;
return verify(sequence,0,len-1);
}
private boolean verify(int[] sequence, int first, int last){
if(first >= last) return true;
int cutIndex = first;
while(cutIndex < last && sequence[cutIndex] <= sequence[last]) cutIndex++;
// int cutIndex = index;
for(int i = cutIndex; i < last; i++){
if(sequence[i] < sequence[last]) return false;
}
// if(index < last) return false;
return verify(sequence,first,cutIndex - 1) && verify(sequence,cutIndex,last - 1);
}
}
树的高度
- 法一:递归
public int maxDepth(TreeNode root) {
return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
- 法二:BFS迭代
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
//LinkedList真是一个全能选手,它即是List,又是Queue,还是Deque
Queue<TreeNode> queue = new LinkedList<>();
//也能用双端队列Deque
//Deque<TreeNode> deque= new LinkedList<>();
queue.add(root);
//deque.addLast(root);
int depth = 0;
while(!queue.isEmpty()){
//注意,这里必须先拿到size!(size是上一层的node个数)
//每遍历一层,深度加一
int queueSize = queue.size();
for(int i = 0; i < queueSize; i++){
TreeNode node = queue.poll();
//TreeNode node = deque.removeFirst();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
depth++;
}
return depth;
}
}
- 法三:DFS迭代
这里电脑上需要导入javafx.util.Pair
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
Deque<Pair<TreeNode,Integer>> stack = new LinkedList<>();
int maxDepth = 0;//最大深度
stack.push(new Pair<>(root,1));
while (!stack.isEmpty()) {
Pair<TreeNode,Integer> pair = stack.pop();
maxDepth = Math.max(maxDepth, pair.getValue());
TreeNode node = pair.getKey();
int curDepth = pair.getValue();
//右子树先入栈,那左子树就先出栈,这是按照先序遍历的顺序
if (node.right != null) {
stack.push(new Pair<>(node.right, curDepth + 1));
}
if (node.left != null){
stack.push(new Pair<>(node.left, curDepth + 1));
}
}
return maxDepth;
}
}
用双栈解决上述问题
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
//用Deque模拟Stack
Deque<TreeNode> stack = new LinkedList<>();
Deque<Integer> level = new LinkedList<>();//保存深度
int maxDepth = 0;//最大深度
stack.push(root);
level.push(1);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
int depth = level.pop();
maxDepth = Math.max(maxDepth,depth);
//右子树先入栈,那左子树就先出栈,这是按照先序遍历的顺序
if (node.right != null) {
stack.push(node.right);
level.push(depth + 1);
}
if (node.left != null){
stack.push(node.left);
level.push(depth + 1);
}
}
return maxDepth;
}
}
递归式dfs(前序遍历)
class Solution {
private int maxLevel = 0;
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
dfs(root, 1);
return maxLevel;
}
private void dfs(TreeNode root,int level){
if (root == null) return;
if (maxLevel < level){
maxLevel = level;
}
dfs(root.left, level + 1);
dfs(root.right, level + 1);
}
}
最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
和<104> 二叉树的最大深度类似的解法,主要有两点不同
- 深度比较函数换成了 Math.min();
- 对于当前节点的左右子树存在一个空时,需要进行特殊的处理。比如
此时该二叉树的最小深度为2,若还是通过找到它们的 左右子树的高度最小值 + 1的话,最小深度为1,所以是不符合要求的,进行特殊的处理。
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
//这道题递归条件里分为三种情况
//1.左孩子和有孩子都为空的情况,说明到达了叶子节点,直接返回1即可
if(root.left == null && root.right == null) return 1;
//2.如果左孩子和由孩子其中一个为空,那么需要返回比较大的那个孩子的深度
int m1 = minDepth(root.left);
int m2 = minDepth(root.right);
//这里其中一个节点为空,说明m1和m2有一个必然为0,所以可以返回m1 + m2 + 1;
if(root.left == null || root.right == null) return m1 + m2 + 1;
//3.最后一种情况,也就是左右孩子都不为空,返回最小深度+1即可
return Math.min(m1,m2) + 1;
}
}
代码可以简化
当左右孩子为空时 m1 和 m2 都为 0
可以和情况 2 进行合并,即返回 m1+m2+1
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
int left = minDepth(root.left);
int right = minDepth(root.right);
if (root.left == null || root.right == null){
return left + right + 1;
}
return Math.min(left, right) + 1;
}
}
BFS
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int minDepth = 0;
while (!queue.isEmpty()){
minDepth++;//遍历下一层前就将深度加1,不要放后面,因为到达叶子结点时直接返回深度了
int size = queue.size();
for (int i = 0; i < size; i++){
TreeNode node = queue.poll();
//判断是否到达终点
if (node.left == null && node.right == null){
return minDepth;
}
if (node.left != null){
queue.add(node.left);
}
if (node.right != null){
queue.add(node.right);
}
}
}
return minDepth;
}
}
DFS
class Solution {
public static int minDepth(TreeNode root) {
// Pair<TreeNode, Integer> 存储了当前节点 和 对应深度
if(root == null) return 0;
Stack<Pair<TreeNode, Integer>> stack = new Stack<>();
stack.add(new Pair<TreeNode, Integer>(root,1));;
int minDepth = Integer.MAX_VALUE;//注意这里,不能初始化为0,不然后面都是0
while (!stack.isEmpty()){
Pair<TreeNode, Integer> pair = stack.pop(); // 从栈中弹出一个Pair对象
TreeNode node = pair.getKey();
if (node.left == null && node.right == null)
minDepth = Math.min(minDepth, pair.getValue()); // 找到了叶子节点, 比较当前节点的深度 和 最小深度
// 当前节点的子节点入栈 - 右节点先入栈(先进后出)
if (node.right != null) stack.add(new Pair<TreeNode, Integer>(node.right, pair.getValue() + 1));
if (node.left != null) stack.add(new Pair<TreeNode, Integer>(node.left, pair.getValue() + 1));
}
return minDepth;
}
}
两节点最长路径
Input:
1
/ \
2 3
/ \
4 5
Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
//一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
//最大直径等于左右子树的最大深度之和,对于每一个结点都需要比较左右子树的深度之和,与上一个值做比较取最大值
class Solution {
private int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return max;
}
private int maxDepth(TreeNode root){
if (root == null){
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
max = Math.max(max, left + right);
//返回深度
return Math.max(left, right) + 1;
}
}
或者
class Solution {
private int maxPath;
public int diameterOfBinaryTree(TreeNode root) {
max(root);
return maxPath;
}
private int max(TreeNode root){
if (root == null) return 0;
int left = max(root.left);
int right = max(root.right);
int leftPath = root.left == null ? 0 : left + 1;
int rightPath = root.right == null ? 0 : right + 1;
maxPath = Math.max(maxPath, leftPath + rightPath);
return Math.max(leftPath,rightPath);
}
}
判断平衡二叉树
a binary tree in which the left and right subtrees of every node
differ in height by no more than 1.
- 自顶向下(暴力法): 先序遍历 + 判断深度
class Solution {
private int height(TreeNode root){
if(root == null){
return 0;
}
return Math.max(height(root.left),height(root.right)) + 1;
}
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);
}
}
针对满二叉树,设共有 n 层,第 i 层有 2i-1个结点,全部结点个数为 2n- 1,设总结点个数为 N,则最后一层结点个数为 2n-1=(N+1)/2
- 自底向上:后序遍历 + 剪枝
class Solution {
private boolean isB = true;
public boolean isBalanced(TreeNode root) {
height(root);
return isB;
}
private int height(TreeNode root){
if(root == null) return 0;
int left = height(root.left);
int right = height(root.right);
if(Math.abs(left - right) > 1) isB = false;
return Math.max(left,right) + 1;//返回结点深度
}
}
复杂度分析:
时间复杂度 O(N): N为树的节点数;最差情况下,需要递归遍历树的所有节点。
空间复杂度 O(N): 最差情况下(树退化为链表时),系统递归需要使用 O(N)的栈空间。
翻转二叉树
- 递归
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null){
return null;
}
TreeNode left = invertTree(root.right);
TreeNode right = invertTree(root.left);
root.left = right;
root.right = left;
return root;
}
}
时间复杂度:每个元素都必须访问一次,所以是O(n)
空间复杂度:最坏的情况下,需要存放O(h)个函数调用(h是树的高度),所以是O(h)
- 迭代(bfs)
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null){
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
TreeNode left = cur.left;
cur.left = cur.right;
cur.right = left;
if (cur.left != null){
queue.add(cur.left);
}
if (cur.right != null){
queue.add(cur.right);
}
}
return root;
}
}
时间复杂度:同样每个节点都需要入队列/出队列一次,所以是O(n)
空间复杂度:最坏的情况下会包含所有的叶子节点,完全二叉树叶子节点是(n+1)/2个,所以时间复杂度是O(n)
合并二叉树
Input:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
Output:
3
/ \
4 5
/ \ \
5 4 7
- 递归
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) return null;
if (t1 == null) return t2;
if (t2 == null) return t1;
TreeNode node = new TreeNode(t1.val + t2.val);
node.left = mergeTrees(t1.left, t2.left);
node.right = mergeTrees(t1.right, t2.right);
return node;
}
}
- 迭代
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null) return t2;
if (t2 == null) return t1;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(t1);
queue.add(t2);
while (queue.size() != 0){
TreeNode r1 = queue.poll();
TreeNode r2 = queue.poll();
r1.val += r2.val;
if (r1.left != null && r2.left != null){
queue.add(r1.left);
queue.add(r2.left);
}else if (r1.left == null){
r1.left = r2.left;
}
if (r1.right != null && r2.right != null){
queue.add(r1.right);
queue.add(r2.right);
}else if (r1.right == null){
r1.right = r2.right;
}
}
return t1;
}
}
判断路径和是否等于一个数
- 递归
首先是 DFS 解法,该解法的想法是一直向下找到叶子节点,如果到叶子节点时sum == 0,说明找到了一条符合要求的路径。
从根节点开始,每当遇到一个节点的时候,从目标值里扣除节点值,一直到叶子节点判断目标值是不是被扣完。
- dfs的3种递归写法
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) return false;
if (root.left == null && root.right == null){
return root.val == sum;
}
return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
}
回溯写法,一旦标记为true,立马返回
class Solution {
private boolean hasPath = false;
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null){
return false;
}
dfs(root, sum);
return hasPath;
}
private void dfs(TreeNode root,int sum){
if (root == null) return;
if (root.left == null && root.right == null){
if (root.val == sum) hasPath = true;
}
dfs(root.left, sum - root.val);
dfs(root.right, sum - root.val);
}
}
差不多,但不是用减法,用加法
class Solution {
private boolean hasPath = false;
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null){
return false;
}
dfs(root, sum,0);
return hasPath;
}
private void dfs(TreeNode root,int sum,int curSum){
if (hasPath || root == null) return;
if (root.left == null && root.right == null){
if (curSum + root.val == sum) hasPath = true;
}
dfs(root.left, sum,curSum + root.val);
dfs(root.right, sum,curSum + root.val);
}
}
时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
空间复杂度:O(H),其中 H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(log N)
- BFS
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null){
return false;
}
Queue<TreeNode> queue = new LinkedList<>();
Queue<Integer> queueSum = new LinkedList<>();
queue.add(root);
queueSum.add(root.val);
while (!queue.isEmpty()){
TreeNode curNode = queue.poll();
int curSum = queueSum.poll();
if (curNode.left == null && curNode.right == null){
if (curSum == sum) return true;
}
if (curNode.left != null){
queue.add(curNode.left);
queueSum.add(curSum + curNode.left.val);
}
if (curNode.right != null){
queue.add(curNode.right);
queueSum.add(curSum + curNode.right.val);
}
}
return false;
}
}
时间复杂度:O(N),其中 N是树的节点数。对每个节点访问一次。
空间复杂度:O(N),其中 N是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
统计路径和等于一个数的路径数量
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
Return 3. The paths that sum to 8 are:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
- 法一:双重递归
- 思路:首先先序递归遍历每个节点,再以每个节点作为起始点递归寻找满足条件的路径。
- 这种题目需要从每个节点开始进行类似的计算,所以第一个递归用来遍历这些节点,第二个递归用来处理这些节点,进行深度优先搜索。
- 以当前节点作为头结点的路径数量
当前节点的左子树中满足条件的路径数量
当前节点的右子树中满足条件的路径数量
将这三部分之和作为最后结果即可
class Solution {
public int pathSum(TreeNode root, int sum) {
if (root == null) return 0;
return helper(root, sum) + pathSum(root.left, sum) +
pathSum(root.right, sum);
}
/*private int helper(TreeNode root,int sum){
if (root == null) return 0;
int num = (root.val == sum) ? 1 : 0;
return num + helper(root.left, sum - root.val) + helper(root.right, sum - root.val);
}*/
//这种也行,速度更快
private int dfs(TreeNode root, int sum) {
if (root == null) return 0;
sum -= root.val;
int count = (sum == 0) ? 1 : 0;
return count + dfs(root.left, sum) + dfs(root.right, sum);
}
}
时间复杂度:遍历n个节点,为每个节点计算以当前节点为路径终点的所有路径和,平均路径长度是logn,所以平均时间复杂度是O(nlogn)
- 法二:计算以当前节点为路径终点的所有路径和。 关键点:用一个数组保存从根节点到当前节点路径
class Solution {
public int pathSum(TreeNode root, int sum) {
return helper(root, sum, new int[1000], 0);//0表示根结点
}
private int helper(TreeNode root,int sum,int[] array,int p) {
if (root == null) return 0;
int n = 0;//路径数
int temp = 0;
array[p] = root.val;
//注意,不能提前break,可能上一层有0或者后面某一段节点和为0
//别忘了array[0]
for (int i = p; i >= 0; i--) {
temp += array[i];
if (temp == sum) {
n++;
}
}
return n + helper(root.left, sum, array, p + 1) +
helper(root.right, sum, array, p + 1);
}
}
时间复杂度:遍历n个节点,为每个节点计算以当前节点为路径终点的所有路径和,平均路径长度是logn,所以平均时间复杂度是O(nlogn)
- 法三:前缀和+hash优化
class Solution {
public int pathSum(TreeNode root, int sum) {
Map<Integer,Integer> map = new HashMap<>();
map.put(0, 1);
return dfs(root, map, sum, 0);
}
private int dfs(TreeNode root,Map<Integer,Integer> map,int sum,int curSum) {
if (root == null) return 0;
int count = 0;
//计算前缀和
curSum += root.val;
if (map.containsKey(curSum - sum)) {
count += map.get(curSum - sum);
}
//更新路径上当前节点前缀和的个数
map.put(curSum, map.getOrDefault(curSum, 0) + 1);
//进入下一层
count += dfs(root.left, map, sum, curSum);
count += dfs(root.right, map, sum, curSum);
//回到本层,撤销递归前做的决定,下一步回到递归上一层进行处理
map.replace(curSum, map.get(curSum) - 1);
return count;
}
}
时间复杂度:每个节点只遍历一次,O(N).
空间复杂度:开辟了一个hashMap,O(N).
类似和为K的子数组
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
暴力法
class Solution {
public int subarraySum(int[] nums, int k) {
int n = 0;
for (int i = 0; i < nums.length; i++) {
int res = 0;
for(int j = i; j < nums.length; j++){
res += nums[j];
if (res == k){
n++;
}
}
}
return n;
}
}
前缀和加哈希表优化
class Solution {
public int subarraySum(int[] nums, int k) {
// map记录前几个数字之和为K出现相同和的次数为V
Map<Integer,Integer> map = new HashMap<>();
//如果前缀和为k(包括nums[0]就是k这种情况),那么pre - k = 0,count值更新为1
map.put(0, 1);
int count = 0;//满足和为K的连续字符串数量
int pre = 0;//前缀和
for (int i = 0; i < nums.length; i++){
pre += nums[i];
// 如果前面数字之和加上这个数字正好等于K(存在一个数字加上nums[i]结果为K
// 说明找到了
if (map.containsKey(pre - k)){
//注意重复的前缀和,此时对应的数目 > 1,叠加就行
count += map.get(pre - k);
}
//计算新的和放入map,前缀和pre如果之前没出现过,对应的Value值就为1,出现过就在原来基础上加一
map.put(pre, map.getOrDefault(pre, 0) + 1);
}
return count;
}
}
时间复杂度:O(n),其中 n 为数组的长度。我们遍历数组的时间复杂度为 O(n),中间利用哈希表查询删除的复杂度均为 O(1),因此总时间复杂度为 O(n)。
空间复杂度:O(n),其中 n 为数组的长度。哈希表在最坏情况下可能有 n 个不同的键值,因此需要 O(n) 的空间复杂度。
另一个树的子树
力扣572
这道题的子树指包含的一个结点及其所有子孙结点,和子树结构不同,像第二个例子可以是子树结构
Given tree s:
3
/ \
4 5
/ \
1 2
Given tree t:
4
/ \
1 2
Return true, because t has the same structure and node values with a subtree of s.
Given tree s:
3
/ \
4 5
/ \
1 2
/
0
Given tree t:
4
/ \
1 2
Return false.
- 双递归
思路和算法
这是一种最朴素的方法 —— DFS 枚举 s 中的每一个节点,判断这个点的子树是否和 t 相等。如何判断一个节点的子树是否和 t 相等呢,我们又需要做一次 DFS 来检查,即让两个指针一开始先指向该节点和 t 的根,然后「同步移动」两根指针来「同步遍历」这两棵树,判断对应位置是否相等。
class Solution {
public boolean isSubtree(TreeNode s, TreeNode t) {
if (t == null) return true;//空树是任何树的子树
if (s == null) return false;//走到这一步,说明t != null
return isSameTree(s, t) || isSubtree(s.left, t) || isSubtree(s.right, t);
}
private boolean isSameTree(TreeNode s,TreeNode t){
if (s == null && t == null) return true;
//走到这一步说明 s 和 t不全为空
if (s == null || t == null) return false;
return s.val == t.val && isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}
}
- 时间复杂度:对于每一个 s 上的点,都需要做一次 DFS 来和 t 匹配,匹配一次的时间代价是 O(|t|),那么总的时间代价就是O(∣s∣×∣t∣)。故渐进时间复杂度为O(∣s∣×∣t∣)。
- 空间复杂度:假设 s 深度为 ds ,t 的深度为 dt ,任意时刻栈空间的最大使用代价是O(max{ds,dt})。故渐进空间复杂度为 O(max{ds,dt}).
对称二叉树
力扣101
短路:
在递归判断过程中存在短路现象,也就是做 与
操作时,如果前面的值返回 false
, 则后面的不再进行计算
递归
class Solution {
public boolean isSymmetric(TreeNode root) {
//if (root == null) return true;
//return isSymmetric(root.left, root.right);
return isSymmetric(root,root);
}
private boolean isSymmetric(TreeNode t1,TreeNode t2){
if (t1 == null && t2 == null) return true;
if (t1 == null || t2 == null) return false;
if (t1.val != t2.val) return false;
return isSymmetric(t1.left,t2.right) && isSymmetric(t1.right,t2.left);
}
}
算法的时间复杂度是 O(n),因为要遍历 n 个节点
空间复杂度是 O(n),空间复杂度是递归的深度,也就是跟树高度有关,最坏情况下树变成一个链表结构,高度是n。
迭代
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
//直接将根节点加两次,可判断二叉树只有一个结点或根节点为空的情况
queue.add(root);
queue.add(root);
while (!queue.isEmpty()){
TreeNode t1 = queue.poll();
TreeNode t2 = queue.poll();
if (t1 == null && t2 == null) continue;
if (t1 == null || t2 == null || t1.val != t2.val) return false;
queue.add(t1.left);
queue.add(t2.right);
queue.add(t1.right);
queue.add(t2.left);
}
return true;
}
}
左叶子节点之和
3
/ \
9 20
/ \
15 7
There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24.
class Solution {
private 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;
}
}
或者用两个递归
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
if (isLeaf(root.left)) {
return root.left.val + sumOfLeftLeaves(root.right);
}
return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
//判断是否是叶子节点
private boolean isLeaf(TreeNode root){
if (root == null) return false;
return root.left == null && root.right == null;
}
}
相同节点值的最大路径长度
class Solution {
private int pathLength = 0;//设置一个全局变量记录最后结果
public int longestUnivaluePath(TreeNode root) {
helper(root);
return pathLength;
}
private int helper(TreeNode root){
if (root == null) return 0;
//单边路径数值
int left = helper(root.left);
int right = helper(root.right);
//如果当前节点和两个子节点的值都不相同,则最后返回的是0
int leftPath = 0,rightPath = 0;
if (root.left != null && root.val == root.left.val){
leftPath = left + 1;
}
if (root.right != null && root.val == root.right.val){
rightPath = right + 1;
}
//计算单侧路径的同时维护根节点的双侧路径
pathLength = Math.max(pathLength, leftPath + rightPath);
//返回的是单边路径
return Math.max(leftPath, rightPath);
}
}
间隔遍历(打家劫舍III,用到动态规划)
3
/ \
2 3
\ \
3 1
Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
暴力递归
思路:
-
对于一个以 node 为根节点的二叉树而言,如果尝试偷取 node 节点,那么势必不能偷取其左右子节点,然后继续尝试偷取其左右子节点的左右子节点。
-
如果不偷取该节点,那么只能尝试偷取其左右子节点。
-
比较两种方式的结果,谁大取谁。
class Solution {
public int rob(TreeNode root) {
if (root == null) return 0;
int money = root.val;
if (root.left != null){
money += rob(root.left.left) + rob(root.left.right);
}
if (root.right != null){
money += rob(root.right.left) + rob(root.right.right);
}
return Math.max(money, rob(root.left) + rob(root.right));
}
}
解决重复子问题,记忆优化
class Solution {
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> memo = new HashMap<>();
return robInternal(root, memo);
}
public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
if (root == null) return 0;
if (memo.containsKey(root)) return memo.get(root);
int money = root.val;
if (root.left != null) {
money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
}
if (root.right != null) {
money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
}
int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
memo.put(root, result);
return result;
}
}
动态规划
每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷
- 当前节点选择偷时,那么两个孩子节点就不能选择偷了
- 当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)
我们使用一个大小为 2 的数组来表示 int[] result = new int[2]
;
0 代表不偷,1 代表偷
任何一个节点能偷到的最大钱的状态可以定义为
- 当前节点选择不偷:当前节点能偷到的最大钱数 = 左右孩子偷或不偷得到的钱=
Math.max{left[0],left[1]} + Math.max{right[0],right[1]}
- 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数=
left[0] + right[0]
class Solution {
public int rob(TreeNode root) {
int[] result = dfs(root);
return Math.max(result[0],result[1]);
}
private int[] dfs(TreeNode root){
if (root == null) {
return new int[2];
}
int[] result = new int[2];
int[] left = dfs(root.left);
int[] right = dfs(root.right);
result[0] = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
result[1] = root.val + left[0] + right[0];
return result;
}
}
打家劫舍
Example 1:
Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
Example 2:
Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).
Total amount you can rob = 2 + 9 + 1 = 12.
dp[i]
:从 i 个房子偷到的最大金额
状态转移方程:dp[i] = max{dp[i-2] + nums[i],dp[i]}
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
if(nums.length == 1) return nums[0];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i-2] + nums[i],dp[i-1]);
}
return dp[nums.length - 1];
}
}
空间优化
对于小偷问题,我们发现,最后一步计算 f(n) 的时候,实际上只用到了 f(n-1) 和 f(n-2) 的结果。n-3 之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。下面的动图比较了空间优化前和优化后的对比关系:
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int pre = 0,cur = 0;
// 循环开始时,cur 表示 dp[k-1],pre 表示 dp[k-2]
// dp[k] = max{ dp[k-1], dp[k-2] + nums[i]}
for (int i : nums
) {
int temp = Math.max(pre + i, cur);
pre = cur;
cur = temp;
}
// 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
return cur;
}
}
时间复杂度O(n),空间复杂度O(1)
或者
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
打家劫舍II(首尾相邻,不能同时偷)
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
//数组复制,下标左闭右开
return Math.max(myRob(Arrays.copyOfRange(nums, 0, nums.length - 1)),
myRob(Arrays.copyOfRange(nums, 1, nums.length)) );
}
private int myRob(int[] nums){
int pre = 0,cur = 0;
for (int i : nums){
int temp = Math.max(pre + i, cur);
pre = cur;
cur = temp;
}
return cur;
}
}
找出二叉树中第二小的节点
Input:
2
/ \
2 5
/ \
5 7
Output: 5
- 没有必要记录最小的值,因为最小的一定是根结点。
- 递归找到比根结点大的值时可以立即返回,不用再遍历当前节点下面的子节点,因为子节点的值不可能比它小。
class Solution {
public int findSecondMinimumValue(TreeNode root) {
return helper(root,root.val);
}
private int helper(TreeNode root,int val){
if(root == null) return -1;
if(root.val > val) return root.val;
int left = helper(root.left,val);
int right = helper(root.right,val);
if(left == -1) return right;
if(right == -1) return left;
return Math.min(left,right);
}
}
层序遍历
一棵树每层节点的平均值
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Double> list = new LinkedList<>();//注意有可能有小数
if(root == null) return list;
queue.add(root);
while(!queue.isEmpty()){
int size = queue.size();
double sum = 0;
for(int i = 0; i < size; i++){
root = queue.poll();
sum += root.val;
if(root.left != null) queue.add(root.left);
if(root.right != null) queue.add(root.right);
}
list.add(sum / size);
}
return list;
}
}
树左下角的值
Input:
1
/ \
2 3
/ / \
4 5 6
/
7
Output:
7
BFS
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
root = queue.poll();
//现加右边
if(root.right != null) queue.add(root.right);
if(root.left != null) queue.add(root.left);
}
return root.val;
}
}
DFS,可参考树的高度
class Solution {
private int maxLevel = -1,val = 0;
public int findBottomLeftValue(TreeNode root) {
dfs(root,0);
return val;
}
public void dfs(TreeNode root,int level){
if(root == null) return;
if(maxLevel < level){
maxLevel = level;
val = root.val;
}
dfs(root.left,level + 1);
dfs(root.right,level + 1);
}
}
二叉树遍历(非递归)
三种遍历方法(人工)得到的结果分别是:
先序:1 2 4 6 7 8 3 5
中序:4 7 6 8 2 1 3 5
后序:7 8 6 4 2 5 3 1
-
先序:考察到一个节点后,即刻输出该节点的值,并继续遍历其左右子树。(根左右)
-
中序:考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树。(左根右)
-
后序:考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值。(左右根)
迭代框架
public List<Integer> traversal(TreeNode root) {
if (root == null) return new ArrayList<Integer>();
TreeNode node = root;
List<Integer> ret = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<TreeNode>();
while(node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
// 先序遍历
node = node.left;
}
node = stack.pop();
// 中序遍历
node = node.right;
// 后序遍历
}
return ret;
}
前序遍历
- 迭代
//遍历顺序中左右,输出顺序中左右
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
while (cur != null){
res.add(cur.val);
stack.push(cur);
cur = cur.left;
}
//直到当前结点有右子节点
cur = stack.pop();
cur = cur.right;
}
return res;
}
}
时间复杂度O(n),空间复杂度O(h),h是树的高度
中序遍历
- 遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {// if (root == null) return res;
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.isEmpty()) {
//找到左子树最左边的结点
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
res.add(root.val);
root = root.right;
}
return res;
}
}
后序遍历
- 迭代写法一
//后序遍历顺序为左右中,将前序遍历顺序中左右->中右左,输出顺序再反一下
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
//addLast方法不是常规List接口的一部分,必须直接实现LinkedList接口
LinkedList<Integer> res = new LinkedList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
if (root == null){
return res;
}
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
//添加到表头,输出顺序正好相反
res.addFirst(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
return res;
}
}
或者用迭代模板
//后序遍历顺序为左右中,将前序遍历顺序中左右->中右左,输出顺序再反一下
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
//addLast方法不是常规List接口的一部分,必须直接实现LinkedList接口
LinkedList<Integer> res = new LinkedList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
if (root == null){
return res;
}
while (root != null || !stack.isEmpty()){
while (root != null){
res.addFirst(root.val);
stack.push(root);
root = root.right;
}
root = stack.pop();
root = root.left;
}
return res;
}
}
- 迭代写法二
后序遍历在决定是否可以输出当前节点的值的时候,需要考虑其左右子树是否都已经遍历完成。
所以需要设置一个last指针。
若 last 等于当前考查节点的右子树,表示该节点的左右子树都已经遍历完成,则可以输出当前节点。
并把 last 节点设置成当前节点,将当前游标节点node设置为空,下一轮就可以访问栈顶元素。
否则,需要接着考虑右子树,node = node.right。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
LinkedList<Integer> res = new LinkedList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
if (root == null){
return res;
}
TreeNode last = null;
while (root != null || !stack.isEmpty()){
while (root != null){
stack.push(root);
root = root.left;
}
root = stack.peek();
if (root.right == null || root.right == last){
res.add(root.val);
last = root;
stack.pop();
root = null;
}else {
root = root.right;
}
}
return res;
}
}
莫里斯算法
Morris遍历使用二叉树节点中大量指向null的指针,由Joseph Morris 于1979年发明。
时间复杂度:O(n)
额外空间复杂度:O(1)
在这边先讲解一下Morris的通用解法过程。
Morris的整体思路就是 以某个根结点开始,找到它左子树的最右侧节点之后与这个根结点进行连接
我们知道,左子树最后遍历的节点一定是一个叶子节点,它的左右孩子都是 null,我们把它右孩子指向当前根节点,这样的话我们就不需要额外空间了。这样做,遍历完当前左子树,就可以回到根节点了。
当然如果当前根节点左子树为空,那么我们只需要保存根节点的值,然后考虑右子树即可。
所以总体思想就是:记当前遍历的节点为 cur。
1、cur.left 为 null,保存 cur 的值,更新 cur = cur.right
2、cur.left 不为 null,找到 cur.left 这颗子树最右边的节点记做 pre
-
pre.right 为 null,那么将 pre.right = cur,更新 cur = cur.left
-
pre.right 不为 null (
pre.right == cur
),说明之前已经访问过,第二次来到这里,表明当前子树遍历完成,保存 cur 的值,更新 cur = cur.right
前序遍历
- 在某个根结点创建连线的时候打印。因为我们是顺着左边的根节点来创建连线,且创建的过程只有一次。
2 打印某些自身无法创建连线的节点,也就是叶子节点。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null){
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<>();
TreeNode cur = root;
TreeNode predecessor = null;
while (cur != null) {
if (cur.left == null) {
res.add(cur.val);
cur = cur.right;
} else {
predecessor = cur.left;
//找到当前左子树的最右侧节点,且这个节点应该在指向根结点之前,否则整个节点又回到了根结点。
while (predecessor.right != null && predecessor.right != cur) {
predecessor = predecessor.right;
}
//这个时候如果最右侧这个节点的右指针没有指向根结点,创建连接然后往下一个左子树的根结点进行连接操作。
if (predecessor.right == null) {//第一次到达左子树最右端
res.add(cur.val);
predecessor.right = cur;
cur = cur.left;
}
//当左子树的最右侧节点有指向根结点,此时说明我们已经回到了根结点并重复了之前的操作,同时在回到根结点的时候我们应该已经处理完左子树的最右侧节点了,把路断开。
else {//第二次到达左子树最右端
predecessor.right = null;
cur = cur.right;
}
}
}
return res;
}
}
空间复杂度用到res,实际还是O(n)
中序遍历
Morris中序遍历和前序遍历相差不大,只是在输出位置上出现了变化。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {// if (root == null) return res;
if (root == null){
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<>();
TreeNode cur = root;
TreeNode predecessor = null;
while (cur != null) {
if (cur.left == null) {
res.add(cur.val);
cur = cur.right;
} else {
predecessor = cur.left;
while (predecessor.right != null && predecessor.right != cur) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {//第一次到达左子树最右端
predecessor.right = cur;
cur = cur.left;
} else {//第二次到达左子树最右端
res.add(cur.val);
predecessor.right = null;
cur = cur.right;
}
}
}
return res;
}
}
后序遍历
写法一,将前序遍历的 left 改成 right,最后倒置输出
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null){
return new ArrayList<Integer>();
}
List<Integer> res = new ArrayList<>();
TreeNode cur = root;
TreeNode predecessor = null;
while (cur != null) {
if (cur.right == null) {
res.add(cur.val);
cur = cur.left;
} else {
predecessor = cur.right;
while (predecessor.left != null && predecessor.left != cur) {
predecessor = predecessor.left;
}
if (predecessor.left == null) {//第一次到达左子树最右端
res.add(cur.val);
predecessor.left = cur;
cur = cur.right;
} else {//第二次到达左子树最右端
predecessor.left = null;
cur = cur.left;
}
}
}
Collections.reverse(res);
return res;
}
}
写法二
我们会发现除了叶子节点只访问一次,其他节点都会访问两次,结合下图。
当第二次访问某个节点的时候,我们只需要将它的左节点,以及左节点的右节点,左节点的右节点的右节点… 逆序添加到 list 中即可。比如上边的例子。
上边的遍历顺序其实就是按照深度优先的方式。
先访问 15, 7, 3, 1 然后往回走
3 第二次访问,将它的左节点逆序加入到 list 中
list = [1]
继续访问 2, 然后往回走
7 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中
list = [1 2 3]
继续访问 6 4, 然后往回走
6 第二次访问, 将它的左节点逆序加入到 list 中
list = [1 2 3 4]
继续访问 5, 然后往回走
15 第二次访问, 将它的左节点, 左节点的右节点, 左节点的右节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7]
然后访问 14 10 8, 然后往回走
10 第二次访问,将它的左节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8]
继续访问 9, 然后往回走
14 第二次访问,将它的左节点,左节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10]
继续访问 13 11, 然后往回走
13 第二次访问, 将它的左节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10 11]
继续遍历 12,结束遍历
然后单独把根节点,以及根节点的右节点,右节点的右节点,右节点的右节点的右节点逆序加入到 list 中
list = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
得到 list 就刚好是后序遍历
当遇到第二次访问的节点,我们将单链表逆序,然后加入到 list 并且还原即可。
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
// 情况 1
if (cur.left == null) {
cur = cur.right;
} else {
// 找左子树最右边的节点
TreeNode pre = cur.left;
while (pre.right != null && pre.right != cur) {
pre = pre.right;
}
// 情况 2.1
if (pre.right == null) {
pre.right = cur;
cur = cur.left;
}
// 情况 2.2,第二次遍历节点
if (pre.right == cur) {
pre.right = null; // 这里可以恢复为 null
//逆序
TreeNode head = reverse(cur.left);
//加入到 list 中,并且把逆序还原(还原树结构)
reverseList(head, list);
cur = cur.right;
}
}
}
TreeNode head = reversList(root);
reversList(head, list);
return list;
}
//反转单链表,将next节点改为right即可
private TreeNode reverse(TreeNode head) {
if (head == null) {
return null;
}
TreeNode tail = head;
head = head.right;
tail.right = null;
while (head != null) {
TreeNode temp = head.right;
head.right = tail;
tail = head;
head = temp;
}
return tail;
}
//反转链表同时将节点值记录到链表中
private TreeNode reverseList(TreeNode head, List<Integer> list) {
if (head == null) {
return null;
}
TreeNode tail = head;
head = head.right;
list.add(tail.val);
tail.right = null;
while (head != null) {
TreeNode temp = head.right;
head.right = tail;
tail = head;
list.add(tail.val);
head = temp;
}
return tail;
}
BST
二叉查找树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。
二叉查找树中序遍历有序。
修剪二叉搜索树
Input:
3
/ \
0 4
\
2
/
1
L = 1
R = 3
Output:
3
/
2
/
1
题目描述:只保留值在 L ~ R 之间的节点
class Solution {
public TreeNode trimBST(TreeNode root, int L, int R) {
//确定终止条件
if(root == null) return null;
if (root.val < L) {
TreeNode right = trimBST(root.right, L, R);
return right;
}
if (root.val > R){
TreeNode left = trimBST(root.left, L, R);
return left;
}
//将下一层处理完左子树的结果赋给root.left
root.left = trimBST(root.left, L, R);
root.right = trimBST(root.right, L, R);
return root;
}
}
- 迭代
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int L, int R) {
if (!root) return nullptr;
// 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭
while (root->val < L || root->val > R) {
if (root->val < L) root = root->right; // 小于L往右走
else root = root->left; // 大于R往左走
}
TreeNode *cur = root;
// 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
while (cur != nullptr) {
while (cur->left && cur->left->val < L) {
cur->left = cur->left->right;
}
cur = cur->left;
}
cur = root;
// 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
while (cur != nullptr) {
while (cur->right && cur->right->val > R) {
cur->right = cur->right->left;
}
cur = cur->right;
}
return root;
}
};
二叉搜索树中第K小的元素
class Solution {
private int cnt,val;
public int kthSmallest(TreeNode root, int k) {
inOrder(root, k);
return val;
}
//实际就是中序遍历,二叉搜索书的中序遍历升序
private void inOrder(TreeNode root,int k){
if (root == null) return;
inOrder(root.left,k);
cnt++;
if (cnt == k){
val = root.val;
return;
}
inOrder(root.right,k);
}
}
把二叉搜索树转换为累加树
使得每个节点的值是原来的节点值加上所有大于它的节点值之和
力扣538
Input: The root of a Binary Search Tree like this:
5
/ \
2 13
Output: The root of a Greater Tree like this:
18
/ \
20 13
按照右-中-左
的顺序遍历累加即可
class Solution {
private int sum;
public TreeNode convertBST(TreeNode root) {
inOrder(root);
return root;
}
private void inOrder(TreeNode root){
if (root == null) return;
inOrder(root.right);
sum += root.val;
root.val = sum;
inOrder(root.left);
}
}
反序的中序Morris遍历
class Solution {
public TreeNode convertBST(TreeNode root) {
TreeNode cur = root;
TreeNode pre = null;
int sum = 0;
while (cur != null){
if (cur.right == null){
sum += cur.val;
cur.val = sum;
cur = cur.left;
}else{
pre = cur.right;
while (pre.left != null && pre.left != cur){
pre = pre.left;
}
if (pre.left == null){
pre.left = cur;
cur = cur.right;
}else if (pre.left == cur){
pre.left = null;
sum += cur.val;
cur.val = sum;
cur = cur.left;
}
}
}
return root;
}
}
二叉搜索树的最近公共祖先
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
return root;
}
}
二叉树的最近公共祖先
力扣236
当我们用递归去做这个题时不要被题目误导,应该要明确一点
这个函数的功能有三个:给定两个节点 p 和 q
如果 p 和 q 都存在,则返回它们的公共祖先;
如果只存在一个,则返回存在的一个;
如果 p 和 q 都不存在,则返回NULL
本题说给定的两个节点都存在,那自然还是能用上面的函数来解决
具体思路:
(1) 如果当前结点 root 等于 NULL,则直接返回 NULL
(2) 如果 root 等于 p 或者 q ,那这棵树一定返回 p 或者 q
(3) 然后递归左右子树,因为是递归,使用函数后可认为左右子树已经算出结果,用 left 和 right 表示
(4) 此时若left为空,那最终结果只要看 right;若 right 为空,那最终结果只要看 left
(5) 如果 left 和 right 都非空,因为只给了 p 和 q 两个结点,都非空,说明一边一个,因此 root 是他们的最近公共祖先
(6) 如果 left 和 right 都为空,则返回空(其实已经包含在前面的情况中了)
时间复杂度是 O(n):每个结点最多遍历一次
空间复杂度是 O(n):需要系统栈空间
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left == null) return right;
if (right == null) return left;
return root;//左右非空,说明p和q在root两侧
}
}
有序数组转换二叉搜索树
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return dfs(nums, 0, nums.length - 1);
}
private TreeNode dfs(int[] nums,int lo,int hi){
if (lo > hi) return null;
int mid = lo + (hi - lo) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = dfs(nums, lo, mid - 1);
root.right = dfs(nums, mid + 1, hi);
return root;
}
}
时间复杂度:用O(1)的时间找到数组中间的元素,每个元素都被遍历到,所以是O(n)
空间复杂度:递归占用的栈空间:O(logn)(平衡二叉树 )
有序链表转换二叉搜索树
力扣109
递归:链表找中点,用到快慢指针
class Solution {
public TreeNode sortedListToBST(ListNode head) {
if (head == null) return null;
if (head.next == null) return new TreeNode(head.val);
ListNode pre = null;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null){
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
pre.next = null;//断开链表,方便下次递归
TreeNode root = new TreeNode(slow.val);
root.left = sortedListToBST(head);
root.right = sortedListToBST(slow.next);
return root;
}
}
中序遍历模拟
主要思想:因为我们知道题目给定的升序数组,其实就是二叉搜索树的中序遍历。那么我们完全可以按照这个顺序去为每个节点赋值。
在这个算法中,每当我们构建完二叉搜索树的左半部分时,链表中的头指针将指向根节点或中间节点(它成为根节点)。 因此,我们只需使用头指针指向当前值作为根节点,并将指针后移一位,即 head = head.next
。
class Solution {
ListNode cur = null;
public TreeNode sortedListToBST(ListNode head) {
int len = 0;
cur = head;
while (head != null){
len++;
head = head.next;
}
return convertListToBST(0, len - 1);
}
private TreeNode convertListToBST(int lo, int hi) {
if (lo > hi) return null;
int mid = lo + (hi - lo) / 2;
//遍历左子树并将当前根节点返回,左子树遍历完head将是根节点或中间节点(它将称为根节点)
TreeNode left = convertListToBST(lo, mid - 1);
//遍历当前根节点并赋值
TreeNode root = new TreeNode(cur.val);
root.left = left;
//指针后移,方便下一次赋值,一定要放在right前面left后面,模拟中序遍历顺序
cur = cur.next;
//遍历右子树并将当前根节点返回
TreeNode right = convertListToBST(mid + 1,hi);
root.right = right;
return root;
}
}
两数之和IV
Input:
5
/ \
3 6
/ \ \
2 4 7
Target = 9
Output: True
中序遍历+双指针
使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。
应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。
class Solution {
public boolean findTarget(TreeNode root, int k) {
// if (root == null) return false;
List<Integer> list = new ArrayList<>();
inOrder(root, list);
int i = 0,j = list.size() - 1;
while (i < j){
int sum = list.get(i) + list.get(j);
if (sum == k) return true;
else if (sum < k){
i++;
}else {
j--;
}
}
return false;
}
private void inOrder(TreeNode root, List<Integer> list){
if (root == null) return;
inOrder(root.left,list);
list.add(root.val);
inOrder(root.right, list);
}
}
时间复杂度O(n),空间复杂度O(n)
Hashset
class Solution {
public boolean findTarget(TreeNode root, int k) {
Set<Integer> set = new HashSet<>();
return helper(root, set, k);
}
private boolean helper(TreeNode root, Set<Integer> set,int k){
if (root == null) return false;
if (set.contains(k - root.val)) return true;
set.add(root.val);
return helper(root.left, set, k) || helper(root.right, set, k);
}
}
时间复杂度O(n) 空间复杂度O(n)
二叉搜索树的最小绝对差
力扣530
利用二叉查找树的中序遍历为有序的性质,计算中序遍历中临近的两个节点之差的绝对值,取最小值。
class Solution {
private int min = Integer.MAX_VALUE;
private TreeNode preNode;
public int getMinimumDifference(TreeNode root) {
inOrder(root);
return min;
}
private void inOrder(TreeNode root){
if (root == null) return;
inOrder(root.left);
if (preNode != null){
min = Math.min(Math.abs(root.val - preNode.val), min);
}
preNode = root;
inOrder(root.right);
}
}
二叉搜索树中出现次数最多的值
力扣501
思路:二叉搜索树的中序遍历是一个升序序列,逐个比对当前结点(root)值与前驱结点(preNode)值。更新当前节点值出现次数(curCnt)及最大出现次数(maxCnt),更新规则:若curCnt=maxCnt
,将root.val
添加到结果向量(list
)中;若curCnt > maxCnt
,清空list
,将root.val
添加到list
,并更新maxCnt
为curCnt
。
答案可能不止一个,也就是有多个值出现的次数一样多
class Solution {
private TreeNode preNode;
private int maxCnt = 0;
private int curCnt = 0;
public int[] findMode(TreeNode root) {
List<Integer> list = new ArrayList<>();
inOrder(root, list);
int cnt = 0;
int[] res = new int[list.size()];
for (int num : list){
res[cnt++] = num;
}
return res;
}
private void inOrder(TreeNode root,List<Integer> list){
if (root == null) return;
inOrder(root.left, list);
if (preNode != null && preNode.val == root.val){
curCnt++;
}else {
curCnt = 1;
}
if (curCnt > maxCnt){
maxCnt = curCnt;
list.clear();//清空,重新添加新的值
list.add(root.val);
}else if (curCnt == maxCnt){
list.add(root.val);
}
preNode = root;
inOrder(root.right, list);
}
}
单词查找树(Trie)
Trie,又称前缀树或字典树,用于判断字符串是否存在或者是否具有某种字符串前缀。
实现一个Trie
力扣208
class Trie {
class Node{
boolean isEnd;
Node[] next = new Node[26];
}
private Node root;
/** Initialize your data structure here. */
public Trie() {
root = new Node();
}
/** Inserts a word into the trie. */
public void insert(String word) {
Node node = root;//root为空链接,不要更改
for (char c : word.toCharArray()){
if (node.next[c - 'a'] == null){
//初始化一个新节点
node.next[c - 'a'] = new Node();
}
node = node.next[c - 'a'];//迭代
}
node.isEnd = true;//单词末尾
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Node node = root;
for (char c : word.toCharArray()){
node = node.next[c - 'a'];
if (node == null) return false;
}
return node.isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
Node node = root;
for (char c : prefix.toCharArray()){
node = node.next[c - 'a'];
if (node == null) return false;
}
return true;
}
}
实现一个Trie,用来实现前缀和
Input: insert("apple", 3), Output: Null
Input: sum("ap"), Output: 3
Input: insert("app", 2), Output: Null
Input: sum("ap"), Output: 5
class MapSum {
class Node{
int val;
boolean isEnd;
Node[] next;
public Node(){
this.val = val;
this.isEnd = false;
next = new Node[26];
}
}
private Node root;
/** Initialize your data structure here. */
public MapSum() {
root = new Node();
}
public void insert(String key, int val) {
Node node = root;
for (char c : key.toCharArray()){
if (node.next[c - 'a'] == null){
node.next[c - 'a'] = new Node();
}
node = node.next[c - 'a'];
}
node.isEnd = true;
node.val = val;
}
public int sum(String prefix) {
Node node = root;
for (char c : prefix.toCharArray()){
if (node.next[c - 'a'] == null) return 0;
node = node.next[c - 'a'];
}
//此时到达前缀末尾
return dfs(node);
}
private int dfs(Node node){
if (node == null) return 0;
int curSum = 0;
if (node.isEnd == true) curSum += node.val;//插入单词的末尾
//可以直接用next指代下一个连接的节点
for (Node cur : node.next){
curSum += dfs(cur);//递归
}
return curSum;
}
}