繁体   English   中英

使用LINQ构建递归层次结构

[英]Building a recursive hierarchy with LINQ

在过去的几个小时中,我一直在与一个看似简单的问题作斗争。 我知道该解决方案将使用LINQ和递归,但我只是无法到达那里。

下面是我的示例代码,以及所需的输出(类似它,我真的不在乎,它正确地建立了基本的层次结构)。

任何帮助都会有所帮助。

using System;
using System.Collections.Generic;

namespace ConsoleApp14
{
    class Program
    {
        static string DoSomething(KeyValuePair<string, string>[] dir)
        {
            return ""; //something here
        }


        static void Main(string[] args)
        {
            KeyValuePair<string, string>[] dir = new[]
            {
                new KeyValuePair<string, string>(@"c:\one\two\three","100.txt"),
                new KeyValuePair<string, string>(@"c:\one\two\three","101.txt"),
                new KeyValuePair<string, string>(@"c:\one\four\five","501.txt"),
                new KeyValuePair<string, string>(@"c:\one\six\","6.txt"),
                new KeyValuePair<string, string>(@"c:\one\six","7.txt"),
                new KeyValuePair<string, string>(@"c:\one\","1.txt"),
                new KeyValuePair<string, string>(@"c:\one\six\","8.txt"),
                new KeyValuePair<string, string>(@"c:\one\two","2.txt"),
                new KeyValuePair<string, string>(@"c:\one\two\three\four\five\six\seven","y.txt"),
                new KeyValuePair<string, string>(@"c:\one\xxx","xxx.txt")
            };

            // this is the output I want, rough indentation and crlf, the ordering is not important, just the hierarchy
            Console.WriteLine(DoSomething(dir));
            //
            //  one
            //  (1.txt)
            //      two
            //      (2.txt)
            //           three
            //           (100.txt)
            //           (101.txt)
            //               four
            //                    five
            //                        six
            //                             seven
            //                             (y.txt)
            //      four
            //           five
            //           (501.txt)
            //      six
            //      (6.txt)
            //      (7.txt)
            //      (8.txt)
            //      xxx
            //      (xxx.txt)
            //
        } 
    }
}

这是一个数据结构问题,而不是算法问题。 一旦有了正确的数据结构,该算法将非常简单。

您需要的数据结构是:节点是文件还是目录:

abstract class Node {}
sealed class Directory : Node {}
sealed class File : Node {}

好的,我们对节点了解多少? 只是它有一个名字:

abstract class Node 
{
  public string Name { get; private set; }
  public Node(string name) { this.Name = name; }
}

我们对文件了解什么? 只是它有一个名字。

sealed class File : Node
{
  public File(string name) : base(name) { }
}

我们对目录了解什么? 它具有名称和子节点列表:

sealed class Directory : Node
{
  public Directory(string name) : base(name) { }
  public List<Node> Children { get; } = new List<Node>();

我们希望能够添加一个孩子:

  public File WithFile(string name)
  {
    // TODO: Is there already a child that has this name? return it.
    // TODO: Otherwise add it
  }
  public Directory WithDirectory(string name) 
  // TODO: Same.

太好了,现在我们可以建立目录并添加子目录或文件; 如果已经存在,我们将其取回。

现在,您的具体问题是什么? 您有一个目录名和一个文件名 序列 ,并且想要将该文件添加到目录中。 所以写!

  public Directory WithDirectory(IEnumerable<string> directories)
  {
    Directory current = this;
    foreach(string d in directories)
      current = current.WithDirectory(d);
    return current;
  }

  public File WithFile(IEnumerable<string> directories, string name)
  {
    return this.WithDirectory(directories).WithFile(name);
  }

现在,您要做的就是将每个路径分解为一系列名称 所以你的算法是

    Directory root = new Directory("c:");
    foreach(KeyValuePair pair in dir) 
    {
        IEnumerable<string> dirs = TODO break up the key into a sequence of strings
        root.WithFile(dirs, pair.Value);
    }

完成后, 您将拥有一个表示树的数据结构

现在您有了一棵树,在Node上编写一个方法:

override string ToString() => this.ToString(0);
string ToString(int indent) 
// TODO can you implement this?

这里的关键是正确的数据结构 目录只是一个名称加上子目录和文件的列表,因此请编写该代码 一旦有了正确的数据结构,其余的自然就可以了。 注意,我编写的每个方法只有几行。 (我作为TODO留下的东西很小。实现它们。)这就是您想要的: 在每种方法中做一件事,并且做得非常好 如果发现编写的是冗长而复杂的大型方法,请停止并将其重构为许多小方法,每个小方法都做一件清楚的事情。

练习:实现一个名为ToBoxyString的ToString版本,该版本产生:

c:
└─one
  ├─(1.txt)
  ├─two
  │ ├─(2.txt)
  │ └─three

... 等等。 它并不像看起来那么难。 这只是一个缩进。 你能弄清楚模式吗?

使用一些我喜欢的实用程序扩展方法:

public static class Ext {
    public static ArraySegment<T> Slice<T>(this T[] src, int start, int? count = null) => (count.HasValue ? new ArraySegment<T>(src, start, count.Value) : new ArraySegment<T>(src, start, src.Length - start));
    public static string Join(this IEnumerable<string> strings, string sep) => String.Join(sep, strings.ToArray());
    public static string Join(this IEnumerable<string> strings, char sep) => String.Join(sep.ToString(), strings.ToArray());
    public static string Repeat(this char ch, int n) => new String(ch, n);
}

您可以使用LINQ来数字化处理路径,这不需要任何递归,但效率不高(对于整个树中的每个深度,它都会两次遍历原始数组)。 该代码看起来很长,但主要是因为我输入了很多注释。

static IEnumerable<string> DoSomething(KeyValuePair<string, string>[] dir) {
    char[] PathSeparators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
    // some local utility functions
    // split a path into an array of its components [drive,dir1,dir2,...]
    string[] PathComponents(string p) => p.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
    // Combine path components back into a canonical path
    string PathCombine(IEnumerable<string> p) => p.Join(Path.DirectorySeparatorChar);
    // return all distinct paths that are depth deep, truncating longer paths
    IEnumerable<string> PathsAtDepth(IEnumerable<(string Path, string[] Components, string Filename)> dirs, int depth)
        => dirs.Select(pftuple => pftuple.Components)
               .Where(pa => pa.Length > depth)
               .Select(pa => PathCombine(pa.Slice(0, depth + 1)))
               .Distinct();

    // split path into components clean up trailing PathSeparators
    var cleanDirs = dir.Select(kvp => (Path: kvp.Key.TrimEnd(PathSeparators), Components: PathComponents(kvp.Key), Filename: kvp.Value));
    // find the longest path
    var maxDepth = cleanDirs.Select(pftuple => pftuple.Components.Length).Max();
    // ignoring drive, gather all paths at each length and the files beneath them
    var pfl = Enumerable.Range(1, maxDepth)
                        .SelectMany(d => PathsAtDepth(cleanDirs, d) // get paths down to depth d
                             .Select(pathAtDepth => new {
                                Depth = d,
                                Path = pathAtDepth,
                                // gather all filenames for pathAtDepth d
                                Files = cleanDirs.Where(pftuple => pftuple.Path == pathAtDepth)
                                                 .Select(pftuple => pftuple.Filename)
                                                 .ToList()
                            }))
                            .OrderBy(dpef => dpef.Path); // sort into tree
    // convert each entry into its directory path end followed by all files beneath that directory
    var stringTree = pfl.SelectMany(dpf => dpf.Files.Select(f => ' '.Repeat(4 * (dpf.Depth - 1)) + $"({f})")
                                                    .Prepend(' '.Repeat(4 * (dpf.Depth - 1)) + Path.GetFileName(dpf.Path)));

    return stringTree;
}

我的DoSomething版本返回IEnumerable<string> ,如果您愿意,可以将其Join输出中的单个字符串中:

Console.WriteLine(DoSomething(dir).Join(Environment.NewLine));

由于我的第一次尝试时间很长,因此我决定将此替代方法添加为单独的答案。 该版本通过dir数组一次后效率更高。

像以前一样使用一些扩展方法:

public static class Ext {
    public static ArraySegment<T> Slice<T>(this T[] src, int start, int? count = null) => (count.HasValue ? new ArraySegment<T>(src, start, count.Value) : new ArraySegment<T>(src, start, src.Length - start));
    public static string Join(this IEnumerable<string> strings, string sep) => String.Join(sep, strings.ToArray());
    public static string Join(this IEnumerable<string> strings, char sep) => String.Join(sep.ToString(), strings.ToArray());
    public static string Repeat(this char ch, int n) => new String(ch, n);
}

我将dir数组处理到一个Lookup ,该Lookup收集每个路径下的所有文件。 然后,我可以将路径分类为一棵树,并为每个路径添加路径及其下方的文件。 对于路径的每个子集,如果在转换为字符串树时它不包含文件,则添加一个空路径条目。

static IEnumerable<string> DoSomething(KeyValuePair<string, string>[] dir) {
    char[] PathSeparators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
    // some local utility functions
    int PathDepth(string p) => p.Count(ch => PathSeparators.Contains(ch));
    string PathToDepth(string p, int d) => p.Split(PathSeparators).Slice(0, d+1).Join(Path.DirectorySeparatorChar);

    // gather distinct paths (without trailing separators) and the files beneath them
    var pathsWithFiles = dir.ToLookup(d => d.Key.TrimEnd(PathSeparators), d => d.Value);
    // order paths with files into tree
    var pfl = pathsWithFiles.Select(pfs => new {
                                Path = pfs.Key, // each path
                                Files = pfs.ToList() // the files beneath it
                            })
                            .OrderBy(dpef => dpef.Path); // sort into tree
    // convert each entry into its directory path end followed by all files beneath that directory
    // add entries for each directory that has no files
    var stringTree = pfl.SelectMany(pf => Enumerable.Range(1, PathDepth(pf.Path))
                                                    // find directories without files
                                                    .Where(d => !pathsWithFiles.Contains(PathToDepth(pf.Path, d)))
                                                    // and add an entry for them
                                                    .Select(d => ' '.Repeat(4 * (d-1)) + Path.GetFileName(PathToDepth(pf.Path, d)))
                                                    // then add all the files
                                                    .Concat(pf.Files.Select(f => ' '.Repeat(4 * (PathDepth(pf.Path)- 1)) + $"({f})")
                                                    // and put the top dir first
                                                    .Prepend(' '.Repeat(4 * (PathDepth(pf.Path)-1)) + Path.GetFileName(pf.Path)))
                                                  );

    return stringTree;
}

同样,您可以像以前一样调用它:

Console.WriteLine(DoSomething(dir).Join(Environment.NewLine));

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM