minpic01.gif

Squeeze, LZH & Co.

Kompressionsverfahren implementiert

Herbert zur Nedden

Ähnlich Parkplätzen, deren Zahl nicht mit der Motorisierungswelle mithalten kann, sind Massenspeicher dem steigenden Datenandrang selten auf Dauer gewachsen. Hochauflösende Bilder, Grafik in Echtfarben, Sound-Samples - all dieses frißt ungemein viel Speicher. Anders als Fahrzeuge lassen sich die langen Bytefolgen allerdings reversibel zusammenquetschen, so daß das teure Speichermedium oder die Datenleitung effizienter einsetzbar werden.

Unterthema: Lauflängen-Kodierung
Unterthema: Huffman-Baum
Unterthema: Squeeze
Unterthema: Lempel-Ziv-Welch-Kompression
Unterthema: LZW mit fester Codelänge von 12 Bit (Crunchen)
Unterthema: LZW, variable Codelänge und Codewiederverwendung
Unterthema: Lempel-Ziv-Storer-Symanski
Unterthema: Arithmetische Kompression

Kompressionsverfahren bestehen aus zwei Teilen: einem Modell und einer Kodierung. Aufgabe des Modells ist es, die Charakteristik der Originaldaten zu erfassen. Das kann sich auf die Häufigkeiten von Bytes beschränken, aber auch das Vorkommen von Bytefolgen sein. Die Kodierung sorgt für die platzsparende Verschlüsselung der Daten. Ein Beispiel dazu: Das Hauptmodell des Squeezing geht nach der Häufigkeitsverteilung unabhängig voneinander betrachteter Bytes vor; als Kodierung dieses Modells kommt das Huffman-Verfahren zum Einsatz.

Bei der Implementierung der Algorithmen helfen Pseudocode-Listings in den entsprechenden Kästen. Der Übersichtlichkeit halber beschränken sich die Angaben auf den Kern des jeweiligen Algorithmus; Vor- und Nachbereitungsroutinen sind nicht enthalten. Der Datentyp Pointer bezeichnet eine Information, die als Zeiger verwendet werden kann; im allgemeinen ist das ein 16-Bit-Wert. Da die Kodierungen oft nicht nur auf Bytes, sondern auf beliebige Zeichenmengen (die Huffman-Kodierung ist für jede abzählbare Menge möglich) anwendbar sind, werden die Daten an den entsprechenden Stellen als Zeichen bezeichnet.

Lauflängenkodierung

Die Lauflängen-Kodierung (Run-Length-Encoding) ist ein sehr einfaches Verfahren - nicht besonders effizient, aber dafür sehr schnell. Zeichen, die mehrmals hintereinander vorkommen, werden nur einmal zusammen mit einem Zähler kodiert. Da auch der Zähler Platz kostet, bleiben zwei- oder dreimal vorkommende Zeichen unkodiert. Es gibt zwei Wege, den Zähler zu kodieren: die Bit-7-Lauflängen-Kodierung und die Standard-Lauflängen-Kodierung.

Wenn die Zeichen Bytes aus dem Bereich von 00h-7Fh sind, ist das für die Kodierung des Zählers nutzbar, indem man mehrfach vorkommende Bytes durch den Zähler mit gesetztem siebten Bit gefolgt vom eigentlichen Zähler kodiert. Dieses Verfahren ist besonders für Textdateien geeignet, in denen selten Bytes mit gesetztem siebten Bit vorkommen. Solche Sonderzeichen sind mit vorangestelltem Zähler 81h zu kodieren.

Die Standard-Lauflängenkodierung kodiert mehrfach vorkommende Bytes in drei Bytes: das ursprüngliche Byte, 90h, Zähler. Da der maximale Zählerstand 255 sein kann, müssen größere Vorkommen unterteilt werden. Weiterhin ist für 90h eine besondere Kodierung nötig, da dieses Byte normalerweise bedeutet: `Zähler folgt´. 90h wird daher als 90 00h kodiert.

Huffman-Kodierung

Das Huffman-Verfahren kodiert häufig vorkommende Zeichen mit wenigen und seltene Zeichen mit mehr Bits. Als Beispiel mögen Originaldaten dienen, die nur die vier Buchstaben A, B, C und D enthalten. Für vier verschiedene Zeichen genügen zwei Bit: 00b, 01b, 10b und 11b). Kommt das A zehnmal, das B fünfmal, das C dreimal und das D zweimal vor, belegen die Daten in durchgehender Zwei-Bit-Verschlüsselung 40 Bit. Kodiert man jedoch das A als 1b, das B als 01b, das C als 001b und das D als 000b, dann genügen 35 Bit.

Zur Festlegung der jeweiligen Bit-Kodierungen sind die Zeichenhäufigkeiten zu ermitteln. Dafür gibt es drei Wege:

Stehen die Häufigkeiten der vorkommenden Zeichen fest, können die Bitkombinationen in passenden Längen verteilt werden, wobei seltene Zeichen besonders viele Bits erhalten. Am Beginn stehen die beiden seltensten; sie bekommen eine 0b beziehungsweise 1b. Nun faßt man beide zusammen und ersetzt sie in der Aufstellung der Häufigkeiten durch ihre Summe. Dann werden wieder die beiden seltensten gesucht, eins um 0b, das andere um 1b erweitert und die beiden zusammengefaßt. Dieses Verfahren endet, wenn nur noch ein Zeichen übrig ist. Bekommt eine Zusammenfassung von Zeichen eine 0b oder 1b an ihren Code gehängt, so bedeutet das, daß die Codes aller zugehörigen Zeichen um dieses Bit verlängert werden.

Die Ermittlung der Codes aus den Häufigkeiten der Zeichen führt zu einem Baum (siehe Kasten `Huffman-Baum´). Dieser Baum dient zum Kodieren und Dekodieren nach Huffman: Das Kodieren geht vom Zeichen aus zur Wurzel hin und ergibt so den Binärcode, der für das Zeichen stehen soll; die Länge des Codes ist die Länge des zurückgelegten Weges.

Das Dekodieren beginnt an der Wurzel mit dem Einlesen eines Bits. Je nach Inhalt verzweigt man im Baum nach der einen (1b) oder anderen (0b) Seite, liest das nächste Bit und folgt wieder entsprechend der nächsten Verzweigung. Am Ende, gewissermaßen auf einem Blatt des Baumes, steht das gesuchte Zeichen.

Stehen die Häufigkeiten vorher fest (statisch), ist die Kodierung nur dann optimal, wenn die angenommenen Häufigkeiten genau passen. Dafür entfällt das vorherige Ermitteln und das Mitübertragen von Häufigkeitsverteilungen.

Für die dynamische Kodierung müssen erst einmal alle Zeichen gezählt werden, um deren Häufigkeiten zu ermitteln und daraus den geeigneten Baum für die Kodierung aufzubauen. Erst im zweiten Durchgang erfolgt die eigentliche Kodierung. Weiterhin müssen die ermittelten Häufigkeiten oder der daraus erstellte Baum an den Dekodierer mitgegeben werden.

Beginnt man die Kodierung mit einem Startwert für die Häufigkeiten und paßt sie mit jedem verarbeiteten Zeichen an (adaptierend), ist die Kodierung zwar zu Beginn nicht optimal, wird aber im Laufe der Zeit immer besser. Der Vorteil dieser Methode gegenüber der dynamischen ist, daß auch der Dekodierer die Häufigkeiten ermittelt und den Baum selbst aufbauen kann. Folglich brauchen bei dieser Art der Huffman-Kodierung weder die Zeichen vorher gezählt noch der Baum übertragen zu werden.

In einigen Fällen kann die adaptierende Huffman-Kodierung sogar besser als die dynamische sein: Ändert sich nämlich die Charakteristik der Daten, zum Beispiel weil sich Programm- und Textteile abwechseln, so paßt sich der adaptierende Huffman jeweils an die Maschinencode- und Textstruktur an.

Squeeze und Unsqueeze

Squeeze verwendet zur Kompression (nach einer Standard-Lauflängen-Kodierung) die dynamische Huffman-Kodierung. Das Modell von Squeeze ist die Häufigkeit von Bytes, wobei diese völlig unabhängig voneinander betrachtet werden. Dadurch ist die Kompressionsrate nur mäßig: Die Annahme, daß alle Bytes stochastisch unabhängig voneinander vorkommen, stimmt meist nicht; zum Beispiel folgt auf ein `q´ fast immer ein `u´.

Die Dekompression kann entweder den von Squeeze gelieferten Baum einlesen und bei jedem gelesenen Bit den entsprechenden Wert aus dem Baum auswerten (siehe Kasten `Squeeze´) oder, sicherlich schneller, aus dem Baum gleich die ensprechenden Maschinenbefehle aufbauen. Zu jedem Knoten des Baumes wird ein Stück Code erzeugt, der das nächste Bit einliest und entsprechend verzweigt, das heißt zu dem Stück Code springt, welches dem Unterbaum entspricht beziehungsweise das gefundene Zeichen ausgibt. Diese Methode ist sehr schnell, denn der Baum muß nur einmal interpretiert werden.

Das Squeeze-Modell geht ausschließlich anhand von Byte-Häufigkeiten vor. Aber schon Programm-Sources enthalten viele sich häufig wiederholende Zeichenfolgen. Die Idee von Lempel und Ziv war nun, vorkommende Zeichenfolgen mit eigenen Codes zu versehen. Bei auf Lempel-Ziv basierenden Kompressionen ist die Bitlänge der Codes zwar fest oder zumindest vorhersehbar, aber dafür können sich hinter einem solchen Code eine ganze Menge von Zeichen verbergen. Es gibt einige Kompressionsprogramme, die auf Lempel-Ziv aufbauende Algorithmen verwenden.

Lempel-Ziv-Welch

Der LZW-Algorithmus ist mitterweile in zwei Versionen in Kompressionsprogrammen implementiert: Die einfachere Variante verwendet eine feste Codelänge und kein Code-Recycling. Effizienter ist der LZW mit variabler Codelänge und Wiederverwendung einmal vergebener Codes, wenn diese nicht gebraucht werden. Die Länge der Codes hängt von der Größe des `Gedächtnisses´ ab. Ursprünglich war dieses 4096 Einträge lang, so daß 12-Bit-Codes genügten. In neueren Programmen kommen auch Codelängen bis 16 Bit vor.

Das Gedächtnis ist eine Tabelle schon erkannter Zeichenfolgen. Die Originaldaten werden so lange gelesen, wie die Zeichen als Zeichenfolge in der Tabelle enthalten sind; das Lesen wird beim Auftreten einer neuen Zeichenfolge unterbrochen. Diese ist um ein Zeichen länger als eine schon gespeicherte, wird `gelernt´ und das neue Zeichen an die vorhandene Zeichenfolge gehängt. Dann geht es mit dem Lesen weiter, wobei das zuletzt gelesene Zeichen zum Ausgangspunkt der nächsten zu lernenden Zeichenfolge wird. Umgekehrt arbeitet die Dekompression: Sie hängt nach dem Dekodieren eines Codes das erste Zeichen der eben dekodierten Zeichenfolge an die vorhergehende.

Zu Beginn der Kompression und Dekompression enthält die Tabelle den gesamten Zeichenvorrat als Zeichenfolgen der Länge eins. Wenn es nun zum Beispiel die Zeichenfolge `ABCDEFGH´ zu lernen gibt, stehen hernach zusätzlich die Folgen `AB´, `ABC´, `ABCD´ und so weiter in der Tabelle. Die Einträge der Tabelle bestehen aus je einem Pointer namens Pred (kurz für Predecessor = Vorgänger) und einem Zeichen, genannt Suffix (Anhängsel). Im Beispiel enthält der Eintrag für die Zeichenfolge `ABCD´ als Suffix das Zeichen `D´ und als Pred einen Zeiger auf den Eintrag der Zeichenfolge `ABC´. Im folgenden stehen die Einträge der Tabelle als Paare der Form (Pred,Suffix).

Es gibt, abgesehen von Zeigern auf andere Tabelleneinträge, einige besondere Pred-Werte: `NoPred´ kennzeichnet Einträge, die den Anfang einer Zeichenkette bilden (No Pred = Kein Vorgänger). Nach der Initialisierung enthält die Tabelle nur Zeichenfolgen der Länge eins als (NoPred, Zeichen). `ImPred´ sperrt Einträge gegen Mißbrauch (Impossible Pred = Unmöglicher Pred), weil die zugehörigen Codes eine besondere Bedeutung haben. `Free´ schließlich signalisiert, daß der Eintrag in der Tabelle frei ist. Die Vergabe freier Tabellenplätze erfolgt durch Hashing, das heißt durch Rechnen mit (Pred, Suffix), damit die Suche nach einem bestimmten Eintrag schneller geht.

Diese Version des LZW verwendet immer 12-Bit-Tabellen-Zeiger (4k-Tabelle); größere Tabellen benötigen eine entsprechend erhöhte Codelänge. Die Tabelleneinträge erfolgen abhängig vom Inhalt (Pred, Suffix). Die Nummer des Tabelleneintrags besteht aus den mittleren 12 Bit des Quadrats der Summe aus Pred und Suffix. Ist der so errechnete Tabelleneintrag belegt, muß ein Ausweicheintrag herhalten. Der aktuelle Eintrag erhält einen entsprechenden Verweis. Dazu dient ein zusätzliches, Link (Verbindung) genanntes Pointer-Feld je Tabelleneintrag.

Die Suche nach dem Ausweicheintrag geschieht wie folgt: Erst wird der Tabelleneintrag, auf den Link zeigt, untersucht. Gibt es ihn, geht die Suche mit eben diesem weiter. Hat das auch nicht funktioniert, addiert man 101, eine Primzahl, auf die aktuelle Tabellenposition und sucht von dort an vorwärts den nächsten freien Tabelleneintrag; dabei wird nach dem letzten Tabellenplatz mit dem ersten weitergemacht. Ist er gefunden, wird dessen Position in Link eingetragen und der neue anschließend belegt. Im Rahmen der Initialisierung werden die Link-Werte als leer vorbelegt.

Der Grund für die Ermittlung des Tabelleneintrags für (Pred, Suffix): Die Suche nach einem bestimmten (Pred, Suffix) in der Tabelle ist erheblich schneller, als diese sequentiell abzugrasen. Deswegen werden auch die Ausweicheinträge vermerkt; schließlich wird bei der Kompression laufend in der Tabelle gesucht. Die Suche funktioniert damit dergestalt, daß erst aus (Pred,Suffix) der passende Tabelleneintrag errechnet wird; anschließend muß nur noch entlang der Link-Kette nachgesehen werden, um festzustellen, ob der gesuchte Eintrag in der Tabelle steht oder nicht. Ist die Tabelle voll, werden alle weiteren Zeichenfolgen auf Basis der bisherigen Eintragungen kodiert.

Code-Recycling

Der erweiterte LZW komprimiert im allgemeinen besser als der einfache, da er über zwei Besonderheiten verfügt:

Die variable Codelänge ist nur anwendbar, wenn die Tabelle von unten an aufgefüllt wird. Andererseits ist das Errechnen der Tabellenplätze äußerst sinnvoll, um die Prüfung, ob ein bestimmter (Pred, Suffix) schon in der Tabelle steht, zu beschleunigen. Deshalb verwendet diese LZW-Version ein weiteres Pointer-Array, genannt Hash. Soll ein neuer Eintrag in die Tabelle, so wird im Hash-Array ein freier Platz ermittelt und dort eingetragen, wohin der neue Eintrag in die Tabelle kommt. Der neue Tabelleneintrag ist einfach der nächste freie. Im Rahmen der Initialisierung wird Zeiger mit dem Wert leer vorbelegt. Das Array Hash hat mehr Einträge als die Tabelle, um so die Möglichkeit einer Kollision beim Hashing zu verringern. Bei einer Kollision wird die Primzahl 5003 so lange auf die aktuelle Position addiert, bis ein freier Platz gefunden wurde.

Die Suche nach einem bestimmten (Pred, Suffix) funktioniert entsprechend: Es wird der zugehörige Eintrag in Hash errechnet; dort steht die Adresse des in der Tabelle gesuchten (Pred, Suffix). Komprimieren und Dekomprimieren führen über die Anzahl der in der Tabelle belegten Einträge Buch und passen die Codelänge immer an, wenn ein weiteres Bit für die Adressierung erforderlich ist. Auf diese Weise ist es möglich, mit kurzen Codelängen anzufangen, ohne darauf verzichten zu müssen, die Adresse von Tabelleneinträgen errechnen und damit schneller finden zu können.

Wenn eine einmal gelernte Zeichenfolge nie mehr vorkommt, kann man sie aus der Tabelle entfernen und den freigewordenen Platz für eine andere Zeichenfolge verwenden. Der Dekompressionsablauf des erweiterten LZW unterscheidet sich vom einfachen, wenn die Gedächtnistabelle voll ist. Statt nur mit dem erworbenen Wissen weiterzumachen, versucht es, unnötiges zu vergessen und damit neues zu lernen. Für die Entscheidung, ob Tabelleneinträge vergessen werden dürfen, erhalten sie `referenziert´-Kennzeichnungen und sind damit löschgeschützt. Bei der Initialisierung werden die Einträge mit Pred = NoPred und mit Pred = ImPred ebenso geschützt.

Beim Crunchen werden die Daten anhand des LZW-Algorithmus komprimiert, wobei die Zeichenfolgen aus Bytes gebildet werden - und zwar je nach Version des Programmes mit fester Codelänge oder mit variabler Codelänge und Codewiederverwendung. Einige Crunch-Versionen haben reservierte Codes für besondere Funktionen. Um Problemen vorzubeugen, werden diese Codes bei der Initialisierung der Tabelle mit Pred = ImPred, also als gesperrt, eingetragen.

Lempel-Ziv-Storer-Szymanski

LZSS ist eine Abwandlung des LZW: Es verwendet anstelle der Tabelle als Gedächtnis einen Ringpuffer, in dem die letzten gelesenen Zeichen der Originaldatei stehen. Vor der Komprimierung einer Zeichenfolge wird nachgesehen, ob sie im Puffer steht. Falls ja, wird ihre Länge und Adresse relativ zur aktuellen Position im Ringpuffer übertragen. Obwohl dieser Algorithmus sehr einfach erscheint (die Tabelle mit den Zeichenfolgen und das Hashing des LZW entfallen), ist die Kompressionsrate verblüffend.

Beim Vergleich der Wissensbeschaffung von LZW und LZSS fällt folgender Unterschied auf: LZW baut eine Tabelle aus den Zeichenfolgen auf, die am Dateianfang stehen - anschließend ist diese Tabelle mit dem Wissen recht statisch. LZSS hingegen sucht die Zeichenfolgen im Puffer, in dem die letzten paar KByte gelesener Daten stehen. Ändern sich die Zeichenfolgen, die in der Datei vorkommen, paßt sich LZSS automatisch an. Das erinnert an den Unterschied zwischen dem dynamischen und dem adaptiven Huffman.

Beim LZSS-Algorithmus gibt es drei zentrale Konstanten: die Größe des Ringpuffers, die maximale Länge einer Zeichenfolge - mehr Zeichen werden beim Vergleichen einfach nicht herangezogen - und die Mindestlänge einer Zeichenfolge. Diese drei Konstanten heißen im folgenden Puffergröße, MaxLänge und MinLänge. Der Wert MinLänge-1 wird im allgemeinen als Schwelle (Threshold) bezeichnet.

Kompressionsprogramme reservieren für den Puffer meist einen Speicherbereich, der MaxLänge-1 Zeichen länger als die Puffergröße ist. Die zusätzlichen Zeichen hinten am Puffer haben dabei stets denselben Inhalt wie die ersten MaxLänge-1 Zeichen. Sie stehen hier ein zweites Mal, damit das Prüfen, ob eine Zeichenfolge im Puffer steht, am Pufferende schneller erfolgen kann.

Weiterhin erzeugt die Kompression entweder die Originaldaten oder eine Längenangabe, gefolgt von einer Adreßangabe. Damit der Dekompressor die erhaltenen Informationen auch verstehen kann, muß er zwischen einem normalen Zeichen und einer Längenangabe unterscheiden können. Dazu wird einfach die Zeichenmenge um Zeichen für die Längenangaben erweitert. Wie bei der Standard-Lauflängen-Kodierung wäre auch ein Zeichen für `Längenangabe folgt´ verwendbar; das ist aber eigentlich auch nichts anderes als eine Erweiterung der Zeichenmenge.

Üblicherweise sind die Zeichen, die der LZSS-Algorithmus verarbeitet, ganz normale Bytes, also Werte von 00h bis FFh. Die Längenangaben werden dann durch die nächsten möglichen Werte, also 100h, 101h, und so weiter dargestellt. Um aus einer Längenangabe den zugehörigen Zeichencode zu erhalten, addiert man dann 100h minus MinLänge dazu. Dieses neunte Bit ist auf den ersten Blick gewöhnungsbedürftig, doch für eine Kodierung wie Huffman ist die Art der Zeichen belanglos - Hauptsache, sie sind zählbar.

Lempel-Ziv-Huffman

Die LZH-Kompression kodiert das LZSS-Modell mit dem adaptiven Huffman - mit einer kleinen Ergänzung in der Art der Kodierung der Adreßangaben. Ausschlaggebend für Huffman ist, daß diese Kodierung beliebige Zeichenmengen effizient kodieren kann. Für Bytefolgen der Längen zwischen drei und 60 sind 256 + 58 Codes (256 Bytes und 58 Längenangaben) erforderlich, da beim adaptiven Huffman die Häufigkeitstabelle (und damit der Baum) vor Beginn der Kompression mit `alle möglichen Zeichen kommen gleich oft vor´ initialisiert wird. Die krumme Anzahl von 256 + 58 Codes stört dabei nicht.

Eine ältere LZH-Implementation verwendet einen 4 kB langen Ringpuffer, so daß die Adreßangaben 12 Bit lang sind. Diese werden ebenfalls kodiert: Die oberen 6 Bit der Adresse werden über eine Tabelle in drei bis acht Bit kodiert. Dabei erhalten kleine Werte wenige Bit; da die Adreßangaben relative Adressen zur aktuellen Pufferposition sind, werden `junge´ Zeichenfolgen bevorzugt. Die unteren 6 Bit bleiben unverändert; insgesamt ist die Adreßangabe damit neun bis 14 Bit lang.

Beim Dekodieren einer Adreßangabe werden erst einmal acht Bit gelesen. Diese umfassen auf jeden Fall den Code für die oberen 6 Bit der Adreßangabe. Über eine Tabelle werden diese und die Länge des Codes, mit dem die oberen 6 Bit kodiert wurden, ermittelt. Ist diese Länge kleiner als acht, stehen in den unteren Bit des eben gelesenen Bytes schon acht minus Länge Bit der unteren 6 Bit der Adreßangabe. Da insgesamt Länge plus 6 Bit ausgegeben wurden und bislang 8 Bit gelesen wurden, müssen noch Länge minus 2 Bit nachgelesen werden, um alle Informationen zu erhalten.

Arithmetische Kodierung

Häufigkeiten lassen sich mit reellen Zahlen zwischen 0 und 1 repräsentieren; dabei bedeutet 0 nie und 1 immer; für die bequemeren Prozentangaben sind die Häufigkeiten mit 100 zu multiplizieren. Ein Intervall besteht aus den Zahlen, die zwischen zwei Randpunkten (Intervallgrenzen genannt) liegen, von denen keiner, einer oder beide zum Intervall dazugehören können. [a, b) bezeichnet das Intervall der Zahlen, die zwischen a und b liegen, wobei a dazugehört (eckige Klammer), b jedoch nicht (runde Klammer). [0, 1) bezeichnet folglich alle Zahlen, die größer oder gleich Null und kleiner als Eins sind.

In der arithmetischen Kodierung sind die Zeichen als Intervalle der Form [a, b) darzustellen, deren Längen den Häufigkeiten entsprechen und, beginnend bei Null, hintereinander- gelegt werden. Da die Summe der Häufigkeiten 1 ist, ergibt diese Kette von Intervallen das Intervall [0, 1). Für möglichst kurze Codes nimmt man natürlich die Zahl mit den wenigsten Stellen, die in dem Intervall des Zeichens liegt. Da alle vorkommenden Zahlen im Intervall [0, 1) liegen, sind sie der Form Null-Komma-Irgendwas; die Kodierung beschränkt sich folglich auf die Nachkommastellen.

Nimmt man das Beispiel mit den Buchstaben `A´ bis `D´ und den Häufigkeiten 0.5, 0.25, 0.15 beziehungsweise 0.1 für die einzelnen Zeichen, baut daraus die Intervalle [0, 0.5), [0.5, 0.75), [0.75, 0.9) und [0.9, 1) auf und ermittelt die passenden Binärcodes, so ergeben sich die vier Codes 0b, 1b, 11b und 1111b entsprechend den Dezimalbrüchen 0.0, 0.5, 0.75 und 0.9375.

Dies ist aber nur der erste Schritt der Kodierung. Bei der arithmetischen Kodierung werden nämlich Zeichenfolgen und nicht einzelne Zeichen in einen Code umgesetzt. Und das geschieht wie folgt: Das erste Zeichen einer Folge liefert ein Intervall. Dieses Intervall ist wiederum genauso wie zu Beginn das Intervall [0, 1) entsprechend der Häufigkeiten der Zeichen teilbar. Dann führt das zweite Zeichen wieder zu einem Intervall, welches im ersten liegt, und so weiter. `BBBB´ zum Beispiel wird über die Intervall-Folge [0.10b, 0.11b) - [0.1010b, 0.1011b) - [0.101010b, 0.101011b) - [0.10101010b, 0.10101011b) in die Zahl 0.10101010b und damit in den Code 10101010b umgesetzt.

Da die Intervallschachtelung nicht unendlich weitergehen kann, muß das Ende einer kodierten Zeichenfolge erkennbar sein. Dazu dient ein ansonsten ungenutztes Zeichen. Normalerweise sind Bytefolgen (Zeichen 00h bis FFh) zu kodieren. Also nimmt man einfach ein weiteres Zeichen als Endekennung - es kommt natürlich in einer Zeichenfolge nur einmal vor - und berechnet die 257 erforderlichen Häufigkeiten.

Die Ermittlung der Häufigkeiten einzelner Zeichen kann, wie bei Huffman, auf dreierlei Arten erfolgen: statisch, dynamisch und adaptierend. Bleiben noch ein paar Anmerkungen zur Implementierung:

Arithmetisch versus Huffman

Das arithmetische und das Huffman-Verfahren verfolgen dasselbe Ziel: zu gegebenen Häufigkeiten einer beliebigen Zeichenmenge möglichst kurze Codes bereitzustellen - im adaptiven Fall passiert das in jedem Schritt. Der Unterschied ist, daß Huffman Codes pro Zeichen liefert, während die arithmetische Kodierung Codes für Zeichenfolgen erzeugt. Es ist mathematisch beweisbar, daß die arithmetische Kodierung effizienter als Huffman ist; diesen Beweis muß ich Ihnen jedoch an dieser Stelle schuldig bleiben.

Die hier beschriebenen Kompressionsalgorithmen sind nicht für bestimmte, sondern für beliebige Daten vorgesehen. Man kann sie auf diversen Systemen von CP/M bis DOS oder Unix finden. Je nachdem, wie die Daten wirklich aussehen, kann das eine oder andere Verfahren das bessere sein. Als Faustregel läßt sich jedoch sagen, daß die durchschnittliche Kompressionsrate von Squeeze über LZW, LZH bis LZAri immer besser wird. Es gibt auch Kompressionsprogramme wie zum Beispiel PKARC, die die Daten erst analysieren und dann entscheiden, welcher Algorithmus zur Anwendung kommt.

Eine andere Situation für die Entwicklung spezieller Algorithmen ergibt sich, wenn es mehr auf die Geschwindigkeit als auf die Kompressionsrate ankommt. Die hier beschriebenen Algorithmen sind im allgemeinen recht zeitintensiv, was nicht immer angebracht ist. Zum Beispiel bei der Speicherung und Übertragung von Bewegtbildern kommt es vor allem auf die Geschwindigkeit der Kompression und Dekompression an; teilweise sogar unter Verzicht auf die exakte Wiederherstellung der Daten. (un)

Literatur

[1] Frank Bauernöppel, Verfahren und Techniken zur Datenkompression, c't 10/91, S. 278

Kasten 1


Lauflängen-Kodierung

mnqpic01.gif

Kasten 2


Huffman-Baum

mnqpic02.gif

Kasten 3


Squeeze

mnqpic03.gif

Kasten 4


Lempel-Ziv-Welch-Kompression

mnqpic04.gif

Kasten 5


LZW mit fester Codelänge von 12 Bit (Crunchen)

mnqpic05.gif

Kasten 6


LZW, variable Codelänge und Codewiederverwendung

mnqpic06.gif

Kasten 7


Lempel-Ziv-Storer-Symanski

mnqpic07.gif

Kasten 8


Arithmetische Kompression

mnqpic08.gif

Stand 10-19-1999 TB


Quelle: ct 7/92