Folien 3: Ausgewählte Algorithmen

Transcription

Folien 3: Ausgewählte Algorithmen
Grundlagen der Informatik
Prof. Dr. Stefan Enderle
NTA Isny
3 Ausgewählte Algorithmen
3.1 Sortieren
• Die Aufgabe „Sortieren einer Menge von
Elementen“ kommt häufig vor.
• Formal:
– Gegeben: n (verschiedene) Objekte a1,a2,..an,
auf denen eine totale Ordnung < definiert ist.
– Gesucht ist eine Permutation
p:{1,...,n} → {1,...,n} mit der Eigenschaft
ap(1) < ap(2) < ... < ap(n)
Sortieren
• Vereinfachung / Verallgemeinerung:
– Die total Ordnung < kann auf beliebigen
Datentypen definiert sein.
– Wir verwenden in den Beispielen jedoch
jeweils Natürliche oder Realzahlen.
3.1.1 Direktes Auswählen
• Gegeben ist ein Array a mit n Werten,
also a[ i ] mit i=1..n.
• Sortieren durch direktes Auswählen
– startet mit der ersten Position und wählt dann
aus allen Elementen das Kleinste für diese
Position aus.
– Danach wird die zweite Position gewählt und
aus den restlichen Elementen wieder das
Kleinste ausgewählt, usw.
Direktes Auswählen - Algorithmus
• Geg: a[ i ] mit i=1..n.
• Tue für alle Positionen i = 1..n-1 :
– Sei k=i
– Prüfe für alle j = i+1..n :
• Ist a[ k ] > a[ j ], dann sei k := j
– vertausche a[ i ] und a[ k ]
„Vertauschen“ von Zahlen
• Bemerkung:
– Ein Vertauschen zweier Zahlen a und b
mittels
a := b
b := a
wäre natürlich naiv, da der Wert von a nach
der ersten Zuweisung verloren wäre.
– Korrekt:
temp := a
a := b
b := temp
3.1.2 Bubble-Sort
• Gegeben ist ein Array a mit n Werten,
also a[ i ] mit i=1..n.
• Bubble-Sort
– geht das komplette Array durch und ordnet
jeweils zwei benachbarte Elemente.
– Falls sich noch Änderungen ergaben, startet
der Vorgang erneut
Bubble-Sort - Algorithmus
• Geg: a[ i ] mit i=1..n.
• Widerhole
– Setze vertauscht := false
– Tue für alle Positionen i = 1..n-1 :
• Wenn a[ i ] > a[ i+1 ]
– vertausche a[ i ] und a[ i+1 ]
– setze vertauscht := true
• bis vertauscht = false
3.1.3 Formale Analyse
• Direktes Auswählen und Bubble-Sort sind „naive“
Sortierverfahren, da sie nicht die bestmögliche
Anzahl an Schleifendurchläufen, Vergleichen und
Vertauschungen erreichen.
• Beispiel: Direktes Auswählen:
– besteht aus zwei Schleifen und dem „Inneren“
mit angenommenem konstantem Aufwand 1.
– Anzahl Durchläufe: <formel>
– Also im wesentlichen quadratischer Aufwand
Formale Analyse
• Beispiel: Bubble-Sort:
– Im günstigsten Fall (wenn alle Elemente bereits
sortiert sind) linearer Aufwand, da die Schleife
nur 1x durchlaufen wird.
– Im Mittel aber auch quadratischer Aufwand
Formale Analyse
• Wie schnell kann man überhaupt sortieren?
• Wir nehmen als Basisoperation die Vergleiche beim
Sortieren.
• Jeder Vergleich ruft eine Verzweigung im
Programmablauf hervor.
• Man kann also jedem Sortier-Algorithmus für eine
Eingabegröße n einen Entscheidungsbaum
zuordnen.
Formale Analyse
• Beispiel: Verzweigungsbaum für 3 Elemente
• Runde Kästchen = Vergleiche
• Eckige Kästchen = Resultat der Sortierung
Formale Analyse
• Für n Elemente gibt es n! mögliche Permutationen.
• Einem korrekten Sortieralgorithmus muss also ein
Entscheidungsbaum zugeordnet sein, der
mindestens n! Blätter hat.
(Im Beispiel 3! = 6 Blätter.)
• Die Höhe des Baumes ist die Anzahl der maximal
notwendigen Vergleiche.
• Frage also: Wie hoch ist ein Binärbaum mindestens,
wenn er n! Blätter hat?
Formale Analyse
• Satz: Ein Binärbaum mit k Blättern hat mindestens
die Höhe  log2 k .
• D.h. jeder Sortier-Algorithmus hat im ungünstigsten
Fall mindestens die Komplexität log2 (n!)
• Als „normale“ Funktion:
– Stirlingsche Formel: (formel)
– Also folgt: (formel)
Formale Analyse
• Fazit: Ein Sortieralgorithmus hat mindestens eine
Komplexität von n*log n.
3.1.4 Quicksort
• Gegeben ist ein Array a mit n Werten,
also a[ i ] mit i=1..n.
• Quicksort
– sucht sich ein Pivot-Element p aus
– Dann wird so vertauscht, dass alle a[i] < p
links und alle a[i]>p rechts von p liegen.
(bild)
– Nun wird Quicksort rekursiv auf dem linken
und rechten Teil separat aufgerufen.
Quicksort - Algorithmus
• Geg: a[ i ] mit i=1..n.
• quicksort(links, rechts)
falls links < rechts dann
teiler := split(links, rechts)
quicksort(links, teiler-1)
quicksort(teiler+1, rechts)
• Start mit
quicksort(1,n)
Quicksort – Algorithmus (2)
•
split(links, rechts) → Pivot Index
Setze i := links-1 und j := rechts
Setze pivot := rechts
wiederhole
// Erhöhe i bis ein Element größer gleich Pivot kommt
wiederhole
i := i + 1
solange a[ i ] < a[pivot]
// Erniedrige j bis ein Element kleiner gleich Pivot kommt
wiederhole
j := j - 1
solange links < j und a[pivot] < a[ j ]
falls i < j dann tausche a[ i ] mit a[ j ]
solange i < j
// setze Pivot an die richtige Stelle und gib seine Position zurück
tausche a[i] mit a[pivot]
liefere i als Pivot-Index zurück
Beispiel
• Beispiel mit „einbeispiel“
Laufzeit von Quicksort
• Die Laufzeit hängt maßgeblich von der
Wahl des Pivot Elements ab:
• Worst-Case:
Wird das Pivot-Element stets so gewählt,
dass es das größte oder kleinste ist,
reduziert sich die Länge bei jedem
Durchlauf um 1 und die Komplexität ist
somit n².
Laufzeit von Quicksort
• Best-Case:
Teilt das Pivot-Element die Länge des
Feldes stets in gleich große Teile, so
ergibt sich genau ein gleichmäßiger
Binärbaum, also Komplexität n*log n.
3.1.5 Heapsort
• Gegeben ist ein Array a mit n Werten,
also a[ i ] mit i=1..n.
• Heapsort
– sortiert alle Elemente in einen Heap ein
(Phase 1)
– Liest dann den Heap geordnet aus
(Phase 2)
Heapsort Algorithmus
• Heapsort:
– Phase 1:
Erzeuge einen unsortierten Baum mit a[i]
Tue für alle inneren Knoten von unten rechts bis
zur Wurzel:
lasse_sinken(Knoten)
– Phase 2:
Wiederhole
Lies die Wurzel aus (=größtes/kleinstes El.).
Setze das rechteste Blatt an die Wurzel
lasse_sinken(Wurzel)
bis Heap leer
Heapsort Algorithmus (2)
• lasse_sinken() prüft, ob der angegebene Knoten
der Heap-Eigenschaft widerspricht und tauscht
dann ggfs. mit dem größeren Nachfolger.
• lasse_sinken(Knoten):
Prüfe, ob der Knoten kleiner (größer) ist, als einer
seiner Nachfolger
Falls ja,
tausche mit dem größeren Nachfolger
lasse_sinken(getauschter_Nachfolger)
Heapsort Eigenschaften
• Heapsort benötigt in jedem Fall maximal
n*log n Durchläufe
• Allerdings benötigt Heap-Sort den
zusätzlichen Speicher für den Heap
3.2 Hashing
• Hashing ist ein Verfahren zum sehr
schnellen Abspeichern und Wiederfinden
von Daten.
• Im besten Fall funktioniert dies sogar in
konstanter Zeit, unabhängig von der
Anzahl der Elemente.
Hashing
• Hashing setzt voraus, dass die Anzahl der
zu speichernden Elemente vorweg
abschätzbar ist.
Die Methode verwendet Arrays, die richtig
dimensioniert werden müssen.
• Die Elemente werden über einen
Schlüssel (Key) gespeichert und wieder
gefunden.
Hashing
• Beispiel-Anwendung: Compilerbau
Verwaltung der „Symboltabelle“
(Mehrere Informationen zu jedem Symbol)
Schlüssel/Symbol
x1
test
PI
Information
Variable, Typ int
Funktion, Typ int, Param string, double
Konstante, Typ double, Wert 3.14159
Hashing
• Idee:
Die Elemente werden nicht sequentiell in
eine Tabelle eingetragen, sondern die
Hash-Funktion liefert den Index in die
Tabelle.
• Hash-Funktion:
h: Key -> int
Hashing
• Um Keys mathematisch beschreiben zu können,
werden sie meist in Zahlen umgewandelt.
• Beispiel:
Seien die möglichen Schlüssel aus
U={a,b,c,...,z}*
Dann lässt sich jedem Wort s aus U eine
natürliche Zahl n(s) zuordnen, indem die
Binärwerte der Ascii-Codes hintereinander
geschrieben werden.
Hashing
• Die Hash-Funktion h soll nun diese Werte in
Indizes umwandeln. Bei einer Tabelle mit m
Einträgen also h : U → [0...m-1]
• h soll möglichst einfach zu berechnen sein
• h soll die Elemente aus U möglichst „zufällig“
und gleichmäßig über die Menge [0...m-1]
streuen.
Hash-Funktionen
• Beispiele für Hash-Funktionen:
– Kongruenzmethode:
Man wähle m als Primzahl
h(s) := n(s) MOD m
– Multiplikationsmethode:
Man wähle eine Konstante a zw. 0 und 1.
t(s) := a * n(s)
h(s):=  m*( t(s) -  t(s) ) 
Hash-Funktionen
• Problem: Da meistens gilt, dass |U| wesentlich
größer ist als m, liefert die Hashfunktion für
unterschiedliche Keys dieselben Werte, also
h(s) = h(t) bei s ungleich t
→ „Kollision“
Ohne Kollisionen
• Annahme: Es kommen keine Kollisionen vor.
(bild ohne Kollisionen)
• Dann verlaufen die Grundoperationen mit einem
einzigen Zugriff:
– Einfügen: a[h(s)] := Daten
– Löschen:
a[h(s)] := <empty>
– Prüfen:
if a[h(s)] not <empty>
Mit Kollisionen
• Annahme: Es kommen Kollisionen vor.
(bild mit Kollisionen)
• Dann wird eine „Kollisionsauflösungsstrategie“
benötigt, z.B.
– Hashing mit Verkettung
– Offenes Hashing
Hashing mit Verkettung
• Hier enthält jeder Array-Eintrag einen Pointer,
der entweder auf NULL, auf ein Element oder
– im Falle der Kollision – auf eine verkettete
Liste von Elementen zeigt.
(bild mit verk. Liste)
Hashing mit Verkettung
• Die Algorithmen sind trotz der Verkettung relativ
einfach.
• Beispiel: Einfügen eines Elementes s
Einfügen(s):
i := h(s)
p := neues LLElement(s, a[ i ]);
a[ i ] := p
Hashing mit Verkettung
• Beispiel: Test eines Elementes s
Test(s) → bool:
i := h(s)
p := a[ i ]
wiederhole solange p nicht NULL
Falls p.key = s return true
p := p.next
return false
Hashing mit Verkettung - Analyse
• Sei n die Anzahl der gespeicherten Elemente.
Sei m die Größe des Arrays.
Dann ist b=n/m der Belegungsfaktor.
Wenn h(s) die Keys gleichmäßig streut, so sind
in jeder verketteten Liste im Mittel b Elemente.
• Der mittlere Suchaufwand liegt somit bei (1+b)
und ist damit unabhängig von n !!
Offenes Hashing
• Beim offenen Hashing werden die Daten im
Kollisionsfall nicht in einer Art „Überlaufliste“
gespeichert, sondern es wird innerhalb der Liste
nach einem alternativen Platz gesucht.
• Bemerkung: Beim Hashing mit Verkettung war
die Anzahl der Einträge nicht auf m beschränkt,
beim offenen Hashing schon.
Offenes Hashing
• Lineares Sondieren:
Ist a[h(s)] bereits besetzt, so wird a[h(s)+1]
geprüft, danach a[h(s)+2] usw.
• Nachteil: „Clusterbildung“:
Es entstehen Bereiche, in denen viele
aufeinanderfolgende Plätze belegt sind. Dies
verschlechtert die mittlere Suchzeit.
Offenes Hashing
• Double Hashing:
Ist a[h(s)] bereits besetzt, wird mit einer zweiten
Hash-Funktion h' die Schrittweite bestimmt:
Sei i = h(s) und a[ i ] bereits belegt
Bestimme ein j
Prüfe (i + j) mod m
Prüfe (i + 2j) mod m
Prüfe (i + 3j) mod m
...
3.3 O-Notation
• Die Laufzeit eines Algorithmus für große n
bezeichneten wir bisher als „linear“,
„quadratisch“, „exponentiell“, etc.
• Auf die genaue Angabe weiterer Faktoren kam
es hierbei nicht an (da z.B. maschinenabhängig).
• Diese Abstraktion wird durch die
„O-Notation“ ebenfalls ermöglicht.
O-Notation
• Definition:
Eine Funktion f(n) ist von der Art O(g(n)),
oder symbolisch f(n)=O(g(n)), falls es Zahlen
c und n0 gibt, so dass für alle n>n0 gilt:
|f(n)| <= c* |g(n)|
O-Notation
• Beispiel: 3n²+10n-4 = O(n²)
Beweis: Wähle c=4, dann muss nur noch ein n0
gefunden werden so dass 3n²+10n-4 < 4n² für
alle n>n0.
(Lsg. n0=10)
O-Notation
•
Innerhalb der O-Notation werden nur einfache Funktionen
verwendet:
– O(1) (lineare Komplexität)
– O(log log n)
– O(log n)
– O(n)
– O(n log n)
– O(n²)
– O(n³)
– ...
– O(nlog n)
– O(2n)
– O(22n)
3.4 Kompressionsalgorithmen
• Datenkompression (Datenkomprimierung) ist die
Reduktion des Speicherbedarfs von Daten
• Die Datenmenge wird reduziert, indem eine günstigere
Repräsentation bestimmt wird, mit der sich die gleichen
Daten in kürzerer Form darstellen lassen.
Kompressionsalgorithmen
• 2 Möglichkeiten der Kompression:
– Verlustfreie Kompression:
Die kodierten Daten entsprechen nach Anwendung
der Dekodiervorschrift exakt denen des Originals.
– Verlustbehafteten Kompression:
Die Daten lassen sich nicht (in jedem Fall) fehlerfrei
rekonstruieren.
Redundanz / Irrelevanz
• Redundanz
bezeichnet das mehrfache Vorhandensein ein und
derselben Information oder das umfangreiche
Beschreiben einer Information, die auch kürzer
dargestellt werden kann. Ein Merkmal ist dann
redundant, wenn es ohne Informationsverlust
weggelassen werden kann.
• Irrelevanz
bezeichnet die „Unbedeutsamkeit“ von Daten. Ein
Merkmal ist dann irrelevant, wenn es ohne größere
Auswirkung weggelassen werden kann.
Redundanz / Irrelevanz
• Beispiel für Redundanz:
In einer Datenbank werden Kunden gespeichert:
Nr
Name
Wohnort
--------------------------------3524
Enderle
Weißenhorn
876
Maier
Isny
Und Rechnungen:
Nr
Betrag
--------------------1
102,30
2
35,90
Kunde
-----------Enderle
Maier
KuNr
---------3524
876
• Speichern der Kundennummer würde ausreichen.
D.h., der zusätzliche Name ist redundant.
Redundanz / Irrelevanz
• Noch ein Beispiel für Redundanz:
In amerikanischen Institutionen gibt es manchmal ein
„Department of Redundancy Department“
•
Kein Beispiel für Redundanz:
Proofread carefully to see if you any words out!
Redundanz / Irrelevanz
• Beispiel für Irrelevanz:
Kompressionsalgorithmen
• 2 Möglichkeiten der Kompression:
– Verlustfreie Kompression:
Die kodierten Daten entsprechen nach Anwendung
der Dekodiervorschrift exakt denen des Originals.
→ Entfernen Redundanz
– Verlustbehafteten Kompression:
Die Daten lassen sich nicht (in jedem Fall) fehlerfrei
rekonstruieren.
→ Entfernen Irrelevanz
3.4.1 Textkompression
•
Unkomprimierte Texte belegen vergleichsweise wenig
Speicherplatz.
•
Trotzdem kann der benötigte Speicherplatz durch ein Verfahren zur
verlustfreien Kompression auf bis zu 10% des ursprünglich
benötigten Platzes reduziert werden.
•
Beispiel-Kodierung:
Ausgangstext:
AUCH EIN KLEINER BEITRAG IST EIN BEITRAG
Kodiertext:
AUCH EIN KLEINER BEITRAG IST -4 -3
Beispiel
•
Ausgangstext:
AUCH EIN KLEINER BEITRAG IST EIN BEITRAG
Kodiertext:
AUCH EIN KLEINER BEITRAG IST -4 -3
•
Kodierung:
– Wird ein bereits benutztes Wort gefunden, wird statt des Wortes
die relative Position des letzten Auftretens des Wortes
gespeichert. (Bis max. n)
– Hierfür müssen alle Wörter gespeichert werden! (Hashing!)
•
Dekodierung:
– Die letzen n Wörter werden in einem Ringspeicher gehalten. Bei
Auftreten einer Zahl -i wird das i-letzte Wort eingesetzt.
Tokenbasierte Kompression
•
Tokenbasierte Kompression:
Häufig wiederkehrende Schlüsselwörter werden durch
Abkürzungen, sog. „Tokens“, ersetzt.
•
Beispiel:
Ausgangstext:
print "Hallo"; print "Ich bin's"
Kodiertext:
3F "Hallo"; 3F "Hier"
•
Dem Kodierer und Dekodierer müssen die Token bekannt sein.
•
Oder die Token müssen am Anfang des Textes definiert sein.
RLE – Run-length Encoding
•
„Lauflängenkodierung“
•
In manchen Anwendungen kommen sehr wenige Zeichen oft
hintereinander vor.
•
Beispiel: Schwarz-weiß Bild
(z.B. Windows Bitmap)
Einzelne Pixel mit W und S gekennzeichnet:
WWWWWWWWWWSWWWWWWWWWWSSS
•
Idee: Häufigkeit der Zeichen wird gezählt und gespeichert.
•
Nach Anwendung der Lauflängenkodierung erhält
man:10W1S10W3S
RLE – Run-length Encoding
•
Problem: Einzelne Zeichen blähen den kodierten Text auf.
•
Originaltext
wird kodiert zu
•
Mögliche Lösung:
– Bei ein- bis dreimaligem Auftreten eines Zeichens bleibt das
Original.
– Bei mind. 4-maligem Auftreten wird ein Kontrollzeichen
eingefügt:
Aus
WSWSWWWWWWWWWSSSSSSWSWWW
wird
WSWSx9Wx6SWSWWW
– Taucht im Datensatz das Kontrollzeichen als Inhalt auf, so wird
es doppelt kodiert:
Aus WSSSSSWWWWWWxWWSWSSSSSSSSSxxxxxWSSW
wird Wx5Sx6WxxWWSWx9Sx5xWSSW
ABAB
1A1B1A1B
Huffman-Code
•
Annahme:
– Die zu komprimierenden Daten sind nicht gleichverteilt.
D.h., es gibt Zeichen, die häufiger und andere die weniger häufig
vorkommen.
•
Idee:
– Die häufig vorkommenden Wörter (bzw. Buchstaben)
bekommen ein kurzes Symbol, die selten vorkommenden ein
längeres.
– Symbole sind Binärzeichen (0,1,00,01,10,11,000,...)
•
Gesucht ist die optimale Zuordnung der Binärzeichen zu den
Wörtern.
Huffman-Code
•
Beispiel mit einzelnen Buchstaben:
– Beispieltext:
Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid
– Tabelle mit den Häufigkeiten der einzelnen Zeichen:
• n=1
• d=3
• B=4
• b=4
• e=4
• i=4
• k=4
• r=4
•
= 6 // Leerzeichen
• a=6
• l=6
• t=6
• u=7
Huffman-Code
•
•
Optimale Zuordnung über Baum-Algorithmus
– Zusammenfassen der kleinsten Elemente
– Iteration, bis Baum alle Elemente umfasst
– Zuordnung: z.B. „Links“ = 0, „Rechts“ = 1
Bit-Repräsentationen (Beispiel):
•
r --> 1101
•
B --> 1000
•
n --> 11100
•
l --> 001
•
k --> 1100
•
i --> 1011
•
--> 1111
•
e --> 1010
•
d --> 11101
•
b --> 1001
•
a --> 000
•
u --> 011
•
t --> 010
(→ Tafel)
Huffman-Code
•
•
Bit-Repräsentationen (Beispiel):
•
r --> 1101
•
B --> 1000
•
n --> 11100
•
l --> 001
•
k --> 1100
•
i --> 1011
•
--> 1111
•
e --> 1010
•
d --> 11101
•
b --> 1001
•
a --> 000
•
u --> 011
•
t --> 010
Kodierter Text:
100000100001111001101000011010111110010011010101110010
101111100000100001111001101000011010111101111100111011
111100011010000110101100001101010111110111111001001101
01011100101011111000110100001101011000011010101111101
Huffman-Code
•
Vergleich des Speicherbedarfs:
– Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid
= 59 Zeichen à 8 Bit => 472 Bit
– Kodierter Text:
100000100001111001101000011010111110010011010101110
010101111100000100001111001101000011010111101111100
111011111100011010000110101100001101010111110111111
001001101010111001010111110001101000011010110000110
10101111101
= 75 Bit
– Kompression um Faktor 6,3
(Ohne Speichern der Übersetzungstabelle!)
Text-Kompression
• Zusammenfassung:
– Token-basierte Kompression, wenn Schlüsselworte
oft vorkommen (z.B. in Programmiersprachen).
– Run-length Encoding, wenn nur wenige Zeichen,
diese aber oft hintereinander vorkommen.
– Huffman-Code, wenn bestimmte Zeichen öfters
vorkommen als andere (z.B. in natürlichen Sprachen)
• Alle Algorithmen sind verlustfrei!
3.4.2 Bildkompression
• Bilder benötigen meist mehr Speicherplatz als Texte.
• Allerdings spielen oft kleine Ungenauigkeiten keine
Rolle.
• Daher werden sehr oft verlustbehaftete
Kompressionsverfahren benutzt.
• Aktuelle Verfahren:
– GIF
– PNG
– JPG
GIF
• „Graphics Interchange Format“
• Verlustfreie Komprimierung
• Geringe Farbtiefe (256 Farben)
• Ersetzte 1987 bei Compuserve das Format RLE für
Schwarz-Weiß-Bilder
• Hiermit war es erstmals möglich, farbige Bilder per
Modem zu übertragen.
GIF
• Eigenschaften:
– Farbinformation stehen in Farbtabelle mit 256
Einträgen
– Jeder Eintrag kann eine Farbe aus 16,7 Mio.
möglichen Farben sein. (256³)
• Erste 6 Zeichen sind „GIF87a“ oder „GIF89a“
• Ab GIF89a ist auch „Transparent“ als Farbe möglich
GIF
• Ablauf bei Speicherung:
– Das Originalbild darf nur max. 256 Farben besitzen.
– Besitzt es mehr als 256 Farben werden Farben durch
ähnliche Farben ersetzt.
– Zuerst wird die Farbtabelle gespeichert:
256 Einträge a 3 Bytes (für RGB)
– Nun werden die einzelnen Pixel als Indizes in die
Farbtabelle gespeichert
• Hierdurch wird aus jedem 3-Byte Pixel ein 1-Byte Index
• Danach wird mit LZW Algorithmus gepackt
GIF
LZW Algorithmus
•
Meist mit 12 Bit
Index
PNG
• „Portable Network Graphics“ ist verlustfrei.
• PNG entstand als Antwort auf GIF, auf das seit 1994 von
Unisys Lizenzgebühren anfallen.
• Idee:
In vielen Bildern unterscheiden sich benachbarte Pixel
nur wenig voneinander. Das bedeutet, dass die
Differenzwerte dieser Pixel vom Betrag her recht klein
sind. Werden nun statt der originalen Pixeldaten die
Differenzwerte zu den vorangegangenen Pixeln
verarbeitet, treten oft Folgen kleiner Werte auf. Dies
begünstigt die geringe Größe von PNG-Dateien.
PNG
• Vorfilter:
Die Grafik-Software kann einen von 4 Vorfiltern
auswählen:
– 0 None
Keine Vorfilterung. Originalen Pixeldaten.
– 1 Sub
Differenzen zu dem links benachbarten Pixel.
– 2 Up
Differenzen zu dem darüber liegenden Pixel.
– 3 Average
Differenz zu dem Mittelwert aus dem darüber liegenden und dem links
benachbarten Pixel.
– 4 Paeth
„Paeth-Predictor-Wert“ aus dem links benachbarten, dem darüber
liegenden und dem schräg links oben benachbarten Pixel.
PNG
• Zur Komprimierung wird der „Deflate“Algorithmus verwendet, der für ZIP entwickelt
wurde.
• „Deflate“ ist eine Kombination aus
Lempel-Ziv-Storer-Szymanski-Algorithmus
und Huffman-Kodierung.
• Grob-Idee:
Mehrfach vorkommende Kombinationen werden
ebenfalls in die Huffman-Codierung eingebaut.
JPEG
• „Joint Photographic Experts Group“
• Wurde 1982 gegründet und trifft sich 3 mal im
Jahr
JPEG
• Verarbeitungsschritte:
– Farbraumumrechnung von RGB nach YCbCr
– Tiefpassfilterung und Unterabtastung der
Farbabweichungssignale Cb und Cr (verlustbehaftet).
– Einteilung in 8×8-Blöcke und diskrete
Kosinustransformation dieser Blöcke.
– Quantisierung (verlustbehaftet).
– Umsortierung.
– Entropiekodierung.
JPEG
• Farbraumumrechnung von
RGB nach YCbCr:
• Natur/Auge: Helligkeitsunterschiede
sind viel besser erkennbar als
Farbunterschiede
• Deshalb:
– hohe Auflösung bei Helligkeit,
– nur halbe Auflösung (=1/4) bei
Farbe (=Unterabtastung)
JPEG
• Einteilung in 8×8-Blöcke
• Diskrete Kosinustransformation dieser Blöcke.
• Im Beispiel kann das Bild mit 6 Koeffizienten gut rekonstruiert
werden (= Quantisierung).
JPEG
• Umsortierung + Entropiekodierung
ähnlich Huffman Kodierung, aber nicht mit fester
Zeichen-Statistik, sondern mit dynamischen
Wahrscheinlichkeiten
3.4.3 Videokompression
• Die Datenrate eines digitalisierten
herkömmlichen Farbfernsehsignals inklusive
Austastlücke liegt bei 216 Mbit/s
• Vergleich:
MPEG-4 = DVD-Auflösung bei nur 1 Mbit/s
3.4.3 Videokompression
• Die Datenrate eines digitalisierten
herkömmlichen Farbfernsehsignals inklusive
Austastlücke liegt bei 216 Mbit/s
• Vergleich:
MPEG-4 = DVD-Auflösung bei nur 1 Mbit/s
Videokompression
• Motion-JPEG (auch bei Apple Quicktime)
– Hier werden Einzelbilder eines Videos als
JPEG komprimiert.
– Vorteil: Das Video kann an jeder Stelle
geschnitten werden oder Einzelbilder
extrahiert werden
– Wird bei Digitalkameras meist verwendet
Videokompression
• Motion-JPEG (auch bei Apple Quicktime)
– Hier werden Einzelbilder eines Videos als
JPEG komprimiert.
– Vorteil: Das Video kann an jeder Stelle
geschnitten werden oder Einzelbilder
extrahiert werden
– Wird bei Digitalkameras meist verwendet
Videokompression
• MPEG (auch bei Apple Quicktime)
– Ähnlich zu Motion-JPEG, aber mit
Bewegungskompensation:
• Block motion compensation bei MPEG-2
• Global motion compensation bei MPEG-4
– Konstante Bildinhalte werden komplett
ausgefiltert
– Für Pixelblöcke, die sich von einem Bild zum
nächsten verschoben haben, wird nur der
Verschiebevektor gespeichert.
3.5 Backtracking
• Backtracking nennt sich eine algorithmische Methode,
bei der rekursive Funktionsaufrufe dazu benutzt werden,
einen ganzen Suchbaum an Möglichkeiten nach einer
oder mehreren Lösungen abzusuchen.
• Backtracking lässt sich immer dann einsetzen, wenn ein
Problem eine bestimmte Suchtiefe besitzt und man
ausgehend von einer bestimmten Teillösung die
Gesamtlösung durch Suchen finden kann, wobei man
evtl. in „Sackgassen“ geraten kann und dann einen
Schritt zurück muss (Backtracking).
Backtracking - Algorithmus
• Backtracking wird meist rekursiv implementiert.
• Ein allgemeiner rekursiver Algorithmus für Backtracking
sieht folgendermaßen aus:
• backtrack (Teillösung)
wenn Teillösung=Gesamtlösung
dann gib Teillösung aus (stop/weiter?)
ansonsten
für jede mögliche Erweiterung der Teillösung tue
backtrack (erweiterte Teillösung)
Backtracking - Algorithmus
• Der Algorithmus wird gestartet mit
backtrack (Leere Lösung)
• Für die Anwendung des Algorithmus auf ein spezielles
Problem müssen folgende Fragen geklärt werden:
– Wie wird eine Teil- oder Gesamtlösung repräsentiert?
– Wie wird geprüft, ob eine gegebene Teillösung eine
Gesamtlösung ist?
– Wie werden zu einer Teillösung die möglichen
Erweiterungen berechnet?
Beispiel: Abwechselnd Ger./Ung.
• Ein Vektor mit n Zahlen soll so mit den Zahlen 0...n-1
gefüllt werden, dass immer eine gerade Zahl neben
einer ungeraden Zahl steht.
• Beispiel: mit n=10
4781056329
Beispiel: Abwechselnd Ger./Ung.
• Wie wird eine Teil- oder Gesamtlösung repräsentiert?
– Array der Länge n für ganze Zahlen
– Teillösung: Elemente 0 bis index-1 sind bereits gefüllt
und sortiert
• Wie wird geprüft, ob eine gegebene Teillösung eine
Gesamtlösung ist?
– Wenn index=n dann sind alle gefüllt und sortiert!
• Wie werden zu einer Teillösung die möglichen
Erweiterungen berechnet?
– Das Array-Element index muss neu belegt werden
– Dafür kommen nur die Zahlen in Frage, die noch nicht
benutzt sind
Beispiel: Abwechselnd Ger./Ung.
• Beispiel-Programm (C++)
(ohne #include etc.)
// Globale Variablen //
const int ANZAHL = 10;
// Maximale Anzahl
int
// Array mit Teillösung
feld[ANZAHL];
bool uebrig[ANZAHL];
// Übrige Zahlen
int main()
{
}
init();
// Initialisiere die Arrays
backtrack(0);
// Starte mit Teillösung 0
Beispiel: Abwechselnd Ger./Ung.
void init()
{
for (int i=0; i<ANZAHL; i++) { // Für alle Elemente...
uebrig[i] = true;
// ... setze auf „übrig“
}
}
void fertig()
{
for (int i=0; i<ANZAHL; i++) { // Für alle Elemente...
cout << feld[i] << " ";
}
cout << endl;
}
// ... gib sie aus
Beispiel: Abwechselnd Ger./Ung.
void backtrack(int index)
{
if (index==ANZAHL) fertig();
else {
for (int i=0; i<ANZAHL; i++) {
if (uebrig[i] &&
((index==0) || ((feld[index-1]+i) % 2==1)) ) {
}
}
}
}
feld[index] = i;
// Nächstes Element
uebrig[i] = false;
// Zahl nicht mehr übrig
backtrack(index+1);
// Löse Restproblem
uebrig[i] = true;
// Zahl wieder übrig
Beispiel: Abwechselnd Ger./Ung.
• Ausgabe:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 9 8 7
0 1 2 3 4 5 8 7 6 9
0 1 2 3 4 5 8 9 6 7
0 1 2 3 4 7 6 5 8 9
.
.
.
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 0 1 2
Beispiel: Magisches Quadrat
• Ein magisches Quadrat mit der Kantenlänge n ordnet die
Zahlen 1...n² so an, dass die Summe
– aller Spalten,
– aller Zeilen,
– und der beiden Diagonalen, gleich sind.
• Beispiel für n=3:
4 9 2
3 5 7
8 1 6
• Zusammenhang von n und Summe → Tafel
Beispiel: Magisches Quadrat
• Wie wird eine Teil- oder Gesamtlösung repräsentiert?
– Array der Länge n für ganze Zahlen
– Teillösung: Elemente 0 bis index-1 sind bereits gefüllt,
aber Korrektheit unklar!
• Wie wird geprüft, ob eine gegebene Teillösung eine
Gesamtlösung ist?
– Wenn index=n dann noch alle Bedingungen prüfen!
• Wie werden zu einer Teillösung die möglichen
Erweiterungen berechnet?
– Das Array-Element index muss neu belegt werden
– Dafür kommen nur die Zahlen in Frage, die noch nicht
benutzt sind
Beispiel: Magisches Quadrat
• Beispiel-Programm (C++)
(ohne #include etc.)
// Globale Variablen //
const int ANZAHL = 9;
// Maximale Anzahl
int
// Array mit Teillösung
feld[ANZAHL];
bool uebrig[ANZAHL];
// Übrige Zahlen
int main()
{
}
init();
// Initialisiere die Arrays
backtrack(0);
// Starte mit Teillösung 0
Beispiel: Abwechselnd Ger./Ung.
void init()
{
for (int i=0; i<ANZAHL; i++) { // Für alle Elemente...
uebrig[i] = true;
}
}
// ... setze auf „übrig“
Beispiel: Magisches Quadrat
void backtrack(int index)
{
if (index==ANZAHL) testeFertig();
else {
for (int i=0; i<ANZAHL; i++) {
if (uebrig[i]) {
}
}
}
}
feld[index] = i;
// Nächstes Element
uebrig[i] = false;
// Zahl nicht mehr übrig
backtrack(index+1);
// Löse Restproblem
uebrig[i] = true;
// Zahl wieder übrig
Beispiel: Magisches Quadrat
void testeFertig()
{
int a = feld[0] + feld[1] + feld[2];
// oben
if ( (a == feld[3] + feld[4] + feld[5]) &&
// mitte
(a == feld[6] + feld[7] + feld[8]) &&
// unten
(a == feld[0] + feld[3] + feld[6]) &&
// links
(a == feld[1] + feld[4] + feld[7]) &&
// mitte
(a == feld[2] + feld[5] + feld[8]) &&
// rechts
(a == feld[0] + feld[4] + feld[8]) &&
// l.o. r.u.
(a == feld[2] + feld[4] + feld[6]) ) {
// l.o. r.u.
cout << feld[0] << " " << feld[1] << " " << feld[2] << endl;
cout << feld[3] << " " << feld[4] << " " << feld[5] << endl;
cout << feld[6] << " " << feld[7] << " " << feld[8] << endl;
cout << endl;
}
}
Beispiel: Magisches Quadrat
• Ausgabe:
1 6 5
3 2 7
5 0 7
7 0 5
8 4 0
8 4 0
6 4 2
2 4 6
3 2 7
1 6 5
1 8 3
3 8 1
1 8 3
3 8 1
5 6 1
7 2 3
6 4 2
2 4 6
0 4 8
0 4 8
5 0 7
7 0 5
7 2 3
5 6 1
Beispiel: Damenproblem
• Das „Damenproblem“ bezeichnet folgende Aufgabe von
C.F.Gauß aus dem Jahr 1850:
„Man finde eine Stellung für
acht Damen auf einem
Schachbrett, so dass sich
keine zwei Damen gegenseitig schlagen können.“
Beispiel: Damenproblem
• Wie wird eine Teil- oder Gesamtlösung repräsentiert?
– Array der Länge n für ganze Zahlen
– Teillösung: Elemente 0 bis index-1 sind bereits gefüllt
und sortiert
• Wie wird geprüft, ob eine gegebene Teillösung eine
Gesamtlösung ist?
– Wenn index=n dann sind alle gefüllt und sortiert!
• Wie werden zu einer Teillösung die möglichen
Erweiterungen berechnet?
– Das Array-Element index muss neu belegt werden
– Dafür kommen nur die Zahlen in Frage, die noch nicht
benutzt sind
Beispiel: Damenproblem
• Beispiel-Programm (C++)
(ohne #include etc.)
// Globale Variablen //
const int ANZAHL = 8;
// Maximale Anzahl
int
// Array mit Teillösung
feld[ANZAHL];
bool uebrig[ANZAHL];
// Übrige Zahlen
int main()
{
}
init();
// Initialisiere die Arrays
backtrack(0);
// Starte mit Teillösung 0
Beispiel: Damenproblem
void init()
{
for (int i=0; i<ANZAHL; i++) { // Für alle Elemente...
uebrig[i] = true;
// ... setze auf „übrig“
}
}
void fertig()
{
for (int i=0; i<ANZAHL; i++) { // Für alle Elemente...
cout << feld[i] << " ";
}
cout << endl;
}
// ... gib sie aus
Beispiel: Damenproblem
void backtrack(int index)
{
if (index==ANZAHL) fertig();
else {
for (int i=0; i<ANZAHL; i++) {
if (uebrig[i] && konsistent(index,i) ) {
}
}
}
}
feld[index] = i;
// Nächstes Element
uebrig[i] = false;
// Zahl nicht mehr übrig
backtrack(index+1);
// Löse Restproblem
uebrig[i] = true;
// Zahl wieder übrig
Beispiel: Damenproblem
bool konsistent(int index, int i)
{
for (int z=0; z<index; z++) {
if (abs(feld[z]-i) == abs(index-z)) return false;
}
return true;
}
Beispiel: Damenproblem
• Ausgabe:
0 4 7 5 2 6 1 3
0 5 7 2 6 3 1 4
0 6 3 5 7 1 4 2
0 6 4 7 1 3 5 2
1 3 5 7 2 0 6 4
.
.
.
7 2 0 5 1 4 6 3
7 3 0 2 5 1 6 4
Weitere Backtracking Beispiele
• Folgende Probleme können elegant mit
Backtracking gelöst werden:
– Springer-Problem
– Solitaire
– Sudoku
– Labyrinth
– Hamiltonscher Kreis im Graph
– Rucksack-Problem
– Erfüllbarkeitsproblem boolscher Ausdrücke
– ...