在文件和目录列表中查找共同的父路径

14

我有一个包含文件和目录的列表 List<string> pathes。现在我想计算每个路径共享的最深公共分支。

我们可以假设它们都有一个共同的路径,但一开始不知道这个路径是什么。

假设我有以下三个条目:

  • C:/Hello/World/This/Is/An/Example/Bla.cs
  • C:/Hello/World/This/Is/Not/An/Example/
  • C:/Hello/Earth/Bla/Bla/Bla

这应该得到结果:C:/Hello/ ,因为 Earth 打破了这个子目录 "链"。

第二个例子:

  • C:/Hello/World/This/Is/An/Example/Bla.cs
  • C:/Hello/World/This/Is/Not/An/Example/

-> C:/Hello/World/This/Is/

你会如何处理?我尝试使用 string.split(@"/") 并从第一个字符串开始检查该数组的每个部分是否包含在其他字符串中。然而,这将是一个非常昂贵的调用,因为我正在遍历 (list_of_entries)^list_of_entries。是否有更好的解决方案可用?

我当前的尝试类似于以下内容(C# + LINQ):

    public string CalculateCommonPath(IEnumerable<string> paths)
    {
        int minSlash = int.MaxValue;
        string minPath = null;
        foreach (var path in paths)
        {
            int splits = path.Split('\\').Count();
            if (minSlash > splits)
            {
                minSlash = splits;
                minPath = path;
            }
        }

        if (minPath != null)
        {
            string[] splits = minPath.Split('\\');
            for (int i = 0; i < minSlash; i++)
            {
                if (paths.Any(x => !x.StartsWith(splits[i])))
                {
                    return i >= 0 ? splits.Take(i).ToString() : "";
                }
            }
        }
        return minPath;
    }

1
https://dev59.com/WWoy5IYBdhLWcg3wZtNC, https://dev59.com/_XI95IYBdhLWcg3w-DDz - 答案很容易找到... - Vojtěch Dohnal
7个回答

10

一个用于获取最长公共前缀的函数可能如下所示:

public static string GetLongestCommonPrefix(string[] s)
{
    int k = s[0].Length;
    for (int i = 1; i < s.Length; i++)
    {
        k = Math.Min(k, s[i].Length);
        for (int j = 0; j < k; j++)
            if (s[i][j] != s[0][j])
            {
                k = j;
                break;
            }
    }
    return s[0].Substring(0, k);
}

如果需要的话,您可能需要在右侧截取前缀。例如,我们想返回 c:/dir 而不是 c:/dir/file

c:/dir/file1
c:/dir/file2

在处理之前,您可能还需要将路径标准化。请参见在C#中标准化目录名称


谢谢,这正是我在寻找的。你能否更新你的答案并添加源代码,以便人们可以直接看到答案?由于我的公司正在阻止访问该网站,我需要通过智能手机调用它。 - Frame91
3
由于这与Java无关,将源代码转换为C#可能会很有价值。 - DGibbs
尝试使用 c:/dirOne/SomeOtherDir c:/dirOne/SoNotCommonDir 进行测试,但结果是 c:/DirOne/So,这是错误的。 - ManniAT
@ManniAT 我想我已经评论过这个案例了,请看:" 然后您可能需要剪切右侧的前缀。 " 以及下面的内容。函数 GetLongestCommonPrefix 适用于一般字符串。 - AlexD

7
我不确定这是最佳的性能解决方案(很可能不是),但它确实非常容易实现。
  • 将列表按字母顺序排序
  • 逐个比较该排序列表中的第一个条目和最后一个条目,查找字符差异,并在找到差异时终止(终止前的值是这两个字符串的最长共享子串)

示例代码

样例代码:

List<string> paths = new List<string>();

paths.Add(@"C:/Hello/World/This/Is/An/Example/Bla.cs");
paths.Add(@"C:/Hello/World/This/Is/Not/An/Example/");
paths.Add(@"C:/Hello/Earth/Bla/Bla/Bla");

List<string> sortedPaths = paths.OrderBy(s => s).ToList();

Console.WriteLine("Most common path here: {0}", sharedSubstring(sortedPaths[0], sortedPaths[sortedPaths.Count - 1]));

当然,这个函数是关于IT技术的:

而且这个函数:

public static string sharedSubstring(string string1, string string2)
{
    string ret = string.Empty;

    int index = 1;
    while (string1.Substring(0, index) == string2.Substring(0, index))
    {
        ret = string1.Substring(0, index);
        index++;
    }

    return ret;
} // returns an empty string if no common characters where found

当仅比较两个以相同首字母开头的文件夹时会失败。 - Pakk
@Pakk,你能用一个fiddle或示例值演示一下吗?如果我只编辑包括paths.Add(@"C:/Hello/World/This/Is/An/Example/Bla.cs");paths.Add(@"C;/Hillo/World/This/Is/Not/An/Example/");,我会得到预期的结果:最常见的字符串是C - DrCopyPaste
1
回想起来,我发现函数确实可以正常工作,只是我考虑了一个有效的路径而不是最常见的字符串作为结果。抱歉(我的示例确实显示了最常见的子字符串:https://dotnetfiddle.net/bcxasj) - Pakk

2

返回 c:/dir 的路径

c:/dir/file1
c:/dir/file2

我会这样编写代码:
public static string GetLongestCommonPrefix(params string[] s)
{
    return GetLongestCommonPrefix((ICollection<string>)s);
}

public static string GetLongestCommonPrefix(ICollection<string> paths)
{
    if (paths == null || paths.Count == 0)
        return null;


    if (paths.Count == 1)
        return paths.First();

    var allSplittedPaths = paths.Select(p => p.Split('\\')).ToList();

    var min = allSplittedPaths.Min(a => a.Length);
    var i = 0;
    for (i = 0; i < min; i++)
    {
        var reference = allSplittedPaths[0][i];
        if (allSplittedPaths.Any(a => !string.Equals(a[i], reference, StringComparison.OrdinalIgnoreCase)))
        {
            break;
        }
    }

    return string.Join("\\", allSplittedPaths[0].Take(i));
}

以下是针对它的一些测试:

[TestMethod]
public void GetLongestCommonPrefixTest()
{
    var str1 = @"C:\dir\dir1\file1";
    var str2 = @"C:\dir\dir1\file2";
    var str3 = @"C:\dir\dir1\file3";
    var str4 = @"C:\dir\dir2\file3";
    var str5 = @"C:\dir\dir1\file1\file3";
    var str6 = @"C:\dir\dir1\file1\file3";


    var res = Utilities.GetLongestCommonPrefix(str1, str2, str3);

    Assert.AreEqual(@"C:\dir\dir1", res);

    var res2 = Utilities.GetLongestCommonPrefix(str1, str2, str3, str4);

    Assert.AreEqual(@"C:\dir", res2);

    var res3 = Utilities.GetLongestCommonPrefix(str1, str2, str3, str5);

    Assert.AreEqual(@"C:\dir\dir1", res3);

    var res4 = Utilities.GetLongestCommonPrefix(str5, str6);

    Assert.AreEqual(@"C:\dir\dir1\file1\file3", res4);

    var res5 = Utilities.GetLongestCommonPrefix(str5);

    Assert.AreEqual(str5, res5);

    var res6 = Utilities.GetLongestCommonPrefix();

    Assert.AreEqual(null, res6);

    var res7 = Utilities.GetLongestCommonPrefix(null);

    Assert.AreEqual(null, res7);
}

哦...我刚意识到你不想这样做,因为可能会很昂贵。但说实话...你还在使用只有128kB RAM的机器上编码吗? - Dr.Gee

2

首先,对包含路径的列表进行排序。然后可以拆分并比较第一个和最后一个条目-如果它们相同,则继续到下一个维度,直到找到差异。

所以,你只需要排序一次,然后检查两个项目。


啊,看来我们俩想到了同样的事情,只不过你发帖速度稍微快了一点 ;) - DrCopyPaste

1
我会对第一个路径中的每个字符进行迭代,并将其与集合中除第一个路径以外的每个路径中的每个字符进行比较:
public string FindCommonPath(List<string> paths)
{
    string firstPath = paths[0];
    bool same = true;

    int i = 0;

    string commonPath = string.Empty;

    while (same && i < firstPath.Length)
    {
        for (int p = 1; p < paths.Count && same; p++)
        {
            same = firstPath[i] == paths[p][i];
        }

        if (same)
        {
            commonPath += firstPath[i];
        }
        i++;
    }

    return commonPath;
}

你可以先遍历列表找到最短的路径,然后可能会稍微改进它。

谢谢您的回答!就我所看到的,这也会像AlexDs的解决方案一样耗费O(n)的时间复杂度。您有发现任何性能差异吗? - Frame91
我相信它的表现是一样的。但它不是 *O(n)*,而是真正的 O(nm)*,其中 m 是最短子字符串的长度。外部循环迭代 m 次,内部循环迭代 n 次。 - Andrew Whitaker
是的,它是O(nm)的时间复杂度,一开始认为可以将O(nx)降至O(n),但你是对的,谢谢! - Frame91

1
给出最佳复杂度的函数,用于获取最长公共目录路径:
private static string GetCommonPath(IEnumerable<string> files)
{
    // O(N, L) = N*L; N  - number of strings, L - string length

    // if the first and last path from alphabetic order matches, all paths in between match
    string first = null;//smallest string
    string last = null;//largest string

    var comparer = StringComparer.InvariantCultureIgnoreCase;
    // find smallest and largest string:
    foreach (var file in files.Where(p => !string.IsNullOrWhiteSpace(p)))
    {
        if (last == null || comparer.Compare(file, last) > 0)
        {
            last = file;
        }

        if (first == null || comparer.Compare(file, first) < 0)
        {
            first = file;
        }
    }

    if (first == null)
    {
        // the list is empty
        return string.Empty;
    }

    if (first.Length > last.Length)
    {
        // first should not be longer
        var tmp = first;
        first = last;
        last = tmp;
    }

    // get minimal length
    var count = first.Length;
    var found = string.Empty;

    const char dirChar = '\\';
    var sb = new StringBuilder(count);
    for (var idx = 0; idx < count; idx++)
    {
        var current = first[idx];
        var x = char.ToLowerInvariant(current);
        var y = char.ToLowerInvariant(last[idx]);

        if (x != y)
        {
            // first and last string character is different - break
            return found;
        }

        sb.Append(current);

        if (current == dirChar)
        {
            // end of dir character
            found = sb.ToString();
        }
    }

    if (last.Length >= count && last[count] == dirChar)
    {
        // whole first is common root:
        return first;
    }

    return found;
}

我不确定您认为什么是“最佳复杂度”,以及您如何证明它?如果这涉及计算复杂度(而不是代码复杂度/可读性),我会建议您放弃使用 StringBuilder,只需保留上次找到的索引(并且仅在最后一次计算和返回子字符串时计算一次)。另一个疑问是如何找到第一个和最后一个字符串,因为它在 comparer.Compare 后面隐藏了很多操作。(而且您正在使用昂贵的 InvariantCultureIgnoreCase,而不是例如 OrdinalIgnoreCase。) - Hilarion

0

这比通过斜杠拆分路径并进行比较要优化得多:

private static string FindCommonPath(string[] paths) {
    var firstPath = paths[0];
    var commonPathLength = firstPath.Length;

    for (int i = 1; i < paths.Length; i++)
    {
        var otherPath = paths[i];
        var pos = -1;
        var checkpoint = -1;

        while (true)
        {
            pos++;

            if (pos == commonPathLength)
            {
                if (pos == otherPath.Length
                    || (pos < otherPath.Length
                        && (otherPath[pos] == '/' || otherPath[pos] == '\\')))
                {
                    checkpoint = pos;
                }
                break;
            }

            if (pos == otherPath.Length)
            {
                if (pos == commonPathLength
                || (pos < commonPathLength
                    && (firstPath[pos] == '/' || firstPath[pos] == '\\')))
                {
                    checkpoint = pos;
                }
                break;
            }

            if ((firstPath[pos] == '/' || firstPath[pos] == '\\')
                && (otherPath[pos] == '/' || otherPath[pos] == '\\'))
            {
                checkpoint = pos;
                continue;
            }

            var a = char.ToLowerInvariant(firstPath[pos]);
            var b = char.ToLowerInvariant(otherPath[pos]);

            if (a != b)
                break;
        }

        if (checkpoint == 0 && (firstPath[0] == '/' || firstPath[0] == '\\'))
            commonPathLength = 1;
        else commonPathLength = checkpoint;

        if (commonPathLength == -1 || commonPathLength == 0)
            return "";
    }

    return firstPath.Substring(0, commonPathLength);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接