DokuWiki - fricklers.org

Trace:

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
repds15:hausaufgaben_loes [2015/09/26 19:18] – [Hashing] 95.222.28.220repds15:hausaufgaben_loes [Unknown date] (current) – removed - external edit (Unknown date) 127.0.0.1
Line 1: Line 1:
-===== Zusätzliche Fragen an Mario - von uns ;) ===== 
  
-==== Fragen und Antworten ==== 
- 
-Bitte nutzt die [[repds15:fragen-antworten|Fragen und Antworten]] für eure Fragen. 
-====== Hausaufgaben (mit Lösungen und Tipps) ====== 
-Diese Seite kann ohne Wiki-Account bearbeitet werden. Teilt eure Lösungen untereinander und korrigiert Fehler, wenn ihr sie findet. 
- 
-===== Neue Aufgaben ==== 
- 
-==== Rekursive Programmierung ==== 
-  - Berechne die Tiefe eines Baums B, der in Kind-Geschwister-Darstellung gegeben ist.  
- 
-Die Tiefe eines Baumes B (und nicht die Tiefe eines Knotens) meint die maximale Tiefe des Baumes B und die ist gleich der Höhe des Baumes. Das ist irritierend, aber man muß sich klarmachen, daß sich nur die Richtung des Weges untescheidet: Der längste Weg von der Wurzel zu einem Blatt bestimmt die Höhe eines Baumes, der umgekehrte Weg die Tiefe. 
- 
-Ein rekursiver Algorithmus zur Ermittlung der Tiefe: 
- 
-<code> 
-global im Baum: maximum = 0 
- 
-int getTiefe(p) 
- if (p == NULL) 
- return 0 
- if (p→LKind == NULL) // p ist Blatt  
- return 0 
- else 
-    return 1 + max( getTiefe(p->LKind), getTiefe(p->RG1), ... getTiefe(p->RGn)) 
-</code> 
- 
-Die letzte Anweisung läßt sich noch genauer notieren: 
- 
-<code> 
-        max = 0 
- for (q=p→LKind; q != null; q=q→RGeschwister) 
- if (q->tiefe > max) 
- max = q->tiefe 
- return 1 + max  
-</code> 
- 
-Alternativ kann man den Algorithmus aus der Vorlesung (Folie 27 von 103 im 2. Foliensatz) 
-voraussetzen und alle Knoten mit ihrer Tiefe markieren: 
- 
-<code> 
-tiefe(wurzel,-1); 
-void tiefe (Knoten *p, int t) 
-    { Knoten *q; t = t+1; 
-    if (p != null) 
-         { p→tiefe = t; 
-         for (q=p→LKind; q != null; q=q→RGeschwister) 
-             tiefe(q,t); }} 
-</code> 
- 
-Die Tiefe wird in einem Attribut des Knotens (hier Zugriff über p-> tiefe) gespeichert. 
-Der Baum habe ein globales Attribut maximum, das mit 0 initialiert ist und die maximale Tiefe 
-speichern soll. Nun genügt eine Traversierung durch den Baum, um das Maximum der Tiefe zu ermitteln. 
- 
-<code> 
-void getTiefe (Knoten* p) 
-{ 
-   Knoten p = Kopf;     // Wurzel 
-   if (p == NULL) 
-    return 0; 
-   for (q=p→LKind; q != null; q=q→RGeschwister) 
-   { 
- getTiefe(q) 
- if (q→tiefe > maximum) 
- maximum = q→tiefe 
-   }} 
-</code> 
- 
-Was spricht für den einen, was für den anderen Weg? 
- 
-  - Gib eine rekursive Implementierung von ''lookup'' in einem Heap H an. Tipp: ''lookup(wo, x)'', wobei ''wo'' die Position im Heap-Array ist. 
-<code> 
-lookupRek(int priority, int wo) 
-{ 
- if(2*wo <= n) 
- { 
- if (H[2*wo] == priority ) 
- print priority 
- else if (H[2*wo] >  priority ) 
- lookupRek( priority, 2*wo); 
- if (H[2*wo+1] == priority ) 
- print priority 
- else if (H[2*wo+1] > priority ) 
- lookupRek( priority, 2*wo+1); 
- } 
-} 
-</code> 
-  - Erweiterung von Übungsblatt 3, Aufgabe 2 (Spielbäume): Statt einem Feld ''Gewinner g'' speichert ein Knoten nun eine Zahl ''int gewinn''. Es gilt: ''gewinn'' > 0, wenn Alice gewinnt und ''gewinn'' < 0, wenn Bob gewinnt. Anfänglich sind nur die Blätter mit den ''gewinn''-Werten markiert. Für alle inneren Knoten ist ''gewinn = 0''. Wer gewinnt das Spiel und wie hoch ist der Gewinn, wenn jeder Spieler nach einem betragsmäßig großen Gewinn strebt? 
- 
-<code> 
-Der MinMax-Algorithmus bleibt erhalten. Nur Details der Implementierung (Datentyp für gewinn) ändern sich. Was ist daran interessant? 
-</code> 
- 
-  - Erweitere die ''lookup(x)''-Funktion eines binären Suchbaums, sodass sie den Pfad von der Wurzel zum Schlüssel zurück gibt, sofern er existiert. 
- 
-==== Datenstrukturen entwerfen ==== 
-  - Eine Schwarz-Weiß-Pixelgrafik mit Länge n und Breite n soll abgespeichert werden. Jedes einzelne Pixel soll in konstanter Laufzeit O(1) auf schwarz bzw. weiß gesetzt werden. Wie viel Speicherplatz benötigen wir in Abhängigkeit von n? Du kannst davon ausgehen, dass die Datenstruktur bereits initialisiert vorliegt. 
-  - Wir überlegen uns eine einfache Komprimierungsmöglichkeit: Wenn viele nebeneinander liegende Pixel die gleiche Farbe haben, dann genügt es, wenn nur einmal die Farbe und die Anzahl der betroffenen Pixel abgespeichert wird. So können wir Speicherplatz sparen. Der Zugriff auf ein Pixel ''p'' darf nun länger dauern, nämlich O(k), wobei k die Anzahl der Pixel links von ''p'' ist. Im besten Fall kann die Laufzeit auch geringer sein. **Tipp**: Was haben diese Aufgabe und ihr Vorgänger mit den Graph-Datenstrukturen aus der Vorlesung gemeinsam? 
-===== Tag 1 ===== 
-==== Mathematische Grundlagen, Asymptotik, Landau-Notation ==== 
- 
-  - Welche Rechenregeln für Logarithmen gibt es? 
-  - Vereinfache die folgenden Terme: 
-    * $\log_2(8) = \log_2(2^3) = 3 \cdot \log(2) = 3 \cdot 1 = 3 \rightarrow \Theta(3) = \Theta(1)$ 
-    * $\log_3(1) = 0$, da $x^0$ mit $x \in \mathbb{N}$ immer 1 ist $\rightarrow \Theta(1)$ 
-    * $\log_2(n^4) = 4 \cdot log_2(n) \rightarrow \Theta(log \ n)$ 
-    * $\log_{10}\left(\frac{1}{n^2}\right) = log_{10}(1) - log_{10}(n^2) = 0 - 2 \cdot log_{10}(n)$, **Anmerkung**: $\Theta$-Schreibweise entfernt; O-Notation haben wir nur für Funktionen $f:\mathbb{N} \to \mathbb{R}_{\geq 0}$ definiert.  
-    * $2^{\log_4(n)} = 2^{(log_4 2) \cdot (log_2 n)} = 2^{(log_2 n) \cdot (log_4 2)} = n^{log_4 2} = n^{1/2} = \sqrt n \rightarrow \Theta(\sqrt n)$ 
-    * $\log_n(n) = 1 \rightarrow \Theta(1)$, da $\forall x \in \mathbb{N}: x^1=x$ 
-    * $\log_a(b) \cdot \log_b(n)$ ist der Basiswechsel (siehe Formel) rückwärts, s.d.: $\log_a(b) \cdot \log_b(n) = log_a n \rightarrow \Theta(log \ n)$ 
-    * $\log_2(4n^3)=log_2(4)+log_2(n^3)=2+3\cdot log_2(n) \rightarrow \Theta (log \ n)$ 
-    * $\log_2(\sqrt{n})=1/2 \cdot log(n^{1/2}) = 1/2 \cdot log_2(n) \rightarrow \Theta(log \ n)$ 
-    * $\log_2\left( \sum_{i=0}^n i \right) =\log_2\left( \frac{n\cdot(n+1)}{2} \right) = \log_2(n) + \log_2(n+1) - \log_2(2) = \Theta(\log(n)) $  
-    * $\log_3\left(\prod_{i=1}^n 3^i \right) = \sum_{i=1}^n \log_3\left(3^i \right) = \sum_{i=1}^n i \log_3(3) =  \sum_{i=1}^n i = \frac{n\cdot(n+1)}{2}$ 
-    * $\sum_{i=1}^n n \cdot i = n \cdot \sum_{i=1}^n i = n \cdot \frac{n \cdot(n+1)}{2} \rightarrow \Theta(n^3)$ 
-    * $\sum_{k=0}^n 2^{-k} = 2- \frac{1}{2^n} \leq 2 = \Theta(1)$ 
-    * $\sum_{k=1}^{\frac{n}{2}} 4^{-k} = (\sum_{k=0}^{\frac{n}{2}} 4^{-k}) - 1 = (\frac{1-(\frac{1}{4})^{(1+n/2)}} {1-\frac{1}{4}})-1 < \frac{1}{\frac{3}{4}}-1 = \frac{1}{3} = \Theta(1)$ 
-    * $\sum_{i=1}^n \frac{2}{i} = O(log(n))$, da $\int \frac{1}{x} dx = \ln(x) + C$ 
-  - Wahr oder falsch? Begründe deine Antwort. 
-    * $1 = \Theta(3)$, wahr 
-    * $2 = \Omega(1)$, wahr 
-    * $3 = \omega(2)$, falsch 
-    * $n = \omega(1)$, wahr, denn $\lim_{n\to \infty} \frac{n}{1} = \infty$. 
-    * $f = \mathcal{O}(g)$ und $g = \mathcal{O}(f) \iff f = \Theta(g)$, wahr, siehe Definition 
-    * $\sum_{k=0}^n 2^{-k} = \mathcal{O}(1)$, wahr, geometrische Summe 
-    * $\log_2(n) = \Theta(\log_8(n))$, wahr, denn $\log_2(n) = \log_2(8) \cdot \log_8(n)$. 
-    * $2^{\log_2(2n)} = \mathcal{O}(n^2)$, wahr, denn $2^{\log_2(2n)} = 2^{1 + \log_2(n)} = 2 \cdot 2^{\log_2(n)} = 2n$ 
-    * $f_1 = \Omega(g_1)$ und $f_2 = \Omega(g_2) \implies f_1 + f_2 = \Omega(g_1 + g_2)$, wahr, einfach in die Definition der Omega-Notation einsetzen. 
-    * $f = o(g) \iff f = \mathcal{O}(g)$, "$\implies$" ist wahr, "$\impliedby$" ist falsch. 
-    * $f(n) = g(\frac{n}{2}) \implies f = \Omega(g)$, falsch, Gegenbeispiel: $g(n) = 2^n$. 
- 
-==== Pseudocode und Laufzeitanalyse ==== 
-Welchen Wert hat k in Abhängigkeit von n nach der Ausführung des Programms? 
-  - <code> 
-k = 1 
-for i=1,2,...,n 
-    k = k + 1 
-</code> $k = \Theta(n)$ 
-  - <code> 
-k = 1 
-for i=1,2,...,n 
-    k = k + 1 
-for j=1,2,...,100 
-    k = k + 2 
-</code> $k = \Theta(n)$ 
-  - <code> 
-k = 1 
-for i=1,2,...,n*n 
-    k = k + 2 
-</code> $k = \Theta(n^2)$ 
-  - <code> 
-k = 1 
-for i=n,n/2,n/4...,1 
-    k = k + 1 
-</code> $k = \log_2(n)$ 
-  - <code> 
-k = 1 
-for i=1,2,...,n 
-    k = k + i 
-</code> $k = 1 + \sum_{i=1}^n i = \Theta(n^2)$ 
-  - <code> 
-k = 1 
-while i < n 
-    i = i + i 
-    k = k + 1 
-</code> Tipp: i verdoppelt sich in jedem Schritt. Wie oft kann es sich verdoppeln bis $i \geq n$ gilt? 
-  - <code> 
-i = 0 
-k = 0 
-while i < n 
-    i = i + k 
-    k = k + 1 
-</code> 
-  - <code> 
-k = 1 
-for i=sqrt(n), 2sqrt(n),...,n 
-   k = k + 1 
-</code> $k = \Theta(\sqrt{n})$, denn $i$ erhöht sich mit Schrittweite $\sqrt{n}$. Nach $\sqrt{n}$ Schritten erreichen wir die obere Schranke $n$ und brechen die for-Schleife ab. 
- 
-Umgekehrt: Schreibe Pseudocode mit den folgenden Laufzeiten: 
-  - $\Theta(1)$ 
-  - $\Theta(n)$ 
-  - $\Theta(n^3)$ 
-  - $\Theta(2^n)$, erzeuge einen vollständigen Binärbaum der Tiefe n-1, denn dieser Baum hat $\Theta(2^n)$ Knoten. Tipp: Schreibe ein rekursives Programm, dass als Parameter die Tiefe des Baumes erhält.     
-  - $\Theta(\sqrt{n})$ 
-    <code> 
-    i = 1 
-    k = 1 
-    while i <= n 
-        i = i + k 
-        k = k + 1 
-    </code> $i = \Theta(k^2)$ (Gauß-Summe), d.h. die Zählvariable i wächst mit quadratischer Geschwindigkeit und erreicht nach $\Theta(\sqrt(n))$ Schritten die Schranke n. 
-  - $\Theta(\log{n})$ 
-  - $\Theta(\log{n^3})$ 
-  - $\Theta(n!)$, gib alle möglichen Permutationen der Zahlen 1,2,...,n aus. 
- 
-**Billiglösung, die immer funktioniert:** Pseudocode mit der Laufzeit $\Theta(f(n))$: 
-<code> 
-for i = 1, 2, ..., f(n) 
-    print "hallo" 
-</code> 
- 
-Was tut der folgende Pseudocode? Angenommen $n = 2^k$ für ein beliebiges $k \in \mathbb{N}$. 
-<code> 
-function raetsel(Array A[0,..,n-1], int l, int r) 
-{ 
-    if l == r  
-    { 
-        return A[l] 
-    } 
-    else 
-    { 
-        grenze = floor( (l+r)/2 )        // untere Gaußklammer 
-        m_l = raetsel(A, l, grenze) 
-        m_r = raetsel(A, grenze + 1, r) 
-        if m_l > m_r 
-        { 
-            return m_l 
-        } 
-        else 
-        { 
-            return m_r 
-        } 
-    } 
-} 
-</code> 
-Welche Ausgabe liefert raetsel(A, 0, 7) für A = [1,5,3,10,2,11,9,4]? 
- 
-**Lösung:** raetsel(A, l, r) geht so ähnlich vor wie die binäre Suche, aber die Funktion liefert den größten Wert aus den Zahlen A[l], A[l+1], ..., A[r]. Das kann man sich folgendermaßen verdeutlichen: Im Basisfall enthält das zu prüfende Intervall nur eine einzige Zahl, die wir zurückgeben. Klar: Das muss das Maximum sein. Im Rekursionsfall teilen wir das Intervall in zwei Hälften und erhalten aus der linken Hälfte (laut Induktionsannahme) das "linke" Maximum m_l und aus der rechten Hälfte das "rechte" Maximum m_r. Die darauf folgende if-Abfrage bewirkt, dass das Maximum des gesamten Intervalls max{m_l, m_r} zurückgegeben wird. 
-{{:repds15:raetsel-baum.png}} 
- 
-==== Rekursionsgleichungen aufstellen und lösen ==== 
-Aufstellen: 
-  - <code> 
-def f(n): 
-    if n == 1 
-        return 1 
-    else 
-        return n + f(n-1) 
-</code> 
-  - <code> 
-def g(n): 
-    if n > 1: 
-        return g(n/2) 
-    else 
-        return 0 
-</code> 
- 
-Lösen: 
-  - $T(1) = 0$ und $T(n) = 2 T(\frac{n}{2}) + \frac{n}{2}$ für $n > 1$ 
-  - $T(2) = 1$ und $T(n) = T(\frac{n}{2}) + n - 1$ für $n > 2$ 
- 
-==== Arrays ==== 
-  * Was ist ein Array? 
-  * Wie sieht ein Array im Arbeitsspeicher aus? 
-  * Wie lang dauert ein Zugriff (lesen oder schreiben) auf das n. Element A[n]? Wieso? 
-  * Schreibe ein Programm in Pseudocode, das ein gegebenes Array A[1,...,n] mit den Werten 2, 4,..., 2n befüllt.  
-<code> 
-for i = 1, 2, ..., n 
-    A[i] = 2*i 
-</code> 
- 
-  * Schreibe ein Programm in Pseudocode, das die Summe der Einträge dieses Arrays ausgibt. 
-<code> 
-sum = 0 
-for(i = 1; i <= n; i++) 
-    sum = sum + A[i] 
-return sum 
-</code> 
- 
-  * Entwerfe eine Datenstruktur, die Zahlen aus dem Intervall {0,1,...,n-1} speichert und die Operationen ''lookup(x)'', ''insert(x)'' und ''remove(x)'' in der Laufzeit O(1) unterstützt. 
-Idee: Das Intervall ist schon gegeben, d.h. es müssen keine "unerwarteten" Zahlen in der Datenstruktur verwaltet werden. Wir wählen ein Array A[0,...,n-1], da Lese- und Schreibzugriffe in konstanter Zeit O(1) möglich sind. A ist ein **Bitvektor**, also A[x] = 1, wenn x in unserer Datenstruktur vorhanden ist und A[x] = 0, sonst.  
-<code> 
-def lookup(x): 
-    return A[x] 
-     
-def insert(x): 
-    A[x] = 1 
-     
-def remove(x): 
-    A[x] = 0 
-</code> 
-==== Listen ==== 
-  * Was ist eine Liste? 
-  * Wie sieht eine Liste im Arbeitsspeicher aus? 
-  * Wie unterscheiden sich Arrays und Listen? 
-  * Welche Zeiger und Funktionen hat die Listen-Implementierung aus der Vorlesung zur Verfügung? 
-  * Wie lange dauert es, das größte Element einer aufsteigend (absteigend) sortierten Liste zu finden? 
-**aufsteigend**: O(n), wobei n die Länge der Liste ist, da wir uns vom head-Element bis zum letzten Element durchhangeln müssen. 
-**absteigend**: O(1) mit folgendem Code: 
-<code> 
-L.movetofront()    // current-Zeiger auf das head-Element zurücksetzen 
-return L.read() 
-</code> 
- 
-  * Wie lange dauert das Einfügen der Zahl 7 in eine (un)sortierte Liste? 
-**unsortiert**: O(1), mit L.insert(7) wird die 7 nach dem current-Element eingefügt. 
- 
-  * Wieso ist binäre Suche auf Listen keine gute Idee? 
-Wir können dort nicht "navigieren", da wir keine Indizes haben. Es ist "schwierig", die Mitte zwischen zwei Elementen zu finden bzw. noch nicht einmal ohne Weiteres möglich, zu entscheiden, welches der beiden Elemente links bzw. rechts liegt. 
- 
-  * Schreibe ein Programm in Pseudocode, das beide Aufgaben (sortiert und unsortiert) erledigt. 
-**unsortiert**: 
-<code> 
-def insert_unsortiert(x): 
-    L.insert(x) 
-</code> 
-**sortiert (aufsteigend)**: 
-<code> 
-def insert_sortiert(x): 
-    L.movetofront()         // verschiebe den current-Zeiger auf das head-Element 
-    while not L.end():      // alternativ: for(Element curr = L.head; curr = curr.next; curr != null) {...} 
-        nachfolge_wert = L.read() 
-        if nachfolge_wert >= x: 
-            L.insert(x)     // füge x nach dem current-Element ein 
-            return;         // Ende der Funktion 
-        else 
-            L.next()        // verschiebe den current-Zeiger auf das nächste Element 
-    L.insert(x)             // Füge x als letztes Element ein (hinten anhängen) 
-</code> 
- 
-  * Die Datenstruktur Liste aus der Vorlesung ist eine einfach verkettete Liste, d.h. jedes Element hat nur einen Zeiger ''next'' auf seinen Nachfolger in der Liste. Erweitere die Datenstruktur, sodass auch jeder Vorgänger über einen Zeiger erreicht werden kann. Welche Veränderungen ergeben sich für die Funktionen ''insert'' und ''remove''? 
-==== Stacks ==== 
-  * Was ist ein Stack? 
-  * Wie lange dauert ein Zugriff auf das oberste/unterste Element? 
-  * Beschreibe, wie mit einer Liste ein Stack implementiert werden kann. 
-<code> 
-def push(x): 
-    L.movetofront()    // nicht unbedingt nötig (current-Zeiger auf das Head-Element verschieben) 
-    L.insert(x)        // x ist nun das erste (= oberste) Element 
-     
-def pop(): 
-    L.movetofront()    // nicht unbedingt nötig 
-    L.remove()         // entferne das oberste Element 
-</code> 
- 
-  * Schreibe ein Programm, das einen Stack A als Eingabe erhält und den Stack B ausgibt, wobei B die Elemente aus A in umgekehrter Reihenfolge enthält. 
-  * Erweitere die Datenstruktur Stack um die Funktion ''bottom()'', die das unterste Element des Stacks in Laufzeit O(1) ausgibt. 
-Benutze eine private Hilfsvariable ''bot'' und ergänze die Funktion ''push(x)'' folgendermaßen:  
-<code> 
-def push(x): 
-    if S.empty(): 
-        bot = x 
-        push_alt(x)    // benutze nun die konventionelle push-Funktion 
-         
-def bottom(): 
-    if not S.empty(): 
-        return bot 
-    else 
-        "FEHLER" 
-</code> 
- 
-  * Bonusfrage: Wie werden Stacks in Programmiersprachen beim Aufruf von Funktionen verwendet? [[http://insecure.org/stf/smashstack.html|Smashing The Stack For Fun And Profit]] 
- 
-==== Queues ==== 
-  * Was ist eine Queue? 
-  * Befolgen Queues das Prinzip LIFO oder FIFO? 
-FIFO (First in, first out). 
- 
-  * Beschreibe die Situation im Supermarkt an der Kasse mithilfe einer Queue. 
-  * Wie würde sich das Einkaufen verändern, wenn Supermarkt-Kassen wie Stacks funktionieren würden? 
-Wenn eine neue Kasse geöffnet wird, dann sollte man sich möglichst ans Ende der Warteschlange anstellen, damit man als erste Person abkassiert wird.  
- 
-  * Welche Algorithmen aus der Vorlesung verwenden Queues? Wieso? 
-  * Schreibe ein Programm in Pseudocode, das eine Liste L erhält und den Wert jedes Listenelements in eine Queue einfügt. 
-<code> 
-def list_to_queue(Liste L): 
-    Q = new Queue() 
-    L.movetofront() 
-    while not L.empty(): 
-        data = L.read() 
-        Q.enqueue(data) 
-        L.remove() 
-</code> 
- 
-  * Lösche das jüngste Element aus einer Queue Q, ähnlich wie ''pop()'' bei Stacks. 
-<code> 
-H = new Queue()  // Hilfsqueue erzeugen 
-while not Q.empty(): 
-    data = Q.dequeue() 
-    if not Q.empty():     // data ist nicht das jüngste Element 
-        H.enqueue(data) 
-Q = H                     // statt alles wieder nach Q zu schaufeln, können wir auch einfach Q durch H ersetzen 
-</code> 
- 
----- 
- 
-===== Tag 2 ===== 
-==== Bäume ==== 
-  * Was ist ein Baum? 
-ein zusammenhängender Graph ohne einfache Kreise 
- 
-  * Was ist ein (vollständig) binärer, ternärer, k-ärer Baum? 
-Ein vollständiger binärer Baum der Tiefe t ist ein binärer Baum, in dem alle Blätter die Tiefe t besitzen und alle inneren Knoten wie auch die Wurzel den Aus-Grad genau 2 besitzen. 
- 
-  * Kann die Datenstruktur Liste auch als Baum aufgefasst werden? Ist ein Baum nur eine verzweigte Liste? 
-ja und ja, man kann beides so auffassen (je nach Implementierung) 
- 
-  * Welche Datenstrukturen für allgemeine Bäume kennen wir? 
-Eltern-Array, Binärbaum (bzw. k-är)-Baum mit Zeigern auf das linke und rechte bzw. auf Kind_1, Kind_2,..., Kind_k, Kind-Geschwister-Darstellung 
- 
-  * Entwerfe eine Datenstruktur für einen ternären Baum. Wieso sind Eltern-Arrays nicht geeignet? 
- 
-<code> 
-struct Knoten 
-{    schluesseltyp schluessel;  
-     infotyp info; // Nutzdaten 
-     Knoten *links, *mitte, *rechts; 
-...  }; 
-</code> 
- 
-Eltern-Arrays: Es kommt auf die geforderten Operationen an. Was spricht dagegen, ein Verzeichnisbaum (directory) als Eltern-Array zu speichern? Die Laufzeit der meisten Operationen (z.B. dir) ist O(n), 
-die Ermitllung des kompletten Pfades von der Wurzel aus O(1). 
- 
-  * Gib eine rekursive Definition für Bäume an: Wie sieht ein Baum im Basisfall (Rekursionsanfang) aus? Wie sieht der Rekursionsfall (Rekursionsschritt) aus? 
-Basisfall: Der Baum ist ein Blatt 
-Rekursionsfall: Der Baum ist ein Knoten mit Kindern, die wiederum Bäume sind. 
-==== Graphen ==== 
-  * Welche Datenstrukturen kennen wir für Graphen? 
-Adjazenzmatrix, Adjazenzliste (und noch die speziellen Datenstrukturen für Bäume) 
- 
-  * Nenne Vor- und Nachteile dieser Datenstrukturen. 
-Tipp: Überlegt euch, wie lange es jeweils dauert, verschiedene Aufgaben zu lösen wie etwa "alle Nachbarn eines Knotens ausgeben", "neue Kante einfügen", "neuen Knoten einfügen", "Zugriff auf die Wurzel", "Zugriff auf den Elternknoten" usw. 
-==== Tiefensuche, Breitensuche, Prä-/In-/Postorder, Topologisches Sortieren ==== 
-  * Führe Tiefen- und Breitensuche an einem selbst gewählten Beispiel aus. Dabei soll jeder Knoten seine Nachbarn in aufsteigender Reihenfolge speichern. 
-  * Wieso müssen wir uns bei der Tiefen- und Breitensuche in allgemeinen Graphen merken, welche Knoten wir schon besucht haben? 
-  * Führe Prä-/In-/Postorder auf einem selbst gewählten Baum aus.  
-[[https://en.wikipedia.org/wiki/Tree_traversal|Wikipedia (en.) - Tree Traversal]], Achtung: Bei Inorder auf Bäumen mit mehr als zwei Kindern  
- 
-  * Finde einen Baum, bei dem alle drei Traversierungen das gleiche Ergebnis liefern. 
-Der Baum ist ein Blatt. 
- 
-  * Gib eine rekursive Implementierung der Postorder-Traversierung an. Orientiere dich an Algorithmus 4.1 (S. 96 im Skript). 
-  * Was hat die Tiefensuche auf Bäumen mit Prä-/In-/Postorder zu tun? 
-  * Welche Laufzeit hätten die Tiefen- und die Breitensuche, wenn der Graph als Adjazenzmatrix statt als Adjazenzliste vorliegen würde? 
-Beim Besuch jedes Knotens müssen wir all seine Nachbarn überprüfen (wurden sie schon besucht?). Die Frage ist daher: Wie lange dauert es pro Knoten, all seine Nachbarn zu finden in der Adjazenzmatrix und in der Adjazenzliste? Adjazenzmatrix: $O(|V|)$, Adjazenzliste: $O(\deg(v))$. Wie lange dauert es dann insgesamt für alle Knoten? 
-Tipp: Mit dem Handshake-Lemma (doppeltes Abzählen) folgt dann, dass die Summe aller Knotengrade das doppelte der Kantenzahl ist: $\sum_{v \in V} \deg(v) = 2 \cdot |E|$ 
- 
-  * Benutzt die (nicht rekursive) Tiefensuche einen Stack oder eine Queue? Wie sieht es mit der Breitensuche aus? 
-Tiefensuche: Stack 
-Breitensuche: Queue 
- 
-  * Unter welchen Voraussetzungen kann ein Graph topologisch sortiert werden? 
-Wenn er keine Kreise hat. 
- 
-  * Ist diese Sortierung immer eindeutig? 
-nein. 
-  * Gib eine topologische Sortierung an einem selbst gewählten Beispiel an. 
- 
-==== Heaps ==== 
-  * Was ist ein Heap? 
-  * Was ist der Unterschied zwischen Min- und Max-Heaps? 
-die Heap-Ordnung: Wurzel ist das kleinste bzw. größte Element. 
- 
-  * In welcher Laufzeit kann bei einem Min-Heap (Max-Heap) auf das kleinste Element zugriffen werden? 
-O(1) durch ''return H[1]'' 
- 
-  * Was ist die Heap-Ordnung? 
-  * Richtig oder falsch: Ein Heap ist ein binärer Suchbaum? 
-Im Allgemeinen nein. 
- 
-  * Gib ein Beispiel für einen Heap an, der ein binärer Suchbaum ist. 
-Jeder Heap, der nur einen Knoten enthält ist ein binärer Suchbaum. Es gibt allerdings auch Min- und Max-Heaps mit zwei Knoten, die die Suchbaum-Eigenschaft erfüllen. 
- 
-  * Füge eine selbst gewählte Folge von acht Zahlen in einen Min-Heap ein.  
-  * Entferne alle Zahlen mit ''delete_min()'' und gib die Reihenfolge an. Was fällt dir auf? 
-[[https://www.cs.usfca.edu/~galles/visualization/Heap.html|Heap-Animation]] 
- 
-  * Benutze diese Beobachtung für folgendes: Schreibe ein Programm in Pseudocode, das ein Array A[1,...,n] als Eingabe erhält und dessen Elemente in aufsteigender Reihenfolge ausgibt. 
-<code> 
-// Das ist Heap-Sort 
- 
-H = new MinHeap() 
-for i=1,2,...,n 
-    H.insert(A[i])    // füge alle Elemente des Arrays in den Heap ein 
-for i=1,2,...,n 
-    min = H.delete_min() 
-    print min 
-</code> 
-**Laufzeit-Analyse**: Der Heap wächst mit jeder ''insert''-Operation um ein Element. Die Laufzeiten steigen somit im schlimmsten Fall: $O(\log(1)), O(\log(2)),..., O(\log(n))$. Umgekehrt verhält es sich mit jedem ''delete_min''. Wie muss ein solches Array aussehen, damit bei jedem ''insert'' ein ''repair_up'' bis zur Wurzel hinauf ausgeführt werden muss? Konstruiere ein solches Worst-Case-Beispiel. Heapsort hat also eine Laufzeit von $O(\sum_{i=1}^n \log(1))$. Eine "ungenauere" obere Abschätzung für die Laufzeit ist $\sum_{i}^n \log(n) = O(n\log(n))$. Ist das im Worst-Case zu ungenau? Nein, denn allein für jeden Wert A[n/2], A[n/2+1],..., A[n] benötigt ''insert'' die Laufzeit $\sum_{i=n/2}^n \log(n/2) = \Omega(n \log(n))$, (Achtung: Die Omega-Abschätzung gilt nur, wenn A eine Worst-Case-Eingabe ist. Im besten Fall kann es weniger sein, z.B. für die Eingabe 1,1,1,1,1,...).  
- 
-  * Wahr oder falsch: Wenn wir die Folge der einzufügenden Zahlen schon vorher kennen, kann das Einfügen aller Zahlen in den Heap in linearer Laufzeit realisiert werden. 
-wahr, mit der buildheap-Funktion 
- 
-  * Entwerfe einen Heap, der als ternärer Baum aufgefasst werden kann. 
-Speichere die Kinder von H[i] an den Positionen H[3i], H[3i+1], H[3i+2]. 
-==== Binäre Suchbäume ==== 
-  * Was ist ein binärer Suchbaum? 
-  * Können auch Buchstaben in einem binären Suchbaum verwaltet werden? 
-  * Welche Tiefe hat ein binärer Suchbaum mit n Schlüsseln im besten und im schlechtesten Fall? 
-  * Was passiert bei ''remove(5)'', wenn der Baum den Schlüssel 5 gar nicht enthält? 
-Nichts oder Fehlermeldung 
-  * Was passiert bei ''insert(5)'', wenn der Baum den Schlüssel 5 bereits enthält? 
-Nichts oder Fehlermeldung (Achtung: andere Implementierungen, z.B. die aus der Datenstrukturen-Visualisierungswebsite fügen auch doppelte Schlüssel ein). 
-  * Gib einen binären Suchbaum mit zehn Knoten an. 
-  * Entferne die Wurzel. 
-  * Entferne einen weiteren inneren Knoten 
-      * mit einem Kind. 
-      * mit zwei Kindern. 
-  * Entferne ein Blatt. 
-  * In welcher Laufzeit kann die Folge 1,2,...,n/2 in einen binären Suchbaum eingefügt werden? 
-Bei dieser Folge landet ''insert(i)'' in Tiefe $\Theta(i)$. Damit ergibt sich die Laufzeit $\sum_{i=1}^{n/2} i = \frac{n/2 \cdot (n/2 + 1)}{2} = \Theta(n^2)$ 
-  * Gib eine Folge an, die in möglichst geringer Laufzeit eingefügt werden kann. Tipp: Breitensuche auf Bäumen. 
-1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ..., 1: Jedes ''insert(1)'' geht in Laufzeit O(1). Der Baum besteht immer nur aus einem einzigen Knoten.  
-==== AVL-Bäume ==== 
-  * Was ist ein AVL-Baum? 
-  * Wahr oder falsch: Jeder AVL-Baum ist ein binärer Suchbaum. 
-  * Wahr oder falsch: Jeder binäre Suchbaum ist ein AVL-Baum. 
-  * Wozu dienen die Rotationen? 
-  * Was ist die Tiefe eines AVL-Baums mit n Knoten im besten und im schlechtesten Fall? 
-  * Welche Nachteile hat die Verwendung von AVL-Bäumen gegenüber üblichen binären Suchbäumen? 
-  * Füge die Zahlen 1,2,...,15 in einen anfangs leeren AVL-Baum ein. Gib bei Balance-Grad-Verletzungen außerdem an, ob es sich um einen ZickZick, ZickZack, ZackZick oder ZackZack-Fall handelt. 
- 
-==== (a,b)-Bäume ==== 
-  * Was ist ein (a,b)-Baum? 
-  * Was bedeutet das a? 
-  * Was bedeutet das b? 
-  * Wie viele Kinder hat ein innerer Knoten, der 3 Schlüssel speichert? 
-  * Füge die folgenden Zahlen in einen anfangs leeren (2,3)-Baum ein: 1, 4, 6, 9, 5, 11, 2, 3, 12, 10, 7, 8. Lösche anschließend wieder alle Schlüssel in der gleichen Reihenfolge. 
- 
----- 
- 
-===== Tag 3 ===== 
- 
-==== Hashing ==== 
-  * Was ist Hashing? 
-  * Was ist eine Kollision? 
-  * Welche Laufzeit haben ''insert(x)'', ''remove(x)'' und ''lookup(x)'' im besten und im schlechtesten Fall? 
- 
-**Insert** 
- 
-^ case:                | best | average | worst |   
-^ mit Verkettung       | O(1) | O(1)+$\lambda$  | O(n)  |   
-^ lineare Adressierung | O(1) | O(1)+$\lambda$  | O(n)  |  
-^ doppeltes Hashing    | O(1) | O(1/1-$\lambda$)| O(n)  |  
- 
-**Remove** 
- 
-^ case:                | best | average | worst |   
-^ mit Verkettung       | O(1) | O(1)+$\lambda$  | O(n)  |   
-^ lineare Adressierung | O(1) | O(1)+$\lambda$  | O(n)  |  
-^ doppeltes Hashing    | O(1) | O(1/1-$\lambda$)| O(n)  |  
- 
-**Lookup** 
- 
-^ case:                | best | average | worst |   
-^ mit Verkettung       | O(1) | O(1)+$\lambda$    | O(n)  |   
-^ lineare Adressierung | O(1) | O(1)    | O(1)  |  
-^ doppeltes Hashing    | O(1) | O(1)    | O(1)  |  
- 
-  * Berechne $3x \mod 5$ für alle $x \in \{0, 1, 4, 5, 10, 12\}$. 
-  * Fülle eine Wertetabelle für $f(x) := 3x \mod 11$ 
-  * Füge die Schlüssel 9, 20, 31, 42, 53 in eine Hash-Tabelle ein und gib jeweils die Reihenfolge der getesteten Zellen an: 
-    * Hashing mit Verkettung 
-    * Hashing mit linearem Austesten  
-    * doppeltes Hashing mit $h(x) := f(x) + i\cdot g(x) \mod 11$, wobei $g(x) := 5 - (x \mod 5)$. 
-    * Was ist der Auslastungfaktor $\lambda$ bei den beiden letzten Verfahren? Hängt er davon ob, ob linear ausgetestet oder doppelt gehasht wurde? 
- 
-^ x   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 
-^ f(x)|                        | 
- 
-^ x   | 0 | 1 | 2 | 3 | 4 |  
-^ g(x)|            
- 
-==== Rekursive Programmierung ==== 
-Löse die folgenden Aufgabenstellungen mit rekursiven Programmen. Was ist jeweils der Basisfall und was ist der Rekursionsfall? 
- 
-  - Maximum einer Liste finden 
-    * Eingabe: Liste L 
-    * Ausgabe: Wert eines größten Listenelements 
-    * <code> 
-// Variante 1:  
-// Basisfall: Liste besteht nur aus einem einzigen Element 
-// Rekursionsfall: Liste besteht aus einem Element, das auf eine weiter Liste zeigt 
- 
-def max1(Element e): 
-    if(e.next == NULL)       // Basisfall 
-        return e.wert 
-    else                     // Rekursion 
-        max_restliste = max1(e.next) 
-        if (e.wert > max_restliste) 
-            return e.wert 
-        else 
-            return max_restliste 
------------------------------------------------- 
-             
-// Variante 2:  
-// Basisfall: leere Liste 
-// Rekursionsfall: Liste besteht aus einem Element, das auf eine weiter Liste zeigt 
- 
-def max2(Element e): 
-    if(e == NULL)          // Basisfall 
-        return -∞  
-    else                   // Rekursion 
-        max_restliste = max1(e.next) 
-        if (e.wert > max_restliste) 
-            return e.wert 
-        else 
-            return max_restliste 
-</code> 
-  - Blätter eines Binärbaums zählen 
-    * Eingabe: Zeiger root auf den Wurzelknoten eines Binärbaums 
-    * Ausgabe: eine natürliche Zahl k > 0, wenn der Binärbaum k Blätter hat 
-    * <code> 
-def count(Knoten v): 
-    if (v == NULL) 
-        return 0 
-    else  
-        if (v.links == NULL und v.rechts == NULL)     
-laufffähiger Code in C++ 
-            // Basisfall: v ist ein Blatt 
-            return 1                
-        else         
-            // Rekursionsfall: v mindestens einen Binärbaum als Kind 
-            return count(v.links) + count(v.rechts)               
-</code> 
-  - größte vollständige Teilbäume finden 
-    * Eingabe: Zeiger root auf den Wurzelknoten eines Binärbaums 
-    * Ausgabe: Zeiger auf einen Knoten, der die Wurzel eines vollständigen Binärbaums mit möglichst vielen Blättern ist 
- 
-**Pseudocode** 
-<code> 
-im Baum: maximum = 0, maxNode = leer 
-im Knoten: zaehler = 0 
- 
-def maxFullBinTree() // Rahmenprozedur 
- markFullBinTree(Wurzel des Binärbaums) 
- return maxnode = Zeiger auf den Knoten des größten vollständigen Teilbaums 
- 
-def markFullBinTree(Knoten) 
- wenn Knoten leer 
- return 
- wenn Knoten ein Blatt 
- zaehler = 1 
- wenn Knoten ein Halbblatt (nur ein Kind) 
- return 
- sonst hat der Knoten zwei Kinder 
- markFullBinTree(Knoten.links) 
- markFullBinTree(Knoten.rechts) 
- wenn beide Teilbäume gleich groß  
- zaehler =  Knoten.links.zaehler + Knoten.rechts.zaehler + 1 
- wenn zaehler > maximum 
- maximum = zaehler 
- maxNode = Knoten 
-return 
-</code> 
-**laufffähiger Code in C++** 
-<code> 
-struct Knoten 
-{    schluesseltyp schluessel; 
-     infotyp info; // Nutzdaten 
-     int    count; //     Knotenzähler, wird mit 0 initialisiert 
-     Knoten *links, *rechts; 
-...  }; 
- 
-class bsbaum 
-{ 
-private: 
-  Knoten* Kopf; // Wurzel 
-  Knoten* maxNode; // Größe des Teilbaums global speichern 
-  int maximum; // Zeiger auf diesen  Teilbaums global speichern 
-...  }; 
- 
- 
-Knoten* bsbaum::maxFullBinTree()   // Rahmenprozedur 
-{ 
- Knoten* Zeiger = Kopf; 
- markFullBinTree(Zeiger); 
- cout << endl << "Anzahl innere Knoten: " << maximum << endl; // zum testen 
- return maxNode; 
-} 
- 
-void bsbaum::markFullBinTree(Knoten* Zeiger)    // markiert Knoten mit Anzahl der Knoten im vollst. Teilbaum 
-{ // in Variable count und ermittelt größten Teilbaum 
-        if (Zeiger == NULL) // leerer Zeiger 
- return; 
- else if ((Zeiger->links == NULL) && (Zeiger->rechts == NULL)) 
- { // Blatt 
- Zeiger->count = 1; // wird gezählt 
- return; 
- } 
- else if (!((Zeiger->links != NULL) && (Zeiger->rechts != NULL))) 
- { // HalbBlatt 
- return; // kein vollständiger Teilbaum 
- } // nichts zu tun 
- else // innerer Knoten mit 2 Kindern 
-    { 
- markFullBinTree(Zeiger->links); // markiere linken Teilbaum 
- markFullBinTree(Zeiger->rechts); // markiere rechten Teilbaum 
- int lb = Zeiger->links->count;  // Knoten im linken, vollständigen TB 
- int rb = Zeiger->rechts->count; // Knoten im rechten, vollständigen TB 
- if  (lb == rb) // wenn Teilbäume gleich groß 
- Zeiger->count = lb + rb + 1;    // zähle ihre Knoten 
- if (Zeiger->count > maximum) // wenn Teilbaum größer als der bisher ermittelte TB 
- { 
- maximum = Zeiger->count; // Größe des Teilbaums global speichern 
- maxNode = Zeiger; // Zeiger auf diesen  Teilbaums global speichern 
- } 
- return; 
- } 
-} 
-</code> 
-==== Datenstrukturen entwerfen ==== 
-  - Entwirf eine Wörterbuch-Datenstruktur, die für die gegebene Menge M := {0, 1,..., n-1} folgende Laufzeiten ermöglicht: 
-    * ''lookup(x)'': O(1) 
-    * ''insert(x)'': O(1) 
-    * ''remove(x)'': O(1)  
-    - **Lösung**: Siehe [[repds15:hausaufgaben_loes#arrays|Arrays]] 
-  - Ein Projekt besteht aus einer geordneten Aufzählung von beliebig vielen Aufgaben. Jede Aufgabe hat eine Bearbeitungszeit. Die Bearbeitungszeit eines Projekts ist die Summe der Bearbeitungszeiten seiner Aufgaben. Entwirf eine Datenstruktur, die uns bei der Planung der Projekte unterstützt. Es soll jeweils das Projekt mit der geringsten Bearbeitungszeit mit der höchsten Priorität durchgeführt werden. Anschließend ist das Projekt abgeschlossen und muss nicht mehr von der Datenstruktur verwaltet werden. Außerdem sollen neue Projekte eingefügt werden können. Der Einfachheit halber setzen wir voraus, dass die Bearbeitungszeiten der Projekte eindeutig sind und dass zu jedem Projekt ''p'' die Bearbeitungszeit in der Variable ''p.zeit'' gespeichert werden kann. Zu Beginn ist ''p.zeit'' nicht initialisiert. Realisiere die folgenden Laufzeiten: 
-    * ''hinzufuegen(p)'': O(|p| + log(k)), wobei |p| die Anzahl der Aufgaben des Projekts p und k die Anzahl der momentan verwalteten Projekte ist. Die Funktion soll das neue Projekt p in die Datenstruktur einfügen. 
-    * ''naechstes()'': O(log(k)). Die Funktion soll das Projekt ausgeben, das als nächstes bearbeitet werden soll.  
- 
-==== Stichwörter: Hinweise auf bestimmte Datenstrukturen ==== 
- 
-=== Arrays === 
-konstante Laufzeit O(1), vorgegebene Menge (feste Größe), Indizes, sortiert, Bitvektor, Array hat eine feste/fixe Größe, Matrix 
- 
-=== Listen === 
-variable Länge, beliebig lang, sortiert, O(n) Laufzeit, veränderliche/dynamische Länge bzw. Größe, Nachfolger, Vorgänger, (Anwendungsbeispiel: dünnbesetzte Matrizen, Graphen mit wenigen Kanten), Kopf (head), current-Zeiger 
- 
-=== Stacks ===  
-push, pop, beides in O(1), LIFO (last in, first out), (Anwendungsbeispiel: Tiefensuche, Parsen von arithmetischen Klammerausdrücken), Stapel, "reverse", Stacks statt Rekursion 
- 
-=== Queue ===  
-enqueue, dequeue, beides in O(1), FIFO (first in, first out), Warteschlange, (Anwendungsbeispiel: Breitensuche, Level-Order-Traversierung) 
- 
-=== Heaps === 
-Prioritätswarteschlange, (nicht unbedingt eindeutige) Prioritäten (Schlüssel), Min-Heap, Max-Heap, insert in O(log(n)), delete_min/max() in O(log(n)), repair_up/down in O(log(n)), Heap-Ordnung, Heap-Struktur, "(fast) vollständiger" Binärbaum, Implementierung als Array der Länge n, Kinder und Eltern können in O(1) ermittelt werden H[i] -> H[2i], H[2i+1], 
-build_heap() in O(n) 
- 
-=== Graphen === 
-Adjazenz, Knoten, Kanten, Kreise, gerichtet, ungerichtet, Nachbarn,  
- 
-=== Binäre Suchbäume === 
-Wörterbuch, eindeutige Schlüssel, linker Teilbaum speichert kleinere Schlüssel, rechter Teilbaum speichert größere Schlüssel, (Anwendungsbeispiel: Inorder-Traversierung liefert aufsteigende Reihenfolge der Schlüssel, ), Spezialfälle: AVL-Bäume, (a,b)-Bäume, Laufzeit im Worst Case O(n) (wegen Tiefe O(n)) 
- 
-=== AVL-Bäume === 
-Wörterbuch, Spezialfall von binären Suchbäumen mit geringer Tiefe O(log(n)), Balance-Grad, Rotation (links, rechts), Zick, Zack, lazy remove,  
- 
-=== (a,b)-Bäume === 
-Erweiterung von binären Suchbäumen, a <= Anzahl Kinder <= b, Big Data, Externspeicher, mehrere Schlüssel pro Knoten, Schlüsselklau, geringe Tiefe $\Omega(\log_b(n)), O(\log_a(n))$, alle Blätter in der gleichen Tiefe 
- 
-=== Hashing === 
-Wörterbuch, Verkettung, Hash-Funktionen, Auslastungsfaktor $\lambda$, modulo, Teilen/Dividieren mit Rest, lineares Austesten (nacheinander nach einem freien Speicherplatz suchen), Kollision, Speicherplatz schon besetzt, $1/(1-\lambda)$ erwartete Zugriffszeit bei zufälligen Eingaben