Burrows-Wheeler-Transformation

aus Wikipedia, der freien Enzyklopädie

Die Burrows-Wheeler-Transformation (BWT) ist ein Algorithmus, der in Datenkompressionstechniken wie bzip2 Anwendung findet, dabei allerdings selbst keine Datenkompression durchführt. Die Transformation wurde von Michael Burrows und David Wheeler 1994 am DEC Systems Research Center (SRC) in Palo Alto entwickelt und basiert auf einer nicht veröffentlichten Transformation von Wheeler aus dem Jahr 1983.[1]

Eigenschaften

Die BWT ist eine umkehrbare Transformation, die aus einem Block von Eingabedaten einen gleich großen Block Ausgabedaten sowie eine wenige Bit große Zusatzinformation (einen Index) erzeugt. Die Ausgabe ist eine Permutation der Eingabe, das heißt, die Zeichenhäufigkeit von Ein- und Ausgabe ist gleich, nur die Reihenfolge kann sich ändern. Die Ausgabe lässt sich im Allgemeinen besser komprimieren, da gleiche Zeichen häufiger hintereinander stehen als in der Eingabe. Aus den Ausgabedaten und dem Index kann man die Eingabedaten wiederherstellen, also die Transformation umkehren.

Die Burrows-Wheeler-Transformation hängt von einer Reihe von Parametern ab:

  • Der verwendete Zeichenvorrat: In den meisten Fällen wird die BWT auf Bytes mit 8 Bit angewendet.
  • Die Sortierreihenfolge der Zeichen ist relativ egal, solange die Zeichen vollständig geordnet sind.
  • Der Index, der als Ausgabe der BWT entsteht, kann 1-basiert oder 0-basiert sein.

Wichtig ist, dass für die Vorwärts- und Rückwärtstransformationen diese Parameter gleich sind.

Vorwärtstransformation

Eingabe:

  • die zu codierenden Daten

Ausgabe:

  • die codierten Daten
  • ein Index

Zuerst werden alle möglichen Rotationen der zu codierenden Daten erzeugt, indem jeweils ein Zeichen vom Ende der Daten an den Anfang verschoben wird. Alle diese Rotationen werden in eine Tabelle geschrieben. Diese Tabelle wird lexikographisch sortiert. Die jeweils letzten Zeichen jeder Zeile ergeben – von oben nach unten gelesen – den codierten Text. Der Index ist die Nummer der Zeile, in der die zu codierenden Daten in der Tabelle vorkommen.

Prinzip

Die Effizienz der Burrows-Wheeler-Transformation beruht auf der Tatsache, dass in jeder Sprache bestimmte Wortteile / Silben üblicherweise nur mit einigen wenigen Buchstaben begonnen werden (z. B. (g)eg-angen, (g)eg-en, …). Durch die Sortierung der oben beschriebenen Tabelle werden solche Wortbestandteile gehäufelt. Da nun die letzte Spalte der Tabelle jeweils das Zeichen davor beinhaltet, häufen sich in der Ausgabe des Algorithmus stückchenweise eben immer die Zeichen, die das jeweilige Wort- / Textsegment am wahrscheinlichsten beginnen.

Zur Veranschaulichung dieses Effektes wird im Folgenden der Ausschnitt aus der sortierten Tabelle des transformierten Wikipedia-Artikels "Nebel" gezeigt (das erste Zeichen ist dabei immer die letzte Spalte der Tabelle, d. h. die Ausgabe):

D - ie Tröpfchendurchmesser innerhalb eines N
D - ie Unterscheidung von Nebeln in bestimmte
d - ie Verfügbarkeit von Wasserdampf und zum
d - ie die Luft enthalten kann, ohne dass Kon
w - ie die vor allem thermischen Oberflächene
s - ie häufig in Richtung von Siedlungsräumen
D - ie höchste Nebelhäufigkeit zeigt sich dab
w - ie in Auftriebsbereichen der Fall. Die wa
s - ie jedoch auch stark zwischen den einzeln
d - ie maximale Wasserdampfmenge, die die Luf
d - ie ohne bestehende Oberflächen nicht mögl
. - ...
. - ...
. - ...
l - ichen Nebeldichte führen, man spricht von
l - ichen Nebelhäufigkeit erhöht ist bzw. man
l - ichen Skalenbereiche können dabei stark s
l - ichen Ursachen und wird im Abschnitt Nebe
e - ichen der Fall. Die wahrgenommene Nebelhä
l - icher Ursachen den Taupunkt erreicht. Die
e - icher einschätzt. Auch die räumlichen Ska
n - icht allzu häufig geschieht. Wenn Nebel b
n - icht möglich ist. So kann dann auch vor a

Beispielcode in Lua

function BWT_vorwaerts(text)
  local len = string.len(text)

  -- Tabelle mit allen Rotationen des Textes erzeugen
  local matrix = {}
  for i = 1, len do
    matrix[i] = string.sub(text, i) .. string.sub(text, 1, i - 1)
  end

  -- Tabelle sortieren
  for i = 1, len do
    for j = i + 1, len do
      if matrix[i] > matrix[j] then
        matrix[i], matrix[j] = matrix[j], matrix[i]
      end
    end
  end

  -- Aus jeder Zeile das letzte Zeichen nehmen
  local codiert = ""
  local index = -1
  for i = 1, len do
    codiert = codiert .. string.sub(matrix[i], -1)
    if matrix[i] == text then
      index = i
    end
  end

  return codiert, index
end

Optimierungsmöglichkeiten

Es ist nicht nötig, die gesamte Tabelle zu speichern, da alle Zeilen den gleichen Text beinhalten, nur an unterschiedlichen Stellen anfangen. Es reicht daher, den Text nur ein einziges Mal zu speichern. Jede Tabellenzeile besteht dann nur noch aus einem Zeiger auf den Start der Zeichenkette. Je größer der zu transformierende Datenblock wird, desto mehr lohnt sich diese Optimierung. Wenn man zum Beispiel 500 Kilobyte transformiert und jede Tabellenzeile für sich speichert, braucht man 500 000 Zeilen à 500.000 Byte, also 250 Gigabyte, nur um die Tabelle im Speicher unterzubringen. Benutzt man jedoch Zeiger, braucht man nur einmal 500.000 Byte für den Text und pro Zeile 4 Byte für den Zeiger (bei 32-Bit-Speicheradressen), also zusammen 2,5 Megabyte.

Beispiel

Eingabe:

  • Zu codierende Daten: „.ANANAS.
Zuerst werden alle Rotationen erzeugt und in eine Tabelle geschrieben:
1: .ANANAS.
2: ..ANANAS
3: S..ANANA
4: AS..ANAN
5: NAS..ANA
6: ANAS..AN
7: NANAS..A
8: ANANAS..
Dann wird diese Tabelle alphabetisch sortiert.
(Bei diesem Beispielalphabet wird der Punkt am Ende einsortiert, bei einer Sortierung auf der Basis des ASCII-Codes wird der Punkt vor dem A einsortiert.)
1: ANANAS..
2: ANAS..AN
3: AS..ANAN
4: NANAS..A
5: NAS..ANA
6: S..ANANA
7: .ANANAS.
8: ..ANANAS
Die jeweils letzten Zeichen jeder Zeile werden hintereinandergeschrieben und ergeben den Ausgabetext.
Der Eingabetext kommt in der 7. Zeile vor, deshalb ist der Index 7.

Ausgabe:

  • Codierte Daten: „.NNAAA.S
  • Index: 7

Rücktransformation

Eingabe:

  • codierter Text
  • ein Index

Ausgabe:

  • uncodierter Text

Für die Rücktransformation gibt es mehrere Verfahren, die im Folgenden erläutert werden. Wenn man nur den codierten Text als Eingabe hat, kann man die Reihenfolge der Zeichen im uncodierten Text bestimmen. Das lässt aber immer noch so viele Möglichkeiten, wie der Text Zeichen hat. Damit die Umkehrung eindeutig wird, braucht man daher noch eine Zahl, die angibt, an welcher Stelle des codierten Textes der uncodierte anfängt. Diese Zahl kann aus dem Index berechnet werden.

Originalverfahren

Um den Text zurückzutransformieren, wird der Text mit einem stabilen Sortierverfahren sortiert, und beim Sortieren wird darauf geachtet, welches Zeichen an welcher Position landet. Dadurch erhält man eine Zuordnung zwischen der codierten Position (in der letzten Zeile der Codierungstabelle) und der sortierten Position (in der ersten Zeile der Codierungstabelle). Diese Zuordnung ist eine Permutation mit nur einem Zyklus, das heißt, wenn man bei einer bestimmten Position anfängt, die Permutation zu durchlaufen, erreicht man alle Elemente einmal und landet erst dann wieder bei dieser Position. Genau das wird auch bei der Rücktransformation gemacht, denn bei diesem Durchlauf kommt man an allen Zeichen des Textes vorbei, in der Reihenfolge, wie sie vor der Vorwärtstransformation angeordnet waren. Dadurch, dass man beim Index anfängt, erhält man die Zeichen in genau der Reihenfolge, wie sie vor der Vorwärtstransformation angeordnet waren.

Beispielcode in Lua

function BWT_rueckwaerts(text, index)
  local len = string.len(text)

  -- Zeichen mit zugehörigen Positionen in einer Tabelle speichern
  local tabelle = {}
  for i = 1, len do
    tabelle[i] = { position = i, zeichen = string.sub(text, i, i) }
  end

  -- Diese Tabelle nach den Zeichen sortieren. Wichtig ist hier,
  -- ein ''stabiles'' Sortierverfahren einzusetzen.
  for i = 1, len - 1 do
    for j = 1, len - 1 do
      if tabelle[j].zeichen > tabelle[j + 1].zeichen then
        tabelle[j], tabelle[j + 1] = tabelle[j + 1], tabelle[j]
      end
    end
  end

  -- Beim Index beginnend einmal durch die Tabelle
  -- wandern und dabei alle Zeichen aufsammeln.
  local decodiert = ""
  local idx = index
  for i = 1, len do
    decodiert = decodiert .. tabelle[idx].zeichen
    idx = tabelle[idx].position
  end

  return decodiert
end

Beispiel

Die Daten (Text: „a!iepdWkii“, Index: 2) sollen zurücktransformiert werden. Die Sortierreihenfolge ist: Ausrufezeichen, Großbuchstaben, Kleinbuchstaben (wie in ASCII).

Die Daten werden mit einem stabilen Sortierverfahren sortiert, und beim Sortieren wird darauf geachtet, welches Zeichen an welcher Position landet.

codierter Text a ! i e p d W k i i
Position 1 2 3 4 5 6 7 8 9 10
sortierter Text ! W a d e i i i k p
sortierte Position 2 7 1 6 4 3 9 10 8 5

Beispiel: Im codierten Text stand das große "W" an Position 7, nach dem Sortieren steht es an Position 2, zusammen mit der Information, dass es von Position 7 kommt. Das stabile Sortierverfahren ist wichtig, um die Reihenfolge der is nicht durcheinanderzubringen. In Zeile 2 stehen sie an den Positionen 3, 9 und 10, und in genau dieser Reihenfolge stehen sie auch in Zeile 3.

Die Zeile codierter Text wird nun nicht mehr benötigt. Um den Text zurückzutransformieren, fängt man bei dem Index, also 2, an. An Position 2 steht im sortierten Text ein W. Die Position des nächsten Zeichens ergibt sich aus der Zeile sortierte Position, also 7. Dort steht ein i, und es geht bei Position 9 weiter. Dort steht ein k, dann kommt 8 (i), 10 (p), 5 (e), 4 (d), 6 (i), 3 (a), 1 (!), 2 (der Index). Der Ursprungstext war also „Wikipedia!“.

Ausführlicheres Beispiel

Der Text „.NNAAA.S“ soll zurücktransformiert werden. In der sortierten Tabelle stand er in Zeile 7.

Aus diesen Daten lässt sich schrittweise die komplette sortierte Matrix rekonstruieren, und wenn das fertig ist, findet man in Zeile 7 den ursprünglichen Text.

Die erste Spalte der Matrix lässt sich ganz einfach rekonstruieren, denn sie enthält alle Zeichen des Textes, bloß sortiert:

 1: A______.
 2: A______N
 3: A______N
 4: N______A
 5: N______A
 6: S______A
 7: .______.
 8: .______S

Wenn man sich jetzt überlegt, dass in allen Zeilen der gleiche Text steht, nur rotiert, kann man nach und nach die Matrix vervollständigen. Zur besseren Übersicht kann man die letzte Spalte noch einmal vor die erste Spalte schreiben.

    8│12345678
 ────┼────────
 1: .│A______.
 2: N│A______N
 3: N│A______N
 4: A│N______A
 5: A│N______A
 6: A│S______A
 7: .│.?_____.
 8: S│.?_____S

Man sieht jetzt, dass auf einen "." entweder ein "A" folgt (Zeile 1) oder ein weiterer "." (Zeile 7). Diese beiden Zeichen müssen also in Zeile 7 und 8 an der Stelle 2 (mit Fragezeichen markiert) stehen. Da, wie schon gesagt, die Matrix sortiert ist und das erste Zeichen in diesen Zeilen gleich ist, muss das "A" in Zeile 7 stehen und der Punkt in Zeile 8. Genauso verfährt man mit den anderen Zeichen: Man sucht alle Nachfolgezeichen, sortiert sie, und trägt sie von oben nach unten in die Zeilen ein, die mit diesem Zeichen aufhören. Damit erhält man die Spalte 2.

  • Auf "A" folgen "N", "N" und "S", die kommen in Zeile 1, 2 und 3.
  • Auf "N" folgen "A" und "A", die kommen in Zeile 4 und 5.
  • Auf "S" folgt ".", der kommt in Zeile 6.
  • Auf "." folgen "A" und ".", die kommen in Zeile 7 und 8.
    8│12345678
 ────┼────────
 1: .│AN_____.
 2: N│AN_____N
 3: N│AS_____N
 4: A│NA_____A
 5: A│NA_____A
 6: A│S._____A
 7: .│.A_____.
 8: S│.._____S

Für die weiteren Spalten funktioniert dieses Verfahren leider nicht mehr. (Beispiel: Die Nachfolger von "A" sind "N", "N" und "S", und wenn man die von oben nach unten in die Zeilen, die mit "A" aufhören, einträgt, steht in Zeile 7 auf einmal ".AS____.", und das kann nicht stimmen.) Aber durch scharfes Hinsehen kann man erkennen, dass irgendwo in dem Wort die Zeichenfolge "S.." vorkommt (Zeile 8). Und es gibt nur eine Möglichkeit, diese Zeichenfolge fortzusetzen, nämlich mit "..A" (Zeile 7). Dann kommt ".AN" (Zeile 1), und nun gibt es das nächste Problem: Es könnte entweder Zeile 4 oder Zeile 5 kommen, denn beide enthalten die Zeichen "ANA". Aber auch für dieses Problem gibt es eine Lösung.

Wenn man sich beim Rekonstruieren der Spalte 2 merkt, aus welcher Zeile die Zeichen kommen und in welche sie eingesetzt werden, erhält man folgende Tabelle:

Spalte 2 in Zeile … 1 2 3 4 5 6 7 8
… kommt aus Zeile … 4 5 6 2 3 8 1 7

Diese Tabelle passt erstaunlich gut zu den Nachfolgern, die man durch scharfes Hinsehen ermitteln kann. Denn der Nachfolger von Zeile 8 ist Zeile 7, der Nachfolger von 7 ist 1, und das ist in der Tabelle genauso. Die Tabelle liefert auch die Antwort auf das Problem mit der Mehrdeutigkeit. Die richtige Reihenfolge der Zeilen kann man jetzt aus der Tabelle ablesen, indem man mit der 7 beginnt (das ist die Zeilennummer, in der der ursprüngliche Text stand) und die Zahl aus der unteren Zeile aufschreibt. Dann sucht man diese Zahl in der oberen Zeile und so weiter. Damit erhält man die Reihenfolge 1, 4, 2, 5, 3, 6, 8, 7.

Im letzten Schritt schaut man für jede dieser Zahlen nach, was in der letzten Spalte der entsprechenden Zeile steht. In Zeile 1 ist das ein ".", in Zeile 4 ein "A", in Zeile 2 ein "N", und wenn man alle diese Zeichen aneinanderhängt, erhält man den Text ".ANANAS.".

Alternatives Verfahren

Dieses Verfahren ist rechenaufwändiger als das Originalverfahren und deshalb eher zur Demonstration geeignet als zur Umsetzung in Computerprogrammen. Es basiert auf der Idee, ausgehend von dem codierten Text, der in der Tabelle der Vorwärtstransformation in der letzten Spalte stand, alle anderen Spalten schrittweise zu rekonstruieren. Wenn dieses Ziel erreicht ist, kann man in der Zeile, die der Index angibt, den zurückcodierten Text ablesen.

  1. Führe die folgenden Schritte aus, bis die Tabelle voll ist:
    1. Der codierte Text wird zeichenweise senkrecht in die letzte Spalte der Tabelle geschrieben.
    2. Die Tabelle wird dann um eine Position nach rechts rotiert, so dass die letzte Spalte zur ersten Spalte wird.
    3. Dann wird die Tabelle sortiert.
  2. Der Text in der Zeile Index ist der gesuchte, zurückcodierte Text.

Erläuterung

Die Tabelle, die bei der Vorwärtstransformation benutzt wird, hat eine wichtige Eigenschaft, die bei dieser Rückwärtstransformation ausgenutzt wird: Die Zeichen der letzten Spalte (und auch jeder anderen) kommen mit der gleichen Häufigkeit in der ersten Spalte der Tabelle vor, bloß sortiert. Das heißt, wenn die erste Spalte nicht bekannt ist, aber eine beliebige andere, kann man die erste Spalte daraus konstruieren.

Nach dem Füllen der letzten Spalte (Schritt 1.1) entspricht der bereits ausgefüllte Teil der Tabelle derjenigen, die auch schon für die Vorwärtstransformation benutzt wurde. Durch das Rotieren und anschließende Sortieren (Schritte 1.2 und 1.3) werden die vorderen Spalten der Tabelle korrekt ausgefüllt, da die Tabelle für die Vorwärtstransformation genauso sortiert war. Durch das Rotieren (Schritt 1.2) wird die letzte Spalte wieder frei, so dass sie wieder gefüllt werden kann, und zwar mit Daten, die zu den schon ausgefüllten vorderen Spalten passen.

Beispielcode in Lua

function BWT_rueckwaerts(text, index)
  local len = string.len(text)

  -- Am Anfang ist die Tabelle leer
  local tabelle = {}
  for i = 1, len do
    tabelle[i] = ""
  end

  for n = 1, len do
    -- Codierten Text zeichenweise vor die erste Spalte setzen
    for i = 1, len do
      tabelle[i] = string.sub(text, i, i) .. tabelle[i]
    end

    -- Tabelle sortieren
    for i = 1, len do
      for j = i + 1, len do
        if tabelle[i] > tabelle[j] then
          tabelle[i], tabelle[j] = tabelle[j], tabelle[i]
        end
      end
    end
  end

  return tabelle[index]
end

Siehe auch

  • Move to front, ein Kodierungsverfahren, das häufig nach der Burrows-Wheeler-Transformation eingesetzt wird.

Einzelnachweise

  1. Ursprüngliche Veröffentlichung: M. Burrows, D. Wheeler: A block sorting lossless data compression algorithm. Technical Report 124. Digital Equipment Corporation, 1994 (PDF; 108 kB)