【问题标题】:How to "zip" or "rotate" a variable number of lists?如何“压缩”或“旋转”可变数量的列表?
【发布时间】:2013-08-01 08:36:45
【问题描述】:

如果我有一个包含任意数量列表的列表,如下所示:

var myList = new List<List<string>>()
{
    new List<string>() { "a", "b", "c", "d" },
    new List<string>() { "1", "2", "3", "4" },
    new List<string>() { "w", "x", "y", "z" },
    // ...etc...
};

...有没有办法以某种方式将列表“压缩”或“旋转”成这样的东西?

{ 
    { "a", "1", "w", ... },
    { "b", "2", "x", ... },
    { "c", "3", "y", ... },
    { "d", "4", "z", ... }
}

显而易见的解决方案是这样做:

public static IEnumerable<IEnumerable<T>> Rotate<T>(this IEnumerable<IEnumerable<T>> list)
{
    for (int i = 0; i < list.Min(x => x.Count()); i++)
    {
        yield return list.Select(x => x.ElementAt(i));
    }
}

// snip

var newList = myList.Rotate();

...但我想知道是否有更清洁的方法,使用 linq 或其他方式?

【问题讨论】:

  • 为什么要专门用LINQ来解决?
  • @Moo-Juice -- 很好,我编辑了我的标题。我想我只是自动假设解决方案会以某种方式使用 linq,但我想不一定是这种情况。
  • 如果序列不是随机访问,您的示例代码效率非常低。
  • 我对另一个使用 LINQ here 的问题更受限制,并且是对您的问题的更直接的回答。

标签: c# algorithm linq


【解决方案1】:

您可以滚动您自己的 ZipMany 实例,该实例手动迭代每个枚举。在投影每个序列后,这可能会比使用 GroupBy 的更大序列表现更好:

public static IEnumerable<TResult> ZipMany<TSource, TResult>(
    IEnumerable<IEnumerable<TSource>> source,
    Func<IEnumerable<TSource>, TResult> selector)
{
   // ToList is necessary to avoid deferred execution
   var enumerators = source.Select(seq => seq.GetEnumerator()).ToList();
   try
   {
     while (true)
     {
       foreach (var e in enumerators)
       {
           bool b = e.MoveNext();
           if (!b) yield break;
       }
       // Again, ToList (or ToArray) is necessary to avoid deferred execution
       yield return selector(enumerators.Select(e => e.Current).ToList());
     }
   }
   finally
   {
       foreach (var e in enumerators) 
         e.Dispose();
   }
}

【讨论】:

  • 在手机上打字肯定需要很长时间...... :)
  • 我去给你清理了。我相信它抓住了你的意图。
  • @JeffMercado 你比我快 5 秒。我有几乎相同的代码(我测试过并且有效)。我认为这是一个很好的编辑。
  • 这种方法会留下未处理的枚举数。
  • @Oliver:啊,现在我明白你问题的主旨了。你是绝对正确的;我忽略了处理枚举器。我会解决的。
【解决方案2】:

您可以通过使用 Select 扩展名来完成此操作,并采用 Func&lt;T, int, TOut&gt;

var rotatedList = myList.Select(inner => inner.Select((s, i) => new {s, i}))
                        .SelectMany(a => a)
                        .GroupBy(a => a.i, a => a.s)
                        .Select(a => a.ToList()).ToList();

这会给你另一个List&lt;List&lt;string&gt;&gt;

细分

.Select(inner => inner.Select((s, i) => new {s, i}))

对于每个内部列表,我们将列表的内容投影到具有两个属性的新匿名对象:s,字符串值,i,原始列表中该值的索引。

.SelectMany(a => a)

我们将结果扁平化为一个列表

.GroupBy(a => a.i, a => a.s)

我们按匿名对象的i 属性分组(记住这是索引)并选择s 属性作为我们的值(仅限字符串)。

.Select(a => a.ToList()).ToList();

对于每个组,我们将 enumerable 更改为一个列表,并为所有组更改另一个列表。

【讨论】:

  • +1,但我可能认为 OP 的原始方法方式更具可读性。
  • 我也在那条船上,但仍然印象深刻
  • @Moo-Juice 我同意。它会产生更好的性能。我认为这是 LINQ 工作的边缘案例,但远非最佳。
  • +1 好答案。即使只是为了答案,而不是效率,这也是一个不错的解决方案。
  • @SimonBelanger:我修改了我的评论,假设你有一个排序的键顺序,它们 IGrouping 将按该顺序返回。
【解决方案3】:

SelectManyGroupBy 与一些索引一起使用怎么样?

// 1. Project inner lists to a single list (SelectMany)
// 2. Use "GroupBy" to aggregate the item's based on order in the lists
// 3. Strip away any ordering key in the final answer
var query = myList.SelectMany(
    xl => xl.Select((vv,ii) => new { Idx = ii, Value = vv }))
       .GroupBy(xx => xx.Idx)
       .OrderBy(gg => gg.Key)
       .Select(gg => gg.Select(xx => xx.Value));

来自 LinqPad:

【讨论】:

  • +1。这实际上更符合问题中使用的方法签名(IEnumerable&lt;IEnumerable&lt;T&gt;&gt;)。
【解决方案4】:

这是一个基于矩阵转置的低效变体:

public static class Ext
{
    public static IEnumerable<IEnumerable<T>> Rotate<T>(
        this IEnumerable<IEnumerable<T>> src)
    {
        var matrix = src.Select(subset => subset.ToArray()).ToArray();
        var height = matrix.Length;
        var width = matrix.Max(arr => arr.Length);

        T[][] transpose = Enumerable
            .Range(0, width)
            .Select(_ => new T[height]).ToArray();
        for(int i=0; i<height; i++)
        {        
            for(int j=0; j<width; j++)
            {            
                transpose[j][i] = matrix[i][j];            
            }
        }

        return transpose;
    }
}

【讨论】:

  • @Moo-Juice 效率不高,尽管可以改进。大量的排列正在进行。 :)
【解决方案5】:

看看linqlib project on codeplex,它有一个旋转功能,完全可以满足您的需求。

【讨论】:

    【解决方案6】:

    您可以使用Range 压缩for 循环:

    var result = Enumerable.Range(0, myList.Min(l => l.Count))
        .Select(i => myList.Select(l => l[i]).ToList()).ToList();
    

    【讨论】:

      【解决方案7】:
      (from count in Range(myList[0].Count)
      select new List<string>(
          from count2 in Range(myList.Count)
          select myList[count2][count])
          ).ToList();
      

      它并不漂亮,但我认为它会起作用。

      【讨论】:

      • 我认为这个解决方案只有在mylist 中有三个列表时才有效。无论有多少列表,我都希望有一个解决方案。
      • 好的,我认为我的编辑实现了这一点。不是 100% 确定,但基本的想法就在那里。
      【解决方案8】:

      这是一个递归的 ZipMany 实现,灵感来自 an answer@Enigmativity 到另一个问题。

      public static IEnumerable<IEnumerable<T>> ZipMany<T>(params IEnumerable<T>[] sources)
      {
          return ZipRecursive(sources);
          IEnumerable<IEnumerable<T>> ZipRecursive(IEnumerable<IEnumerable<T>> ss)
          {
              if (!ss.Skip(1).Any())
              {
                  return ss.First().Select(i => Enumerable.Repeat(i, 1));
              }
              else
              {
                  return ZipRecursive(ss.Skip(1).ToArray()).Zip(ss.First(), (x, y) => x.Prepend(y));
              }
          }
      }
      

      优点: 避免了与枚举器配置相关的问题。
      缺点: 扩展性差。递归开销在大约 3000 个可枚举时变得明显,之后呈指数增长。

      【讨论】:

        猜你喜欢
        • 2013-09-23
        • 2012-09-27
        • 1970-01-01
        • 1970-01-01
        • 2020-07-28
        • 1970-01-01
        • 2020-10-31
        • 2015-10-14
        • 1970-01-01
        相关资源
        最近更新 更多