Fun with binary heap trees


... [Seminar "Haskell"] ... [Inhaltsübersicht] ... [weiter] ...

Binary Heap Trees


Definition

Bei Binary Heap Trees handelt es sich um eine besondere Art der Binärbäume. Sie lassen sich durch folgende Datenstruktur repräsentieren:

Data (Ord a) => Tree a = Null | Fork a (Tree a) (Tree a)

Die Daten werden hier nicht am Ende eines Weges durch den Baum in einem Blatt gespeichert sondern direkt in den einzelnen Knoten des Baumes. Dabei gilt, dass die Werte der Kind-Knoten eines Baumes immer größer oder gleich dem Wurzelelement sein müssen. Dies setzt sich durch die rekursive Definition der Datenstruktur bis in die Blätter fort. Dies hat den Vorteil, dass sich das kleinste Element des Baumes immer in der Wurzel befindet und somit extrem leicht zu ermitteln ist.

Zwischen dem linken und dem rechten Ast eines Baumes ist keine Ordnungsrelation definiert. Hier ist man, wenn man von Performance-Gesichtspunkten absieht frei ein der Auswahl einer Einfüge-Strategie. Aus diesem Grunde müssen Binary Heap Trees auch keine "Balanced Trees" sein. Es theoretisch denkbar, einen Binary Heap Tree als sortierte lineare Liste zu implementieren.


Anwendungen

Binary Heap Trees eignen sich dadurch, dass sich das kleines Element immer in der Wurzel befindet für Warteschlangen mit Prioritäten. Bei solchen Warteschlangen hat immer das Element mit der kleinsten Prioritätszahl die höchste Priorität und wird zuerst verarbeitet. Da sich dieses Element bereits in der Wurzel befindet, ist es sehr einfach zu ermitteln.

Eine andere Anwendung ist allgemein das Sortieren von Elementen einer Liste. Binary Heap Trees können, wenn sie richtig implementiert wurden, sehr effizient beim Sortieren arbeiten. Die Zeitkomplexität ist hier O(log n).


Funktionen auf Binary Heap Trees

Für die Verwendunge von Binary Heap Trees müssen wir folgende Funktionen zur Verfügung stellen. Eine für den Test auf Leere, da wir auch leere Bäume erzeugen können, eine zur Ermittlung des kleinsten Elements, zum Löschen des kleinsten Elements, zum Einfügen eines neuen Elementes in einen vorhandenen Baum und schliesslich eine zum Zusammenführen von zwei Bäumen.

isEmpty   :: (Ord a) => Tree a -> Bool
minElem   :: (Ord a) => Tree a -> a
deleteMin :: (Ord a) => Tree a -> Tree a
insert    :: (Ord a) => a -> Tree a -> Tree a
merge     :: (Ord a) => Tree a -> Tree a -> Tree a

Die ersten beiden Funktionen sind sehr einfach zu implementieren. Sie laufen in konstanter Zeit:

isEmpty   :: (Ord a) => Tree a -> Tree a
isEmpty Null         = True
isEmpty (Fork x a b) = False


minElem   :: (Ord a) => Tree a -> a
minElem (Fork x a b) = x

Die beiden Funktionen deleteMin und insert können auf die Funktion merge abgebildet werden, da sie jeweils nur Sonderfäll dieser sind. Damit besitzen diese die gleiche Laufzeitkomplexität wie merge.

deleteMin :: (Ord a) => Tree a -> Tree a
deleteMin (Fork x a b) = merge a b

insert    :: (Ord a) => a -> Tree a -> Tree a
insert x a = merge (Fork y Null Null) a

Bleibt noch die merge Funktion zu implementieren. Für die Fälle, dass ein Baum mit einem leeren Baum zusammengeführt wird, ist der Fall recht einfach: Der Baum bleibt, wie er ist. In allen anderen Fällen muss der Baum mit dem kleineren Wurzelelement ermittelt werden und mit dem anderen Baum verschmolzen werden. Dazu wird die Funktioen join eingeführt.

merge     :: (Ord a) => Tree a -> Tree a -> Tree a
merge a Null                 = a
merge Null b                 = b
merge a b
    | minElem a <= minElem b = join a b
    | otherwise              = join b a

In der join Funktion schliesslich müssen die drei Bäume, die nach dem Extrahieren des neuen Root-Elementes übrig bleiben auf die beiden Äste der Wurzel verteilt werden. Dazu müssen zwei dieser Bäume wieder zusammengefasst werden.

join      :: (Ord a) => Tree a -> Tree a -> Tree a
join (Fork x a b) c = Fork x ? ?

Es gibt insgesamt sechs verschiedene Arten, diese drei Bäume zusammenzuführen. Aber welche ist die sinnvollste?

join (Fork x a b) c = Fork x a (merge b c)
join (Fork x a b) c = Fork x b (merge a c)
join (Fork x a b) c = Fork x c (merge a b)
join (Fork x a b) c = Fork x (merge b c) a
join (Fork x a b) c = Fork x (merge a c) b
join (Fork x a b) c = Fork x (merge a b) c

Beispiele für die sechs join Varianten

Die folgenden Bäume entstanden jeweils durch das Einfügen der folgenden Zahlenfolge: 5, 2, 1, 6, 3, 4.

Fork x a (merge b c)
1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6


Fork x b (merge a c)
    1
   / \
  2   4
 / \   \
5   3   6


Fork x c (merge a b)
  1
 / \
4   2
   / \
  3   5
     /
    6


Fork x (merge b c) a
    1
   / \
  4   2
 /   / \
6   3   5


Fork x (merge a c) b
          1
         /
        2
       /
      3
     /
    4
   /
  5
 /
6


Fork x (merge a b) c
    1
   / \
  2   4
 / \ 
5   3
 \
  6