Algorithmen und Datenstrukturen: Unterschied zwischen den Versionen

Aus CCWiki
Zur Navigation springen Zur Suche springen
 
(39 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
Im folgenden Themenbereich geht es um Algorithmen und Datenstrukturen und um die Landau Symbole oder O - Notation und wie diese im Kontext von Algorithmen und Datenstrukturen verwendet werden.
Im folgenden Themenbereich geht es um '''Algorithmen und Datenstrukturen''' und um die '''Landau Symbole''' oder '''O - Notation''' und wie diese im Kontext von '''Algorithmen und Datenstrukturen''' verwendet werden.
<br>
<br>
Bei den Alghorithmen werden wir uns auf Sortieralgorithmen beschränken, und uns mit deren Laufzeitanalyse (und der daraus resultierennden O - Notation) befassen.
Bei den '''Alghorithmen''' werden wir uns auf '''Sortieralgorithmen''' beschränken und uns mit deren '''Laufzeitanalyse''' (und der daraus resultierenden O - Notation) befassen.
<br>
<br>
Bei den Datenstrukturen beschränken wir uns auf Arrays, Listen und Bäume und ebenfalls um deren Laufzeitanalyse für verschiedene Operationen.
Bei den Datenstrukturen beschränken wir uns auf {{Link|Array|Arrays}}, {{Link|Liste|Listen}} und {{Link|Bäume}} und ebenfalls um deren '''Laufzeitanalyse''' für verschiedene Operationen.
{{TOC limit|3}}
{{TOC limit|3}}
= Landau Symbole oder O - Notation =
= Landau Symbole oder O - Notation =


'''Landau-Symbole''' (auch '''O-Notation''') werden in der Mathematik und in der Informatik verwendet, um das asymptotische Verhalten von Funktionen und Folgen zu beschreiben. In der Informatik werden sie bei der Analyse von Algorithmen verwendet und geben ein Maß für die Anzahl der Elementarschritte oder der Speichereinheiten in Abhängigkeit von der Größe des gegebenen Problems an. (siehe [[https://de.wikipedia.org/wiki/Landau-Symbole]]).
<cite>'''Landau-Symbole''' (auch '''O-Notation''') werden in der Mathematik und in der Informatik verwendet, um das asymptotische Verhalten von Funktionen und Folgen zu beschreiben. In der Informatik werden sie bei der Analyse von Algorithmen verwendet und geben ein Maß für die Anzahl der Elementarschritte oder der Speichereinheiten in Abhängigkeit von der Größe des gegebenen Problems an <ref>https://de.wikipedia.org/wiki/Landau-Symbole</ref>.</cite>
  Unter asymptotischem Verhalten verstehen wir hier das Grenzverhalten eines Algorithmus, vereinfacht gesagt, wie kann sich die Laufzeit im schlechtesten Fall entwickeln. Um das Grenzverhalten eines Algorithmus zu betrachten, analysieren wir wieviele Operationen im schlechtesten Fall in bezug auf die größe des Problems (z.B.: Anzahl der Elemente) benötigt werden. Konstante Werte können dabei vernachlässigt werden (z.B.: eine Schleife muss 2 mal durchlaufen werden, dieser Wert ist eine Konstante da unabhängig von der Anzahl der Elemente).
  Unter asymptotischem Verhalten verstehen wir hier das Grenzverhalten eines Algorithmus, vereinfacht gesagt, wie kann sich die Laufzeit im schlechtesten Fall entwickeln. Um das Grenzverhalten eines Algorithmus zu betrachten, analysieren wir wieviele Operationen im schlechtesten Fall in Bezug auf die größe des Problems (z.B.: Anzahl der Elemente) benötigt wird. Konstante Werte können dabei vernachlässigt werden (z.B.: eine Schleife muss '''2''' mal durchlaufen werden. Dieser Wert ist eine Konstante da sie unabhängig von der Anzahl der Elemente ist, es wird also immer '''2''' mal durchlaufen).


== Beispiel ==
== Beispiel ==
Suchen des kleinsten und größten Elements in einem Array.
Suchen des kleinsten und größten Elements in einem {{Link|Array}}.
{| class="wikitable"
{| class="wikitable"
|-
|-
| 1 || 2 || 3 || 4 || 5 || 6
| 1 || 2 || 3 || 4 || 5 || 6
|}
|}
Um das größte Element zu finden, werden 5 Schritte benötigt, dies entspricht der Länge des Arrays - 1. Für die Suche des größten Elements benötigen wir ebenfalls 5 Schritte. Somit ist die Laufzeit für unseren Algorithmus '''2 * (n - 1)''', die Konstanten werden entfernt: '''O(n)'''
Um das größte Element zu finden, werden 5 Schritte benötigt, dies entspricht der Länge des {{Link|Array|Arrays}} - 1. Für die Suche des größten Elements benötigen wir ebenfalls 5 Schritte. Somit ist die Laufzeit des Algorithmus '''2 * (n - 1)'''. Die Konstanten werden entfernt, somit bleibt: '''O(n)'''
<syntaxhighlight lang='java' line="'line'">
{{JML|code=
public void findMinAndMax(int[] arr) {
public void findMinAndMax(int[] arr) {
   int min = arr[0];
   int min = arr[0];
Zeile 30: Zeile 30:
   System.out.println("Max: "+max);
   System.out.println("Max: "+max);
}
}
</syntaxhighlight>
}}
In der O - Notation werden einige gebräuchliche Schranken verwendet, wenn wir einen Algorithmus oder eine Datenstruktur analysieren, verwenden wir diese Schranken und nicht die exakte Mathematische repräsentation des Problems, auch wenn dies möglich wäre (siehe [[Algorithmen_und_Datenstrukturen#Bubblesort|Bubblesort]])
In der '''O - Notation''' werden einige gebräuchliche Schranken verwendet, wenn wir einen Algorithmus oder eine Datenstruktur analysieren, verwenden wir diese Schranken und nicht die exakte Mathematische repräsentation des Problems, auch wenn dies möglich wäre (siehe [[Algorithmen_und_Datenstrukturen#Bubblesort|Bubblesort]])


{| class="wikitable"
{| class="wikitable"
Zeile 38: Zeile 38:
! O - Notation !! Bedeutung !! Beispiel
! O - Notation !! Bedeutung !! Beispiel
|-
|-
| O(1) || Das Laufzeitverhalten hängt nicht von der Anzahl der Elemente ab und ist konstant || Element an der Stelle N aus einem Array oder einer ArrayListe holen
| O(1) || Das Laufzeitverhalten hängt nicht von der Anzahl der Elemente ab und ist konstant || Element an der Stelle N aus einem {{Link|Array}} oder einer {{Link|ArrayList|ArrayListe}} holen
|-
|-
| O(n) || Das Laufzeitverhalten verlängert sich linear mit der Anzahl der Element || Suchen eines bestimmten Elements in einem unsortierten Array  
| O(n) || Das Laufzeitverhalten verlängert sich linear mit der Anzahl der Element || Suchen eines bestimmten Elements in einem unsortierten {{Link|Array}}
|-
|-
| O(n log n) || Super lineares Wachstum :-DDD || Mergesort, Heapsort, durchschnittsfall Quicksort
| O(n log n) || Super lineares Wachstum :-DDD || Mergesort, Heapsort, Durchschnittsfall Quicksort
|-
|-
| O(n<sup>2</sup>) || Quadratisches Wachstum, die Laufzeit wächst quadratisch zur Anzahl der Elemente || Bubblesort
| O(n<sup>2</sup>) || Quadratisches Wachstum, die Laufzeit wächst quadratisch zur Anzahl der Elemente || {{Link|Bubblesort}}
|-
|-
| O(log<sub>2</sub>n) || 2er Logarithmisches Wachstum || Suche im balancierten Binärbaum
| O(log<sub>2</sub>n) || 2er Logarithmisches Wachstum || Suche im balancierten {{Link|Bäume|Binärbaum}}
|}
|}


= Datenstrukturen =
= Datenstrukturen =


In der Informatik und Softwaretechnik ist eine Datenstruktur ein Objekt, welches zur Speicherung und Organisation von Daten dient. Es handelt sich um eine Struktur, weil die Daten in einer bestimmten Art und Weise angeordnet und verknüpft werden, um den Zugriff auf sie und ihre Verwaltung effizient zu ermöglichen. (siehe [[https://de.wikipedia.org/wiki/Datenstruktur]])
<cite>In der Informatik und Softwaretechnik ist eine '''Datenstruktur''' ein Objekt, welches zur Speicherung und Organisation von Daten dient. Es handelt sich um eine Struktur, weil die Daten in einer bestimmten Art und Weise angeordnet und verknüpft werden, um den Zugriff auf sie und ihre Verwaltung effizient zu ermöglichen <ref>https://de.wikipedia.org/wiki/Datenstruktur</ref>.</cite>
<br><br>
Im folgenden sollen einige Datenstrukturen erläutert werden, für jede Datenstruktur gibt es andere Einsatzgebiete die für deren Eigenschaften optimal sind.
Im folgenden sollen einige Datenstrukturen erläutert werden, für jede Datenstruktur gibt es andere Einsatzgebiete die für deren Eigenschaften optimal sind.


== Array ==
== Array ==
Ein Array oder Feld ist eine sehr effiziente doch wenig dynamische Datenstruktur. In nahezu allen Programmiersprachen findet sich diese Datenstruktur wieder. Weiter ist auch ein dynamisches vergrößern oder verkleinern in einigen Programmiersprachen automatisch implementiert ('''Javascript''').
Ein '''Array''' oder '''Feld''' ist eine sehr effiziente doch wenig dynamische '''Datenstruktur'''. In nahezu allen Programmiersprachen findet sich diese '''Datenstruktur''' wieder. Weiter ist auch ein dynamisches vergrößern oder verkleinern in einigen Programmiersprachen automatisch implementiert (z.B.: [[Webtechnologien#Javascript|Javascript]]).
<br>
<br>
Gehen wir jedoch von Programmiersprachen wie '''Java''' oder '''C''' aus, so hat ein Array immer eine fixe größe. Das heißt, zur Laufzeit (nach dem das Programm compiliert und gestartet wurde) kann diese nicht verändert werden.
Gehen wir jedoch von Programmiersprachen wie '''Java''' oder '''C''' aus, so hat ein '''Array''' immer eine '''fixe Größe'''. Das heißt, zur Laufzeit (nach dem das Programm compiliert und gestartet wurde) kann diese nicht verändert werden.
=== Vorteile ===
=== Vorteile ===
* '''Der Speicheroverhead ist sehr gering''' - Es wird nur der Speicher reserviert, der für die Anzahl der Elemente benötigt wird.
* '''Der Speicheroverhead ist sehr gering''' - Es wird nur der Speicher benötigt, der für die Anzahl der Elemente benötigt wird.
* '''Geringe Zugriffszeit''' - Wird ein Element an einer stelle angesprochen, so kann für den Zugriff die Exakte Speicheradresse berechnet werden.
* '''Geringe Zugriffszeit''' - Wird ein Element an einer Stelle angesprochen, so kann für den Zugriff die '''exakte Speicheradresse berechnet werden'''.


=== Nachteile ===
=== Nachteile ===
* '''Wenig flexibel''' - Wie bereits erwähnt, muss die größe meist fix definiert werden. Eine vergrößerung/verkleinerung ist nicht möglich, ermöglicht die Programmiersprache dies, so hat dies meist zurfolge, dass das Array neu erstellt wird, und die Elemente kopiert werden müssen.
* '''Wenig flexibel''' - Wie bereits erwähnt, muss die Größe meist fix definiert werden. Eine Vergrößerung/Verkleinerung ist nicht möglich, ermöglicht die Programmiersprache dies, so hat dies zur Folge, dass das Array neu erstellt wird, und die Elemente kopiert werden müssen.


=== Einsatzgebiete ===
=== Einsatzgebiete ===
Überall dort, wo eine fixe Speichergröße benötigt und effizient wichtig ist, dort sollte die Verwendung von Arrays in betracht gezogen werden.
Überall dort, wo eine fixe Speichergröße benötigt und Effizienz wichtig ist, dort sollte die Verwendung von Arrays in Betracht gezogen werden.
* '''Bildmanipulation''' - Wird ein Bild zur Bearbeitung geladen, so kann die ''Bitmap'' in einem Array gespeichert werden. jedes Element im Array entspricht dann einem Pixel.
* '''Bildmanipulation''' - Wird ein Bild zur Bearbeitung geladen, so kann die ''Bitmap'' in einem '''Array''' gespeichert werden. jedes Element im '''Array''' entspricht dann einem Pixel.
* '''Puffern von Daten''' - Beim Lesen und Schreiben werden Daten meist in einem Array zwischengespeichert (gepuffert). Danach kann die Bearbeitung erfolgen. Weiters kann bei jedem Lese oder Schreibvorgang erneut das selbe Array verwendet werden, was die Effizienz weiter erhöht.
* '''Puffern von Daten''' - Beim Lesen und Schreiben werden Daten meist in einem '''Array''' zwischengespeichert (gepuffert). Danach kann die Bearbeitung erfolgen. Weiters kann bei jedem Lese oder Schreibvorgang erneut das selbe Array verwendet werden, was die Effizienz weiter erhöht. Für solche Operationen wird meist eine fixe Puffergröße verwendet, deswegen eignet sich das '''Array''' hierfür.
* '''Jegliches Schachbrettartige Problem mit fixer Größe''' - Schiffe versenken, Schach,...
* '''Jegliches schachbrettartige Problem mit fixer Größe''' - Schiffe versenken, Schach,...


== Liste ==
== Liste ==
Die Liste ist eine sehr gebräuchliche Datenstruktur. Sie ist gekennzeichnet durch, je nach implementierung, gute Zugriffszeiten oder erhöhte Flexibilität.
Die '''Liste''' ist eine sehr gebräuchliche '''Datenstruktur'''. Sie ist gekennzeichnet durch, je nach implementierung, gute Zugriffszeiten oder erhöhte Flexibilität.
<br>
<br>
Beziehen wir uns auf die Programmiersprache Java, so ist die Liste ein '''Interface''', das heißt eine Schnittstelle. Diese Schnittstelle definiert einige '''Methoden''' die eine Implementierung erfüllen muss. Einige davon wären:
Beziehen wir uns auf die Programmiersprache '''Java''', so ist die {{AL|Klassen|Klasse}} {{JSL|Liste}} ein {{AL|Interface}}, also eine {{AL|Interface|Schnittstelle}}. Diese {{AL|Interface|Schnittstelle}} definiert einige {{AL|Methoden}} die eine Implementierung erfüllen muss. Einige davon wären:
* <syntaxhighlight lang="java" inline>E get(int index)</syntaxhighlight> - Gibt das Element an der Stelle '''index''' aus
* {{JSL|E get(int index)}} - Gibt das Element an der Stelle '''index''' aus
* <syntaxhighlight lang="java" inline>E remove(int index)</syntaxhighlight> - Gibt das Element an der Stelle '''index''' aus
* {{JSL|E remove(int index)}} - Löscht das Element an der Stelle '''index'''
* <syntaxhighlight lang="java" inline>boolean add(E element)</syntaxhighlight> - Fügt ein Element an das Ende der Liste an
* {{JSL|boolean add(E element)}} - Fügt ein Element an das Ende der Liste hinzu
* <syntaxhighlight lang="java" inline>boolean add(int index, E element)</syntaxhighlight> - Fügt ein Element an einer bestimmten Position ein, und schiebt nachfolgende Elemente nach rechts (Index wird erhöht)
* {{JSL|boolean add(int index, E element)}} - Fügt ein Element an einer bestimmten Position ein, und schiebt nachfolgende Elemente nach rechts
* ...
* ...
Hier ist nun ersichtlich, dass die Flexibilität der Liste ein essenzieller Bestandteil der Definition ist.
Hier ist nun ersichtlich, dass die '''Flexibilität''' der '''Liste''' ein essenzieller Bestandteil der Definition ist.
<br>
<br>
Anbei einige Implementierungen des Interfaces List.
Anbei einige Implementierungen des {{AL|Interface|Interfaces}} {{JSL|List}}.
=== ArrayList ===
=== ArrayList ===
Die ArrayList vereint die Eigenschaften einer Liste und eines Arrays. Die interne Implementierung verwendet ein Array von fixer größe um Elemente zu speichern. Wird diese fixe größe überschritten, so wird intern ein neues Array angelegt, je nach implementierung mit einem gewissen Vergrößerungsfaktor. Nach dem neuanlegen des internen Arrays, werden alle Elemente kopiert und in das neue Array eingefügt. Wird ein Element gelöscht oder an einer Stelle eingefügt, so müssen bestehende Elemente verschoben werden.
Die '''ArrayList''' vereint die Eigenschaften einer '''Liste''' und eines {{AL|Array|Arrays}}. Die interne Implementierung verwendet ein {{AL|Array}} von fixer Größe um Elemente zu speichern. Wird diese fixe Größe überschritten, so wird intern ein neues {{AL|Array}} angelegt, je nach Implementierung mit einem gewissen Vergrößerungsfaktor. Nach dem Neuanlegen des internen {{AL|Array|Arrays}}, werden alle Elemente kopiert und in das neue {{AL|Array}} eingefügt. Wird ein Element gelöscht oder an einer Stelle eingefügt, so müssen bestehende Elemente verschoben werden.
[[Datei:Removing Element from ArrayList diagram.png|mini|none|400px|Element wird aus ArrayListe entfernt. Quelle: https://beginnersbook.com/2013/12/java-arraylist/]]
[[Datei:Removing Element from ArrayList diagram.png|mini|none|400px|Element wird aus ArrayListe entfernt. <ref>https://beginnersbook.com/2013/12/java-arraylist/</ref>]]
In Java wird die größe des internen Arrays nicht automatisch reduziert, wenn Elemente gelöscht werden. Dafür gibt es die Methode <syntaxhighlight lang="java" inline>myArrayList.trim()</syntaxhighlight>
In Java wird die größe des internen Arrays nicht automatisch reduziert, wenn Elemente gelöscht werden. Dafür gibt es die Methode {{JSL|myArrayList.trim()}}


==== Vorteile ====
==== Vorteile ====
* '''Relativ geringer Speicheroverhead''' - Dies verhält sich ähnlich wie beim Array. Jedoch können in der ArrayList keine primitiven Datentypen gespeichert werden. Dies bedeutet, es wird nur ein Array von Referenzen erstellt. In jedem Platz findet sich eine Referenz auf das wirkliche Objekt. Dies bedeutet in Bezug auf primitive Datentypen einen weit höheren Speicherbedarf als bei einem Array.
* '''Relativ geringer Speicheroverhead''' - Dies verhält sich ähnlich wie beim {{AL|Array}}. Jedoch können in der '''ArrayList''' keine {{AL|Primitive_Datentypen|primitiven Datentypen}} gespeichert werden, sondern nur '''Objekte''' von {{AL|Klassen}}. Dies bedeutet, es wird nur ein {{AL|Array}} von '''Referenzen''' erstellt. In jedem Platz findet sich eine '''Referenz''' auf das wirkliche '''Objekt'''. Dies bedeutet in Bezug auf {{AL|Primitive_Datentypen|primitive Datentypen}} einen weit höheren Speicherbedarf als bei einem Array.
* '''Geringe Zugriffszeit''' - Wird ein Element an einer stelle angesprochen, so kann für den Zugriff die Exakte Speicheradresse berechnet werden.
* '''Geringe Zugriffszeit''' - Wird ein Element an einer Stelle angesprochen, so kann für den Zugriff die exakte Speicheradresse berechnet werden.


==== Nachteile ====
==== Nachteile ====
* '''Ineffizientes Löschen''' - Laufzeit beim Löschen nicht optimal, Elemente müssen verschoben werden.
* '''Ineffizientes Löschen''' - Laufzeit beim Löschen nicht optimal, Elemente müssen verschoben werden.
* '''Ineffizientes Einfügen''' - Laufzeit beim Einfügen von Elementen an einer gewissen Stelle langsam, wie beim löschen müssen alle Nachfolgenden Elemente verschoben werden.
* '''Ineffizientes Einfügen''' - Laufzeit beim Einfügen von Elementen an einer gewissen Stelle langsam, wie beim löschen müssen alle nachfolgenden Elemente verschoben werden.
* '''Ineffizientes Hinzufügen, wenn internes Array voll''' - Laufzeit beim Hinzufügen von Elementen bei vollem Array langsam, ein neues Array muss erstellt werden und alle Elemente
* '''Ineffizientes Hinzufügen, wenn internes {{AL|Array}} voll''' - Laufzeit beim Hinzufügen von Elementen bei vollem {{AL|Array}} langsam, ein neues {{AL|Array}} muss erstellt und alle Elemente müssen in das neue {{AL|Array}} kopiert werden


==== Code beispiel ====
==== Code beispiel ====
<syntaxhighlight lang='Java' line="'line'>
{{JML|code=
List<Integer> myArrayList = new ArrayList<>();
List<Integer> myArrayList = new ArrayList<>();
myArrayList.add(1);
myArrayList.add(1);
Zeile 106: Zeile 105:
myArrayList.remove(0);
myArrayList.remove(0);
System.out.println("Element an der Stelle 2: "+myArrayList.get(1));
System.out.println("Element an der Stelle 2: "+myArrayList.get(1));
</syntaxhighlight>
}}
* Zeile 1) Die die ArrayListe wird erzeugt und einer Variable des Typs List zugewiesen. Warum ist das möglich? Weil ArrayList das Interface List implementiert. D.h.: Eine ArrayList ist eine List.
* Zeile 1) Die {{JSL|ArrayList}} wird erzeugt und einer Variable des Typs {{JSL|List}} zugewiesen. Warum ist das möglich? Weil {{JSL|ArrayList}} das {{AL|Interface}} {{JSL|List}} implementiert. D.h.: Eine {{JSL|ArrayList}} ist eine {{JSL|List}}.
* Zeile 2) - 4) fügen dem Array einen int hinzu. Hier ist zu beachten, der primitive Datentyp '''int''' wird automatisch in die Klasse '''Integer''' umgewandelt. Dieser mechanismus nennt sich Autoboxing.
* Zeile 2) - 4) fügen der {{JSL|ArrayList}} einen '''int''' hinzu. Hier ist zu beachten, der {{AL|Primitive_Datentypen|primitive Datentyp}} '''int''' wird automatisch in die {{AL|Klasse}} {{JSL|java.lang.Integer}} umgewandelt. Dieser mechanismus nennt sich {{AL|Primitive_Datentypen|Autoboxing}}.
* Zeile 5) Das Element an der Stelle 0 wird aus der ArrayList entfernt
* Zeile 5) Das Element an der Stelle 0 wird aus der {{JSL|ArrayList}} entfernt
* Zeile 6) Ausgabe des Elements an der Stelle 2 (Index 1)
* Zeile 6) Ausgabe des Elements an der Stelle 2 (Index 1)


=== Einsatzgebiete ===
==== Einsatzgebiete ====
Die Einsatzgebiete überschneiden sich mit herkömmlichen Arrays. Benötigt man mehr flexibilität und soll '''nicht''' mit primitiven Datentypen gearbeitet werden, so kann anstatt eines Arrays die ArrayListe verwendet werden. Ist die Anzahl der Elemente start fluktuierend (oftmaliges löschen/einfügen/hinzufügen), so sollte man aus oben genannten Gründen auf eine andere Datenstruktur ausweichen.
Die Einsatzgebiete überschneiden sich mit herkömmlichen {{Link|Array|Arrays}}. Benötigt man mehr flexibilität und soll '''nicht''' mit {{AL|Primitive_Datentypen|primitiven Datentypen}} gearbeitet werden, so kann anstatt eines {{Link|Array|Arrays}} die '''ArrayListe''' verwendet werden. Ist die Anzahl der Elemente start fluktuierend (oftmaliges löschen/einfügen/hinzufügen), so sollte man aus oben genannten Gründen auf eine andere '''Datenstruktur''' ausweichen.


=== LinkedList oder verkettete Liste ===
=== LinkedList oder verkettete Liste ===
Die verkettete Liste ist eine sehr dynamische Datenstruktur. Ihre Größe ist flexibel. Die Daten werden in Knoten gehalten, welche im Falle der einfach verketteten Liste auf den Nachfolger und bei der doppelt verketteten Liste zusätzlich auf den Vorgänger Knoten zeigen. Elemente können einfach an bestimmten Stellen eingefügt oder gelöscht werden, es müssen lediglich die Nachfolger bzw. Vorgänger aktualisiert werden.
Die '''verkettete Liste''' ist eine sehr '''dynamische Datenstruktur'''. Ihre Größe ist flexibel. Die Daten werden in Knoten gehalten, welche im Falle der '''einfach verketteten Liste''' auf den Nachfolger und bei der '''doppelt verketteten Liste''' zusätzlich auf den Vorgänger Knoten zeigen. Elemente können einfach an bestimmten Stellen eingefügt oder gelöscht werden, es müssen lediglich die Nachfolger bzw. Vorgänger aktualisiert werden.
[[Datei:Singly linked list and doubly linked list in Java.jpg|mini|none|Einfach und doppelt verkettete Liste. Quelle: https://www.java67.com/2016/01/how-to-implement-singly-linked-list-in-java-using-generics-example.html]]
[[Datei:Singly linked list and doubly linked list in Java.jpg|mini|none|Einfach und doppelt verkettete Liste. <ref>https://www.java67.com/2016/01/how-to-implement-singly-linked-list-in-java-using-generics-example.html</ref>]]


==== Vorteile ====
==== Vorteile ====
Zeile 124: Zeile 123:


==== Nachteile ====
==== Nachteile ====
* '''Zufälliger Zugriff langsam''' - Werden in der LinkedList Elemente mit ihrem Index angesprochen, so muss zuerst die komplette Liste bis zum entsprechenden Index durchlaufen werden.
* '''Zufälliger Zugriff langsam''' - Werden in der '''LinkedList''' Elemente mit ihrem Index angesprochen, so muss zuerst die komplette Liste bis zum entsprechenden Index durchlaufen werden.
* '''Speicheroverhead''' - Da die Daten in Knoten gespeichert werden müssen entsteht automatisch ein Speicheroverhead.
* '''Speicheroverhead''' - Da die Daten in Knoten gespeichert werden müssen entsteht automatisch ein Speicheroverhead für die Datenstruktur des Knoten.


==== Einsatzgebiete ====
==== Einsatzgebiete ====
Zeile 131: Zeile 130:


==== Code Beispiel ====
==== Code Beispiel ====
''Hinzufügen eines Elements''
{{JML|code=
<syntaxhighlight lang='Java' line="'line'">
//Datenstruktur Node
public boolean add(Integer element) {
public class Node {
   Node newNode = new Node(element);
  private Integer value;
   if(root == null) {
   private Node next;
     root = newNode;
 
   } else {
   public Node(Integer value) {
     Node current = root;
     this.value = value;
    while(current.next() != null) {
   }
      current = current.next();
 
    }
  public void setNext(Node next) {
     current.setNext(newNode);
     this.next = next;
  }
 
  public Node next();
    return this.next;
  }
 
  public Integer getValue() {
     return this.value;
   }
   }
  return true;
}
}
</syntaxhighlight>


''Holen eines Elements''
//Die Implementierung der LinkedList
<syntaxhighlight lang='Java' line="'line'">
public class MyList {
public int get(int index) {
  //Referenz für erstes Listenelement
  Node current = root;
  private Node root;
  for(int i=0; i<index && current != null; i++) {
 
    current = current.next();
  //Hinzufügen eines Elements
  public boolean add(Integer element) {
    Node newNode = new Node(element);
    if(root == null) {
      root = newNode;
    } else {
      Node current = root;
      while(current.next() != null) {
        current = current.next();
      }
      current.setNext(newNode);
    }
    return true;
   }
   }
   if(current == null) {
 
    throw new IndexOutOfBoundsException();
   //Holen eines Elements
  public int get(int index) {
    Node current = root;
    for(int i=0; i<index && current != null; i++) {
      current = current.next();
    }
    if(current == null) {
      throw new IndexOutOfBoundsException();
    }
    return current.getValue();
   }
   }
  return current.getValue();
}
}
</syntaxhighlight>
}}


== Assoziativer Speicher ==
== Assoziativer Speicher ==
Der '''assoziative Speicher''' oder auch '''Map''', oder '''assoziatives Array''', ist eine Sammlung von '''Key''', '''Value''' Paaren. Sie eignet sich sehr gut dafür, für einen bestimmten Schlüssel einen Wert zu hinterlegen. Die Operationen, einfügen, löschen, abrufen, sind Konstant und weisen im Normalfall ein Laufzeitverhalten von '''O(1)''' auf. Die Reihenfolge der Element ist nicht definiert.
Der '''assoziative Speicher''' oder auch '''Map''', oder '''assoziatives Array''', ist eine Sammlung von '''Key''', '''Value''' Paaren. Sie eignet sich sehr gut dafür, für einen bestimmten '''Schlüssel''' einen '''Wert''' zu hinterlegen. Die Operationen, einfügen, löschen, abrufen, sind Konstant und weisen im Normalfall ein Laufzeitverhalten von '''O(1)''' auf. Die Reihenfolge der Element ist nicht definiert.
{{JML|code=
Map<String, Integer> nameAgeMap = new HashMap<String, Integer>();
 
//Speichern in Map: Key = Luke, Value = 35
nameAgeMap.put("Luke", 35);
 
//Holen aus der Map mit dem Key = Luke
System.out.println(nameAgeMap.get("Luke"));
}}


== Bäume ==
== Bäume ==
Bäume sind Datenstrukturen, welche im Kontext von Suchstrukturen, automatisch einer gewissen Ordnung folgen. Bereits nach dem Einfügen sind diese Element sortiert.
'''Bäume''' sind '''Datenstrukturen''', welche im Kontext von '''Suchstrukturen''', automatisch einer gewissen '''Ordnung''' folgen. Bereits nach dem Einfügen sind diese Element sortiert.
<br>
<br>
Es gibt eine Vielzahl verschiedener Baumstrukturen, die sich in einigen Punkten unterscheiden:
Es gibt eine Vielzahl verschiedener Baumstrukturen, die sich in ihren Eigenschaften unterscheiden:
* Binärbaum oder Binärer Suchbaum
* Binärbaum oder Binärer Suchbaum
* AVL Baum
* AVL Baum
Zeile 175: Zeile 209:
* ...
* ...
=== Elemente eines Baums ===
=== Elemente eines Baums ===
Betrachten wir die folgende Grafik, so können wir die Elemente leicht identifizieren. Weiters ist zu beachten, Bäume in der Informatik werden meist mit der Wurzel ganz oben dargestellt, nicht wie im echten Leben. Weiters wächst auch der Baum nach unten.
Betrachten wir die folgende Grafik, so können wir die Elemente leicht identifizieren. Weiters ist zu beachten, '''Bäume''' in der Informatik werden meist mit der '''Wurzel''' ganz oben dargestellt, nicht wie im echten Leben. Weiters wächst auch der '''Baum''' nach unten.
[[Datei:1280px-Binary-tree-Knuth.svg.png|mini|none|Elemente eines Baums. Quelle: https://de.wikipedia.org/wiki/Rot-Schwarz-Baum]]
[[Datei:1280px-Binary-tree-Knuth.svg.png|mini|none|Elemente eines Baums. Quelle: https://de.wikipedia.org/wiki/Rot-Schwarz-Baum]]
* '''Wurzel''' - Das erste Element im Baum
* '''Wurzel''' - Das erste Element im '''Baum'''
* '''Blatt oder äußerer Knoten''' - Knoten ganz außen ohne Nachkommen
* '''Blatt oder äußerer Knoten''' - Knoten ganz außen ohne Nachkommen
* '''Innerer Knoten''' - Knoten dazwischen
* '''Innerer Knoten''' - Knoten dazwischen
=== Eigenschaften eines Baums ===
=== Eigenschaften eines Baums ===
Im folgenden betrachten wir zwei verschiedene Eigenschaften von Bäumen, die {{Link|Höhe_eines_Baumes|Höhe}} und die {{Link|Balance}}.
==== Höhe eines Baumes ====
==== Höhe eines Baumes ====
Die höhe des Baumes ist die maximal auftretende Tiefe, also wieviele Element wächst der Baum nach unten. Somit hätte ein Baum mit nur einer Wurzel die Höhe von 0, das heißt er wächst 0 ebenen nach unten. In der Graphentheorie, ist eine Zuordnung der Höhe, nur auf nichtleere Bäume definiert. ''Teilweise wird die Höhe auch um 1 erhöht, damit ein leerer Baum die Höhe 0 hat und ein Baum mit nur einer Wurzel 1. Das soll uns aber jetzt nicht weiter beschäftigen.''
Die '''höhe des Baumes''' ist die maximal auftretende Tiefe, also wieviele Element wächst der '''Baum''' nach unten. Somit hätte ein '''Baum''' mit nur einer '''Wurzel''' die '''Höhe''' von 0, das heißt er wächst 0 Ebenen nach unten. In der Graphentheorie ist eine Zuordnung der Höhe, nur auf nichtleere Bäume definiert.
<br>
Teilweise wird die Höhe auch um 1 erhöht, damit ein leerer Baum die Höhe 0 hat und ein Baum mit nur einer Wurzel die Höhe 1. Das soll uns aber jetzt nicht weiter beschäftigen.
Folgende Grafik illustriert wie die Höhe eines Baumes bestimmt wird. Man beachte: ''Minimale Anzahl Knoten'' trifft hier nur auf balancierte bäume zu.
Folgende Grafik illustriert wie die Höhe eines Baumes bestimmt wird.
[[Datei:Baumhöhe.png|600px|none|mini|Höhe eines Baumes bestimmen. Quelle: https://www1.pub.informatik.uni-wuerzburg.de/databases/Binaere_Suchbaeume/avl_trees/properties/height.html]]
[[Datei:Baumhöhe.png|600px|none|mini|Höhe eines Baumes bestimmen. <ref>https://www1.pub.informatik.uni-wuerzburg.de/databases/Binaere_Suchbaeume/avl_trees/properties/height.html</ref>]]


==== Balance ====
==== Balance ====
Ob ein Binärer Suchbaum balanciert ist oder nicht, ist entscheidend für die Laufzeit der binären Suche.
Ob ein Binärer Suchbaum balanciert ist oder nicht, ist entscheidend für die Laufzeit der '''binären Such'''e.
<br><br>
<br><br>
'''Ein Baum ist balanciert wenn sich die Höhe aller Teilbäume nicht mehr als um einen gewissen Wert unterscheidet. Bei z.b.: AVL Bäumen darf die Höhendifferenz maximal 1 betragen, dies können wir auch für den Binärbaum annehmen'''
'''Ein Baum ist balanciert wenn sich die Höhe aller Teilbäume nicht mehr als um einen '''gewissen Wert''' unterscheidet. Bei z.b.: '''AVL Bäumen''' darf die Höhendifferenz maximal 1 betragen, dies können wir auch für den '''Binärbaum''' annehmen'''
<br><br>
<br><br>
Um die Balance zu bestimmen, müssen wir alle Teilbäume betrachten, wir gehen den Baum von unten nach oben durch und schreiben zu jedem Knoten die Differenz der Höhe des linken und rechten Teilbaums. Folgende Grafik sollte das recht gut illustrieren:
Um die Balance zu bestimmen, müssen wir alle Teilbäume betrachten, wir gehen den Baum von Unten nach Oben durch und schreiben zu jedem Knoten die Differenz der Höhe des linken und rechten Teilbaums. Folgende Grafik sollte das recht gut illustrieren.
Die Balance jedes Baumes (bzw. Teilbaumes) wird wie folgt berechnet: die Länge des rechten Teilbaumes Minus die Länge des linken Teilbaumes. Umso näher die Balance bei 0 ist, desto besser ausbalanciert ist der Baum. '''Philipp'''
[[Datei:AVL-tree-wBalance K.svg.png|mini|none|Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum]]
[[Datei:AVL-tree-wBalance K.svg.png|mini|none|Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum]]
Ist ein Baum nicht balanciert, so wächst die Suche nach einem Element, je nach grad der Entwartung, bis zur Laufzeit '''O(n)'''. Das bei der linearen Suche in einem Array oder einer Liste.
{| class="wikitable"
[[Datei:Balancierter_Baum_-_entarteter_Suchbaum.PNG|mini|none|Entarteter Suchbaum. Quelle: https://de.wikipedia.org/wiki/Balancierter_Baum]]
|+ Berchnung der Balance
|-
! Knoten !! Linker Teilbaum !! Rechter Teilbaum !! Balance
|-
| C || C -> 0 || C -> 0 || 0 - 0 -> '''0'''
|-
| D || D - C -> 1 || D -> 0 || 0 - 1 -> '''-1'''
|-
| F || F - D - C -> 2 || F - G -> 1 || 1 - 2 -> '''-1'''
|-
| J || J - F - D - C -> 3 || J - P - V - S - Q -> 4 || 4 - 3 -> '''1'''
|}
Ist ein Baum nicht balanciert, so wächst die Suche nach einem Element, je nach grad der Entartung, bis zur Laufzeit '''O(n)'''. Das ist die selbe Laufzeit wie bei der linearen Suche in einem {{Link|Array}} oder einer {{Link|Liste}}.
[[Datei:Balancierter_Baum_-_entarteter_Suchbaum.PNG|mini|none|Entarteter Suchbaum. <ref>https://de.wikipedia.org/wiki/Balancierter_Baum</ref>]]


=== Suche in einem Binärbaum ===
=== Suche in einem Binärbaum ===
Das Laufzeitverhalten der Suche in einem Binärbaum entspricht immer seiner Höhe (je nach Höhendefinition + 1). Betrachten wir folgendes Beispiel:
Das '''Laufzeitverhalten''' der '''Suche''' in einem '''Binärbaum''' entspricht immer seiner Höhe (je nach Höhendefinition + 1). Betrachten wir folgendes Beispiel:
[[Datei:AVL-tree-wBalance K.svg.png|300px|none|Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum]]
[[Datei:AVL-tree-wBalance K.svg.png|300px|none|Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum]]
* Die Höhe des Baumes beträgt 4, somit sind 5 Operationen nötig
* Die Höhe des Baumes beträgt 4, somit sind 5 Operationen nötig
* Wir suchen nach ''U''
* Wir suchen nach ''U''
* Die Vergleiche und die Navigation fassen wir als eine Operation zusammen
* Die Vergleiche und die Navigation fassen wir als eine Operation zusammen (die Konstante streichen wir direkt)
* Vergleich mit ''J'', ''P'', ''V'', ''S'' und zu guter letzt ''U''
* Vergleich mit ''J'', ''P'', ''V'', ''S'' und zu guter letzt ''U''
<br>
<br>
Die Höhe eines balancierten Baumes können wir vereinfacht mit '''log<sub>2</sub>(n)''' annehmen. Somit ist der Aufwand unserer Suche '''O(log<sub>2</sub>(n))'''
Die '''Höhe''' eines '''balancierten Baumes''' können wir vereinfacht mit '''log<sub>2</sub>(n)''' annehmen. Somit ist der Aufwand unserer Suche '''O(log<sub>2</sub>(n))'''


=== Code Beispiele ===
=== Code Beispiele ===
Sortierte Ausgabe eines binären Suchbaums '''(In-Order)'''
 
<syntaxhighlight lang='Java' line="'line'">
{{JML|code=
public void printInOrder() {
 
   print(root);
//Datenstruktur für die Knoten
public class Node {
  private Node left;
  private Node right;
  private int value;
 
  public Node(int value) {
    this.value = value;
  }
 
  public Node getLeft() {
    return this.left;
  }
 
  public Node getRight() {
    return this.right;
  }
 
  public void setLeft(Node node) {
    this.left = node;
  }
 
  public void setRight(Node node) {
    this.right = node;
   }
 
  public int getValue() {
    return this.value;
  }
}
}


public void printInOrder(Node node) {
//Implementierung des Baumes selbst
   if(node == null) {
public class MyTree {
     return;
  //Referenz auf die Wurzel
  private Node root;
 
  //Sortierte Ausgabe eines binären Suchbaums (In-Order)
   public void printInOrder() {
     print(root);
   }
   }
  //Rekursiver Aufruf für den Linken Teilbaum
  print(node.getLeft());
  //Ausgabe des aktuellen Wertes
  System.out.print(" "+node.getValue()+" ");
  //Rekursiver Aufruf für den Linken Teilbaum
  print(node.getRight());
}
</syntaxhighlight>


Einfügen eines Elements
  private void printInOrder(Node node) {
<syntaxhighlight lang='Java' line="'line'">
    if(node == null) {
public void add(int value) {
      return;
  add(root, new Node(value));
    }
}
    //Rekursiver Aufruf für den Linken Teilbaum
    print(node.getLeft());
    //Ausgabe des aktuellen Wertes
    System.out.print(" "+node.getValue()+" ");
    //Rekursiver Aufruf für den Linken Teilbaum
    print(node.getRight());
  }


private void add(Node node, Node newNode) {
  //Einfügen eines Elements
  if(root == null) {
  public void add(int value) {
    root = newNode;
    add(root, new Node(value));
    return;
   }
   }
   if(node.getValue() < newNode.getValue()) {
 
     if(node.getLeft() == null) {
   private void add(Node node, Node newNode) {
       node.setLeft(newNode);
     if(root == null) {
    } else {
       root = newNode;
       add(node.getLeft(), newNode);
       return;
     }
     }
  } else {
    if(node.getValue() < newNode.getValue()) {
    if(node.getRight() == null) {
      if(node.getLeft() == null) {
       node.setRight(newNode);
        node.setLeft(newNode);
       } else {
        add(node.getLeft(), newNode);
      }
     } else {
     } else {
       add(node.getRight(), newNode);
       if(node.getRight() == null) {
        node.setRight(newNode);
      } else {
        add(node.getRight(), newNode);
      }
     }
     }
   }
   }
}
</syntaxhighlight>


Finden eines Elements
  //Finden eines Elements
<syntaxhighlight lang='Java' line="'line'">
  public boolean contains(int value) {
public boolean contains(int value) {
    return contains(root, value);
  return contains(root, value);
  }
}


private void contains(Node node, int value) {
  private void contains(Node node, int value) {
  if(node == null) {
    if(node == null) {
    return false;
      return false;
  }
    }
  if(node.getValue() == value) {
    if(node.getValue() == value) {
    return true;
      return true;
  } else if(node.getValue() < value) {
    } else if(node.getValue() < value) {
    return contains(node.getLeft(), value);
      return contains(node.getLeft(), value);
  } else {
    } else {
    return contains(node.getRight(), value);
      return contains(node.getRight(), value);
    }
   }
   }
}
}
</syntaxhighlight>
}}
 
== O Notation ==
== O Notation ==
Hier findet sich die O - Notation für einige gebräuchliche Operationen und dessen Erklärung. Man beachte, es muss immer vom schlechtesten Fall ausgegangen werden.
Hier findet sich die '''O - Notation''' für einige gebräuchliche Operationen und dessen Erklärung. Man beachte, es wird hierbei immer vom schlechtesten Fall ausgegangen. Es wird auch oft die '''O - Notation''' für die '''Minimal-, Durchschnitts-, und Maximallaufzeit''' angegeben.
{| class="wikitable"
{| class="wikitable"
|+ Element an bestimmtem Index holen
|+ Element an bestimmtem Index holen
Zeile 293: Zeile 378:
| LinkedList || O(n) || Liste muss bis zum gewünschten Index durchlaufen werden, das Löschen selbst hat eine konstante Laufzeit, es müssen nur die Verknüpfungen aktualisiert werden
| LinkedList || O(n) || Liste muss bis zum gewünschten Index durchlaufen werden, das Löschen selbst hat eine konstante Laufzeit, es müssen nur die Verknüpfungen aktualisiert werden
|-
|-
| ArrayList || O(n) || Element kann direkt durch berechnung des Index gelöscht werden, jedoch müssen alle Nachfolgenden Elemente nach links verschoben werden
| ArrayList || O(n) || Element kann direkt durch Berechnung des Index gelöscht werden, jedoch müssen alle nachfolgenden Elemente nach links verschoben werden
|-
|-
| Array || O(n) || Element kann direkt durch berechnung des Index gelöscht werden, jedoch müssen alle Nachfolgenden Elemente nach links verschoben werden
| Array || O(n) || Element kann direkt durch Berechnung des Index gelöscht werden, jedoch müssen alle nachfolgenden Elemente nach links verschoben werden
|}
|}
{| class="wikitable"
{| class="wikitable"
Zeile 304: Zeile 389:
| LinkedList || O(n) || Liste muss bis zum gewünschten Index durchlaufen werden, das Einfügen selbst hat eine konstante Laufzeit, ein neuer Knoten wird erstellt und die Verknüpfungen werden aktualisiert
| LinkedList || O(n) || Liste muss bis zum gewünschten Index durchlaufen werden, das Einfügen selbst hat eine konstante Laufzeit, ein neuer Knoten wird erstellt und die Verknüpfungen werden aktualisiert
|-
|-
| ArrayList || O(n) || Element kann direkt durch berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden
| ArrayList || O(n) || Element kann direkt durch Berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden
|-
|-
| Array || O(n) || Element kann direkt durch berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden
| Array || O(n) || Element kann direkt durch Berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden
|}
|}


= Sortieralgorithmen =
= Sortieralgorithmen =
Sortieralgorithmen werden benötigt um Datenstrukturen nach gewissen Kriterien zu sortieren. Dazu müssen die Elemente natürlich vergleichbar sein, d.h. es muss möglich sein herauszufinden ob ein Element "größer" oder "kleiner" als ein anderes ist.
'''Sortieralgorithmen''' werden benötigt um '''Datenstrukturen''' nach '''gewissen Kriterien''' zu sortieren. Dazu müssen die Elemente natürlich '''vergleichbar''' sein, d.h. es muss möglich sein herauszufinden ob ein Element '''größer''' oder '''kleiner''' als ein anderes ist.


== Eigenschaften ==  
== Eigenschaften ==  
Sortieralgorithmen können über mehrere Eigenschaften verfügen.
'''Sortieralgorithmen''' können über mehrere '''Eigenschaften''' verfügen.
=== In-Place ===
=== In-Place ===
Das bedeutet der Sortieralgorithmus benötigt "keinen" weiteren Speicherplatz beim sortieren, bzw. wenn der Speicherbedarf konstant ist, und nicht mit der Anzahl der Elemente wächst. Z.b. der Sortieralgorithmus Mergesort, teilt die Daten der bestehenden Liste in Sublisten auf, er ist somit nicht In-Place.
Das bedeutet der '''Sortieralgorithmus''' benötigt '''keinen''' weiteren Speicherplatz, oder der Speicherbedarf ist '''konstant''' und nicht von der Anzahl der Elemente abhängig. Z.b. der Sortieralgorithmus Mergesort teilt die Daten der bestehenden Liste in Sublisten auf, er ist somit nicht In-Place.
=== Stabil ===
=== Stabil ===
Ein Sortieralgorithmus arbeitet stabil, wenn dieser eine bestehende korrekte Ordnung nicht verändert.
Ein '''Sortieralgorithmus''' arbeitet '''stabil''', wenn dieser eine bestehende korrekte Ordnung '''nicht''' verändert.


== Bubblesort ==
== Bubblesort ==
Bubblesort ist ein vergleichsweise akademischer Algorithmus. Er ist wenig effizient, arbeitet jedoch '''Stabil''' und '''In-Place'''. Folgende Grafik illustriert die Funktionsweise, man beachte dass bei jedem Durchlauf die Anzahl der Vergleiche um 1 abnimmt, da beim letzten Vergleich immer die höchste Zahl ganz rechts steht. '''Weiters kann die Sortierung abgebrochen werden, wenn in einem Durchlauf keine Zahl vertauscht wurde.'''
Bubblesort ist ein vergleichsweise akademischer Algorithmus. Er ist wenig effizient, arbeitet jedoch '''stabil''' und '''In-Place'''. Folgende Grafik illustriert die Funktionsweise. Man beachte, dass bei jedem Durchlauf die Anzahl der Vergleiche um '''1''' abnimmt, da beim letzten Vergleich immer die höchste Zahl ganz rechts steht. Weiters kann die Sortierung abgebrochen werden, wenn in einem Durchlauf keine Zahl vertauscht wurde.
[[Datei:Bubble-sort-example-300px.gif|mini|none|Funktionsweise Bubblesort. Quelle: https://de.wikipedia.org/wiki/Bubblesort]]
[[Datei:Bubble-sort-example-300px.gif|mini|none|Funktionsweise Bubblesort. <ref>https://de.wikipedia.org/wiki/Bubblesort</ref>]]


=== Code ===
=== Code ===
Bubblesort in Java
Folgendes Code Beispiel zeigt die Implementierung von '''Bubblesort''' in '''Java''':
<syntaxhighlight lang='Java' line="'line'">
{{JML|code=
public class Bubblesort {
public class Bubblesort {
public static void sort(int[] arr) {
  public static void sort(int[] arr) {
int operations = 0;
    int operations = 0;
int n = arr.length;
    int n = arr.length;
boolean swapped = false;
    boolean swapped = false;
do {
    do {
swapped = false;
      swapped = false;
for (int i = 0; i < n - 1; i++) {
      for (int i = 0; i < n - 1; i++) {
operations++;
        operations++;
if (arr[i] > arr[i + 1]) {
        if (arr[i] > arr[i + 1]) {
int tmp = arr[i];
          int tmp = arr[i];
arr[i] = arr[i + 1];
          arr[i] = arr[i + 1];
arr[i + 1] = tmp;
          arr[i + 1] = tmp;
swapped = true;
          swapped = true;
}
        }
}
      }
// Letztes Element ist an der richtigen stelle, n um 1 reduzieren
      // Letztes Element ist an der richtigen stelle, n um 1 reduzieren
n = n - 1;
      n = n - 1;
} while (swapped);
    } while (swapped);
System.out.println("Operations: " + operations);
    System.out.println("Operations: " + operations);
}
  }


public static void main(String[] args) {
  public static void main(String[] args) {
int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
sort(arr);
    sort(arr);
arr = new int[] { 6, 5, 4, 3, 2, 1 };
    arr = new int[] { 6, 5, 4, 3, 2, 1 };
sort(arr);
    sort(arr);
arr = new int[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
    arr = new int[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
sort(arr);
    sort(arr);
}
  }
}
}


</syntaxhighlight>
}}


=== Laufzeitanalyse ===
=== Laufzeitanalyse ===
Für die Laufzeitanalyse verwenden wir die maximal mögliche Laufzeit.
Für die Laufzeitanalyse verwenden wir die '''maximal''' mögliche '''Laufzeit'''.
Betrachten wir hierfür folgendes Array:
Betrachten wir hierfür folgendes Array:
10,8,5,3,2,1
10,8,5,3,2,1
* 1) Durchlauf: Die Zahl 10 wird mit 8,5,3,2 und 1 verglichen und vertauscht.  
* 1) Durchlauf: Die Zahl 10 wird mit 8,5,3,2 und 1 verglichen und vertauscht.  
* 2) Durchlauf: Die Zahl 8 wird mit 5,3,2 und 1 verglichen und vertauscht, '''mit 10 muss nichtmehr verglichen werden'''.
* 2) Durchlauf: Die Zahl 8 wird mit 5,3,2 und 1 verglichen und vertauscht, '''mit 10 muss nicht mehr verglichen werden'''.
* 3) Durchlauf: Die Zahl 5 wird mit 3,2 und 1 verglichen und vertauscht, '''mit 10, 8 muss nichtmehr verglichen werden'''.
* 3) Durchlauf: Die Zahl 5 wird mit 3,2 und 1 verglichen und vertauscht, '''mit 10, 8 muss nicht mehr verglichen werden'''.
* 4) Durchlauf: Die Zahl 3 wird mit 2 und 1 verglichen und vertauscht, '''mit 10, 8, 5 muss nichtmehr verglichen werden'''.
* 4) Durchlauf: Die Zahl 3 wird mit 2 und 1 verglichen und vertauscht, '''mit 10, 8, 5 muss nicht mehr verglichen werden'''.
* 5) Durchlauf: Die Zahl 2 wird mit 1 verglichen und vertauscht, '''mit 10, 8, 5, 3 muss nichtmehr verglichen werden'''.
* 5) Durchlauf: Die Zahl 2 wird mit 1 verglichen und vertauscht, '''mit 10, 8, 5, 3 muss nicht mehr verglichen werden'''.
* Ende, die Zahl 1 muss nichtmehr verglichen werden.
* Ende, die Zahl 1 muss nicht mehr verglichen werden.


Vergleichen und vertauschen betrachten wir nun als eine Operation.
Vergleichen und Vertauschen betrachten wir nun als eine Operation (da Konstant).


* 1) Durchlauf -> 5 Operationen
* 1) Durchlauf -> 5 Operationen
Zeile 384: Zeile 469:
* 1) (n-1) + (n-2) + ... 1
* 1) (n-1) + (n-2) + ... 1
* 2) ''Das ist ja der kleine Gauß minus n:'' (n * (n + 1))/2 - n
* 2) ''Das ist ja der kleine Gauß minus n:'' (n * (n + 1))/2 - n
* 3) (n<sup>2</sup> + n - 2*n)/2
* 3) (n * (n + 1))/2 - (2*n)/2
* 4) (n<sup>2</sup> - n)/2
* 4) (n<sup>2</sup> + n - 2*n)/2
* 5) 1/2*(n<sup>2</sup> - n)
* 5) (n<sup>2</sup> - n)/2
* 6) n<sup>2</sup> - n -> Konstante Werte werden bei der O - Notation entfernt
* 6) 1/2*(n<sup>2</sup> - n)
* 7) n<sup>2</sup> -> n ist zu n<sup>2</sup> vernachlässigbar und wird entfernt
* 7) n<sup>2</sup> - n -> Konstante Werte werden bei der O - Notation entfernt
* 8) n<sup>2</sup> -> n ist zu n<sup>2</sup> vernachlässigbar und wird entfernt


==== Laufzeit ====
==== Laufzeit ====
'''Somit ist die Laufzeit von Bubblesort O(n<sup>2</sup>)'''
'''Somit ist die Laufzeit von '''Bubblesort''' O(n<sup>2</sup>)'''
==== Laufzeit Bestcase ====
==== Laufzeit Bestcase ====
Liegt bereits eine sortierte Liste vor, so benötigt Bubblesort nur einen Durchlauf. Dabei erfolgen '''n - 1''' vergleiche. Die Konstante wird entfernt.
Liegt bereits eine sortierte Liste vor, so benötigt '''Bubblesort''' nur einen Durchlauf. Dabei erfolgen '''n - 1''' vergleiche. Die Konstante wird entfernt.
<br>
<br>
'''Die Laufzeit im Bestcase ist O(n)'''
'''Die Laufzeit im Bestcase ist O(n)'''

Aktuelle Version vom 10. Februar 2021, 09:35 Uhr

Im folgenden Themenbereich geht es um Algorithmen und Datenstrukturen und um die Landau Symbole oder O - Notation und wie diese im Kontext von Algorithmen und Datenstrukturen verwendet werden.
Bei den Alghorithmen werden wir uns auf Sortieralgorithmen beschränken und uns mit deren Laufzeitanalyse (und der daraus resultierenden O - Notation) befassen.
Bei den Datenstrukturen beschränken wir uns auf Arrays, Listen und Bäume und ebenfalls um deren Laufzeitanalyse für verschiedene Operationen.

Landau Symbole oder O - Notation

Landau-Symbole (auch O-Notation) werden in der Mathematik und in der Informatik verwendet, um das asymptotische Verhalten von Funktionen und Folgen zu beschreiben. In der Informatik werden sie bei der Analyse von Algorithmen verwendet und geben ein Maß für die Anzahl der Elementarschritte oder der Speichereinheiten in Abhängigkeit von der Größe des gegebenen Problems an [1].

Unter asymptotischem Verhalten verstehen wir hier das Grenzverhalten eines Algorithmus, vereinfacht gesagt, wie kann sich die Laufzeit im schlechtesten Fall entwickeln. Um das Grenzverhalten eines Algorithmus zu betrachten, analysieren wir wieviele Operationen im schlechtesten Fall in Bezug auf die größe des Problems (z.B.: Anzahl der Elemente) benötigt wird. Konstante Werte können dabei vernachlässigt werden (z.B.: eine Schleife muss 2 mal durchlaufen werden. Dieser Wert ist eine Konstante da sie unabhängig von der Anzahl der Elemente ist, es wird also immer 2 mal durchlaufen).

Beispiel

Suchen des kleinsten und größten Elements in einem Array.

1 2 3 4 5 6

Um das größte Element zu finden, werden 5 Schritte benötigt, dies entspricht der Länge des Arrays - 1. Für die Suche des größten Elements benötigen wir ebenfalls 5 Schritte. Somit ist die Laufzeit des Algorithmus 2 * (n - 1). Die Konstanten werden entfernt, somit bleibt: O(n)

public void findMinAndMax(int[] arr) {
  int min = arr[0];
  int max = arr[0];
  for(int i=1; i<arr.length; i++) {
    min = Math.min(min, arr[i]);
  }
  for(int i=1; i<arr.length; i++) {
    max = Math.max(max, arr[i]);
  }
  System.out.println("Min: "+min);
  System.out.println("Max: "+max);
}

In der O - Notation werden einige gebräuchliche Schranken verwendet, wenn wir einen Algorithmus oder eine Datenstruktur analysieren, verwenden wir diese Schranken und nicht die exakte Mathematische repräsentation des Problems, auch wenn dies möglich wäre (siehe Bubblesort)

Gebräuchliche Beschränkungen (es gibt viele weitere!)
O - Notation Bedeutung Beispiel
O(1) Das Laufzeitverhalten hängt nicht von der Anzahl der Elemente ab und ist konstant Element an der Stelle N aus einem Array oder einer ArrayListe holen
O(n) Das Laufzeitverhalten verlängert sich linear mit der Anzahl der Element Suchen eines bestimmten Elements in einem unsortierten Array
O(n log n) Super lineares Wachstum :-DDD Mergesort, Heapsort, Durchschnittsfall Quicksort
O(n2) Quadratisches Wachstum, die Laufzeit wächst quadratisch zur Anzahl der Elemente Bubblesort
O(log2n) 2er Logarithmisches Wachstum Suche im balancierten Binärbaum

Datenstrukturen

In der Informatik und Softwaretechnik ist eine Datenstruktur ein Objekt, welches zur Speicherung und Organisation von Daten dient. Es handelt sich um eine Struktur, weil die Daten in einer bestimmten Art und Weise angeordnet und verknüpft werden, um den Zugriff auf sie und ihre Verwaltung effizient zu ermöglichen [2]. Im folgenden sollen einige Datenstrukturen erläutert werden, für jede Datenstruktur gibt es andere Einsatzgebiete die für deren Eigenschaften optimal sind.

Array

Ein Array oder Feld ist eine sehr effiziente doch wenig dynamische Datenstruktur. In nahezu allen Programmiersprachen findet sich diese Datenstruktur wieder. Weiter ist auch ein dynamisches vergrößern oder verkleinern in einigen Programmiersprachen automatisch implementiert (z.B.: Javascript).
Gehen wir jedoch von Programmiersprachen wie Java oder C aus, so hat ein Array immer eine fixe Größe. Das heißt, zur Laufzeit (nach dem das Programm compiliert und gestartet wurde) kann diese nicht verändert werden.

Vorteile

  • Der Speicheroverhead ist sehr gering - Es wird nur der Speicher benötigt, der für die Anzahl der Elemente benötigt wird.
  • Geringe Zugriffszeit - Wird ein Element an einer Stelle angesprochen, so kann für den Zugriff die exakte Speicheradresse berechnet werden.

Nachteile

  • Wenig flexibel - Wie bereits erwähnt, muss die Größe meist fix definiert werden. Eine Vergrößerung/Verkleinerung ist nicht möglich, ermöglicht die Programmiersprache dies, so hat dies zur Folge, dass das Array neu erstellt wird, und die Elemente kopiert werden müssen.

Einsatzgebiete

Überall dort, wo eine fixe Speichergröße benötigt und Effizienz wichtig ist, dort sollte die Verwendung von Arrays in Betracht gezogen werden.

  • Bildmanipulation - Wird ein Bild zur Bearbeitung geladen, so kann die Bitmap in einem Array gespeichert werden. jedes Element im Array entspricht dann einem Pixel.
  • Puffern von Daten - Beim Lesen und Schreiben werden Daten meist in einem Array zwischengespeichert (gepuffert). Danach kann die Bearbeitung erfolgen. Weiters kann bei jedem Lese oder Schreibvorgang erneut das selbe Array verwendet werden, was die Effizienz weiter erhöht. Für solche Operationen wird meist eine fixe Puffergröße verwendet, deswegen eignet sich das Array hierfür.
  • Jegliches schachbrettartige Problem mit fixer Größe - Schiffe versenken, Schach,...

Liste

Die Liste ist eine sehr gebräuchliche Datenstruktur. Sie ist gekennzeichnet durch, je nach implementierung, gute Zugriffszeiten oder erhöhte Flexibilität.
Beziehen wir uns auf die Programmiersprache Java, so ist die Liste ein Interface, also eine Schnittstelle. Diese Schnittstelle definiert einige Methoden die eine Implementierung erfüllen muss. Einige davon wären:

  • E get(int index) - Gibt das Element an der Stelle index aus
  • E remove(int index) - Löscht das Element an der Stelle index
  • boolean add(E element) - Fügt ein Element an das Ende der Liste hinzu
  • boolean add(int index, E element) - Fügt ein Element an einer bestimmten Position ein, und schiebt nachfolgende Elemente nach rechts
  • ...

Hier ist nun ersichtlich, dass die Flexibilität der Liste ein essenzieller Bestandteil der Definition ist.
Anbei einige Implementierungen des Interfaces List.

ArrayList

Die ArrayList vereint die Eigenschaften einer Liste und eines . Die interne Implementierung verwendet ein von fixer Größe um Elemente zu speichern. Wird diese fixe Größe überschritten, so wird intern ein neues angelegt, je nach Implementierung mit einem gewissen Vergrößerungsfaktor. Nach dem Neuanlegen des internen , werden alle Elemente kopiert und in das neue eingefügt. Wird ein Element gelöscht oder an einer Stelle eingefügt, so müssen bestehende Elemente verschoben werden.

Element wird aus ArrayListe entfernt. [3]

In Java wird die größe des internen Arrays nicht automatisch reduziert, wenn Elemente gelöscht werden. Dafür gibt es die Methode myArrayList.trim()

Vorteile

  • Relativ geringer Speicheroverhead - Dies verhält sich ähnlich wie beim . Jedoch können in der ArrayList keine primitiven Datentypen gespeichert werden, sondern nur Objekte von . Dies bedeutet, es wird nur ein von Referenzen erstellt. In jedem Platz findet sich eine Referenz auf das wirkliche Objekt. Dies bedeutet in Bezug auf primitive Datentypen einen weit höheren Speicherbedarf als bei einem Array.
  • Geringe Zugriffszeit - Wird ein Element an einer Stelle angesprochen, so kann für den Zugriff die exakte Speicheradresse berechnet werden.

Nachteile

  • Ineffizientes Löschen - Laufzeit beim Löschen nicht optimal, Elemente müssen verschoben werden.
  • Ineffizientes Einfügen - Laufzeit beim Einfügen von Elementen an einer gewissen Stelle langsam, wie beim löschen müssen alle nachfolgenden Elemente verschoben werden.
  • Ineffizientes Hinzufügen, wenn internes voll - Laufzeit beim Hinzufügen von Elementen bei vollem langsam, ein neues muss erstellt und alle Elemente müssen in das neue kopiert werden

Code beispiel

List<Integer> myArrayList = new ArrayList<>();
myArrayList.add(1);
myArrayList.add(1);
myArrayList.add(1);
myArrayList.remove(0);
System.out.println("Element an der Stelle 2: "+myArrayList.get(1));
  • Zeile 1) Die ArrayList wird erzeugt und einer Variable des Typs List zugewiesen. Warum ist das möglich? Weil ArrayList das Interface List implementiert. D.h.: Eine ArrayList ist eine List.
  • Zeile 2) - 4) fügen der ArrayList einen int hinzu. Hier ist zu beachten, der primitive Datentyp int wird automatisch in die Klasse java.lang.Integer umgewandelt. Dieser mechanismus nennt sich Autoboxing.
  • Zeile 5) Das Element an der Stelle 0 wird aus der ArrayList entfernt
  • Zeile 6) Ausgabe des Elements an der Stelle 2 (Index 1)

Einsatzgebiete

Die Einsatzgebiete überschneiden sich mit herkömmlichen Arrays. Benötigt man mehr flexibilität und soll nicht mit primitiven Datentypen gearbeitet werden, so kann anstatt eines Arrays die ArrayListe verwendet werden. Ist die Anzahl der Elemente start fluktuierend (oftmaliges löschen/einfügen/hinzufügen), so sollte man aus oben genannten Gründen auf eine andere Datenstruktur ausweichen.

LinkedList oder verkettete Liste

Die verkettete Liste ist eine sehr dynamische Datenstruktur. Ihre Größe ist flexibel. Die Daten werden in Knoten gehalten, welche im Falle der einfach verketteten Liste auf den Nachfolger und bei der doppelt verketteten Liste zusätzlich auf den Vorgänger Knoten zeigen. Elemente können einfach an bestimmten Stellen eingefügt oder gelöscht werden, es müssen lediglich die Nachfolger bzw. Vorgänger aktualisiert werden.

Einfach und doppelt verkettete Liste. [4]

Vorteile

  • Einfügen und Löschen effizient - Es müssen lediglich die Zeiger auf den Vorgänger/Nachfolger geändert werden.
  • Speicherbedarf flexibel - Der Speicherbedarf wächst und schrumpft mit der Anzahl der Elemente.

Nachteile

  • Zufälliger Zugriff langsam - Werden in der LinkedList Elemente mit ihrem Index angesprochen, so muss zuerst die komplette Liste bis zum entsprechenden Index durchlaufen werden.
  • Speicheroverhead - Da die Daten in Knoten gespeichert werden müssen entsteht automatisch ein Speicheroverhead für die Datenstruktur des Knoten.

Einsatzgebiete

  • Wenn die größe der Liste stark variiert, d.h. oftmals Elemente eingefügt, gelöscht und hinzugefügt werden und kein zufälliger Zugriff benötigt wird.

Code Beispiel

//Datenstruktur Node
public class Node {
  private Integer value;
  private Node next;

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

  public void setNext(Node next) {
    this.next = next;
  }

  public Node next();
    return this.next;
  }

  public Integer getValue() {
    return this.value;
  }
}

//Die Implementierung der LinkedList
public class MyList {
  //Referenz für erstes Listenelement
  private Node root;

  //Hinzufügen eines Elements
  public boolean add(Integer element) {
    Node newNode = new Node(element);
    if(root == null) {
      root = newNode;
    } else {
      Node current = root;
      while(current.next() != null) {
        current = current.next();
      }
      current.setNext(newNode);
    }
    return true;
  }

  //Holen eines Elements
  public int get(int index) {
    Node current = root;
    for(int i=0; i<index && current != null; i++) {
      current = current.next();
    }
    if(current == null) {
      throw new IndexOutOfBoundsException();
    }
    return current.getValue();
  }
}

Assoziativer Speicher

Der assoziative Speicher oder auch Map, oder assoziatives Array, ist eine Sammlung von Key, Value Paaren. Sie eignet sich sehr gut dafür, für einen bestimmten Schlüssel einen Wert zu hinterlegen. Die Operationen, einfügen, löschen, abrufen, sind Konstant und weisen im Normalfall ein Laufzeitverhalten von O(1) auf. Die Reihenfolge der Element ist nicht definiert.

Map<String, Integer> nameAgeMap = new HashMap<String, Integer>();

//Speichern in Map: Key = Luke, Value = 35
nameAgeMap.put("Luke", 35);

//Holen aus der Map mit dem Key = Luke
System.out.println(nameAgeMap.get("Luke"));

Bäume

Bäume sind Datenstrukturen, welche im Kontext von Suchstrukturen, automatisch einer gewissen Ordnung folgen. Bereits nach dem Einfügen sind diese Element sortiert.
Es gibt eine Vielzahl verschiedener Baumstrukturen, die sich in ihren Eigenschaften unterscheiden:

  • Binärbaum oder Binärer Suchbaum
  • AVL Baum
  • Rot schwarz baum
  • B*Baum
  • ...

Elemente eines Baums

Betrachten wir die folgende Grafik, so können wir die Elemente leicht identifizieren. Weiters ist zu beachten, Bäume in der Informatik werden meist mit der Wurzel ganz oben dargestellt, nicht wie im echten Leben. Weiters wächst auch der Baum nach unten.

Elemente eines Baums. Quelle: https://de.wikipedia.org/wiki/Rot-Schwarz-Baum
  • Wurzel - Das erste Element im Baum
  • Blatt oder äußerer Knoten - Knoten ganz außen ohne Nachkommen
  • Innerer Knoten - Knoten dazwischen

Eigenschaften eines Baums

Im folgenden betrachten wir zwei verschiedene Eigenschaften von Bäumen, die Höhe und die Balance.

Höhe eines Baumes

Die höhe des Baumes ist die maximal auftretende Tiefe, also wieviele Element wächst der Baum nach unten. Somit hätte ein Baum mit nur einer Wurzel die Höhe von 0, das heißt er wächst 0 Ebenen nach unten. In der Graphentheorie ist eine Zuordnung der Höhe, nur auf nichtleere Bäume definiert.

Teilweise wird die Höhe auch um 1 erhöht, damit ein leerer Baum die Höhe 0 hat und ein Baum mit nur einer Wurzel die Höhe 1. Das soll uns aber jetzt nicht weiter beschäftigen.

Folgende Grafik illustriert wie die Höhe eines Baumes bestimmt wird.

Höhe eines Baumes bestimmen. [5]

Balance

Ob ein Binärer Suchbaum balanciert ist oder nicht, ist entscheidend für die Laufzeit der binären Suche.

Ein Baum ist balanciert wenn sich die Höhe aller Teilbäume nicht mehr als um einen gewissen Wert unterscheidet. Bei z.b.: AVL Bäumen darf die Höhendifferenz maximal 1 betragen, dies können wir auch für den Binärbaum annehmen

Um die Balance zu bestimmen, müssen wir alle Teilbäume betrachten, wir gehen den Baum von Unten nach Oben durch und schreiben zu jedem Knoten die Differenz der Höhe des linken und rechten Teilbaums. Folgende Grafik sollte das recht gut illustrieren.

Die Balance jedes Baumes (bzw. Teilbaumes) wird wie folgt berechnet: die Länge des rechten Teilbaumes Minus die Länge des linken Teilbaumes. Umso näher die Balance bei 0 ist, desto besser ausbalanciert ist der Baum. Philipp
Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum
Berchnung der Balance
Knoten Linker Teilbaum Rechter Teilbaum Balance
C C -> 0 C -> 0 0 - 0 -> 0
D D - C -> 1 D -> 0 0 - 1 -> -1
F F - D - C -> 2 F - G -> 1 1 - 2 -> -1
J J - F - D - C -> 3 J - P - V - S - Q -> 4 4 - 3 -> 1

Ist ein Baum nicht balanciert, so wächst die Suche nach einem Element, je nach grad der Entartung, bis zur Laufzeit O(n). Das ist die selbe Laufzeit wie bei der linearen Suche in einem Array oder einer Liste.

Entarteter Suchbaum. [6]

Suche in einem Binärbaum

Das Laufzeitverhalten der Suche in einem Binärbaum entspricht immer seiner Höhe (je nach Höhendefinition + 1). Betrachten wir folgendes Beispiel:

Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum
Balance eines Baumes. Quelle: https://de.wikipedia.org/wiki/AVL-Baum
  • Die Höhe des Baumes beträgt 4, somit sind 5 Operationen nötig
  • Wir suchen nach U
  • Die Vergleiche und die Navigation fassen wir als eine Operation zusammen (die Konstante streichen wir direkt)
  • Vergleich mit J, P, V, S und zu guter letzt U


Die Höhe eines balancierten Baumes können wir vereinfacht mit log2(n) annehmen. Somit ist der Aufwand unserer Suche O(log2(n))

Code Beispiele

//Datenstruktur für die Knoten
public class Node {
  private Node left;
  private Node right;
  private int value;

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

  public Node getLeft() {
    return this.left;
  }

  public Node getRight() {
    return this.right;
  }

  public void setLeft(Node node) {
    this.left = node;
  }

  public void setRight(Node node) {
    this.right = node;
  }

  public int getValue() {
    return this.value;
  }
}

//Implementierung des Baumes selbst
public class MyTree {
  //Referenz auf die Wurzel
  private Node root;

  //Sortierte Ausgabe eines binären Suchbaums (In-Order)
  public void printInOrder() {
    print(root);
  }

  private void printInOrder(Node node) {
    if(node == null) {
      return;
    }
    //Rekursiver Aufruf für den Linken Teilbaum
    print(node.getLeft());
    //Ausgabe des aktuellen Wertes
    System.out.print(" "+node.getValue()+" ");
    //Rekursiver Aufruf für den Linken Teilbaum
    print(node.getRight());
  }

  //Einfügen eines Elements
  public void add(int value) {
    add(root, new Node(value));
  }

  private void add(Node node, Node newNode) {
    if(root == null) {
      root = newNode;
      return;
    }
    if(node.getValue() < newNode.getValue()) {
      if(node.getLeft() == null) {
        node.setLeft(newNode);
      } else {
        add(node.getLeft(), newNode);
      }
    } else {
      if(node.getRight() == null) {
        node.setRight(newNode);
      } else {
        add(node.getRight(), newNode);
      }
    }
  }

  //Finden eines Elements
  public boolean contains(int value) {
    return contains(root, value);
  }

  private void contains(Node node, int value) {
    if(node == null) {
      return false;
    }
    if(node.getValue() == value) {
      return true;
    } else if(node.getValue() < value) {
      return contains(node.getLeft(), value);
    } else {
      return contains(node.getRight(), value);
    }
  }
}

O Notation

Hier findet sich die O - Notation für einige gebräuchliche Operationen und dessen Erklärung. Man beachte, es wird hierbei immer vom schlechtesten Fall ausgegangen. Es wird auch oft die O - Notation für die Minimal-, Durchschnitts-, und Maximallaufzeit angegeben.

Element an bestimmtem Index holen
Datenstruktur O - Notation Begründung
LinkedList O(n) Liste muss bis zum gewünschten Index durchlaufen werden
ArrayList O(1) Index kann berechnet werden
Array O(1) Index kann berechnet werden
Element an bestimmtem Index löschen
Datenstruktur O - Notation Begründung
LinkedList O(n) Liste muss bis zum gewünschten Index durchlaufen werden, das Löschen selbst hat eine konstante Laufzeit, es müssen nur die Verknüpfungen aktualisiert werden
ArrayList O(n) Element kann direkt durch Berechnung des Index gelöscht werden, jedoch müssen alle nachfolgenden Elemente nach links verschoben werden
Array O(n) Element kann direkt durch Berechnung des Index gelöscht werden, jedoch müssen alle nachfolgenden Elemente nach links verschoben werden
Element an bestimmtem Index einfügen
Datenstruktur O - Notation Begründung
LinkedList O(n) Liste muss bis zum gewünschten Index durchlaufen werden, das Einfügen selbst hat eine konstante Laufzeit, ein neuer Knoten wird erstellt und die Verknüpfungen werden aktualisiert
ArrayList O(n) Element kann direkt durch Berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden
Array O(n) Element kann direkt durch Berechnung des Index eingefügt werden, jedoch müssen alle Elemente ab diesem Index nach rechts verschoben werden

Sortieralgorithmen

Sortieralgorithmen werden benötigt um Datenstrukturen nach gewissen Kriterien zu sortieren. Dazu müssen die Elemente natürlich vergleichbar sein, d.h. es muss möglich sein herauszufinden ob ein Element größer oder kleiner als ein anderes ist.

Eigenschaften

Sortieralgorithmen können über mehrere Eigenschaften verfügen.

In-Place

Das bedeutet der Sortieralgorithmus benötigt keinen weiteren Speicherplatz, oder der Speicherbedarf ist konstant und nicht von der Anzahl der Elemente abhängig. Z.b. der Sortieralgorithmus Mergesort teilt die Daten der bestehenden Liste in Sublisten auf, er ist somit nicht In-Place.

Stabil

Ein Sortieralgorithmus arbeitet stabil, wenn dieser eine bestehende korrekte Ordnung nicht verändert.

Bubblesort

Bubblesort ist ein vergleichsweise akademischer Algorithmus. Er ist wenig effizient, arbeitet jedoch stabil und In-Place. Folgende Grafik illustriert die Funktionsweise. Man beachte, dass bei jedem Durchlauf die Anzahl der Vergleiche um 1 abnimmt, da beim letzten Vergleich immer die höchste Zahl ganz rechts steht. Weiters kann die Sortierung abgebrochen werden, wenn in einem Durchlauf keine Zahl vertauscht wurde.

Funktionsweise Bubblesort. [7]

Code

Folgendes Code Beispiel zeigt die Implementierung von Bubblesort in Java:

public class Bubblesort {
  public static void sort(int[] arr) {
    int operations = 0;
    int n = arr.length;
    boolean swapped = false;
    do {
      swapped = false;
      for (int i = 0; i < n - 1; i++) {
        operations++;
        if (arr[i] > arr[i + 1]) {
          int tmp = arr[i];
          arr[i] = arr[i + 1];
          arr[i + 1] = tmp;
          swapped = true;
        }
      }
      // Letztes Element ist an der richtigen stelle, n um 1 reduzieren
      n = n - 1;
    } while (swapped);
    System.out.println("Operations: " + operations);
  }

  public static void main(String[] args) {
    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    sort(arr);
    arr = new int[] { 6, 5, 4, 3, 2, 1 };
    sort(arr);
    arr = new int[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
    sort(arr);
  }
}

Laufzeitanalyse

Für die Laufzeitanalyse verwenden wir die maximal mögliche Laufzeit. Betrachten wir hierfür folgendes Array: 10,8,5,3,2,1

  • 1) Durchlauf: Die Zahl 10 wird mit 8,5,3,2 und 1 verglichen und vertauscht.
  • 2) Durchlauf: Die Zahl 8 wird mit 5,3,2 und 1 verglichen und vertauscht, mit 10 muss nicht mehr verglichen werden.
  • 3) Durchlauf: Die Zahl 5 wird mit 3,2 und 1 verglichen und vertauscht, mit 10, 8 muss nicht mehr verglichen werden.
  • 4) Durchlauf: Die Zahl 3 wird mit 2 und 1 verglichen und vertauscht, mit 10, 8, 5 muss nicht mehr verglichen werden.
  • 5) Durchlauf: Die Zahl 2 wird mit 1 verglichen und vertauscht, mit 10, 8, 5, 3 muss nicht mehr verglichen werden.
  • Ende, die Zahl 1 muss nicht mehr verglichen werden.

Vergleichen und Vertauschen betrachten wir nun als eine Operation (da Konstant).

  • 1) Durchlauf -> 5 Operationen
  • 2) Durchlauf -> 4 Operationen
  • 3) Durchlauf -> 3 Operationen
  • 4) Durchlauf -> 2 Operationen
  • 5) Durchlauf -> 1 Operation

Das sind für ein Array der Länge 6, 15 Operationen.

Die allgemeine Formel lautet

  • 1) (n-1) + (n-2) + ... 1
  • 2) Das ist ja der kleine Gauß minus n: (n * (n + 1))/2 - n
  • 3) (n * (n + 1))/2 - (2*n)/2
  • 4) (n2 + n - 2*n)/2
  • 5) (n2 - n)/2
  • 6) 1/2*(n2 - n)
  • 7) n2 - n -> Konstante Werte werden bei der O - Notation entfernt
  • 8) n2 -> n ist zu n2 vernachlässigbar und wird entfernt

Laufzeit

Somit ist die Laufzeit von Bubblesort O(n2)

Laufzeit Bestcase

Liegt bereits eine sortierte Liste vor, so benötigt Bubblesort nur einen Durchlauf. Dabei erfolgen n - 1 vergleiche. Die Konstante wird entfernt.
Die Laufzeit im Bestcase ist O(n)