比较List <string>以包含字符串C#

时间:2016-06-30 14:18:28

标签: c# list compare big-o contains

我有两个包含字符串的列表。每个列表都存储路径信息。 List1包含每个文件的完整UNC路径。 List2包含每个路径的修剪版本。

我尝试使用List2中的部分路径信息作为密钥以及List1的完整路径信息作为值来构建字典。

示例:

List1 = { "\\\\some\path111\to\file1.txt", "\\\\some\path222\to\file2.txt", "\\\\some\path333\to\file3.txt" };
List2 = { "\to\file3.txt", "\to\file2.txt", "\to\file1.txt" };

预期比较结果:

{\to\file1.txt, \\\\some\path111\to\file1.txt}
{\to\file2.txt, \\\\some\path111\to\file2.txt}
{\to\file3.txt, \\\\some\path111\to\file3.txt}

我设法写了一些我需要的东西,但它的运行速度非常慢(见下文)。我想知道我能做些什么来加快速度或将信息存储在不同的集合中以获得更快的匹配。这两个列表各约500,000个字符串。

private Dictionary<string, string> FullPathBuilder(List<string> partialPathList, List<string> fullPathList)
{
    Dictionary<string, string> result = new Dictionary<string, string>();
    try
    {
        foreach (string partPath in partialPathList)
        {
            foreach (string matchedFullPath in fullPathList.Where(s => s.Contains(partPath)))
            {
                if (ThirdPartyRepackVariables.cancelQC)
                {
                    break;
                }

                // get the match.  
                if (matchedFullPath != null)
                {
                    if (!result.ContainsKey(partPath))
                    {
                        result.Add(partPath, matchedFullPath);
                        ThirdPartyRepackVariables.pathsUpdated++;
                    }
                }
                else
                {
                    result.Add(partPath, partPath);
                    ThirdPartyRepackVariables.unmatchedPaths.Add(partPath);
                }
            }
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, "Error building path cross reference");
    }
    return result;
}

5 个答案:

答案 0 :(得分:4)

您当前的实施要求您循环超过500,000个项目(您的外部foreach循环)。对于每一个,您可以查看内部集合中的500,000个项目(您的Where语句)以查找匹配项。

假设完整路径始终以部分结束(即,部分始终包含完整文件名),您可以通过路径的反向对两个列表进行排序来加快速度。

让两个列表以相同的顺序排序,允许您在第二个列表上快捷循环。

答案 1 :(得分:1)

解决方案

使用Tim Copenhaver's excellent suggestion对输入列表进行排序,我构建了一个可以进行所需合并的函数:

public static IDictionary<string, string> MergePaths(
    IEnumerable<string> partialPaths, IEnumerable<string> fullPaths)
{
    var sortedPartialPaths = partialPaths
        .Select(p => new { Original = p, Reverse = p.Reverse() })
        .OrderBy(p => p.Reverse)
        .ToList();

    var sortedFullPaths = fullPaths
        .Select(p => new { Original = p, Reverse = p.Reverse() })
        .OrderBy(p => p.Reverse)
        // Capture the index of each full path so we can skip full paths later.
        .Select((p, i) => new { p.Original, p.Reverse, Index = i })
        .ToList();

    var lastFullPathIndex = 0;

    return sortedPartialPaths.ToDictionary(
        pp => pp.Original,
        pp => 
        {
            var matchedFullPath = sortedFullPaths
                // Skip all full paths that have already been matched.
                .Skip(lastFullPathIndex)
                // Skip all full paths that are smaller in terms of string sort order.
                .SkipWhile(fp => fp.Reverse.CompareTo(pp.Reverse) < 0)
                // Only take the full paths that end with the matching partial path. 
                // Should only take one. If there are more the rest will be discarded.
                .TakeWhile(fp => fp.Reverse.StartsWith(pp.Reverse))
                .FirstOrDefault();

            // Update the index of our last match.
            lastFullPathIndex = matchedFullPath?.Index ?? lastFullPathIndex;

            return matchedFullPath?.Original;
        });
}

请注意,它不使用框架的Skip()版本,而是使用此IList<T>特定的实现:

public static IEnumerable<T> Skip<T>(this IList<T> list, int count)
{
    for (var i = count; i < list.Count; i++)
        yield return list[i];
}

原始版本太慢了,因为它实际上并没有阻止IList<T>类型的超级流量枚举。

它还使用以下扩展方法来反转string s:

public static string Reverse(this string s)
{
    var arr = s.ToCharArray();
    Array.Reverse(arr);
    return new string(arr);
}

效果

我使用简单的1:1映射(如René Vogts answer中所述)作为性能基线。如果我们假设每个部分路径可以恰好匹配一个完整路径,则以下代码将执行最佳,即它将与路径数量线性比例(不计算排序):

public static IDictionary<string, string> MergePathsOneToOne(
    IList<string> partialPaths, IList<string> fullPaths)
{
    var sortedPartialPaths = partialPaths.OrderBy(p => p.Reverse()).ToList();
    var sortedFullPaths = fullPaths.OrderBy(p => p.Reverse()).ToList();

    return Enumerable
        .Range(0, sortedPartialPaths.Count)
        .ToDictionary(
            i => sortedPartialPaths[i], 
            i => sortedFullPaths[i]);
}

在我的机器上MergePathsOneToOne()需要大约6-7秒才能完成500,000条路径。

但是,如果部分路径没有匹配的完整路径,或者您收集了根本不匹配的完整路径,则会失败。

我的解决方案的执行速度几乎与1:1版本相同(500,000路径为7-8秒)。但更重要的是,它与1:1版本相同。

请参阅此处用于性能测试的代码:https://gist.github.com/bert2/de9ff3b347ac32d5cebecc4d8149a452

在那里,您还会发现另外两个MergePaths()的实现。一个是使用LinkedList<T>而另一个List<T>.FindIndex(startIndex, predicate)是为了跳过完整路径,但它们的表现并不是那么好。

请用一粒盐进行性能测量。由于预热不足,GC干扰和缺乏平滑,您无法比较绝对值。但是,它们应该让您了解每个算法在更改路径数时的扩展程度。

答案 2 :(得分:1)

我发生了几件事

  • 包含一个非常慢的运算符,因为你总是在最后匹配 只需使用"

  • 进行检查即可
  • 除非你期待多场比赛(这很可能, 但会破坏你的字典)然后你想要排除匹配 来自未来循环的结果,你也想要突破当前 在第一场比赛中循环

  • 如果您在比较之前对列表进行排序,则更有可能进行匹配 在循环的早期

将其更改为代码并获得

EndsWith

你的代码在20秒钟内输入

两个测试都使用此代码生成测试数据

    //Test clocks at 137ms
    public static Dictionary<string, string> FullPathBuilderImproved(IEnumerable<string> partialPathList, IEnumerable<string> fullPathList)
    {
        Dictionary<string, string> result = new Dictionary<string, string>();
        partialPathList = partialPathList.OrderBy(s => string.Concat(s.Reverse()));
        List<string> unmatchedList = fullPathList.OrderBy(s =>string.Concat(s.Reverse())).ToList();

        foreach (string partPath in partialPathList)
        {
            string matchedFullPath = unmatchedList.FirstOrDefault(f => f.EndsWith(partPath));
            if (matchedFullPath != null)
            {
                result.Add(partPath, matchedFullPath);
                unmatchedList.Remove(matchedFullPath);
            }
            else
            {
                result.Add(partPath, partPath);

            }
        }
        return result;
    }
编辑:在评论中我认为Abbondanza反向函数会比我使用的反向连接更快

答案 3 :(得分:0)

我认为一个好方法是这样的:

Dictionary<string, string> result = List2.ToDictionary(s => s, 
                       s => List1.FirstOrDefault(f => f.EndsWith(s)) ?? s);

但我不擅长确定执行时间的确切行为。它通过修剪路径列表迭代一次。但是对于每个修剪的路径,扫描完整路径列表以查找匹配元素。所以我猜它就像O(n * logn)(虽然我甚至不知道这种表示法是否正确)。

要更新ThirdPartyRepackVariables的其他两个属性,您可以更改代码:

Dictionary<string, string> result = List2.ToDictionary(s => s, 
                       s =>
                       {
                           string v =  List1.FirstOrDefault(f => f.EndsWith(s));
                           if (v == null)
                           {
                               ThirdPartyRepackVariables.unmatchedPaths.Add(s);
                               return s;
                           }
                           ThirdPartyRepackVariables.pathsUpdated++;
                           return v;
                       });

如果您有一对一的匹配(因此对于每个修剪过的路径都有一条完整路径,另一条路径也是如此)您可以先将列表排序为Tim Copenhaver建议:

var sortedList1 = list1.OrderBy(s => new string(s.Reverse().ToArray())).ToList();
var sortedList2 = list2.OrderBy(s => new string(s.Reverse().ToArray())).ToList();

然后只需将它们变成字典:

Dictionary<string, string> result = Enumerable.Range(0, sortedList1.Count)
                       .ToDictionary(i => sortedList2[i], i => sortedList1[i]);

答案 4 :(得分:0)

执行此操作的一种方法是使用Aho-Corasick字符串匹配算法。我在https://www.informit.com/guides/content.aspx?g=dotnet&seqNum=869处有一个实现。

以下是您如何使用它:

// build the automaton
var matcher = new AhoCorasickStringSearcher();
foreach (var partial in partialList)
{
    matcher.AddItem(partial);
}
matcher.CreateFailureFunction();

// now search each line . . .
foreach (var line in fullPaths)
{
    var matches = matcher.Search(line);
    // here, if matches contains items, you can add them to your dictionary
}

这应该很快执行。

您需要阅读文章以了解有关如何使用它的更多详细信息,但这是它的要点。

那就是说,反转字符串和排序,然后运行标准合并算法的建议是一个很好的建议。它易于实施,而且可能更快。