如何使用LINQ将列表垂直分成n个部分?

6

我想将一个列表分成若干部分,不知道在列表中会有多少项。这个问题不同于那些想要将列表分成固定大小块的人。

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

我希望值能够垂直分割

分为2个:

-------------------
| item 1 | item 6 |
| item 2 | item 7 |
| item 3 | item 8 |
| item 4 | item 9 |
| item 5 |        |

分成三个部分:

| item 1 | item 4 | item 7 |
| item 2 | item 5 | item 8 |
| item 3 | item 6 | item 9 |

分成4个部分:

| item 1 | item 4 | item 6 | item 8 |
| item 2 | item 5 | item 7 | item 9 |
| item 3 |        |        |        |

我找到了几个可以实现此功能的C#扩展,但它们没有按我想要的方式分配值。以下是我发现的内容:
// this technic is an horizontal distribution
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
{
    int i = 0;
    var splits = from item in list
                    group item by i++ % parts into part
                    select part.AsEnumerable();
    return splits;
}

结果是这样的,但我的问题是值水平分布
| item 1 | item 2 |
| item 3 | item 4 |
| item 5 | item 6 |
| item 7 | item 8 |
| item 9 |        |

或者

| item 1 | item 2 | item 3 |
| item 4 | item 5 | item 6 |
| item 7 | item 8 | item 9 |

任何想法如何在垂直方向上分配我的数值并有可能选择我想要的分割数量吗?

现实生活中

对于那些想知道我何时想要垂直拆分列表的情况,这里是我网站部分截图的屏幕截图:

所以你想把列表的前一半放在一个列表中,后一半放在另一个列表中。你知道列表中有多少个元素...也许你可以尝试创建一个列表,并将前一半的元素插入到那个新列表中...嗯... - ean5533
7
var list1 = a.Take(a.Length / 2).ToList(); var list2 = a.Skip(list1.Count).ToList(); - Ilia G
你是否想要将列表分成两个相等(类似)长度的列表,固定页面长度或根据内容值进行拆分?此外,您是否希望能够处理无限集合或固定集合?(显然,无限集合不可能平均分成两半) - Matthew Whited
1
@Oliver 乍一看似乎是这样,但是OP想要的结果与那个问题以及例如这个问题不同。 - Mario S
显示剩余4条评论
3个回答

15

使用.Take().Skip()方法,你可以:

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

int splitIndex = 4; // or (a.Length / 2) to split in the middle.

var list1 = a.Take(splitIndex).ToArray(); // Returns a specified number of contiguous elements from the start of a sequence.
var list2 = a.Skip(splitIndex).ToArray(); // Bypasses a specified number of elements in a sequence and then returns the remaining elements.
你可以使用.ToList()代替.ToArray(),如果你需要一个List<int>

编辑:

在您稍微修改(或澄清)了您的问题后,我猜这就是您所需要的内容:

public static class Extensions
{
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int parts)
    {
        var list = new List<T>(source);
        int defaultSize = (int)((double)list.Count / (double)parts);
        int offset = list.Count % parts;
        int position = 0;

        for (int i = 0; i < parts; i++)
        {
            int size = defaultSize;
            if (i < offset)
                size++; // Just add one to the size (it's enough).

            yield return list.GetRange(position, size);

            // Set the new position after creating a part list, so that it always start with position zero on the first yield return above.
            position += size;
        }
    }
}

使用它:

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var lists = a.Split(2);

这将生成:

分成2部分:a.Split(2);

| item 1 | item 6 |
| item 2 | item 7 |
| item 3 | item 8 |
| item 4 | item 9 |
| item 5 |        |

将a分为3部分:a.Split(3);

| item 1 | item 4 | item 7 |
| item 2 | item 5 | item 8 |
| item 3 | item 6 | item 9 |

将字符串分成4个部分: a.Split(4);

| item 1 | item 4 | item 6 | item 8 |
| item 2 | item 5 | item 7 | item 9 |
| item 3 |        |        |        |

另外,如果你已经有了:

int[] b = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 10 items

将其分为4块:b.Split(4);

| item 1 | item 4 | item 7 | item 9 |
| item 2 | item 5 | item 8 | item 10|
| item 3 | item 6 |        |        |

1
代码很好,但我想要一个函数,可以将列表分成我想要的部分。我的问题没有表达清楚,我已经更新了描述以更准确地展示我想要做什么。对此感到抱歉! - Alexandre Jobin
@AlexandreJobin 我已经更新了答案。这是你想要的吗? - Mario S
@AlexandreJobin 我必须说我不太清楚为什么和在哪里会这样分割列表,但它有点引人入胜 =) - Mario S
1
@jessehouwing 是的,当我写下这段代码时已经很晚了。我已经更新了这个方法,这样它就不会遍历列表那么多次了。虽然,我认为至少要遍历一次列表才能得到所需的结果,这是不可避免的。 - Mario S
1
@Mario,我已经更新了我的描述,展示了一个真实的场景,我需要将我的列表垂直地分成几部分。谢谢你的代码,它运行得非常好! - Alexandre Jobin

1

这似乎非常好地解决了问题。可能还可以更有效地完成,但这已经足够令人费解了... 这样做要容易得多:

1|4|7|10
2|5|8
3|6|9

然后:

1|4|7|9
2|5|8|10
3|6|

起初我忽略了LINQ请求,因为我很难理解它。使用普通数组操作的解决方案可能会得到类似这样的结果:
    public static IEnumerable<IEnumerable<TListItem>> Split<TListItem>(this IEnumerable<TListItem> items, int parts)
        where TListItem : struct
    {
        var itemsArray = items.ToArray();
        int itemCount = itemsArray.Length;
        int itemsOnlastRow = itemCount - ((itemCount / parts) * parts);
        int numberOfRows = (int)(itemCount / (decimal)parts) + 1;

        for (int row = 0; row < numberOfRows; row++)
        {
            yield return SplitToRow(itemsArray, parts, itemsOnlastRow, numberOfRows, row);
        }
    }

    private static IEnumerable<TListItem> SplitToRow<TListItem>(TListItem[] items, int itemsOnFirstRows, int itemsOnlastRow,
                                                                int numberOfRows, int row)
    {
        for (int column = 0; column < itemsOnFirstRows; column++)
        {
            // Are we on the last row?
            if (row == numberOfRows - 1)
            {
                // Are we within the number of items on that row?
                if (column < itemsOnlastRow)
                {
                    yield return items[(column + 1) * numberOfRows -1];
                }
            }
            else
            {
                int firstblock = itemsOnlastRow * numberOfRows;
                int index;

                // are we in the first block?
                if (column < itemsOnlastRow)
                {
                    index = column*numberOfRows + ((row + 1)%numberOfRows) - 1;
                }
                else
                {
                    index = firstblock + (column - itemsOnlastRow)*(numberOfRows - 1) + ((row + 1)%numberOfRows) - 1;
                }

                yield return
                    items[index];
            }
        }
    }

LINQ 伪代码如下:
//WARNING: DOES NOT WORK
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
{
    int itemOnIndex = 0;
    var splits = from item in list
                 group item by MethodToDefineRow(itemOnIndex++) into row
                 select row.AsEnumerable();
    return splits;
}

但是如果不知道物品的数量,就无法计算放置它的位置。

因此,通过进行一些预先计算,您可以使用LINQ实现与上述相同的功能,这需要两次遍历IEnumerable,似乎没有绕过这个问题的方法。诀窍是计算将分配给每个值的行。

    //WARNING: Iterates the IEnumerable twice
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> list, int parts)
    {
        int itemOnIndex = 0;
        int itemCount = list.Count();
        int itemsOnlastRow = itemCount - ((itemCount / parts) * parts);
        int numberOfRows = (int)(itemCount / (decimal)parts) + 1;
        int firstblock = (numberOfRows*itemsOnlastRow);

        var splits = from item in list
                     group item by (itemOnIndex++ < firstblock) ? ((itemOnIndex -1) % numberOfRows) : ((itemOnIndex - 1 - firstblock) % (numberOfRows - 1)) into row
                     orderby row.Key
                     select row.AsEnumerable();
        return splits;
    }

-2
使用.Take(#OfElements)指定要获取的元素数量。
您还可以使用.First.Last

.First 只会返回一个值(在没有谓词的情况下为 1),而 .Last 同样如此(只不过如果没有谓词,则为 9)。 - Mario S
太棒了,我的亲爱的沃森! - jordan

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