Humboldt-Universität zu Berlin Institut für Informatik Prof. Dr. Ulf Leser M. Bux, B. Grußien, J. Sürmeli, S. Wandelt Berlin, den 15.06.2015 Übungen zur Vorlesung Algorithmen und Datenstrukturen Übungsblatt 5 Abgabe: Montag den 29.6.2015 bis 11:10 Uhr vor der Vorlesung im Hörsaal oder bis 10:45 Uhr in den Fächern im Raum RUD25 4.402. Die Übungsblätter sind in Gruppen von 2 Personen zu bearbeiten. Sie können auf diesem Übungsblatt bis zu 50 Punkte erhalten. Zur Erinnerung: Jedes Übungsblatt muss bearbeitet werden. Sie müssen mindestens ein Blatt für wenigstens eine Aufgabe jedes Übungsblattes abgeben. Die Lösungen sind auf nach Aufgaben getrennten Blättern abzugeben. Vermerken Sie auf allen Abgaben Ihre Namen, Ihre Matrikelnummern, den Namen Ihrer Goya-Gruppe und welchen Übungstermin bei welchem Übungsleiter Sie besuchen. Heften Sie bitte die zu einer Aufgabe gehörenden Blätter vor der Abgabe zusammen. Beachten Sie auch die aktuellen Hinweise auf der Übungswebsite unter: https://u.hu-berlin.de/alg_ds_ss15_u Konventionen: • Für ein Array A ist |A| die Länge von A, also die Anzahl der Elemente in A. Die Indizierung aller Arrays auf diesem Blatt beginnt bei 1 (und endet also bei |A|). Bitte beginnen Sie die Indizierung der Arrays in Ihren Lösungen auch bei 1. • Da beim Hashing überwiegend mit modulo-Werten gerechnet wird, wird beim Hashing die Indizierung aller Arrays A mit 0 beginnen und bei |A| − 1 enden. Bitte beachten Sie dies bei Aufgaben 1 und 2. • Mit der Aufforderung “Analysieren Sie die Laufzeit” ist hier gemeint, dass Sie eine möglichst gute obere Schranke der Zeitkomplexität angeben sollen und diese begründen sollen. • Ein Stern markiert eine tendenziell schwierigere Aufgabe. Aufgabe 1 (Hashing-Schreibtischtest) 2 + 2 + 3 + 3 + 2 = 12 Punkte Gegeben sei eine Hashtabelle mit 11 Feldern für Einträge (Index 0 bis 10, siehe Konventionen) und eine Hashfunktion h(x) = x mod 11. Führen Sie für die folgenden Hashverfahren einen Schreibtischtest durch, indem Sie jeweils die Werte 19, 2, 14, 41, 33, 47 und 12 in dieser Reihenfolge in eine leere Tabelle einfügen und diese nach jeder Einfügeoperation ausgeben. a) Hashing mit direkter Listenverkettung Hierbei ist die Hashtabelle als Array von einfach verketteten Listen realisiert. b) Offenes Hashing mit linearem Sondieren Bei Kollisionen wird hier das einzufügende Element an der nächsten freien Stelle links vom berechneten Hashwert eingefügt. Das heißt, die Sondierungsreihenfolge (die Reihenfolge, in der die Plätze im Array durchgegangen werden, bis erstmals ein freier Platz angetroffen wird) ist gegeben durch s(x, i) = (x − i) mod 11 für i = 0, . . . , 10. c) Doppeltes Hashing Das doppelte Hashing ist ein offenes Hashing, bei dem die Sondierungsreihenfolge von einer zweiten Hashfunktion h0 (x) = 1 + (x mod 7) abhängt. Die Position für das i-te Sondieren ist bestimmt durch die Funktion s(x, i) = (h(x) − i · h0 (x)) mod 11 für i = 0, . . . , 10. d) Geordnetes Hashing Das geordnete Hashing ist ein offenes Hashing, bei dem die gemäß Sondierungsreihenfolge angetroffenen Elemente (hier absteigend) sortiert werden. Die Position eines neu einzufügenden Elements x ist die erste Position gemäß der Sondierungereihenfolge, an der ein kleineres Element oder noch kein Element im Array steht. Wenn auf ein kleines Element y getroffen wird, wird dieses durch das neue Element x ersetzt. Danach wird das Element y neu nach diesem Verfahren eingefügt, und das Ganze rekursiv fortgesetzt. Beim geordneten Hashing werden stets Sondierungsververfahren benutzt, bei denen sich für das Element y anhand seiner Position direkt die Folgepositionen innerhalb der Sondierungsreihenfolge bestimmen lassen. Zum Sondieren nutzen Sie das doppelte Hashing mit der in Aufgabenteil c) gegebenen Sondierungsfunktion. e) Uniformes offenes Hashing Hier erhält jeder Schlüssel mit gleicher Wahrscheinlichkeit eine der 11! Permutationen von {0, 1, . . . , 10} als Sondierungsreihenfolge zugeordnet. Als „zufällige“ Permutationen nutzen Sie: L(19) = 2, 10, 9, 3, 0, 8, 7, 6, 1, 4, 5 L(2) = 1, 5, 9, 3, 7, 10, 4, 8, 6, 2, 0 L(14) = 0, 8, 5, 2, 1, 10, 7, 9, 6, 3, 4 L(41) = 1, 4, 10, 5, 2, 0, 7, 3, 6, 9, 8 L(33) = 6, 8, 4, 0, 9, 1, 5, 2, 10, 3, 7 L(47) = 4, 2, 3, 5, 1, 8, 9, 0, 7, 6, 10 L(12) = 1, 3, 6, 0, 4, 5, 2, 10, 8, 7, 9 Mehr zum uniformen offenen Hashing finden Sie auch in Aufgabe 2. Aufgabe 2 (Uniformes offenes Hashing) 2 + 4 + 2 + 2 + 3 + 3 = 16 Punkte Wir betrachten in dieser Aufgabe den Idealfall für offene Hashverfahren. Dieser Fall liegt vor, wenn die Sondierungsfolge für einen Schlüsselwert gleichverteilt (d.h. uniform) aus allen möglichen Permutationen der Hashtabellenpositionen gezogen wird. Sei T eine Hashtabelle mit m Slots mit den Indizes 0, 1, . . . , m − 1. Sei P erm(m) die Menge aller m! Permutationen der Elemente 0, 1, 2, . . . , m − 1, d. h., P erm(m) enthält m! viele unterschiedliche Listen L, wobei jedes L eine Reihenfolge der Indizes 0, 1, . . . , m − 1 ist, |L| = m gilt, und die Elemente in L paarweise verschieden sind. Weiterhin sei U die Menge der möglichen Schlüsselwerte und es sei L : U → P erm(m) eine Funktion, die jedem Schlüssel k ∈ U uniform zufällig eine Liste L(k) ∈ P erm(m) zuweist. Für eine gegebene Liste L ∈ P erm(m) und einen 1 Schlüsselwert k ∈ U ist die Wahrscheinlichkeit also m! , dass L(k) = L gilt. Sei L(k)[i] das i-te Element der Liste L(k) beginnend bei i = 0. Die Sondierungsreihenfolge für einen Schlüsselwert k ∈ U ist durch die Funktion s(k, i) = L(k)[i] gegeben, d. h., beim i-ten Versuch wird das i-te Element in L als Position in der Hashtabelle T ausgewählt. Wir nehmen an, dass sich genau n < m Elemente in der Hashtabelle T befinden, somit beträgt der Loadfaktor von T genau n α= m und es muss mindestens einen freien Slot in T geben. Bei der Suche nach einem Schlüssel k in der Hashtabelle T (siehe Algorithmus search(T, k)) wird nun anhand der zugehörigen Liste L(k) die Hashtabelle durchsucht, bis k gefunden oder auf einen leeren Slot von T zugegriffen wird. search(T, k) Input: Hashtabelle T und Schlüsselwert k Output: True, falls k ∈ T ; False, sonst for i = 0 to m − 1 do if T [L(k)[i]] = k then return True; else if T [L(k)[i]] = null then return False; end if end for a) Sei Ta die folgende Hashtabelle: 0 1 2 3 4 5 6 7 8 – 2 – 12 47 – 5 – 44 9 – 10 41 Bestimmen Sie die Ausgabe von search(Ta , 41) und search(Ta , 33). Benutzen Sie dazu die Sondierungsreihenfolgen aus Aufgabe 1e). Geben Sie zudem für beide Suchen den Wert von Ta [L(k)[i]] bei jedem Durchlauf der for-Schleife an. Wir betrachten im Folgenden eine einmalige erfolglose Suche nach einem Schlüssel k in der oben allgemein beschriebenen Hashtabelle T der Größe m, in der bereits n Einträge belegt sind. Seien pi die Wahrscheinlichkeit, dass bei search(T, k) genau i-mal auf einen belegten Slot in der Tabelle zugegriffen wird, und qi die Wahrscheinlichkeit, dass bei search(T, k) mindestens i-mal auf einen belegten Slot der Tabelle zugegriffen wird. b) Bestimmen Sie (in Abhängigkeit von m und n) die Wahrscheinlichkeiten q1 , q2 , q3 und folgern Sie daraus qi für 1 ≤ i ≤ n. c) Bestimmen Sie qi für i > n. d) Zeigen Sie für alle i ≥ 1, dass die Ungleichung qi ≤ αi gilt. e) Zeigen Sie, dass die Gleichung P∞ i=1 i · pi = P∞ i=1 qi gilt. Hinweis: Wie lässt sich qi , die Wahrscheinlichkeit, dass mindestens i-mal auf einen belegten Slot zugegriffen wird, durch Wahrscheinlichkeiten für genaue Zugriffzahlen ausdrücken? f) Wir interessieren uns für die erwartete Anzahl von Tabellenzugriffen bei einer erfolglosen Suche nach Schlüssel k ∈ / T . Damit search(T, k) nach i + 1 Zugriffen auf T erfolglos abbricht, muss vorher i-mal auf einen belegten Slot und dann einmal auf einen freien Slot zugegriffen werden. Sei E der Erwartungswert für die Anzahl der Zugriffe auf einen belegten Slot bei der Suche nach k. Erwartungswert E lautet E= n X i · pi = i=0 ∞ X i · pi , i=0 wobei die letzte Gleichung gilt, da pi = 0 für i > n. Der gesuchte Erwartungswert ist somit 1+E=1+ ∞ X i · pi . i=0 Zeigen Sie, dass 1 + E ≤ 1 1−α gilt. Hinweis: Verwenden Sie Aufgabenteile d) und e) und Ihr Wissen über geometrische Reihen. Aufgabe 3 (Heaps, binäre Suchbäume) 3+4+2+2 = 11 Punkte Zur Erinnerung: Der Median einer aufsteigend sortierten n-elementigen Folge a1 , a2 . . . , an ist das Element ab n2 c+1 . a) Betrachten Sie den folgenden Algorithmus: Median_via_Heap(Array A) Input: Array A mit n Elementen Output: Medianelement aus A 1: B := fast_build_heap(A); 2: i := b n 2 c + 1; 3: C := [B[i], B[i + 1], . . . , B[n]]; 4: D:= fast_build_heap(C); 5: return extract_max(D); # d. h. B ist max-heapgeordnetes Array A # d.h. C ist Array aller Blattelemente des Max-Heaps B # d. h. D ist max-heapgeordnetes Array C Der Pseudocode von fast_build_heap und extract_max ist auf Übungsblatt 4 zu finden. Zeigen Sie, dass der obige Algorithmus zur Bestimmung des Medians nicht korrekt ist. b) Nehmen Sie nun an, dass Sie einen binären Suchbaum B mit Wurzel r gegeben haben, der die Schlüssel b1 < . . . < bn enthält. Für jeden Knoten k von B sei k.left der linke Kindsknoten von k, falls dieser existiert, und null, falls k keinen linken Kindsknoten hat. Analog sei k.right definiert. Außerdem sei jeder Knoten k mit der Anzahl k.size der Knoten in dem durch k induzierten Teilbaum beschriftet. (Der durch k induzierte Teilbaum ist der größtmögliche Teilbaum mit Wurzel k.) Zum Beispiel ist für die Wurzel r von B also r.size die Anzahl der Knoten in B. Für jedes Blatt k gilt k.size = 1. Für jeden inneren Knoten k, mit k.left 6= null und k.right 6= null gilt k.size = k.left.size+k.right.size+1. i) Entwerfen Sie einen Algorithmus getElement(r, i), der als Eingabe die Wurzel r eines Baumes B und eine Zahl i mit 1 ≤ i ≤ r.size erhält, und den Knoten mit Schlüssel bi zurückgibt. ii) Analysieren Sie die Laufzeit Ihres Algorithmus in Abhängigkeit von r.size für beliebige Eingaben r und i mit 1 ≤ i ≤ r.size. iii) Wann ist der Schlüssel der Wurzel von B der Median von b1 , . . . , bn ? Geben Sie ein notwendiges und hinreichendes Kriterium an. Aufgabe 4 (Binäre Suchbäume) 2 + 3 + 3 +3 = 11 Punkte In der Vorlesung haben Sie binäre Suchbäume kennengelernt, die ganze Zahlen als Schlüssel speichern und die Operationen Einfügen, Suchen und Löschen von Schlüsseln unterstützen. Wir betrachten hier nur Suchbäume, die keine Duplikate enthalten, d. h., alle Schlüssel des Baumes sind paarweise verschieden. a) Zeigen Sie: Wenn ein Knoten eines binären Suchbaumes zwei Kinder hat, dann hat sein Nachfolger, d. h. der Knoten im Baum mit dem nächstgrößeren Schlüssel, kein linkes Kind. b) Beweisen oder widerlegen Sie folgende Aussage: Wenn man aus einem binären Suchbaum zwei verschiedene Elemente x und y löscht, so erhält man unabhängig von der Löschreihenfolge stets den gleichen Suchbaum. Wenden Sie dabei die Symmetric Predecessor-Methode an, d.h. beim Löschen eines Knotens v mit zwei Kindern wird der Knoten v durch den größten Knoten im linken Teilbaum ersetzt (siehe Vorlesung). c) Zeigen Sie, dass es zu jedem binären Suchbaum T eine Reihenfolge seiner Elemente gibt, so dass man durch Einfügen der Elemente in dieser Reihenfolge in einen anfangs leeren Baum den Baum T erhält. d) Angenommen Sie suchen einen Schlüssel k in einem binären Suchbaum und finden ihn schließlich. Seien P die Schlüssel auf dem zugehörigen Suchpfad und L bzw. R die Schlüssel in Teilbäumen, die links bzw. rechts vom Pfad P liegen (d. h., L, R und P sind paarweise disjunkt). Beweisen oder widerlegen Sie, dass dann für jede Wahl von x ∈ L, y ∈ P und z ∈ R die Aussage x ≤ y ≤ z gilt.