Как "застегнуть" или "повернуть" переменное количество списков?
Если у меня есть список, содержащий произвольное количество списков, например:
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 перечислимых значениях и впоследствии растут в геометрической прогрессии.