代码随想录算法训练营Day16
104.二叉树的最大深度
题目
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
- 树中节点的数量在
[0, 104]区间内。 -100 <= Node.val <= 100
思路
递归方法
递归的方法就是将问题分解为每一个子树的高度,然后回溯到父节点时,父节点的高度为子节点的高度加一。
确定递归函数的参数和返回值
每轮需要累计一个子树的高度,因此每次都需要传入子树的根节点;
要求子树的高度,那么返回值自然是当前的子树的高度;
确定终止条件
不难理解,如果当前节点为空,那么应该返回高度为0,说明父节点已经是叶子结点,高度为1。
确定单层递归的逻辑
如果当前的节点不是空,那么它的高度可以通过他的子节点来获得(通过递归实现)。
分别求出它的左子树和右子树的高度,取其中的最大值再加一,就是以它自己为根节点的子树的高度了。
迭代方法
二叉树的最大深度,其实就是二叉树的层数,因此利用层序遍历来进行层数累加一样可以求得
初始条件
- 新建int类型变量返回值result,初始值为0
- 判定输入的根节点是否为空,若为空,直接返回result
- 新建一个队列queue,类型为二叉树节点(或者指针)。将根节点入队,进入循环中
循环结束条件
如果queue为空,则循环结束。
循环内部逻辑
- 取得当前队列的长度len,表示的是本层的所有节点个数,根据len对这一层的所有节点进行处理
- 如果current的左孩子不为空,则将其加入queue中
- 如果current的右孩子不为空,则将其加入queue中
- 处理完len个队中数据后,表面本层的数据已经全部遍历了,result+1
代码实现
递归方法
func maxOf(a, b int) int {
if a > b {
return a
}
return b
}
func maxDepth(root *TreeNode) int {
result := 0
if root == nil {
//为空,则返回0
return result
}
leftDepth := maxDepth(root.Left)
rightDepth := maxDepth(root.Right)
//当前节点高度为最高子节点高度的最大值加一
currentDepth := 1 + maxOf(leftDepth, rightDepth)
return currentDepth
}
迭代方法
func maxDepth(root *TreeNode) int {
result := 0
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
for len(queue) > 0 {
currentLen := len(queue) //取得当前层次的节点数
for i := 0; i < currentLen; i++ {
//根据节点数出队,记录本层的节点信息
head := queue[0]
queue = queue[1:]
if head.Left != nil {
queue = append(queue, head.Left)
}
if head.Right != nil {
queue = append(queue, head.Right)
}
}
//本层节点全部遍历后,层数加一
result++
}
return result
}
111.二叉树的最小深度
题目
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
**说明:**叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
提示:
- 树中节点数的范围在
[0, 105]内 -1000 <= Node.val <= 1000
思路
递归方法
和求最大深度类似,当前的节点的最小深度可以表示为其子节点的最小深度+1。但需要注意的是,根据题目的定义:
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
因此,当一个节点的孩子节点中,一边为空,一边不是空的时候,它的最小深度并不是1,而是取决于非空的那个子树的最小深度,这是需要注意的地方。
确定递归函数的参数和返回值
每轮需要累计一个子树的最小深度,因此每次都需要传入子树的根节点;
要求子树的最小深度,那么返回值自然是当前的子树的深度;
确定终止条件
不难理解,如果当前节点为空,那么应该返回深度为0。
确定单层递归的逻辑
如果当前的节点不是空,那么它的高度可以通过他的子节点来获得(通过递归实现)。
如之前分析所说,如果当前节点有一个子节点为空,那么它的最小深度应该取非空的节点那边的最小深度,其他情况则依旧分别求出它的左子树和右子树的最小深度,取其中的最小值再加一,就是以它自己为根节点的子树的最小深度了。
迭代方法
二叉树的最小深度,同样可以通过层序遍历来实现,但因为求的是最小的深度,那么必然和层序遍历有区别。
显然,一颗树的最小深度,就是从根节点向下遍历时,最早遇到叶子节点的那一层的层数,因此,循环内增加一个遇到叶子结点就返回本层层数的提前终止设置即可达成目标。
初始条件
- 新建int类型变量返回值result,初始值为0
- 判定输入的根节点是否为空,若为空,直接返回result
- 新建一个队列queue,类型为二叉树节点(或者指针)。将根节点入队,进入循环中
循环结束条件
如果queue为空,则循环结束。
循环内部逻辑
- 取得当前队列的长度len,表示的是本层的所有节点个数,根据len对这一层的所有节点进行处理,当前出队的节点记作current
- 遍历前本层节点开始前result就加一,表示现在已经在这一层了。
- 如果current是叶子节点,那么说明本层就是最小深度所在了,直接返回result
- 如果current的左孩子不为空,则将其加入queue中
- 如果current的右孩子不为空,则将其加入queue中
代码实现
递归方法
func min(a, b int) int {
if a < b {
return a;
}
return b;
}
func minDepth(root *TreeNode) int {
if root == nil {
return 0;
}
// 左孩子为空,右孩子不空,则最小深度来自右孩子这边
if root.Left == nil && root.Right != nil {
return 1 + minDepth(root.Right);
}
// 左孩子不空,右孩子为空,则最小深度来自左孩子这边
if root.Right == nil && root.Left != nil {
return 1 + minDepth(root.Left);
}
currentDepth := 1 + min(minDepth(root.Left), minDepth(root.Right))
return currentDepth;
}
迭代方法
func minDepth(root *TreeNode) int {
result := 0
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
for len(queue) > 0 {
currentLen := len(queue) //取得当前层次的节点数
//到达新一层后就可以加一了
result++
for i := 0; i < currentLen; i++ {
//根据节点数出队,记录本层的节点信息
head := queue[0]
queue = queue[1:]
if(head.Left == nil && head.Right == nil){
return result
}
if head.Left != nil {
queue = append(queue, head.Left)
}
if head.Right != nil {
queue = append(queue, head.Right)
}
}
}
return result
}
222.完全二叉树的节点个数
题目
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
- 树中节点的数目范围是
[0, 5 * 104] 0 <= Node.val <= 5 * 104- 题目数据保证输入的树是 完全二叉树
思路
递归方法
递归方法本质还是递归遍历。
确定递归函数的参数和返回值
每轮需要累计一个子树的节点个数,因此每次都需要传入子树的根节点;
要求子树的节点个数,那么返回值自然是当前的子树的节点个数;
确定终止条件
不难理解,如果当前节点为空,那么应该返回节点数为0。
确定单层递归的逻辑
如果当前的节点不是空,那么以它为根节点的子树的节点总数可以通过他的左右子树来获得(通过递归实现)。
分别求出它的左子树和右子树的节点数,相加之后再加一(它本身是一个节点),就是以它自己为根节点的子树的总结点数了。
迭代方法
节点个数统计的迭代方法可以任意使用一种二叉树迭代方法改造实现,本次采用和前两个题目相同的层序遍历思路实现:
初始条件
- 新建int类型变量返回值count,初始值为0
- 判定输入的根节点是否为空,若为空,直接返回count
- 新建一个队列queue,类型为二叉树节点(或者指针)。将根节点入队,进入循环中
循环结束条件
如果queue为空,则循环结束,返回count。
循环内部逻辑
- 取得当前队列的长度len,表示的是本层的所有节点个数,根据len对这一层的所有节点进行处理
- 每出队一个节点,count+1
- 如果current的左孩子不为空,则将其加入queue中
- 如果current的右孩子不为空,则将其加入queue中
利用性质的高效方法
以上的方法适用于所有二叉树的节点统计,时间复杂度都是O(n).但实际上完全二叉树具有很多特殊性质,可以让节点统计更加高效。
介绍完全二叉树性质之前,先了解一下满二叉树的概念
满二叉树
一棵高度为h且含有2^h-1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。下图就是一个满二叉树:
满二叉树的特性有:
- 满二叉树的
叶结点都集中在二叉树的最下一层,并且除叶结点之外的每个结点度数均为2。 - 对满二叉树按层序编号:约定编号从
根结点(根结点编号为1) 起,自上而下,自左向右。每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲编号为**⌊i/2⌋(向下取整), 若有左孩子,则左孩子编号为2i**,若有右孩子,则右孩子编号为2i+1。
完全二叉树
高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。下图为一个完全二叉树:
完全二叉树特性有:
- 若i<=⌊n/2⌋,则结点i为分支结点,否则为叶结点。(即最后一个叶子节点的双亲是最后一个分支节点)
- 叶结点只可能在层次最大的两层上出现。对于最大层次中的叶结点,都依次排列在该层最左边的位置上。
- 若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
- 按层序编号后,一旦出现某结点(编号为i) 为叶结点或只有左孩子,则编号大于i的结点均为叶结点。
- 若n为奇数,则每个分支结点都有左孩子和右孩子;若n为偶数,则编号最大的分支结点(编号为n/2) 只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
不难发现,满二叉树其实也是一种特殊的完全二叉树,特殊在于满二叉树的节点数可以直接根据树的高度求出,一颗高度为h的满二叉树的节点数为2^h-1个。
那么完全二叉树从构造上和满二叉树有什么联系呢?首先完全二叉树一定是按照满二叉树的节点排列来拼成的,相当于一个同等高度的满二叉树;并且,完全二叉树的子树里,一定会有一个满二叉树,为什么呢?可以用反证法来证明:
-
假设一个
完全二叉树的子树中没有满二叉树。这意味着该子树的每一层都不是完全填满的。 -
然而,这与完全二叉树的定义相矛盾,因为完全二叉树的所有层(除了可能的最后一层)都是完全填满的。特别是,在子树的顶层和接近顶层的部分,必须完全填满,否则它就不会是原始完全二叉树的一部分。
-
因此,假设不成立。所以,一个完全二叉树的子树中一定有一个满二叉树。
同理,我们也可以证明一下为什么完全二叉树的子树一定都是完全二叉树:
- 假设存在一个
完全二叉树的子树不是完全二叉树。 - 由于子树不是
完全二叉树,那么它要么在除最后一层外的层中有空缺,要么最后一层的节点不是从左到右填满。 - 如果子树在除最后一层外的层中有空缺,那么原始的完全二叉树在这些层也将有空缺,与完全二叉树的定义矛盾。
- 如果子树的最后一层节点不是从左到右填满,那么原始的完全二叉树在这一层也将不是从左到右填满,同样与完全二叉树的定义矛盾。
- 因此,我们的假设不成立,所以一个完全二叉树的子树一定也是完全二叉树。
因此我们可以得出两个重要结论:
- 完全二叉树的子树一定是完全二叉树
- 在这些完全二叉子树中,一定有满二叉树(最明显的就是叶子节点,显然可以当做一个满二叉树)
那么如何判定完全二叉树是不是满二叉树呢,不难发现,不是满二叉树的完全二叉树,一直沿着左子树向下统计的高度和一直沿着右子树统计的高度是不相等的,左侧高度会比最右侧高度高1个单位,而满二叉树则一定相等。
综上分析,我们不难根据这些结论做一个递归方法进行求解
确定递归函数的参数和返回值
每轮需要累计一个子树的节点个数,因此每次都需要传入子树的根节点;
要求子树的节点个数,那么返回值自然是当前的子树的节点个数;
确定终止条件
- 如果节点为空,那么节点数自然是0;
- 如果以当前节点为根节点的子树是满二叉树,那么可以直接根据求得的高度计算出总节点数并返回。
确定单层递归的逻辑
如果当前的节点不是空,也不是满二叉树,那么以它为根节点的子树的节点总数可以通过他的左右子树来获得(通过递归实现)。
分别求出它的左子树和右子树的节点数,相加之后再加一(它本身是一个节点),就是以它自己为根节点的子树的总结点数了。
代码实现
递归方法
func countNodes(root *TreeNode) int {
count := 0
if root == nil {
//为空,则返回0
return count
}
leftCount := countNodes(root.Left) //左子树节点个数
rightCount := countNodes(root.Right) //右子树节点个数
return 1 + leftCount + rightCount
}
迭代方法
func countNodes(root *TreeNode) int {
count := 0
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
for len(queue) > 0 {
currentLen := len(queue) //取得当前层次的节点数
for i := 0; i < currentLen; i++ {
//根据节点数出队,记录本层的节点信息
head := queue[0]
queue = queue[1:]
//每出队一个个数加一
count++
if head.Left != nil {
queue = append(queue, head.Left)
}
if head.Right != nil {
queue = append(queue, head.Right)
}
}
}
return count
}
利用特性的优化方法
func countNodes(root *TreeNode) int {
if root == nil {
return 0
}
leftHeight := 0 //左侧高度
rightHeight := 0 //右侧高度
left := root.Left
right := root.Right
//沿着左子树统计左侧高度
for left != nil {
left = left.Left
leftHeight++
}
//沿着右子树统计右侧高度
for right != nil {
right = right.Right
rightHeight++
}
//如果相等,表示这是满二叉树
if leftHeight == rightHeight {
//直接计算出高度为2^h-1
return (2 << leftHeight) - 1
}
//如果不是满二叉树,则递归求解
return countNodes(root.Left) + countNodes(root.Right) + 1
}