二叉树专题二

234 阅读8分钟

题目链接

  1. 完全二叉树的节点个数 leetcode-cn.com/problems/co…
  2. 二叉搜索树的第k大节点 leetcode-cn.com/problems/er…
  3. 树的子结构 leetcode-cn.com/problems/sh…
  4. 监控二叉树 leetcode-cn.com/problems/bi…
  5. 二叉树最大宽度 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的完全二叉树,节点个数一定在 [2h,2(h+1)1][2^h,2^(h+1)-1]的范围内,可以在该范围内通过二分查找的方式得到完全二叉树的节点个数。

  • 到这一步,我们的问题就转变成:在二叉树的某一层存在某个节点k,这个k刚好是这这一层的二分点
    leetcode提供了一个思路,利用位运算来查找二分点,其思路如下:

如果第k个节点位于第h层,则k的二进制表示包含h+1位,其中最高位是1,其余各位从高到低表示从根节点到第k个节点的路径,0表示移动到左子节点,1表示移动到右子节点。通过位运算得到第k个节点对应的路径,判断该路径对应的节点是否存在,即可判断第k个节点是否存在。 image.png

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 = 1

   3
  / \
 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树对应子节点的值

整体流程:

  1. 遍历整个A树所有节点
  2. 匹配到根B树根节点值相等时,再查看A树左右子节点和B树左右子节点的情况
  3. 依照该顺序向下遍历
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:

image.png

输入:[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
}

题目总结

二叉树的题目,实际上很多时候我们需要

  • 先把遇到的问题细化到尽可能小的维度(逐层等)
  • 再把问题抽象出来处理
  • 套入其他的思路(二分法等)