Datenstrukturen đŸ—„ïž

⚠ Wichtig: Dies ist ein Kapitel ĂŒber verschiedene Arten von Datenstrukturen (nicht Datentypen!) und wie sie in Java implementiert sind bzw. werden können. Die Liste ist aber natĂŒrlich keineswegs vollstĂ€ndig! Es soll hier wirklich nur um jene Datenstrukturen gehen, die hĂ€ufig im normalen, alltĂ€glichen Gebrauch sind bzw. um “Klassiker”, die sich gut zum Lernen einer Programmiersprache selbst implementieren lassen. Ebenfalls enorm wichtige Datenstrukturen (wie etwa Heap oder Stack zur Speicherverwaltung) werden hier bewusst ausgespart, weil sie eher zu den IT-Grundlagen gehören, als in einen Java-Wegweiser.

Eine 👉 Datenstruktur ist ein Objekt, das Daten oder andere Objekte speichert bzw. referenziert. Es gibt zahlreiche Arten von Datenstrukturen, die sich jeweils in Aufbau und Funktionsweise (und somit in Vor- und Nachteilen) stark unterscheiden. Dieser Artikel thematisiert nur einige grundlegende Datenstrukturen, die sich sehr schön selbst in Java implementieren lassen.

💬 FĂŒr eine umfangreiche Übersicht ĂŒber viele verschiedene Datenstrukturen sei 🔗auf diese Seite verwiesen!

Listen

Eine Liste ist ein abstraktes Konzept einer Datenstruktur, deren Elemente eine stabile Reihenfolge besitzen, mehrfach vorkommen können und keine durch die Definition der Liste beschrÀnkte Anzahl haben.
Es sollen hier als Beispiel fĂŒr selbst implementierte Listen-Strukturen die verketteten Listen vorgestellt werden - fĂŒr die in der Java Class Library verfĂŒgbaren Implementationen von Listen, siehe Artikel zum 🔭 Collections Framework!

(Einfach) Verkettete Listen

💬 Es ist hier die Rede von einfach verketteten Listen. Im nĂ€chsten Abschnitt werden (darauf aufbauend) zweifach verkettete Listen besprochen.

Bei einer verketteten Liste handelt es sich um eine sehr einfache Datenstruktur, bei der die Daten sogenannten Knoten (engl.: nodes) zugeordnet sind. Diese Knoten bilden die eigentliche verkettete Liste (zusammen mit einem Start-Verweis auf den ersten Knoten!).

Ein einzelner Knoten besteht dabei aus nur zwei Elementen: Dem Datenfeld und einem Verweis (Referenz) auf den nÀchsten Knoten (Verkettung!):

Verkettete Liste
Beispiel: Verkettete Liste mit Integer-Werten; Löschung eines Knotens

Somit “kennt” ein Knoten immer nur den von ihm aus nĂ€chsten Knoten in der Liste. Soll ein Knoten aus der Liste entfernt werden, muss nur die Referenz auf diesen Knoten (ausgehend vom Knoten davor) auf den nĂ€chsten Knoten abgeĂ€ndert werden (siehe Grafik oben).

In Java sÀhe eine sehr einfache Implementation einer Verketteten Liste (mit Integer-Werten) etwa so aus:

public class Node {

    public int value;
    public Node next;

    public Node (int value){
        this.value = value;
    }

}

💬 Auf private Klassenattribute sowie Getter und Setter wurde zugunsten der Übersichtlichkeit hier verzichtet. Eigentlich sollte natĂŒrlich beides vorhanden sein!

Aus Instanzen dieser simplen Klasse lÀsst sich bereits eine verkettete Liste konstruieren:

Node first = new Node(12); // Start-Knoten
first.next = new Node(99); // zweiter Knoten
first.next.next = new Node(37); // dritter Knoten

Auch das Entfernen des zweiten Knotens (wie in der Grafik oben) ist wie beschrieben möglich:

first.next = first.next.next;

Da nun keine Referenz auf den zweiten Knoten mehr existiert, ist dieser effektiv “entfernt” - d.h. das Objekt wird irgendwann vom Garbage Collector der JVM entsorgt.

Dieses effiziente Entfernen von Elementen ist einer der Vorteile von verketteten Listen. Ein weiterer ist die “von Natur aus” unbegrenzte Anzahl von Elementen, denn die Knoten-Objekte sind nicht linear im Speicher abgelegt, sondern können irgendwo verteilt gespeichert sein, solange sie einander referenzieren.

Allerdings werden im obigen Beispiel auch die Nachteile von verketteten Listen deutlich: Einzelne Elemente lassen sich nur ĂŒber die Referenz vom VorgĂ€nger-Knoten ansprechen. Soll also ein Knoten mit einem ganz bestimmten Wert (oder etwa der n-te Knoten der Liste) gefunden werden, muss linear ĂŒber die Liste iteriert werden, bis der gesuchte Knoten gefunden ist. Eine sehr teuere Operation!

Zweifach verkettete Listen

Ausgehend von einfach verketteten Listen (siehe oben!) lassen sich zweifach verkettete Listen als eine konzeptuelle Erweiterung beschreiben, bei der jeder Knoten nicht nur den Folgeknoten, sondern auch den VorgÀnger-Knoten referenziert. Somit kann (von jedem Knoten aus) die Liste in beide Richtungen durchlaufen werden:

Zweifach verkettete Liste
Beispiel: Zweifach verkettete Liste mit Integer-Werten

Eine entsprechende Klasse sÀhe (wieder vereinfacht ohne private Klassenattribute!) etwa so aus:

public class Node {

    public int value;
    public Node previous;
    public Node next;

    // Konstruktor fĂŒr Wert, etc. ...
}

Dadurch lassen sich bestimmte Operationen einfacher ausfĂŒhren - etwa das Entfernen eines Knotens mit einem bestimmten Wert, denn der zu entfernende Knoten referenziert selbst die beiden Nachbarknoten, deren Referenzen (auf den zu löschenden Knoten) geĂ€ndert werden mĂŒssen! Lesen Sie diesen Artikel, um mehr ĂŒber die doppelt verknĂŒpfte Liste zu erfahren.

BĂ€ume

Ein Baum ist (u.a.) eine hierarchische Datenstruktur, die (Ă€hnlich wie die verkettete Liste) Daten in Knoten speichert. Ein Knoten speichert außerdem Verweise auf die Knoten, die in der Baumstruktur direkt unter ihm liegen. Diese Verweise werden auch Kanten genannt. Ein Baum besitzt außerdem eine Wurzel (oder: Wurzelknoten), die ganz oben in der Hierarche steht.

Baum

Quelle: commons.wikimedia.org; Matthias Kleine / CC BY-SA

Es existieren viele Arten von spezialisierten Baumstrukturen - an dieser Stelle wollen wir aber nur auf die binĂ€ren SuchbĂ€ume hinaus, da sich mit ihnen sehr gut weitere interessante Konzepte veranschaulichen und ĂŒben lassen (👉 Rekursion).

BinÀre SuchbÀume

Ein BinÀrbaum ist ganz einfach ein Baum, dessen Wurzel und Knoten maximal zwei Kind-Knoten (also zwei Referenzen auf darunterliegende Knoten / Nachkommen) besitzen.

Eine spezialform von BinĂ€rbĂ€umen sind binĂ€re SuchbĂ€ume. Diese werden bereits sortiert befĂŒllt, d.h. es gibt eine Regel zum EinfĂŒgen von neuen Knoten, die jedem neu einzufĂŒgenden Knoten genau seinen Platz zuweisen. Nach dieser Regel ist die Baumstruktur anschließend sehr effizient durchsuchbar (👉 divide and conquer; Rekursion). Diese Regel besagt, dass der linke Nachkomme (linke Referenz auf Kind-Knoten) einen kleineren oder gleichen Wert enthalten muss und jeder rechte Nachkomme (rechte Referenz
) einen grĂ¶ĂŸeren oder gleichen Wert enthalten muss:

BinÀrer Suchbaum

Quelle: commons.wikimedia.org; Mhombach / CC BY-SA

Wenn ein BinĂ€rbaum nach dieser Regel aufgebaut ist, dann halndelt es sich um einen binĂ€ren Suchbaum. Dieser Aufbau ermöglich nun eine sehr schnelle Suche nach einem bestimmten Knoten, da nach dem “divide and conquer“-Prinzip nur ein sehr kleiner Teil des Baumes durchsucht werden muss. In der Grafik oben ist etwa der Pfad zum Knoten (bzw. Blatt) mit dem Wert 5 hervorgehoben. Der Ablauf dieser Suche ist bei jedem Knoten mit dem Wert n (egal welchen Wert er trĂ€gt) gleich:

  1. Ist n der gesuchte Wert?
    • Wenn ja: FERTIG!.
    • Wenn nein: Weiter zu Schritt 2.
  2. Ist der gesuchte Wert grĂ¶ĂŸer als n?
    • Wenn ja: Weiter zum linken Teilbaum mit Schritt 1.
    • Wenn nein: Weiter zum rechten Teilbaum mit Schritt 1.
  3. Der gesuchte Wert ist nicht im Baum enthalten 😒

Dieser Vorgang eignet sich natĂŒrlich hervorragend fĂŒr eine rekursive Implementierung!