【问题标题】:Parse indented text tree in Java在 Java 中解析缩进的文本树
【发布时间】:2014-03-11 05:21:35
【问题描述】:

我有一个需要使用 java 解析的缩进文件, 我需要一些方法将它放在 Section 类中,如下所示

    root
     root1
       text1
         text1.1
         text1.2
       text2
         text2.1
         text2.2

     root2
       text1
         text1.1
         text1.2
       text2
         text2.1
         text2.2.2

我有一个类来放置看起来像缩进的东西

public class Section 
{

    private List<Section> children;
    private String text;
    private int depth;
    public Section(String t)
    {
       text =t;
    }

    public List<Section> getChildren()
    {
        if (children == null)
      {
            children = new ArrayList<Section>();
       }
        return children;
}

public void setChildren(List<Section> newChildren)
{
    if (newChildren == null) {
        children = newChildren;
    } else {
        if (children == null) {
            children = new ArrayList<Section>();
        }
        for (Section child : newChildren) {
            this.addChild(child);
        }
    }
}

public void addChild(Section child)
{
    if (children == null) {
        children = new ArrayList<Section>();
    }
    if (child != null) {
        children.add(child);
    }
}

public String getText()
{
    return text;
}

public void setText(String newText)
{
    text =newText;
}
public String getDepth()
{
    return depth;
}

 public void setDepth(int newDepth)
 {
    depth = newDepth;
 }
}

我需要一些方法来解析文件并将其放置在预期结果中,我们是一个 Section 对象,如下所示

Section= 

Text="Root"
Children
Child1: Text= "root1" 

        Child1: "text1"
            Child1="Text 1.1"
            Child2="Text 1.2"
        Child2: "text2"
            Child1="Text 2.1"
            Child2="Text 2.2"
            Children
Child2: Text= "root2" 
        Child1: "text1"
            Child1="Text 1.1"
            Child2="Text 1.2"
        Child2: "text2"
            Child1="Text 2.1"
            Child2="Text 2.2"


Here is some code that I have started
   int indentCount=0;
   while(String text = reader.readline()
   {
   indentCount=countLeadingSpaces(String word);
   //TODO create the section here
   }


public static int countLeadingSpaces(String word)
{
    int length=word.length();
    int count=0;

   for(int i=0;i<length;i++)
   {
       char first = word.charAt(i); 
        if(Character.isWhitespace(first))
        {
            count++;           
        }
        else
        {
            return count;
        }
   }

 return count;

}

【问题讨论】:

  • 看起来你可以通过计算Section 前面的空格来检测它的深度。因此,如果深度大于前一行的深度,则将其添加为 Section 的子代,否则将其创建为新的 Section。顺便说一句,您可能需要以下两者之一:(i) parent 字段,或 (ii) depth 字段。
  • 您发布的代码并没有真正尝试解决您所询问的问题。您是否编写了一些代码来尝试解决上述问题?
  • @Dukeling 刚刚编辑了它
  • @Chthonic Project 我刚刚添加了深度场。

标签: java algorithm


【解决方案1】:

我还添加了一个父指针。也许没有它也可以解析文本,但是父指针使它更容易。首先,你需要有更多的构造函数:

static final int root_depth = 4; // assuming 4 whitespaces precede the tree root

public Section(String text, int depth) {
    this.text     = text;
    this.depth    = depth;
    this.children = new ArrayList<Section>();
    this.parent   = null;
}

public Section(String text, int depth, Section parent) {
    this.text     = text;
    this.depth    = depth;
    this.children = new ArrayList<Section>();
    this.parent   = parent;
}

然后,当你开始解析文件时,逐行读取:

Section prev = null;
for (String line; (line = bufferedReader.readLine()) != null; ) {
    if (prev == null && line begins with root_depth whitespaces) {
        Section root = new Section(text_of_line, root_depth);
        prev = root;
    }
    else {
        int t_depth = no. of whitespaces at the beginning of this line;
        if (t_depth > prev.getDepth())
            // assuming that empty sections are not allowed
            Section t_section = new Section(text_of_line, t_depth, prev);
            prev.addChild(t_section);
        }
        else if (t_depth == prev.getDepth) {
            Section t_section = new Section(text_of_line, t_depth, prev.getParent());
            prev.getParent().addChild(t_section);
        }
        else {
            while (t_depth < prev.getDepth()) {
                prev = prev.getParent();
            }
            // at this point, (t_depth == prev.getDepth()) = true
            Section t_section = new Section(text_of_line, t_depth, prev.getParent());
            prev.getParent().addChild(t_section);
        }
    }
}

我已经掩盖了伪代码的一些细节,但我认为您已经大致了解了如何进行此解析。请记住实现 addChild()、getDepth()、getParent() 等方法。

【讨论】:

  • 它无法正常工作,prev 应始终设置为创建的部分后添加为子项
【解决方案2】:

令人惊讶的复杂问题...但这里有一个伪代码

intialize a stack
push first line to stack
while (there are more lines to read) {
 S1 = top of stack // do not pop off yet
 S2 = read a line
 if depth of S1 < depth of S2 {
  add S2 as child of S1
  push S2 into stack
 }
 else {
  while (depth of S1 >= depth of S2 AND there are at least 2 elements in stack) {
   pop stack
   S1 = top of stack // do not pop
  }
  add S2 as child of S1
  push S2 into stack
 }
}
return bottom element of stack

其中深度是# 前导空格。 您可能必须修改或包装 Section 类来存储行的深度。

【讨论】:

  • 我花了最后一个小时寻找递归解决方案,但递归并不总是最好的树工具...
【解决方案3】:

C# 中的实现

基于this answer,我创建了一个 C# 解决方案。

它允许多个根并假设输入结构如下:

Test
    A
    B
    C
        C1
        C2
    D
Something
    One
    Two
    Three

代码的一个示例用法是:

var lines = new[]
{
    "Test",
    "\tA",
    "\tB",
    "\tC",
    "\t\tC1",
    "\t\tC2",
    "\tD",
    "Something",
    "\tOne",
    "\tTwo",
    "\tThree"
};

var roots = IndentedTextToTreeParser.Parse(lines, 0, '\t');

var dump = IndentedTextToTreeParser.Dump(roots);
Console.WriteLine(dump);

您可以指定根缩进(默认为零)以及缩进字符(默认为制表符\t)。

完整代码:

namespace MyNamespace
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;

    public static class IndentedTextToTreeParser
    {
        // https://stackoverflow.com/questions/21735468/parse-indented-text-tree-in-java

        public static List<IndentTreeNode> Parse(IEnumerable<string> lines, int rootDepth = 0, char indentChar = '\t')
        {
            var roots = new List<IndentTreeNode>();

            // --

            IndentTreeNode prev = null;

            foreach (var line in lines)
            {
                if (string.IsNullOrEmpty(line.Trim(indentChar)))
                    throw new Exception(@"Empty lines are not allowed.");

                var currentDepth = countWhiteSpacesAtBeginningOfLine(line, indentChar);

                if (currentDepth == rootDepth)
                {
                    var root = new IndentTreeNode(line, rootDepth);
                    prev = root;

                    roots.Add(root);
                }
                else
                {
                    if (prev == null)
                        throw new Exception(@"Unexpected indention.");
                    if (currentDepth > prev.Depth + 1)
                        throw new Exception(@"Unexpected indention (children were skipped).");

                    if (currentDepth > prev.Depth)
                    {
                        var node = new IndentTreeNode(line.Trim(), currentDepth, prev);
                        prev.AddChild(node);

                        prev = node;
                    }
                    else if (currentDepth == prev.Depth)
                    {
                        var node = new IndentTreeNode(line.Trim(), currentDepth, prev.Parent);
                        prev.Parent.AddChild(node);

                        prev = node;
                    }
                    else
                    {
                        while (currentDepth < prev.Depth) prev = prev.Parent;

                        // at this point, (currentDepth == prev.Depth) = true
                        var node = new IndentTreeNode(line.Trim(indentChar), currentDepth, prev.Parent);
                        prev.Parent.AddChild(node);
                    }
                }
            }

            // --

            return roots;
        }

        public static string Dump(IEnumerable<IndentTreeNode> roots)
        {
            var sb = new StringBuilder();

            foreach (var root in roots)
            {
                doDump(root, sb, @"");
            }

            return sb.ToString();
        }

        private static int countWhiteSpacesAtBeginningOfLine(string line, char indentChar)
        {
            var lengthBefore = line.Length;
            var lengthAfter = line.TrimStart(indentChar).Length;
            return lengthBefore - lengthAfter;
        }

        private static void doDump(IndentTreeNode treeNode, StringBuilder sb, string indent)
        {
            sb.AppendLine(indent + treeNode.Text);
            foreach (var child in treeNode.Children)
            {
                doDump(child, sb, indent + @"    ");
            }
        }
    }

    [DebuggerDisplay(@"{Depth}: {Text} ({Children.Count} children)")]
    public class IndentTreeNode
    {
        public IndentTreeNode(string text, int depth = 0, IndentTreeNode parent = null)
        {
            Text = text;
            Depth = depth;
            Parent = parent;
        }

        public string Text { get; }
        public int Depth { get; }
        public IndentTreeNode Parent { get; }
        public List<IndentTreeNode> Children { get; } = new List<IndentTreeNode>();

        public void AddChild(IndentTreeNode child)
        {
            if (child != null) Children.Add(child);
        }
    }
}

我还包含一个方法 Dump() 将树转换回字符串,以便更好地调试算法本身。

【讨论】:

    【解决方案4】:

    我使用递归函数调用实现了另一种解决方案。据我估计,它的性能会比 Max Seo 的建议更差,尤其是在深层层次结构上。但是,它更容易理解(在我看来),因此可以根据您的特定需求进行修改。看看,如果你有任何建议,请告诉我。

    一个好处是它可以处理具有多个根的树。

    问题描述 - 只是为了清楚一点......

    假设我们有一个构造节点,它可以包含数据并且有零个或多个 子节点,它们也是节点。基于文本输入,我们要构建树 节点数,其中每个节点的数据是一行的内容,而 树中节点的位置由行位置和缩进指示, 所以缩进的一行是前一行的子行,它 缩进较少。

    算法说明

    假设我们有一个行列表,定义一个函数:

    • 如果输入列表至少有两行:
      • 从列表中删除第一行
      • 从列表中删除所有满足所有条件的行:
        • 缩进比第一行高
        • 在缩进小于或等于第一行的下一行之前发生
      • 将这些行递归地传递给函数,并将结果设置为第一行的子行
      • 如果它还有剩余的行,则将它们递归地传递给函数,并将它们与第一行合并,作为结果
      • 如果没有剩余的行,则返回一个以第一行作为单个元素的列表
    • 如果输入列表只有一行:
      • 将该行的子级设置为空列表
      • 返回列表
    • 如果输入列表没有元素
      • 返回一个空列表
    • 从列表中删除第一行

    使用行列表调用函数,将生成树列表, 基于他们的缩进。如果树只有一个根,则生成的树将 成为结果列表的第一个元素。

    伪代码

    List<Node> LinesToTree( List<Line> lines )
    {
        if(lines.count >= 2)
        {
            firstLine = lines.shift
            nextLine = lines[0]
            children = List<Line>
    
            while(nextLine != null && firstLine.indent < nextLine.indent)
            {
                children.add(lines.shift)
                nextLine = lines[0]
            }
    
            firstLineNode = new Node
            firstLineNode.data = firstLine.data
            firstLineNode.children = LinesToTree(children)
    
            resultNodes = new List<Node>
            resultNodes.add(firstLineNode)
    
            if(lines.count > 0)
            {
                siblingNodes = LinesToTree(lines)
                resultNodes.addAll(siblingNodes)
                return resultNodes
            }
            else
            {
                return resultNodes
            }
        }
        elseif()
        {
            nodes = new List<Node>
            node = new Node
            node.data = lines[0].data
            node.children = new List<Node>
            return nodes
        }
        else
        {
            return new List<Node>
        }
    }
    

    使用数组的 PHP 实现

    该实现可通过委托进行自定义,以获取缩进,以及输出数组中子字段的名称。

    public static function IndentedLinesToTreeArray(array $lineArrays, callable $getIndent = null, $childrenFieldName = "children")
    {
        //Default function to get element indentation
        if($getIndent == null){
            $getIndent = function($line){
                return $line["indent"];
            };
        }
    
        $lineCount = count($lineArrays);
    
        if($lineCount >= 2)
        {
            $firstLine = array_shift($lineArrays);
            $children = [];
            $nextLine = $lineArrays[0];
    
            while($getIndent($firstLine) < $getIndent($nextLine)){
                $children[] = array_shift($lineArrays);
                if(!isset($lineArrays[0])){
                    break;
                }
                $nextLine = $lineArrays[0];
            }
    
            $firstLine[$childrenFieldName] = self::IndentedLinesToTreeArray($children, $getIndent, $childrenFieldName);
    
            if(count($lineArrays)){
                return array_merge([$firstLine],self::IndentedLinesToTreeArray($lineArrays, $getIndent, $childrenFieldName));
            }else{
                return [$firstLine];
            }
        }
        elseif($lineCount == 1)
        {
            $lineArrays[0][$childrenFieldName] = [];
            return $lineArrays;
        }
        else
        {
            return [];
        }
    }
    

    【讨论】:

    • 附加信息方面的最佳答案(根本不需要)。我的 Haskell 实现 gist.github.com/ulysses4ever/5b77ae93a0c0c2e40ce2 Btw afaics:您不需要分别考虑剩余两条线和一条剩余线的情况:前者的算法可以处理后者。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-22
    • 2011-11-04
    • 2019-05-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多