二叉树是一种数据结构，并且拥有种类复杂的分支，本文作为入门篇，只介绍一些基本二叉树的题型，像二叉搜索树等等不在此篇介绍。

二叉树其实是链表的升级版，即链表同时拥有两个 Next 指针，就变成了二叉树。

二叉树可以根据一些特性，比如搜索二叉树，将查找的时间复杂度降低为 logn，而且堆这种数据结构，也是一种特殊的二叉树，可以以 O(1) 的时间复杂度查找最大值或者最小值。所以二叉树的变种很多，都可以很好的解决具体场景的问题。

## 精读

要入门二叉树，就必须理解二叉树的三种遍历策略，分别是：前序遍历、中序遍历、后序遍历，这些都属于深度优先遍历。

所谓前中后，就是访问节点值在什么时机，其余时机按先左后右访问子节点。比如前序遍历，就是先访问值，再访问左右；后续遍历就是先访问左右，再访问值；中序遍历就是左，值，右。

用递归方式遍历树非常简单：

```typescript
function visitTree(node: TreeNode) {
  // 三选一：前序遍历
  // console.log(node.val)
  visitTree(node.left)
  // 三选一：中序遍历
  // console.log(node.val)
  visitTree(node.right)
  // 三选一：后序遍历
  // console.log(node.val)
}
```

当然题目需要我们巧妙利用二叉树三种遍历的特性来解题，比如重建二叉树。

### 重建二叉树

重建二叉树是一道中等题，题目如下：

> 输入某二叉树的前序遍历和中序遍历的结果，请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
>
> 例如
>
> 前序遍历 preorder = `[3,9,20,15,7]`
> 
> 中序遍历 inorder = `[9,3,15,20,7]`

先给你二叉树前序与中序遍历结果，让你重建二叉树，这种逆向思维的题目就难了不少。

仔细观察遍历特性可以看出，我们也许能推测出一些关键节点的位置，再通过数组切割递归一下就能解题。

前序遍历第一个访问的一定是根节点，因此 `3` 一定是根节点，然后我们在中序遍历找到 `3`，这样 **左边就是所有左子树的中序遍历结果，右边就是所有右子树的中序遍历结果**，我们只要再找到 **左子树的前序遍历结果与右子树的前序遍历结果**，就可以递归了，终止条件是左或右子树只有一个值，那样就代表叶子节点。

那么怎么找左右子树的前序遍历呢？上面例子中，我们找到了 `3` 的左右子树的中序遍历结果，由于前序遍历优先访问左子树，因此我们数一下中序遍历中，`3` 左边的数量，只有一个 `9`，那么我们从前序遍历的 `3,9,20,15,7` 在 `3` 之后推一位，那么 `9` 就是左子树前序遍历结果，`9` 后面的 `20,15,7` 就是右子树的前序遍历结果。

最后只要递归一下就能解题了，我们将输入不断拆解为左右子树的的输入，直到达到终止条件。

解决此题的关键是，不仅要知道如何写前中后序遍历，还要知道前序遍历第一个节点是根节点，后序遍历最后一个节点是根节点，中序遍历以根节点为中心，左右分别是其左右子树，这几个重要延伸特征。

说完了反向，我们说正向，即递归一棵二叉树。

其实二叉树除了递归，还有一种常见的遍历方法是利用栈进行广度优先遍历，典型题目有从上到下打印二叉树。

### 从上到下打印二叉树

从上到下打印二叉树是一道简单题，题目如下：

> 从上到下按层打印二叉树，同一层的节点按从左到右的顺序打印，每一层打印到一行。

这道题要求从左到右顺序打印，完全遵循广度优先遍历，我们可以在二叉树递归时，先不要急着读取值，而是按照左、中、右，遇到左右子树节点，就推入栈的末尾，利用 `while` 语句不断循环，直到栈空为止。

利用展开时追加到栈尾，并不断循环处理栈元素的方式非常优雅，而且符合栈的特性。

当然如果题目要求倒序打印，你就可以以 右、中、左 的顺序进行处理。

接下来看看深度优先遍历，典型题目是二叉树的深度。

### 二叉树的深度

二叉树的深度是一道简单题，题目如下：

> 输入一棵二叉树的根节点，求该树的深度。从根节点到叶节点依次经过的节点（含根、叶节点）形成树的一条路径，最长路径的长度为树的深度。

由于二叉树有多种分支，在遍历前，我们并不知道哪条路线是最深的，所以必须利用递归尝试。

我们可以转换一下思路，用函数式语义方式来理解。假设我们有了这样一个函数 `deep` 来求二叉树深度，那么这个函数内容是什么呢？二叉树只可能存在左右子树，所以 `deep` 必然是左右子树的最大深度的最大值 +1（它自己）。

而求左右子树深度可以复用 `deep` 函数形成递归，我们只需要考虑边界情况，即访问节点不存在时，返回深度 `0` 即可，因此代码如下：

```typescript
function deep(node: TreeNode) {
  if (!node) return 0
  return Math.max(deep(node.left), deep(node.right)) + 1
}
```

从这可以看出，二叉树一般能用比较优雅的递归函数解决，如果你的解题思路不包含递归，往往就不是最优雅的解法。

类似优雅的题目还有，平衡二叉树。

### 平衡二叉树

平衡二叉树是一道简单题，题目如下：

> 输入一棵二叉树的根节点，判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过 1，那么它就是一棵平衡二叉树。

同理，我们设函数 `isBalance` 就是答案函数，那么一个平衡二叉树的特征，必然是其左右子树也是平衡的，所以可以写成：

```typescript
function isBalance(node: TreeNode) {
  if (root == null) return true
  return isBalance(node.left) && isBalance(node.right)
}
```

但是哪里不对，左右子树平衡还不够啊，万一左右子树之间深度相差超过 1 就坏了，所以还要求一下左右子树的深度，我们复用上题的函数 `deep`，整理一下如下：

```typescript
function isBalance(node: TreeNode) {
  if (root == null) return true
  return isBalance(root.left) && isBalance(root.right) &&
    Math.abs(deep(root.left) - deep(root.right)) < 2
}
```

这道题提醒我们，不是所有递归都能完美写成仅自己调用自己的模式，不同题目要辅以其他函数，要敏锐的察觉到还缺少哪些条件。

还有一种递归，不是简单的函数自身递归自身，而是要构造出另一个函数进行递归，原因是递归参数不同。典型的题目有对称的二叉树。

### 对称的二叉树

对称的二叉树是一道简单题，题目如下：

> 请实现一个函数，用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样，那么它是对称的。

我们要注意，一颗二叉树的镜像比较特殊，比如最左节点与最右节点互为镜像，但它们的父节点并不相同，因此 `isSymmetric(tree)` 这样的参数是无法子递归的，我们必须拆解为左右子树作为参数，让它们进行相等判断，在传参时，将父级不同，但互为镜像的左右节点传入即可。

所以我们必须起一个新函数 `isSymmetricNew(left, right)`，将 `left.left` 与 `right.right` 对比，将 `left.right` 与 `right.left` 对比即可。

具体代码就不写了，然后注意一下边界情况即可。

这道题的重点是，由于镜像的关系，并不拥有相同的父节点，因此必须用一个新参数的函数进行递归。

那如果这道题反过来呢？要求构造一个二叉树镜像呢？

### 二叉树的镜像

二叉树的镜像是一道简单题，题目如下：

> 请完成一个函数，输入一个二叉树，该函数输出它的镜像。

判断镜像比较容易，但构造镜像就要想一想了：

```text
例如输入：
     4
   /   \
  2     7
 / \   / \
1   3 6   9

镜像输出：
     4
   /   \
  7     2
 / \   / \
9   6 3   1
```

观察发现，其实镜像可以理解为左右子树互换，同时 **其各子树的左右子树再递归互换**，这就构成了一个递归：

```typescript
function mirrorTree(node: TreeNode) {
  if (node === null) return null

  const left = mirrorTree(node.left)
  const right = mirrorTree(node.right)
  node.left = right
  node.right = left
  return node
}
```

我们要从下到上，因此先生成递归好的左右子树，再进行当前节点的互换，最后返回根节点即可。

接下来介绍一些有一定难度的经典题。

### 二叉树的最近公共祖先

二叉树的最近公共祖先是一道中等题，题目如下：

> 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

题目很简短，也很明确，就是寻找最近的公共祖先。显然，根节点是所有节点的公共祖先，但不一定是最近的。

我们还是用递归，先考虑特殊情况：如果任意节点等于当前节点，那么当前节点一定就是最近公共祖先，因为另一个节点一定在其子节点中。

然后，利用递归思想思考，假设我们利用 `lowestCommonAncestor` 函数分别找到左右子节点的最近公共祖先会怎样？

```typescript
function lowestCommonAncestor(node, a, b) {
  const left = lowestCommonAncestor(node.left)
  const right = lowestCommonAncestor(node.right)
}
```

如果左右节点都找不到，说明只可能当前节点是最近公共子节点：

```typescript
if (!left && !right) return node
```

如果左节点找不到，则右节点就是答案，否则相反：

```typescript
if (!left) return right
return left
```

这里巧妙利用了函数语义进行结果判断。

### 二叉树的右视图

二叉树的右视图是一道中等题，题目如下：

> 给定一棵二叉树，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。

想象一束光照，从二叉树右侧向左照射，自上而下读取即是答案。

其实这道题可以认为是一道融合题。右侧的光束可以认为是分层照射的，那么当我们用广度优先算法遍历时，对于每一层，都找到最后一个节点打印，并且按顺序打印就是最终答案。

有一道二叉树的题目，是根据树的深度，按照广度优先遍历打印成二维数组，记录树的深度其实也有巧妙办法，即在栈尾追加元素时，增加一个深度 key，那么访问时自然就可以读到深度值。

### 完全二叉树的节点个数

完全二叉树的节点个数是一道中等题，题目如下：

> 给你一棵 **完全二叉树** 的根节点 `root` ，求出该树的节点个数。
>
> **完全二叉树** 的定义如下：在完全二叉树中，除了最底层节点可能没填满外，其余每层节点数都达到最大值，并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 `h` 层，则该层包含 `1 ~ 2^h` 个节点。

用递归解决这道题的话，关键要分几种情况探讨完全二叉树。

由于最底层可能没有填满，但最底层一定有节点，而且是按照从左到右填的，那么递归遍历左节点就可以获取树的最大深度，通过最大深度我们可以快速计算出节点个树，前提是二叉树必须是满的。

但最底层节点可能不满，那怎么办呢？分情况即可，首先，如果一直按照 `node.right....right` 递归获得右侧节点深度，发现和最大深度相同，那么就是一个满二叉树，直接计算出结果即可。

我们再看 `node.right...left` 的深度如果等于最大深度，说明 `node.left` 也就是左子树是个满二叉树，可以通过数学公式 `2^n-1` 快速算出节点个树。

如果不等于最大深度呢？**则说明右子树深度减 1 是满二叉树**，也可以通过数学公式快速计算节点个数，再通过递归计算另一边即可。

## 总结

从题目中可以感受到，二叉树的解题魅力在于递归，二叉树问题中，我们可以同时追求优雅与答案。

> 讨论地址是：[精读《算法 - 二叉树》· Issue #331 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/331)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
