关于二叉树的一些有趣问题
leetcode341扁平化嵌套列表迭代器
不难发现:
class NestedInteger {
Integer val;
List<NestedInteger> list;
}
/* 基本的 N 叉树节点 */
class TreeNode {
int val;
TreeNode[] children;
}
所以其实输入的就是一个类似于n叉树的结构。所要求的结果就是叶子结点的遍历结果。那么现在这个数据结构里面有两种形式,一种是整数型,另外一种是列表型。我们需要找到所有的整数型并存储输出。代码实际上就是实现n叉树的遍历。
代码实现
class NestedIterator implements Iterator<Integer> {
private Iterator<Integer> it;
public NestedIterator(List<NestedInteger> nestedList) {
// 存放将 nestedList 打平的结果
List<Integer> result = new LinkedList<>();
for (NestedInteger node : nestedList) {
// 以每个节点为根遍历
traverse(node, result);
}
// 得到 result 列表的迭代器
this.it = result.iterator();
}
public Integer next() {
return it.next();
}
public boolean hasNext() {
return it.hasNext();
}
// 遍历以 root 为根的多叉树,将叶子节点的值加入 result 列表
private void traverse(NestedInteger root, List<Integer> result) {
if (root.isInteger()) {
// 到达叶子节点
result.add(root.getInteger());
return;
}
// 遍历框架
for (NestedInteger child : root.getList()) {
traverse(child, result);
}
}
}
代码优化
上面的思路中,一次性算出了所有叶子节点的值,全部装到result
列表,也就是内存中,next
和hasNext
方法只是在对result
列表做迭代。如果输入的规模非常大,构造函数中的计算就会很慢,而且很占用内存。为了满足迭代器的求值为惰性的,可以调用hasNext
时,如果nestedList
的第一个元素是列表类型,则不断展开这个元素,直到第一个元素是整数类型。由于调用next
方法之前一定会调用hasNext
方法,这就可以保证每次调用next
方法的时候第一个元素是整数型,直接返回并删除第一个元素即可。
public class NestedIterator implements Iterator<Integer> {
private LinkedList<NestedInteger> list;
public NestedIterator(List<NestedInteger> nestedList) {
// 不直接用 nestedList 的引用,是因为不能确定它的底层实现
// 必须保证是 LinkedList,否则下面的 addFirst 会很低效
list = new LinkedList<>(nestedList);
}
public Integer next() {
// hasNext 方法保证了第一个元素一定是整数类型
return list.remove(0).getInteger();
}
public boolean hasNext() {
// 循环拆分列表元素,直到列表第一个元素是整数类型
while (!list.isEmpty() && !list.get(0).isInteger()) {
// 当列表开头第一个元素是列表类型时,进入循环
List<NestedInteger> first = list.remove(0).getList();
// 将第一个列表打平并按顺序添加到开头
for (int i = first.size() - 1; i >= 0; i--) {
list.addFirst(first.get(i));
}
}
return !list.isEmpty();
}
}
这道题对于迭代器设计模式和n叉树的数据结构有很好的启发性,所以建议掌握并熟练应用。特此记录~
leetcode236二叉树的最近公共祖先
这个问题其实在git中的分支合并有所应用,在这里就不详细展开,专注问题本身。
只要是二叉树问题,就离不开三大遍历方式,所以可以先将整体框架搭建出来。
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
}
然后通过已经定义的函数思考三个问题。
1、这个函数是干嘛的?
2、这个函数参数中的变量是什么的是什么?
3、得到函数的递归结果,你应该干什么?
解答:
1、输入p和
q节点,并且它们都在以
root为根的树中,函数返回的即使是
p和
q的最近公共祖先节点
2、函数参数中的变量是root
,因为根据框架,lowestCommonAncestor(root)
会递归调用root.left
和root.right
;至于p
和q
,我们要求它俩的公共祖先,它俩肯定不会变化的。
3、得到函数结果之后,我们就需要开始分情况讨论了。(base case)如果root节点为空,自然返回null,如果root
本身就是p
或者q
,比如说root
就是p
节点吧,如果q
存在于以root
为根的树中,显然root
就是最近公共祖先。
接着可以补全刚开始写的整体框架了。
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 两种情况的 base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
}
然后加入普适性后序遍历的代码即可完成题目,至于为什么是后续遍历也非常好想。好比两个子节点先走完再走根节点,自然第一时间的时候相遇就是最近公共祖先。
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 情况 1
if (left != null && right != null) {
return root;
}
// 情况 2
if (left == null && right == null) {
return null;
}
// 情况 3
return left == null ? right : left;
}
情况 1,如果p
和q
都在以root
为根的树中,那么left
和right
一定分别是p
和q
(从 base case 看出来的)。
情况 2,如果p
和q
都不在以root
为根的树中,直接返回null
。
情况 3,如果p
和q
只有一个存在于root
为根的树中,函数返回该节点。
这里三种情况在想函数定义时实际上就应该想得比较全面了,加深理解,注意掌握。
leetcode222完全二叉树的节点个数
遍历实现
class Solution {
public int countNodes(TreeNode root) {
// base case
if (root == null) return 0;
int left = countNodes(root.left);
int right = countNodes(root.right);
return 1 + left + right;
}
}
拓展:满二叉树
public int countNodes(TreeNode root) {
int h = 0;
// 计算树的高度
while (root != null) {
root = root.left;
h++;
}
// 节点总数就是 2^h - 1
return (int)Math.pow(2, h) - 1;
}
代码改进
由于完全二叉树是介于满二叉树和普通二叉树之间的一种,所以可以结合两种节点计算方式来简化时间复杂度。
public int countNodes(TreeNode root) {
TreeNode l = root, r = root;
// 记录左、右子树的高度
int hl = 0, hr = 0;
while (l != null) {
l = l.left;
hl++;
}
while (r != null) {
r = r.right;
hr++;
}
// 如果左右子树的高度相同,则是一棵满二叉树,注意前面求右子树是一直取的最右边的节点,所以才可以判断为满二叉树
// 不然单是根据左右子树高度相同是无法得出是满二叉树的结论的
if (hl == hr) {
return (int)Math.pow(2, hl) - 1;
}
// 如果左右高度不同,则按照普通二叉树的逻辑计算
return 1 + countNodes(root.left) + countNodes(root.right);
}
复杂度分析
注意:一棵完全二叉树的两棵子树,至少有一棵是满二叉树。所以虽然有两个递归,即左右子树,但是有一个是通过if直接返回的,不会进入递归。所以算法的递归深度就是树的高度 O(logN),每次递归所花费的时间就是 while 循环,需要 O(logN),所以总体的时间复杂度是 O(logN*logN)。