Как "застегнуть" или "повернуть" переменное количество списков?

Если у меня есть список, содержащий произвольное количество списков, например:

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 или иначе?

Ответ 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();
   }
}

Ответ 2

Вы можете сделать это, используя расширение Select с помощью Func<T, int, TOut>:

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<List<string>>.

Структура

.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();

Для каждой группы мы перечислили перечисление в список и другой список для всех групп.

Ответ 3

Как насчет использования SelectMany и GroupBy с некоторыми индексами?

// 1. Project inner lists to a single list (SelectMany)
// 2. Use "GroupBy" to aggregate the item 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:

we groupa da items

Ответ 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;
    }
}

Ответ 5

Взгляните на проект linqlib на codeplex, он имеет функцию поворота, которая делает именно то, что вам нужно.

Ответ 6

Вы можете конденсировать циклы for, используя Range:

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();

Это не очень, но я думаю, что это сработает.

Ответ 8

Вот рекурсивная реализация ZipMany вдохновленная ответом @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 перечислимых значениях и впоследствии растут в геометрической прогрессии.