Двустороннее слияние широко изучается как часть алгоритма сортировки слиянием. Но мне интересно узнать, как лучше всего выполнить N-образное слияние?
Допустим, у меня есть N
файлы, в каждом из которых содержится 1 миллион целых чисел. Мне нужно объединить их в один файл, в котором будут эти 100 миллионов отсортированных целых чисел.
Имейте в виду, что вариант использования этой проблемы - это внешняя сортировка, основанная на диске. Следовательно, в реальных сценариях также будет ограничение памяти. Таким образом, наивный подход слияния двух файлов за раз (99 раз) не сработает. Допустим, у нас есть только небольшое скользящее окно памяти, доступное для каждого массива.
Я не уверен, есть ли уже стандартизированное решение для этого N-образного слияния. (Гугл мне мало что сказал) .
Но если вы знаете, есть ли хороший алгоритм n-way слияния, отправьте algo / link.
Сложность по времени: если мы значительно увеличим количество N
объединяемых файлов ( ), как это повлияет на временную сложность вашего алгоритма?
Спасибо за ответы.
Меня нигде об этом не спрашивали, но я подумал, что это может быть интересный вопрос для интервью. Поэтому помечен.
Ответы:
Как насчет следующей идеи:
Поскольку добавление элементов в приоритетную очередь может быть выполнено за логарифмическое время, элемент 2 равен O (N × log N) . Поскольку (почти все) итерации цикла while добавляют элемент, весь цикл while составляет O (M × log N), где M - общее количество чисел для сортировки.
Предполагая, что все файлы имеют непустую последовательность чисел, мы имеем M> N и, следовательно, весь алгоритм должен быть O (M × log N) .
источник
Найдите "Полифазное слияние", посмотрите классику - Дональд Кнут и EHFriend.
Кроме того, вы можете взглянуть на предлагаемое слияние смарт-блоков, предложенное Сейедафсари и Хасанзаде, которое, как и предыдущие предложения, использует очереди приоритетов.
Еще один интересный аргумент - это алгоритм слияния на месте, разработанный Кимом и Кутцнером.
Я также рекомендую эту статью Виттера: Алгоритмы внешней памяти и структуры данных: работа с большими данными .
источник
Одна простая идея - сохранить приоритетную очередь диапазонов для слияния, сохраненную таким образом, чтобы диапазон с наименьшим первым элементом удалялся первым из очереди. Затем вы можете выполнить N-стороннее слияние следующим образом:
Правильность этого алгоритма по сути является обобщением доказательства того, что двухстороннее слияние работает правильно - если вы всегда добавляете наименьший элемент из любого диапазона, и все диапазоны отсортированы, вы в конечном итоге получаете отсортированную последовательность в целом.
Сложность выполнения этого алгоритма можно определить следующим образом. Пусть M - общее количество элементов во всех последовательностях. Если мы используем двоичную кучу, то мы делаем не более O (M) вставок и O (M) удалений из очереди с приоритетом, поскольку для каждого элемента, записанного в выходную последовательность, есть удаление из очереди для извлечения наименьшей последовательности, за которым следует enqueue, чтобы вернуть оставшуюся часть последовательности в очередь. Каждый из этих шагов требует O (lg N) операций, потому что вставка или удаление из двоичной кучи с N элементами занимает время O (lg N). Это дает чистое время выполнения O (M lg N), которое растет менее чем линейно с количеством входных последовательностей.
Возможно, есть способ сделать это еще быстрее, но это кажется довольно хорошим решением. Использование памяти составляет O (N), потому что нам нужны накладные расходы O (N) для двоичной кучи. Если мы реализуем двоичную кучу, сохраняя указатели на последовательности, а не на сами последовательности, это не должно быть большой проблемой, если у вас нет действительно смешного количества последовательностей для слияния. В этом случае просто объедините их в группы, которые умещаются в памяти, а затем объедините все результаты.
Надеюсь это поможет!
источник
Простой подход к объединению k отсортированных массивов (каждый длиной n) требует времени O (nk ^ 2), а не времени O (nk). Когда вы объединяете первые 2 массива, это занимает 2n времени, тогда, когда вы объединяете третий с выходом, это занимает 3n времени, так как теперь мы объединяем два массива длиной 2n и n. Теперь, когда мы объединяем этот вывод с четвертым, это слияние требует 4n времени. Таким образом, последнее слияние (когда мы добавляем k-й массив в наш уже отсортированный массив) требует времени k * n. Таким образом, общее необходимое время составляет 2n + 3n + 4n + ... k * n, который равен O (nk ^ 2).
Похоже, мы можем сделать это за O (kn) раз, но это не так, потому что каждый раз наш объединяемый массив увеличивается в размере.
Хотя мы можем достичь лучшего результата, используя «разделяй и властвуй». Я все еще работаю над этим и опубликую решение, если найду его.
источник
См. Http://en.wikipedia.org/wiki/External_sorting . Вот мой подход к слиянию k-way на основе кучи, с использованием буферизованного чтения из источников для имитации сокращения ввода-вывода:
public class KWayMerger<T> { private readonly IList<T[]> _sources; private readonly int _bufferSize; private readonly MinHeap<MergeValue<T>> _mergeHeap; private readonly int[] _indices; public KWayMerger(IList<T[]> sources, int bufferSize, Comparer<T> comparer = null) { if (sources == null) throw new ArgumentNullException("sources"); _sources = sources; _bufferSize = bufferSize; _mergeHeap = new MinHeap<MergeValue<T>>( new MergeComparer<T>(comparer ?? Comparer<T>.Default)); _indices = new int[sources.Count]; } public T[] Merge() { for (int i = 0; i <= _sources.Count - 1; i++) AddToMergeHeap(i); var merged = new T[_sources.Sum(s => s.Length)]; int mergeIndex = 0; while (_mergeHeap.Count > 0) { var min = _mergeHeap.ExtractDominating(); merged[mergeIndex++] = min.Value; if (min.Source != -1) //the last item of the source was extracted AddToMergeHeap(min.Source); } return merged; } private void AddToMergeHeap(int sourceIndex) { var source = _sources[sourceIndex]; var start = _indices[sourceIndex]; var end = Math.Min(start + _bufferSize - 1, source.Length - 1); if (start > source.Length - 1) return; //we're done with this source for (int i = start; i <= end - 1; i++) _mergeHeap.Add(new MergeValue<T>(-1, source[i])); //only the last item should trigger the next buffered read _mergeHeap.Add(new MergeValue<T>(sourceIndex, source[end])); _indices[sourceIndex] += _bufferSize; //we may have added less items, //but if we did we've reached the end of the source so it doesn't matter } } internal class MergeValue<T> { public int Source { get; private set; } public T Value { get; private set; } public MergeValue(int source, T value) { Value = value; Source = source; } } internal class MergeComparer<T> : IComparer<MergeValue<T>> { public Comparer<T> Comparer { get; private set; } public MergeComparer(Comparer<T> comparer) { if (comparer == null) throw new ArgumentNullException("comparer"); Comparer = comparer; } public int Compare(MergeValue<T> x, MergeValue<T> y) { Debug.Assert(x != null && y != null); return Comparer.Compare(x.Value, y.Value); } }
Вот одна из возможных реализаций
MinHeap<T>
. Некоторые тесты:[TestMethod] public void TestKWaySort() { var rand = new Random(); for (int i = 0; i < 10; i++) AssertKwayMerge(rand); } private static void AssertKwayMerge(Random rand) { var sources = new[] { GenerateRandomCollection(rand, 10, 30, 0, 30).OrderBy(i => i).ToArray(), GenerateRandomCollection(rand, 10, 30, 0, 30).OrderBy(i => i).ToArray(), GenerateRandomCollection(rand, 10, 30, 0, 30).OrderBy(i => i).ToArray(), GenerateRandomCollection(rand, 10, 30, 0, 30).OrderBy(i => i).ToArray(), }; Assert.IsTrue(new KWayMerger<int>(sources, 20).Merge().SequenceEqual(sources.SelectMany(s => s).OrderBy(i => i))); } public static IEnumerable<int> GenerateRandomCollection(Random rand, int minLength, int maxLength, int min = 0, int max = int.MaxValue) { return Enumerable.Repeat(0, rand.Next(minLength, maxLength)).Select(i => rand.Next(min, max)); }
источник
Я написал этот фрагмент кода в стиле STL, который выполняет N-образное слияние, и решил опубликовать его здесь, чтобы помочь другим не изобретать колесо. :)
Предупреждение: это только слегка протестировано. Перед использованием проверьте. :)
Вы можете использовать это так:
Это также позволяет использовать пары итераторов вместо самих контейнеров.
Если вы используете Boost.Range, вы можете удалить часть шаблонного кода.
Код:
источник
}
источник
Реализация в Java алгоритма min heap для объединения k отсортированных массивов:
источник