查找出现在所有列表(或数组或集合)中的值

时间:2015-09-21 09:44:13

标签: c# algorithm

鉴于以下内容:

List<List<int>> lists = new List<List<int>>();

lists.Add(new List<int>() { 1,2,3,4,5,6,7 });
lists.Add(new List<int>() { 1,2 });
lists.Add(new List<int>() { 1,2,3,4 });
lists.Add(new List<int>() { 1,2,5,6,7 });

确定所有列表中出现哪些数字的最佳/最快方法是什么?

5 个答案:

答案 0 :(得分:5)

您可以使用.net 3.5。Intersect()扩展方法: -

List<int> a = new List<int>() { 1, 2, 3, 4, 5 };
List<int> b = new List<int>() { 0, 4, 8, 12 };

List<int> common = a.Intersect(b).ToList();

答案 1 :(得分:4)

Linq已提供Intersect,您也可以利用Aggregate

var result = lists.Aggregate((a, b) => a.Intersect(b).ToList());

答案 2 :(得分:4)

要为两个列表执行此操作,可以使用x.Intersect(y)

要做好几个我们想要做的事情:

var intersection = lists.Aggregate((x, y) => x.Intersect(y));

但是这不起作用,因为lambda的结果不是List<int>,因此无法反馈。这可能会诱使我们尝试:

var intersection = lists.Aggregate((x, y) => x.Intersect(y).ToList());

但是这会对ToList()进行n-1次不必要的调用,而这种调用相对较贵。我们可以通过以下方式解决这个问题:

var intersection = lists.Aggregate(
  (IEnumerable<int> x, IEnumerable<int> y) => x.Intersect(y));

这适用于相同的逻辑,但是在lambda中使用显式类型时,我们可以将Intersect()的结果反馈回来而不会浪费时间和内存每次创建一个列表,从而提供更快的结果。

如果这出现了很多,我们可以通过滚动我们自己而不是使用Linq来进一步(轻微)性能提升:

public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source)
{
  using(var en = source.GetEnumerator())
  {
    if(!en.MoveNext()) return Enumerable.Empty<T>();
    var set = new HashSet<T>(en.Current);
    while(en.MoveNext())
    {
      var newSet = new HashSet<T>();
      foreach(T item in en.Current)
        if(set.Remove(item))
          newSet.Add(item);
      set = newSet;
    }
    return set;
  }
}

这假定它仅供内部使用。如果它可以从另一个程序集调用它应该有错误检查,也许应该定义,以便只对调用代码的第一个MoveNext()执行交叉操作:

public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source)
{
  if(source == null)
    throw new ArgumentNullException("source");
  return IntersectAllIterator(source);
}
public static IEnumerable<T> IntersectAllIterator<T>(IEnumerable<IEnumerable<T>> source)
{
  using(var en = source.GetEnumerator())
  {
    if(en.MoveNext())
    {
      var set = new HashSet<T>(en.Current);
      while(en.MoveNext())
      {
        var newSet = new HashSet<T>();
        foreach(T item in en.Current)
          if(set.Remove(item))
            newSet.Add(item);
        set = newSet;
      }
      foreach(T item in set)
        yield return item;
    }
  }
}

(在这最后两个版本中,如果我们最终清空套装,那么就有机会进行短路,但如果这种情况相对经常发生,它只会得到回报,否则会导致净损失。) / p>

相反,如果这些不是问题,如果我们知道我们只是想要对列表执行此操作,我们可以使用Count进一步优化和指数:

public static IEnumerable<T> IntersectAll<T>(this List<List<T>> source)
{
  if (source.Count == 0) return Enumerable.Empty<T>();
  if (source.Count == 1) return source[0];
  var set = new HashSet<T>(source[0]);
  for(int i = 1; i != source.Count; ++i)
  {
    var newSet = new HashSet<T>();
    var list = source[i];
    for(int j = 0; j != list.Count; ++j)
    {
      T item = list[j];
      if(set.Remove(item))
        newSet.Add(item);
    }
    set = newSet;
  }
  return set;
}

而且,如果我们知道我们总是希望将结果放在列表中,并且我们知道要么我们不会改变列表,要么输入列表无关紧要变异了,我们可以优化零或一个列表的情况(但如果我们可能不需要列表中的输出,这会花费更多):

public static List<T> IntersectAll<T>(this List<List<T>> source)
{
  if (source.Count == 0) return new List<T>(0);
  if (source.Count == 1) return source[0];
  var set = new HashSet<T>(source[0]);
  for(int i = 1; i != source.Count; ++i)
  {
    var newSet = new HashSet<T>();
    var list = source[i];
    for(int j = 0; j != list.Count; ++j)
    {
      T item = list[j];
      if(set.Remove(item))
        newSet.Add(item);
    }
    set = newSet;
  }
  return new List<T>(set);
}

然而,除了使该方法不太广泛适用之外,这在使用方式方面存在风险,因此只有在您不知道要改变的情况下才适用于内部代码事后的输入或输出,或者这不重要。

答案 3 :(得分:1)

如果你不相信Intersect方法,或者你只是想看看发生了什么,这里有一段代码可以解决这个问题:

  // Output goes here
  List<int> output = new List<int>();

  // Make sure lists are sorted
  for (int i = 0; i < lists.Count; ++i) lists[i].Sort();

  // Maintain array of indices so we can step through all the lists in parallel
  int[] index = new int[lists.Count];

  while(index[0] < lists[0].Count)
  {
    // Search for each value in the first list
    int value = lists[0][index[0]];

    // No. lists that value appears in, we want this to equal lists.Count
    int count = 1;

    // Search all the other lists for the value
    for (int i = 1; i < lists.Count; ++i)
    {
      while (index[i] < lists[i].Count)
      {
        // Stop if we've passed the spot where value would have been
        if (lists[i][index[i]] > value) break;

        // Stop if we find value
        if (lists[i][index[i]] == value)
        {
          ++count; 
          break; 
        }

        ++index[i];
      }

      // If we reach the end of any list there can't be any more matches so end the search now
      if (index[i] >= lists[i].Count) goto done;
    }

    // Store the value if we found it in all the lists
    if (count == lists.Count) output.Add(value);

    // Skip multiple occurrances of the same value
    while (index[0] < lists[0].Count && lists[0][index[0]] == value) ++index[0];
  }

  done:

修改

我对此感到无聊,并对此与Jon Hanna的版本做了一些基准测试。他的速度一直很快,通常在50%左右。如果碰巧有预先排序的列表,我的赢利差不多。此外,您还可以获得20%左右的不安全优化。我以为我会分享那个。

答案 4 :(得分:0)

您也可以使用SelectManyDistinct

来获取它
List<int> result = lists
                   .SelectMany(x => x.Where(e => lists.All(l => l.Contains(e))))
                   .Distinct().ToList();

编辑:

List<int> result2 = lists.First().Where(e => lists.Skip(1).All(l => l.Contains(e)))
                         .ToList();

编辑2:

List<int> result3 = lists
        .Select(l => l.OrderBy(n => n).Take(lists.Min(x => x.Count()))).First()
        .TakeWhile((n, index) => lists.Select(l => l.OrderBy(x => x)).Skip(1).All(l => l.ElementAt(index) == n))
        .ToList();