Łączenie dwukierunkowe jest szeroko badane jako część algorytmu Mergesort. Ale jestem zainteresowany, aby dowiedzieć się, w jaki sposób najlepiej wykonać scalanie N-way?
Powiedzmy, że mam N
pliki, w których każdy posortował 1 milion liczb całkowitych. Muszę je scalić w jeden pojedynczy plik, który będzie zawierał te 100 milionów posortowanych liczb całkowitych.
Należy pamiętać, że przypadkiem użycia tego problemu jest w rzeczywistości sortowanie zewnętrzne oparte na dysku. Dlatego w rzeczywistych scenariuszach również wystąpiłoby ograniczenie pamięci. Tak więc naiwne podejście polegające na łączeniu 2 plików naraz (99 razy) nie zadziała. Powiedzmy, że dla każdej tablicy mamy tylko małe przesuwne okno pamięci.
Nie jestem pewien, czy istnieje już ustandaryzowane rozwiązanie tego połączenia N-way. (Googlowanie niewiele mi mówiło) .
Ale jeśli wiesz, czy dobry algorytm scalania n-way, prześlij algo / link.
Złożoność czasowa: jeśli znacznie zwiększymy liczbę plików ( N
) do scalenia, jak wpłynie to na złożoność czasową algorytmu?
Dziękuję za odpowiedzi.
Nigdzie nie zadano mi tego, ale czułem, że może to być interesujące pytanie do wywiadu. Dlatego oznaczone.
Odpowiedzi:
A co z następującym pomysłem:
Ponieważ dodawanie elementów do kolejki priorytetowej może odbywać się w czasie logarytmicznym, pozycja 2 to O (N × log N) . Ponieważ (prawie wszystkie) iteracje pętli while dodają element, cała pętla while to O (M × log N), gdzie M to całkowita liczba liczb do sortowania.
Zakładając, że wszystkie pliki mają niepusty ciąg liczb, mamy M> N, a zatem cały algorytm powinien mieć wartość O (M × log N) .
źródło
Wyszukaj „Polyphase merge”, sprawdź klasykę - Donald Knuth & EHFriend.
Warto też przyjrzeć się propozycji Smart Block Merging autorstwa Seyedafsari i Hasanzadeh, która podobnie jak wcześniejsze sugestie wykorzystuje kolejki priorytetowe.
Innym interesującym powodem jest algorytm łączenia na miejscu autorstwa Kim & Kutzner.
Polecam również artykuł Vittera: Algorytmy pamięci zewnętrznej i struktury danych: radzenie sobie z ogromnymi danymi .
źródło
Jednym prostym pomysłem jest zachowanie kolejki priorytetowej zakresów do scalenia, przechowywanej w taki sposób, aby zakres z najmniejszym pierwszym elementem był usuwany jako pierwszy z kolejki. Następnie możesz wykonać scalanie N-way w następujący sposób:
Poprawność tego algorytmu jest zasadniczo uogólnieniem dowodu na to, że scalanie dwukierunkowe działa poprawnie - jeśli zawsze dodajesz najmniejszy element z dowolnego zakresu, a wszystkie zakresy są posortowane, kończy się posortowaniem sekwencji jako całości.
Złożoność środowiska wykonawczego tego algorytmu można znaleźć w następujący sposób. Niech M będzie całkowitą liczbą elementów we wszystkich sekwencjach. Jeśli używamy stosu binarnego, to robimy co najwyżej O (M) wstawień i O (M) usunięć z kolejki priorytetowej, ponieważ dla każdego elementu zapisanego w sekwencji wyjściowej jest kolejka do wyciągnięcia najmniejszej sekwencji, po której następuje enqueue, aby umieścić resztę sekwencji z powrotem w kolejce. Każdy z tych kroków wymaga operacji O (lg N), ponieważ wstawienie lub usunięcie ze stosu binarnego zawierającego N elementów zajmuje O (lg N) czasu. Daje to czas wykonania netto O (M lg N), który rośnie mniej niż liniowo wraz z liczbą sekwencji wejściowych.
Być może istnieje sposób, aby to osiągnąć jeszcze szybciej, ale wydaje się to całkiem dobrym rozwiązaniem. Użycie pamięci wynosi O (N), ponieważ potrzebujemy O (N) narzutu dla sterty binarnej. Jeśli zaimplementujemy stertę binarną, przechowując wskaźniki do sekwencji, a nie do samych sekwencji, nie powinno to stanowić większego problemu, chyba że masz naprawdę śmieszną liczbę sekwencji do scalenia. W takim przypadku po prostu połącz je w grupy, które mieszczą się w pamięci, a następnie połącz wszystkie wyniki.
Mam nadzieję że to pomoże!
źródło
Proste podejście do scalania k posortowanych tablic (każda o długości n) wymaga O (nk ^ 2) czasu, a nie O (nk) czasu. Ponieważ łączenie pierwszych 2 tablic zajmuje 2n czasu, a kiedy łączysz trzecią tablicę z wynikiem, zajmuje to 3n czasu, ponieważ teraz łączymy dwie tablice o długości 2n i n. Teraz, kiedy połączymy to wyjście z czwartym, to scalenie wymaga 4n czasu, tak więc ostatnie scalenie (kiedy dodajemy k-tą tablicę do naszej już posortowanej tablicy) wymaga k * n czasu, a więc całkowity wymagany czas wynosi 2n + 3n + 4n + ... k * n, czyli O (nk ^ 2).
Wygląda na to, że możemy to zrobić w czasie O (kn), ale tak nie jest, ponieważ za każdym razem nasza tablica, którą scalamy, rośnie.
Chociaż możemy osiągnąć lepszą granicę za pomocą dziel i rządź. Nadal nad tym pracuję i publikuję rozwiązanie, jeśli je znajdę.
źródło
Zobacz http://en.wikipedia.org/wiki/External_sorting . Oto moje podejście do scalania k-way opartego na stercie, używającego buforowanego odczytu ze źródeł do emulacji redukcji we / wy:
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); } }
Oto jedna możliwa implementacja
MinHeap<T>
. Niektóre testy:[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)); }
źródło
Napisałem ten fragment kodu w stylu STL, który łączy N-way scalanie i pomyślałem, że opublikuję go tutaj, aby pomóc innym uniemożliwić innym wymyślanie koła. :)
Ostrzeżenie: jest tylko lekko przetestowany. Przetestuj przed użyciem. :)
Możesz go używać w ten sposób:
Pozwala także na używanie par iteratorów zamiast samych kontenerów.
Jeśli używasz Boost.Range, możesz usunąć część kodu standardowego.
Kod:
źródło
}
źródło
Implementacja w Javie algorytmu minimalnej sterty do łączenia k posortowanych tablic:
źródło