Java PriorityQueue
Nutze die heap-basierte PriorityQueue in Java, um Elemente in Prioritätsreihenfolge mit natürlicher oder benutzerdefinierter Sortierung abzurufen.
PriorityQueue<E> ist der Heap des JDK. Es ist eine Queue, aber statt FIFO ist der Kopf immer das kleinste Element gemäß einem Comparator (oder der natürlichen Reihenfolge, wenn keiner angegeben wurde). offer ist O(log n), poll und peek am Kopf sind jeweils O(log n) und O(1), und der zugrundeliegende Speicher ist ein flaches Array — keine Knotenallokierung pro Element. Das macht sie zum richtigen Werkzeug für Scheduler-ähnliche Probleme: „Arbeite immer an dem, was als Nächstes am dringendsten ist."
Was „Kopf" hier bedeutet
Bei einer FIFO-Warteschlange ist der Kopf derjenige, der zuerst ankam. Bei einer PriorityQueue ist der Kopf derjenige mit dem kleinsten Wert zu diesem Zeitpunkt — nicht der kleinste zum Einfügezeitpunkt:
peek()gibt das aktuelle Minimum zurück.poll()entfernt das aktuelle Minimum und balanciert neu; das nächstepeekzeigt das neue Minimum.offer(x)fügt ein und balanciert neu; wennxdas neue Minimum ist, gibt das nächstepeekes zurück.
„Kleinstes" wird bestimmt durch:
- Den
Comparator, der dem Konstruktor übergeben wurde, falls vorhanden. - Andernfalls die natürliche Reihenfolge des Elements über
Comparable. Wenn die Elemente nichtComparablesind und keinComparatorübergeben wurde, wirft der erste Vergleichsaufruf eineClassCastException.
Es gibt keinen Maximalhaufen-Modus. Wenn ein Max-Heap gewünscht wird, übergib Comparator.reverseOrder() (oder den benutzerdefinierten Comparator umgekehrt) — das ist das Standardidiom und wird im folgenden Beispiel verwendet.
Einen erstellen
// Natural order (Comparable elements).
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// Reversed for max-heap behaviour.
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
// Custom comparator — by length, then alphabetically.
PriorityQueue<String> byLen = new PriorityQueue<>(
Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
// Pre-sized + comparator (initial capacity must come first).
PriorityQueue<String> bigByLen = new PriorityQueue<>(1_000,
Comparator.comparingInt(String::length));
// From an existing collection.
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(40, 10, 30, 20));Der Einzelargument-Konstruktor, der eine Collection entgegennimmt, ist O(n) — er heapifiziert in einem Schritt statt n O(log n)-offers. Nützlich, wenn alle Daten bereits vorhanden sind.
Iteration erfolgt NICHT in Prioritätsreihenfolge
Das ist die Überraschung, die die meisten Menschen einmal erlebt. Das Iterieren über eine PriorityQueue durchläuft das zugrundeliegende Heap-Array in der Reihenfolge, in der der Heap seine Knoten zufällig speichert — nicht in aufsteigender Reihenfolge:
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(40, 10, 30, 20));
System.out.println(pq); // possibly [10, 20, 30, 40]
for (int x : pq) System.out.print(x + " "); // possibly: 10 20 30 40
// possibly: 10 40 30 20Die Druckreihenfolge ist ein Implementierungsdetail des Heap-Layouts. Um Elemente in Prioritätsreihenfolge zu erhalten, muss der Inhalt geleert werden:
while (!pq.isEmpty()) System.out.print(pq.poll() + " "); // sortedBeachte dies bei forEach, stream, toArray oder wenn die Warteschlange zum Debuggen ausgegeben wird. Der von stream() zurückgegebene Stream ist nicht sortiert, und das Hinzufügen von .sorted() erfordert, dass die Elemente Comparable sind (oder es kann ein Comparator übergeben werden).
Das Mutieren eines Elements nach offer bricht den Heap
Die Priorität wird zum Zeitpunkt der Einfügung berechnet. Wenn ein Element eingefügt und danach das Feld, auf das der Comparator schaut, mutiert wird, ist der Heap in einem ungültigen Zustand — poll kann den falschen Kopf zurückgeben, contains kann verfehlen, die Invariante geht verloren. Es gibt keine update- oder decrease-key-Methode.
Die beiden Workarounds:
- Unveränderliche Elemente verwenden — Records mit Prioritätsfeldern oder Wrapper-Records wie
record Task(int priority, String name) {}. Dann gibt es nach dem Einfügen nichts zu mutieren. - Entfernen und neu einfügen, wenn die Priorität wirklich geändert werden muss:
pq.remove(task)istO(n)(es sucht), dann istpq.offer(taskWithNewPriority)O(log n).
Für eine kleine Warteschlange ist das Entfernen und erneute Einfügen in Ordnung. Für „Ich schreibe Dijkstras Algorithmus neu und brauche schnelles decrease-key" ist eine PriorityQueue nicht das richtige Werkzeug — dann braucht man einen indizierten Heap oder ein TreeSet aus (priority, node)-Paaren.
null und Thread-Sicherheit
Gleiche Regeln wie beim Rest von Queue:
- Keine Nulls.
pq.offer(null)wirftNullPointerException. - Nicht thread-sicher. Gleichzeitiger Zugriff benötigt
PriorityBlockingQueue, den Cousin ausjava.util.concurrent.
Ein Praxisbeispiel: ein kleiner Task-Scheduler
Das folgende Programm verwendet eine PriorityQueue, um Aufgaben nach Priorität zu planen (niedrigere Zahl = dringender). Es zeigt auch das Max-Heap-Idiom und die Überraschung bei der Iterationsreihenfolge.
Die Ausgabe lesen:
toString()undforEachzeigten die Aufgaben in irgendeiner Reihenfolge — nicht sortiert. Keines von beiden sollte zum Debuggen verwendet werden, ob die Priorität stimmt — mitpollleeren, um die echte Reihenfolge zu sehen.- Der Bulk-Konstruktor hat in einem Schritt einen korrekt geordneten Heap erzeugt — das ist der lineare Heapify-Pfad.
- Der Mutationsblock am Ende ist die Fußangel in konzentrierter Form. Wir haben die Priorität des Arrays nach dem Einfügen mutiert, der Heap hatte keine Möglichkeit, neu zu balancieren, und jetzt lügt
peek. Der Fix besteht entweder darin, einen unveränderlichen Wrapper zu verwenden, oder nach der Änderungremoveundofferaufzurufen.
Was kommt als Nächstes
Das nächste Kapitel behandelt die Implementierung, auf die man zurückgreift, wenn man eine FIFO-Warteschlange oder einen Stack oder ein Deque möchte — und diejenige, zu der das JDK-Javadoc als Standard für alle drei rät: ArrayDeque. Es ist die Klasse, die hinter den Kulissen den Großteil der Arbeit im Beispielcode der vorherigen zwei Kapitel erledigt.