Datenstrukturen
Wie man Daten anordnet, damit genau die Operationen billig werden, die man braucht: Array und Liste, Hashtabelle, Suchbaum, Heap, Stapel und Schlange – und was die Java-Collections intern wirklich tun.
Eine Datenstruktur legt fest, wie Daten im Speicher angeordnet sind – und damit, welche Operationen billig und welche teuer sind. Kein Aufbau ist für alles gut: Ein Array findet das zehnte Element sofort, aber Einfügen am Anfang verschiebt alles. Eine Hashtabelle findet einen Schlüssel in konstanter Zeit, kennt aber keine Reihenfolge. Ein Suchbaum hält alles sortiert, braucht dafür aber logarithmische Zeit.
Die Wahl folgt aus der Frage: Welche Operationen braucht mein Programm am häufigsten? Die Java-Collections sind fertige, sehr gute Umsetzungen der klassischen Strukturen – wer weiß, was in ihnen steckt, wählt die richtige.
Der Werkzeugkasten
| Struktur | Stark bei | Schwach bei | In Java |
|---|---|---|---|
| Array / dynamisches Array | Zugriff per Index O(1), Durchlaufen (Cache!) | Einfügen/Löschen vorn oder in der Mitte O(n), Suchen O(n) | int[], ArrayList |
| Verkettete Liste | Einfügen/Löschen an bekannter Stelle O(1) | Zugriff per Index O(n), schlecht für den Cache | LinkedList |
| Stapel (Stack) | zuletzt hinein, zuerst heraus – O(1) | alles andere | ArrayDeque mit push/pop |
| Schlange (Queue) | zuerst hinein, zuerst heraus – O(1) | alles andere | ArrayDeque mit offer/poll |
| Hashtabelle | Einfügen, Finden, Löschen per Schlüssel O(1) im Mittel | keine Reihenfolge, keine Bereichsabfragen | HashMap, HashSet |
| Balancierter Suchbaum | sortiert, Bereiche, Minimum, Nachbarn – alles O(log n) | langsamer als Hashing bei reinem Nachschlagen | TreeMap, TreeSet |
| Heap | kleinstes (oder größtes) Element O(1), Einfügen und Entnehmen O(log n) | Suchen nach beliebigen Werten O(n) | PriorityQueue |
| Graph | Beziehungen beliebiger Art: Wege, Nachbarn, Netze | braucht eigene Algorithmen | keine Standardklasse – meist Map<Knoten, List<Knoten>> |
Arrays und Listen kamen schon in der Speicherhierarchie vor: Ein Array liegt am Stück im Speicher, eine verkettete Liste verstreut. Dass die Liste in der O-Notation beim Einfügen besser aussieht, ändert wenig – man muss die Einfügestelle ja erst finden, und das ist bei ihr O(n) mit vielen Cache-Fehlschlägen.
Die Hashtabelle: wie HashMap funktioniert
Eine Hashtabelle ist ein Array von Fächern (Buckets). Aus dem Schlüssel wird eine Zahl berechnet, der Hashcode, und daraus das Fach. Zum Finden rechnet man dasselbe noch einmal und schaut in genau ein Fach – egal, wie groß die Tabelle ist. Landen zwei Schlüssel im selben Fach, ist das eine Kollision; sie liegen dann in einer kleinen Liste nebeneinander. Probier es mit echten Java-Hashcodes:
Drei Dinge folgen daraus für jeden Java-Entwickler:
equalsundhashCodegehören zusammen. Gleiche Objekte müssen gleiche Hashcodes haben, sonst findet die Map sie nicht – sie schaut im falschen Fach nach. Records erzeugen beides automatisch und richtig.- Schlüssel dürfen sich nicht ändern. Ändert sich ein Feld, das in den Hashcode eingeht, nachdem der Schlüssel in der Map liegt, liegt er im falschen Fach – und ist für
getunauffindbar, obwohl er noch da ist. - Ein schlechter Hashcode macht aus O(1) O(n). Seit Java 8 wandelt HashMap ein überfülltes Fach in einen kleinen Baum um und begrenzt den Schaden auf
O(log n). Gute Verteilung bleibt trotzdem wichtig.
Suchbäume: sortiert und trotzdem schnell
Ein binärer Suchbaum hält Werte so, dass links von jedem Knoten nur Kleinere und rechts nur Größere stehen. Gesucht wird von der Wurzel abwärts, bei jedem Schritt eine Richtung – wie die binäre Suche aus dem vorigen Eintrag. Aber nur, wenn der Baum ausgewogen ist. Füge die Zahlen 1 bis 15 in verschiedener Reihenfolge ein:
Deshalb benutzen echte Implementierungen selbstbalancierende Bäume, die sich beim Einfügen und Löschen umbauen. Javas TreeMap ist ein Rot-Schwarz-Baum: Jeder Knoten trägt eine Farbe, und ein paar Regeln über die Farben garantieren, dass kein Weg mehr als doppelt so lang ist wie ein anderer. Datenbanken benutzen die breiteren B-Bäume, deren Knoten eine ganze Speicherseite füllen. Der Lohn für den Aufwand: Ein Baum kennt die Reihenfolge. firstKey(), ceilingKey(x), subMap(von, bis) – all das kann eine Hashtabelle nicht.
Der Heap: immer das Kleinste zuerst
Manchmal braucht man nicht alles sortiert, sondern nur immer das kleinste Element: die nächste fällige Aufgabe, den kürzesten offenen Weg, die Anfrage mit der höchsten Priorität. Dafür gibt es den Heap: ein Baum, in dem jeder Knoten kleiner ist als seine Kinder. Die Wurzel ist also immer das Minimum. Der Clou: Er wird lückenlos in einem Array gespeichert – die Kinder von Position i stehen an 2i+1 und 2i+2.
Ein neues Element kommt ans Ende und steigt auf, solange es kleiner ist als sein Elternknoten. Beim Entnehmen rückt das letzte Element an die Spitze und sinkt ab. Beides berührt nur einen Weg von der Wurzel zu einem Blatt – O(log n). Auf diesem Prinzip beruhen auch der Sortieralgorithmus Heapsort und Dijkstras Algorithmus für kürzeste Wege in Navigationssystemen.
Graphen
Ein Graph besteht aus Knoten und Kanten: Städte und Straßen, Personen und Freundschaften, Klassen und ihre Abhängigkeiten, Dienste und ihre Aufrufe. Gespeichert wird er meist als Adjazenzliste – für jeden Knoten die Liste seiner Nachbarn – oder bei dichten Graphen als Matrix. Die wichtigsten Algorithmen darauf: Breitensuche (kürzester Weg nach Anzahl Schritten), Tiefensuche (Erreichbarkeit, Zyklen finden), Dijkstra (kürzester Weg nach Gewicht) und topologisches Sortieren – genau das tut Maven, wenn es Module in die richtige Build-Reihenfolge bringt.
Für Entwickler übersetzt
| Ich brauche … | Nimm |
|---|---|
| eine Liste, meist hinten anfügen und durchlaufen | ArrayList – fast immer die richtige Wahl |
| vorn und hinten einfügen und entnehmen, Stapel oder Schlange | ArrayDeque – nicht Stack, nicht LinkedList |
| schnelles Nachschlagen per Schlüssel | HashMap / HashSet |
| … mit Einfügereihenfolge | LinkedHashMap / LinkedHashSet |
| … sortiert, mit Bereichsabfragen | TreeMap / TreeSet |
| immer das kleinste oder dringendste Element | PriorityQueue |
| Schlüssel sind Enum-Werte | EnumMap / EnumSet – intern ein Array oder ein Bitfeld, sehr schnell |
| gleichzeitiger Zugriff aus mehreren Threads | ConcurrentHashMap, ConcurrentLinkedQueue, BlockingQueue |
| Millionen Zahlen ohne Boxing | int[] oder Bibliotheken wie fastutil und Eclipse Collections |
// Seit Java 21: SequencedCollection – einheitlicher Zugriff auf Anfang und Ende
List<String> liste = new ArrayList<>(List.of("a", "b", "c"));
liste.getFirst(); liste.getLast(); liste.reversed();
LinkedHashMap<String, Integer> m = new LinkedHashMap<>();
m.putFirst("neu", 1); // vorn einfügen
m.firstEntry(); m.pollLastEntry();Häufige Irrtümer
O(n) mit vielen Cache-Fehlschlägen. In der Praxis ist ArrayList fast immer schneller.LinkedHashMap.int-Werte, aber unendlich viele Strings. "Aa" und "BB" haben in Java denselben Hashcode. Kollisionen sind normal; deshalb vergleicht die Map am Ende mit equals.Stack stammt aus Java 1.0, erbt von Vector und synchronisiert jeden Aufruf. Die Dokumentation empfiehlt selbst ArrayDeque.