最近刷了一波二叉树的专题,这篇文章是对于最近刷题的一些总结。本文依据 leetcode
的规则,以根结点所在层为第一层,维基百科上根结点的深度为0,也就是第0层。
二叉树的分类
普通二叉树
最多有两棵子树的树被称为二叉树。
满二叉树
定义:二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上。
特征:如果该二叉树有k层,则一共有(2^k - 1)个结点。
如下图所示:
完全二叉树
在完全二叉树中,除了最底层结点可能没填满外,其余每层结点数都填满(除了最底层,第n层有2^(n - 1)个结点,根结点为第一层),并且最下面一层的结点都集中在该层最左边的若干位置。
特征:
如果把满二叉树从右至左、从下往上删除一些节点,剩余的结构就构成完全二叉树。
若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。
完全二叉树的分辨如下图所示:
二叉搜索(查找)树
二叉搜索树是一个有序树,每个结点是带有数据的。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 左、右子树也分别为二叉排序树
- 没有相等的结点(一般二叉搜索树在构造时,进行比较如果相等就不会进行插入)
二叉搜索树如下图所示
二叉树的遍历方式(重点)
二叉树主要有两种遍历方式:
深度优先遍历:从根结点开始,先往下走(一般走左孩子),遇到叶子节点再往回走。
一般有前序遍历、中序遍历、后序遍历。
广度优先遍历:一般是从上到下,从左到右,一层一层地遍历。
最基本的层次遍历。
上面这几种遍历方式是最常见的遍历方式,使用起来并不麻烦,但是重点是如何理解这些遍历的本质,在此记录以下我刷题过后对二叉树的一些理解。
对遍历方式最基本的记忆或者理解方式:
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
这里最主要是对根、左、右这三个结点的理解,假如我们通过一个指针对这棵树进行遍历,这里的根结点、左孩子结点、右孩子结点不是指针第一次经过该结点的顺序,而是在以根、左、右这三个结点为整体的一个区域内,对结点执行操作的次序,并且操作时要进行局部区域的处理(理解)。
以下面这棵二叉树来分析理解:
对于一棵树,别人给你的百分之99都是根结点,因此我们每次对二叉树的处理出发点都是从根结点,很明显除了前序遍历,其他都不是以根开头的顺序,因此这也说明了,这个顺序不是经过顺序,而是执行操作的顺序。
要点:
操作根结点 局部区域 移动方向向上
操作左结点 局部区域 移动方向为左下(左结点为新区域的根结点)
操作右结点 局部区域 移动方向为右下(右结点为新区域的根结点)
下面来进行详细分析,首先是前序遍历(根左右):
前序遍历:5 -> 4 -> 1 -> 2 -> 6 -> 7 -> 8
步骤分析:
①首先从根结点出发,从局部看,此时根结点为5,左结点为4,右节点为6。要操作的是5,5往上进行局部区域移动,由于已经是整棵树的根结点,因此无法移动,直接执行操作处理。
结果:5
指针经过结点: 5
②还是对于该局部区域,左结点为4,往左下方进行区域移动,得到下图新区域
对于新的区域,4为根结点,1为左结点,2为右结点。对根结点操作(记住前序操作宗旨:根左右),因此对4进行操作,区域向上移,但是由于这个操作涉及回溯(回溯这个概念还没理解透彻,学会了再总结这个概念),当该区域还有未处理结点时,不进行回溯,因此也不用移动。所以此时对4进行操作。
结果: 5 -> 4
指针经过结点: 5 -> 4
③还是对于该区域,接下来操作左结点,向左下方移动,此时已经到达第部,因此直接处理结点1。
结果: 5 -> 4 -> 1
指针经过结点: 5 -> 4 -> 1
③接下来操作右节点,向右下方移动,此时已经到达第部,因此直接处理结点2。
结果: 5 -> 4 -> 1 -> 2
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2
处理完了该区域,区域回溯,回到下图所示。
④此时5为根结点,4为左结点,都已经处理过了,因此处理右结点。
结果: 5 -> 4 -> 1 -> 2 -> 6
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6
⑤往右下方移动区域,如下图所示,此时根结点为6,左结点为7,右结点为8,同理,通过根左右的顺序,可以直接得到答案。
结果: 5 -> 4 -> 1 -> 2 -> 6 -> 7 -> 8
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8
由于前序遍历是从优先处理根操作,我们又是从根结点出发,因此可能不好理解操作这个概念。
下面来分析中序遍历(左根右):
中序遍历:1 -> 4 -> 2 -> 5 -> 7 -> 6 -> 8
①题目一般给的出发点是root,因此从局部看如下图所示,此时根结点为5,左结点为4,右节点为6。
中序遍历要处理左结点,向左下方进行移动,得到如下图所示区域。
可以看到此时4为根结点,1为左结点,2为右结点,再向左下方移动,就只剩1了,因此第一个操作的结点就为1。
结果:1
指针经过结点: 5 -> 4 -> 1
②进行回溯,得到区域如下图,左结点已经处理过了,因此处理根结点,往上移动涉及回溯,因此直接执行操作结点4。
结果:1 -> 4
指针经过结点: 5 -> 4 -> 1 -> 4
③根据上图所示,接下来应该操作右结点2。
结果:1 -> 4 -> 2
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2
④此时这边全部操作完,进行回溯,回溯到如下图所示区域。
根据左根右的顺序,此时处理根结点5(根结点已经无法往上移动),直接操作。
结果:1 -> 4 -> 2 -> 5
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5
⑤接下来对应上图的右结点6,区域可以往右下移动,因此得到的新区域如下图所示。
此时根结点为6,左结点为7,右结点为8。处理左结点7,往右下移动,剩下7,直接操作。
结果:1 -> 4 -> 2 -> 5 -> 7
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7
⑥回溯一步,到达下图所示区域,左结点已经处理过了,处理根结点,根结点往上移动会回溯,因此不移动,直接操作。
⑦最后一步,此时显然是处理右结点8。
结果:1 -> 4 -> 2 -> 5 -> 7 -> 6 -> 8
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8
最后来分析后序遍历(左右根):
后序遍历:1 -> 2 -> 4 -> 7 -> 8 -> 6 -> 5
①每次开始都是一样从root出发,此时根结点为5,左结点为4,右节点为6。
前面的区域移动和中序遍历一样(因为都是先处理左结点),因此不过多赘述,得到以下区域和结果。
结果:1
指针经过结点: 5 -> 4 -> 1
②操作完结点1后回溯到上图所示区域(操作结点1时区域就只有1这个结点,见中序遍历图),此时根据左右中,处理结点2,向右下移动,然后只剩一个,直接处理。
结果:1 -> 2
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2
③同理,再回溯到①中的图,操作根结点,往上移动会回溯,直接操作。
结果:1 -> 2 -> 4
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4
④再次回溯,然后如下图所示。左结点处理过了,操作右结点6,向右下方区域移动。
得到新的区域如下图所示,6为根结点,7为左结点,8为右结点。
根据左右根的次序,再向左下区域移动,就可以执行操作7(此时5,6都没被操作,但是很明显指针已经经过了这两个结点)。
结果:1 -> 2 -> 4 -> 7
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7
⑤操作完之后回溯到上图区域,根据左右根,将要操作8,因此向右下方移动,只剩一个结点直接操作。
结果:1 -> 2 -> 4 -> 7 -> 8
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8
⑥操作完会回溯到步骤④的区域,此时只剩下根结点操作,向上移动涉及回溯,因此直接操作。
结果:1 -> 2 -> 4 -> 7 -> 8 -> 6
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8 -> 6
⑦该区域全部操作完,可以进行回溯,回到如下图所示区域,左右结点已经操作过了,此时操作根结点。
结果:1 -> 2 -> 4 -> 7 -> 8 -> 6 -> 5
指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8 -> 6 -> 5
三种遍历方式的结果对比:
前序遍历结果:5 -> 4 -> 1 -> 2 -> 6 -> 7 -> 8
前序遍历指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8
中序遍历结果:1 -> 4 -> 2 -> 5 -> 7 -> 6 -> 8
前序遍历指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8
后序遍历结果:1 -> 2 -> 4 -> 7 -> 8 -> 6 -> 5
前序遍历指针经过结点: 5 -> 4 -> 1 -> 4 -> 2 -> 4 -> 5 -> 6 -> 7 -> 6 -> 8 -> 6 -> 5
**特点:**前序遍历操作的第一个结点是根结点;中序遍历的操作,通过根结点可以明显划分左右子树的数据;后序遍历操作的最后一个结点是根结点。
总结:在练习二叉树的题目时,注意入口永远是根结点root,各个遍历的结果不是经过的顺序,而是操作的顺序,对于根、左、右这三个字眼,最好理解为根结点,左子树,右子树。由于根结点没有延伸,所以前序遍历不容易理解操作的意思,当我们操作左子树时,左结点又可以作为新树的根结点,左子树又可以分为左子树的左子树和左子树的右子树,要不断划分到最根源进行操作,这个操作有点像动态规划划分子问题时的思想。
注:本文图片摘自Carl哥代码随想录!!!