Grundlagen

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.

Lesestand

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

StrukturStark beiSchwach beiIn Java
Array / dynamisches ArrayZugriff per Index O(1), Durchlaufen (Cache!)Einfügen/Löschen vorn oder in der Mitte O(n), Suchen O(n)int[], ArrayList
Verkettete ListeEinfügen/Löschen an bekannter Stelle O(1)Zugriff per Index O(n), schlecht für den CacheLinkedList
Stapel (Stack)zuletzt hinein, zuerst heraus – O(1)alles andereArrayDeque mit push/pop
Schlange (Queue)zuerst hinein, zuerst heraus – O(1)alles andereArrayDeque mit offer/poll
HashtabelleEinfügen, Finden, Löschen per Schlüssel O(1) im Mittelkeine Reihenfolge, keine BereichsabfragenHashMap, HashSet
Balancierter Suchbaumsortiert, Bereiche, Minimum, Nachbarn – alles O(log n)langsamer als Hashing bei reinem NachschlagenTreeMap, TreeSet
Heapkleinstes (oder größtes) Element O(1), Einfügen und Entnehmen O(log n)Suchen nach beliebigen Werten O(n)PriorityQueue
GraphBeziehungen beliebiger Art: Wege, Nachbarn, Netzebraucht eigene Algorithmenkeine 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:

Eine HashMap von innen

Wie im JDK: Startgröße 16 Fächer, Hashcode von String wie in Java, Verteilung mit h ^ (h >>> 16), Fach = Hash & (Größe − 1). Bei mehr als 75 % Füllung verdoppelt sich die Tabelle.

Drei Dinge folgen daraus für jeden Java-Entwickler:

  • equals und hashCode gehö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 get unauffindbar, 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:

Gleiche Werte, andere Reihenfolge

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.

PriorityQueue zum Zuschauen

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 durchlaufenArrayList – fast immer die richtige Wahl
vorn und hinten einfügen und entnehmen, Stapel oder SchlangeArrayDeque – nicht Stack, nicht LinkedList
schnelles Nachschlagen per SchlüsselHashMap / HashSet
… mit EinfügereihenfolgeLinkedHashMap / LinkedHashSet
… sortiert, mit BereichsabfragenTreeMap / TreeSet
immer das kleinste oder dringendste ElementPriorityQueue
Schlüssel sind Enum-WerteEnumMap / EnumSet – intern ein Array oder ein Bitfeld, sehr schnell
gleichzeitiger Zugriff aus mehreren ThreadsConcurrentHashMap, ConcurrentLinkedQueue, BlockingQueue
Millionen Zahlen ohne Boxingint[] 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

„LinkedList ist besser zum Einfügen.“ – Nur wenn man die Stelle schon hat, etwa über einen Iterator. Sonst muss man sie erst suchen, in O(n) mit vielen Cache-Fehlschlägen. In der Praxis ist ArrayList fast immer schneller.
„Eine HashMap gibt ihre Einträge in Einfügereihenfolge zurück.“ – Manchmal sieht es so aus, besonders bei kleinen Zahlen. Verlassen darf man sich darauf nie; die Reihenfolge kann sich mit der Tabellengröße und der Java-Version ändern. Dafür gibt es LinkedHashMap.
„Hashcodes sind eindeutig.“ – Es gibt nur rund vier Milliarden 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.
„Für einen Stapel nimmt man die Klasse Stack.“ – Stack stammt aus Java 1.0, erbt von Vector und synchronisiert jeden Aufruf. Die Dokumentation empfiehlt selbst ArrayDeque.
„Eine PriorityQueue ist sortiert.“ – Nur die Spitze ist garantiert das Minimum. Wer über sie iteriert oder sie ausgibt, sieht die Heap-Reihenfolge, keine sortierte.

Verwandte Begriffe