题目链接
- 完全二叉树的节点个数 leetcode-cn.com/problems/co…
- 二叉搜索树的第k大节点 leetcode-cn.com/problems/er…
- 树的子结构 leetcode-cn.com/problems/sh…
- 监控二叉树 leetcode-cn.com/problems/bi…
- 二叉树最大宽度 leetcode-cn.com/problems/ma…
题解及分析
完全二叉树的节点个数
给你一棵 完全二叉树的根节点root,求出该树的节点个数。 完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第h层,则该层包含1~ 2h个节点。
思路一:直接递归
既然是求二叉树节点的个数,那我们只要递归的去加二叉树的左子树和右子树就可以了。
这种算法的优点是通用,缺点当然是没有任何性能优化
var countNodes = function(root) {
if (root === null) {
return 0
}
return countNodes(root.left) + countNodes(root.right) + 1
}
思路二:优化
leetcode提供了一个思路,用二分法+位运算来处理二叉树的问题
规定根节点位于第0层,完全二叉树的最大层数为h。根据完全二叉树的特性可知,完全二叉树的最左边的节点一定位于最底层,因此从根节点出发,每次访问左子节点,直到遇到叶子节点,该叶子节点即为完全二叉树的最左边的节点,经过的路径长度即为最大层数h。对于最大层数为h的完全二叉树,节点个数一定在 的范围内,可以在该范围内通过二分查找的方式得到完全二叉树的节点个数。
- 到这一步,我们的问题就转变成:在二叉树的某一层存在某个节点k,这个k刚好是这这一层的二分点
leetcode提供了一个思路,利用位运算来查找二分点,其思路如下:
如果第k个节点位于第h层,则k的二进制表示包含h+1位,其中最高位是1,其余各位从高到低表示从根节点到第k个节点的路径,0表示移动到左子节点,1表示移动到右子节点。通过位运算得到第k个节点对应的路径,判断该路径对应的节点是否存在,即可判断第k个节点是否存在。
var countNodes = function(root) {
if (root === null) {
return 0
}
let level = 0
let node = root
// 先获取二叉树的最大节点深度
while(node.left !== null) {
level++
node = node.left
}
// 获取节点所在个数区间的最大值和最小值
let low = 1 << level
let high = (1 << (level + 1)) - 1
while(low < high) {
const mid = Math.floor((high - low + 1) / 2) + low
if(exist(root, level, mid)) {
low = mid
} else {
high = mid - 1
}
}
return low
}
var exist = function(root, level, k) {
let bits = 1 << (level - 1)
let node = root
while (node !== null && bits > 0) {
/**
* 如果第k个节点位于第h层,则k的二进制表示包含h+1位
* 其中最高位是1,其余各位从高到低表示从根节点到第k个节点的路径
* 0表示移动到左子节点,1表示移动到右子节点
*/
if (!(bits & k)) {
node = node.left
} else {
node = node.right
}
bits >>= 1
}
return node !== null
}
二叉搜索树的第k大节点
给定一棵二叉搜索树,请找出其中第 k 大的节点的值。
示例 1:
输入: root = [3,1,4,null,2], k = 13 / \ 1 4 \ 2输出: 4
题目实际上没怎么看明白...leetcode大神的意思如下:
本文解法基于此性质:二叉搜索树的中序遍历为递增序列 。
根据以上性质,易得二叉搜索树的 中序遍历倒序为递减序列 。
因此,求“二叉搜索树第 kk 大的节点”可转化为求“此树的中序遍历倒序的第k个节点”。
var kthLargest = function(root, k) {
let ans = []
const mid = root => {
if(!root) return
mid(root.left)
ans.push(root.val)
mid(root.right)
}
mid(root)
return ans[ans.length - k]
}
树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树A:3 / \ 4 5 / \ 1 2给定的树B:
4 / 1返回true,因为B与A的一个子树拥有相同的结构和节点值。
观察题意,如果B是A的子结构,那么需要满足以下的条件:
- A,B不能为空
- B子节点可以为空
- A树root节点的值和B树root节点的值必须一样
- 在满足上一个条件之后,我们才继续查找A树和B树对应子节点的值
整体流程:
- 遍历整个A树所有节点
- 匹配到根B树根节点值相等时,再查看A树左右子节点和B树左右子节点的情况
- 依照该顺序向下遍历
var isSubStructure = function(A, B) {
return (A !== null && B !== null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B))
}
var recur = (A, B) => {
if(B === null) return true
if(A === null || A.val !== B.val ) return false
return recur(A.left, B.left) && recur(A.right, B.right)
}
监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。示例 1:
输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
思路一:贪心算法--参考Jeff Wong大神的思路
根据子节点的状态来确认父节点的状态
我们给子节点三个状态
- 0 : 表示没有覆盖到
- 1 : 覆盖到了,但是该节点没有摄像头
- 2 : 覆盖到了,且该节点有摄像头 我们编写一个递归函数去查找每个节点的子节点的状态,同时制定以下策略:
- 当查询节点为空节点,我们视为显示器已经覆盖到了
- 当查询节点的叶节点为空,我们利用贪心的策略:使用其父节点放置摄像头,能覆盖更多的节点
- 我们将当前节点视为临时的根节点,递归的查询当前节点的左右子树计数
- 如果左右子树其中一个为0,我们视为子节点有没有覆盖到的,必须在当前节点放置摄像头
- 如果左右子树其中一个为2,我们视为节点里有任意一个放置了摄像头,当前节点不需要重复放置,因为已经被覆盖
- 子节点两个都被覆盖到了,但没有放置摄像头,因此当前节点没有覆盖(可以带入题目提供的root节点来思考)
var count = 0
var minCameraCover = function(root) {
if(root === null) {
return 0
}
if(root.left === null && root.right === null) {
return 1
}
let val = check(root)
if(val === 0) {
count += 1
}
return count
}
var check = node => {
if (node == null) {
return 1
}
if (node.left === null && node.right === null) {
return 0
}
let left = check(node.left)
let right = check(node.right)
if (left === 0 || right === 0) {
count++
return 2
}
if (left == 2 || right == 2) {
return 1
}
return 0
}
(该解法在走测试用例时无误,但提交的时候报错~)
思路二:动态规划 直接参考官方的题解即可,不是很理解这个做法~后续会继续思考并补上
var minCameraCover = function(root) { const dfs = root => { if(!root) { return [Math.floor(Number.MAX_SAFE_INTEGER / 2), 0, 0] } const [la, lb, lc] = dfs(root.left) const [ra, rb, rc] = dfs(root.right) const a = lc + rc + 1 const b = Math.min(a, Math.min(la + rb, ra + lb)) const c = Math.min(a, lb + rb) return [a, b, c] } return dfs(root)[1] }
二叉树最大宽度
给定一个二叉树,编写一个函数来获取这个树的最大宽度。树的宽度是所有层中的最大宽度。这个二叉树与满二叉树(full binary tree)结构相同,但一些节点为空。
每一层的宽度被定义为两个端点(该层最左和最右的非空节点,两端点间的null节点也计入长度)之间的长度。
示例 1: 输入:1 / \ 3 2 / \ \ 5 3 9输出: 4
解释: 最大值出现在树的第 3 层,宽度为 4 (5,3,null,9)。
思路:编号
我们按照从上到下从左到右的顺序,给树的每个节点加上一个编号。这个编号实际上就是节点的数量。
那么最大宽度即是某一行的最后一个节点的编号。
实现思路
- 用[编号,节点]的方式来描述一个节点对应的元素,并将这些元素存在数组中
- 逐层(从上到下从左到右)遍历二叉树,计算每一个节点的编号
- 计算每一层最后一个有效节点(即不为null的节点)和每一层第一个节点的编号差
var widthOfBinaryTree = function (root) {
if(!root) {
return 0
}
let ans = 0
let queue = [[0, root]]
while(queue.length) {
ans = Math.max(ans, queue[queue.length - 1][0] - queue[0][0] + 1)
let tmp = []
for(const [i, n] of queue) {
n.left && tmp.push([i * 2, n.left])
n.right && tmp.push([i * 2 + 1, n.right])
}
// 这里计算的意义是防止层数超出js最大有效数字时显示NaN
tmp.forEach(i => i[0] -= queue[0][0])
queue = tmp
}
return ans
}
题目总结
二叉树的题目,实际上很多时候我们需要
- 先把遇到的问题细化到尽可能小的维度(逐层等)
- 再把问题抽象出来处理
- 套入其他的思路(二分法等)