【问题标题】:How to match 2 lists most effectively (fast)?如何最有效地(快速)匹配 2 个列表?
【发布时间】:2010-11-18 01:41:38
【问题描述】:

我有 2 个lists<string> 的项目,源和目标。源列表中的项目将在目标列表中有 0 到 n 个匹配项,但不会有重复匹配项。

考虑到两个列表都已排序,您将如何在性能方面最有效地进行匹配。

例子:

source = {"1", "2", "A", "B", ...}
target = {"1 - new music", "1 / classic", "1 | pop", "2 edit", "2 no edit", "A - sing", "B (listen)", ...}

基本上匹配是简单的前缀匹配,但是假设你有一个名为MatchName的方法。如果您要进行更优化的搜索,您可以使用新功能。 NameMatch 只是比较两个字符串并返回一个布尔值。

最后 source[0] 将有 source[0]。在这种情况下匹配包含 target[0, 1 和 2]。

【问题讨论】:

  • 这和你的其他问题本质上是不是一模一样:stackoverflow.com/questions/1250853/…
  • 我想在这个中,您可以根据元素已排序的知识添加更多优化。
  • 它们很相似,但这个关注的是循环。我想知道是否必须为源列表中的每个项目循环目标列表。
  • 不,您应该只能在排序后循环一次。
  • 更新了我的答案。认为这会奏效。

标签: c# .net performance optimization search


【解决方案1】:

我不确定这是否值得尝试优化。你可以用这个实现某种二进制搜索,但它的有效性会相当有限。我们在谈论多少个元素?

目标中没有不匹配的元素

假设列表已排序,并且target 中不存在无法与source 匹配的元素:

static List<string>[] FindMatches(string[] source, string[] target)
{
    // Initialize array to hold results
    List<string>[] matches = new List<string>[source.Length];
    for (int i = 0; i < matches.Length; i++)
        matches[i] = new List<string>();

    int s = 0;
    for (int t = 0; t < target.Length; t++)
    {
        while (!MatchName(source[s], target[t]))
        {
            s++;
            if (s >= source.Length)
                return matches;
        }

        matches[s].Add(target[t]);
    }

    return matches;
}

带有不匹配的元素

如果target 中存在的元素可能与source 中不匹配,则上述将中断(如果元素不在目标末尾)。为了解决这个问题,最好使用不同的实现进行比较。我们需要它返回“小于”、“等于”或“大于”而不是布尔值,就像在排序中使用的比较器一样:

static List<string>[] FindMatches(string[] source, string[] target)
{
    // Initialize array to hold results
    List<string>[] matches = new List<string>[source.Length];
    for (int i = 0; i < matches.Length; i++)
        matches[i] = new List<string>();

    int s = 0;
    for (int t = 0; t < target.Length; t++)
    {
        int m = CompareName(source[s], target[t]);
        if (m == 0)
        {
            matches[s].Add(target[t]);
        }
        else if (m > 0)
        {
            s++;
            if (s >= source.Length)
                return matches;
            t--;
        }
    }

    return matches;
}

static int CompareName(string source, string target)
{
    // Whatever comparison you need here, this one is really basic :)
    return target[0] - source[0];
}

两者在其他方面基本相同。如您所见,您循环遍历目标元素一次,当您不再找到匹配项时将索引推进到源数组。

如果源元素的数量有限,则可能值得进行更智能的搜索。如果源元素的数量也很大,那么假定的好处就会减少。

再一次,第一个算法需要 0.18 秒,在我的机器上使用 100 万个目标元素,处于调试模式。第二个更快(0.03 秒),但这是因为正在进行的比较更简单。可能您必须将所有内容都与第一个空白字符进行比较,从而显着降低速度。

【讨论】:

  • 程序启动时不只有一次。
  • 我对这个答案的唯一问题是,如果目标列表中有源列表无法​​匹配的元素,它会失败。当然,这可能很好!
  • @Andrew:我添加了一种不同的风格来解释这种可能性,但我已经远离了 OP 提供的 NameMatch 签名。您需要一个完整的比较器(小于、等于、大于),就像在您的示例中一样。
  • @Thorarin 我很想写这样的东西,但我不想增加摆弄 OP 循环计数器的复杂性。
【解决方案2】:

随着项目的排序,您可以循环遍历列表:

string[] source = {"1", "2", "A", "B" };
string[] target = { "1 - new music", "1 / classic", "1 | pop", "2 edit", "2 no edit", "A - sing", "B (listen)" };

List<string>[] matches = new List<string>[source.Length];
int targetIdx = 0;
for (int sourceIdx = 0; sourceIdx < source.Length; sourceIdx++) {
   matches[sourceIdx] = new List<string>();
   while (targetIdx < target.Length && NameMatch(source[sourceIdx], target[targetIdx])) {
      matches[sourceIdx].Add(target[targetIdx]);
      targetIdx++;
   }
}

【讨论】:

  • 与我的实现基本相同,但交换了源和目标循环。出于某种原因,编译器似乎不太喜欢这种解决方案。可能是因为for循环有一些优化,目标元素的数量大于源元素的数量。无论如何,差异非常小,可以说您的版本可能更容易理解,因为它不使用否定逻辑?
【解决方案3】:

这是一个仅循环遍历两个列表一次的答案,使用两个列表作为优化排序的逻辑。就像大多数人所说的那样,我不会太担心优化,因为任何这些答案都可能足够快,我会选择最具可读性和可维护性的解决方案。

话虽如此,我的咖啡需要一些处理,所以你去吧。下面的优点之一是它允许目标列表中的内容在源列表中没有匹配项,尽管我不确定您是否需要该功能。

class Program
{
    public class Source
    {
        private readonly string key;
        public string Key { get { return key;}}

        private readonly List<string> matches = new List<string>();
        public List<string> Matches { get { return matches;} }

        public Source(string key)
        {
            this.key = key;
        }
    }

    static void Main(string[] args)
    {
        var sources = new List<Source> {new Source("A"), new Source("C"), new Source("D")};
        var targets = new List<string> { "A1", "A2", "B1", "C1", "C2", "C3", "D1", "D2", "D3", "E1" };

        var ixSource = 0;
        var currentSource = sources[ixSource++];

        foreach (var target in targets)
        {
            var compare = CompareSourceAndTarget(currentSource, target);

            if (compare > 0)
                continue;

            // Try and increment the source till we have one that matches 
            if (compare < 0)
            {
                while ((ixSource < sources.Count) && (compare < 0))
                {
                    currentSource = sources[ixSource++];
                    compare = CompareSourceAndTarget(currentSource, target);
                }
            }

            if (compare == 0)
            {
                currentSource.Matches.Add(target);
            }

            // no more sources to match against
            if ((ixSource > sources.Count))
                break;
        }

        foreach (var source in sources)
        {
            Console.WriteLine("source {0} had matches {1}", source.Key, String.Join(" ", source.Matches.ToArray()));
        }
    }

    private static int CompareSourceAndTarget(Source source, string target)
    {
        return String.Compare(source.Key, target.Substring(0, source.Key.Length), StringComparison.OrdinalIgnoreCase);
    }
}

【讨论】:

  • 嗯,我喜欢的条件太多了,我发现你迭代源代码的方式有点令人困惑。也许只是还没有喝咖啡。另一方面,它确实将匹配项放在源对象中,如问题所述,但我认为这是留给 OP 的练习,为了代码简洁:)
  • 当 (compare
  • 好吧,我的新版本(考虑了无法匹配的元素)也不是那么漂亮。特别是t-- 来抵消for 循环中的进步,但替代方案是while 循环和continue 的一些狡猾使用:P
【解决方案4】:

已编辑、重写、未经测试,应该具有 O(source + target) 性能。 用法可以是 MatchMaker.Match(source, target).ToList();

public static class MatchMaker
{
    public class Source
    {
        char Term { get; set; }
        IEnumerable<string> Results { get; set; }
    }

    public static IEnumerable<Source> Match(IEnumerable<string> source, IEnumerable<string> target)
    {
        int currentIndex = 0;
        var matches = from term in source
                      select new Source
                      {
                          Term = term[0],
                          Result = from result in target.FromIndex(currentIndex)
                                       .TakeWhile((r, i) => {
                                           currentIndex = i;
                                           return r[0] == term[0];
                                       })
                                   select result
                      };
    }
    public static IEnumerable<T> FromIndex<T>(this IList<T> subject, int index)
    {
        while (index < subject.Count) {
            yield return subject[index++];
        }
    }
}

一个简单的 LinQ,可能不是最快,但最清晰:

var matches = from result in target
              from term in source
              where result[0] == term[0]
              select new {
              Term: term,
              Result: result
              };

我反对过早优化。

【讨论】:

  • 谢谢,你的第三行应该是术语而不是来源吗?我不明白你为什么索引为 [0]?
  • 确实是术语。并索引 0 以获取它的 char 值。你不能比较字符串和字符。但是你的问题有点不清楚。现在我明白了,这可能无法满足您的需求。
  • 这是 O(n^2),所以考虑到标题中包含“快速”一词,可能还不够好。
  • 虽然我还是在进行基准测试,但这个 LINQ 解决方案需要 0.87 秒来处理 1M 个元素(我添加了一个 GroupBy)。真的还不算太糟糕。
【解决方案5】:

既然都是排序的,不就是一个基本的O(N)合并循环吗?

ia = ib = 0;
while(ia < na && ib < nb){
  if (A[ia] < B[ib]){
    // A[ia] is unmatched
    ia++;
  }
  else if (B[ib] < A[ia]){
    // B[ib] is unmatched
    ib++;
  }
  else {
    // A[ia] matches B[ib]
    ia++;
    ib++;
  }
}
while(ia < na){
  // A[ia] is unmatched
  ia++;
}
while(ib < nb){
  // B[ib] is unmatched
  ib++;
}

【讨论】:

    【解决方案6】:

    我认为最好的方法是准备一个索引。像这样(Javascript)

    index = [];
    index["1"] = [0,1,2];
    index["2"] = [3,4];
    

    在这种情况下,实际上并不需要排序良好的列表。

    【讨论】:

    • 这适用于 JavaScript,但是什么 C# 数据结构支持这一点?如果不匹配 &lt;*,*&lt;*&gt;&gt;,我会给你 +1。
    • Arpit,这需要不那么快的数据结构字典。
    【解决方案7】:

    好吧,一旦您越过当前源前缀,您显然就会停止循环遍历目标列表。在这种情况下,您最好使用前缀方法而不是匹配方法,这样您就可以知道当前前缀是什么,并在超过它时停止搜索目标。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-05
      • 2016-05-06
      • 2017-06-04
      • 1970-01-01
      • 2018-08-21
      • 2019-12-04
      相关资源
      最近更新 更多