二叉树总结

关于二叉树,最重要的莫过于遍历了。

  • 深度优先遍历和层次遍历(广度优先遍历)
  • 递归实现和迭代实现
  • 前序遍历、后序遍历以及中序遍历

那么一共有(3+2)*2=10种模版之多。先看前序遍历。

前序遍历

递归实现

这是最简单的前序遍历的递归实现

1
2
3
4
5
6
7
void dfs(TreeNode* root){
if(root==nullptr) reutrn root;
// 处理root->val //中
dfs(root->left); //左
dfs(root->right); //右
return;
}

迭代实现

借助栈stack实现前序遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
int[] preorderTraversal(TreeNode root){
if(root == null) return root;
List<Integer> ans = new ArrayList<Integer>();
Stack<TreeNode> stk = new Stack<>();
stk.push(root);
while(!stk.isEmpty()){
TreeNode cur = stk.pop();
ans.add(cur.val);
if(cur.right!=null) stk.push(cur.right);
if(cur.left!=null) stk.push(cur.left);
}
return ans;
}

注意点:

  1. 空节点不入栈(若空节点入栈,则出栈时需要加以校验)
  2. 入栈顺序(入栈顺序与前序遍历相反)

层次遍历

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// val值存储说明:一层存一个List,所以是二维数组
public List<List<Integer>> ans = new ArrayList<>();

public void bfs(TreeNode root, int depth){
if(root==null) return ;
// 需要判断当前的depth是否是要处理
if(ans.size() == depth)
ans.add(new ArrayList<Integer>());
// 处理val
ans[depth].add(root.val);
// 处理下一层
bfs(root.left, 1+depth);
bfs(root.right, 1+depth);
}

// 调用方式
bfs(root, 0);

注意点:

  1. 答案的存储方式:一层存一个List,所以是个二维数组;
  2. 层次遍历需要传入深度信息,利用二维数组来判断当前遍历到的层数。

迭代实现

迭代实现借助队列,由于队列先进先出,可以记录一层的数量。

(1)实现一

1
2
3
4
5
6
7
8
9
10
11
12
13
public int[] levelOrder(TreeNode root){
if(root==null) return;
List<Integer> ans = new ArrayList<>();
Queue<TreeNode> que = new ArrayList<>();
que.offer(root);
while(!que.isEmpty()){
TreeNode node = que.poll();
ans.add(node.val);
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
return ans.toArray();
}

注意点:

  1. 空节点不入队或节点出队判空
  2. 可以不用二维数组处理答案

(2)实现二

巧妙的利用队列的性质,for 长度遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// val值存储说明:一层存一个List,所以是二维数组
public List<List<Integer>> ans = new ArrayList<>();

public int[] levelOrder(TreeNode root){
if(root==null) return;
List<Integer> ans = new ArrayList<>();
Queue<TreeNode> que = new ArrayList<>();
que.offer(root);
while(!que.isEmpty()){
int count = que.size();
for(int i=0; i<count;++i){ // 特殊处理!
TreeNode node = que.poll();
ans.add(node.val);
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
}

return ans-to-int-array;
}

迭代实现中序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node != NULL) {
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)

st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。

if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}

注意处理节点的技巧是:中节点后添加nullptr标志。

在首次遇到节点时,并不首先处理它,而是将它加入栈中,紧跟其后添加一个nullptr标志。而当我们再次取到nullptr时,它提醒我们该处理一个结点了。

这个技巧的思想是:标记,直到遍历完才处理数据。

题目练习

每种题目都尝试递归和迭代两种写法!迭代可以实现层序遍历和深度优先遍历(前中后序遍历)。

BFS

填充每个节点的下一个右侧指针

https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public Node connect(Node root) {
if(root==null) return root;
Queue<Node> que = new ArrayDeque<>();
que.offer(root);
while(!que.isEmpty()){
int count = que.size();
for(int i=0; i<count; ++i){
Node node = que.poll();
if(i!=count-1) node.next = que.peek(); //每层最后一个不需要设置next
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
}
return root;
}
}

层次遍历的应用。

二叉树的最小深度

https://leetcode.cn/problems/minimum-depth-of-binary-tree/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int minDepth(TreeNode root) {
int ans = 0;
if(root == null) return ans;
Queue<TreeNode> que = new ArrayDeque<>();
que.offer(root);
boolean stop = false;
while(!que.isEmpty()){
int size = que.size(); // 层次遍历
++ans;
for(int i=0; i<size; ++i){
TreeNode cur = que.poll();
if(cur.left==null && cur.right==null) //碰到叶子结点就返回
stop = true;
if(cur.left != null)
que.offer(cur.left);
if(cur.right != null)
que.offer(cur.right);
}
if(stop) break;
}
return ans;
}

层次遍历-碰到叶子节点就返回。实际是bfs的应用。

左下角的值

https://leetcode.cn/problems/find-bottom-left-tree-value/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int findBottomLeftValue(TreeNode root) {
if(root==null) return 0;
Queue<TreeNode> que = new ArrayDeque<>();
que.offer(root);
int ans =-1;
while(!que.isEmpty()){
int size = que.size();
for(int i=0; i<size; ++i){
TreeNode node = que.poll();
if(i<1) ans = node.val;
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
}
return ans;
}
}

什么是左下角?数最深一层的第一个节点!还是层次遍历。

DFS

相同的树

https://leetcode.cn/problems/same-tree/

1
2
3
4
5
6
7
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null || q==null) return q==p ? true:false;
if(p.val != q.val) return false;
return isSameTree(p.left, q.left)&&isSameTree(p.right, q.right);
}
}

翻转二叉树

https://leetcode.cn/problems/invert-binary-tree/description/

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public TreeNode invertTree(TreeNode root) {
//递归思想
if(root==null) return root;
TreeNode rl = invertTree(root.left);
TreeNode rr = invertTree(root.right);
root.left = rr;
root.right = rl;
return root;
}
}

这道题考的其实是递归的思想。

左叶子之和

https://leetcode.cn/problems/sum-of-left-leaves/

注意左叶子的定义,需要通过父节点来判断。

1
2
3
4
5
6
7
8
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root==null) return 0;
if(root.left!=null && root.left.left==null && root.left.right==null)
return root.left.val+sumOfLeftLeaves(root.right)+sumOfLeftLeaves(root.left);
return sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right);
}
}
1
2
3
if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) {
左叶子节点处理逻辑
}

路径总和

https://leetcode.cn/problems/path-sum/

1
2
3
4
5
6
7
8
9
10
class Solution {
public boolean dfs(TreeNode root, int targetSum){
if(root==null) return false;
if(root.left==null&&root.right==null&&targetSum==root.val) return true;
return dfs(root.left, targetSum-root.val) || dfs(root.right, targetSum-root.val);
}
public boolean hasPathSum(TreeNode root, int targetSum) {
return dfs(root, targetSum);
}
}

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

路径开始节点固定:根结点。那么很容易利用递归整棵树。

路径总和2

https://leetcode.cn/problems/path-sum-ii/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public void dfs(TreeNode root, int targetSum){
if(root==null) return;
path.add(root.val);
if(root.left==null&&root.right==null&&targetSum==root.val){
ans.add(new ArrayList<Integer>(path));
}
dfs(root.left, targetSum-root.val);
dfs(root.right, targetSum-root.val);
path.remove(path.size()-1);
}
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return ans;
}
}

回溯法来收集路径上的值。

路径总和3

https://leetcode.cn/problems/sum-root-to-leaf-numbers/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
int ans=0;
List<Integer> path = new ArrayList<>();
public void dfs(TreeNode root, int cur){
if(root==null) return;
path.add(root.val);
int next_cur = cur*10 +root.val;
if(root.left==null&&root.right==null){
ans +=next_cur;
}
dfs(root.left, next_cur);
dfs(root.right, next_cur);
path.remove(path.size()-1);
}
public int sumNumbers(TreeNode root) {
dfs(root, 0);
return ans;
}

二叉树的所有路径

https://leetcode.cn/problems/binary-tree-paths/

(1)字符串普通拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public List<String> ans = new ArrayList<>();
public void dfs(TreeNode root, String str){
if(root==null) return;
if(root!=null && root.left==null && root.right==null){
ans.add(str+String.valueOf(root.val));
return;
}
str = str + String.valueOf(root.val) + "->"; //这一步新建String
dfs(root.left, str);
dfs(root.right, str);
}
public List<String> binaryTreePaths(TreeNode root) {
dfs(root, "");
return ans;
}
}

(2)java优化写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public List<String> res = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) {
if(root==null) return res;
dfs(root,new StringBuilder());
return res;
}
public void dfs(TreeNode r, StringBuilder s){
if(r==null) return;
s.append(r.val);
if(r.left==null && r.right==null){
res.add(s.toString());
}
dfs(r.left,new StringBuilder(s).append("->"));
dfs(r.right,new StringBuilder(s).append("->"));
}
}

二叉树的直径

https://leetcode.cn/problems/diameter-of-binary-tree/description/

(1)暴力DFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
Map<TreeNode, Integer> map = new HashMap<>();
public int dfs(TreeNode root){ //dfs求root节点的深度
if(root==null) return 0;
int left = 1 + dfs(root.left);
int right = 1 + dfs(root.right);
int ret = Math.max(left, right);
if(!map.containsKey(root))
map.put(root, ret);
return ret;
}
public int diameterOfBinaryTree(TreeNode root) {
if(root==null) return 0;
int now = dfs(root.left) + dfs(root.right);
int l = diameterOfBinaryTree(root.left);
int r = diameterOfBinaryTree(root.right);
now = Math.max(now, l);
now = Math.max(now, r);
return now;
}
}

二叉树的直径:二叉树中从一个节点到另一个节点的最长边数和。

从一个节点到另外一个节点,肯定是会经过过某棵树的根,以这个根将路径分为两部分,左子树的高度和右子树的高度。

定义树的高度:从根节点到叶子节点的最长路径边的个数,叶子节点的高度为0。

由此得到递归公式:

1
根节点为root的二叉树的直径 = max(root->left的直径,root->right的直径,2 + root->left高度+root->right高度)

实际上操作时,我们定义新高度值为高度值都加1,即下面情况

1
根节点为root的二叉树的直径 = max(root->left的直径,root->right的直径,1+root->left高度 + 1+root->right高度)

(2)优化DFS

我们利用DFS求高度,遍历了每个节点。同时为了求解答案,我们还需要遍历每个节点的直径。能够将这两个遍历合二为一呢?

答案是可以!在求高度时,我们得出了左右子树的高度。求当前root节点高度的操作是取max,而求直径的操作是加和,两者互不干预。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
int ans = 0;
public int dfs(TreeNode root){
if(root==null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
int depth = 1 + Math.max(left, right);
ans = Math.max(ans, left+right);
return depth;
}
public int diameterOfBinaryTree(TreeNode root) {
dfs(root);
return ans;
}
}

构建二叉树

进阶题目

其他题目

扁平化嵌套列表

https://leetcode.cn/problems/flatten-nested-list-iterator/description/

(1)暴力遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class NestedIterator implements Iterator<Integer> {

private Iterator<Integer> myIt;
private List<Integer> myList = new ArrayList<>();

public void traverse(List<NestedInteger> nestedList){
if(nestedList.isEmpty()) return;
for(NestedInteger node: nestedList){
if(node.isInteger())
myList.add(node.getInteger());
else
traverse(node.getList());
}

}
public NestedIterator(List<NestedInteger> nestedList) {
traverse(nestedList);
myIt = myList.iterator();
}

@Override
public Integer next() {
return myIt.next();
}

@Override
public boolean hasNext() {
return myIt.hasNext();
}
}

其实是N叉树的深度优先遍历。结合迭代器。

(2)优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 优化解法,迭代器惰性求值
public class NestedIterator implements Iterator<Integer> {
private LinkedList<NestedInteger> list;

public NestedIterator(List<NestedInteger> nestedList) {
list = new LinkedList<>(nestedList);
}

@Override
public Integer next() {
return list.remove(0).getInteger();
}

@Override
public boolean hasNext() {
while(!list.isEmpty() && !list.get(0).isInteger()){
List<NestedInteger> nodeList = list.remove(0).getList();
for(int i=nodeList.size()-1; i>=0; --i){
list.addFirst(nodeList.get(i));
}
}
return !list.isEmpty(); // list非空就有下一个
}
}

优化:上述解法将所有叶子节点值存入一个List中,当输入规模非常大时,上述计算会很慢,占用内存。一般迭代器是“惰性”求值的,也就是说,你要一个值,我算一个值,而不是一次将所有结果都求出来。

1
2
3
4
res = []
while iterator.hasNext()
append iterator.next() to the end of res
return res

在调用next前会有hasNext来判断是否已经达到尾巴。那么hashNext做的事就是判断list的第一个元素是否是列表,如果是列表就将其拆开。注意列表是嵌套的,所以要用while循环。

滑动谜题

https://leetcode.cn/problems/sliding-puzzle/description/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Solution {
int ans = 0;
public boolean isCompleted(int[][] board){
for(int i=0; i<3;++i){
if(board[0][i] != (i+1)) return false;
}
if(board[1][0]!=4) return false;
if(board[1][1]!=5) return false;
return true;
}
public int slidingPuzzle(int[][] array) {
Set<String> set = new HashSet<>();
Queue<int[][]> que = new ArrayDeque<>();
que.offer(array);
// 分阶段出队
while(!que.isEmpty()){
int size = que.size();
for(int i=0; i<size; ++i){
int[][] board = que.poll();
if(isCompleted(boarad)) return ans;
// 确定0的位置
int m=0,n=0;
for(int k=0; k<6; ++k){
if(board[k/3][k%3]==0){
m = k/3;
n = k%3;
break;
}
}
// 上
if(m == 1){
int[][] tmp = new int[2][3];
tmp[0] = Arrays.copyOf(board[0], 3);
tmp[1] = Arrays.copyOf(board[1], 3);
tmp[m][n] = tmp[m-1][n];
tmp[m-1][n] = 0;
if(!set.contains(Arrays.deepToString(tmp))){
que.offer(tmp);
set.add(Arrays.deepToString(tmp));
}
}
// 下
if(m == 0){
int[][] tmp = new int[2][3];
tmp[0] = Arrays.copyOf(board[0], 3);
tmp[1] = Arrays.copyOf(board[1], 3);
tmp[m][n] = tmp[m+1][n];
tmp[m+1][n] = 0;
if(!set.contains(Arrays.deepToString(tmp))){
que.offer(tmp);
set.add(Arrays.deepToString(tmp));
}
}
// 左
if(n==1 || n==2){
int[][] tmp = new int[2][3];
tmp[0] = Arrays.copyOf(board[0], 3);
tmp[1] = Arrays.copyOf(board[1], 3);
tmp[m][n] = tmp[m][n-1];
tmp[m][n-1] = 0;
if(!set.contains(Arrays.deepToString(tmp))){
que.offer(tmp);
set.add(Arrays.deepToString(tmp));
}
}
// 右
if(n==0 || n==1){
int[][] tmp = new int[2][3];
tmp[0] = Arrays.copyOf(board[0], 3);
tmp[1] = Arrays.copyOf(board[1], 3);
tmp[m][n] = tmp[m][n+1];
tmp[m][n+1] = 0;
if(!set.contains(Arrays.deepToString(tmp))){
que.offer(tmp);
set.add(Arrays.deepToString(tmp));
}
}
}
ans++;
}
return -1;
}
}

求最短路径:广度优先搜索+回溯法

注意点:

  • 广搜的实现-队列
  • 层次广搜的特定实现方式:for-que.size(), 特别注意要先得到size,否则队列的size会变
  • 为了避免无穷尽的搜索,用set记录走过局面。(这里局面用数组toString的方式记录)
作者

Desirer

发布于

2024-06-04

更新于

2024-06-04

许可协议