赞
踩
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
(注意满二叉树和完美二叉树并不是一个概念)
求节点的数量和求高度类似,递归写法依旧是用后序遍历,统计左右子树的节点数量并返回上一层,进行累加。
class Solution { public: int countNodes(TreeNode* root) { //终止条件 if(root==NULL){ return 0; } //后序遍历 int leftNum = countNodes(root->left); int rightNum = countNodes(root->right); int Num = 1+leftNum+rightNum; return Num; } };
完全二叉树是除了底层节点之外全满,并且底层节点从左到右没有断开的二叉树。
当我们计算满二叉树的时候,节点数量可以直接用公式,假设满二叉树层数为n,那么节点数量就是2^n-1
。例如下图中中间的完全二叉树,前三层的节点数量就是2^3-1=7
。
因此可以通过完全二叉树除了底层都是满的这一特性来计算节点数。
遍历一个二叉树的子树,其子树如果是满二叉树,可以先得到子树的深度,然后直接2^n-1。
下面问题就是如何判断子树是否是满二叉树,并计算其深度。如果是满二叉树,向左遍历的深度和向右遍历的深度一定是相等的,并且,本题目规定了是一个完全二叉树,因此不存在左右深度相同但是中间存在NULL的情况。如下图所示。
但是图中的完全二叉树,其左子树是满二叉树,因此可以计算。右子树继续向下遍历,遇到了单个节点,单个节点也可以算满二叉树,因此也可以计算满二叉树节点数量并返回上层。也就是说,一直向下递归,一定可以遇到符合满二叉树条件的节点,不会进入死循环,因为最后的叶子节点一定是满二叉树。
因此,我们可以先往左递归,计算左侧深度,再一直往右递归,计算右侧深度,如果两侧深度相同,说明子树是满二叉树,直接通过2^n-1
计算其节点数目,返回给上一层。
这种写法的好处是,只需要遍历外侧节点,不需要遍历内侧,在二叉树足够大的情况下,中间侧的节点都不遍历,只遍历左右侧节点,能节省较多的时间开销。
int getNum(TreeNode* root){ //这种解法终止条件比较复杂,除了遇到空的情况,还要判断遇到满二叉树的情况 //遇到空 if(root==NULL){ return 0; } //遇到满二叉树,返回公式计算的子树节点数,也是终止条件 TreeNode* left = root->left; TreeNode* right = root->right; int leftDepth = 0; int rightDepth = 0; //一直向左计算深度 //这里注意,当left=left->left==NULL的时候,leftDepth就不会++了,也就是说leftDepth统计的就是真实的深度 while(left){ left = left->left; leftDepth++; } //一直向右计算深度 while(right){ right = right->right; rightDepth++; } if(leftDepth==rightDepth){ //指数计算方法:位运算 // 此处注意(2<<1) 相当于2^2,所以leftDepth初始为0 return (2<<leftDepth)-1; } //单层递归:后序遍历 int leftNum = getNum(root->left); int rightNum = getNum(root->right); int Num = 1+leftNum+righNum; return Num; }
c++中,2<<1相当于2的2次方,也就是说2<<1 = 4。2<<0是2的1次方,相当于没有移动。因此leftDepth
是从0开始的。但是计算的时候,相当于从2<<0也就是2的1次方开始计算。
在最后计算节点数量的时候,根节点那一层的数量应该是2<<0-1
,所以深度的初值必须从0开始取。
对于二叉树的深度或高度的定义,有两种常见的约定:
这两种约定都是常见的,选择哪一种主要取决于具体的应用场景和需求。例如,在111.二叉树最小深度题目中,显然采用了第二种约定,将只有一个节点的树(只有根节点)的深度视为1。在这种约定下,计算最小深度时,对于每个节点,我们都需要计算其左右子树的最小深度,然后加1(因为需要算上该节点自身)。
但是在本题222.完全二叉树的节点个数中,当计算满二叉树的节点数量时,采用了将根节点深度视为0的约定,因为根据公式,满二叉树的节点数为 2^d - 1
,其中d是树的深度,根节点深度为0,满二叉树节点数量就为 2<<0 - 1 = 2 - 1 = 1
,这与实际情况(只有一个节点)相符。这也是一个合理的约定,因为在这种情况下,我们关心的是整个树的结构,而不是单个节点的深度。
总的来说,对于树的深度(或高度)的定义没有固定的规定,取决于具体的应用场景和需求。只要在实际应用中保持一致,就不会出现问题。
在C++中,如果想计算 a
的 b
次方,通常使用 <cmath>
库中的 pow(a, b)
函数。
但是,如果 b
是2的幂,也就是说我们要计算的是2的多少次方,使用位运算会更快,因为位运算通常比浮点数运算更快。这是为什么本题中使用了位运算。然而,如果 b
不是2的幂,那么位运算就不能用来计算 a
的 b
次方了。
正常情况下,不是2的多少次方的运算,都是通过库的pow(a,b)
来解决。示例:
#include <iostream>
#include <cmath> // 引入cmath库以使用pow函数
int main() {
double a = 2.0;
double b = 3.0;
double result = std::pow(a, b); // 计算a的b次方
std::cout << "The result of " << a << " to the power of " << b << " is: " << result << std::endl;
return 0;
}
pow(a, b)
函数对于所有的浮点数a
和b
都有定义,这意味着它可以用来计算诸如pow(2.0, 0.5)
(即2的平方根)这样的表达式。但如果你只需要计算整数的整数次幂,也可以使用标准库<cmath>
中的pow
函数,如std::pow(2, 3)
。
但要注意的是,使用浮点数的运算可能会存在一定的精度问题,尤其是在处理大数或者需要高精度计算的情况下,这点在实际编程中需要特别注意。
class Solution { public: int countNodes(TreeNode* root) { //使用完全二叉树的特性完成题目 //终止条件 if(root==NULL){ return 0; } TreeNode* left = root->left; TreeNode* right = root->right; int leftDepth = 0; int rightDepth = 0; while(left){ left = left->left; leftDepth++; } while(right){ right = right->right; rightDepth++; } if(leftDepth==rightDepth){ return (2<<leftDepth)-1; } //单层递归:后序遍历 int leftNum = countNodes(root->left); int rightNum = countNodes(root->right); int Num = 1+leftNum+rightNum; return Num; } };
这种做法的时间复杂度是O(logn×logn)
。原因是它结合了二分查找和递归的思想。
在每一层递归中,代码首先通过两个while循环计算左子树和右子树的深度(高度),这个操作的时间复杂度是O(log n),因为在完全二叉树中,树的深度(高度)是log n(这里的n是树的节点总数)。
然后,如果左子树和右子树的深度(高度)相同,说明左子树是一棵满二叉树,直接通过公式 (2<<leftDepth)-1
计算节点数。如果左子树和右子树的深度(高度)不同,代码会递归地在左子树和右子树中调用 countNodes
函数。
注意,由于每一层递归中,代码都在二叉树中的一半节点中进行查找,这与二分查找的思想类似。所以,这个代码的总时间复杂度是O(log n × log n)。
为什么是O(log n × log n)而不是O(log n)呢?这是因为在每一层递归(深度)中,代码都要进行O(log n)的操作(两个while循环计算深度),并且树的深度也是O(log n),所以总的时间复杂度就是O(log n × log n)。
对于完全二叉树,O(log n × log n)的时间复杂度通常优于O(N)。
首先注意:O(log n × log n)=O((log n)^2),虽然是平方级别的复杂度,但仍然是对数级别的复杂度。对数级别的复杂度在大多数情况下优于线性复杂度。O(log n × log n)通常被用来描述那些每一层复杂度为O(log n),并且总共有log n层的算法。这样描述有助于理解算法的工作机制,但是实质上O(log n) × O(log n) = O((log n)^2)。
在第二种做法中,代码利用了完全二叉树的特性,只需要在一半的节点上进行操作。并且,每一层递归都会将问题规模减半,因此它的时间复杂度是对数级别的。
相比之下,第一种做法中的后序遍历算法需要访问树中的每一个节点一次,因此它的时间复杂度是线性的。
但需要注意的是,这只是理论上的比较。在实际情况中,由于各种因素(例如计算机的缓存效果、函数调用的开销等),实际性能可能会有所不同。实际上,如果树的大小不是特别大,那么简单的后序遍历算法可能会更快一些,因为它的实现更简单,没有额外的函数调用开销。
在理论上,O(log n × log n)的时间复杂度优于O(N),但在实际应用中,哪一种算法更好可能取决于具体的情境。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。