Lösungen

Lösungen

Aufgabe: Max Sort implementieren

Zur Implementierung von Max Sort durchlaufen wir das Feld von hinten nach vorne und tauschen dabei jeweils das entsprechende Element mit dem größten seiner Vorgänger. Wir definieren dazu einen Index j in Abhängigkeit von i, der das Feld rückwärts durchläuft.

def max_sort(a):
    for i in range(0, len(a)):
        j = len(a) - i - 1
        swap(a, j, max_pos(a, j))

Ein Aufruf max_pos(a, to) liefert die Position des größten Elementes in a bis zur Position to.

def max_pos(a, to):
    pos = 0
    for i in range(0, to):
        if a[i+1] > a[pos]:
            pos = i + 1
    return pos

Aufgabe: Insertion Sort experimentell analysieren

Bei den durchgeführten Tests mit sortierten Arrays ist insertion_sort deutlich schneller als min_sort:

1000: 
0.00014853477478027344
2000: 
0.0003032684326171875
4000: 
0.0005826950073242188
8000: 
0.0012249946594238281

Die wichtigere Beobachtung ist jedoch nicht, dass die Laufzeiten geringer sind, sondern, dass sie langsamer ansteigen: Bei Verdoppelung der Eingabegröße ergibt sich ungefähr die doppelte Laufzeit und nicht mehr wie bei min_sort die vierfache.

Dies gilt jedoch nur für bereits sortierte Arrays. Um unsere Tests mit unsortierten Arrays zu wiederholen, definieren wir eine Funktion, die umgekehrt sortierte Arrays gegebener Größe erzeugt.

def descending(size):
    a = [None] * size
    for i in range(0, size):
        a[i] = size - i
    return a

Zum Beispiel liefert der Aufruf descending(5) das Ergebnis [5,4,3,2,1] zurück. Wenn wir diese Funktion zum Erzeugen der Testeingaben verwenden, ergibt sich die folgende Ausgabe:

1000: 
0.12551259994506836
2000: 
0.49935364723205566
4000: 
2.014533519744873
8000: 
8.014490604400635

Bei Verdoppelung der Eingabegröße umgekehrt sortierter Arrays vervierfacht sich also die Laufzeit von Insertion Sort, wie wir es auch schon bei Selection Sort beobachtet hatten.

Bonusaufgabe: Insertion Sort rekursiv implementieren

Die folgende rekursive Prozedur implementiert den Insertion Sort Algorithmus.

def ins_sort(a, to):
    if to > 0:
        ins_sort(a, to-1)
        insert_backwards(a, to)

Falls to <= 0 gilt, ist das zu sortierende Anfangsstück bereits sortiert, da es aus höchstens einem Element besteht. Falls to > 0 ist, sortieren wir rekursiv das Anfangsstück bis zur Position to-1 und fügen dann das Element an Position to rückwärts in den sortierten Bereich ein.

Die rekursive Implementierung führt die selben Vergleiche und Vertauschungen aus wie die vorherige Implementierung mit einer Zählschleife. Die Laufzeiten der beiden Implementierungen sind also ungefähr gleich.

Hausaufgabe: Quick Sort dokumentieren

Zu Beginn wird das Array

a = [3,2,1,6,7,4,8,5]

mit dem Aufruf partition(a,0,7) partitioniert. Nach diesem Aufruf (der die Position 2 zurückliefert) sind die Elemente von a wie folgt umgeordnet.

a = [1,2,3,6,7,4,8,5]

Nun wird der Bereich von Position 0 bis 1 mit dem Aufruf partition(a,0,1) partitioniert, wobei keine Elemente vertauscht werden. Anschließend folgt der Aufruf partition(a,3,7) des zweiten rekursiven Aufrufs von quick_sort. Er liefert als Ergebnis die Position 5 zurück und ordnet die Elemente in a wie folgt um.

a = [1,2,3,5,4,6,8,7]

Schließlich folgen in weiteren rekursiven Aufrufen von quick_sort die Aufrufe partition(a,3,4) und partition(a,6,7), die nacheinandner dafür sorgen, dass a sortiert wird.

a = [1,2,3,4,5,6,8,7]
a = [1,2,3,4,5,6,7,8]

Aufgabe: Aufgabe: Einfache Prozedur zum Sortieren analysieren

  1. Der Rumpf der mutierenden Prozedur simple_sort enthält zwei geschachtelte Zählschleifen. Die äußere verwendet die Zählvariable i, die innere j. Der innere Schleifenrumpf enthält eine optionale Anweisung deren Bedingung die Elemente an den Indizes i und j vergleicht. Im Rumpf der optionalen Anweisung steht ein Aufruf der Prozedur swap.

  2. Die folgende Programmtabelle dokumentiert die Ausführung des Aufrufs simple_sort([2,1,3]).

    PPija[i] < a[j]a
    0[2,1,3]
    10
    20
    3False
    21
    3False
    22
    3True
    4[3,1,2]
    11
    20
    3True
    4[1,3,2]
    21
    3False
    22
    3False
    12
    20
    3False
    21
    3True
    4[1,2,3]
    22
    3False
  3. Die Prozedur simple_sort sortiert jedes beliebige übergebene Array von Zahlen.

  4. Relevant für die Laufzeit ist vor allem die Anzahl durchgeführter Vergleiche. Sie ist eine obere Schranke für die Anzahl der durchgeführten Vertauschungen. Die Laufzeit ist beschrieben durch $O(n^2)$, falls $n$ die Anzahl der Elemente des übergebenen Arrays ist.

  5. Alle in der Aufgabe genannten Aussagen treffen zu.

Aufgabe: Merge-Sort Funktion implementieren

Die rekursive Funktion merge_sort liefert eine Kopie ihrer Eingabe zurück, wenn diese höchstens ein Element enthält. Wenn nicht, wird das übergebene Array in zwei Hälften geteilt, die rekursiv sortiert und dann zusammengeführt werden.

def merge_sort(a):
    if len(a) <= 1:
        return a + [] # return a copy
    else:
        half = len(a) // 2
        return merge(merge_sort(a[0:half]), merge_sort(a[half:len(a)]))

Die Funktion merge fügt zwei sortierte Arrays zu einem sortierten Array zusammen:

def merge(a, b):
    c = [None] * (len(a) + len(b))
    l = 0
    r = 0
    # as long as there are elements left
    while l + r < len(c):
        # we pick the next element from a
        if l < len(a) and (r >= len(b) or a[l] <= b[r]):
            c[l+r] = a[l]
            l = l + 1
        else: # or the next element from b
            c[l+r] = b[r]
            r = r + 1
    return c

Sowohl für sortierte als auch für unsortierte Eingaben wird die Laufzeit bei Verdoppelung der Eingabegröße etwas mehr als verdoppelt. Dies legt die (tatsächlich zutreffende) Vermutung nah, dass die Laufzeit in \(O(n\cdot log(n))\) ist.

Bonusaufgabe: Heap-Sort Prozedur implementieren

def swap(a, i, j):
    temp = a[i]
    a[i] = a[j]
    a[j] = temp

def left_child(pos):
    return 2*pos + 1

def right_child(pos):
    return 2*pos + 2

# restore heap property (node >= child) assuming it for children
def repair(a, root, size):
    max = root

    left = left_child(root)
    if left < size and a[max] < a[left]: # left child exists and is larger
        max = left
    
    right = right_child(root)
    if right < size and a[max] < a[right]: # right child exists and is larger
        max = right
    
    if max != root: # heap property is violated
        swap(a, root, max)
        repair(a, max, size)

def make_heap(a):
    for i in range(0, len(a)):
        repair(a, len(a) - i - 1, len(a))

def heap_sort(a):
    make_heap(a)
    for i in range(0, len(a)-1):
        j = len(a) - i - 1
        swap(a, 0, j)
        repair(a, 0, j)