DFS (depth first search ) 深度优先遍历
BFS (breadth first search ) 广度优先遍历
BST (binary search tree) 二叉搜索树
AVL(Adelson-Velsky and Landis) 平衡二叉搜索树
heap 堆
满二叉树:是一种特殊的完全二叉树。只有度为0的节点和度为2的节点,且度为0的节点在同一层
完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
二叉搜索树:前面两种都是没有数值的,这个是有数值的树。且是有序树
平衡二叉搜索树: 又被称为AVL(Adelson-Velsky and Landis)树
要么是一棵空树,
要么它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树(所谓高度,即层数)
平衡二叉搜索树的增删操作的时间复杂度都是 logN
红黑树
堆
前缀树 字典树 Trie
C++ 中 map、set、multimap、multiset的底层实现都是平衡二叉搜索树,所以他们的增删操作的时间复杂度都是 logN。
(unordered_set 和 unordered_map)的底层实现是哈希表,增删操作的时间复杂度为 1。
// 链式存储的二叉树的节点定义方式
struct TreeNode
{
int val ;
TreeNode* leftChild ;
TreeNode* rightChild ;
**// 注意初值列是用小括号幅值,而不是等于号哦!!!!**
TreeNode (int x) : val (x) , left (NULL), right (NULL) {}
}
相当于用 广度优先遍历 BFS,从上到下,从左到右。
如果父节点的数组下标为 i ,则它的左孩子下标为 2 * i +1,右孩子下标为 2 * i +2。
栈的应用: 其实就是一种实现递归的结构,因此前中后序遍历的逻辑可以借助栈,来实现 非递归 的方式。
队列的应用: 层序遍历的实现一般是使用队列,因为队列先进先出的特点,巧好是实现层次遍历所需要的。
递归在本质上就是一种栈的结构,如果可以用栈来实现,自然而然就想到可以用递归来实现。
递归代码虽然看起来简洁,但是当链表非常长的时候,就会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。因此用栈基于循环实现的代码的鲁棒性要好一些。
处理前序遍历和后序遍历比较相似,也比较直观,但是中序遍历就跟这两种的思路不同,由于它遍历和访问的顺序不同导致的结果
在用迭代法遍历树的时候,其实是有两个操作
1)处理节点:即将元素放进 result 数组中,处理的过程即出栈
2)访问节点:即遍历节点,访问的过程即压栈
对于前序遍历,其顺序是 中左右,先访问的元素是中间节点,先处理的元素也是中间节点,由于访问元素和处理元素的顺序是一致的,都是中间节点,所以可以比较便捷
对于中序遍历,其顺序是左中右,先访问的是根节点,然后一层一层向下访问,直到达到树左面的最底部,在开始处理节点(也就是在把节点的数组都放进 result 数组中),这造成了处理顺序和访问顺序是不一致的。 因此在使用迭代法写中序遍历时,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
对与后序遍历,其顺序为左右中,是根据前序遍历改成的。
——>先序遍历:中左右——>调整左右顺序:中右左——>反转数组——>左右中
1、为什么要有标记法呢?
因为第一种迭代法中,前中后 三种迭代遍历 的实现代码的风格不那么统一,中序和前后序处理的思路完全不同。
2、那么怎么解决中序遍历中,访问节点和处理节点不一致的情况呢?
将访问的节点放入栈中,把要处理的节点也放入栈中,但是要做标记。
标记的方法就是要处理的节点放入栈之后,紧接着放入一个空指针,作为标记
3、根据中序遍历,就可以用标记法统一来处理 前中后序的遍历了
下图为前序遍历:注意因为是用的栈来存储数据,因此实际压栈的顺序为 右、左、中
层序遍历需要借助一个 辅助的数据结构 即队列,来实现。
层序遍历方式与图论中的广度优先遍历一样,这里只是应用在二叉树上。
递归的实现其实就是:每一次递归调用都会把 函数的局部变量、参数值、返回地址等压入栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因
如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!
如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。
想要实现回溯,就需要记录两个遍历,一个是节点,一个是头结点到该节点的路径数值的总合。
可以用一个 pair
一个pair 为栈中的一个元素。
一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。
关于回溯,什么情况下需要回溯呢?
好像是需要,用到一个与节点对应的值时,这个一般也可理解为,递归函数的参数有两个,一个是节点,另一个是与别的对应的变量。因为这个变量是需要参与计算的,所以才需要回溯
* 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定是前序,都是先构造中节点
* 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算
* 求二叉搜索树的属性,一定是中序,要不白瞎了有序性
对于求普通二叉树的属性问题,有时候也会用前序,比如在求深度,和找所有路径时,这样是为了方便让父节点指向子节点。所以求普通二叉树的属性还是要具体问题具体分析。
确定哪些参数是递归过程中需要处理的,那么就在递归函数里加上这个参数,
并且还要明确每次递归的返回值是什么,进而确定递归函数的返回类型
写完递归算法在运行时,经常会遇到栈溢出的错误,就是没写终止条件,或者终止条件写的不对,
操作系统也是一个栈的结构来保存每一层递归的信息,
如果没有终止,操作系统的内存栈必然就会溢出。
确定每一层递归需要处理的信息,
在这里也就会重复调用自己来实现递归的过程。
递归算法的时间复杂度 = 递归的次数 * 每次递归中的操作次数
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
由图可以知道:
* 当求高度的时候,只能从下往上去查,所以只能是**后序遍历**( 左 右 中)
* 当求深度的时候,可以从上到下去查,这时候是用**前序遍历**(中 左 右)。但当要求的求根节点的深度时,根节点的深度就是这棵树的最大高度,所以此时也可以用后序遍历。
后续遍历
递归写法:(左右中)
迭代写法:
整体思路是:
1)先定义一个函数,专门用来求高度,
2)这个函数通过栈模拟后序遍历,来找每一个节点的高度,其实就是通过求传入节点对应子树的最大深度,将这个最大深度作为其高度。
3)然后再用栈来模拟前序遍历,遍历每一个节点的时候,再去再去判断左右孩子的高度是否符合。
迭代法遍历:
BST 用迭代法时,不需要借助栈或队列就可实现遍历,
一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。
对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。
反转即是把每个节点的左右孩子交换一下,(左右孩子对应的子树也一起交换)
代码实现:可以有9种写法
只能用 后序遍历 来,因为要比较的是对称结构,即比较根节点对应的 左孩子的左右中遍历结果 和 右孩子的右左中遍历结果,这两种遍历结果是否相等。因为要通过递归函数的返回值做计算。
代码实现:
递归法:后序遍历(其实并非标准的后续遍历啦 ~ 要明白那个过程就可)
1、确定递归函数的参数和返回值
因为要比较的是根节点的两个子树是否是互相翻转的关系,进而判断这个树是否为对称树,所以要比较的是两个子树,所以要传入的参数自然是左子树节点和右子树节点。
返回值类型为 bool 类型
2、确定终止条件
3、确定递归循环的步骤
迭代法:
可以用队列、栈、数组等来实现,主要的思想都是一样的。
因为要通过递归函数的返回值做计算。
像这种要通过先获得左右孩子的数据,然后再得到自身的数据,这样就是采用后序遍历的逻辑
代码实现:
* 递归法:后序遍历
* 迭代法:层序遍历
注意:为什么要回溯?
以本题中的递归函数为例: getDepth(TreeNode* node, int depth)
即对 depth 的+1 或者 -1,的操作,为啥呢,因为无论什么情况,都需要保证,递归函数中的两个参数表示的同一层的,
而在向下一层递归的过程中,相当于 node->left ( 或 node->right )节点向下了一层,所以 depth 当然也要向下走了一层咯。
但是因为 目前所处的位置还是在本层里,所以,还是要保证本层的 depth 是本层的, 下一层的 depth 是下一层的。
注意最大深度和最小深的区别是什么?
1、最小深的处理
2、这道题还学到了一点,如何跳出双层嵌套的循环:
在第一个循环中加一个 flag 标记,
然后在第二个循环中加一个判断,如果满足了某个条件需要跳出双层循环,就对这个标记赋值位K,且break,
然后在第一个循环中加一个判断,当标记值为K时,就 break。
这个标记可以用 int 或 bool 都可以的。
这里的递归要用前序遍历,因为这样才方便把父节点指向孩子节点,找到对应的路径。
前序遍历递归三部曲:
vector& result
,注意这里 通过引用传递,
if(cur == nullptr) { 终止逻辑代码 }
,这样写不方便找到叶子节点if(cur->left == nullptr && cur->right == nullptr) { 终止逻辑代码 }
cur
为空为什么没问题?是因为下面的逻辑可以控制空节点不入栈。而且根节点会在主函数中判断是否为空。vector
数组方便做回溯处理,因此用vector
来记录路径,注意是 vector
int 类型的path
,转为 string 类型 sPath
主要介绍递归法:
本题要求 最后一行 的 最左边的值
所以是要沿着两个方向,
* 一个是自上而下找深度,最终的结果必须是深度最大的,对应的就是最后一行
* 一个是自左向右,要记录最左边的叶子节点的值
第一个出彩的地方
求和 不用减法,而是做加法,用待求的值,依次减去路径上的值,如果最后得到的是 0,就返回 true,否则返回 false
递归的实现代码:
本题使用 前 中 后 序的递归都可,因为中节点没有处理逻辑
需要遍历树的所有路径
三部曲来一遍:
这里关于回溯写一下自己的想法:因为传入的参数 有节点 和一个int 类型变量,因此在处 理递归逻辑的时候,需要对节点进行处理,因为要进下一层,而处理完,还需要回溯到本层,这样才能保证本函数的节点和本函数的值的正确的对应关系,回溯就相当于撤销了递 归处理节点的过程而已。
通常可以分开来写,或者直接写。
第二个出彩的地方:
如果是用栈来实现呢?
栈如何来模拟回溯呢? 想要实现回溯,就需要记录两个遍历,一个是节点,一个是头结点到该节点的路径数值的总合。
可以用一个 pair
一个pair 为栈中的一个元素。
构造一棵二叉树可以分为如下六步:
* 第一步:如果数组大小为0的话,说明是空节点
* 第二步:如果不为空,那么取后序数组最后一个元素 k 作为节点元素
* 第三步:找到 k 在中序数组的位置,作为切割点
* 第四步:切割中序数组,切成中序左数组和中序右数组 (注意这里的顺序不要反了,一定是先切中序数组)
* 第五步:然后将后序数组切成后序左数组和后序右数组
* 第六步:递归处理左区间和右区间
这个题,让我明白了一个点,就是 1、2、3 这三个地方的返回值分别代表的不同的含义
1 和 2 是为了在遇到叶子节点时 ,返回上一层
3 是在遍历完整棵树之后才会进行的操作,返回
涉及到二叉搜索树就用中序遍历
两种解法都类似中序遍历:
递归法
迭代法
同样也是类似于中序遍历的三种解法:
递归把树压入一个数组,然后再进行判断
边递归边判断
迭代法
注意:
二叉搜索树 BST 的两个陷阱,
1)不能单纯只比较 左节点小于中间节点,右节点大于中间节点,而是要比较 左子树所有节点小于中间节点,右子树所有节点大于中间节点
2)关于最小节点的定义,样例中的最小节点可能是 int 的最小值,如果是这样的话,那自定义最小的 int 来进行比较时就不对了。可以不去定义这个最小变量,用最左边节点的数值进行比较,之后每一次也都要进行更新。
什么是众数?
在一组数中,出现次数最多的数
什么是中位数?
按顺序排列的一组数据中,位于中间位置的数
法一:普通二叉树通用递归方法::
1、首先在方法一中学到的第一个要点:
如果要搜索的是普通二叉树,而不是本题说明的二叉搜索树BST,对于常规解法,需要对pair进行排序,这时候有一个第一次用到的sort用法
// vec 是一个存储了pair类型的数组,现在需要按照pair的second值来对vec进行排序
// 所以定义了 cmp 函数,在 cmp 函数中定义了排序的规则,
vector> vec;
bool static cmp (const pair& a, const pair& 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 类型的变量来计数,对频率值进行更新
法三:迭代法
与法二基本类似,同样是采用中序遍历
思路:
找公共祖先,那得自底向上查找。
怎么自底向上查找呢?回溯呀,回溯的过程就是从底向上。
后序遍历就是天然的回溯的过程,他最先处理的一定是叶子节点。注意:二叉树只能通过后序遍历(即:回溯)实现自底向上的遍历方式。
那么如何判断一个节点是节点q和节点p的公共祖先呢?
如果找到一个节点,发现左子树出现节点p,右子树出现节点q,或者是左子树出现节点q,右子树出现节点p,那么这个节点就是他们的最近公共祖先。
总结:
使用后序遍历,回溯的过程就是自底向上遍历节点,一旦发现满足这个条件的节点,就是最近的公共祖先节点了。
理解一:
理解二:
先看root,如果root先占了一个值,那直接返回root,否则就看left, right
只要left, right任意一个是么有的,那就返回有的那个
因为么有的那个下面肯定没有目标值,有的那个肯定是找到了目标值才返回来的,无论多深,返回上去的时候那个left, right都是返回那个目标值
如果在两个值不在同一层,那么就是第一步了,如果在同一层,那就是它上面的那个值,所以逻辑是写在left, right里面实现的。
与上一题不同的是 这是一颗BST,可以有效地利用 BST 树的性质
BST是有序的,在从上到下遍历的过程中,当前节点的值在 【q, p】的区间里,则说明该节点就是最近的公共祖先,
普通二叉树需要利用回溯,由底向下查找,后续递归遍历
BST 只需要从上向下遍历即可,这里 前中后序遍历 都是可以的。这里并没有处理中间节点的逻辑,因此遍历顺序也就无所谓了
BST 用迭代法时,不需要借助栈或队列就可实现遍历,
一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。
对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。
1、递归返回值直接返回节点
2、不用递归返回值,而是定义一个节点用来临时存储前一个节点
注意:删除比插入思路更难
单层递归逻辑:
共有 5 种情况:
修剪问题其实就是从二叉树中删除节点的问题,
递归三部曲:
本题是需要遍历整棵树,通常在遍历整棵树的时候,我们不设返回值,
本题在确定递归函数的返回值时,可以有返回值,也可以没有返回值,但是没有返回值的处理会比较麻烦。因此设定本题用函数的返回值
有返回值的话,我们可以通过递归函数的返回值来移除节点
本题的修剪操作不是在终止条件是进行的,所以终止条件就是简单地遇到空节点就返回
对于单层递归逻辑这里,前两步和第三步的关系,刚开始真的很难想,其实现在可能也没有搞明白,
其实前两步就相当于把root变为符合 [low , high]这个区间的一个节点了
第三步是相当于对root的左右子树的修剪
这里包含了两层关系,
第一层是判断根节点是否符合,也就是 单层递归逻辑中的前两步
第二层是判断根结点的 左右子树 是否符合,也就是 单层递归逻辑中的第三步
发现自己想递归过程的时候,总是莫名其妙的会混淆,常常是因为 当前节点是什么搞混了,要明白当前的节点是随着递归的过程在改变,尤其是有递归函数的返回值时,更应该注意一下,return 给了谁,谁又是当前的节点,这样才能搞明白这个逻辑,要不然就是一头雾水的
一位录友很棒的理解:
1、当前节点看做根节点
2、判断当前节点是否在指定的区间内
3、如果小于左区间,说明当前节点的左子树需要被剪掉,就以当前节点的右孩子为根节点继续去找,返回给上一层进行连接(返回值一定是右子树中的某个节点,相当于把左子树剪掉了)
4、如果大于右区间,说明当前节点的右子树需要被剪掉,就以当前节点的左孩子为根节点继续去找,返回给上一层进行连接(返回值一定是左子树中的某个节点,相当于把右子树剪掉了)
5、如果在区间内,就既要找当前节点的左孩子,又要去找当前节点的右孩子,分别将找到的节点对当前节点的左孩子和根节点的右孩子进行更新。
1、转换为一棵高度平衡的二叉搜索树 与 转换为一棵普通二叉搜索树 有什么区别呢?
其实 不用强调平衡,也会是平衡,因为用数组来构造二叉树,构成平衡是自然而然的事儿。
因为大家都默认从数组中间位置取值作为节点元素,一般不会随机取
2、构造二叉树的精髓就是找寻分割点,
分割点作为当前节点,然后递归左区间和右区间