---
主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black
贡献主题:github.com/xitu/juejin…
theme: juejin highlight:
树的遍历
树的遍历算法分为深度优先搜索(dfs)和广度优先搜索(bfs)两种;
dfs 根据根节点的出现位置细分为先序、中序、后序三种方式。bfs 是自上而下、逐层遍历,即层次遍历。
先序遍历
先序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
递归
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
public void dfs(TreeNode root, List<Integer> ans) {
if (root == null) return;
ans.add(root.val);
dfs(root.left, ans);
dfs(root.right, ans);
}
}
迭代
手工维护一个栈,模拟递归调用。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
iterate(root, ans);
return ans;
}
public void iterate(TreeNode root, List<Integer> ans) {
if (root == null) return;
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
root = stack.poll();
ans.add(root.val);
if (root.right != null) stack.push(root.right);
if (root.left != null) stack.push(root.left);
}
}
}
莫里斯算法
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
morris(root, ans);
return ans;
}
public void morris(TreeNode root, List<Integer> ans) {
if (root == null) return;
TreeNode node = root;
while (node != null) {
if (node.left != null) {
TreeNode pred = node.left;
while (pred.right != null && pred.right != node) {
pred = pred.right;
}
if (pred.right == null) {
pred.right = node;
ans.add(node.val);
node = node.left;
} else if (pred.right == node) {
pred.right = null;
node = node.right;
}
} else {
ans.add(node.val);
node = node.right;
}
}
}
}
先判定 root 左孩子是否非空。
1. 非空则沿 root 左孩子的右子树 pred 一路遍历。
1. 当 pred.right == null 时,构建 pred.right = node 的伪边;当前node的值加入结果集;new node = node.left;
2. 当 pred.right == node 时,代表沿之前构建的伪边遍历回来了,意味着左子树遍历完毕,开始遍历右子树。取消伪边;new node = node.right;
2. 为空则 new node = node.right 继续遍历
中序遍历
先序遍历首先访问左孩子,然后访问根节点,最后访问右孩子。
递归
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
public void dfs(TreeNode root, List<Integer> ans) {
if (root == null) return;
dfs(root.left, ans);
ans.add(root.val);
dfs(root.right, ans);
}
}
迭代
思路是沿左孩子节点遍历到底,节点保存到栈中,末尾左孩子节点的值保存到结果集,再遍历左孩子的右子树。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
iterate(root, ans);
return ans;
}
public void iterate(TreeNode root, List<Integer> ans) {
if (root == null) return;
LinkedList<TreeNode> stack = new LinkedList<>();
while (root != null || stack.size() > 0) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.poll();
ans.add(root.val);
root = root.right;
}
}
}
莫里斯算法
同先序遍历的莫里斯算法唯一不同的是:左孩子节点非空情况,保存结果集的时间点不同;中序遍历是当 pred.right 遍历回 node 左子树遍历完毕时,将node的值保存到结果集。先序遍历是当 pred.right == null 构建伪边时,保存node值。
public void morris(TreeNode root, List<Integer> ans) {
if (root == null) return;
TreeNode node = root;
while (node != null) {
if (node.left != null) {
TreeNode pred = node.left;
while (pred.right != null && pred.right != node) {
pred = pred.right;
}
if (pred.right == null) {
pred.right = node;
node = node.left;
} else if (pred.right == node) {
pred.right = null;
ans.add(node.val);
node = node.right;
}
} else {
ans.add(node.val);
node = node.right;
}
}
}
后序遍历
后序遍历是左孩子->根节点->右孩子的遍历顺序。
递归
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
dfs(root, ans);
return ans;
}
public void dfs(TreeNode root, List<Integer> ans) {
if (root == null) return;
dfs(root.left, ans);
dfs(root.right, ans);
ans.add(root.val);
}
}
迭代
后序遍历的迭代方式需要注意两点:
- stack 弹出的节点 root。如果 root.right != null 需要将 root 再次入栈,这会带来一个问题:再次入栈的root它的右孩子总不为空,会陷入死循环。
- 为了避免再次入栈带来的死循环问题,需要记录 root.right 右孩子节点,判断当已经访问过右孩子节点时不再 push_back root。
public void iterate(TreeNode root, List<Integer> ans) {
if (root == null) return;
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode pred = null;
while (root != null || stack.size() > 0) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.poll();
if (root.right != null && root.right != pred) {
stack.push(root.right);
root = root.right;
} else {
ans.add(root.val);
pred = root;
root = null;
}
}
}
莫里斯算法
二叉树的层次遍历
层次遍历就是逐层遍历树结构。使用广度优先搜索维护一个队列执行层次遍历。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
bfs(root, ans);
return ans;
}
public void bfs(TreeNode root, List<List<Integer>> ans) {
if (root == null) return;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> t = new ArrayList<>();
for (int i = 0; i < size; ++i) {
TreeNode node = queue.poll();
t.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
ans.add(t);
}
}
}
利用递归解决问题
二叉树的最大深度
自底而上
自底而上
public int maxDepth(TreeNode root) {
return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
自顶而下
自顶而下意味着每个递归层级我们要先访问节点,计算出一个临时值。在递归调用时将临时值传递给递归函数。在遇到叶子节点时,比较临时值和结果值。
class Solution {
int ans = 0;
public int maxDepth(TreeNode root) {
helper(root, 1);
return ans;
}
public void helper(TreeNode root, int level) {
if (root == null) return;
if (root.left == null && root.right == null) {
ans = Math.max(ans, level);
}
helper(root.left, level + 1);
helper(root.right, level + 1);
}
}
对称二叉树
判断一棵树是否是镜像对称的,即是否满足 l.left == r.right && l.right == r.left。可以通过递归和迭代两种方式求解。
递归
注意边界条件 root == null 空树是对称的。
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return helper(root.left, root.right);
}
public boolean helper(TreeNode l, TreeNode r) {
if (l == null && r == null) return true;
if (l == null || r == null) return false;
return l.val == r.val && helper(l.left, r.right) && helper(l.right, r.left);
}
}
迭代
递归解法转换成迭代法的通常思路是,引入一个队列。这里在初始化时将 root 入队两次,每次迭代出队两个节点 u v。判断 uv 节点值是否相等。再按照 u.left -> v.right,u.right -> v.left 的顺序保证每两个相邻节点的值应该是相等的。
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return iterate(root, root);
}
public boolean iterate(TreeNode u, TreeNode v) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(u);
queue.offer(v);
while (!queue.isEmpty()) {
u = queue.poll();
v = queue.poll();
if (u == null && v == null) {
continue;
}
if (u == null || v == null || u.val != v.val) return false;
queue.offer(u.left);
queue.offer(v.right);
queue.offer(u.right);
queue.offer(v.left);
}
return true;
}
}
路径总和
要特别注意:是到叶子节点的路径总和,不能提前退出。
递归
public boolean dfs(TreeNode root, int sum) {
if (root == null) return false;
if (root.left == null && root.right == null && sum == root.val) return true;
return hasPathSum(root.left, sum - root.val) ||
hasPathSum(root.right, sum - root.val);
}
迭代
迭代法使用到两个队列:节点队列、值队列。节点队列保存下次迭代访问的节点,值队列保存根节点到当前节点的路径和。话句话说入队 root.left 时,是将 root.val + root.left.val 的累加值入队值队列。
public boolean iterate(TreeNode root, int sum) {
if (root == null) return false;
Queue<TreeNode> queueNode = new LinkedList<>();
Queue<Integer> queueVal = new LinkedList<>();
queueNode.offer(root);
queueVal.offer(root.val);
while (!queueNode.isEmpty()) {
root = queueNode.poll();
int val = queueVal.poll();
if (root.left == null && root.right == null && val == sum) return true;
if (root.left != null) {
queueNode.offer(root.left);
int leftSum = val + root.left.val;
queueVal.offer(leftSum);
}
if (root.right != null) {
queueNode.offer(root.right);
int rightSum = val + root.right.val;
queueVal.offer(rightSum);
}
}
return false;
}
路径总和二
找到所有从根节点到叶子节点路径总和等于给定值 sum 的路径。
先序遍历
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> ans = new ArrayList<>();
dfs(root, sum, ans, new LinkedList<>());
return ans;
}
/**先序遍历-记录遍历路径*/
public void dfs(TreeNode root, int sum, List<List<Integer>> ans, Deque<Integer> visited) {
if (root == null) return;
visited.offer(root.val);
if (root.left == null && root.right == null && root.val == sum) {
ans.add(new ArrayList<>(visited));
}
if (root.left != null) {
dfs(root.left, sum - root.val, ans, visited);
}
if (root.right != null) {
dfs(root.right, sum - root.val, ans, visited);
}
// root 是左孩子或右孩子节点,从已选择的列表中剔除
visited.pollLast();
}
}
BFS
迭代遍历二叉树,并通过哈希表记录当前节点与它的父节点。其他和路径总和一迭代解法相同。找到目标叶子节点,借助哈希表向上遍历找父节点路径,保存到结果集。重复这一过程,直到二叉树遍历完毕。
class Solution {
Map<TreeNode, TreeNode> parentMap = new HashMap<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<List<Integer>> ans = new ArrayList<>();
bfs(root, sum, ans);
return ans;
}
public void bfs(TreeNode root, int sum, List<List<Integer>> ans) {
if (root == null) return;
Queue<Integer> vals = new LinkedList<>();
Queue<TreeNode> nodes = new LinkedList<>();
vals.offer(root.val);
nodes.offer(root);
while (!nodes.isEmpty()) {
root = nodes.poll();
int v = vals.poll();
if (root.left == null && root.right == null && v == sum) {
//recursive find parent
findPathSum(root, ans);
} else {
if (root.left != null) {
parentMap.put(root.left, root);
vals.offer(root.left.val + v);
nodes.offer(root.left);
}
if (root.right != null) {
parentMap.put(root.right, root);
vals.offer(root.right.val + v);
nodes.offer(root.right);
}
}
}
}
public void findPathSum(TreeNode node, List<List<Integer>> ans) {
List<Integer> t = new LinkedList<>();
while (node != null) {
t.add(node.val);
node = parentMap.get(node);
}
Collections.reverse(t);
ans.add(t);
}
}
路径总和三
双递归
双递归暴力求解,时间复杂度 O(n^2)。空间复杂度O(1)。外层递归判断从当前节点出发的路径个数+从孩子节点出发的路径个数。
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);
}
public int helper(TreeNode root, int sum) {
if (root == null) return 0;
int t = 0;
sum -= root.val;
if (sum == 0) {
t++;
}
return t + helper(root.left, sum) + helper(root.right, sum);
}
}
前缀和
前缀和:到达当前节点的之前元素的和。如果两个节点的前缀和相同,那么这两节点之前的元素个数是零。引申一下:题意要求我们求解两点之间路径和等于给定值sum的路径个数,可以转换成AB两点间的前缀和的差值是sum。
- 抵达当前节点后将前缀和累加
- 查询前缀和哈希表中当前前缀和的路径个数,没有则取默认值0
- 将当前路径的前缀和更新到哈希表中,prefixSumCount.put(prefixSumCount.getOrDefault(currSum,0) + 1);
class Solution {
public int pathSum(TreeNode root, int sum) {
// key是前缀和, value是大小为key的前缀和出现的次数
Map<Integer, Integer> prefixSumCount = new HashMap<>();
// 前缀和为0的一条路径
prefixSumCount.put(0, 1);
// 前缀和的递归回溯思路
return recursionPathSum(root, prefixSumCount, sum, 0);
}
/**
* 前缀和的递归回溯思路
* 从当前节点反推到根节点(反推比较好理解,正向其实也只有一条),有且仅有一条路径,因为这是一棵树
* 如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
* 所以前缀和对于当前路径来说是唯一的,当前记录的前缀和,在回溯结束,回到本层时去除,保证其不影响其他分支的结果
* @param node 树节点
* @param prefixSumCount 前缀和Map
* @param target 目标值
* @param currSum 当前路径和
* @return 满足题意的解
*/
private int recursionPathSum(TreeNode node, Map<Integer, Integer> prefixSumCount, int target, int currSum) {
// 1.递归终止条件
if (node == null) {
return 0;
}
// 2.本层要做的事情
int res = 0;
// 当前路径上的和
currSum += node.val;
//---核心代码
// 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
// 当前节点->root节点反推,有且仅有一条路径,如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
// currSum-target相当于找路径的起点,起点的sum+target=currSum,当前点到起点的距离就是target
res += prefixSumCount.getOrDefault(currSum - target, 0);
// 更新路径上当前节点前缀和的个数
prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);
//---核心代码
// 3.进入下一层
res += recursionPathSum(node.left, prefixSumCount, target, currSum);
res += recursionPathSum(node.right, prefixSumCount, target, currSum);
// 4.回到本层,恢复状态,去除当前节点的前缀和数量
prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
return res;
}
}
前缀和-和为K的子数组
前缀和数组的双重遍历
preSum[i] 的含义就是 nums[0...i-1]的和,那么如果我们想要求 nums[j... i] 的和只需要 preSum[i+1] - preSum[j] 即可。在本题求解和为 k 的子数组中,比如要求解 nums[0...i] 存在多少个和为K的子数组,是把 i 作为固定结束坐标,j 从0开始递增一直到 i-1,判断 nums[i] - nums[0..i-1] = K 的个数。
public int violence(int[] nums, int k) {
if (nums == null || nums.length == 0) return 0;
//构造前缀和数组
int[] preSum = new int[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
preSum[i + 1] = preSum[i] + nums[i];
}
int ret = 0;
for (int i = 1; i < preSum.length; i++) {
for (int j = 0; j < i; j++) {
if (preSum[i] - preSum[j] == k) ret++;
}
}
return ret;
}
哈希表
借助哈希表实现查询有几个sum_0j == sum_0i - k,实现上是到哈希表中查找(sum_0i -k)出现的个数。
这样时间复杂度降低到O(N),空间复杂度O(N)。
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> preSum = new HashMap<>();
//base case
preSum.put(0, 1);
int ans = 0, sum_0i = 0;
for (int i = 0; i < nums.length; i++) {
//当前下标元素的前缀和
sum_0i += nums[i];
//前缀和差是k的开始节点
int sum_0j = sum_0i - k;
//累加答案
ans += preSum.getOrDefault(sum_0j, 0);
//更新当前前缀和-出现次数到哈希表
preSum.put(sum_0i, preSum.getOrDefault(sum_0i, 0) + 1);
}
return ans;
}
}