【LeetCode】#222 完全二叉树的节点个数 算法分析

【LeetCode】#222 完全二叉树的节点个数 算法分析

题目链接:222. 完全二叉树的节点个数

思路1 递归

这个就不多说了,毕竟各位年轻人不讲码德,笔者在此点到为止。

代码

C/C++

class Solution {
    
    
public:
    int countNodes(TreeNode* root) {
    
    
        if(!root)
            return 0;
        int left = countNodes(root->left);
        int right = countNodes(root->right);
        return left + right + 1;
    }
};

算法分析

时间复杂度: O ( n ) O(n) O(n) 设节点数为 n n n,考虑最坏情况下,完全二叉树为满二叉树,算法运行时间 T ( n ) = 2 T ( n − 1 2 ) + O ( 1 ) T(n) = 2T(\frac{n-1}{2}) + O(1) T(n)=2T(2n1)+O(1),我们将它放大为: T ( n ) ≤ 2 T ( n 2 ) + O ( 1 ) T(n) \le 2T(\frac{n}{2}) + O(1) T(n)2T(2n)+O(1),符合主定理的 C a s e 1 Case 1 Case1 (主定理的描述见此链接的"知识梳理"部分),即 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n);另一个角度,由于需要访问树中所有节点,每次访问开销为 O ( 1 ) O(1) O(1),故总开销为 O ( n ) O(n) O(n)

空间复杂度: O ( l g n ) O(lgn) O(lgn) 空间开销主要为系统栈的开销,由于采用的是深度优先策略,故系统栈最大开销时对应 树的高度,对于完全二叉树而言,最好、最坏、平均情况均为 O ( l g n ) O(lgn) O(lgn) (可以想想为什么)。

当然,也可以采用广度优先策略,这里就不多说了,详见 LeetCode 题解。

思路2 二分查找

充分利用 完全二叉树 的性质:高度为 h h h 时,第 i ∈ [ 0 , h − 1 ] i \in [0, h-1] i[0,h1] 层节点数达到最大值 2 i 2^i 2i,第 h h h 层节点 全部靠左,数目在 [ 1 , 2 h ] [1, 2^h] [1,2h] 之间。

由此,如果我们知道了 树的高度 h h h, 那么除了最后一层外其余层的节点数我们可以在 O ( 1 ) O(1) O(1) 时间内计算出来。对于完全二叉树,树的高度可以从获取 最左边的节点 来得知, 这个过程的时间开销为 O ( l g n ) O(lgn) O(lgn)

有了树高 h h h,我们就可以利用 完全二叉树 的性质,自顶向下、自左向右 地对各个节点进行 编号 1 , 2 , 3 , . . . , k 1, 2, 3, ..., k 1,2,3,...,k, 其中 k k k 总节点数,为编号为 k k k 的节点, k ∈ [ 2 h , 2 h + 1 − 1 ] k \in [2^h, 2^{h+1}-1] k[2h,2h+11],然后我们需要解决的最后一个问题是:确定 ⌈ \lceil 编号为 k k k 的节点存在时 ⌋ \rfloor k k k 的最大值

一个简单的方式:从同高度满二叉树的最大编号开始,按照递减的编号顺序,检验此编号的节点是否在该树中存在。从以上算法的描述中,我们可以注意到这个过程有两步,且很容易获得它们的时间开销:

  • 确认编号 → \rightarrow O ( n ) O(n) O(n)
  • 检查编号是否存在 → \rightarrow O ( l g n ) O(lgn) O(lgn)

这样我们的算法总开销反而比 ⌈ \lceil 思路一 ⌋ \rfloor 还高,为 O ( n l g n ) O(nlgn) O(nlgn)。因此,我们可以分别分析以上两个步骤,看看是否存在优化的空间。

  1. 根据完全二叉树的性质,最后一层的编号是从左到右 连续 的,具有 随机存取 特性,因此我们可以采用 二分查找 替换 线性查找,使得复杂度降至 O ( l g n ) O(lgn) O(lgn)

  2. 我们来讨论 ⌈ \lceil 检验编号是否存在 ⌋ \rfloor 这一算法的实现方案,实际上,我们可以根据 编号的二进制编码 来找寻树中对应节点,采用这种方式的依据是:树为完全二叉树,编号对应的二进制数与节点的位置之间存在明确的联系 —— 节点对应的二进制数按高位的第2位 开始,每一位对应 0 或 1 ,可以分别映射为 向左节点移动向右节点移动,这对于除了第 0 0 0 层外任意层的节点来说都成立。

    以一个高度为 3 的完全二叉树为例,每个编码确实符合以上述映射关系。

              1               1
    		/   \
    	   2     3            10, 11
    	  / \    /\
    	 4   5  6  7    	  100, 101, 110, 111
    	/\  /
       8 9 10	              1000, 1001, 1010
    

    可能有年轻人会问,为什么只有完全二叉树可以,其他类型的树不行呢?

    我的理解是:其他类型树的节点编号没有上述那么明确的映射关系,即编号的二进制编码不能指导我们找到节点所在位置;至于完全二叉树,它是满二叉树于最后一层从右向左依次剔除一些节点后形成的树。 满二叉树标识了特定规模问题的 全集,假定问题规模为 n n n,则全集中非空子集的总数为 2 n − 1 2^n-1 2n1,也就是满二叉树的节点总数。而根据二项式定理,我们有:
    2 n = ( 1 + 1 ) n = ∑ i = 0 n ( n i ) × 1 a n − i × 1 b i 2^n = (1+1)^n = \sum\limits_{i=0}^{n}\tbinom{n}{i}\times1_a^{n-i} \times 1_b^{i} 2n=(1+1)n=i=0n(in)×1ani×1bi

    式中的 1 a 1_a 1a 1 b 1_b 1b 构成了两种不同的状态,我们利用这两种状态,分别模拟 向左子树移动向右子树移动

    大意了,扯远了。这段我这个老同志自己也没搞得很清楚,这些都是瞎哔哔的,就当图一乐了(若有哪位年轻人能解释清楚,欢迎在评论区分享~ ),但至少知道这么编码可行就好了。

    确定了编码方案可行,再来看看这个算法的具体过程。可以举个具体例子:你可以想象有一只蜘蛛位于一个树形蜘蛛网上(见上一个图例),它想要确认编号 10 10 10 的网节点是否存在,于是从根节点出发,然后根据 10 10 10 的二进制编码 1010 1010 1010 来找寻,从第 2 2 2 位起:

    1. 首先取第 2 2 2 位, 即 1010 & 0100 = 0 1010 \& 0100 = 0 1010&0100=0,故往左节点爬向 节点 2 2 2
    2. 然后取第 3 3 3 位, 即 1010 & 0010 = 1 1010 \& 0010 = 1 1010&0010=1,故往右节点爬向 节点 5 5 5
    3. 最后取第 4 4 4 位, 即 1010 & 0010 = 0 1010 \& 0010 = 0 1010&0010=0,故往左节点爬向 节点 10 10 10
    4. 发现 节点 10 10 10 是存在的,那么蜘蛛就可以安心返回了 ;然而,节点 10 10 10 并不存在,是蜘蛛看错了;那怎么办… 关我P事,我只关心能不能找到这个节点(至少在这个算法内,是或否不重要),蜘蛛只是个 “工具蛛” 而已。 =3=

    按照上述过程来实现算法是非常简单的,详见代码的 exists() 函数,或去看官方题解,这里就不作赘述了。(虽然是用 Python3 写的,但基本思路不会随语言而变化,改用其他语言实现是很容易的)

代码

Python3

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
	'''利用完全二叉树的编码特性,判断第k个节点是否存在'''
    def exists(self, root: TreeNode, level: int, k: int) -> bool:
        bits: int = 1 << (level-1)
        while root and bits > 0:
            if (k&bits) > 0:
                root = root.right
            else:
                root = root.left
            bits >>= 1
        return root != None
                
    def countNodes(self, root: TreeNode) -> int:
        if not root:
            return 0
        node: TreeNode = root
        level: int = 0
        while node.left:
            level += 1
            node = node.left
        low: int = 1 << level
        high: int = (1 << (level+1)) - 1
        while low < high:
            mid: int = (low+high+1) // 2
            if self.exists(root, level, mid):
                low = mid
            else:
                high = mid-1
        return low    

说明:

  1. 注意运算符优先级!(没错,我说的就是(1 << (level+1)) - 1 ,其中的括号一个都不能少;还有按位与)

    方便起见,我把百度百科上的优先级表搬了过来,省去年轻人 O ( 1 ) O(1) O(1) 的查找时间(笑):

    在这里插入图片描述

  2. 二分查找 low = midhigh = mid-1 以及 返回值 别弄错了

算法分析

时间复杂度: O ( l g 2 n ) O(lg^2n) O(lg2n) 时间开销主要在二分查找的代码块中,二分查找开销为 Θ ( l g n ) \Theta(lgn) Θ(lgn),其中每一轮都有一个检查节点是否存在的过程,因为检查的是最后一层的节点,故开销为 Θ ( l g n ) \Theta(lgn) Θ(lgn),因此总开销为 O ( l g n × l g n ) O(lgn \times lgn) O(lgn×lgn)

空间复杂度: O ( 1 ) O(1) O(1)

最后,可能又有年轻人问了:你这冗长而丑陋的 O ( l g 2 n ) O(lg^2n) O(lg2n),难道比我 递归 O ( n ) O(n) O(n) 要快吗?

没有错哈。这里,我作为讲码德的老同志,先给个结论:
对 于 任 意 常 量 α > 0 , 恒 有 : l g b n = o ( n α )    ⟺    lim ⁡ n → ∞ l g b n n α = 0 , 其 中 b 为 任 意 实 常 量 对于任意常量 \alpha \gt 0, \quad 恒有: lg^bn = o(n^\alpha) \\ \iff \lim\limits_{n \rightarrow \infty} \frac{lg^bn}{n^\alpha} = 0,\quad 其中 b为任意实常量 α>0,lgbn=o(nα)nlimnαlgbn=0b

具体的证明 ⌈ \lceil 非常复杂 ⌋ \rfloor ,就先交给各位年轻人了。(证明可见《CLRS》第三版 原版 P55-P57 或 中文版 P32-P33

最后的最后,劝各位年轻人耗子尾汁,多讲码德,不要再犯 ⌈ \lceil 思路一 ⌋ \rfloor 这种小聪明,不要老在自己的 ⌈ \lceil 小天地 ⌋ \rfloor 里搞窝里斗,来骗,来偷袭,我这种 10101 10101 10101 岁的老同志。

谢谢朋友们 (\抱拳)

笔者水平有限,如果描述有什么不妥的地方,欢迎各位大佬在评论区批评指正~




仔细想想,算了,还是给各位年轻人省去查找的时间,在这里简单地证明一下吧。

对 于 所 有 使 得 a > 1 的 实 常 量 a 和 b , 多 项 式 的 增 长 比 不 过 指 数 : lim ⁡ n → ∞ n b a n = 0 我 们 用 l g n 替 代 上 式 的 n , 用 2 α 替 代 a ( α > 0 ) 从 而 有 lim ⁡ n → ∞ l g b n n α = 0 , 证 毕 对于所有使得 a \gt 1的实常量a和b ,\\ 多项式的增长比不过指数:\lim\limits_{n \rightarrow \infty}\frac{n^b}{a^n} = 0 \\ 我们用 lgn替代上式的n,用2^\alpha替代a\quad (\alpha>0)\\ 从而有\lim\limits_{n \rightarrow \infty} \frac{lg^bn}{n^\alpha} = 0,证毕 使a>1abnlimannb=0lgnn2αa(α>0)nlimnαlgbn=0

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/110038555