Java每日编码问题#008

daily Coding Problem is a website which will send you a programming challenge to your inbox every day. I want to show beginners how to solve some of these problems using Java, so this will be an ongoing series of my solutions. Feel free to pick them apart in the comments!

Problem

A unival tree (which stands for "universal value") is a tree where all nodes under it have the same value.

Given the root to a binary tree, count the number of unival subtrees.

For example, the following tree has 5 unival subtrees:

   0
  / \
 1   0
    / \
   1   0
  / \
 1   1

Strategy

剧透!除非您想查看我的解决方案,否则请不要在下面查看!


Jargon: A binary tree is a tree data structure made of nodes, where each node can have 0, 1, or 2 child nodes. Child nodes are connected to their parent nodes by edges. A child node only ever has a single parent node. When traversing the tree by travelling along its edges, a parent node is always closer to the root node than its child nodes (by 1 edge).

If a parent node has two child nodes, they are ordered and referred to as the left child and the right child. A node's children and its children's children, etc. are that node's descendants, while its parent and its parent's parent, etc. are that node's ancestors.

Left and right nodes can be referred to as sibling nodes. Nodes with no children are leaves. Nodes with no parents are root nodes (or, rarely, orphan nodes). A subtree is a portion of a larger binary tree, which comprises a particular node of the original tree and all of that node's descendants. Note that a subtree is itself a binary tree, so all of the above nomenclature can be applied to it, as well.

问题陈述说示例树有五个单子树。 所以它一定在数树叶 (nodes with no children) as unival subtrees. The first thing we can do, then, is simply get the number of 树叶 in the tree. The number of 树叶 puts a lower bound on the number of unival subtrees.

我们需要做的下一件事是查看不是叶子的节点并对其进行标记Integer unival = <0 or 1>如果它们是一元子树的根节点,并且-1除此以外。 我们需要四个状态-(1)未确定的单亲状态,(2)已确定和0-单值,(3)已确定和1-单值,(4)已确定且不是单值。

因此,我们首先构建一个二叉树,其中每个节点都有一个属性一元的并将所有节点初始化为一元的 = null(状态不确定)。 然后,我们可以编写一个循环,该循环从树的根节点开始,并执行以下操作:

  1. 检查是否这个是空值 for this node. 如果这个是空值对于此节点,我们尚未确定此节点的子树的单亲状态; 转到(2)。 如果这个是not 空值 for this node, attempt to move to this node's parent. 如果this node has a parent, refocus on the parent node, then go to the start of step (1). 如果this node doesn't have a parent, we're done! 如果this node has no parent, then it是the root node of the entire binary tree. 如果we know its unival status, then we must know the unival status of every one of its descendant nodes. To check if a subtree是unival, we must check the unival status of its root node's children (of which there can be 0, 1, or 2 in a binary tree. 如果this node has 0 children, this subtree是unival. Set 这个 = 此节点的值,然后转到(1b)。 如果this node has 1 child, check the unival status of that child 如果the child's unival status是non-空值,通过检查其价值和孩子的价值来设置父母的单身状态,然后转到步骤(1b) 如果the child's unival status是空值,将焦点重新放在该子节点上,然后转到步骤(1a)。 如果this subtree's root node has 2 children, check the unival status of the left child node 如果the left node's unival status是non-空值, check the unival status of the right node 如果the right node's unival status是non-空值,我们可以从这两个节点的状态确定其父级的单亲状态。 进行设置,然后移至步骤(1b)。 如果the right node's unival status是空值,请重新关注该节点,然后转到步骤(1a)。 如果the left node's unival status是空值,请重新关注该节点,然后转到步骤(1a)。

因此,我们从树的根节点开始,然后沿着树的左侧向下移动,直到到达叶子。 然后,我们从左到右,从下到上“填充”树,直到最后回到根节点。 到那时,我们已经拥有了所有一元的状态设置为-1个,0,or 1个,and we can simply count the number of 一元的 > -1个 nodes to get the total number of 一元的 subtrees in the tree.

Code

Before we do anything with unival subtrees, we need a binary tree data structure. It's not difficult to code one of these from scratch, so let's do that. First, we have a generic 乙inaryNode class (using a 乙uilder pattern):

package DCP;

public class BinaryNode {

  public static class Builder {

    private Integer    value  = null;
    private BinaryNode left   = null;
    private BinaryNode right  = null;

    private BinaryNode parent         = null;
    private Boolean    parentHasLeft  = null;
    private Boolean    parentHasRight = null;

    public Builder (Integer value) {
      this.value = value;
    }

    public Builder left (BinaryNode left) {

      if (left.parent() != null)
        throw new IllegalStateException(
          "ERROR: left node already has a parent");

      this.left = left;
      return this;
    }

    public Builder right (BinaryNode right) {

      if (right.parent() != null)
        throw new IllegalStateException(
          "ERROR: right node already has a parent");

      this.right = right;
      return this;
    }

    public Builder parent (BinaryNode parent) {

      if (parent == null) return this;

      this.parentHasLeft  = (parent.left()  != null);
      this.parentHasRight = (parent.right() != null);

      if (this.parentHasLeft && this.parentHasRight)
        throw new IllegalStateException(
          "ERROR: parent already has two children");

      this.parent = parent;
      return this;
    }

    public BinaryNode build() {

      BinaryNode node = new BinaryNode(this.value);

      node.left   = this.left;
      node.right  = this.right;
      node.parent = this.parent;

      if (this.left  != null) this.left.parent(node);
      if (this.right != null) this.right.parent(node);

      if (this.parent != null) {
        if (this.parentHasLeft) this.parent.right(node);
        else                    this.parent.left(node);
      }

      return node;
    }
  }

  private Integer    value    = null;
  private BinaryNode parent   = null;
  private BinaryNode left     = null;
  private BinaryNode right    = null;

  private BinaryNode (Integer value) {
    if (value < 0 || value > 1)
      throw new IllegalArgumentException(
        "this is a binary tree... `value` can only be 0 or 1");
    this.value = value;
  }

  public Integer value() {
    return this.value;
  }

  public BinaryNode parent() {
    return this.parent;
  }

  private void parent (BinaryNode parent) {
    this.parent = parent;
  }

  public BinaryNode left() {
    return this.left;
  }

  private void left (BinaryNode left) {
    this.left = left;
  }

  public BinaryNode right() {
    return this.right;
  }

  private void right (BinaryNode right) {
    this.right = right;
  }

...然后我们只需要添加一些点就可以获取并设置节点的单足状态:

...
  private Integer    unival   = null;
...
  public Integer unival() {
    return this.unival;
  }

  public void unival (Integer unival) {
    this.unival = unival;
  }
...

我还添加了一种不错的(但并非绝对必要的)方法来打印树的ASCII表示形式(值或单精度状态):

  public void valueTree() { tree(false, 10, 10, "", false); }

  public void univalTree() { tree(true, 10, 10, "", false); }

  // some code here based on http://bit.ly/2HgFBRo
  private void tree (boolean unival, int nLevelsUp, int nLevelsDown, String indent, boolean isTail) {

    // start with any node on the tree
    BinaryNode node = this;

    // find eldest allowed node using nLevelsUp
    for (int ii = 0; ii < nLevelsUp; ++ii) {
      if (node.parent != null) {
        node = node.parent;
        ++nLevelsDown;
      } else {
        --nLevelsUp;
      }
    }

    // get number of ancestors of this node
    BinaryNode ptr = this;
    int gen = 0;

    while (ptr.parent() != null) {
      ++gen; ptr = ptr.parent();
    }

    Integer treeValue = unival ? node.unival : node.value;
    String treeLabel = (treeValue == null ? "null" : treeValue.toString());

    System.out.printf("  %3d %s|-- %s%n", gen, indent, treeLabel);

    int nChildren = (this.left == null ? 0 : 1) + (this.right == null ? 0 : 1);
    BinaryNode lastChild = (nChildren > 1 ? this.right : this.left);

    if (nLevelsDown > 0) {
      if (nChildren > 1)
        this.left.tree(unival, 0, nLevelsDown-1, indent + (isTail ? "    " : "|   "), false);

      if (nChildren > 0)
        lastChild.tree(unival, 0, nLevelsDown-1, indent + (isTail ? "    " : "|   "), true);
    }

    return;
  }

因此,我们可以创建具有0、1或2个子节点的节点。 这是一个示例,说明如何创建一个小的二叉树并检查其属性:

$ jshell -cp target/008-1.0-SNAPSHOT.jar 
|  Welcome to JShell -- Version 11.0.2
|  For an introduction type: /help intro

jshell> import DCP.BinaryNode

jshell> (new BinaryNode.Builder(1)).build()
$2 ==> DCP.BinaryNode@210366b4

jshell> (new BinaryNode.Builder(0).parent($2)).build()
$3 ==> DCP.BinaryNode@2b2948e2

jshell> (new BinaryNode.Builder(1).parent($2)).build()
$4 ==> DCP.BinaryNode@57536d79

jshell> (new BinaryNode.Builder(1).parent($4)).build()
$5 ==> DCP.BinaryNode@5a8e6209

jshell> $2.valueTree()
    0 |-- 1
    1 |   |-- 0
    1 |   |-- 1
    2 |       |-- 1

大! 现在,我们只需要考虑在问题的背景下如何使用这些方法。 让我们采用上面定义的过程并替换行动与代码段。 下面,这个是对我们当前关注的节点的引用,该节点在整个过程中都会发生变化。 当我在下面说“重新关注X节点”时,我的意思是这个现在引用该节点:

  1. (step 1) 如果(this.unival == 空值)前往(2) 否则父= this.父母() 如果(父项!= 空值)重新关注父母然后返回到步骤(1)的开始 否则返回//我们就完成了! (step 2) if (this.left == 空值 && this.right == 空值) this.unival(this.value)并转到(1b) 否则if(this.left!= 空值 ^ this.right!= 空值)移到那个非空值子节点,然后转到步骤(1)的开始。 其他 if (this.left != 空值) if (this.right != 空值) this.unival(...)根据左右子节点的值设置该节点的单例状态,然后转到步骤(1b) 其他重新关注the right node, then move back to the start of step (2). 其他重新关注the left node, then move back to the start of step (2).

这样行吗? 这是我编写的用于测试该算法的脚本:

// Algo.java

import DCP.BinaryNode;

BinaryNode root  = (new BinaryNode.Builder(1)).build();

BinaryNode nodeA = (new BinaryNode.Builder(0).parent(root)).build();
BinaryNode nodeB = (new BinaryNode.Builder(0).parent(nodeA)).build();
BinaryNode nodeC = (new BinaryNode.Builder(1).parent(nodeA)).build();
BinaryNode nodeD = (new BinaryNode.Builder(1).parent(root)).build();
BinaryNode nodeE = (new BinaryNode.Builder(1).parent(nodeD)).build();
BinaryNode nodeF = (new BinaryNode.Builder(1).parent(nodeD)).build();

root.valueTree();

BinaryNode thisnode = root;

while (true) {

  // (1a)
  if (thisnode.unival() == null) {

    // (2a)
    if (thisnode.left() == null && thisnode.right() == null) {
      thisnode.unival(thisnode.value());

    // (2b)
    } else if (thisnode.left() != null ^ thisnode.right() != null) {
      thisnode = (thisnode.left() == null ? thisnode.right() : thisnode.left());

    // (2c)
    } else {
      if (thisnode.left() != null) {

        if (thisnode.right() != null)
          thisnode.unival((
            thisnode.left().value() == thisnode.right().value()
              ? thisnode.left().value()
              : -1
          ));

        else thisnode = thisnode.right();

      } else thisnode = thisnode.left();
    }

  // (1b)
  } else {
    BinaryNode parent = thisnode.parent();

    // (1bi)
    if (parent != null) thisnode = parent;
    else break;
  }

}

System.out.println();
root.univalTree();

...让我们在壳:

$ jshell -cp target/008-1.0-SNAPSHOT.jar 
|  Welcome to JShell -- Version 11.0.2
|  For an introduction type: /help intro

jshell> /open Algo.java
    0 |-- 1
    1 |   |-- 0
    2 |   |   |-- 0
    2 |   |   |-- 0
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

    0 |-- -1
    1 |   |-- null
    2 |   |   |-- null
    2 |   |   |-- null
    1 |   |-- null
    2 |       |-- null
    2 |       |-- null

不完全的! 为什么会这样呢?

问题出在我们定义的算法的步骤(2c)或以下几行中:

...
      if (thisnode.left() != null) {

        if (thisnode.right() != null)
          thisnode.unival((
            thisnode.left().value() == thisnode.right().value()
              ? thisnode.left().value()
              : -1
          ));
...

此代码检查这个节点有一个非空值左右孩子,但是不检查单身状态这些节点中的一个已经确定! 结果是,我们确定根节点有两个子节点,然后设置根节点的单亲状态,然后退出,而不会更新任何其他节点的单亲状态。

这是一个快速修复,我们要做的就是将上述行更改为:

...
      if (thisnode.left() != null && thisnode.left().unival() != null) {

        if (thisnode.right() != null && thisnode.right().unival() != null)
          thisnode.unival((
            thisnode.left().value() == thisnode.right().value()
              ? thisnode.left().value()
              : -1
          ));
...

让我们再试一次!

jshell> /open Algo.java
    0 |-- 1
    1 |   |-- 0
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

    0 |-- -1
    1 |   |-- -1
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

看起来不错! 让我们尝试另一棵树:

jshell> /open Algo.java
    0 |-- 1
    1 |   |-- 0
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 0

    0 |-- -1
    1 |   |-- -1
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- -1
    2 |       |-- 1
    2 |       |-- 0

它似乎行为正常。 最后一步是计算树的单节点状态为的节点数0要么1个。 我们可以通过遍历树并为每个非负节点增加一些全局计数器来实现。 就像是:

public int countUnivalSubtrees (BinaryNode node) {

  boolean hasLeft  = (node.left() != null);
  boolean hasRight = (node.right() != null);

  int sumLeft  = hasLeft  ? countUnivalSubtrees(node.left()) : 0;
  int sumRight = hasRight ? countUnivalSubtrees(node.right()) : 0;

  return((node.unival() >= 0 ? 1 : 0) + sumLeft + sumRight);
}

System.out.println("\nNumber of unival subtrees: " + countUnivalSubtrees(root));

这个几乎可以,但是效果不佳。 你知道为什么吗?

也许一个例子会有所帮助:

jshell> /open Algo.java
    0 |-- 0
    1 |   |-- 0
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- 0
    2 |       |-- 0
    2 |       |-- 1

    0 |-- 0
    1 |   |-- -1
    2 |   |   |-- 0
    2 |   |   |-- 1
    1 |   |-- -1
    2 |       |-- 0
    2 |       |-- 1

Number of unival subtrees: 5

当一个节点有两个孩子,并且我们检查它们的单亲状态时,我们仅检查它们是否相等,而不检查它们是否有效:

            thisnode.left().value() == thisnode.right().value()
              ? thisnode.left().value()
              : -1

我们需要检查它们是否有效(不等于-1):

            thisnode.left().value() == thisnode.right().value() &&
            thisnode.left().unival() >= 0
              ? thisnode.left().value()
              : -1

这样行吗?

jshell> /open Algo.java
    0 |-- 0
    1 |   |-- 1
    2 |   |   |-- 1
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

    0 |-- 1
    1 |   |-- 1
    2 |   |   |-- 1
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

Number of unival subtrees: 7

几乎! 我们只需要检查子节点的子树的单例状态是否等于该节点的值即可:

            thisnode.left().value() == thisnode.right().value() &&
            thisnode.left().unival() >= 0  &&
            thisnode.left().value() == thisnode.value()
              ? thisnode.left().value()
              : -1

瞧!

jshell> /open Algo.java
    0 |-- 0
    1 |   |-- 1
    2 |   |   |-- 1
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

    0 |-- -1
    1 |   |-- 1
    2 |   |   |-- 1
    2 |   |   |-- 1
    1 |   |-- 1
    2 |       |-- 1
    2 |       |-- 1

Number of unival subtrees: 6

Discussion

最后,我们可以计算给定树中的无节子树的数量。 让我们在提示中给出的示例树上再尝试一次:

jshell> /open Algo.java
    0 |-- 0
    1 |   |-- 0
    1 |   |-- 1
    2 |       |-- 0
    2 |       |-- 1
    3 |           |-- 1
    3 |           |-- 1

    0 |-- -1
    1 |   |-- 0
    1 |   |-- -1
    2 |       |-- 0
    2 |       |-- 1
    3 |           |-- 1
    3 |           |-- 1

Number of unival subtrees: 5

看起来不错! 并且其值与提示中的值相同。 在本文的早期版本中,我使用了布尔值的价值一元的 status, but quickly realised that there are more than two states (一元的 vs not 一元的, for instance). This necessitated a wider data type.

在整个问题解决方案中,我也做了一些小疏忽,有一点令人困惑值和一元的。 通过实施二叉树并实际解决该问题,此“每日编码问题”需要一些工作,但最终我们得到了一个不错的解决方案。

This solution could likely be optimised a bit, but that's a problem for another day.


All the code for my Daily Coding Problems solutions is available at github.com/awwsmm/daily.

有什么建议吗? 在评论中让我知道。

If you enjoyed this post, please consider supporting my work by buying me a coffee!

from: https://dev.to//awwsmm/java-daily-coding-problem-008-1dk4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值