【问题标题】:Recursion of Level Order Traversal of N-ary treeN叉树的层序遍历的递归
【发布时间】:2021-09-07 07:01:08
【问题描述】:

我目前正在研究 N-ary 树,我偶然发现了 Level Order Traversal。理论上它看起来很容易,在代码上运行并不难,但现在我想加强它并添加递归,这样我就可以更好地理解它。事情是我现在发现这样做非常困难。有我的代码:

- The node class

    

import java.util.ArrayList;
import java.util.List;

/**
 * Implementation of a generic tree node containing the data and a list of children.
 *
 * @param <T> Generic type meant to implement reference types into the tree.
 */
public class Node<T> {
  private T data;
  private List<Node<T>> children;

  /**
   * Constructor that initializes the data and the list of children of the current node.
   *
   * @param data The value of the node.
   */
  public Node(T data) {
    this.data = data;
    children = new ArrayList<>();
  }

  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }

  public List<Node<T>> getChildren() {
    return children;
  }

  public void setChildren(List<Node<T>> children) {
    this.children = children;
  }
}






-The tree class


import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/** Implementation of a generic n-ary tree. */
public class Tree<T> {
  private Node root;
  private List<Node<T>> nodes;
  /**
   * Constructor that initializes the root node of the tree.
   *
   * @param data The value of the root node.
   */
  public Tree(T data) {
    root = new Node<>(data);
  }

  public Node getRoot() {
    return root;
  }

  /**
   * Method that implements the Level Order Traversal algorithm. It's a left to right traverse where
   * each level of the tree is being printed out. First the root , then it's children and then each
   * child's children etc.
   *
   * @param root The root node of a tree.
   */
  public String levelOrderTraversal(Node root) {
    StringBuilder result = new StringBuilder();
    if (root == null) {
      return "";
    }
    result.append("\n");
    Queue<Node> q = new LinkedList<>();
    q.add(root);
    while (!q.isEmpty()) {
      int queueSize = q.size();
      while (queueSize > 0) {
        Node node = q.peek();
        q.remove();
        result.append(node.getData().toString()).append(" ");
        for (int i = 0; i < node.getChildren().size(); i++) {
          q.add((Node) node.getChildren().get(i));
        }
        queueSize--;
      }
      result.append("\n");
    }
    return result.toString();
  }

  /**
   * This method serves to recursively move through and retrieve the nodes, so they can be printed
   * out to the user.
   *
   * @param root The root node of the tree.
   */
  private void walkThroughElements(Node root) {
    if (root == null) {
      return;
    }
    nodes.add(root);
    for (Object node : root.getChildren()) {
      walkThroughElements((Node) node);
    }
  }
  /**
   * Implementation of pre-order traversal of a generic tree. This traversal visit the root node
   * first , prints it , then visits the whole left sub-tree (the list of every child node), prints
   * every node and then traverses the right sub-tree , prints the nodes and ends the algorithm.
   *
   * @param root The root node of the tree.
   * @return The nodes of the tree as a string.
   */
  private String preOrderTraversal(Node<T> root) {
    nodes = new ArrayList<>();
    StringBuilder result = new StringBuilder();
    walkThroughElements(root);
    for (Node node : nodes) {
      result.append(node.getData()).append(" ");
    }
    result.setLength(result.length() - 1);
    return result.toString();
  }

  public String preOrderTraversal() {
    return preOrderTraversal(root);
  }
}

有没有一种有效的方法,或者递归地运行这个级别顺序遍历方法是否有意义?

这是修改后的关卡顺序代码

    public String levelOrderTraversal(Node root) {
    StringBuilder result = new StringBuilder();
    if (root == null) {
      return "";
    }
    result.append("\n");
    Queue<Node> q = new LinkedList<>();
    q.add(root);
    collectNodes(root, root.getChildren());
    result.append(root.getData().toString()).append(" ");
    result.append("\n");
    return result.toString();
  }

它在调用 collectNodes 的那一行给出错误。

这就是 collectNodes() 的样子。

    private void collectNodes(Node<T> node, List<Node<T>> nodes) {
    nodes.add(node);
    for (Object child : node.getChildren()) {
      collectNodes((Node) child, nodes);
    }
  }

【问题讨论】:

  • 我假设您在问将非递归 levelOrderTraversal() 转换为递归是否有意义?当然,它是有道理的,而且可能更容易阅读。在您真正遇到问题之前,我不会担心效率,这里的差异不应该那么大。请注意,您的递归 walkThroughElements() 可以通过在递归调用该方法之前首先将所有子项添加到列表中来转换为“级别顺序”或“广度优先”。
  • @Thomas 是的,完全正确!但是我无法真正完成将 walkThroughElements() 转换为 level-order 的整个过程,您介意稍微扩展您的解释吗,因为不幸的是我的递归速度有点慢。 :)
  • 您正在调用collectNodes(root, root.getChildren());,它使用子列表作为结果,因此您正在迭代该列表通过添加元素来修改它。相反,创建一个新列表并将其传递给该调用,然后从该新列表构建您的结果字符串,即List&lt;Node&lt;T&gt;&gt; collectedNodes = new ArrayList&lt;&gt;(); collectNodes(root, collectedNodes ); /*now use collectedNodes to build the string*/
  • @Thomas 是的,这似乎有效,但有点像 Pre-order ,我想我现在必须环顾四周才能解决这个问题
  • 是的,您的代码将是深度优先的方法。如果你想要广度优先,你需要先添加子节点(并且只添加子节点,而不是节点本身),然后进行递归调用。

标签: java algorithm tree-traversal


【解决方案1】:

您可以使用迭代(例如通过堆栈)或递归来解决这些迭代问题。让我们使用一种收集节点的方法,就像您的walkThroughElements()

深度优先

递归

//add the node and then go deeper
void collect(Node node, Collection<Node> nodes) {
  nodes.add(node);

  for(Node child : node.getChildren()) {
    collect(child, nodes);
  }
}

迭代

class Level {
  final Node node;
  Iterator<Node> childItr;

  //constructor, setters, getters
}

void collect(Collection<Node> nodes) {
  Stack<Level> levels = new Stack<>();

  nodes.add(root);
  levels.push(new Level(root, root.getChildren().iterator()));

  while( !levels.isEmpty() ) {
    Level currentLevel = levels.peek();
    
    //remove the current level as it doesn't have any more children
    if( !currentLevel.childItr.hasNext() ) {
      levels.pop();
    } else {
      //get the next child and add it to the result
      Node child = currentLevel.childItr.next();
      nodes.add(child);

      //go down to the child's level
      levels.push(new Level(child, child.getChildren().iterator())
    }
  }
}

广度优先

递归

//add the children first (i.e. the entire level) and then go deeper
void collectChildren(Node node, Collection<Node> nodes) {      
  for(Node child : node.getChildren()) {
    nodes.add(child);
    collectChildren(child, nodes);
  }
}

//special case: root node
void collect(Collection<Node> nodes) {
   nodes.add(root);
   collectChildren(root, nodes);
}

迭代

void collect(Collection<Node> nodes) {
  Queue<Node> nodesToProcess = new LinkedList<>();

  nodesToProcess.add(root);

  while( !nodesToProcess.isEmpty() ) {
    Node node = nodesToProcess.poll();

    nodes.add(node);

    nodesToProcess.addAll(node.getChildren());
  }
}

如您所见,深度优先的递归比广度优先的递归更容易,但无论如何也易于阅读。递归将使用调用堆栈来维护状态,因此它占用(非堆)内存并且对其深度也有限制(取决于有多少内存,但臭名昭著的 StackOverflowException 会告诉你要么有错误,要么树太深)。

广度优先的迭代更容易,并且需要额外的结构,如堆栈或队列。这些结构需要一些堆内存,并且由于一些优化,可能会更快,但总的来说,我不会在这里担心性能差异,因为它们应该只在非常大的树上表现出来 - 在这种情况下递归可能已经达到调用堆栈限制。

【讨论】:

  • 谢谢,我觉得我现在对这个问题有了一些了解,我会尝试在我的代码中实现这一点,然后回来展示发生了什么。
  • 使用这个时ConcurrentModificationException正常吗?
  • @JebvamUst 不,这不正常。这通常发生在使用迭代器(或“for each”)进行迭代并修改主体内的同一集合(可能在另一个方法调用中)时。您是否有机会在迭代期间更改子项的集合?
  • 不,我不认为我是,只是获取孩子并将它们添加到节点列表中。
  • @JebvamUst 好吧,如果不看一些代码就很难判断。你能把它添加到你的问题中吗?
【解决方案2】:

使用递归通常会比迭代慢并且使用更多的堆栈空间。但是是的,这也可以使用递归方式(DFS 方法)来解决。

供您参考:https://leetcode.com/problems/binary-tree-level-order-traversal/discuss/33562/Java-1ms-DFS-recursive-solution-and-2ms-BFS-iterative-solution

【讨论】:

  • 是的,我在这里和那里看到过这样的解决方案,但问题是我的 Node 类中没有左右指针,是否可以在没有实际拥有这两个的情况下递归地执行它,或者它们是强制性的?
【解决方案3】:

正确的做法是偷懒

// assume not null Node::data (if so, wrap into Optional or similar)
static <T> Stream<T> levelOrderTraversal(Node<T> tree) {

    final List<Node<T>> fifo = new ArrayList<>();

    if(tree != null)
        fifo.add(tree);

    final Function<T, T> next = o -> {
        if(fifo.isEmpty())
            return null; // End Of Stream
        Node<T> x = fifo.remove(0);
        System.out.println("++++++ " + x.data);
        if(x.children != null)
            fifo.addAll(x.children);
        return x.data;
    };

    return Stream.iterate(null, next::apply).skip(1).takeWhile(Objects::nonNull);
}

例如

++++++ foo
foo
++++++ bar1
bar1
++++++ bar2
bar2
++++++ par11
par11
++++++ par12
par12
++++++ par21
par21
++++++ par22
par22
++++++ subA
subA
++++++ subB
subB

您只维护中间遍历级别(而不是整个树)。

例如,如果您记录每个节点扩展并过滤树,则不会记录所有节点

    levelOrderTraversal(tree)
            .takeWhile(x -> !x.equals("bar2"))
            .forEach(System.out::println);

仅扩展 foobar1bar2 节点

++++++ foo
foo
++++++ bar1
bar1
++++++ bar2

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-23
    • 1970-01-01
    • 2010-11-20
    • 1970-01-01
    • 2016-05-01
    • 1970-01-01
    相关资源
    最近更新 更多