Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

Ähnliche Präsentationen


Präsentation zum Thema: ""—  Präsentation transkript:

18 3.3 Verkettete Listen Erweiterung der (nicht-leeren!) Liste:
tmp = head; while (tmp.next != null) tmp = tmp.next; tmp.next = new ListNode(4); tmp 4 null tmp tmp tmp head null 1 2 3 Will man schließlich ein neues Element an das Ende der Liste anhängen, so muß man zunächst das Ende der Liste aufsuchen: …

19 3.3 Verkettete Listen Entfernen des letzten Elementes aus der (aus mehr als 1 Element bestehenden!) Liste: ListNode<Integer> pred = null; tmp = head; while (tmp.next != null) { pred = tmp; tmp = tmp.next; } pred.next = null; tmp pred null pred pred pred pred null tmp tmp tmp tmp head null 1 2 3 4 null Eine etwas 'knifflige' Operation bei dieser Repräsentation als einfach vorwärts-verkettete Liste ist das Entfernen des letzten Elements: …

20 3.3 Verkettete Listen Rekursive Traversierung einer Liste vom Anfang zum Ende und wieder zurück: void traverse(ListNode<E> p) { ListNode<Integer> Node; if (Node = p; p != null; Node = Node.next) { process(Node.data); } } initialer Aufruf: traverse(head); Will man die Liste vom ersten bis zum letzten Knoten und wieder zurück zum ersten Knoten traversieren, so kann man dies durch folgende rekursive Methode realisieren: …

21 3.3 Verkettete Listen old null present
Iterative Traversierung der Liste vom Anfang zum Ende und wieder zurück: ListNode<E> head, tmp; … ListNode<E> old, present; present = head; for (int i = 1; i <= 2; i++) { old = null; while (present != null) { process(present.data); tmp = present.next; present.next = old; old = present; present = tmp; } present = old; } head null null present old old present present old present old present old old present present old present old old present present old old present Will man aber nicht den zusätzlichen Speicherplatz investieren, der für die rekursiven Aufrufe notwendig ist, so kann man diese Traversierung auch folgendermaßen iterativ implementieren: … Am besten kann man sich die beiden Zeiger present und old als die Zinken einer Gabel vorstellen, die über die Knoten bewegt wird und die Verbindung zwischen einem Knoten und seinem Vorgänger sicherstellt. Unschön ist hierbei vom Programmcode her die for-Schleife, die zweimal durchlaufen wird. Diese for-Schleife läßt sich vermeiden, wenn man den folgenden Trick benutzt. Bewirkt eine Listenumkehr

22 3.3 Verkettete Listen Programmcode für iterative Traversierung der Liste vom Anfang zum Ende und wieder zurück wird eleganter, wenn der letzte Knoten nicht auf null, sondern auf sich selbst verweist: head ListNode<E> head, tmp; … ListNode<E> old, present; present = head; old = null; while (present != null) { process(present.data); tmp = present.next; present.next = old; old = present; present = tmp; } Der letzte Knoten der Liste verweist nicht auf null, sondern auf sich selbst. Dann sieht der Programmcode für die iterative Traversierung folgendermaßen aus: …

23 3.3 Verkettete Listen Erweiterungen: Zirkuläre Listen: entry e1 e2 ...
Eine lineare Liste ist manchmal handlicher zu benutzen, wenn der Schwanz auf den Kopf der Liste zurückverweist, wodurch eine zirkuläre Liste entsteht. In einer zirkulären Liste werden der Anfangsknoten und der Endknoten durch einen einzigen Eingangsknoten ersetzt, und jeder Knoten kann von jedem anderen Knoten aus erreicht werden, ohne daß man mit dem externen Zeiger entry starten muß: … In einer doppelt-verketteten Liste enthält jeder Knoten zwei Zeiger, einen auf den Nachfolgerknoten und einen auf den Vorgängerknoten. Die Liste kann in beiden Richtungen traversiert werden: …

24 3.3 Verkettete Listen Beispiel: Josephus-Funktion
N Personen (rechts N=9) wählen einen Anführer, indem sie sich in einem Kreis aufstellen und jede M-te Person (M=5) aus dem Kreis entlassen. Nach dem Weggang einer Person wird der Kreis wieder geschlossen. Die letzte Person wird der Anführer (ein mathematisch gebildeter Möchtegern-Anführer ermittelt im Voraus, welche Position er im Kreis einzunehmen hat). Josephus-Funktion J(N, M): Die Identität des gewählten Anführers. J(9,5) = 8.

25 3.3 Verkettete Listen Doppelt-verkettete Listen: entry null e1 e2 … ei
Doppelt-verkettete zirkuläre Listen: e1 e2 en ei ei+1 entry Eine lineare Liste ist manchmal handlicher zu benutzen, wenn der Schwanz auf den Kopf der Liste zurückverweist, wodurch eine zirkuläre Liste entsteht. In einer zirkulären Liste werden der Anfangsknoten und der Endknoten durch einen einzigen Eingangsknoten ersetzt, und jeder Knoten kann von jedem anderen Knoten aus erreicht werden, ohne daß man mit dem externen Zeiger entry starten muß: … In einer doppelt-verketteten Liste enthält jeder Knoten zwei Zeiger, einen auf den Nachfolgerknoten und einen auf den Vorgängerknoten. Die Liste kann in beiden Richtungen traversiert werden: …

26 3.3 Verkettete Listen Mehrfach verkettete Listen:
Knoten haben mehrere Verbindungsfelder und gehören zu unabhängig verwalteten verketteten Listen. Bsp. a, b: Start einer Liste. Doppelt-verkettete Listen: Spezialfall von mehrfach verketteten Listen

27 3.3 Verkettete Listen Realisierung verketteter Listen:
i.d.R. Referenzen auf Objekte Arrays von Ganzzahlen, um verkettete Listen, z.B. für Josephus-Spiel (rechts), zu implementieren. Wir realisieren Verbindungen mit Arrayindizes (Warum und wann ist dies möglich?) Die Abstraktionsmöglichkeit von Java hilft, solche Details zu verbergen

28 3.4 Arrays vs. Verkettete Listen
Listen: Lokale Veränderungen können mit einem Aufwand durchgeführt werden, der unabhängig von der Größe der Liste ist - vorausgesetzt wir kennen die Speicherpositionen der involvierten Elemente Verkettete Listen: Ein aus einer Liste gelöschtes Element hinterlässt keine Lücke, die wie beim Array gefüllt werden müsste. Stattdessen hinterlässt es freien Speicherplatz  Speicherplatzverwalter (garbage collector) sammelt den nicht mehr benötigten Speicherplatz wieder ein. Das Spektrum der Datenstrukturen reicht von statischen Objekten, wie einer Tabelle von Konstanten, zu dynamischen Strukturen wie Listen. Listen erlauben nicht nur, die gespeicherten Datenwerte zu ändern, sondern auch die Größe und Struktur der Liste können sich zur Laufzeit auf Grund von Einfüge- und Löschoperationen oder Reorganisationen verändern. Die meisten der bisher diskutierten Datenstrukturen können ihre Größe und Struktur nur eingeschränkt verändern. Ein zirkulärer Puffer z.B. unterstützt Einfügen an einem und Löschen am anderen Ende und kann bis zu einer vorgegebenen maximalen Größe wachsen. Ein Heap unterstützt Löschen an einem Ende des Arrays, und Einfügen irgendwo im Array. In einer Liste kann jede lokale Veränderung mit einem Aufwand durchgeführt werden, der unabhängig von der Größe der Liste ist - vorausgesetzt wir kennen die Speicherpositionen der involvierten Elemente. Die Schlüsselidee, um diese Forderung zu erfüllen, besteht darin, benötigten Speicherplatz nicht mehr wie beim Array in einem großen zusammenhängenden Teil zu allozieren, sondern den benötigten Speicherplatz dynamisch in kleinen Fragmenten anzulegen, die ein gegebenes Objekt speichern können. Da dann die Datenelemente mehr oder weniger zufallsmäßig verstreut und nicht zusammenhängend im Speicher abgelegt werden, erzeugt eine Einfüge- oder Löschoperation keinen Dominoeffekt, durch den andere Datenelemente herumgeschoben werden. Ein Element wird irgendwo im Speicher abgelegt und mit anderen Elementen durch sogenannte Zeiger oder Referenzen (d.h. Speicheradressen, an denen die Elemente gegenwärtig abgelegt sind) verknüpft.

29 3.4 Arrays vs. Verkettete Listen
Primzahlensieb von Eratosthenes: Array vorteilhaft, weil die Effizienz des Algorithmus darauf beruft, schnell auf eine beliebige Arrayposition zugreifen zu können Josephus-Problem: Verkettete Liste vorteilhaft, weil die Effizienz des Algorithmus darauf beruft, schnell Elemente entfernen zu können. Achtung: Verkettete Listen könnten durchaus intern mit Arrays realisiert werden! Die Wechselwirkung zwischen Datenstrukturen und Algorithmen bildet das Herz des Entwurfsprozesses und Kern der Vorlesung! Das Spektrum der Datenstrukturen reicht von statischen Objekten, wie einer Tabelle von Konstanten, zu dynamischen Strukturen wie Listen. Listen erlauben nicht nur, die gespeicherten Datenwerte zu ändern, sondern auch die Größe und Struktur der Liste können sich zur Laufzeit auf Grund von Einfüge- und Löschoperationen oder Reorganisationen verändern. Die meisten der bisher diskutierten Datenstrukturen können ihre Größe und Struktur nur eingeschränkt verändern. Ein zirkulärer Puffer z.B. unterstützt Einfügen an einem und Löschen am anderen Ende und kann bis zu einer vorgegebenen maximalen Größe wachsen. Ein Heap unterstützt Löschen an einem Ende des Arrays, und Einfügen irgendwo im Array. In einer Liste kann jede lokale Veränderung mit einem Aufwand durchgeführt werden, der unabhängig von der Größe der Liste ist - vorausgesetzt wir kennen die Speicherpositionen der involvierten Elemente. Die Schlüsselidee, um diese Forderung zu erfüllen, besteht darin, benötigten Speicherplatz nicht mehr wie beim Array in einem großen zusammenhängenden Teil zu allozieren, sondern den benötigten Speicherplatz dynamisch in kleinen Fragmenten anzulegen, die ein gegebenes Objekt speichern können. Da dann die Datenelemente mehr oder weniger zufallsmäßig verstreut und nicht zusammenhängend im Speicher abgelegt werden, erzeugt eine Einfüge- oder Löschoperation keinen Dominoeffekt, durch den andere Datenelemente herumgeschoben werden. Ein Element wird irgendwo im Speicher abgelegt und mit anderen Elementen durch sogenannte Zeiger oder Referenzen (d.h. Speicheradressen, an denen die Elemente gegenwärtig abgelegt sind) verknüpft.

30 3.5 Zusammengesetzte Datenstrukturen
Arrays und verkettete Listen liefern eine erste Stufe der Abstraktion, auf der komplexere Datenstrukturen definiert werden können Mehrdimensionale Arrays: Deklaration: Typ[][] Arrayname // 2-dimensional Typ[][][] Arrayname // 3-dimensional alternativ: Typ Arrayname[][] Initialisierung: Arrayname = new Typ[Size1][Size2]; oder (für nicht notwendigerweise rechteckige Arrays): Arrayname = new Typ[Size][]; for (int i = 0; i < Size; i++) Arrayname[i] = new Typ[Size_i];

31 3.5 Zusammengesetzte Datenstrukturen
Beispiel: Deklaration: int[][] table; Erzeugung: table = new int[3][2]; oder table = new int[3][]; table[1] = new int[2];

32 3.5 Zusammengesetzte Datenstrukturen
Beispiel: Effiziente Speicherung von nicht-rechteckigen Strukturen int pascal [][] = { { 1 }, { 1, 1 }, { 1, 2, 1 }, { 1, 3, 3, 1 }, { 1, 4, 6, 4, 1 } } ; 1 1 1 2 1 3 1 4 6 Pascal[4][1]

33 3.5 Zusammengesetzte Datenstrukturen
Beispiel: Darstellung von Graphen mit Adjazenzmatrix Einfachheitshalber ordnen wir den Knoten Indizes zu. Adjazenzmatrix = 2D Matrix: Zeile i und Spalte j hat den Wert true, wenn es eine Kante von Knoten i zu Knoten j gibt. Das Array ist für ungerichtete Graphen symmetrisch. Per Konvention weisen wir den Elementen in der Diagonalen true-Werte zu. Boolean adj[][] = new boolean[V][V];

34 3.5 Zusammengesetzte Datenstrukturen
Dünn besetzte Adjazenzmatrix: Ein Graph mit wenigen Kanten kann auch mit einem Array von Adjazenzlisten darstellt werden. Der Speicherbedarf ist proportional zur Anzahl der Knoten plus Anzahl der Kanten.

35 Zusammenfassung 3.1 Grundlegendes 3.2 Arrays (Felder)
3.3 Verkettete Listen 3.4 Arrays vs. Verkettete Listen 3.5 Zusammengesetzte Datenstrukturen


Herunterladen ppt ""

Ähnliche Präsentationen


Google-Anzeigen