Heap und Hashmap
Einführung
Du arbeitest an einem Projekt mit vielen parallelen Aufgaben: Ein kritischer Bug, ein Kundenmeeting, Routine-E-Mails. Klar ist, dass nicht alle Aufgaben gleich dringend sind. Gleichzeitig musst du schnell auf Benutzerdaten zugreifen können, ohne lange Listen zu durchsuchen. Wie entscheidest du, was zuerst dran ist, und wie findest du blitzschnell die richtigen Informationen?

Genau hier kommen zwei wichtige Datenstrukturen ins Spiel: PriorityQueue (basierend auf dem Heap) und HashMap. Die PriorityQueue sorgt dafür, dass dringende Aufgaben automatisch bevorzugt werden.
Die HashMap ermöglicht dir, Informationen direkt über einen eindeutigen Schlüssel abzurufen. Gemeinsam bilden sie zentrale Werkzeuge, um komplexe Abläufe in der Informatik effizient zu organisieren.
Lernziele
Nach dieser Lerneinheit kannst du:
- Das Prinzip einer PriorityQueue erklären und ihre technische Umsetzung durch einen Heap nachvollziehen.
- Den Unterschied zwischen Min-Heap und Max-Heap beschreiben und deren jeweilige Einsatzszenarien benennen.
- Die Funktionsweise einer HashMap erläutern und typische Methoden wie
put(),get(),containsKey()undremove()anwenden. - Praxisrelevante Anwendungsfälle von PriorityQueue und HashMap identifizieren und ihre Vorteile in realen IT-Szenarien begründen.
Hinweis: Die folgenden Inhalte sind möglicherweise nicht prüfungsrelevant. In der Abschlussprüfung Teil 2 werden in der Regel nur zweidimensionale Arrays im Bereich der Datenstrukturen behandelt.
Was ist ein Heap?
Um das Konzept des Heaps zu verstehen, hilft eine Analogie aus dem echten Leben: die Notaufnahme eines Krankenhauses. Patienten werden dort nicht in der Reihenfolge ihres Eintreffens behandelt – das wäre das Prinzip einer normalen Warteschlange (Queue).
Stattdessen werden sie nach der Dringlichkeit ihrer medizinischen Verfassung triagiert. Ein Patient mit Verdacht auf einen Herzinfarkt wird sofort behandelt, auch wenn eine Person mit einem verstauchten Knöchel schon länger wartet. Die Priorität entscheidet, nicht die Ankunftszeit.
Genau dieses Prinzip setzt eine PriorityQueue (Prioritätswarteschlange) in der Informatik um. Sie ist eine abstrakte Datenstruktur, die Elemente nicht nach dem FIFO-Prinzip (First-In, First-Out) verwaltet, sondern stets das Element mit der höchsten Priorität an den Anfang stellt.
Was ist ein Heap?
Die technische Realisierung dieser Prioritätswarteschlange erfolgt in Java und vielen anderen Sprachen durch eine spezielle baumähnliche Datenstruktur namens Heap.
Ein Heap ist so konstruiert, dass er eine bestimmte Ordnungseigenschaft, die sogenannte Heap-Bedingung (oder Heap-Invariante), zu jeder Zeit aufrechterhält: Jeder Knoten im Baum hat eine höhere (oder gleiche) Priorität als seine Kinder. Dadurch ist garantiert, dass sich das Element mit der global höchsten Priorität immer an der Wurzel, also an der absoluten Spitze des Baumes, befindet.
Es gibt zwei Haupttypen von Heaps:
- Min-Heap: Der Wert des Elternknotens ist immer kleiner oder gleich dem seiner Kindknoten. Das Element mit dem kleinsten Wert hat somit die höchste Priorität und befindet sich an der Wurzel. Die
java.util.PriorityQueuein Java ist standardmäßig ein Min-Heap. - Max-Heap: Der Wert des Elternknotens ist immer größer oder gleich dem seiner Kindknoten. Hier hat das Element mit dem größten Wert die höchste Priorität.
Heap: Hinweis
Für Entwickler ist es entscheidend zu verstehen, dass sie in der Praxis selten einen Heap von Grund auf neu implementieren. Stattdessen verwenden sie eine fertige Klasse wie die PriorityQueue.
Der Heap ist der clevere und effiziente “Motor” unter der Haube, der dafür sorgt, dass die PriorityQueue ihre Aufgabe – die Verwaltung nach Priorität – so performant erledigen kann. Dieses Prinzip der Trennung von Was (die Funktionalität einer Priority Queue) und Wie (die Implementierung durch einen Heap) ist ein zentrales Konzept in der modernen Softwareentwicklung.
Warum einen Heap verwenden?
Die PriorityQueue, basierend auf dem Heap, ist unverzichtbar in Szenarien, in denen Aufgaben oder Daten nicht sequenziell, sondern nach Wichtigkeit abgearbeitet werden müssen.
Typische Anwendungsfälle sind:
- Task-Scheduling in Betriebssystemen: Das Betriebssystem muss entscheiden, welcher Prozess als Nächstes Rechenzeit auf der CPU erhält. Prozessen mit höherer Priorität (z. B. eine Benutzereingabe) wird Vorrang vor Hintergrundprozessen mit niedrigerer Priorität (z. B. eine Dateisicherung) gegeben.
- Support-Ticket-Systeme: Ein Ticket, das einen kompletten Systemausfall meldet (“Severity 1”), muss sofort bearbeitet werden, während eine allgemeine Anfrage zur Bedienung warten kann.
- Netzwerk-Routing und Routenplanung: Algorithmen zur Wegfindung, wie der Algorithmus von Dijkstra, verwenden eine Priority Queue, um aus einer Menge von möglichen nächsten Wegpunkten immer denjenigen mit der geringsten Entfernung zum Startpunkt auszuwählen und als Nächstes zu untersuchen.
- Lastverteilung auf Servern (Load Balancing): Anfragen an ein Server-Cluster können nach ihrer Wichtigkeit priorisiert werden, um sicherzustellen, dass kritische Transaktionen auch unter hoher Last bevorzugt behandelt werden.
Der Heap in Aktion: Die PriorityQueue in Java
Betrachten wir ein praktisches Szenario: die Entwicklung eines einfachen Task-Managers. Eine niedrige Zahl soll eine hohe Priorität bedeuten (1 = am wichtigsten).
Schritt 1: Die Datenstruktur für unsere Aufgaben definieren (Task-Klasse)
Zuerst brauchen wir eine Klasse, die eine einzelne Aufgabe beschreibt. Damit die PriorityQueue weiß, wie sie unsere Aufgaben sortieren soll, muss diese Klasse das Comparable-Interface implementieren. Darin legen wir fest, dass die Priorität das Sortierkriterium ist.
// Die Task-Klasse repräsentiert eine Aufgabe mit Beschreibung und Priorität.
// Sie implementiert Comparable, damit die PriorityQueue weiß, wie Tasks zu vergleichen sind.
class Task implements Comparable<Task> {
private String description;
private int priority;
public Task(String description, int priority) {
this.description = description;
this.priority = priority;
}
public int getPriority() {
return priority;
}
@Override
public String toString() {
return "Task{" + "description='" + description + '\'' + ", priority=" + priority + '}';
}
// Die compareTo-Methode definiert die Sortierlogik.
// Ein niedrigerer Prioritätswert bedeutet eine höhere Priorität.
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.getPriority());
}
}Zusammenfassung des Codes:
- Was wurde gemacht? Es wurde eine Klasse Task als Bauplan für unsere Aufgaben erstellt. Sie hat Eigenschaften wie description und priority.
- Warum wurde es so gemacht? Durch implements Comparable
und die compareTo-Methode geben wir der PriorityQueue eine klare Anweisung, wie sie Task-Objekte vergleichen soll – nämlich anhand ihrer Prioritätsnummer. Ohne diese “Sortieranleitung” wüsste Java nicht, welche Aufgabe wichtiger ist.
Der Heap in Aktion: Die PriorityQueue in Java
Schritt 2: PriorityQueue erstellen und Aufgaben hinzufügen
Jetzt erstellen wir die PriorityQueue und fügen einige Aufgaben in unsortierter Reihenfolge hinzu.
Die PriorityQueue sorgt bei jedem add()-Aufruf intern dafür, dass die Heap-Ordnung erhalten bleibt.
import java.util.PriorityQueue;
public class TaskManager {
public static void main(String[] args) {
// Erstellen einer PriorityQueue für Task-Objekte.
PriorityQueue<Task> taskQueue = new PriorityQueue<>();
// Hinzufügen von Aufgaben in unsortierter Reihenfolge.
taskQueue.add(new Task("Dokumentation aktualisieren", 3));
taskQueue.add(new Task("Kritischen Bug fixen", 1));
taskQueue.add(new Task("Kundenmeeting vorbereiten", 2));
taskQueue.add(new Task("E-Mails beantworten", 4));
}
}Zusammenfassung des Codes:
- Was wurde gemacht? Eine leere PriorityQueue mit dem Namen taskQueue wurde erstellt. Anschließend wurden vier neue Task-Objekte mit unterschiedlichen Prioritäten hinzugefügt.
- Warum wurde es so gemacht? Dies simuliert ein reales Szenario, in dem Aufgaben zu unterschiedlichen Zeiten und mit unterschiedlicher Dringlichkeit anfallen. Die PriorityQueue ist nun unser zentraler “Manager”, der diese Aufgaben automatisch nach Wichtigkeit sortiert.
Der Heap in Aktion: Die PriorityQueue in Java
Schritt 3: Die wichtigste Aufgabe ansehen (ohne sie zu entfernen)
Mit der peek()-Methode können wir uns das Element mit der höchsten Priorität ansehen, das ganz oben im Heap liegt. Die Warteschlange selbst bleibt dabei unverändert.
// Fortsetzung der main-Methode...
// Die peek()-Methode zeigt die Aufgabe mit der höchsten Priorität an.
System.out.println("Nächste Aufgabe (höchste Priorität): " + taskQueue.peek());
// Erwartete Ausgabe: Task{description='Kritischen Bug fixen', priority=1}Zusammenfassung des Codes:
- Was wurde gemacht? Die Methode peek() wurde aufgerufen, um die vorderste Aufgabe in der Warteschlange abzurufen und auszugeben.
- Warum wurde es so gemacht? peek() ist wie ein kurzer Blick auf den nächsten Punkt auf der To-do-Liste, ohne ihn abzuhaken. Es ist nützlich, um zu sehen, was als Nächstes ansteht, ohne die Reihenfolge der Aufgaben zu verändern.
Der Heap in Aktion: Die PriorityQueue in Java
Schritt 4: Alle Aufgaben nach Priorität abarbeiten
Die poll()-Methode entfernt das Element mit der höchsten Priorität aus der Warteschlange und gibt es zurück.
In einer Schleife können wir so alle Aufgaben in der korrekten Reihenfolge abarbeiten – von der wichtigsten zur unwichtigsten.
// Fortsetzung der main-Methode...
System.out.println("\n--- Aufgaben werden abgearbeitet ---");
while (!taskQueue.isEmpty()) {
Task currentTask = taskQueue.poll();
System.out.println("Bearbeite: " + currentTask);
}Zusammenfassung des Codes:
- Was wurde gemacht? Eine while-Schleife läuft, solange die taskQueue nicht leer ist. In jeder Wiederholung der Schleife wird mit poll() die Aufgabe mit der höchsten Priorität geholt und entfernt.
- Warum wurde es so gemacht? Dies simuliert den eigentlichen Arbeitsprozess. poll() stellt sicher, dass immer die dringendste Aufgabe zuerst erledigt wird. Die Schleife garantiert, dass alle Aufgaben abgearbeitet werden, bis die Warteschlange leer ist.
Was ist eine HashMap?
Stell dir vor, du möchtest die Definition eines Wortes in einem dicken Wörterbuch nachschlagen. Du würdest nicht auf der ersten Seite anfangen und das gesamte Buch durchblättern.
Stattdessen nutzt du die alphabetische Sortierung, um direkt zur richtigen Seite zu springen. Ein Telefonbuch oder das Lager eines Möbelhauses, in dem jeder Artikel eine eindeutige Nummer hat, funktionieren nach demselben Prinzip.
Eine HashMap ist das digitale Äquivalent zu diesem Prinzip. Sie speichert Informationen in Schlüssel-Wert-Paaren (Key-Value-Pairs). Jeder Wert ist einem einzigartigen Schlüssel zugeordnet. Wird ein bereits vorhandener Schlüssel erneut hinzugefügt, überschreibt der neue Wert den alten.
Was ist eine HashMap?
Die “Magie” hinter der Geschwindigkeit einer HashMap ist die Hash-Funktion. Diese berechnet aus dem Schlüssel eine eindeutige Regalnummer (Index), an dem der Wert gespeichert wird. Beim Abrufen wird derselbe Index berechnet und der Wert sofort gefunden - ohne langes Suchen.
Kann es zu Überschneidungen kommen? Ja, das nennt man Kollision. Moderne HashMaps lösen das, indem sie an dieser Stelle eine kleine Liste mit allen Werten führen. Für dich als Entwickler bleibt das verborgen.
Warum eine HashMap verwenden?
Der Hauptgrund ist die Performance: Einfügen, Abrufen und Löschen von Elementen erfolgen im Durchschnitt in konstanter Zeit (O(1)) – unabhängig davon, ob 100 oder 10 Millionen Einträge gespeichert sind.
Zum Vergleich: Um einen Benutzer mit ID “ID-12345” in einer ArrayList mit einer Million Einträgen zu finden, müsstest du im schlimmsten Fall alle durchsuchen. In einer HashMap erhältst du das Ergebnis sofort über den Schlüssel.
Typische Anwendungsbereiche:
- Datenbank-Indizierung und Caching: Schneller Zugriff über IDs oder zum Speichern berechneter Ergebnisse.
- Konfigurationsdaten: Einstellungen als Schlüssel-Wert-Paare, z. B.
"database_url" -> "jdbc:mysql://...". - Datenrepräsentation: Eindeutige Identifier wie Personalnummern, ISBNs oder IP-Adressen.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Stellen wir uns eine einfache Benutzerverwaltung vor. Jeder Benutzer hat eine eindeutige ID (Integer) und ein Benutzer-Objekt.
Schritt 1: Die Datenstruktur für unsere Benutzer definieren (Benutzer-Klasse)
Dies ist eine einfache Klasse, welche die Daten eines Benutzers enthält.
// Eine einfache Klasse zur Repräsentation eines Benutzers.
class Benutzer {
private String name;
private String email;
public Benutzer(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return "Benutzer{" + "name='" + name + '\'' + ", email='" + email + '\'' + '}';
}
}Zusammenfassung des Codes:
- Was wurde gemacht? Eine einfache Klasse Benutzer wurde als Vorlage für Benutzerobjekte erstellt. Sie speichert den Namen und die E-Mail-Adresse.
- Warum wurde es so gemacht? Diese Klasse bündelt zusammengehörige Informationen (Name und E-Mail) in einem einzigen Objekt. Das macht den Code übersichtlicher, als wenn du für jeden Benutzer einzelne Variablen verwalten müsstest.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Schritt 2: HashMap erstellen und Benutzer hinzufügen
Wir erstellen eine HashMap, bei der die Benutzer-ID der Schlüssel (Integer) und das Benutzer-Objekt der Wert ist. Mit put(key, value) fügen wir neue Einträge hinzu.
import java.util.HashMap;
import java.util.Map;
public class Benutzerverwaltung {
public static void main(String[] args) {
Map<Integer, Benutzer> benutzerDB = new HashMap<>();
// Hinzufügen von Benutzern mit put(key, value)
benutzerDB.put(101, new Benutzer("Alice Wonder", "alice@example.com"));
benutzerDB.put(102, new Benutzer("Bob Builder", "bob@example.com"));
benutzerDB.put(103, new Benutzer("Charlie Brown", "charlie@example.com"));
}
}Zusammenfassung des Codes:
- Was wurde gemacht? Eine HashMap namens benutzerDB wurde erstellt. Sie speichert eine Integer-ID als Schlüssel und ein Benutzer-Objekt als Wert. Drei Benutzer wurden mit der put-Methode hinzugefügt.
- Warum wurde es so gemacht? Wir bauen hier unsere “Benutzerdatenbank”. Die HashMap ist ideal, weil sie eine eindeutige ID (den Schlüssel) direkt mit den vollständigen Benutzerdaten (dem Wert) verknüpft, wie ein Register in einem Aktenschrank.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Schritt 3: Benutzer über ID abrufen
Mit get(key) kannst du blitzschnell auf die Daten eines Benutzers zugreifen, indem du seine ID angibst. Wenn der Schlüssel nicht existiert, gibt die Methode null zurück.
// Fortsetzung der main-Methode...
Benutzer gesuchterBenutzer = benutzerDB.get(102);
System.out.println("Benutzer mit ID 102 gefunden: " + gesuchterBenutzer);
// Erwartete Ausgabe: Benutzer{name='Bob Builder', email='bob@example.com'}Zusammenfassung des Codes:
- Was wurde gemacht? Mit benutzerDB.get(102) wurden die Daten des Benutzers mit der ID 102 direkt abgerufen.
- Warum wurde es so gemacht? Dies demonstriert die Superkraft der HashMap: den extrem schnellen Zugriff. Anstatt eine lange Liste zu durchsuchen, kannst du direkt auf einen Eintrag zugreifen, wenn du seinen Schlüssel kennst.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Schritt 4: Prüfen, ob eine ID bereits vergeben ist
Bevor du einen neuen Benutzer anlegst, kannst du mit containsKey(key) prüfen, ob die ID schon existiert.
// Fortsetzung der main-Methode...
if (benutzerDB.containsKey(101)) {
System.out.println("\nDie Benutzer-ID 101 ist bereits vergeben.");
}Zusammenfassung des Codes:
- Was wurde gemacht? Mit containsKey(101) wurde überprüft, ob ein Eintrag mit dem Schlüssel 101 bereits in der Map vorhanden ist.
- Warum wurde es so gemacht? Dies ist eine wichtige Sicherheitsprüfung. Da jeder Schlüssel in einer HashMap einzigartig sein muss, nutzt du diese Methode, um sicherzustellen, dass du nicht versehentlich die Daten eines vorhandenen Benutzers überschreibst.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Schritt 5: Daten aktualisieren und entfernen
// Fortsetzung der main-Methode...
// Aktualisieren von Benutzer 101
benutzerDB.put(101, new Benutzer("Alice Wonderland", "alice.w@example.com"));
// Entfernen von Benutzer 103
benutzerDB.remove(103);
System.out.println("\nDatenbank nach Änderungen: " + benutzerDB);Zusammenfassung des Codes:
- Was wurde gemacht? Zuerst wurde put() erneut mit dem Schlüssel 101 aufgerufen, um die Daten von Alice zu aktualisieren. Danach wurde der Benutzer mit der ID 103 durch remove() komplett gelöscht.
- Warum wurde es so gemacht? Dies zeigt, wie einfach die Datenpflege mit einer HashMap ist. put() dient sowohl zum Einfügen als auch zum Aktualisieren, während remove() gezielt einzelne Einträge löscht.
Die HashMap in Aktion: Anwendungsbeispiele in Java
Schritt 6: Über alle Einträge iterieren
Um alle verbleibenden Benutzer anzuzeigen, kannst du eine for-each-Schleife über das entrySet() der Map verwenden. Jeder Eintrag (Entry) enthält dabei sowohl den Schlüssel als auch den Wert.
// Fortsetzung der main-Methode...
System.out.println("\n--- Alle verbleibenden Benutzer ---");
for (Map.Entry<Integer, Benutzer> eintrag : benutzerDB.entrySet()) {
System.out.println("ID: " + eintrag.getKey() + ", Daten: " + eintrag.getValue());
}Zusammenfassung des Codes:
- Was wurde gemacht? Eine for-each-Schleife durchläuft alle Einträge der HashMap. Die entrySet()-Methode liefert dafür eine Sammlung aller Schlüssel-Wert-Paare.
- Warum wurde es so gemacht? Manchmal musst du alle in der HashMap gespeicherten Daten verarbeiten (z. B. für einen Report). Diese Schleife ist der Standardweg, um auf jeden einzelnen Eintrag zuzugreifen und sowohl den Schlüssel (ID) als auch den Wert (Benutzerdaten) zu erhalten.
Zusammenfassung
Zusammenfassung:
PriorityQueue und Heap
Eine PriorityQueue ist eine Warteschlange, die Elemente nicht nach der Ankunftszeit, sondern nach ihrer Priorität sortiert. Die technische Grundlage dafür ist der Heap, eine spezielle Baumstruktur, welche die sogenannte Heap-Bedingung sicherstellt.
-
Definition PriorityQueue: Eine Datenstruktur, die stets das Element mit der höchsten Priorität zuerst bereitstellt.
-
Heap-Bedingung: Jeder Knoten hat eine höhere oder gleiche Priorität als seine Kinder.
-
Heap-Typen:
- Min-Heap: Elternknoten ≤ Kindknoten. Kleinster Wert steht an der Wurzel.
- Max-Heap: Elternknoten ≥ Kindknoten. Größter Wert steht an der Wurzel.
-
Implementierung in Java:
java.util.PriorityQueuenutzt standardmäßig einen Min-Heap. -
Praxisbeispiel: Entwicklung eines Task-Managers mit einer
Task-Klasse, die dasComparable-Interface implementiert. Dadurch kann die PriorityQueue Aufgaben automatisch nach Priorität sortieren. -
Anwendungsfälle:
- Betriebssysteme beim Prozess-Scheduling
- Support-Ticket-Systeme
- Routenplanung mit Dijkstra-Algorithmus
- Lastverteilung auf Servern
Die PriorityQueue übernimmt also die Verwaltung, während der Heap die effiziente Umsetzung ermöglicht.
HashMap
Eine HashMap ist eine Datenstruktur, die Schlüssel-Wert-Paare speichert und über eine Hash-Funktion extrem schnellen Zugriff auf Werte ermöglicht.
-
Definition HashMap: Eine Sammlung, die jedem Schlüssel genau einen Wert zuordnet. Bei doppeltem Schlüssel wird der alte Wert überschrieben.
-
Funktionsweise:
- Schlüssel wird durch eine Hash-Funktion in einen Index umgerechnet.
- Werte werden im internen Array an diesem Index gespeichert.
- Kollisionen (mehrere Schlüssel führen zum selben Index) werden durch Listenverwaltung gelöst.
-
Leistungsfähigkeit: Operationen wie Einfügen, Abrufen oder Löschen erfolgen im Durchschnitt in konstanter Zeit O(1).
-
Praxisbeispiel: Benutzerverwaltung mit einer
Benutzer-Klasse und einer HashMap, in der die Benutzer-ID als Schlüssel und das Benutzer-Objekt als Wert dient. -
Wichtige Methoden:
put(key, value)- Einfügen oder Aktualisieren von Datenget(key)- Abrufen eines Werts über seinen SchlüsselcontainsKey(key)- Prüfen, ob ein Schlüssel existiertremove(key)- Löschen eines Eintrags- Iteration mit
entrySet()- Zugriff auf alle Schlüssel-Wert-Paare
-
Anwendungsfälle:
- Datenbank-Indizierung und Caching
- Speicherung von Konfigurationsdaten
- Allgemeine Nutzung bei eindeutig identifizierbaren Daten (z. B. ISBN, IP-Adresse)
Die HashMap eignet sich besonders, wenn extrem schneller Zugriff auf Daten über eindeutige Schlüssel erforderlich ist.