Permutations
Du erhältst eine Liste nums mit verschiedenen Ganzzahlen. Gib jede Anordnung dieser Werte zurück, jeweils als Liste, in der jeder Wert genau einmal vorkommt, sodass n Werte n! Anordnungen ergeben. Liste sie in lexikografischer Reihenfolge auf: Vergleiche zwei Anordnungen Position für Position; die erste Abweichung entscheidet. Für [1, 2, 3] bedeutet das, dass [1, 2, 3] an erster und [3, 2, 1] an letzter Stelle steht.
Funktion
- numsinteger-array
- die Werte, alle unterschiedlich, in beliebiger Reihenfolge
- Gibt zurückinteger-2d-array
- jede Anordnung der Werte, aufgelistet in lexikografischer Reihenfolge
Einschränkungen
1 ≤ nums.length ≤ 6-10 ≤ nums[i] ≤ 10- Alle Werte in
numssind verschieden. numskann in beliebiger Reihenfolge vorliegen.
Beispiele
- Eingabe
- nums = [3, 1, 2]
- Ausgabe
- [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
- Erklärung
- Drei Werte haben 3! = 6 Anordnungen. Sortiert lauten die Werte 1, 2, 3, daher kommen die Anordnungen, die mit 1 beginnen, zuerst, und
[1, 2, 3]kommt vor[1, 3, 2], weil 2 an der zweiten Position kleiner als 3 ist. Die Reihenfolge der Eingabe spielt keine Rolle.
- Eingabe
- nums = [2, -1]
- Ausgabe
- [[-1, 2], [2, -1]]
- Erklärung
- Zwei Werte können in zwei Reihenfolgen geschrieben werden.
[-1, 2]kommt zuerst, weil -1 kleiner als 2 ist.
- Eingabe
- nums = [7]
- Ausgabe
- [[7]]
- Erklärung
- Ein Wert hat genau eine Ordnung, die Liste selbst.
+13 versteckte Tests beim Einreichen
Weiterführende Frage
Kannst du ausgehend von einer Anordnung die nächste in lexikografischer Reihenfolge direkt erzeugen, in O(n) Zeit und mit O(1) zusätzlichem Speicherplatz?
Tipps
Öffne sie nacheinander. Jeder verrät ein bisschen mehr.
Erstelle eine Anordnung Position für Position. Wie viele Werte können an die erste Position, wie viele an die zweite, und was sagt dir das über die Gesamtzahl?
Behalte im Blick, welche Werte bereits gesetzt sind. Probiere an jeder Position jeden Wert aus, der noch frei ist, und gib ihn wieder frei, wenn du damit fertig bist, damit der nächste Versuch vom selben Zustand aus startet.
Sortiere die Werte und schreibe dann eine rekursive Hilfsfunktion. Wenn der Pfad alle
nWerte enthält, speichere eine Kopie. Andernfalls durchlaufe die Werte vom kleinsten zum größten, überspringe die bereits verwendeten, markiere einen als verwendet und füge ihn hinzu, rufe die Funktion rekursiv auf, entferne ihn dann wieder und hebe die Markierung auf. Wenn du zuerst den kleinsten freien Wert ausprobierst, sind die Anordnungen bereits sortiert.
Lösung
Eine Liste mit n verschiedenen Werten hat n! Anordnungen, bei sechs Werten 720, und die Antwort muss sie alle auflisten, daher beträgt der Aufwand mindestens n × n!. Die Herausforderung besteht darin, jede Anordnung genau einmal zu erstellen und sie in lexikografischer Reihenfolge auszugeben. Backtracking über die sortierten Werte, wobei stets zuerst der kleinste noch nicht verwendete Wert ausprobiert wird, erledigt beides gleichzeitig.
In jede Lücke einsetzen, dann sortieren
Idee
Erweitere die Anordnungen Schritt für Schritt um jeweils einen Wert. Ohne Werte gibt es eine Anordnung: die leere Liste. Um den Wert 3 zur Anordnung [1, 2] hinzuzufügen, setze ihn in jede ihrer drei Lücken: [3, 1, 2], [1, 3, 2] und [1, 2, 3]. Mach das für jede Anordnung, die du hast, und aus den Anordnungen von k Werten werden die Anordnungen von k+1 Werten.
Jede Anordnung von k+1 Werten wird genau einmal erstellt: Nimm den neuesten Wert heraus, und du erhältst die Anordnung, aus der sie entstanden ist, während die Position des neuesten Werts die Lücke angibt. Die Anzahl wächst also wie 1, 2, 6, 24, und n Werte ergeben n! Anordnungen.
Sie entstehen nicht in der erforderlichen Reihenfolge. Für [1, 2, 3] ist die erste erstellte Anordnung [3, 2, 1], daher musst du zum Schluss sortieren, indem du die Positionen nacheinander vergleichst. Dieser Sortierschritt ist der aufwendige Teil: Für n! Anordnungen braucht man ungefähr n! × log(n!) Vergleiche, und bei jedem werden bis zu n Werte gelesen. Bei sechs Werten sind das ungefähr 720 × 9.5 × 6, also etwa 41.000 Lesezugriffe. Bei dieser Methode bleibt außerdem eine ganze Generation von Anordnungen im Speicher, während die nächste erstellt wird.
Algorithmus
- Beginne mit einer Liste, die eine leere Anordnung enthält.
- Erstelle für jeden Wert in
numseine neue Liste: Kopiere für jede bisherige Anordnung und jede Lücke von 0 bis zu ihrer Länge die Anordnung, wobei der Wert an dieser Lücke eingefügt wird. - Ersetze die alte Liste durch die neue.
- Sortiere die Anordnungen Position für Position und gib sie zurück.
def permute(nums):
perms = [[]]
for value in nums:
grown = []
for perm in perms:
# Put value into every gap of perm, both ends included.
for gap in range(len(perm) + 1):
grown.append(perm[:gap] + [value] + perm[gap:])
perms = grown
# Insertion order is not lexicographic, so sort at the end.
perms.sort()
return permsBacktracking mit einem verwendeten Array
Idee
Fülle n Plätze von links nach rechts. Für den ersten Platz gibt es n Kandidaten, für den zweiten n-1 und so weiter; daher kommt n!. Stelle diese Entscheidungen als Baum dar: Die Wurzel ist ein leerer Pfad, jede Kante fügt einen weiteren Wert hinzu, und jedes Blatt auf Tiefe n ist eine vollständige Anordnung. Bei sortierten Werten 1, 2, 3 hat die Wurzel die Kinder [1], [2] und [3]; [1] hat die Kinder [1, 2] und [1, 3]; jedes davon hat ein Blatt.
Backtracking durchläuft diesen Baum mit einem gemeinsamen path und einem used-Flag pro Wert. An jedem Knoten durchläuft es die Werte und überspringt die bereits verwendeten. Für jeden freien Wert wählt es diesen aus (markiert ihn als verwendet und hängt ihn an), erkundet den nächsten Schritt (ruft sich eine Ebene tiefer rekursiv auf) und macht die Auswahl rückgängig (entfernt ihn und markiert ihn wieder als frei). Durch diesen Schritt wird genau der Zustand wiederhergestellt, den die Schleife zuvor hatte, sodass der nächste Wert vom selben Knoten aus ausprobiert wird. Ein Pfad der Länge n ist ein Blatt: Erstelle eine Kopie davon und kehre zurück.
Die richtige Reihenfolge ergibt sich von selbst. Die Schleife probiert zuerst den kleinsten freien Wert aus, und der Durchlauf beendet alle Anordnungen mit einem bestimmten Präfix, bevor er dieses Präfix ändert. Daher kommen alle Anordnungen, die mit 1 beginnen, vor allen, die mit 2 beginnen, und unter ihnen kommt [1, 2, ...] vor [1, 3, ...]. Das ist lexikografische Reihenfolge. Deshalb sortierst du auch nums zuerst: Die Schleife geht die Indizes der Reihe nach durch, daher müssen die Indizes nach Wert geordnet sein.
Der Baum hat ungefähr e × n! Knoten (e ist etwa 2.72), und jeder davon durchläuft eine Schleife der Länge n. Daher beträgt die Laufzeit O(n × n!), dieselbe Größenordnung wie die Größe der Antwort. Zusätzlich zur Ausgabe enthalten der Pfad, die Flags und der Aufrufstapel jeweils höchstens n Einträge.
Algorithmus
- Sortiere die Werte und erstelle ein
used-Array mitnfalschen Markierungen. - Schreibe
explore(). WennpathnWerte enthält, füge eine Kopie zum Ergebnis hinzu und kehre zurück. - Andernfalls markiere für jeden Index
ivon 0 bis n-1, dessen Wert frei ist, ihn als verwendet und fügevalues[i]hinzu (wähle aus), rufeexplore()auf (erkunde), entferne ihn anschließend und markiere ihn als frei (mache die Auswahl rückgängig). - Rufe
explore()einmal auf und gib das Ergebnis zurück.
def permute(nums):
values = sorted(nums)
n = len(values)
result = []
path = []
used = [False] * n
def explore():
# A full path is a leaf of the decision tree: one finished ordering.
if len(path) == n:
result.append(path[:])
return
# Smallest unused value first, so the leaves come out in lexicographic order.
for i in range(n):
if used[i]:
continue
used[i] = True
path.append(values[i]) # choose
explore() # explore
path.pop() # un-choose
used[i] = False
explore()
return result
Stolperfallen und Grenzfälle
Backtracking-Fehler entstehen fast immer dadurch, dass ein Zustand nicht wiederhergestellt oder versehentlich gemeinsam genutzt wird.
pathaufzeichnen, anstatt eine Kopie davon zu speichern. Alle n! Einträge verweisen am Ende auf dieselbe Liste, die nach Abschluss der Suche leer ist.- Eine Auswahl nur teilweise rückgängig machen. Wenn du den Wert entfernst, aber
used[i]gesetzt lässt, erscheint dieser Wert in einem späteren Zweig nie wieder, und du erhältst weniger als n! Anordnungen. numsvorher nicht sortieren. Die Suche findet zwar weiterhin jede Anordnung, sie folgen aber der Reihenfolge der Eingabe, sodass die Eingabe[3, 1, 2]zuerst aufgelistet würde.- Die Tauschmethode verwenden (
nums[start]mit jeder späteren Position tauschen, rekursiv aufrufen, zurücktauschen), ohne abschließend zu sortieren. Sie findet alle n! Anordnungen, listet aber für[1, 2, 3][3, 2, 1]vor[3, 1, 2]auf. - Mit einer Suche in
pathprüfen, ob ein Wert bereits verwendet wurde. Das funktioniert hier nur, weil die Werte unterschiedlich sind, und kostet bei jedem Schritt n. Ein Flag pro Index benötigt O(1) und funktioniert auch bei wiederholten Werten.
Häufige Fragen4
Wie viele Permutationen hat eine Liste mit n verschiedenen Elementen?
n!, gelesen „n Fakultät“: n Möglichkeiten für die erste Position, n-1 für die zweite, bis hin zu einer für die letzte, miteinander multipliziert. Drei Werte ergeben 6 Anordnungen, sechs ergeben 720, und zehn ergeben bereits 3,628,800, weshalb bei Permutationsproblemen n klein gehalten wird.
Wie hoch ist die Zeitkomplexität beim Erzeugen aller Permutationen?
O(n × n!). Es gibt n! Anordnungen, und jede vollständig aufzuschreiben dauert n Schritte. Daher kann keine Methode besser sein, wenn sie alle zurückgeben muss. Backtracking erreicht diese Grenze und benötigt zusätzlich zur Ausgabe O(n) Speicherplatz für den aktuellen Pfad, die verwendeten Markierungen und die Rekursion.
Warum erzeugt Backtracking Permutationen in lexikografischer Reihenfolge?
Es handelt sich um eine Tiefensuche, die zuerst den kleinsten verfügbaren Wert ausprobiert. Sie vervollständigt jede Anordnung, die mit einem bestimmten Präfix beginnt, bevor sie zum nächsten Präfix übergeht, und probiert die Präfixe von klein nach groß aus. Das entspricht der Reihenfolge, in der ein Wörterbuch Wörter sortiert, sofern die Eingabe vor Beginn der Suche sortiert wird.
Wie erzeugt man Permutationen, wenn die Eingabe Duplikate enthält?
Sortiere die Werte und überspringe an jeder Position einen Wert, der dem vorherigen entspricht, solange diese frühere Kopie nicht verwendet wird: i > 0, values[i] == values[i-1] und !used[i-1]. Dadurch werden gleiche Werte in ihrer ursprünglichen Reihenfolge angeordnet, sodass jede unterschiedliche Anordnung genau einmal erstellt wird.
Ähnliche Aufgaben
Aufgaben mit denselben Ideen. Wer zwei oder drei davon löst, behält das Muster.
Python
def permute(nums):
# Schreibe hier den CodeFall 1
Fall 2
Fall 3
Eingabe
nums = [3, 1, 2]
Erwartet
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]