使用LINQ通过位置合并两个列表的条目

5

假设我有两个列表,其条目如下:

List<int> a = new List<int> { 1, 2, 5, 10 };
List<int> b = new List<int> { 6, 20, 3 };

我想创建另一个列表c,其中的条目是从两个列表按位置插入的项目。因此,列表c将包含以下条目:

List<int> c = {1, 6, 2, 20, 5, 3, 10}

有没有一种使用LINQ在.NET中完成这个操作的方法?我看了一下.Zip() LINQ扩展,但不确定如何在这种情况下使用它。
提前感谢!

1
你想要:从列表1中选择一个项目,然后是从列表2中选择的项目,接着是从列表1中选择的下一个项目,以此类推?如果其中一个列表包含较少的元素,该怎么办?只需将另一个列表中剩余的元素丢弃吗?例如,如果列表分别为a1,a2,a3b1,b2,b3,b4,b5,您想要的结果是a1,b1,a2,b2,a3,b3,b4,b5 - Lasse V. Karlsen
因为在.NET中使用LINQ有一种方法可以做到这一点。 - Lasse V. Karlsen
没错。我想要从列表1中取出一个项目,然后是列表2中的项目(相同索引)。我们还假设列表2的计数始终<=列表1中的项目计数。我希望有一个LINQ解决方案,最好是易读的。 :) - Eric
可能是重复的问题:如何将两个IEnumerables合并(或压缩)在一起? - Tim Schmelter
1
这种情况下,旧但经典的foreach循环会给出更易读的结果... 为什么你要使用Linq? - user1162766
显示剩余3条评论
6个回答

12

使用LINQ完成此操作,您可以使用下面这段LINQPad示例代码:


void Main()
{
    List<int> a = new List<int> { 1, 2, 5, 10 };
    List<int> b = new List<int> { 6, 20, 3 };

    var result = Enumerable.Zip(a, b, (aElement, bElement) => new[] { aElement, bElement })
        .SelectMany(ab => ab)
        .Concat(a.Skip(Math.Min(a.Count, b.Count)))
        .Concat(b.Skip(Math.Min(a.Count, b.Count)));

    result.Dump();
}

输出:

LINQPad example output

这将会:

  • 将两个列表合并(当其中一个列表没有元素时停止)
  • 生成包含两个元素的数组(a中的一个元素和b中的另一个元素)
  • 使用 SelectMany 来“展平”它,使其成为一个值序列
  • 将剩余的列表连接在一起(只有两次调用 Concat 中的一次或零次才会添加任何元素)

现在,话虽如此,个人认为我会使用以下代码:

public static IEnumerable<T> Intertwine<T>(this IEnumerable<T> a, IEnumerable<T> b)
{
    using (var enumerator1 = a.GetEnumerator())
    using (var enumerator2 = b.GetEnumerator())
    {
        bool more1 = enumerator1.MoveNext();
        bool more2 = enumerator2.MoveNext();

        while (more1 && more2)
        {
            yield return enumerator1.Current;
            yield return enumerator2.Current;

            more1 = enumerator1.MoveNext();
            more2 = enumerator2.MoveNext();
        }

        while (more1)
        {
            yield return enumerator1.Current;
            more1 = enumerator1.MoveNext();
        }

        while (more2)
        {
            yield return enumerator2.Current;
            more2 = enumerator2.MoveNext();
        }
    }
}

原因:

  • 它没有重复枚举ab
  • 我对Skip的性能持怀疑态度
  • 它可以处理任何IEnumerable<T>,而不仅仅是List<T>

我非常喜欢第二种解决方案。看起来正是我需要的,而且扩展方法也非常方便。谢谢! - Eric
是的,我已经+1了,但这更好。我怀疑跳过在性能方面是否值得担心,但我想它会导致a或b枚举超过一次。我怀疑除此之外没有任何性能损失(你在第一个要点中已经涵盖了)。 - Chris
.Zip()不是期望两个列表具有相同数量的元素吗? - John Alexiou

3
我会创建一个扩展方法来完成它。
public static List<T> MergeAll<T>(this List<T> first, List<T> second)
{
    int maxCount = (first.Count > second. Count) ? first.Count : second.Count;
    var ret = new List<T>();
    for (int i = 0; i < maxCount; i++)
    {
        if (first.Count < maxCount)
            ret.Add(first[i]);
        if (second.Count < maxCount)
            ret.Add(second[i]);
    }

    return ret;
}

这将遍历两个列表一次。如果一个列表比另一个列表大,它将继续添加直到完成。

2
你可以尝试这段代码:
List<int> c = a.Select((i, index) => new Tuple<int, int>(i, index * 2))
               .Union(b.Select((i, index) => new Tuple<int, int>(i, index * 2 + 1)))
               .OrderBy(t => t.Second)
               .Select(t => t.First).ToList();

这段代码将两个集合合并为一个,并使用索引对该集合进行排序。第一个集合的元素具有偶数索引,第二个集合的元素具有奇数索引。


1
刚刚为此编写了一个小扩展程序:

public static class MyEnumerable
{
    public static IEnumerable<T> Smash<T>(this IEnumerable<T> one, IEnumerable<T> two)
    {
        using (IEnumerator<T> enumeratorOne = one.GetEnumerator(), 
                              enumeratorTwo = two.GetEnumerator())
        {
            bool twoFinished = false;

            while (enumeratorOne.MoveNext())
            {
                yield return enumeratorOne.Current;

                if (!twoFinished && enumeratorTwo.MoveNext())
                {
                    yield return enumeratorTwo.Current;
                }
            }

            if (!twoFinished)
            {
                while (enumeratorTwo.MoveNext())
                {
                    yield return enumeratorTwo.Current;
                }
            }
        }
    }
}

使用方法:

var a = new List<int> { 1, 2, 5, 10 };
var b = new List<int> { 6, 20, 3 };

var c = a.Smash(b); // 1, 6, 2, 20, 5, 3, 10

var d = b.Smash(a); // 6, 1, 20, 2, 3, 5, 10

这将适用于任何 IEnumerable,因此您也可以执行以下操作:
var a = new List<string> { "the", "brown", "jumped", "the", "lazy", "dog" };
var b = new List<string> { "quick", "dog", "over" };

var c = a.Smash(b); // the, quick, brown, fox, jumped, over, the, lazy, dog

变量 twoFinished 永远不会成为 true - maf-soft
不错的发现。随意修改! - dav_i

1
你可以使用 Concat 和一个按索引排序的匿名类型:
List<int> c = a
    .Select((val, index) => new { val, index })
    .Concat(b.Select((val, index) => new { val, index }))
    .OrderBy(x => x.index)
    .Select(x => x.val)
    .ToList();

然而,由于这并不优雅,也比以下方法效率低:
c = new List<int>(a.Count + b.Count);
int max = Math.Max(a.Count, b.Count);
int aMax = a.Count;
int bMax = b.Count;
for (int i = 0; i < max; i++)
{ 
    if(i < aMax)
        c.Add(a[i]);
    if(i < bMax)
        c.Add(b[i]);
}

我根本不会使用LINQ。


0

抱歉我添加了第三个扩展方法,受前两个启发,但我喜欢更短的形式:

static IEnumerable<T> Intertwine<T>(this IEnumerable<T> a, IEnumerable<T> b)
{
    using (var enumerator1 = a.GetEnumerator())
    using (var enumerator2 = b.GetEnumerator()) {
        bool more1 = true, more2 = true;
        do {
            if (more1 && (more1 = enumerator1.MoveNext()))
                yield return enumerator1.Current;
            if (more2 && (more2 = enumerator2.MoveNext()))
                yield return enumerator2.Current;
        } while (more1 || more2);
    }
}

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