如何最有效地(快速)匹配2个列表?

时间:2009-08-09 09:14:01

标签: c# .net performance optimization search

我有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只比较2个字符串并返回一个bool。

在最后,源[0]将包含源[0] .Matches包含目标[0,1和2]。

7 个答案:

答案 0 :(得分:3)

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

目标

中没有不匹配的元素

假设列表已排序,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中没有匹配,则上述内容将中断(如果元素不在目标的末尾)。要解决这个问题,最好采用不同的实现进行比较。我们需要它返回'小于','相等'或'大于',而不是布尔值,就像在排序中使用Comparer一样:

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秒),但这是因为正在进行更简单的比较。可能你必须将所有内容与第一个空白字符进行比较,使其显着变慢。

答案 1 :(得分:1)

编辑,重写,未经测试,应具有O(源+目标)性能。 用法可以是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
              };

我反对过早优化。

答案 2 :(得分:1)

在对项目进行排序时,您可以遍历列表:

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

答案 3 :(得分:1)

这是一个答案,它只使用两个列表作为优化排序的逻辑遍历两个列表。像大多数人所说的那样,我不会过于担心优化问题,因为对于任何这些答案来说,它可能足够快,我会选择最易读和可维护的解决方案。

话虽这么说,我需要和我的咖啡有关,所以你走了。下面的优点之一是它允许目标列表中的内容在源列表中没有匹配,但我不确定您是否需要该功能。

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

答案 4 :(得分:1)

由于它们是排序的,它不只是一个基本的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++;
}

答案 5 :(得分:0)

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

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

在这种情况下,并不真正需要排序良好的列表。

答案 6 :(得分:0)

当你超过当前的源前缀时,你显然会停止遍历目标列表。在这种情况下,使用前缀方法比使用匹配方法更好,这样您就可以知道当前前缀是什么,并且如果超过它就停止搜索目标。

相关问题