在Java中计算树中的节点

Posted

技术标签:

【中文标题】在Java中计算树中的节点【英文标题】:Counting nodes in a tree in Java 【发布时间】:2009-02-13 20:50:39 【问题描述】:

首先,我发誓这不是作业,这是我在一次采访中被问到的问题。我想我把它弄得一团糟(尽管我确实意识到解决方案需要递归)。问题来了:

实现 count() 方法,该方法返回树中的节点数。如果一个节点既没有左孩子也没有右孩子,相关的getXXChild()方法将返回null

class Tree 

  Tree getRightChild() 
    // Assume this is already implemented
  

  Tree getLeftChild() 
    // Assume this is already implemented
  

  int count() 
    // Implement me
  

我问这个问题的原因只是想看看正确的解决方案,从而衡量我的问题有多糟糕。

干杯, 托尼

【问题讨论】:

// 不……你没说拜托 @Tony 你能发布你想出的任何解决方案吗?如果我们指出您的设计存在的问题,而不是仅仅把正确的答案扔在那里,这可能对您更有价值。 如果这是一个面试问题,面试官可能不仅希望看到“代码”(发布了许多很好的解决方案),而且还希望讨论递归与迭代(参见例如 David Hanak 的答案)和堆栈与堆相比。 int numberOfRightKids = getRightChild() != null ? getRightChild().count() : 0; int numberOfLeftKids = getLeftChild() != null ? getLeftChild().count() : 0 int me = 1;返回 numberOfLeftKids + numberOfRightKids + 我; Java 没有 Functor 接口?哈哈。 【参考方案1】:
int count() 
  Tree right = getRightChild();
  Tree left = getLeftChild();
  int c = 1;                                      // count yourself!
  if ( right != null ) c += right.count();        // count sub trees
  if ( left != null ) c += left.count();          // ..
  return c;

【讨论】:

将“返回计数”编辑为“返回 c”。 :-)【参考方案2】:

一个简单的递归解决方案:

int count() 
   Tree l = getLeftTree();
   Tree r = getRightTree();
   return 1 + (l != null ? l.count() : 0) + (r != null ? r.count() : 0);

一个不那么简单的非递归的:

int count() 
    Stack<Tree> s = new Stack<Tree>();
    s.push(this);
    int cnt = 0;
    while (!s.empty()) 
        Tree t = s.pop();
        cnt++;
        Tree ch = getLeftTree();
        if (ch != null) s.push(ch); 
        ch = getRightTree();
        if (ch != null) s.push(ch); 
    
    return cnt;

后者可能更节省内存,因为它用堆栈和迭代代替了递归。它也可能更快,但如果没有测量就很难判断。一个关键的区别是递归解决方案使用堆栈,而非递归解决方案使用堆来存储节点。

编辑:这是迭代解决方案的一个变体,它对堆栈的使用较少:

int count() 
    Tree t = this;
    Stack<Tree> s = new Stack<Tree>();
    int cnt = 0;
    do 
        cnt++;
        Tree l = t.getLeftTree();
        Tree r = t.getRightTree();
        if (l != null) 
            t = l;
            if (r != null) s.push(r);
         else if (r != null) 
            t = r;
         else 
            t = s.empty() ? null : s.pop();
        
     while (t != null);
    return cnt;

您是否需要更高效或更优雅的解决方案自然取决于您的树的大小以及您打算使用此例程的频率。记住 Hoare 所说的:“过早的优化是万恶之源。”

【讨论】:

这不会用 Java 编译。不幸的是,您需要显式测试某些东西是否 == null,因此您需要在此处使用 l== null 和 r == null。 谢谢!前段时间我积极使用Java。 如何允许将空值压入堆栈,但仅在弹出的对象不为空时才增加计数器。您可以通过这种方式删除 1 if 语句,然后您也可以执行 s.push(getLeft()) 和 s.push(getRight()) 我相信这在 CPU 上会稍微贵一些(额外的推送/弹出),但它绝对是一种选择,而且,就像你说的,比你说的少一条线。 :-) 我还以为是高德纳说的! :)【参考方案3】:

我更喜欢这个,因为它写着:

左返回计数 + 右计数 + 1

  int count() 
      return  countFor( getLeftChild() ) + countFor( getRightChild() ) + 1;
  
  private int countFor( Tree tree )   
       return tree == null ? 0 : tree.count();
  

更倾向于文学编程。

顺便说一句,我不喜欢 Java 上如此常用的 getter/setter 约定,我认为使用 leftChild() 会更好:

  return countFor( leftChild() ) + countFor( rightChild() ) + 1;

就像 Hoshua Bloch 在这里解释的那样 http://www.youtube.com/watch?v=aAb7hSCtvGw 至少。 32:03

如果你能正确理解你的代码...

但是,我不得不承认 get/set 约定现在几乎是语言的一部分。 :)

对于许多其他部分,遵循此策略会创建自记录代码,这是一件好事。

托尼:我想知道,你在采访中的回答是什么。

【讨论】:

-1 分反对 JavaBean 习语,这已被证明非常有用。 (反正我给你加分了!) @oxbow:是的,Java bean 模型从来没有达到预期的效果(即拥有真正独立的 bean),但这个成语非常有益,特别是在 web 开发中。【参考方案4】:
return (getRightChild() == null ? 0 : getRightChild.count()) + (getLeftChild() == null ? 0 : getLeftChild.count()) + 1;

或者类似的东西。

【讨论】:

在实践中我不会把它写成单行的,但事实上你可以说明简单性...... +1 为什么不呢?无法击败一点三元滥用:-) 如果你在采访中写到,我会很生气,没有留下深刻印象。 @Kip 如果我最终参加 Java 面试,我不会那么恼火 :-)【参考方案5】:

这样的事情应该可以工作:

int count()

    int left = getLeftChild() == null ? 0 : getLeftChild().count();
    int right = getRightChild() == null ? 0 : getRightCHild().count();

    return left + right + 1;

【讨论】:

记得自己数数:P Pfft 你们在说什么,我总是加 1 ;-) 谢谢! 谁说count应该返回null?问题说“相关方法”,所以它指的是“leftNode/rightNode”。这是你的观点还是遗漏了什么? 我也认为他的意思是如果没有子节点,count() 方法应该返回 null。直到二读才清楚。当他说“相关方法”时,我认为他的意思是与这个问题相关的方法。【参考方案6】:
class Tree 

  Tree getRightChild() 
    // Assume this is already implemented
  

  Tree getLeftChild() 
    // Assume this is already implemented
  

  int count() 
   return 1 
      + getRightChild() == null? 0 : getRightChild().count()
      + getLeftChild() == null? 0 : getLeftChild().count();
  

【讨论】:

【参考方案7】:

您可以通过遍历许多ways 来计算这棵树。简单的前序遍历,代码将是(基于您定义的函数):

int count() 
    count = 1;
    if (this.getLeftChild() != null)
        count += this.getLeftChild().count();
    if (this.getRightChild() != null)
        count += this.getRightChild().count();
    return count;

【讨论】:

【参考方案8】:

实现方法:

public static int countOneChild(Node root)

    ...

计算具有一个孩子的二叉树中内部节点的数量。将该函数添加到tree.java 程序中。

【讨论】:

【参考方案9】:

我是通过前序递归做到的。尽管使用 localRoot 并不完全遵循采访格式,但我想你明白了。

private int countNodes(Node<E> localRoot, int count) 
    if (localRoot == null) 
        return count;     
    count++; // Visit root
    count = countNodes(localRoot.left, count); // Preorder-traverse (left)
    count = countNodes(localRoot.right, count); // Preorder-traverse (right)
    return count;


public int countNodes() 
   return countNodes(root, 0);

【讨论】:

【参考方案10】:

这是一个标准的递归问题:

count():
    cnt = 1 // this node
    if (haveRight) cnt += right.count
    if (haveLeft)  cnt += left.count
return cnt;

非常低效,如果树很深,那就是一个杀手,但这对你来说是递归......

【讨论】:

但这已经是最有效的了,不需要缓存更高级别的计数,对吧? 算法很好,只是没有真正回答原始问题。另外,haveLeft 和 haveRight 虽然很明显,但并未在此处声明。【参考方案11】:
int count()


   int retval = 1;
    if(null != getRightChild()) retval+=getRightChild().count();
    if(null != getLeftChild()) retval+=getLeftChild().count();
    return retval;


上帝希望我没有犯错。

编辑:我确实做到了。

【讨论】:

你的 if 语句需要明确检查 == null,否则这不适用于 Java。 是的,记得我发帖的时候 :)【参考方案12】:

当然,如果您想在计数时避免访问树中的每个节点,并且处理时间对您来说比内存更有价值,您可以通过在构建树时创建计数来作弊。

    每个节点都有一个 int 计数, 初始化为一个,其中 表示节点数 以该节点为根的子树。

    插入节点时,在 从递归插入返回 例程,增加计数 当前节点。

public void insert(Node root, Node newNode) 
  if (newNode.compareTo(root) > 1) 
    if (root.right != null) 
      insert(root.right, newNode);
    else
      root.right = newNode;
   else 
    if (root.left != null)
      insert(root.left, newNode);
    else
      root.left = newNode;
  
  root.count++;

然后从任意点获取计数只需要查找 node.count

【讨论】:

【参考方案13】:

我的第一次尝试没有添加任何新内容,但后来我开始怀疑递归深度以及是否可以重新排列代码以利用最新 Java 编译器的尾调用优化功能。主要问题是空测试 - 可以使用 NullObject 解决。我不确定 TCO 是否可以同时处理这两个递归调用,但它至少应该优化最后一个。

static class NullNode extends Tree 

    private static final Tree s_instance = new NullNode();

    static Tree instance() 
        return s_instance;
    

    @Override
    Tree getRightChild()   
        return null;
      

    @Override
    Tree getLeftChild()   
        return null;
      

    int count()   
        return 0;
    


int count()       
    Tree right = getRightChild();      
    Tree left  = getLeftChild();      

    if ( right == null )  right = NullNode.instance(); 
    if ( left  == null )  left  = NullNode.instance(); 

    return 1 + right.count() + left.count();
   

NullNode 的精确实现取决于 Tree 中使用的实现 - 如果 Tree 使用 NullNode 而不是 null,那么子访问方法可能应该抛出 NullPointerException 而不是返回 null。无论如何,主要思想是使用 NullObject 以尝试从 TCO 中受益。

【讨论】:

您必须在返回之前对每个计数调用的结果进行更多处理(将它们添加到返回寄存器的当前值),所以我认为您在这里没有适当的尾调用。 :-( 白金天蓝色:感谢您的评论。这似乎是我的想法中的一个致命缺陷。如果 TCO 不能处理返回值的累积,那么我想我需要更好的优化:-(【参考方案14】:

与二叉树相关的问题应该在面试中被预料到。我会说在下一次面试之前花点时间通过this链接。解决了大约14个问题。您可以看看解决方案是如何完成的。这将使您了解将来如何解决二叉树的问题。

我知道您的问题是针对 count 方法的。这也在我提供的链接中实现

【讨论】:

【参考方案15】:
class Tree 

  Tree getRightChild() 
    // Assume this is already implemented
  

Tree getLeftChild() 
    // Assume this is already implemented
 

 int count() 
    if(this.getLeftChild() !=null && this.getRightChild()!=null) 
        return 1 + this.getLeftChild().count() + this.getRightChild().count();
    elseif(this.getLeftChild() !=null && this.getRightChild()==null)
        return 1 + this.getLeftChild().count();
    elseif(this.getLeftChild() ==null && this.getRightChild()!=null)
        return 1 + this.getRightChild().count();
    else return 1;//left & right sub trees are null ==> count the root node
  

【讨论】:

以上是关于在Java中计算树中的节点的主要内容,如果未能解决你的问题,请参考以下文章

计算具有给定高度的二叉树中的所有节点

已知树中非叶子节点的度数和数量,如何计算树中叶子节点的个数?

计算给定范围内 AVL 树中的节点数

计算avl树中节点的平衡因子

用于计算树中根的左侧节点数的函数[复制]

设计一个算法,计算出给定二叉树中任意2 个结点之间的最短路径。