In-Place-Algorithmus zum Verschachteln eines Arrays

62

Sie erhalten ein Array von Elementen2n

a1,a2,,an,b1,b2,bn

Die Aufgabe besteht darin, das Array mithilfe eines In-Place-Algorithmus so zu verschachteln, dass das resultierende Array aussieht

b1,a1,b2,a2,,bn,an

Wenn die Vor-Ort-Anforderung nicht vorhanden wäre, könnten wir leicht ein neues Array erstellen und Elemente kopieren, die einen -Zeitalgorithmus liefern.O(n)

Mit der In-Place-Anforderung erhöht ein Divisions- und Eroberungsalgorithmus den Algorithmus auf .θ(nlogn)

Die Frage ist also:

Gibt es einen -Zeitalgorithmus, der ebenfalls vorhanden ist?O(n)

(Hinweis: Sie können das einheitlichen Kosten WORD RAM - Modell übernehmen, so an Ort und Stelle übersetzt Raumbeschränkung).O(1)

Aryabhata
quelle
1
Dies ist ein Stackoverflow, aber es gibt keine Qualitätslösung. Die bestbewertete Antwort lautet: "Dieses Problem ist nicht so trivial, wie die Leute glauben. Hausaufgaben? LOL. Es gibt eine Lösung für arXiv. " Die arxiv-Lösung erfordert jedoch eine gewisse Zahlentheorie und die Bezugnahme auf Beweise in anderen Artikeln. Es wäre schön, hier eine prägnante Lösung zu haben.
Joe
1
Ebenfalls auf cstheory: cstheory.stackexchange.com/questions/13943/…
Yuval Filmus
Ein weiterer Thread zu Stack Overflow: stackoverflow.com/questions/15996288/…
Nayuki

Antworten:

43

Hier ist die Antwort, die sich auf den Algorithmus aus dem von Joe verlinkten Artikel bezieht : http://arxiv.org/abs/0805.1598

Θ(nlogn)

1) Teilen und Erobern

Wir sind gegeben

a1,a2,,b1,b2,bn

m=Θ(n)

[a1,a2,,am,b1,b2,,bm],[am+1,,an,bm+1,bn]

und rekursieren.

b1,b2,bm,am+1,an

am+1,an,b1,bm

m

O(n)

Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2) Permutationszyklen

Eine andere Herangehensweise an das Problem besteht darin, die Permutation als eine Menge von disjunkten Zyklen zu betrachten.

1

j2jmod2n+1

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.AA

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=3k13,32,,3k13m,m0

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=3kO(n)31O(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 .m2m+13m=Θ(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<1O(n)O(1)

Aryabhata
quelle
4
Das ist schön.
Raphael
1
Sehr schön. Ich gehe die Permutationsbeispiele durch und verstehe jetzt das meiste davon. Zwei Fragen: 1. Wie findet man eigentlich den Wert m? Papier behauptet, es dauert O (log n), warum? 2. Ist es möglich, ein Array mit einem ähnlichen Ansatz zu DE-interleaven?
15.
2
@ num3ric: 1) Sie finden die höchste Potenz von die . Es wird also . 2). Ja, es ist möglich, ich glaube, ich habe irgendwo eine Antwort zum Stackoverflow hinzugefügt. Ich glaube, dass die Zyklusleiter in diesem Fall für (für = Potenz von ). 3<nO(logn)2a3b2m+13
Aryabhata
@Aryabhata warum greifen wir nur auf eine "Hälfte" anstatt auf zwei "Hälften" zurück?
sinoTrinity
1
@Aryabhata Kann dieser Algorithmus erweitert werden, um mehr als zwei Arrays zu verschachteln? Zum Beispiel verwandeln Sie in oder etwas ähnliches. a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Doub
18

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 Adas erste Array, Bdas zweite |A| = |B| = Nund nehmen Sie es der Einfachheit halber N=2^kfür einige kan. Sei A[i..j]das Subarray von Amit Indizes ibis jeinschließlich. Arrays sind 0-basiert. Lassen Sie RightmostBitPos(i)die (0-basierte) Position des am weitesten rechts liegenden Bits, das '1' ist i, von rechts zählen. Der Algorithmus arbeitet wie folgt.

GetIndex(i) {
    int rightPos = RightmostBitPos(i) + 1;
    return i >> rightPos;
}

Interleave(A, B, N) {
    if (n == 1) {
        swap(a[0], b[0]);
    }
    else {
        for (i = 0; i < N; i++)
            swap(A[i], B[GetIndex(i+1)]);

        for (i = 1; i <= N/2; i*=2)
            Interleave(B[0..i/2-1], B[i/2..i-1], i/2);

        Interleave(B[0..N/2], B[N/2+1..N], n/2);
    }
}

Nehmen wir ein Array mit 16 Zahlen und beginnen wir mit dem Interleaven mithilfe von Swaps. Dann sehen wir, was passiert:

1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16

Von besonderem Interesse ist der erste Teil des zweiten Arrays:

|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7

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):

16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0   8  4 12  2  6 10 14  1  3  5  7  9 11 13 15

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.lognlognO(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:

Interleave the first half:
1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8   | 13 14 15 16
13 6 7 8  | 5 14 15 16
13 5 7 8  | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8
Alex ten Brink
quelle
Interessant. Wären Sie bereit, einen formellen Beweis zu schreiben? Ich weiß, dass es einen anderen Algorithmus gibt (auf den in dem Artikel, den Joe gefunden hat, Bezug genommen wird), der sich mit Bits befasst. Vielleicht haben Sie es wiederentdeckt!
Aryabhata
1

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

step swapspace index changed
1    A: 1         0
2    B: 2         0
3    A: 2 3       1
4    B: 4 3       0     
5    A: 4 3 5     2
6    B: 4 6 5     1
7    A: 4 6 5 7   3
...

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 Nabwechselnde Operationen append und swap_oldest erstellt wurde, enthält N/2Elemente, deren Alter durch angegeben ist A025480(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 tauschen S[ 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 insgesamt N + N/4 + N/8 + ....Swaps durchlaufen, was streng genommen weniger als ist 3N/2.

Berechnung von A025480:
Dies ist in OEIS definiert als a(2n) = n, a(2n+1) = a(n).Eine alternative Formulierung ist a(n) = isEven(n)? n/2 : a((n-1)/2). Dies führt zu einem einfachen Algorithmus mit bitweisen Operationen:

index_t a025480(index_t n){
    while (n&1) n=n>>1;
    return n>>1;  
}

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:

static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }

index_t unshuffle_item(index_t j, index_t sz)
{
  index_t i = j;
  do {
    i = a025480(sz / 2 + i);
  }
  while (i < j);
  return i;
}

void interleave(value_t a[], index_t n_items)
{
  index_t i = 0;
  index_t midpt = larger_half(n_items);
  while (i < n_items - 1) {

    //for out-shuffle, the left item is at an even index
    if (is_even(i)) { i++; }
    index_t base = i;

    //emplace left half.
    for (; i < midpt; i++) {
      index_t j = a025480(i - base);
      SWAP(a + i, a + midpt + j);
    }

    //unscramble swapped items
    index_t swap_ct  = larger_half(i - base);
    for (index_t j = 0; j + 1 < swap_ct ; j++) {
      index_t k = unshuffle_item(j, i - base);
      if (j != k) {
        SWAP(a + midpt + j, a + midpt + k);
      }
    }
    midpt += swap_ct;
  }
}

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_evenTest am Anfang der Schleife negiert wird.

Ahelly
quelle