Найти общий родительский путь в списке файлов и каталогов

Я получил список файлов и каталогов List<string> pathes. Теперь я хотел бы вычислить самую глубокую общую ветвь, которую каждый путь разделяет друг с другом.

Можно предположить, что все они имеют общий путь, но вначале это неизвестно.

Скажем, у меня есть следующие три записи:

  • C:/Hello/World/This/Is/An/Example/Bla.cs
  • C:/Привет/Мир/Это/Есть/Не/Пример/
  • C:/Привет/Земля/Бла/Бла/Бла

Это должно привести к результату: C:/Hello/, поскольку Земля разрывает эту «цепочку» подкаталогов.

Второй пример:

  • C:/Hello/World/This/Is/An/Example/Bla.cs
  • C:/Привет/Мир/Это/Есть/Не/Пример/

-> C:/Привет/Мир/Это/Есть/

Как бы вы поступили? Я попытался использовать string.split(@"/") и начать с первой строки и проверить, содержится ли каждая часть этого массива в других строках. Однако это был бы очень дорогой вызов, поскольку я итерирую (list_of_entries)^list_of_entries. Есть ли лучшее решение?

Моя текущая попытка будет выглядеть примерно так (С# + 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;
    }

person Frame91    schedule 21.07.2014    source источник
comment
stackoverflow.com/questions/8578110/, stackoverflow.com/questions/2070356/ - ответ найти довольно легко...   -  person Vojtěch Dohnal    schedule 21.07.2014


Ответы (6)


Функция для получения самого длинного общего префикса может выглядеть так:

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#.

person AlexD    schedule 21.07.2014
comment
Спасибо, именно то, что я искал, не могли бы вы обновить свой ответ и добавить к нему исходный код, чтобы люди могли видеть ответ напрямую? Мне нужно было позвонить на этот сайт через смартфон, так как моя компания блокирует этот сайт. - person Frame91; 21.07.2014
comment
Возможно, стоит преобразовать исходный код в С#, так как это не связано с Java. - person DGibbs; 21.07.2014
comment
Попробовал c:/dirOne/SomeOtherDir c:/dirOne/SoNotCommonDir и получил c:/DirOne/So, что неверно - person ManniAT; 14.10.2018
comment
@ManniAT Кажется, я прокомментировал этот случай, см.: Тогда вам может понадобиться вырезать префикс справа. и ниже. Функция GetLongestCommonPrefix предназначена для строк в целом. - person AlexD; 15.10.2018

Я не знаю, является ли это лучшим решением (возможно, нет), но его очень легко реализовать.

  • Отсортируйте список по алфавиту
  • сравните первую запись в этом отсортированном списке с последней в этом списке, символ за символом, и завершите работу, когда обнаружите разницу (значение перед завершением является самой длинной общей подстрокой обеих этих строк)

Пример скрипта

Образец кода:

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]));

И эта функция, конечно же:

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
person DrCopyPaste    schedule 21.07.2014
comment
Ошибка при сравнении только двух папок с одинаковой первой буквой - person Pakk; 05.02.2018
comment
@Pakk, можете ли вы продемонстрировать это с помощью скрипки или образцов значений? Если я редактирую, чтобы включить только paths.Add(@"C:/Hello/World/This/Is/An/Example/Bla.cs"); и paths.Add(@"C;/Hillo/World/This/Is/Not/An/Example/");, я получаю ожидаемый результат: Наиболее распространенная строка здесь: C - person DrCopyPaste; 06.02.2018
comment
оглядываясь назад - я вижу, что функция работает правильно, просто в результате я думал о действительном пути вместо наиболее распространенной строки. Извинения (мой пример, который действительно показывает наиболее распространенную подстроку: dotnetfiddle.net/bcxasj) - person Pakk; 06.02.2018

Сначала отсортируйте список с путями для проверки. Затем вы можете разделить и сравнить первый и последний элемент - если они одинаковы, перейдите к следующему измерению, пока не найдете разницу.

Таким образом, вам просто нужно отсортировать один раз, а затем проверить два элемента.

person Alex    schedule 21.07.2014
comment
ах, кажется, у нас обоих была одна и та же идея, только вы немного быстрее ее опубликовали;) - person DrCopyPaste; 21.07.2014

Чтобы вернуть 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);
}
person Dr.Gee    schedule 28.08.2018
comment
Ох... Я только что понял, что ты не хочешь этого делать, потому что это может быть дорого. Но, если честно... Вы до сих пор программируете на машине с 128 КБ ОЗУ? - person Dr.Gee; 28.08.2018

Я бы перебирал каждый символ в первом пути, сравнивая его с каждым символом в каждом пути (кроме первого) в наборе путей:

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;
}

Вы можете сначала просмотреть список, чтобы найти кратчайший путь и, возможно, немного улучшить его.

person Andrew Whitaker    schedule 21.07.2014
comment
Спасибо за Ваш ответ! Насколько я вижу, это также будет стоить O (n), как и решение AlexD. Вы видите разницу в производительности? - person Frame91; 21.07.2014
comment
Я считаю, что он работает так же. Это не O(n), а действительно O(nm)*, где m — длина кратчайшей подстроки. Внешний цикл повторяется m раз, а внутренний цикл повторяется n раз. - person Andrew Whitaker; 21.07.2014
comment
Да, это O(n*m), сначала подумал, что можно уменьшить O(n*x) до O(n), но ты прав, спасибо! - person Frame91; 21.07.2014

Функция, которая дает вам самый длинный общий путь к каталогу с максимально возможной сложностью:

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;
}
person KamilKaczorek    schedule 10.09.2020