写在前面:
今天带来一道 Leetcde Easy 的题,但别觉得我在水帖,这道题目在 Google 的面试题中甚至可以升级到 Leetcode medium to hard 的级别,而今天我要带来的正是他的高阶要求,怎么样利用 Complete Binary Tree 的特性对 Tree traversal 进行优化,并且在 Runtime 的限制内解决问题,就让我们一起来看看这道题吧!
题目介绍:
题目信息:
- 题目链接:https://leetcode.com/problems/count-complete-tree-nodes/description/
- 题目类型:Binary Tree,Recursion,TreeTraversal,Math
- 题目难度:Easy (但我带来的这道升级难度应该为 Medium 以上)
- 题目来源:Google 高频面试题
题目介绍:
- 给定一个 Binary Tree,并且这个 Tree 是 Complete Binary Tree
- Complete Binary Tree:
- 除了最后一层以外,所有上层的每一层都没有空节点,均有左右两个 children
- 最后一层中,从左到右之间没有空隙空节点
- 要求找出这个给定 tree 的所有节点个数并返回
- 要求 Runtime 必须在 O(n) 以内
题目想法:
BruteForce:
这道题之所以是 Easy 的原因就是在于它本身不难,只需要用最基本的 Recursion 将整个 tree 遍历一遍就行,每经过一个非空节点增加一个个数,最后返回总个数。
这种方法的代码我就不详细说了,非常简单。但是 Runtime 上需要遍历每一个节点,所以复杂度为 O(n),并不能满足 Google 面试对这道题目的要求,并且也没有利用 Complete Tree 的性质,所以我们优化的方向主要探索如何利用 Complete Tree
Complete Tree 除了最后一层:
因为 Complete Tree 除了最后一层以外,其他层都是保证所有节点都是塞满的,所以我们的问题就变成了:前面所有节点 + 最后一层有几个节点
而针对一个 满 Binary Tree 的节点,我们只需要知道他的深度 d,再利用公式:
num = pow(2, d) - 1
即可求出除了最后一层以外的所有点。
最后一层遍历,利用 binary search
因为最后一层的点一定是从左到右,中间没有空隙的。我们可以通过 binary tree 的思想逼近找到最右边的那个存在的点,这样就可以确定最后一层一共有几个节点了
根据公式,最后一层最多一共有: 2^d 个节点,而第一个节点我们已经确定是有了(complete tree),所以我们只需要将剩下的 2^d - 1 个节点编号成为 1, 2, ....., 2^d - 1,对这些点做一个binary search。
最后一层 Binary Tree 思路:当目标点不存在时,我们知道右边绝对不会再有节点了,所以我们直接看左半边就行。而当目标点存在时,我们知道左半边一定都有,我们只需要看右半边就可以,这样依次逼近,当 左右 节点相等时,我们就能找到这个最边缘的点,也就可以知道最后一层一共有几个了
而对于每一次的 目标点是否存在,我们又可以利用一次 binary search,因为 binary tree 和 binary search 的结构是相似的:
找寻目标点的 Binary Search 思路:我们最后一层所有点编为 0 -> 2^d - 1,然后展开 binary tree,遍历 d 次,d 为深度
每次遍历时,如果目标点小于 最后一点的 target,我们知道他肯定在 tree 的右半边,我们将root 节点变为 root-> right,反之我们去 root->left。如果我们遍历到了一个空节点,我们直接返回这个点吧不存在
如果我们遍历 d 次后还在实体点,则说明这个点存在,返回 true
而最后的结果也将是:
2^d + 1 + 最后一层点数
题目代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int getDepth(TreeNode* root){
//stop at the last level
int d = 0;
while(root->left){
//since it is a complete tree, every level shall have the leftmost
root = root->left;
d += 1;
}
return d;
}
bool exists(int target, TreeNode* root, int depth){
//the last level have total 2^d node, denote from 0 to 2^d - 1
int left = 0, right = pow(2, depth) - 1;
for(int i = 0; i < depth; i++){
int pivot = (left + right) / 2;
//use the binary tree property, if the target smaller than pivot, then it must be
//in the left subtree of the tree, and vice versa
if(target <= pivot){
root = root->left;
right = pivot;
}else{
root = root->right;
left = pivot + 1;
}
if(root == nullptr){
break;
}
}
//at the very last, if the node is not nullptr, then we shall say it is exists.
return (root != nullptr);
}
int countNodes(TreeNode* root) {
if(!root)
return 0;
//find the depth of the tree, then calculate the d-1 complete nodes:
int d = getDepth(root);
int res = pow(2, d) - 1;
if(d == 0)
return 1;
//use binary search to check how many nodes exist in the last level
//actually use the binary tree to target the right-most node at the last level
int left = 1, right = pow(2, d) - 1;
while(left <= right){
int pivot = (left + right) / 2;
//if the pivot exist, then we are confident to say all the left behind is exist
if(exists(pivot, root, d)){
left = pivot + 1;
}else{
//since complete tree, if pivot don't exist, then every node at the right
//side won't exist
right = pivot - 1;
}
}
return res + left;
}
};
- Runtime:O(log^2 N) + O(log^N) = O(log^N)
- Space: O(1)
- 满足小于 O(N) 的需求,而且会比 O(N) 快很多