其他分享
首页 > 其他分享> > 二叉树总结点睛

二叉树总结点睛

作者:互联网

一、二叉树

1、常见名词

DFS   (depth first search )  	深度优先遍历

BFS (breadth first search ) 	广度优先遍历

BST  (binary search tree)    二叉搜索树

AVL(Adelson-Velsky and Landis)	平衡二叉搜索树

heap  堆

2、常见分类

3、常见应用

​ C++ 中 map、set、multimap、multiset的底层实现都是平衡二叉搜索树,所以他们的增删操作的时间复杂度都是 logN。

​ (unordered_set 和 unordered_map)的底层实现是哈希表,增删操作的时间复杂度为 1。

4、二叉树存储方式

// 链式存储的二叉树的节点定义方式
struct TreeNode
{
    int val ;
    TreeNode* leftChild ;
    TreeNode* rightChild ;
    **// 注意初值列是用小括号幅值,而不是等于号哦!!!!**
    TreeNode (int x) : val (x) , left (NULL), right (NULL)  {}
}

5、二叉树的遍历(与图论中的最基本的两种遍历方式相同)

6、遍历方式的实现

栈的应用: 其实就是一种实现递归的结构,因此前中后序遍历的逻辑可以借助栈,来实现 非递归 的方式。

队列的应用: 层序遍历的实现一般是使用队列,因为队列先进先出的特点,巧好是实现层次遍历所需要的。

递归在本质上就是一种栈的结构,如果可以用栈来实现,自然而然就想到可以用递归来实现。
递归代码虽然看起来简洁,但是当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。

因此用栈基于循环实现的代码的鲁棒性要好一些。

1、递归法
2、迭代法

7、用迭代法遍历的代码实现,有两种

* 1)第一种迭代实现

处理前序遍历和后序遍历比较相似,也比较直观,但是中序遍历就跟这两种的思路不同,由于它遍历和访问的顺序不同导致的结果

在用迭代法遍历树的时候,其实是有两个操作

1)处理节点:即将元素放进 result 数组中,处理的过程即出栈

2)访问节点:即遍历节点,访问的过程即压栈

对于前序遍历,其顺序是 中左右,先访问的元素是中间节点,先处理的元素也是中间节点,由于访问元素和处理元素的顺序是一致的,都是中间节点,所以可以比较便捷

对于中序遍历,其顺序是左中右,先访问的是根节点,然后一层一层向下访问,直到达到树左面的最底部,在开始处理节点(也就是在把节点的数组都放进 result 数组中),这造成了处理顺序和访问顺序是不一致的。 因此在使用迭代法写中序遍历时,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

对与后序遍历,其顺序为左右中,是根据前序遍历改成的。

——>先序遍历:中左右——>调整左右顺序:中右左——>反转数组——>左右中

* 2)第二种迭代实现 / 标记法

​ 1、为什么要有标记法呢?

​ 因为第一种迭代法中,前中后 三种迭代遍历 的实现代码的风格不那么统一,中序和前后序处理的思路完全不同。

​ 2、那么怎么解决中序遍历中,访问节点和处理节点不一致的情况呢?

​ 将访问的节点放入栈中,把要处理的节点也放入栈中,但是要做标记。

​ 标记的方法就是要处理的节点放入栈之后,紧接着放入一个空指针,作为标记

​ 3、根据中序遍历,就可以用标记法统一来处理 前中后序的遍历了

下图为前序遍历:注意因为是用的栈来存储数据,因此实际压栈的顺序为 右、左、中

image-20210427234618073

8、层序遍历法的实现

​ 层序遍历需要借助一个 辅助的数据结构 即队列,来实现。

​ 层序遍历方式与图论中的广度优先遍历一样,这里只是应用在二叉树上。

二、递归三部曲

递归的实现其实就是:每一次递归调用都会把 函数的局部变量、参数值、返回地址等压入栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因

0、先介绍一些技巧:

1、关于递归函数是否需要返回值呢?

​ 如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!

​ 如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。

2、栈如何来模拟回溯呢?

​ 想要实现回溯,就需要记录两个遍历,一个是节点,一个是头结点到该节点的路径数值的总合。

​ 可以用一个 pair<TreeNode*, int> 即 : pair<节点指针,路径数值>

一个pair 为栈中的一个元素。

3、什么时候递归函数前面加if,什么时候不加if

一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。

关于回溯,什么情况下需要回溯呢?

​ 好像是需要,用到一个与节点对应的值时,这个一般也可理解为,递归函数的参数有两个,一个是节点,另一个是与别的对应的变量。因为这个变量是需要参与计算的,所以才需要回溯

4、二叉树题目选择什么遍历顺序?
* 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定是前序,都是先构造中节点
* 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算
* 求二叉搜索树的属性,一定是中序,要不白瞎了有序性

对于求普通二叉树的属性问题,有时候也会用前序,比如在求深度,和找所有路径时,这样是为了方便让父节点指向子节点。所以求普通二叉树的属性还是要具体问题具体分析。

1、确定递归函数的参数和返回值

确定哪些参数是递归过程中需要处理的,那么就在递归函数里加上这个参数,

并且还要明确每次递归的返回值是什么,进而确定递归函数的返回类型

2、确定终止条件

写完递归算法在运行时,经常会遇到栈溢出的错误,就是没写终止条件,或者终止条件写的不对,

操作系统也是一个栈的结构来保存每一层递归的信息,

如果没有终止,操作系统的内存栈必然就会溢出。

3、确定单层递归的逻辑

确定每一层递归需要处理的信息,

在这里也就会重复调用自己来实现递归的过程。

4、时间复杂度的分析

递归算法的时间复杂度 = 递归的次数 * 每次递归中的操作次数

三、平衡二叉树

1、定义:

​ 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

图片

​ 由图可以知道:

	* 当求高度的时候,只能从下往上去查,所以只能是**后序遍历**( 左 右 中)
	* 当求深度的时候,可以从上到下去查,这时候是用**前序遍历**(中 左 右)。但当要求的求根节点的深度时,根节点的深度就是这棵树的最大高度,所以此时也可以用后序遍历。

2、遍历顺序

​ 后续遍历

四、二叉搜索树 BST

​ 迭代法遍历:

BST 用迭代法时,不需要借助栈或队列就可实现遍历,

一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。

对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。

五、做题总结

第226题 反转二叉树

第101题 对称二叉树

第104题 二叉树的最大深度

第111题 二叉树的最小深度

第257题 二叉树的所有路径

第513题 找左下角的值

第112题 路径总和

第106题 从中序与后序遍历序列构造二叉树

第617题 合并二叉树

第700题 搜索一棵二叉树

第98题 验证二叉树

第530题 二叉搜索树的最小绝对差

第501题 二叉搜索树的众数

第236题 二叉树的最近公共祖先

第235题 二叉搜索树的公共祖先

第701题 二叉搜索树中的插入操作

第450题 删除搜素树中的节点

第669题 修剪一棵二叉搜索树

第108题 将有序数组转换为二叉搜索树

哈哈哈

第226题、递归不能用中序

​ 反转即是把每个节点的左右孩子交换一下,(左右孩子对应的子树也一起交换)

​ 代码实现:可以有9种写法

第101题、递归法只能后续遍历实现(左右中)

​ 只能用 后序遍历 来,因为要比较的是对称结构,即比较根节点对应的 左孩子的左右中遍历结果右孩子的右左中遍历结果,这两种遍历结果是否相等。因为要通过递归函数的返回值做计算。

​ 代码实现:

第104题、递归法只能后序遍历实现(左右中)

因为要通过递归函数的返回值做计算。

像这种要通过先获得左右孩子的数据,然后再得到自身的数据,这样就是采用后序遍历的逻辑

代码实现:

* 递归法:后序遍历
* 迭代法:层序遍历

​ 注意:为什么要回溯?

​ 以本题中的递归函数为例: getDepth(TreeNode* node, int depth)

回溯

即对 depth 的+1 或者 -1,的操作,为啥呢,因为无论什么情况,都需要保证,递归函数中的两个参数表示的同一层的,

而在向下一层递归的过程中,相当于 node->left ( 或 node->right )节点向下了一层,所以 depth 当然也要向下走了一层咯。

但是因为 目前所处的位置还是在本层里,所以,还是要保证本层的 depth 是本层的, 下一层的 depth 是下一层的。

第111题、递归法只能后序遍历实现(左右中)

注意最大深度和最小深的区别是什么?

1、最小深的处理

2、这道题还学到了一点,如何跳出双层嵌套的循环

​ 在第一个循环中加一个 flag 标记,

​ 然后在第二个循环中加一个判断,如果满足了某个条件需要跳出双层循环,就对这个标记赋值位K,且break,

​ 然后在第一个循环中加一个判断,当标记值为K时,就 break。

这个标记可以用 int 或 bool 都可以的。

第257题、这道题开始接触真正的回溯算法

​ 这里的递归要用前序遍历,因为这样才方便把父节点指向孩子节点,找到对应的路径。

前序遍历递归三部曲:

第513题 层序遍历很简单,但是偏要用一下递归法

主要介绍递归法:

​ 本题要求 最后一行最左边的值

​ 所以是要沿着两个方向,

* 一个是自上而下找深度,最终的结果必须是深度最大的,对应的就是最后一行
* 一个是自左向右,要记录最左边的叶子节点的值

第112题

第一个出彩的地方

​ 求和 不用减法,而是做加法,用待求的值,依次减去路径上的值,如果最后得到的是 0,就返回 true,否则返回 false

递归的实现代码:

​ 本题使用 前 中 后 序的递归都可,因为中节点没有处理逻辑

需要遍历树的所有路径

三部曲来一遍:

这里关于回溯写一下自己的想法:因为传入的参数 有节点 和一个int 类型变量,因此在处 理递归逻辑的时候,需要对节点进行处理,因为要进下一层,而处理完,还需要回溯到本层,这样才能保证本函数的节点和本函数的值的正确的对应关系,回溯就相当于撤销了递 归处理节点的过程而已。

通常可以分开来写,或者直接写。

第二个出彩的地方:

如果是用栈来实现呢?

栈如何来模拟回溯呢? 想要实现回溯,就需要记录两个遍历,一个是节点,一个是头结点到该节点的路径数值的总合。

可以用一个 pair<TreeNode*, int> 即 : pair<节点指针,路径数值>

一个pair 为栈中的一个元素。

第106题

构造一棵二叉树可以分为如下六步:

* 第一步:如果数组大小为0的话,说明是空节点
* 第二步:如果不为空,那么取后序数组最后一个元素 k 作为节点元素
* 第三步:找到 k 在中序数组的位置,作为切割点
* 第四步:切割中序数组,切成中序左数组和中序右数组 (注意这里的顺序不要反了,一定是先切中序数组)
* 第五步:然后将后序数组切成后序左数组和后序右数组
* 第六步:递归处理左区间和右区间

第617题

这个题,让我明白了一个点,就是 1、2、3 这三个地方的返回值分别代表的不同的含义

1 和 2 是为了在遇到叶子节点时 ,返回上一层

3 是在遍历完整棵树之后才会进行的操作,返回

image-20210502135906753

第700题

涉及到二叉搜索树就用中序遍历

两种解法都类似中序遍历:

​ 递归法

​ 迭代法

第98题

同样也是类似于中序遍历的三种解法:

​ 递归把树压入一个数组,然后再进行判断

​ 边递归边判断

​ 迭代法

注意:

​ 二叉搜索树 BST 的两个陷阱,

​ 1)不能单纯只比较 左节点小于中间节点,右节点大于中间节点,而是要比较 左子树所有节点小于中间节点,右子树所有节点大于中间节点

​ 2)关于最小节点的定义,样例中的最小节点可能是 int 的最小值,如果是这样的话,那自定义最小的 int 来进行比较时就不对了。可以不去定义这个最小变量,用最左边节点的数值进行比较,之后每一次也都要进行更新。

第501题

什么是众数?

​ 在一组数中,出现次数最多的数

什么是中位数?

​ 按顺序排列的一组数据中,位于中间位置的数

法一:普通二叉树通用递归方法::

1、首先在方法一中学到的第一个要点:

如果要搜索的是普通二叉树,而不是本题说明的二叉搜索树BST,对于常规解法,需要对pair进行排序,这时候有一个第一次用到的sort用法

// vec 是一个存储了pair类型的数组,现在需要按照pair的second值来对vec进行排序
// 所以定义了 cmp 函数,在 cmp 函数中定义了排序的规则,

vector<pair<int, int>> vec;
bool static cmp (const pair<int, int>& a, const pair<int, int>& b)
{
	// 返回 a > b ,则为降序排列,即最大值在前
	return a.second > b.second;
	
	// 返回 a < b ,则为升序排列,即最小值在前	即: return a.second 《 b.second;
}

sort(vec.begin(), vec.end(), cmp);	// 前两个参数表示排序的范围,cmp来确定排序的方式

总结:通用法思路比较直接,主要就是最后怎么对频率进行排序这点需要注意。

法二:BST递归法

2、因为二叉搜索树是有大小顺序的,那么就比较相邻元素即可统计出现的频率

3、既然是要比较相邻两个节点,那么此时就需要定义一个 指向前一个节点的指针pre 用来存储前一个节点,和一个 指向当前节点的指针cur 用来存储当前节点,注意在定义指针时,为了避免野指针,要将其初始化为空指针哦~,这样当判断出 pre == nullptr 时,我们就知道比较的是第一个元素。

4、本题中要求的是最大频率的元素的集合,那么就有可能不止一个元素是最大频次,这时候有两种处理方式,

​ 第一种先遍历一遍数组,找出最大频率,然后再重新遍历一遍数组,把出现频率最大的元素放进集合

​ 第二种是更可取的,就是多定义一个变量maxCount,用来更新最大频率,把频率等于maxCount的元素放进结果集合中,但是当出现比maxCount更大的频率时,就更新结果集合,将其情况,并更新maxCount。这样只遍历一次就能达到效果。

5、关于比较

​ 这里涉及到两处比较:

​ 一个是pre节点与cur节点的比较,分了三种情况来处理

​ 一个是频率的比较,没有使用map容器来计数,而是定义一个 int 类型的变量来计数,对频率值进行更新

法三:迭代法

与法二基本类似,同样是采用中序遍历

第236题

思路:

找公共祖先,那得自底向上查找。

怎么自底向上查找呢?回溯呀,回溯的过程就是从底向上。

后序遍历就是天然的回溯的过程,他最先处理的一定是叶子节点。注意:二叉树只能通过后序遍历(即:回溯)实现自底向上的遍历方式。

那么如何判断一个节点是节点q和节点p的公共祖先呢?

​ 如果找到一个节点,发现左子树出现节点p,右子树出现节点q,或者是左子树出现节点q,右子树出现节点p,那么这个节点就是他们的最近公共祖先。

总结:

使用后序遍历,回溯的过程就是自底向上遍历节点,一旦发现满足这个条件的节点,就是最近的公共祖先节点了。

图片

理解一:

  1. 如果你能在当前这棵树上找到p和q的公共节点,那就返回这个公共节点
  2. 如果没有找到公共节点但是找到了p或者q它们自己,那你就返回找到的那个p或者q
  3. 再否则你啥也没找到,就返回null,而不是单纯的返回公共节点。

理解二:

  1. 先看root,如果root先占了一个值,那直接返回root,否则就看left, right

  2. 只要left, right任意一个是么有的,那就返回有的那个

  3. 因为么有的那个下面肯定没有目标值,有的那个肯定是找到了目标值才返回来的,无论多深,返回上去的时候那个left, right都是返回那个目标值

  4. 如果在两个值不在同一层,那么就是第一步了,如果在同一层,那就是它上面的那个值,所以逻辑是写在left, right里面实现的。

第235题

与上一题不同的是 这是一颗BST,可以有效地利用 BST 树的性质

BST是有序的,在从上到下遍历的过程中,当前节点的值在 【q, p】的区间里,则说明该节点就是最近的公共祖先,

普通二叉树需要利用回溯,由底向下查找,后续递归遍历

BST 只需要从上向下遍历即可,这里 前中后序遍历 都是可以的。这里并没有处理中间节点的逻辑,因此遍历顺序也就无所谓了

BST 用迭代法时,不需要借助栈或队列就可实现遍历,

一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。

对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。

第701题

​ 1、递归返回值直接返回节点

​ 2、不用递归返回值,而是定义一个节点用来临时存储前一个节点

第450题

注意:删除比插入思路更难

单层递归逻辑:

共有 5 种情况:

第669题

修剪问题其实就是从二叉树中删除节点的问题,

递归三部曲:

本题是需要遍历整棵树,通常在遍历整棵树的时候,我们不设返回值,

本题在确定递归函数的返回值时,可以有返回值,也可以没有返回值,但是没有返回值的处理会比较麻烦。因此设定本题用函数的返回值

有返回值的话,我们可以通过递归函数的返回值来移除节点

本题的修剪操作不是在终止条件是进行的,所以终止条件就是简单地遇到空节点就返回

对于单层递归逻辑这里,前两步和第三步的关系,刚开始真的很难想,其实现在可能也没有搞明白,
其实前两步就相当于把root变为符合 [low , high]这个区间的一个节点了
第三步是相当于对root的左右子树的修剪

这里包含了两层关系,
第一层是判断根节点是否符合,也就是 单层递归逻辑中的前两步
第二层是判断根结点的 左右子树 是否符合,也就是 单层递归逻辑中的第三步

发现自己想递归过程的时候,总是莫名其妙的会混淆,常常是因为 当前节点是什么搞混了,要明白当前的节点是随着递归的过程在改变,尤其是有递归函数的返回值时,更应该注意一下,return 给了谁,谁又是当前的节点,这样才能搞明白这个逻辑,要不然就是一头雾水的

一位录友很棒的理解:

1、当前节点看做根节点

2、判断当前节点是否在指定的区间内

3、如果小于左区间,说明当前节点的左子树需要被剪掉,就以当前节点的右孩子为根节点继续去找,返回给上一层进行连接(返回值一定是右子树中的某个节点,相当于把左子树剪掉了)

4、如果大于右区间,说明当前节点的右子树需要被剪掉,就以当前节点的左孩子为根节点继续去找,返回给上一层进行连接(返回值一定是左子树中的某个节点,相当于把右子树剪掉了)

5、如果在区间内,就既要找当前节点的左孩子,又要去找当前节点的右孩子,分别将找到的节点对当前节点的左孩子和根节点的右孩子进行更新。

第108题

1、转换为一棵高度平衡的二叉搜索树 与 转换为一棵普通二叉搜索树 有什么区别呢?

​ 其实 不用强调平衡,也会是平衡,因为用数组来构造二叉树,构成平衡是自然而然的事儿。

​ 因为大家都默认从数组中间位置取值作为节点元素,一般不会随机取

2、构造二叉树的精髓就是找寻分割点,

​ 分割点作为当前节点,然后递归左区间和右区间

标签:总结,遍历,递归,二叉,点睛,二叉树,返回值,节点
来源: https://blog.csdn.net/linping_/article/details/118077275