二叉树系列1.md 15.7 KB
Newer Older
L
labuladong 已提交
1
# 东哥手把手带你刷二叉树(思维篇)
L
labuladong 已提交
2 3 4

<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
5
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
L
labuladong 已提交
6 7 8 9
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
10
![](https://labuladong.github.io/algo/images/souyisou1.png)
L
labuladong 已提交
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.0;点击这里体验 [刷题全家桶](https://labuladong.gitee.io/algo/images/others/%E5%85%A8%E5%AE%B6%E6%A1%B6.jpg)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
L
labuladong 已提交
13 14 15



L
labuladong 已提交
16
读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:
L
labuladong 已提交
17

L
labuladong 已提交
18 19 20 21 22 23
| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [114. Flatten Binary Tree to Linked List](https://leetcode.com/problems/flatten-binary-tree-to-linked-list/) | [114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/) | 🟠
| [116. Populating Next Right Pointers in Each Node](https://leetcode.com/problems/populating-next-right-pointers-in-each-node/) | [116. 填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) | 🟠
| [226. Invert Binary Tree](https://leetcode.com/problems/invert-binary-tree/) | [226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/) | 🟢
| - | [剑指 Offer 27. 二叉树的镜像](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/) | 🟢
L
labuladong 已提交
24 25 26

**-----------**

L
labuladong 已提交
27
> 本文有视频版:[二叉树/递归的框架思维(纲领篇)](https://www.bilibili.com/video/BV1nG411x77H/)
L
labuladong 已提交
28

L
labuladong 已提交
29
PS:[刷题插件](https://mp.weixin.qq.com/s/OE1zPVPj0V2o82N4HtLQbw) 集成了手把手刷二叉树功能,按照公式和套路讲解了 150 道二叉树题目,可手把手带你刷完二叉树分类的题目,迅速掌握递归思维。
L
labuladong 已提交
30

L
labuladong 已提交
31
本文承接 [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结),先复述一下前文总结的二叉树解题总纲:
L
labuladong 已提交
32

L
labuladong 已提交
33 34 35 36 37 38 39 40 41
> 二叉树解题的思维模式分两类:
>
> **1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。
>
> **2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
>
> 无论使用哪种思维模式,你都需要思考:
> 
> **如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
L
labuladong 已提交
42

L
labuladong 已提交
43
本文就以几道比较简单的题目为例,带你实践运用这几条总纲,理解「遍历」的思维和「分解问题」的思维有何区别和联系。
L
labuladong 已提交
44

L
labuladong 已提交
45
### 一、翻转二叉树
L
labuladong 已提交
46

L
labuladong 已提交
47
我们先从简单的题开始,看看力扣第 226 题「翻转二叉树」,输入一个二叉树根节点 `root`,让你把整棵树镜像翻转,比如输入的二叉树如下:
L
labuladong 已提交
48

L
labuladong 已提交
49 50 51 52 53 54
```python
     4
   /   \
  2     7
 / \   / \
1   3 6   9
L
labuladong 已提交
55 56
```

L
labuladong 已提交
57
算法原地翻转二叉树,使得以 `root` 为根的树变成:
L
labuladong 已提交
58

L
labuladong 已提交
59 60 61 62 63 64
```python
     4
   /   \
  7     2
 / \   / \
9   6 3   1
L
labuladong 已提交
65 66
```

L
labuladong 已提交
67
不难发现,只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树。
L
labuladong 已提交
68

L
labuladong 已提交
69
那么现在开始在心中默念二叉树解题总纲:
L
labuladong 已提交
70

L
labuladong 已提交
71
**1、这题能不能用「遍历」的思维模式解决**
L
labuladong 已提交
72

L
labuladong 已提交
73
可以,我写一个 `traverse` 函数遍历每个节点,让每个节点的左右子节点颠倒过来就行了。
L
labuladong 已提交
74

L
labuladong 已提交
75
单独抽出一个节点,需要让它做什么?让它把自己的左右子节点交换一下。
L
labuladong 已提交
76

L
labuladong 已提交
77 78 79
需要在什么时候做?好像前中后序位置都可以。

综上,可以写出如下解法代码:
L
labuladong 已提交
80 81

```java
L
labuladong 已提交
82 83 84 85 86
// 主函数
TreeNode invertTree(TreeNode root) {
    // 遍历二叉树,交换每个节点的子节点
    traverse(root);
    return root;
L
labuladong 已提交
87 88
}

L
labuladong 已提交
89 90 91 92 93
// 二叉树遍历函数
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
L
labuladong 已提交
94

L
labuladong 已提交
95 96 97 98 99
    /**** 前序位置 ****/
    // 每一个节点需要做的事就是交换它的左右子节点
    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;
L
labuladong 已提交
100

L
labuladong 已提交
101 102 103 104 105
    // 遍历框架,去遍历左右子树的节点
    traverse(root.left);
    traverse(root.right);
}
```
L
labuladong 已提交
106

L
labuladong 已提交
107
你把前序位置的代码移到后序位置也可以,但是直接移到中序位置是不行的,需要稍作修改,这应该很容易看出来吧,我就不说了。
L
labuladong 已提交
108

L
labuladong 已提交
109
按理说,这道题已经解决了,不过为了对比,我们再继续思考下去。
L
labuladong 已提交
110

L
labuladong 已提交
111
**2、这题能不能用「分解问题」的思维模式解决**
L
labuladong 已提交
112

L
labuladong 已提交
113
我们尝试给 `invertTree` 函数赋予一个定义:
L
labuladong 已提交
114

L
labuladong 已提交
115 116 117
```java
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root);
L
labuladong 已提交
118 119
```

L
labuladong 已提交
120
然后思考,对于某一个二叉树节点 `x` 执行 `invertTree(x)`,你能利用这个递归函数的定义做点啥?
L
labuladong 已提交
121

L
labuladong 已提交
122
我可以用 `invertTree(x.left)` 先把 `x` 的左子树翻转,再用 `invertTree(x.right)``x` 的右子树翻转,最后把 `x` 的左右子树交换,这恰好完成了以 `x` 为根的整棵二叉树的翻转,即完成了 `invertTree(x)` 的定义。
L
labuladong 已提交
123

L
labuladong 已提交
124
直接写出解法代码:
L
labuladong 已提交
125 126

```java
L
labuladong 已提交
127
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
L
labuladong 已提交
128 129 130 131
TreeNode invertTree(TreeNode root) {
    if (root == null) {
        return null;
    }
L
labuladong 已提交
132 133 134
    // 利用函数定义,先翻转左右子树
    TreeNode left = invertTree(root.left);
    TreeNode right = invertTree(root.right);
L
labuladong 已提交
135

L
labuladong 已提交
136 137 138
    // 然后交换左右子节点
    root.left = right;
    root.right = left;
L
labuladong 已提交
139

L
labuladong 已提交
140
    // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
L
labuladong 已提交
141 142 143 144
    return root;
}
```

L
labuladong 已提交
145
这种「分解问题」的思路,核心在于你要给递归函数一个合适的定义,然后用函数的定义来解释你的代码;如果你的逻辑成功自恰,那么说明你这个算法是正确的。
L
labuladong 已提交
146

L
labuladong 已提交
147
好了,这道题就分析到这,「遍历」和「分解问题」的思路都可以解决,看下一道题。
L
labuladong 已提交
148

L
labuladong 已提交
149
### 第二题、填充节点的右侧指针
L
labuladong 已提交
150

L
labuladong 已提交
151
这是力扣第 116 题「填充每个二叉树节点的右侧指针」,看下题目:
L
labuladong 已提交
152

L
labuladong 已提交
153
![](https://labuladong.github.io/algo/images/二叉树系列/title1.png)
L
labuladong 已提交
154

L
labuladong 已提交
155
函数签名如下:
L
labuladong 已提交
156 157 158 159 160 161 162

```java
Node connect(Node root);
```

题目的意思就是把二叉树的每一层节点都用 `next` 指针连接起来:

L
labuladong 已提交
163
![](https://labuladong.github.io/algo/images/二叉树系列/1.png)
L
labuladong 已提交
164 165 166

而且题目说了,输入是一棵「完美二叉树」,形象地说整棵二叉树是一个正三角形,除了最右侧的节点 `next` 指针会指向 `null`,其他节点的右侧一定有相邻的节点。

L
labuladong 已提交
167 168 169
这道题怎么做呢?来默念二叉树解题总纲:

**1、这题能不能用「遍历」的思维模式解决**
L
labuladong 已提交
170

L
labuladong 已提交
171 172 173 174 175
很显然,一定可以。

每个节点要做的事也很简单,把自己的 `next` 指针指向右侧节点就行了。

也许你会模仿上一道题,直接写出如下代码:
L
labuladong 已提交
176 177

```java
L
labuladong 已提交
178 179
// 二叉树遍历函数
void traverse(Node root) {
L
labuladong 已提交
180
    if (root == null || root.left == null) {
L
labuladong 已提交
181
        return;
L
labuladong 已提交
182
    }
L
labuladong 已提交
183
    // 把左子节点的 next 指针指向右子节点
L
labuladong 已提交
184 185
    root.left.next = root.right;

L
labuladong 已提交
186 187
    traverse(root.left);
    traverse(root.right);
L
labuladong 已提交
188 189 190
}
```

L
labuladong 已提交
191
但是,这段代码其实有很大问题,因为它只能把相同父节点的两个节点穿起来,再看看这张图:
L
labuladong 已提交
192

L
labuladong 已提交
193
![](https://labuladong.github.io/algo/images/二叉树系列/1.png)
L
labuladong 已提交
194

L
labuladong 已提交
195
节点 5 和节点 6 不属于同一个父节点,那么按照这段代码的逻辑,它俩就没办法被穿起来,这是不符合题意的,但是问题出在哪里?
L
labuladong 已提交
196

L
labuladong 已提交
197
**传统的 `traverse` 函数是遍历二叉树的所有节点,但现在我们想遍历的其实是两个相邻节点之间的「空隙」**
L
labuladong 已提交
198

L
labuladong 已提交
199 200 201 202 203 204 205
所以我们可以在二叉树的基础上进行抽象,你把图中的每一个方框看做一个节点:

![](https://labuladong.github.io/algo/images/二叉树系列/3.png)

**这样,一棵二叉树被抽象成了一棵三叉树,三叉树上的每个节点就是原先二叉树的两个相邻节点**

现在,我们只要实现一个 `traverse` 函数来遍历这棵三叉树,每个「三叉树节点」需要做的事就是把自己内部的两个二叉树节点穿起来:
L
labuladong 已提交
206 207 208 209 210

```java
// 主函数
Node connect(Node root) {
    if (root == null) return null;
L
labuladong 已提交
211 212
    // 遍历「三叉树」,连接相邻节点
    traverse(root.left, root.right);
L
labuladong 已提交
213 214 215
    return root;
}

L
labuladong 已提交
216 217
// 三叉树遍历框架
void traverse(Node node1, Node node2) {
L
labuladong 已提交
218 219 220
    if (node1 == null || node2 == null) {
        return;
    }
L
labuladong 已提交
221 222
    /**** 前序位置 ****/
    // 将传入的两个节点穿起来
L
labuladong 已提交
223 224 225
    node1.next = node2;
    
    // 连接相同父节点的两个子节点
L
labuladong 已提交
226 227
    traverse(node1.left, node1.right);
    traverse(node2.left, node2.right);
L
labuladong 已提交
228
    // 连接跨越父节点的两个子节点
L
labuladong 已提交
229
    traverse(node1.right, node2.left);
L
labuladong 已提交
230 231 232
}
```

L
labuladong 已提交
233 234 235
这样,`traverse` 函数遍历整棵「三叉树」,将所有相邻节的二叉树节点都连接起来,也就避免了我们之前出现的问题,把这道题完美解决。

**2、这题能不能用「分解问题」的思维模式解决**
L
labuladong 已提交
236

L
labuladong 已提交
237
嗯,好像没有什么特别好的思路,所以这道题无法使用「分解问题」的思维来解决。
L
labuladong 已提交
238

L
labuladong 已提交
239
### 第三题、将二叉树展开为链表
L
labuladong 已提交
240

L
labuladong 已提交
241 242 243
这是力扣第 114 题「将二叉树展开为链表」,看下题目:

![](https://labuladong.github.io/algo/images/二叉树系列/title2.png)
L
labuladong 已提交
244 245 246 247 248 249 250

函数签名如下:

```java
void flatten(TreeNode root);
```

L
labuladong 已提交
251
**1、这题能不能用「遍历」的思维模式解决**
L
labuladong 已提交
252

L
labuladong 已提交
253
乍一看感觉是可以的:对整棵树进行前序遍历,一边遍历一边构造出一条「链表」就行了:
L
labuladong 已提交
254

L
labuladong 已提交
255 256 257 258 259
```java
// 虚拟头节点,dummy.right 就是结果
TreeNode dummy = new TreeNode(-1);
// 用来构建链表的指针
TreeNode p = dummy;
L
labuladong 已提交
260

L
labuladong 已提交
261 262 263 264 265 266 267
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序位置
    p.right = new TreeNode(root.val);
    p = p.right;
L
labuladong 已提交
268

L
labuladong 已提交
269 270 271 272
    traverse(root.left);
    traverse(root.right);
}
```
L
labuladong 已提交
273

L
labuladong 已提交
274
但是注意 `flatten` 函数的签名,返回类型为 `void`,也就是说题目希望我们在原地把二叉树拉平成链表。
L
labuladong 已提交
275

L
labuladong 已提交
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
这样一来,没办法通过简单的二叉树遍历来解决这道题了。

**2、这题能不能用「分解问题」的思维模式解决**

我们尝试给出 `flatten` 函数的定义:

```java
// 定义:输入节点 root,然后 root 为根的二叉树就会被拉平为一条链表
void flatten(TreeNode root);
```

有了这个函数定义,如何按题目要求把一棵树拉平成一条链表?

对于一个节点 `x`,可以执行以下流程:

1、先利用 `flatten(x.left)``flatten(x.right)``x` 的左右子树拉平。

2、将 `x` 的右子树接到左子树下方,然后将整个左子树作为右子树。

![](https://labuladong.github.io/algo/images/二叉树系列/2.jpeg)

这样,以 `x` 为根的整棵二叉树就被拉平了,恰好完成了 `flatten(x)` 的定义。

直接看代码实现:
L
labuladong 已提交
300 301 302 303 304 305 306

```java
// 定义:将以 root 为根的树拉平为链表
void flatten(TreeNode root) {
    // base case
    if (root == null) return;
    
L
labuladong 已提交
307
    // 利用定义,把左右子树拉平
L
labuladong 已提交
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
    flatten(root.left);
    flatten(root.right);

    /**** 后序遍历位置 ****/
    // 1、左右子树已经被拉平成一条链表
    TreeNode left = root.left;
    TreeNode right = root.right;
    
    // 2、将左子树作为右子树
    root.left = null;
    root.right = left;

    // 3、将原先的右子树接到当前右子树的末端
    TreeNode p = root;
    while (p.right != null) {
        p = p.right;
    }
    p.right = right;
}
```

L
labuladong 已提交
329
你看,这就是递归的魅力,你说 `flatten` 函数是怎么把左右子树拉平的?
L
labuladong 已提交
330

L
labuladong 已提交
331
不容易说清楚,但是只要知道 `flatten` 的定义如此并利用这个定义,让每一个节点做它该做的事情,然后 `flatten` 函数就会按照定义工作。
L
labuladong 已提交
332

L
labuladong 已提交
333
至此,这道题也解决了,我们前文 [k个一组翻转链表](https://labuladong.github.io/article/fname.html?fname=k个一组反转链表) 的递归思路和本题也有一些类似。
L
labuladong 已提交
334

L
labuladong 已提交
335
最后,首尾呼应,再次默写二叉树解题总纲。
L
labuladong 已提交
336

L
labuladong 已提交
337
二叉树解题的思维模式分两类:
L
labuladong 已提交
338

L
labuladong 已提交
339
**1、是否可以通过遍历一遍二叉树得到答案**?如果可以,用一个 `traverse` 函数配合外部变量来实现,这叫「遍历」的思维模式。
L
labuladong 已提交
340

L
labuladong 已提交
341
**2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案**?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
L
labuladong 已提交
342

L
labuladong 已提交
343
无论使用哪种思维模式,你都需要思考:
L
labuladong 已提交
344

L
labuladong 已提交
345
**如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做**?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
L
labuladong 已提交
346

L
labuladong 已提交
347
希望你能仔细体会,并运用到所有二叉树题目上。
L
labuladong 已提交
348

L
labuladong 已提交
349
接下来可阅读:
L
labuladong 已提交
350

L
labuladong 已提交
351 352
* [手把手刷二叉树(第二期)](https://labuladong.github.io/article/fname.html?fname=二叉树系列2)
* [手把手刷二叉树(第三期)](https://labuladong.github.io/article/fname.html?fname=二叉树系列3)
L
labuladong 已提交
353 354


L
labuladong 已提交
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390

<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [东哥带你刷二叉搜索树(构造篇)](https://labuladong.github.io/article/fname.html?fname=BST3)
 - [东哥带你刷二叉搜索树(特性篇)](https://labuladong.github.io/article/fname.html?fname=BST1)
 - [东哥带你刷二叉树(构造篇)](https://labuladong.github.io/article/fname.html?fname=二叉树系列2)
 - [两种思路解决单词拼接问题](https://labuladong.github.io/article/fname.html?fname=单词拼接)
 - [二叉树的递归转迭代的代码框架](https://labuladong.github.io/article/fname.html?fname=迭代遍历二叉树)
 - [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
 - [后序遍历的妙用](https://labuladong.github.io/article/fname.html?fname=后序遍历)
 - [归并排序详解及应用](https://labuladong.github.io/article/fname.html?fname=归并排序)

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
| - | [剑指 Offer 26. 树的子结构](https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/?show=1) |
| - | [剑指 Offer 27. 二叉树的镜像](https://leetcode.cn/problems/er-cha-shu-de-jing-xiang-lcof/?show=1) |

</details>



**_____________**

L
labuladong 已提交
391
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
392 393

![](https://labuladong.github.io/algo/images/souyisou2.png)
L
labuladong 已提交
394 395 396


======其他语言代码======