【问题标题】:Sort subgroups of lines in text file对文本文件中的行子组进行排序
【发布时间】:2022-01-13 23:34:48
【问题描述】:

我有一个名为filelist.txt 的服务器属性文件。此文件列表包含根据目录前缀分组排列的文件列表。内容的顺序很重要,因为文件必须以特定的文件名开始和结束,并且第一个子组必须首先出现。例如,文件如下所示:

config.txt
../../linux/a.txt
../../linux/c.txt
../../linux/d.txt
../../linux/b.txt
../../certificates/../../d.txt
../../certificates/../../a.txt
../../certificates/../../c.txt
../../certificates/../..b.txt
../../bin/b.txt
../../bin/a.txt
properties.server

我想知道的是,在保持子组的整体顺序的同时,以干净有效的方式在这些子组中进行排序的最佳方式是什么?

我编写了这段代码,它可以按子组过滤并对其进行排序:

try(Stream<String> lines = Files.lines(Paths.get("src/filelist.txt"))){

            List<String> linez = lines.filter(l -> l.contains("linux")).sorted().collect(Collectors.toList());

            BufferedWriter bw = new BufferedWriter(new FileWriter("src/output.txt"));

            for(String line : linez){
                bw.write(line+"\n");
            }

            bw.close();

我可以有一个List&lt;String&gt;,它将包含我的所有行,我可以过滤原始文件行,对它们进行排序,然后将它们添加到此列表中。

有几件事我不喜欢:

  1. 我没有覆盖原始文件,我正在写入一个新文件。我想看看是否有办法覆盖而不是写入新文件。
  2. 似乎有点迟钝。如果添加了新的目录前缀怎么办?然后我将不得不再次编辑此代码以过滤和排序新的目录前缀组。此外,根据每个过滤器为每个子组制作一堆不同的列表会感觉很奇怪,而不是像lines.filter(...).sort().filter(...).sort().filter(...) 这样的事情,但我认为这种语法还没有意义,因为一旦应用了过滤器,排序后我无法取消应用过滤器。能够过滤、就地排序,然后应用另一个过滤器和排序等等,真是太好了。

我有什么选择?

【问题讨论】:

    标签: java sorting java-stream


    【解决方案1】:

    这基于Shawn’s answer,但通过直接对原始列表中的受影响组进行排序来简化操作。

    try {
        Path datafile = Path.of(args[0]);
        var lines = Files.readAllLines(datafile);
    
        // ensure that the list is mutable
        if(lines.getClass() != ArrayList.class) lines = new ArrayList<>(lines);
    
        int first = 1; // skip first line
        int last = lines.size() - 1; // and last line
    
        if(first >= last) return;
    
        Path previous = Path.of(lines.get(first));
        for (int i = first + 1; i < last; i++) {
            Path p = Path.of(lines.get(i));
            if(!p.getParent().equals(previous.getParent())) {
                lines.subList(first, i).sort(null);
                first = i;
                previous = p;
            }
        }
        // Handle the last block of paths
        if(first < last) lines.subList(first, last).sort(null);
    
        // Overwrite the original input file
        Files.write(datafile, lines);
    } catch (IOException e) {
        System.err.println(e);
        System.exit(1);
    }
    

    请注意,Files.lines 返回的列表类型未指定,但实际上始终为ArrayList。该解决方案保护自己免受返回列表不可变但在现实生活中不执行复制操作的假设情况。所以它在形式上是正确和有效的。

    【讨论】:

    • 不可能是这样的吗? try (Stream&lt;String&gt; stream = Files.lines(Paths.get("src/filelist.txt"))) { stream.skip(1) .map(Path::of) .collect(Collectors.groupingBy(Path::getParent, LinkedHashMap::new, Collectors.collectingAndThen(Collectors.toList(), paths -&gt; paths.stream().sorted().collect(Collectors.toList())))).values(); }
    • @HadiJ 是的,这可行,但需要额外的存储空间,甚至比 Shawn 的回答还要多,后者利用了源文件中组项目已经相邻的事实。
    【解决方案2】:

    流不是最好的方法(至少不是整个工作;也许是其中的一部分)。下面使用查看每一行的方法,如果到最后一个组件的路径与之前读取的路径相同,则将其添加到列表中,如果不是,则对该路径列表进行排序,然后将它们添加到排序后的结果,使得每个前缀组的顺序不变,但每个组最终都是排序的。

    覆盖输入文件只是使用同一个文件进行读取和写入的问题 - 只需确保在打开它进行写入之前读取所有内容。下面只是在处理之前将文件读入列表。

    import java.util.ArrayDeque;
    import java.nio.file.Path;
    import java.nio.file.Files;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class Demo {
        public static void main(String[] args) {
            try {
                Path datafile = Path.of(args[0]);
                var lines = Files.readAllLines(datafile);
                var sorted = new ArrayDeque<String>(lines.size());
    
                // Add first line
                sorted.addLast(lines.get(0));
    
                // Iterate through all the remaining but the last line
                var block = new ArrayDeque<Path>();
                for (int i = 1; i < lines.size() - 1; i++) {
                    Path p = Path.of(lines.get(i));
                    if (!block.isEmpty()
                        && !p.getParent().equals(block.getLast().getParent())) {
                        // Current path has a different prefix than the current
                        // block. Sort block and add it to output
                        block.stream()
                            .sorted()
                            .forEachOrdered(sp ->
                                            sorted.addLast(sp.toString()));
                        // And reset for a new path prefix
                        block.clear();
                    }
                    block.addLast(p);
                }
                // Handle the last block of paths
                block.stream()
                    .sorted()
                    .forEachOrdered(sp -> sorted.addLast(sp.toString()));
    
                // Add last line
                sorted.addLast(lines.get(lines.size() - 1));
    
                // Overwrite the original input file
                Files.write(datafile, sorted);
            } catch (IOException e) {
                System.err.println(e);
                System.exit(1);
            }
        }
    }
    

    【讨论】:

    • 我看不出在这里使用ArrayDeque 的原因。两者,添加到结尾和明确,与ArrayList 一样好,但后者允许直接sort 并且可以write 用一个语句返回。好吧,我想后者也可以使用双端队列。
    • @Holger getLast() 非常好用。
    • 我不确定这是否应该推动这个决定。事实上,当你只保留变量Path last中的最后一个时,你不仅不需要getLast(),也不需要在列表中存储Path对象而是可以使用原始的@987654332 @。然后,您可以简单地使用addAll,而无需再次在Path 上调用toString()。您甚至可以更进一步,摆脱临时双端队列/列表,并在lines 列表中识别组边界,并使用lines.subList(…).sort(null) 对组进行排序并使用Files.write(datafile, lines); 回写,而无需复制。跨度>
    • @Holger 这听起来太复杂了。对于我愿意付出的努力来说,这当然太复杂了。
    • 是否太复杂可能是主观的,也可能是我不擅长描述它。所以如果你不尝试也没关系,我添加了答案。代码更好地解释了我的意思……哦,当然,我赞成提供基础。
    猜你喜欢
    • 2011-10-15
    • 1970-01-01
    • 1970-01-01
    • 2019-06-25
    • 2021-08-25
    • 1970-01-01
    • 2013-03-16
    • 2021-10-28
    相关资源
    最近更新 更多