二叉树整理

# 二叉树相关oj整理

### 基本概念

二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有深度遍历和广度遍历,深度遍历有前序、中序以及后序三种遍历方法,广度遍历即我们平常所说的层次遍历。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁,而对于广度遍历来说,需要其他数据结构的支撑,比如堆,队列。

四种主要的遍历思想为:

前序遍历:根结点 ---> 左子树 ---> 右子树

中序遍历:左子树---> 根结点 ---> 右子树

后序遍历:左子树 ---> 右子树 ---> 根结点

层次遍历:只需按层次遍历即可


### 基本排序题(简单)

**144.二叉树的前序遍历**
<span id="jump11">
给定一个二叉树,返回它的 前序 遍历。
 示例:
  ```
输入: [1,null,2,3]  
   1
    \
     2
    /
   3 
输出: [1,2,3]
 ```
进阶: 递归算法很简单,你可以通过迭代算法完成吗?

[解析](#jump1)

**102.二叉树的层次遍历**
<span id="jump12">
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7],
  ```
    3
   / \
  9  20
    /  \
   15   7
 ```
返回其层次遍历结果:
  ```
[
  [3],
  [9,20],
  [15,7]
]
  ```
[解析](#jump2)

###基本应用(简单)
**二叉树的最大深度**
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
  ```
    3
   / \
  9  20
    /  \
   15   7
```
返回它的最大深度 3 。

来自 <https://leetcode-cn.com/explore/learn/card/data-structure-binary-tree/3/solve-problems-recursively/12/> 


**对称二叉树**
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
```
   1
   / \
  2   2
 / \ / \
3  4 4  3
```

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

```
    1
   / \
  2   2
   \   \
   3    3
```
   
说明:
如果你可以运用递归和迭代两种方法解决这个问题,会很加分。

来自 <https://leetcode-cn.com/explore/learn/card/data-structure-binary-tree/3/solve-problems-recursively/13/> 


**路径总和**
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例: 
给定如下二叉树,以及目标和 sum = 22,
```
              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1
```
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。

**验证二叉搜索树**
给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:

```
输入:
    2
   / \
  1   3
```

输出: true
示例 2:

```
输入:
    5
   / \
  1   4
     / \
    3   6
```

输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
     根节点的值为 5 ,但是其右子节点值为 4 。


###中阶应用(中等)
**1038. 从二叉搜索树到更大和树**
<span id="jump13">
来自 <https://leetcode-cn.com/problems/binary-search-tree-to-greater-sum-tree/> 

给出二叉搜索树的根节点,该二叉树的节点值各不相同,修改二叉树,使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

提醒一下,二叉搜索树满足下列约束条件:

节点的左子树仅包含键小于节点键的节点。
节点的右子树仅包含键大于节点键的节点。
左右子树也必须是二叉搜索树。
 

示例:
图
![markdown](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/05/03/tree.png "markdown")



输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
 

提示:

树中的节点数介于 1 和 100 之间。
每个节点的值介于 0 和 100 之间。
给定的树为二叉搜索树。

[解析](#jump3)

**814. 二叉树剪枝**
<span id="jump14">
来自 <https://leetcode-cn.com/problems/binary-tree-pruning/> 

给定二叉树根结点 root ,此外树的每个结点的值要么是 0,要么是 1。

返回移除了所有不包含 1 的子树的原二叉树。

( 节点 X 的子树为 X 本身,以及所有 X 的后代。)

示例1:
输入: [1,null,0,0,1]
输出: [1,null,0,null,1]
 
解释: 
只有红色节点满足条件“所有不包含 1 的子树”。
右图为返回的答案。


示例2:
输入: [1,0,1,0,0,0,1]
输出: [1,null,1,null,1]



示例3:
输入: [1,1,0,1,1,0,1,0]
输出: [1,1,0,1,1,null,1]



说明:

给定的二叉树最多有 100 个节点。
每个节点的值只会为 0 或 1 。

[解析](#jump4)

###进阶应用
**968. 监控二叉树**
<span id="jump15">
给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

 

示例 1:






输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
示例 2:




输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
[解析](#jump5)

**99. 恢复二叉搜索树**
<span id="jump16">
 
二叉搜索树中的两个节点被错误地交换。

请在不改变其结构的情况下,恢复这棵树。

示例 1:

输入: [1,3,null,null,2]
```
   1
  /
 3
  \
   2
```
输出: [3,1,null,null,2]
```
   3
  /
 1
  \
   2
示例 2:
```
输入: [3,1,4,null,null,2]
```
  3
 / \
1   4
   /
  2
```
输出: [2,1,4,null,null,3]
```
  2
 / \
1   4
   /
  3
```
进阶:

使用 O(n) 空间复杂度的解法很容易实现。
你能想出一个只使用常数空间的解决方案吗?
[解析](#jump6)

**431. 将 N 叉树编码为二叉树**
<span id="jump17">
Design an algorithm to encode an N-ary tree into a binary tree and decode the binary tree to get the original N-ary tree. An N-ary tree is a rooted tree in which each node has no more than N children. Similarly, a binary tree is a rooted tree in which each node has no more than 2 children. There is no restriction on how your encode/decode algorithm should work. You just need to ensure that an N-ary tree can be encoded to a binary tree and this binary tree can be decoded to the original N-nary tree structure.
For example, you may encode the following 3-ary tree to a binary tree in this way:

设计一种算法,将N元树编码为二叉树,然后对二叉树进行解码以获得原始N元树。 N叉树是一棵有根树,其中每个节点最多只能有N个子节点。类似地,二叉树是一棵有根树,其中每个节点最多有2个子节点。编码/解码算法的工作方式没有限制。您只需要确保可以将N元树编码为二叉树,并且可以将该二叉树解码为原始的N元树结构。 例如,您可以通过以下方式将以下3进制树编码为二进制树



 

 
Note that the above is just an example which might or might not work. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.
 
请注意,以上只是可能有效或无效的示例。您不一定需要遵循这种格式,因此请发挥创造力并自己提出不同的方法。

Note:
1. N is in the range of [1, 1000]
2. Do not use class member/global/static variables to store states. Your encode and decode algorithms should be stateless.

注意:
1. N在[1,1000]范围内
不要使用类成员/全局/静态变量来存储状态。您的编码和解码算法应该是无状态的。
[解析](#jump7)


<span id="jump1"></span>
[二叉树的前序遍历](#jump11)
方法1:递归

首先,定义树的存储结构 TreeNode。

```java
/* Definition for a binary tree node. */
    public class TreeNode {
      int val;
      TreeNode left;
      TreeNode right;
    
      TreeNode(int x) {
        val = x;
      }
    }

class Solution {
        public List<Integer> preorderTraversal(TreeNode root) {
            List<Integer> result = new ArrayList<>();
            rec(root,result);
            return result;
        }
        public static void rec(TreeNode root, List<Integer> res){
            if(root==null) return ;
            res.add(root.val);
            rec(root.left,res);
            rec(root.right,res);
        }
}
```




方法 2:迭代
算法

从根节点开始,每次迭代弹出当前栈顶元素,并将其孩子节点压入栈中,先压右孩子再压左孩子。

在这个算法中,输出到最终结果的顺序按照 Top->Bottom 和 Left->Right,符合前序遍历的顺序。
```java
class Solution {
  public List<Integer> preorderTraversal(TreeNode root) {
    LinkedList<TreeNode> stack = new LinkedList<>();
    LinkedList<Integer> output = new LinkedList<>();
    if (root == null) {
      return output;
    }

    stack.add(root);
    while (!stack.isEmpty()) {
      TreeNode node = stack.pollLast();
      output.add(node.val);
      if (node.right != null) {
        stack.add(node.right);
      }
      if (node.left != null) {
        stack.add(node.left);
      }
    }
    return output;
  }
}
```
算法复杂度

时间复杂度:访问每个节点恰好一次,时间复杂度为 O(N)O(N) ,其中 NN 是节点的个数,也就是树的大小。
空间复杂度:取决于树的结构,最坏情况存储整棵树,因此空间复杂度是 O(N)O(N)。

<span id="jump2"></span>
[二叉树的层次遍历](#jump12)
方法 1:递归
算法

最简单的解法就是递归,首先确认树非空,然后调用递归函数 helper(node, level),参数是当前节点和节点的层次。程序过程如下:

输出列表称为 levels,当前最高层数就是列表的长度 len(levels)。比较访问节点所在的层次 level 和当前最高层次 len(levels) 的大小,如果前者更大就向 levels 添加一个空列表。
将当前节点插入到对应层的列表 levels[level] 中。
递归非空的孩子节点:helper(node.left / node.right, level + 1)。
实现

```java

class Solution {
    List<List<Integer>> levels = new ArrayList<List<Integer>>();

    public void helper(TreeNode node, int level) {
        // start the current level
        if (levels.size() == level)
            levels.add(new ArrayList<Integer>());

         // fulfil the current level
         levels.get(level).add(node.val);

         // process child nodes for the next level
         if (node.left != null)
            helper(node.left, level + 1);
         if (node.right != null)
            helper(node.right, level + 1);
    }
    
    public List<List<Integer>> levelOrder(TreeNode root) {
        if (root == null) return levels;
        helper(root, 0);
        return levels;
    }
}
```
复杂度分析

时间复杂度:O(N)O(N),因为每个节点恰好会被运算一次。
空间复杂度:O(N)O(N),保存输出结果的数组包含 N 个节点的值。

方法 2:迭代
算法

上面的递归方法也可以写成迭代的形式。

我们将树上顶点按照层次依次放入队列结构中,队列中元素满足 FIFO(先进先出)的原则。在 Java 中可以使用 Queue 接口中的 LinkedList实现。在 Python 中如果使用 Queue 结构,但因为它是为多线程之间安全交换而设计的,所以使用了锁,会导致性能不佳。因此在 Python 中可以使用 deque 的 append() 和 popleft() 函数来快速实现队列的功能。

第 0 层只包含根节点 root ,算法实现如下:

初始化队列只包含一个节点 root 和层次编号 0 : level = 0。
当队列非空的时候:
在输出结果 levels 中插入一个空列表,开始当前层的算法。
计算当前层有多少个元素:等于队列的长度。
将这些元素从队列中弹出,并加入 levels 当前层的空列表中。
将他们的孩子节点作为下一层压入队列中。
进入下一层 level++。
实现

```java
class Solution {
  public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> levels = new ArrayList<List<Integer>>();
    if (root == null) return levels;

    Queue<TreeNode> queue = new LinkedList<TreeNode>();
    queue.add(root);
    int level = 0;
    while ( !queue.isEmpty() ) {
      // start the current level
      levels.add(new ArrayList<Integer>());

      // number of elements in the current level
      int level_length = queue.size();
      for(int i = 0; i < level_length; ++i) {
        TreeNode node = queue.remove();

        // fulfill the current level
        levels.get(level).add(node.val);

        // add child nodes of the current level
        // in the queue for the next level
        if (node.left != null) queue.add(node.left);
        if (node.right != null) queue.add(node.right);
      }
      // go to next level
      level++;
    }
    return levels;
  }
}
```
复杂度分析

时间复杂度:O(N)O(N),因为每个节点恰好会被运算一次。
空间复杂度:O(N)O(N),保存输出结果的数组包含 N 个节点的值。

<span id="jump3"></span>
[从二叉搜索树到更大和树](#jump13)
思路:根据二叉搜索树的定义,大于根结点的值都在右子树上,小于根结点的值都在左子树上,于是此题要求将二叉搜索树转变为更大和树,便可等价为求某个节点右边的节点和加上当前节点值(这个值就是题目所说的更大和)赋值给当前节点的新树,那么如何求这个更大和呢?根据二叉搜索树的性质,我们只需按照反转的中序遍历即可求到某个节点的更大值和。


于是解题代码如下:
```java
class Solution {
    public TreeNode bstToGst(TreeNode root) {
        dfs(root);
        return root;
    }
    
    int sum = 0;// 记录每个节点的右边的所有节点和,即原树中大于 node.val 的值之和
    private void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        
        dfs(root.right);// 先访问所有大于root的节点
        root.val += sum;// 赋值给当前节点
        sum = root.val;// 保存更大和
        dfs(root.left);// 后访问小于root的节点
    }
}
```

<span id="jump4"></span>
[二叉树剪枝](#jump14)
从底向上后续遍历,如果当前元素为0且左右子树为null,则删除节点
```java
class Solution {
 public TreeNode pruneTree(TreeNode root) {
    if (root == null) return null;
    root.left = pruneTree(root.left);
    root.right = pruneTree(root.right);
    if (root.val == 0 && root.left == null && root.right == null)
        return null;
    return root;
}
}
```
<span id="jump5"></span>
[监控二叉树](#jump15)
当遍历到一个节点时,我们可以定义三种状态:
0 : 初始状态,如果节点为null可以返回,也就是不影响其他节点,当两个节点都是0时,我们直接设置当前节点为未监控状态
1: 未监控状态,如果子节点含有该状态,则此节点必须添加摄像头,同时返回当前状态为监控态
2: 监控态,表明此节点已经被监控,当子节点为此状态时,父节点不需要添加摄像头,可以返回初始态
```
private int dfs(TreeNode node){
        if (node == null) return 0;

        int l = dfs(node.left);
        int r = dfs(node.right);

        if (l + r == 0)  
            return 1;
        else if (l == 1 || r == 1) {
            cameras ++; return 2;
        } else  
            return 0;
    }
 ```
当调用时,有一个小技巧,我们需要为传入的根节点添加一个虚拟的头,因为向上遍历时,根节点的监控状态我们无法保证,所以添加一个虚拟头可以简化编程。
 ```
public int minCameraCover(TreeNode root) {
        TreeNode dummyHead = new TreeNode(0);
        dummyHead.left = root;
        dfs(dummyHead);
        return cameras;
    }
```

时间复杂度O(N)
空间复杂度如果不算递归的隐式调用栈,为O(1),否则为O(h),h为树的高度。

<span id="jump6"></span>
[恢复二叉搜索树](#jump16)
 ```
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
 
class Solution {
    TreeNode firstNode = null;
    TreeNode secondNode = null;
    TreeNode preNode = new TreeNode(Integer.MIN_VALUE);

    public void recoverTree(TreeNode root) {

        in_order(root);
        int tmp = firstNode.val;
        firstNode.val = secondNode.val;
        secondNode.val = tmp;
    }

    private void in_order(TreeNode root) {
        if (root == null) return;
        in_order(root.left);
        if (firstNode == null && preNode.val > root.val) firstNode = preNode;
        if (firstNode != null && preNode.val > root.val) secondNode = root;
        preNode = root;
        in_order(root.right);
    }
}
 ```

<span id="jump7"></span>
[将 N 叉树编码为二叉树](#jump17)

我们可以看出,N叉树根结点1的第一个子结点3被挂到了二叉树的左子结点上,同一层的结点2挂到了结点3的右子结点上,同一层的结点4被挂到了结点2的右子结点上。而结点3本身的子结点也按照这个规律,第一个子结点5挂到了结点3的左子结点上,而同一排的结点6挂到了结点5的右子结点上。
对于解码,也是同样的规律,先根据根结点值新建一个空的N叉树结点,由于我们的编码规律,根结点是一定没有右子结点的,所以取出左子结点 cur,并且开始循环,如果 cur 结点存在,那么我们对 cur 递归调用解码函数,将返回的结点加入当前N叉树结点的 children 数组中,然后 cur 再赋值为其右子结点,继续递归调用解码函数,再加入 children 数组,如此便可将二叉树还原为之前的N叉树,参见代码如下:
 ```
class Codec {
public:

    // Encodes an n-ary tree to a binary tree.
    TreeNode* encode(Node* root) {
        if (!root) return NULL;
        TreeNode *res = new TreeNode(root->val);
        if (!root->children.empty()) {
            res->left = encode(root->children[0]);
        }
        TreeNode *cur = res->left;
        for (int i = 1; i < root->children.size(); ++i) {
            cur->right = encode(root->children[i]);
            cur = cur->right;
        }
        return res;
    }

    // Decodes your binary tree to an n-ary tree.
    Node* decode(TreeNode* root) {
        if (!root) return NULL;
        Node *res = new Node(root->val, {});
        TreeNode *cur = root->left;
        while (cur) {
            res->children.push_back(decode(cur));
            cur = cur->right;
        }
        return res;
    }
};

 ```

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值