Sie erhalten ein Array von Elementen
Die Aufgabe besteht darin, das Array mithilfe eines In-Place-Algorithmus so zu verschachteln, dass das resultierende Array aussieht
Wenn die Vor-Ort-Anforderung nicht vorhanden wäre, könnten wir leicht ein neues Array erstellen und Elemente kopieren, die einen -Zeitalgorithmus liefern.
Mit der In-Place-Anforderung erhöht ein Divisions- und Eroberungsalgorithmus den Algorithmus auf .
Die Frage ist also:
Gibt es einen -Zeitalgorithmus, der ebenfalls vorhanden ist?
(Hinweis: Sie können das einheitlichen Kosten WORD RAM - Modell übernehmen, so an Ort und Stelle übersetzt Raumbeschränkung).
algorithms
arrays
permutations
in-place
Aryabhata
quelle
quelle
Antworten:
Hier ist die Antwort, die sich auf den Algorithmus aus dem von Joe verlinkten Artikel bezieht : http://arxiv.org/abs/0805.1598
1) Teilen und Erobern
Wir sind gegeben
und rekursieren.
2) Permutationszyklen
Eine andere Herangehensweise an das Problem besteht darin, die Permutation als eine Menge von disjunkten Zyklen zu betrachten.
Wenn wir irgendwie genau wüssten, wie die Zyklen lauten, könnten wir die Permutation realisieren, indem wir ein Element auswählen , bestimmen, wohin dieses Element führt (unter Verwendung der obigen Formel), das Element an der Zielposition in einen temporären Raum verschieben und platzieren Setzen Sie das Element in diesen Zielort und fahren Sie mit dem Zyklus fort. Sobald wir mit einem Zyklus fertig sind, gehen wir zu einem Element des nächsten Zyklus über und folgen diesem Zyklus und so weiter.A A
Dies würde uns einen Zeitalgorithmus geben, aber es wird davon ausgegangen, dass wir "irgendwie wussten, was die genauen Zyklen waren" und versuchten, diese Buchführung innerhalb der Raumbegrenzung durchzuführen ist das, was dieses Problem schwer macht.O(n) O(1)
Hier wird die Zahlentheorie angewendet.
Es kann gezeigt werden, dass in dem Fall, in dem , die Elemente an den Positionen , in verschiedenen Zyklen sind und jeder Zyklus ein Element enthält an der Position .2n+1=3k 1 3,32,…,3k−1 3m,m≥0
Dies nutzt die Tatsache, dass ein Generator von .2 (Z/3k)∗
Wenn also , erhalten wir durch Verfolgen des Zyklus-Ansatzes einen -Zeitalgorithmus, da wir für jeden Zyklus genau wissen, wo wir beginnen müssen: Potenzen von (einschließlich ) (diese) kann in berechnet werden .2n+1=3k O(n) 3 1 O(1)
3) Endgültiger Algorithmus
Jetzt kombinieren wir die beiden oben genannten Zyklen: Teilen und Erobern + Permutieren.
Wir dividieren und erobern, aber wählen so, dass eine Potenz von und .m 2m+1 3 m=Θ(n)
Anstatt also beide "Hälften" zu wiederholen, verwenden wir nur eine und erledigen -Zusatzarbeit .Θ(n)
Dies gibt uns die Wiederholung (für einige ) und gibt uns somit eine Zeit, Raumalgorithmus!T(n)=T(cn)+Θ(n) 0<c<1 O(n) O(1)
quelle
Ich bin mir ziemlich sicher, dass ich einen Algorithmus gefunden habe, der nicht auf Zahlentheorie oder Zyklustheorie beruht. Beachten Sie, dass es einige Details zu klären gibt (möglicherweise morgen), aber ich bin ziemlich zuversichtlich, dass sie klappen werden. Ich bewege mich wie ich schlafen soll, nicht weil ich versuche Probleme zu verbergen :)
Seien Sie
A
das erste Array,B
das zweite|A| = |B| = N
und nehmen Sie es der Einfachheit halberN=2^k
für einigek
an. SeiA[i..j]
das Subarray vonA
mit Indizesi
bisj
einschließlich. Arrays sind 0-basiert. Lassen SieRightmostBitPos(i)
die (0-basierte) Position des am weitesten rechts liegenden Bits, das '1' isti
, von rechts zählen. Der Algorithmus arbeitet wie folgt.Nehmen wir ein Array mit 16 Zahlen und beginnen wir mit dem Interleaven mithilfe von Swaps. Dann sehen wir, was passiert:
Von besonderem Interesse ist der erste Teil des zweiten Arrays:
Das Muster sollte klar sein: Wir fügen abwechselnd eine Zahl am Ende hinzu und ersetzen die niedrigste durch eine hohe Zahl. Beachten Sie, dass wir immer eine Zahl hinzufügen, die um eins höher ist als die höchste Zahl, die wir bereits haben. Wenn wir irgendwie genau herausfinden könnten, welche Zahl zu einem bestimmten Zeitpunkt die niedrigste ist, können wir das leicht tun.
Jetzt schauen wir uns größere Beispiele an, um zu sehen, ob wir ein Muster sehen können. Beachten Sie, dass wir die Größe des Arrays nicht korrigieren müssen, um das obige Beispiel zu erstellen. Irgendwann erhalten wir diese Konfiguration (die zweite Zeile subtrahiert 16 von jeder Zahl):
Dies zeigt deutlich ein Muster: "1 3 5 7 9 11 13 15" sind alle 2 auseinander, "2 6 10 14" sind alle 4 auseinander und "4 12" sind 8 auseinander. Wir können daher einen Algorithmus entwickeln, der uns sagt, wie die nächstkleinere Zahl aussehen wird: Der Mechanismus ist ziemlich genau, wie Binärzahlen funktionieren. Sie haben ein bisschen für die letzte Hälfte des Arrays, ein bisschen für das zweite Quartal und so weiter.
Wenn wir daher genug Platz haben, um diese Bits zu speichern (wir brauchen Bits, aber unser Rechenmodell erlaubt dies - ein Zeiger in das Array braucht auch Bits), können wir herausfinden, welche Zahl in zu tauschen ist Zeit amortisiert.logn logn O(1)
Wir können daher die erste Hälfte des Arrays in Zeit und Swaps in ihren verschachtelten Zustand bringen . Wir müssen jedoch die zweite Hälfte unseres Arrays reparieren, die völlig durcheinander zu sein scheint ("8 6 5 7 13 14 15 16").O(n) O(n)
Wenn wir nun die erste Hälfte dieses zweiten Teils 'sortieren' können, erhalten wir "5 6 7 8 13 14 15 16", und rekursives Verschachteln dieser Hälfte reicht aus: Wir verschachteln das Array in time ( rekursive Aufrufe, die jeweils die Eingabegröße halbieren). Beachten Sie, dass wir keinen Stack benötigen, da diese Aufrufe rekursiv sind, sodass unsere Speicherplatznutzung bleibt .O(n) O(logn) O(1)
Die Frage ist nun: Gibt es ein Muster in dem Teil, den wir sortieren müssen? Wenn wir 32 Zahlen versuchen, erhalten wir "16 12 10 14 9 11 13 15", um das Problem zu beheben. Beachten Sie, dass wir hier genau das gleiche Muster haben! "9 11 13 15", "10 14" und "12" sind in derselben Weise wie zuvor zusammengefasst.
Der Trick besteht nun darin, diese Unterteile rekursiv zu verschachteln. Wir verschachteln "16" und "12" mit "12 16". Wir verschachteln "12 16" und "10 14" mit "10 12 14 16". Wir verschachteln "10 12 14 16" und "9 11 13 15" mit "9 10 11 12 13 14 15 16". Dies sortiert den ersten Teil.
Genau wie oben betragen die Gesamtkosten dieser Operation . Addiert man all dies zusammen, erhält man immer noch eine Gesamtlaufzeit von .O(n) O(n)
Ein Beispiel:
quelle
Hier ist ein nicht-rekursiver In-Place-Algorithmus in linearer Zeit, mit dem zwei Hälften eines Arrays ohne zusätzlichen Speicher verschachtelt werden können.
Die allgemeine Idee ist einfach: Gehen Sie von links nach rechts durch die erste Hälfte des Arrays und tauschen Sie die richtigen Werte aus. Wenn Sie fortfahren, werden die noch zu verwendenden linken Werte in den von den rechten Werten freigegebenen Bereich getauscht. Der einzige Trick ist herauszufinden, wie man sie wieder herausholt.
Wir beginnen mit einem Array der Größe N, das in zwei nahezu gleiche Hälften aufgeteilt ist.
[ left_items | right_items ]
Während wir es verarbeiten, wird es
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]
Der Auslagerungsbereich wächst mit dem folgenden Muster: A) Vergrößern Sie den Bereich, indem Sie das benachbarte rechte Element entfernen und ein neues Element von links einlagern. B) Tauschen Sie den ältesten Artikel mit einem neuen Artikel von links aus. Wenn die linken Elemente mit 1..N nummeriert sind, sieht dieses Muster wie folgt aus
Die Reihenfolge, in der sich der Index ändert, ist genau OEIS A025480 , was mit einem einfachen Verfahren berechnet werden kann. Auf diese Weise kann der Auslagerungsort nur anhand der Anzahl der bisher hinzugefügten Elemente gefunden werden. Dies ist auch der Index des aktuell platzierten Elements.
Das ist alles, was wir brauchen, um die erste Hälfte der Sequenz in linearer Zeit zu füllen.
Wenn wir zum Mittelpunkt gelangen, besteht das Array aus drei Teilen:
[ placed_items | swapped_left_items | remaining_right_items]
Wenn wir die ausgetauschten Elemente entschlüsseln können, haben wir das Problem auf die Hälfte der Größe reduziert und können es wiederholen.Um den Swap-Bereich zu entschlüsseln, verwenden wir die folgende Eigenschaft: Eine Sequenz, die durch
N
abwechselnde Operationen append und swap_oldest erstellt wurde, enthältN/2
Elemente, deren Alter durch angegeben istA025480(N/2)..A025480(N-1)
. (Ganzzahlige Division, kleinere Werte sind älter).Wenn beispielsweise die linke Hälfte ursprünglich die Werte 1..19 enthielte, würde der Swap Space enthalten
[16, 12, 10, 14, 18, 11, 13, 15, 17, 19]
. A025480 (9..18) ist[2, 5, 1, 6, 3, 7, 0, 8, 4, 9]
genau die Liste der Indizes der Elemente vom ältesten zum neuesten.So können wir unseren Swap-Bereich entschlüsseln, indem wir ihn durchlaufen und
S[i]
mit ihm tauschenS[ A(N/2 + i)]
. Dies ist auch eine lineare Zeit.Die verbleibende Komplikation ist, dass Sie irgendwann eine Position erreichen, an der der richtige Wert bei einem niedrigeren Index liegen sollte, der jedoch bereits ausgetauscht wurde. Der neue Speicherort ist leicht zu finden: Führen Sie die Indexberechnung erneut durch, um festzustellen, wohin der Artikel ausgetauscht wurde. Es kann erforderlich sein, der Kette einige Schritte zu folgen, bis Sie einen nicht vertauschten Ort finden.
Zu diesem Zeitpunkt haben wir die Hälfte des Arrays zusammengeführt und die Reihenfolge der nicht zusammengeführten Teile in der anderen Hälfte mit genau vertauschten Elementen beibehalten
N/2 + N/4
. Wir können den Rest des Arrays für insgesamtN + N/4 + N/8 + ....
Swaps durchlaufen, was streng genommen weniger als ist3N/2
.Berechnung von A025480:
Dies ist in OEIS definiert als
a(2n) = n, a(2n+1) = a(n).
Eine alternative Formulierung ista(n) = isEven(n)? n/2 : a((n-1)/2)
. Dies führt zu einem einfachen Algorithmus mit bitweisen Operationen:Dies ist eine amortisierte O (1) -Operation über alle möglichen Werte für N. (1/2 braucht 1 Schicht, 1/4 braucht 2, 1/8 braucht 3, ...) . Es gibt eine noch schnellere Methode, die eine kleine Nachschlagetabelle verwendet, um die Position des niedrigstwertigen Nullbits zu finden.
In Anbetracht dessen ist hier eine Implementierung in C:
Dies sollte ein ziemlich cachefreundlicher Algorithmus sein, da auf 2 der 3 Datenpositionen nacheinander zugegriffen wird und die verarbeitete Datenmenge stark abnimmt. Diese Methode kann von einem Out-Shuffle in ein In-Shuffle umgewandelt werden, indem der
is_even
Test am Anfang der Schleife negiert wird.quelle