У меня есть несколько огромных отсортированных счетных последовательностей, которые я хочу объединить. Списками тезисов управляют как IEnumerable
но уже отсортированы. Так как входные списки отсортированы, должно быть возможно объединить их в одном прохождении, ничто не обращаясь.
Я хотел бы сохранить задержанное поведение при выполнении.
Я пытался записать наивный алгоритм, которые делают это (см. ниже). Однако это выглядит довольно ужасным, и я уверен, что это может быть оптимизировано. Это может существовать более академический алгоритм...
IEnumerable<T> MergeOrderedLists<T, TOrder>(IEnumerable<IEnumerable<T>> orderedlists,
Func<T, TOrder> orderBy)
{
var enumerators = orderedlists.ToDictionary(l => l.GetEnumerator(), l => default(T));
IEnumerator<T> tag = null;
var firstRun = true;
while (true)
{
var toRemove = new List<IEnumerator<T>>();
var toAdd = new List<KeyValuePair<IEnumerator<T>, T>>();
foreach (var pair in enumerators.Where(pair => firstRun || tag == pair.Key))
{
if (pair.Key.MoveNext())
toAdd.Add(pair);
else
toRemove.Add(pair.Key);
}
foreach (var enumerator in toRemove)
enumerators.Remove(enumerator);
foreach (var pair in toAdd)
enumerators[pair.Key] = pair.Key.Current;
if (enumerators.Count == 0)
yield break;
var min = enumerators.OrderBy(t => orderBy(t.Value)).FirstOrDefault();
tag = min.Key;
yield return min.Value;
firstRun = false;
}
}
Метод может использоваться как этот:
// Person lists are already sorted by age
MergeOrderedLists(orderedList, p => p.Age);
принятие следующего Person
класс существует где-нибудь:
public class Person
{
public int Age { get; set; }
}
Дубликаты должны быть сохранены, мы не заботимся об их порядке в новой последовательности. Вы видите какую-либо очевидную оптимизацию, которую я мог использовать?
Вот мой четвертый (спасибо @tanascius за продвижение этого к чему-то гораздо большему LINQ):
public static IEnumerable<T> MergePreserveOrder3<T, TOrder>(
this IEnumerable<IEnumerable<T>> aa,
Func<T, TOrder> orderFunc)
where TOrder : IComparable<TOrder>
{
var items = aa.Select(xx => xx.GetEnumerator()).Where(ee => ee.MoveNext())
.OrderBy(ee => orderFunc(ee.Current)).ToList();
while (items.Count > 0)
{
yield return items[0].Current;
var next = items[0];
items.RemoveAt(0);
if (next.MoveNext())
{
// simple sorted linear insert
var value = orderFunc(next.Current);
var ii = 0;
for ( ; ii < items.Count; ++ii)
{
if (value.CompareTo(orderFunc(items[ii].Current)) <= 0)
{
items.Insert(ii, next);
break;
}
}
if (ii == items.Count) items.Add(next);
}
else next.Dispose(); // woops! can't forget IDisposable
}
}
Результаты:
for (int p = 0; p < people.Count; ++p)
{
Console.WriteLine("List {0}:", p + 1);
Console.WriteLine("\t{0}", String.Join(", ", people[p].Select(x => x.Name)));
}
Console.WriteLine("Merged:");
foreach (var person in people.MergePreserveOrder(pp => pp.Age))
{
Console.WriteLine("\t{0}", person.Name);
}
List 1:
8yo, 22yo, 47yo, 49yo
List 2:
35yo, 47yo, 60yo
List 3:
28yo, 55yo, 64yo
Merged:
8yo
22yo
28yo
35yo
47yo
47yo
49yo
55yo
60yo
64yo
Улучшено с поддержкой Tuple .Net 4.0:
public static IEnumerable<T> MergePreserveOrder4<T, TOrder>(
this IEnumerable<IEnumerable<T>> aa,
Func<T, TOrder> orderFunc) where TOrder : IComparable<TOrder>
{
var items = aa.Select(xx => xx.GetEnumerator())
.Where(ee => ee.MoveNext())
.Select(ee => Tuple.Create(orderFunc(ee.Current), ee))
.OrderBy(ee => ee.Item1).ToList();
while (items.Count > 0)
{
yield return items[0].Item2.Current;
var next = items[0];
items.RemoveAt(0);
if (next.Item2.MoveNext())
{
var value = orderFunc(next.Item2.Current);
var ii = 0;
for (; ii < items.Count; ++ii)
{
if (value.CompareTo(items[ii].Item1) <= 0)
{ // NB: using a tuple to minimize calls to orderFunc
items.Insert(ii, Tuple.Create(value, next.Item2));
break;
}
}
if (ii == items.Count) items.Add(Tuple.Create(value, next.Item2));
}
else next.Item2.Dispose(); // woops! can't forget IDisposable
}
}
Я бы предположил, что это может улучшить ясность и производительность:
T
, IEnumerable
, упорядоченных в соответствии с вашим сравнением функция на T
IEnumerable
добавьте элемент в приоритетную очередь, аннотированный ссылкой на IEnumerable
, откуда он был создан IEnumerable
в его аннотацию, чтобы следующий элемент MoveNext ()
вернул истину, добавьте следующий элемент в приоритетную очередь, аннотированный ссылкой на IEnumerable
, который вы только что продвинули MoveNext ()
вернул false, ничего не добавляйте в очередь приоритетов Похоже, это очень полезная функция, поэтому я решил попробовать ее. Мой подход очень похож на hightechrider в том, что он разбивает проблему на объединение двух отсортированных IEnumerables в один, затем берет его и объединяет со следующим в списке. Скорее всего, вы можете провести некоторую оптимизацию, но она работает с моим простым тестовым примером:
public static IEnumerable<T> mergeSortedEnumerables<T>(
this IEnumerable<IEnumerable<T>> listOfLists,
Func<T, T, Boolean> func)
{
IEnumerable<T> l1 = new List<T>{};
foreach (var l in listOfLists)
{
l1 = l1.mergeTwoSorted(l, func);
}
foreach (var t in l1)
{
yield return t;
}
}
public static IEnumerable<T> mergeTwoSorted<T>(
this IEnumerable<T> l1,
IEnumerable<T> l2,
Func<T, T, Boolean> func)
{
using (var enumerator1 = l1.GetEnumerator())
using (var enumerator2 = l2.GetEnumerator())
{
bool enum1 = enumerator1.MoveNext();
bool enum2 = enumerator2.MoveNext();
while (enum1 || enum2)
{
T t1 = enumerator1.Current;
T t2 = enumerator2.Current;
//if they are both false
if (!enum1 && !enum2)
{
break;
}
//if enum1 is false
else if (!enum1)
{
enum2 = enumerator2.MoveNext();
yield return t2;
}
//if enum2 is false
else if (!enum2)
{
enum1 = enumerator1.MoveNext();
yield return t1;
}
//they are both true
else
{
//if func returns true then t1 < t2
if (func(t1, t2))
{
enum1 = enumerator1.MoveNext();
yield return t1;
}
else
{
enum2 = enumerator2.MoveNext();
yield return t2;
}
}
}
}
}
Затем, чтобы проверить это:
List<int> ws = new List<int>() { 1, 8, 9, 16, 17, 21 };
List<int> xs = new List<int>() { 2, 7, 10, 15, 18 };
List<int> ys = new List<int>() { 3, 6, 11, 14 };
List<int> zs = new List<int>() { 4, 5, 12, 13, 19, 20 };
List<IEnumerable<int>> lss = new List<IEnumerable<int>> { ws, xs, ys, zs };
foreach (var v in lss.mergeSortedEnumerables(compareInts))
{
Console.WriteLine(v);
}
Я подозреваю, что LINQ достаточно умен, чтобы воспользоваться существующим ранее порядком сортировки:
IEnumerable<string> BiggerSortedList = BigListOne.Union(BigListTwo).OrderBy(s => s);
Моя версия ответа sixlettervariables. Я уменьшил количество вызовов orderFunc (каждый элемент проходит через orderFunc только один раз), и в случае равенства сортировка пропускается. Это оптимизировано для небольшого числа источников, большего числа элементов в каждом источнике и, возможно, дорогого orderFunc.
public static IEnumerable<T> MergePreserveOrder<T, TOrder>(
this IEnumerable<IEnumerable<T>> sources,
Func<T, TOrder> orderFunc)
where TOrder : IComparable<TOrder>
{
Dictionary<TOrder, List<IEnumerable<T>>> keyedSources =
sources.Select(source => source.GetEnumerator())
.Where(e => e.MoveNext())
.GroupBy(e => orderFunc(e.Current))
.ToDictionary(g => g.Key, g => g.ToList());
while (keyedSources.Any())
{
//this is the expensive line
KeyValuePair<TOrder, List<IEnumerable<T>>> firstPair = keyedSources
.OrderBy(kvp => kvp.Key).First();
keyedSources.Remove(firstPair.Key);
foreach(IEnumerable<T> e in firstPair.Value)
{
yield return e.Current;
if (e.MoveNext())
{
TOrder newKey = orderFunc(e.Current);
if (!keyedSources.ContainsKey(newKey))
{
keyedSources[newKey] = new List<IEnumerable<T>>() {e};
}
else
{
keyedSources[newKey].Add(e);
}
}
}
}
}
Я готов поспорить, что это может быть улучшено с помощью SortedDictionary, но не настолько смел, чтобы попробовать решение с его помощью без редактора.
Вот решение без СОРТИРОВКИ ... только минимум количество сравнений. (Я опустил фактическую передачу функции порядка для простоты). Обновлено для построения сбалансированного дерева: -
/// <summary>
/// Merge a pair of ordered lists
/// </summary>
public static IEnumerable<T> Merge<T>(IEnumerable<T> aList, IEnumerable<T> bList)
where T:IComparable<T>
{
var a = aList.GetEnumerator();
bool aOK = a.MoveNext();
foreach (var b in bList)
{
while (aOK && a.Current.CompareTo(b) <= 0) {yield return a.Current; aOK = a.MoveNext();}
yield return b;
}
// And anything left in a
while (aOK) { yield return a.Current; aOK = a.MoveNext(); }
}
/// <summary>
/// Merge lots of sorted lists
/// </summary>
public static IEnumerable<T> Merge<T>(IEnumerable<IEnumerable<T>> listOfLists)
where T : IComparable<T>
{
int n = listOfLists.Count();
if (n < 2)
return listOfLists.FirstOrDefault();
else
return Merge (Merge(listOfLists.Take(n/2)), Merge(listOfLists.Skip(n/2)));
}
public static void Main(string[] args)
{
var sample = Enumerable.Range(1, 5).Select((i) => Enumerable.Range(i, i+5).Select(j => string.Format("Test {0:00}", j)));
Console.WriteLine("Merged:");
foreach (var result in Merge(sample))
{
Console.WriteLine("\t{0}", result);
}
Сколько списков необходимо объединить? Похоже, ваш алгоритм не будет эффективным, если вам нужно объединить много разных списков. Эта строка является проблемой:
var min = enumerators.OrderBy(t => orderBy(t.Value)).FirstOrDefault();
Это будет выполняться один раз для каждого элемента во всех списках, поэтому ваше время выполнения будет O (n * m), где n - ОБЩЕЕ количество элементов во всех списках, а n - количество списков. Выраженное в терминах средней длины списка в списке списков, время выполнения равно O (a * m ^ 2).
Если вам нужно объединить множество списков, я бы предложил использовать кучу . Затем на каждой итерации вы можете удалить наименьшее значение из кучи и добавить в кучу следующий элемент из списка, из которого взято наименьшее значение.
Вот мое решение:
Алгоритм берет первый элемент каждого списка и помещает их в небольшой вспомогательный класс (сортированный список, который принимает несколько элементов с одинаковым значением). Этот отсортированный список использует бинарную вставку.
Итак, первый элемент в этом списке - это элемент, который мы хотим вернуть следующим. После этого мы удаляем его из отсортированного списка и вставляем следующий элемент из его исходного списка (по крайней мере, до тех пор, пока этот список содержит еще какие-либо элементы). И снова мы можем вернуть первый элемент нашего отсортированного списка. Когда отсортированный список пуст, мы использовали все элементы из всех исходных списков и закончили.
В этом решении используется меньше операторов foreach
и нет OrderBy
на каждом шаге - что должно улучшить поведение во время выполнения. Только двоичная вставка должна выполняться снова и снова.
IEnumerable<T> MergeOrderedLists<T, TOrder>( IEnumerable<IEnumerable<T>> orderedlists, Func<T, TOrder> orderBy )
{
// Get an enumerator for each list, create a sortedList
var enumerators = orderedlists.Select( enumerable => enumerable.GetEnumerator() );
var sortedEnumerators = new SortedListAllowingDoublets<TOrder, IEnumerator<T>>();
// Point each enumerator onto the first element
foreach( var enumerator in enumerators )
{
// Missing: assert true as the return value
enumerator.MoveNext();
// Initially add the first value
sortedEnumerators.AddSorted( orderBy( enumerator.Current ), enumerator );
}
// Continue as long as we have elements to return
while( sortedEnumerators.Count != 0 )
{
// The first element of the sortedEnumerator list always
// holds the next element to return
var enumerator = sortedEnumerators[0].Value;
// Return this enumerators current value
yield return enumerator.Current;
// Remove the element we just returned
sortedEnumerators.RemoveAt( 0 );
// Check if there is another element in the list of the enumerator
if( enumerator.MoveNext() )
{
// Ok, so add it to the sorted list
sortedEnumerators.AddSorted( orderBy( enumerator.Current ), enumerator );
}
}
Мой вспомогательный класс (использующий простую двоичную вставку):
private class SortedListAllowingDoublets<TOrder, T> : Collection<KeyValuePair<TOrder, T>> where T : IEnumerator
{
public void AddSorted( TOrder value, T enumerator )
{
Insert( GetSortedIndex( value, 0, Count - 1 ), new KeyValuePair<TOrder, T>( value, enumerator ) );
}
private int GetSortedIndex( TOrder item, int startIndex, int endIndex )
{
if( startIndex > endIndex )
{
return startIndex;
}
var midIndex = startIndex + ( endIndex - startIndex ) / 2;
return Comparer<TOrder>.Default.Compare( this[midIndex].Key, item ) < 0 ? GetSortedIndex( item, midIndex + 1, endIndex ) : GetSortedIndex( item, startIndex, midIndex - 1 );
}
}
Что не реализовано сейчас: проверка на пустой список, что вызовет проблемы.
И класс SortedListAllowingDoublets
может быть улучшен, чтобы принимать компаратор вместо использования Comparer
самостоятельно.