ueb13_4 - oth

Werbung
Algorithmen und Datenstrukturen
Fachhochschule Regensburg
Algorithmen und Datenstrukturen
Binary Heap1
Übung13_4
Algorithmen und Datenstrukturen
Name: ________________________
Lehrbeauftragter: Prof. Sauer
Vorname: _____________________
Die Klasse BinaryHeap in C++2
Heap-Ordnung
Falls das kleinste Element am schnellsten gefunden werden soll, steht sinnvollerweise das kleinste
Element an der Spitze des Heap. In einem Heap sind dann immer die Daten der Vorgängerknoten
kleiner als die der Nachfolgeknoten.
Grundlegende Heap-Operationen
Einfügen
Eine Lücke wird geschaffen. Wenn das einzufügende Element ohne Verletzung der Heap-Ordnung in
die Lücke in die Lücke passt, dann ist alles bereits erledigt. Anderenfalls ist das Element, das den
Vorgänger zur Lücke markiert, mit dem Element, das in die Lücke aufgenommen werden soll, zu
tauschen, z.B. Aufnahme des Schlüssels mit dem Wert 14 in den Heap.
13
13
21
16
24
65
31
26
32
19
21
68
24
14
65
13
14
16
24
65
1
2
21
26
32
19
68
31
Skriptum, 1.3.4.2.1
pr13228 bzw. pr31_112, pr13_421
1
16
14
26
32
19
31
68
Algorithmen und Datenstrukturen
Die Strategie, die bei dem vorliegenden Einfügen verfolgt wird, ist ein aufwärts gerichtetes
Durchdringen bis ein geeigneter Punkt gefunden wird.
// Einfuegen eines neuen Elements in den Heap,
// aktualisieren der Struktur
template <class T>
void Heap<T> :: einfuegen(const T& mrk)
{
// Pruefe auf vollen Heap; terminiere, falls wahr
if (heapGroesse == maxheapGroesse)
fehler("Heap voll");
// Speicher das Element am Ende des Heap und inkrementiere
// "heapGroesse"
hListe[heapGroesse] = mrk;
filtereRauf(heapGroesse);
heapGroesse++;
}
template <class T>
void Heap<T> :: filtereRauf(int i)
{
int aktpos, elternpos;
T ziel;
// aktpos durchlaeuft den Elternpfad
// ziel ist hListe[i]
aktpos = i;
elternpos = (i - 1) / 2;
ziel = hListe[i];
// Durchlaufe den Elernpfad bis zur Wurzel
while (aktpos != 0)
{
// Elternteil kleiner gleich Ziel: Heap OK
if (hListe[elternpos] <= ziel) break;
else
// Uebertrage den Wert des Kindes zu den Eltern und
// aktualisiere die Indexe fuer das naechste Elternteil
{
hListe[aktpos] = hListe[elternpos];
aktpos = elternpos;
elternpos = (aktpos - 1) / 2;
}
}
hListe[aktpos] = ziel;
}
Der zeitliche Aufwand liegt bei O(log N), falls das einzufügende Element das Minimum ist und bis zur
Wurzel durchdringen muß.
2
Algorithmen und Datenstrukturen
Löschen des kleinsten Werts
Das Löschen des kleinsten Element an der Spitze bedingt ein ähnliches Vorgehen wie beim Einfügen.
Falls der kleinste Schlüsselwert entfernt wird, entsteht eine Lücke, die von den nachfolgenden
Schlüsseln geschlossen werden muß, z.B.:
13
31
14
16
24
65
21
26
32
19
14
68
16
24
31
65
21
26
32
14
31
16
65
21
26
68
13
14
24
19
19
21
68
24
32
65
16
31
26
19
68
32
14
21
16
24
65
31
26
19
68
32
Die Strategie ist ein abwärts gerichtetes Durchdringen. Im schlimmsten Fall liegt der Aufwand bei
O(log N).
// Gib den Wert des ersten Elements zurueck und
// aktualisiere den Heap.
template <class T>
T Heap<T> :: loeschen(void)
{
T hmrk;
// Pruefe auf einen leeren Heap
if (heapGroesse == 0)
fehler("Heap leer");
3
Algorithmen und Datenstrukturen
// Kopiere die Wurzel auf hmrk;
// Ersetze den wurzel-Wert durch den letzten Wert im Heap,
// Erniedige die heapGroesse
hmrk = hListe[0];
hListe[0] = hListe[heapGroesse - 1];
heapGroesse--;
// Auf von filtereRunter zur Positionierung des
// neuen Wurzel-Werts im Heap
filtereRunter(0);
// Rueckgabe des alten Wurzel-Werts;
return hmrk;
}
template <class T>
void Heap<T> :: filtereRunter(int i)
{
int aktpos, kindpos;
T ziel;
// Beginne mit i und setze dessen Wert auf Ziel
aktpos = i;
ziel = hListe[i];
// Berechne den linken Index vom Kind mit Bewegung nach unten
// Der Weg endet am Ende der Liste
kindpos = 2 * i + 1;
while (kindpos < heapGroesse)
{
// Index des rechten Kinds: kindpos + 1
// Setze den Index auf den kleineren der beiden Kinder
if ((kindpos + 1 < heapGroesse) &&
(hListe[kindpos + 1] <= hListe[kindpos]))
kindpos = kindpos + 1;
// Eltern sind kleiner als die Kinder: Heap OK
if (ziel <= hListe[kindpos]) break;
else
{
// Uebertrage den Wert des kleineren Kinds auf die
// Eltern. Die Position des kleineren Kinds ist dann frei
hListe[aktpos] = hListe[kindpos];
// Aktualisieren der Indexe
aktpos = kindpos;
kindpos = 2 * aktpos + 1;
}
}
// Zuweisung von ziel zur freien Position
hListe[aktpos] = ziel;
}
Andere Heap-Operationen
macheHeap()3: N Elemente werden in einen leeren Heap platziert. Das kann über N
aufeinanderfolgende Einfügevorgänge geschehen. Da jedes Einfügen im Schnitt zwischen O(1) und
O(log N) Aufwendungen benötigt, liegt die totale Laufzeit des Algorithmus bei O(N), im schlechtesten
Fall bei O(NlogN).
Generell sind N Elemente in dem Baum gemäß der einem Heap zugeordneten Struktur-Eigenschaft
anzuordnen. Bewerkstelligen tut dies durchdringeRunter(i). Die Methode durchdringt vom
Knoten i aus das Feld und erzeugt einen im Sinne eines Heap geordneten Baum.
// Konstruktor zum Konvertieren eines "array" in einen "heap"
template <class T>
Heap<T> :: Heap(T feld[], int n)
3
macheHeap() ist hier im Rahmen des folgenden Konstruktors angegeben
4
Algorithmen und Datenstrukturen
{
int aktpos;
// n == 0 ist ein falsche "array"-Groesse, terminiere
if ( n <= 0) fehler("Listengroesse verkehrt");
// Parameter n setzt die "heap"-Groesse und die maximale
// Heap-Groesse
maxheapGroesse = n;
heapGroesse =n;
// Zuweisen Adresse von feld an heapListe
hListe = feld;
// Setze die aktuelle Position auf den letzten Index der Eltern
aktpos = (heapGroesse - 2) / 2;
//
while (aktpos >= 0)
{
//
filtereRunter(aktpos);
aktpos--;
}
}
}
Bsp.: Im folgenden Beispiel ist der Baum nicht im Sinne eines Heap geordnet. Die aktuelle Belegung
des Felds mit Elementen liegt bei 15. Aufgerufen wird im ersten Schritt zur Herstellung der HeapEigenschaft filtereRunter(7). Es ergibt sich bis zum Erreichen eines vollständig eingerichteten
Heap:
150
80
30
100
40
10
20
90
70
60
50
110
120
140
130
Abb.: Ausgangsituation
150
80
30
100
40
10
20
90
70
60
50
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(7)
5
Algorithmen und Datenstrukturen
150
80
30
100
40
10
20
90
50
60
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(6)
150
80
30
100
40
10
20
90
50
60
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(5)
150
80
20
100
40
10
30
90
50
60
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(4)
6
Algorithmen und Datenstrukturen
150
80
20
100
40
10
30
90
50
60
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(3)
150
10
20
100
40
60
30
90
50
80
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(2)
10
20
30
100
40
60
150
90
50
80
70
110
120
140
130
Abb.: Nach Aufruf von filtereRunter(1)
Zur Berechnung der Laufzeit von macheHeap() muß die Anzahl der gestrichelten Linien festgestellt
werden. Das kann durch Bestimmung der Summe aller Höhen der Knoten im Heap geschehen. Das
wäre die maximale Größe von gestrichelten Linien.
Ein perfekt ausgeglichener Binärbaum der Höhe h umfasst 2 h+1-1 Knoten, die Summe der Höhen der
Knoten liegt bei 2h-1-(h+1). Aus dieser Summe folgt für das Leistungsverhalten O(N), wobei N die
Anzahl der Knoten ist.
7
Algorithmen und Datenstrukturen
Heap-Algorithmen der STL in C++
Die folgenden Heap-Eigenschaften bilden die Voraussetzung für die Anwendung der HeapAlgorithmen:
- Die n Elemente eines Heap liegen in einem Array auf den Positionen 0 bis n – 1.
- Die Art der Anordnung der Elemente im Array entspricht einem vollständigen binären Baum, bei dem
alle Ebenen besetzt sind. Die einzige mögliche Ausnahme bildet die unterste Ebene, in der alle
Elemente auf der linken Seite erscheinen.
99
[0]
33
56
[1]
[2]
21
30
[3]
[4]
11
[7]
h[0]
99
20
48
[5]
[6]
9
25
1
10
17
[8]
[9]
[10]
[11]
[12]
[1]
33
[2]
56
[3]
21
[4]
30
[5]
20
[6]
48
[7]
11
[8]
9
[9]
25
[10]
1
40
[13]
[11]
10
[12]
17
[13]
40
Abb.: Array-Repräsentation eines Heap
Das Element h[0] ist die Wurzel, jedes Element h[j] , j > 0 hat einen Elternknoten h[(j1)/2]
- Jedem Element h[j] ist eine Priorität zugeordnet, die größer oder gleich der Priorität der Kindknoten
h[2j+1] und h[j+2] ist4.
- Ein Array h mit n Elementen ist genau dann ein Heap, wenn h[(j-1)/2] >= h[j] für 1<=j<=n gilt. Daraus
folgt automatisch, dass h[0] das größte Element ist. Eine Priorityqueue entnimmt einfach das oberste
Element eines Heap. Anschließend wird er rekonstruiert, d.h. das nächstgrößte Element rückt an die
Spitze.
Die C++-Standardbibliothek bietet 4 Heap-Algorithmen an, die auf alle Container, auf die mit RandomAccess-Iteratoren zugegriffen werden kann, anwendbar sind:
pop_heap() entfernt das Element mit der höchsten Priorität
template <class RandomAccessIterator>
void pop_heap(RandomAccessIterator first,RandomAccessIterator last);
template <class RandomAccessIterator>
void pop_heap(RandomAccessIterator first,RandomAccessIterator last,
Compare comp));
Das Entfernen besteht in einem Vertauschen des Werts mit der höchsten Priorität, der an der Stelle
first steht, mit dem Wert an der Stelle last – 1.
4
Große Zahlen bedeuten hohe Prioritäten
8
Algorithmen und Datenstrukturen
push_heap() fügt ein Element in einen vorhandenen Heap
template <class RandomAccessIterator>
void push_heap(RandomAccessIterator first,RandomAccessIterator last);
template <class RandomAccessIterator>
void push_heap(RandomAccessIterator first,RandomAccessIterator last,
Compare comp));
make_heap() sorgt dafür, dass die Heap-Bedingung für alle Elemente innerhalb eines Bereichs gilt.
template <class RandomAccessIterator>
void make_heap(RandomAccessIterator first,RandomAccessIterator last);
template <class RandomAccessIterator>
void make_heap(RandomAccessIterator first,RandomAccessIterator last,
Compare comp));
Die Komplexität ist proportional zur Anzahl der Elemente zwischen first und last.
sort_heap() verwandelt einen Heap in eine sortierte Sequenz. Die Sortierung ist nicht stabil, die
Komplexität ist O(NlogN), wenn N die Anzahl der sortierten Elemente ist.
template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first,RandomAccessIterator last);
template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first,RandomAccessIterator last,
Compare comp));
Die Sequenz ist aufsteigend sortiert. Elemente hoher Priorität kommen an das Ende der Sequenz.
Diese Algorithmen müssen keine Einzelheiten über Container wissen. Ihnen werden lediglich 2
Iteratoren übergeben, die den zu bearbeitenden Bereich markieren. Zwar ist less<T> als
Prioritätskriterium vorgegeben, aber vielleicht wird ein anderes Kriterium gewünscht. Dafür gibt es für
jeden Algorithmus eine überladene Variante, welche die Übergabe eines Vergleichsobjekts erlaubt.
Bsp.: ueb1202.cpp, ueb1203.cpp, ueb1204.cpp,ueb1205.cpp in /progr/pgc/ueb12_2
9
Herunterladen