Java每日编码问题#003

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

Given the root to a binary tree, implement serialize(root), which serializes the tree into a string, and deserialize(s), which deserializes the string back into the tree.

For example, given the following Node class

class Node:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

The following test should pass:

node = Node('root', Node('left', Node('left.left')), Node('right'))
assert deserialize(serialize(node)).left.left.val == 'left.left'

Strategy

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


好的,这显然不是在用Java编写的,因为示例代码和测试是用Python给出的。 我们应该做的第一件事是用Java重写上面的代码。

__init()__ in Python acts like -- but is not a direct analog of -- an object constructor in Java. So we should define a Java Node class with a constructor that takes a val (value) and two Nodes as children.

In the Python example, the child nodes default to None, which is again sort-of, kind-of, not really like a null value in Java. So we should allow left and/or right to be null.

Python passes an explicit self to any method defined for an object, unlike most other programming languages. This is why there's a self in the __init__() method (the first of four arguments), but there are only three arguments passed to create node in the test. In Java, this is passed implicitly to the methods of any object, so we can drop it in the Java version.

这个问题的症结在于要求字符串节点:: to串()Java上的方法节点转换节点到序列化的(串)对象的表示。 然后,该表示应能够直接转换为节点与节点 节点::from串()方法或类似的东西。 这两种方法分别是序列化和反序列化方法。

Code

Building the Node class

让我们开始构建这个准系统节点提示定义的类:

public class Node {

  private int val;
  private Node left;
  private Node right;

  public Node (int val, Node left, Node right) {

    this.val = val;
    this.left = left;
    this.right = right;

  }

}

这可以很容易地在实例化壳:

jshell> /open Node.java

jshell> Node n = new Node(3, null, null)
n ==> Node@f6c48ac

我认为,这是此类的最简单的实现,它满足提示的要求。 注意值必须具有某种类型,所以我将其整型以上。 我们可以使节点通用:

public class Node<T> {

  private T val;
  private Node<T> left;
  private Node<T> right;

  public Node (T val, Node<T> left, Node<T> right) {

    this.val = val;
    this.left = left;
    this.right = right;

  }

}

尽管现在我们必须明确声明存储在其中的数据类型节点:

jshell> /open Node.java

jshell> Node<Integer> n = new Node<>(3, null, null)
n ==> Node@14bf9759

由于每个Java类都是宾语,我们可以声明所有节点作为节点<宾语>如果这太重了。(但是,如果我们要这样做,则最好将值宾语andforgothegenerics.)

Anyway, back to the task at hand. Let's create a serialization method which we'll call toString() (the Java standard). By default, toString() called on an object returns the type of that object and its hash:

jshell> n.toString()
$13 ==> "Node@5f341870"

我们希望我们的序列化方法包括对节点这样就可以根据返回的内容进行重构串,如有必要。 在这里,我们遇到了麻烦。

Unless we restrict ourselves to some subset of objects (say numbers, characters, and strings) and write some code which can easily parse these objects from their String representations, it's going to be very difficult to serialize our Nodes in this manner.

尽管在提示中没有明确指出,但在测试中仅将字符串用作数据。 我认为如果我们限制自己仅使用字符串值s。 在这种情况下,我们可以再次重写节点:

public class Node {

  private String val;
  private Node left;
  private Node right;

  public Node (String val, Node left, Node right) {

    this.val = val;
    this.left = left;
    this.right = right;

  }

}

在示例中,我们可以轻松访问剩下(要么对)的孩子节点通过打电话剩下()要么对()方法。 我们也可以得到值与值()方法。 让我们添加这些:

public class Node {

  private String val;
  private Node left;
  private Node right;

  public Node (String val, Node left, Node right) {

    this.val = val;
    this.left = left;
    this.right = right;

  }

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

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

  public String val() { return this.val; }

}

如预期的那样壳...

jshell> Node n = new Node("3", null, new Node("right", null, null))
n ==> Node@10bdf5e5

jshell> n.left()
$19 ==> null

jshell> n.right()
$20 ==> Node@617faa95

jshell> n.val()
$21 ==> "3"

...but we still need those serialization / deserialization methods. One last thing to do before we add them: although Python is more flexible than Java in that you can provide the arguments to a method in any order using named parameters, in the test above, one of the nodes is created with only a single (left) child by ommitting the right child. Another is created with no children nodes at all. Let's add some alternative constructors with one and zero child Nodes to emulate this behaviour:

public class Node {

  private String val;
  private Node left;
  private Node right;

  public Node (String val, Node left, Node right) {
    this.val = val;
    this.left = left;
    this.right = right;
  }

  public Node (String val, Node left) {
    this.val = val;
    this.left = left;
    this.right = null;
  }

  public Node (String val) {
    this.val = val;
    this.left = null;
    this.right = null;
  }

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

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

  public String val() { return this.val; }

}

为了避免重复自己,我们还可以这样做:

public class Node {

  private String val;
  private Node left;
  private Node right;

  public Node (String val, Node left, Node right) {
    this.val = val;
    this.left = left;
    this.right = right;
  }

  public Node (String val, Node left) {
    this(val, left, null);
  }

  public Node (String val) {
    this(val, null, null);
  }

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

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

  public String val() { return this.val; }

}

现在,我们可以使用几乎完全相同的语法创建与示例中相同的节点:

jshell> /open Node.java

jshell> Node node = new Node("root", new Node("left", new Node("left.left")), new Node("right"))
node ==> Node@4e1d422d
Serialization

最后,我们可以创建序列化和反序列化方法。 让我们从序列化开始。 我们需要编写一种在其中编码有关给定所有信息的方法节点, 它的值以及其所有子代和他们的值s。 听起来很复杂。

在最简单的情况下,我们有一个节点没有孩子(哪里剩下和对都是空值)。 让我们先解决这个问题。 我们可以做类似的事情:

  public String toString() {

    StringBuilder sb = new StringBuilder("Node(");

    if (val == null) {
      sb.append("null");

    } else {
      sb.append("\"");
      sb.append(val);
      sb.append("\"");
    }

    if (this.left == null) {
      sb.append(", null");
    }

    if (this.right == null) {
      sb.append(", null");
    }

    sb.append(")");

    return sb.toString();

  }

请注意,我使用串Builder而不是串,因为当我们合并许多小文件时,它的性能更高串在一起。

在中创建对象时壳,它使用对象的toString()将其打印到终端的方法,让我们看看我们的方法如何在没有孩子的情况下工作节点:

jshell> /open Node.java

jshell> Node node = new Node("test", null, null);
node ==> Node("test", null, null)

看起来不错!现在,当节点已children,weneedtoperformthesestepsrecursively.Inplaceofoneofthe空值sabove,thereshouldbeanother节点(...)printed.Thisisaneasyfix--if剩下or对isnon-空值,wejustcall剩下.toString()or对.toString()withinthetoString()method:

  public String toString() {

    StringBuilder sb = new StringBuilder("Node(");

    if (val == null) {
      sb.append("null");

    } else {
      sb.append("\"");
      sb.append(val);
      sb.append("\"");
    }

    if (this.left == null) {
      sb.append(", null");

    } else {
      sb.append(", ");
      sb.append(this.left.toString());
    }

    if (this.right == null) {
      sb.append(", null");

    } else {
      sb.append(", ");
      sb.append(this.right.toString());
    }

    sb.append(")");

    return sb.toString();

  }

}

这是如何运作的?

jshell> /open Node.java

jshell> Node node = new Node("test", null, new Node("right", null, null));
node ==> Node("test", null, Node("right", null, null))

jshell> Node node = new Node("root", new Node("left", new Node("left.left")), new Node("right"))
node ==> Node("root", Node("left", Node("left.left", null, ... Node("right", null, null))

...正好! 因此,我们已经成功实现了序列化方法,toString()。 反序列化呢? 这有点复杂。

要反序列化,需要注意以下几点:

  • 所有值s是空值要么串s用引号引起来所有节点s是空值要么begin with 节点(节点s can have 0, 1,要么2 children

这很复杂。 序列化方法的设计目的是使输出易于阅读,而不会产生太多其他影响。 让我们看看是否可以对其进行调整,以使其更易于通过反序列化方法进行解析:

  public String toString() {

    StringBuilder sb = new StringBuilder("Node: ");

    if (val == null) {
      sb.append("null");

    } else {
      sb.append("\"");
      sb.append(val);
      sb.append("\"");
    }

    if (this.left == null) {
      sb.append("\n  null");

//    } else {
//      sb.append(", ");
//      sb.append(this.left.toString());
    }

    if (this.right == null) {
      sb.append("\n  null");

//    } else {
//      sb.append(", ");
//      sb.append(this.right.toString());
    }

    sb.append("\n");

    return sb.toString();

  }

现在,输出如下所示:

jshell> /open Node.java

jshell> Node node = new Node("test");
node ==> Node: "test"
  null
  null


jshell> System.out.print(node.toString())
Node: "test"
  null
  null

在此修订版中,值在一个节点:和剩下和对孩子们被印在值。 看起来像什么节点有孩子吗? 简单的方法行得通吗?

  public String toString() {

    StringBuilder sb = new StringBuilder("Node: ");

    if (val == null) {
      sb.append("null");

    } else {
      sb.append("\"");
      sb.append(val);
      sb.append("\"");
    }

    if (this.left == null) {
      sb.append("\n  null");

    } else {
      sb.append("\n  ");
      sb.append(this.left.toString());
    }

    if (this.right == null) {
      sb.append("\n  null");

    } else {
      sb.append("\n  ");
      sb.append(this.right.toString());
    }

    sb.append("\n");

    return sb.toString();

  }
jshell> /open Node.java

jshell> Node node = new Node("test", null, new Node("right"));
node ==> Node: "test"
  null
  Node: "right"
  null
  null



jshell> System.out.print(node.toString())
Node: "test"
  null
  Node: "right"
  null
  null


...哦,不完全是。 最后,我们有太多的空白行,并且对的孩子没有按预期缩进。 我们可以创建第二个toString()带有参数的方法缩进。 这可以是孩子的级别数节点 should be 缩进ed in the output.

经过一些调整后,我最终得到这样的结果:

  public String toString() {
    return toString(0);
  }

  public String toString (int indent) {

    String spacer = "  ";
    String bump = String.join("", Collections.nCopies(indent, spacer));

    StringBuilder sb = new StringBuilder(bump);
    sb.append("Node: ");

    bump = bump + spacer;

    if (val == null) {
      sb.append("null");

    } else {
      sb.append("\"");
      sb.append(val);
      sb.append("\"");
    }

    if (this.left == null) {
      sb.append("\n");
      sb.append(bump);
      sb.append("null");

    } else {
      sb.append("\n");
      sb.append(this.left.toString(indent+1));
    }

    if (this.right == null) {
      sb.append("\n");
      sb.append(bump);
      sb.append("null");

    } else {
      sb.append("\n");
      sb.append(this.right.toString(indent+1));
    }

    return sb.toString();

  }

...看起来像这样:

jshell> Node node = new Node("test", new Node("left"), new Node(null));
node ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> System.out.print(node.toString())
Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

这是稍微不同的序列化方法,有望使反序列化更加容易。 现在,要反序列化,我们从左到右工作。 最左边节点将永远是根节点。 如果我们向右移动两个空格并在该列上向上和向下查找,我们将(在顶部)找到剩下儿童节点,可以是空值或节点本身。 在底部,我们发现对儿童节点。 的值由节点持有的总是在节点:标记,并且始终是串用引号括起来,除非是空值。

Deserialization

为了反序列化上面的输出,我们需要从左到右解析节点树。 的值由一个持有节点出现在节点:标记及其子项(剩下和对)位于其下方。 让我们再次从最简单的示例开始节点没有孩子:

jshell> Node node = new Node("test");
node ==> Node: "test"
  null
  null

jshell> System.out.print(node.toString())
Node: "test"
  null
  null

首先,我们找到值通过简单地查看“节点:“。如果结果子字符串用引号引起来,我们将其删除。否则,子字符串必须为空值:

  public static Node fromString (String serialized) {

    String marker = "Node: ";
    int valStart = serialized.indexOf(marker) + marker.length();
    int valEnd   = serialized.indexOf("\n");

    String val = serialized.substring(valStart, valEnd);

    if (val.charAt(0) == '"')
      val = val.substring(1, val.length()-1);

    else
      val = null;

    System.out.println("val:");
    System.out.println(val);

    return null;

  }

这将打印:

jshell> /open Node.java

jshell> Node node = new Node("test", new Node("left"), new Node(null));
node ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> node.toString()
$84 ==> "Node: \"test\"\n  Node: \"left\"\n    null\n    null\n  Node: null\n    null\n    null"

jshell> Node.fromString(node.toString())
val:
test
$85 ==> null

所以值被正确解析! 接下来就是解析孩子。 如果他们是空值,我们添加一个空值儿童。 如果不是,我们递归地称为fromString()对他们的方法! 但是它们可能离树很远(剩下 might have 17 generations of children and grandchildren, for instance). 所以how do we find 对?

我们知道在序列化表示中它恰好缩进了两个空格,因此我们可以将串分成几行,并找出仅两行应在A之前的两个空格处缩进空值或一个节点:出现。

或者,由于第一行之后的所有行都以\ n后面跟一些空格,我们可以替换的所有实例\ n再加上两个空格\ n,然后删除第一行,然后查找这两行别以任何空格开头。

  public static Node fromString (String serialized) {

    String marker = "Node: ";
    int valStart = serialized.indexOf(marker) + marker.length();
    int valEnd   = serialized.indexOf("\n");

    String val = serialized.substring(valStart, valEnd);

    if (val.charAt(0) == '"')
      val = val.substring(1, val.length()-1);
    else val = null;

    String modified = serialized.replaceAll("\n  ", "\n");
    modified = modified.substring(valEnd+1);

    System.out.println(modified);

    return null;
  }

上面的结果是“减少”整个序列化输出的两个空格:

jshell> Node node = new Node("test", new Node("left"), new Node(null));
node ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> System.out.print(node.toString())
Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null
jshell> Node.fromString(node.toString())
Node: "left"
  null
  null
Node: null
  null
  null
$102 ==> null

现在,从第一行开始,剩下节点,andfromthefirstnon-indentedlineafterthefirst,wehavethe对节点.Weneedtohandle空值nodes,asthecodewe'vewrittennowwouldthrowanerrorforthem:

  public static Node fromString (String serialized) {

    String marker = "Node: ";
    int valStart = serialized.indexOf(marker);

    if (valStart < 0) return null; 

    valStart += marker.length();
    int valEnd = serialized.indexOf("\n");

    String val = serialized.substring(valStart, valEnd);

    if (val.charAt(0) == '"')
      val = val.substring(1, val.length()-1);
    else val = null;

    String modified = serialized.replaceAll("\n  ", "\n");
    modified = modified.substring(valEnd+1);

    System.out.println(modified);

    return null;
  }

...然后我们需要找到两条非缩进的行,以便我们可以在剩下和对儿童节点s。 非缩进的行将是空值或一个节点,因此它将是唯一的实例\ ñ换行符后紧跟一个ñ or añ ñ:

jshell> /open Node.java

jshell> Node node = new Node("test", new Node("left"), new Node(null));
node ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> System.out.print(node.toString())
Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null
jshell> Node.fromString(node.toString())
Node: "left"
  null
  null
Node: null
  null
  null
27
$110 ==> null

...所以对从字符索引开始27。 最后,我们可以将孩子们分成对和剩下 nodes和run this process on them again, recursively (comments added, as well):

  public static Node fromString (String serialized) {

    // is this a non-null Node?
    String marker = "Node: ";
    int valStart = serialized.indexOf(marker);

    // if null, return a null Node
    if (valStart < 0) return null;

    // otherwise, get the `val` of the Node
    valStart += marker.length();
    int valEnd = serialized.indexOf("\n");
    String val = serialized.substring(valStart, valEnd);

    // is `val` null?
    if (val.charAt(0) == '"')
      val = val.substring(1, val.length()-1);
    else val = null;

    // de-dent the serialized representation and look for children
    String modified = serialized.replaceAll("\n  ", "\n");
    modified = modified.substring(valEnd+1);

    // at what character does the `right` child start?
    int rightStart = Math.max(
        modified.indexOf("\nN"),
        modified.indexOf("\nn")
      ) + 1;

    // child node `left`
    Node left = null;

    // if `left` is not `null`
    if (modified.substring(0, 4) != "null") 
      left = fromString(modified.substring(0, rightStart));

    // child node `right`
    Node right = null;

    // if `right` is not `null`
    if (modified.substring(rightStart, rightStart+4) != "null")
      right = fromString(modified.substring(rightStart));

    return new Node(val, left, right);
  }

它在这里运行壳:

jshell> /open Node.java

jshell> Node node = new Node("test", new Node("left"), new Node(null));
node ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> System.out.print(node.toString())
Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null
jshell> Node copy = Node.fromString(node.toString())
copy ==> Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

jshell> System.out.print(copy.toString())
Node: "test"
  Node: "left"
    null
    null
  Node: null
    null
    null

让我们检查提示中给出的测试示例:

jshell> Node node = new Node("root", new Node("left", new Node("left.left")), new Node("right"))
node ==> Node: "root"
  Node: "left"
    Node: "left.left" ...  "right"
    null
    null

jshell> Node.fromString(node.toString()).left().left().val()
$129 ==> "left.left"

jshell> Node.fromString(node.toString()).left().left().val().equals("left.left")
$130 ==> true

它按预期工作!

Discussion

这比我最初想象的要花费更长的时间。 我第一次尝试序列化导致了-我认为应该是-非常复杂的反序列化。 因此,我重构为外观不太雅致的序列化,该序列化更容易进行反序列化。

我的序列化和反序列化方法都依赖于递归来完成工作,我认为这是实现此目标的最佳方法。 (您无法提前知道节点树的深度。)

The deserialization method might not be optimal, as its written in such a way that lots of data needs to be held on the stack. It's not tail call optimizable as written. I think, ideally, we would want to find the most deeply-nested Nodes first, create them, and move up the tree, rather than moving from the top-down. It's not immediately obvious to me how we would go about doing that.

一世've written nodetrees in the past with much nicer-looking toString() implementations, though they're not serialized, as all of the data is not contained within the String representation. This is because these nodetrees often allow data which is not strictly String in nature.

最后要注意的一件事是-因为我正在使用\ n分离节点,如果\ n出现在值在任何地方都可能存在问题。 应清理输入内容或添加特殊例外,以便\ n在一个值不会破坏反序列化。


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

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

from: https://dev.to//awwsmm/java-daily-coding-problem-003-59ef

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值