使用LINQ C#旋转-转置一个List<List<string>>列表

16

我有一个List<List<string>>,从远程数据源(即WCF)返回。因此,我需要使用LINQ将以下数据修改为用户友好的列表

对应的C#代码:

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"}
}

适当的屏幕截图: 现有

在此输入图像描述

我需要将数据旋转如下屏幕截图:拟议中

在此输入图像描述

请帮助我使用LINQ C#旋转数据。


主列表里面总是会有两个子列表吗? - vmutafov
@vmutafov 是的。List<List<string>> - B.Balamanigandan
问题是:PersonInfo 是否总是包含两个列表,还是可能会有超过2个? - Dennis_E
我不确定你是否理解我的意思。我的意思是,名字列表和数字列表总是会有一个吗? - vmutafov
刚刚在谷歌上搜索了 c# 旋转2D列表 linq,找到了这个链接:https://dev59.com/Jmw05IYBdhLWcg3w6GAw - Radinator
2
这被称为“转置”,而不是“旋转”。 - Good Night Nerd Pride
7个回答

32

这是一个简单而灵活的解决方案,它可以处理任意维度的多个内部列表。

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"}
};


var result = PersonInfo
    .SelectMany(inner => inner.Select((item, index) => new { item, index }))
    .GroupBy(i => i.index, i => i.item)
    .Select(g => g.ToList())
    .ToList();

我意识到这可能会重新激活这个线程,但由于我并不完全熟悉VB.net的这一部分,我想知道如果输入列表不平衡,是否可以插入一个Nothing或类似的占位符值? - pmackni
3
完全有可能。我建议您提出一个单独的问题——如何将一个不规则数组转换为一个矩形数组,并用特定的值填充空缺。 - Oliver
惊讶于这个代码可以工作(它确实可以)!这在很大程度上取决于 GroupBy 的实现方式,因为它保证保留分组的顺序,并且还保留了组内元素的顺序!话虽如此,我认为您可以省略 .ToList() 调用,因为 IEnumerable 在底层是数组 - 3dGrabber
1
@3dGrabber .ToList() 的调用是为了满足问题中指定的类型。它们对于转置并不是必需的,因此你可以完全省略最后两行。 - Oliver

10

这是一个通用的扩展方法

public static IEnumerable<IEnumerable<T>> Pivot<T>(this IEnumerable<IEnumerable<T>> source)
{
    var enumerators = source.Select(e => e.GetEnumerator()).ToArray();
    try
    {
        while (enumerators.All(e => e.MoveNext()))
        {
            yield return enumerators.Select(e => e.Current).ToArray();
        }
    }
    finally
    {
        Array.ForEach(enumerators, e => e.Dispose());
    }
}

所以你可以

var result = PersonInfo.Pivot();

2
假设在 PersonInfo 中只有两个列表:
var rotated = PersonInfo[0]
    .Zip(PersonInfo[1], (a, b) => new List<string> { a, b }).ToList();

如果PersonInfo里面可以有任意数量的列表:
Enumerable.Range(0, PersonInfo[0].Count)
    .Select(i => PersonInfo.Select(lst => lst[i]).ToList()).ToList();

2
您可以使用 Enumerable.RangeEnumerable.ElementAtOrDefault 两个方法:
List<List<string>> rotated = Enumerable.Range(0, PersonInfo.Max(list => list.Count))
 .Select(i => PersonInfo.Select(list => list.ElementAtOrDefault(i)).ToList())
 .ToList();

PersonInfo.Max(list => list.Count) 返回列表的最大大小。这将成为主列表的新大小,在本例中为 3。Enumerable.Range就像一个 for 循环。对于每个列表,它现在会选择这些索引处的所有字符串。如果大小不同,您将得到 null(因为 ElementAtOrDefault)。

如果列表具有相同的大小,则可以应用相同的查询以获取原始列表:

PersonInfo = Enumerable.Range(0, rotated.Max(list => list.Count))
 .Select(i => rotated.Select(list => list.ElementAtOrDefault(i)).ToList())
 .ToList();

作为扩展:

public static IEnumerable<IList<T>> Rotate<T>(this IEnumerable<IList<T>> sequences)
{
    var list = sequences as IList<IList<T>> ?? sequences.ToList();
    int maxCount = list.Max(l => l.Count);
    return Enumerable.Range(0, maxCount)
        .Select(i => list.Select(l => l.ElementAtOrDefault(i)).ToList());
}

使用方法:

IEnumerable<IList<string>> rotated = PersonInfo.Rotate();
IEnumerable<IList<string>> rotatedPersonInfo = rotated.Rotate(); // append ToList to get the original list

@Oliver:为什么删除?当然,列表必须具有相同的大小,但这在这种情况下是成立的。但还是谢谢你指出来,我已经修复了我的答案。 - Tim Schmelter
我读成了 Min 而不是 Max(呵呵),所以我指的是数据丢失而不是额外的值。虽然不同但仍然有区别! - Oliver

0

只需像这样做:

var persons = Enumerable.Range(0, PersonInfo.First().Count()).Select(i => PersonInfo.Select(e => e[i]).ToList()).ToList();

或者

var persons = Enumerable.Range(0, PersonInfo[0].Count()).Select(i => {
    return PersonInfo.Select(e => {
        return e[i];
    }).ToList();
}).ToList();

然后检查结果如下:

persons.ForEach(p => Console.WriteLine("{0} {1}", p[0], p[1]));

0

这将Zip的思想扩展到任意数量的列表。Zip将截断行列表到最小秩。

List<List<string>> PersonInfo = new List<List<string>>()
{
    new List<string>() {"John", "Peter", "Watson"},
    new List<string>() {"1000", "1001", "1002"},
    new List<string>() {"2000", "2001", "2002"},
    new List<string>() {"3000", "3001", "3002"}
};

var seed = Enumerable.Empty<List<string>>();
var transformed = PersonInfo.Aggregate(seed, (acc, r) =>
   acc.Any()
 ? acc.Zip(r, (row, nextElement) => { row.Add(nextElement); return row; })
 : r.Select(e => new List<string> { e }) //initialize target list using first row
); 

-1

试试这个:

List<List<string>> PersonInfo = new List<List<string>>(){
new List<string>() {"John", "Peter", "Watson"},
new List<string>() {"1000", "1001", "1002"}};

List<List<string>> PivitedPersonInfo = new List<List<string>>();
for (int i = 0; i < PersonInfo.First().Count; i++)
{
    PivitedPersonInfo.Add(PersonInfo.Select(x => x.ElementAt(i)).ToList());
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接