Burrows-Wheeler Transform Compression

The Burrows-Wheeler Transform is a fully reversible algorithm that rearranges the characters in a block of data. It does not compress the data itself but permutes it in such a way that characters with similar contexts are grouped together, making the output highly amenable to other compression algorithms like RLE and entropy coding.

A Paradigm Shift in Lossless Compression

In our journey through lossless compression algorithms, we have encountered two major philosophies. Statistical methods, like Huffman coding, excel by analyzing the frequency of individual symbols. Dictionary-based methods, like the Lempel-Ziv family, achieve compression by finding and referencing exact repetitions of entire strings. Both are incredibly powerful, but they operate on distinct types of redundancy. This raises a compelling question: is there an approach that can capture a more subtle, contextual kind of redundancy, one that is not just about how often a character appears, but about the environments in which it appears?

This is where the enters the stage. Invented by Michael Burrows and David Wheeler in 1994, BWT is not a compression algorithm in itself. Instead, it is a brilliant and computationally intensive pre-processing step, a reversible transformation that shuffles a block of data. The magic of BWT is that it does not just shuffle the data randomly; it reorganizes it in a profound way, clustering characters that tend to appear in similar contexts next to each other. This process converts subtle, long-range contextual patterns into simple, highly localized repetitions that are trivial for other, simpler compression algorithms to handle. BWT is the core engine behind the popular bzip2 archiver, known for often achieving better compression ratios than traditional DEFLATE-based methods like gzip on large files.

The Forward BWT Transform: Sorting and Extracting

To understand BWT, it is crucial to follow the forward transformation step-by-step. The process is based on block-sorting, which means it operates on a chunk of data all at once, rather than streaming it byte-by-byte. Let us transform the example string "BANANA".

Step 1: Create All Cyclic Rotations

First, we need to create a list of every possible cyclic shift, or rotation, of the input string. A cyclic shift is what you get if you move the first character to the end of the string, repeatedly. It is often helpful to imagine the string is written in a circle.

BANANA
ANANAB
NANABA
ANABAN
NABANA
ABANAN

Step 2: Lexicographically Sort the Rotations

The next, and most computationally significant, step is to sort this list of rotations alphabetically. This is called , the same way you would sort words in a dictionary.

IndexSorted Rotation
0ABANAN
1ANABAN
2ANANAB
3BANANA
4NABANA
5NANABA

Step 3: Extract the Output - The Last Column

The primary output of the BWT is remarkably simple: it is the sequence of characters in the last column of this sorted table. This column is often referred to as LL.

L = NNB A A A

Observe what has happened. The original, mixed string "BANANA" has been transformed into "NNBAAA". The transform has clustered identical characters together. All the 'A's are now in a contiguous run, which is extremely easy for a simple compressor like Run-Length Encoding to handle. This grouping is the entire purpose of the transform.

Step 4: Record the Primary Index

There is one more crucial piece of information we need to save. For the transform to be reversible, we must know which row in the sorted table corresponds to our original, unsorted string. In our example, the original string "BANANA" is in row 3 of the sorted table. Therefore, our primary index, often denoted as II, is 3.

The complete output of the forward Burrows-Wheeler Transform for "BANANA" is the pair: ("NNBAAA", 33).

The Inverse BWT Transform: Reconstructing the Original

The reverse process seems daunting at first. How can we possibly unscramble the original string using only the last column and an index number? The key lies in a remarkable and non-obvious property that connects the first and last columns of the sorted matrix.

The Last-to-First (LF) Mapping Property

Let us look again at our sorted matrix. Let's call the first column FF and the last column LL.

F L
A N
A N
A B
B A
N A
N A

Notice that FF is simply a sorted version of the original string's characters (A,A,A,B,N,N). This is a critical insight: we can reconstruct FF just by sorting the characters of LL!

The crucial property that makes BWT reversible is the Last-to-First (LF) Mapping:

For any row in the sorted matrix, the character in the Last column precedes the character in the First column in the original, unsorted string. Furthermore, the kk-th occurrence of a specific character in column LL corresponds to the kk-th occurrence of that same character in column FF.

This mapping allows us to "jump" from a character in LL to its corresponding position in FF, and since each character in FF is the beginning of a sorted rotation, this effectively allows us to reconstruct the original string backwards.

Detailed Inverse Walkthrough

Let us reconstruct "BANANA" using only our received information: L=L = "NNBAAA" and I=3I = 3.

  1. Step 1: Reconstruct First Column (F): Sort the characters of LL alphabetically. Sorting "NNBAAA" gives us F=F = "AAABNN".
  2. Step 2: Start with the Primary Index: Our index I=3I=3 tells us the original string ended with the character at L[3]L[3]. Looking at our string L=N0N1B2A3A4A5L = N_0N_1B_2A_3A_4A_5, L[3]L[3] is 'A'. So, the last character of our original string is 'A'.
  3. Step 3: Apply the LF-Mapping: The character we just found ('A') is the 1st 'A' in the L column (reading from top). By the LF-mapping property, this corresponds to the 1st 'A' in the F column. The 1st 'A' in F=A0A1A2B3N4N5F = A_0A_1A_2B_3N_4N_5 is at index 0. This tells us our next step. The row we need to look at next is row 0.
  4. Step 4: Walk Backwards - Iteration 2: Go to row 0. The character in L[0]L[0] is 'N'. This is the second-to-last character of our original string. So far we have: NA.
  5. Step 5: Repeat LF-Mapping: The character we just found ('N') is the 1st 'N' in the L column. This corresponds to the 1st 'N' in the F column, which is at index 4. Our next row is 4.
  6. Step 6: Continue Iterating:
    • Go to row 4. L[4]L[4] is 'A'. This is the 3rd 'A' in L, so it maps to the 3rd 'A' in F, at index 2. We now have: ANA. Next row is 2.
    • Go to row 2. L[2]L[2] is 'B'. This is the 1st 'B' in L, maps to the 1st 'B' in F, at index 3. We now have: BANA. Next row is 3.
    • Go to row 3. L[3]L[3] is 'A'. This is the 1st 'A' in L, maps to the 1st 'A' in F, at index 0. We have: ABANA. Next row is 0.
    • Go to row 0. L[0]L[0] is 'N'. This is the 1st 'N' in L, maps to the 1st 'N' in F, at index 4. We have: NABANA. We have jumped back to the original index. (Error in walkthrough - let me retrace, the reconstruction is backwards) Let's be more formal. Let T be the reconstructed string, initially empty.
      1. i=3i = 3. Prepend L[3]L[3] = 'A' to T. T = "A". The 'A' at L[3]L[3] is the first 'A'. So we jump to the index of the first 'A' in FF, which is 0. New i=0i = 0.
      2. i=0i = 0. Prepend L[0]L[0] = 'N' to T. T = "NA". The 'N' at L[0]L[0] is the first 'N'. So we jump to the index of the first 'N' in FF, which is 4. New i=4i = 4.
      3. i=4i = 4. Prepend L[4]L[4] = 'A' to T. T = "ANA". The 'A' at L[4]L[4] is the second 'A'. So we jump to the index of the second 'A' in FF, which is 1. New i=1i = 1.
      4. i=1i = 1. Prepend L[1]L[1] = 'N' to T. T = "NANA". The 'N' at L[1]L[1] is the second 'N'. Jump to index of second 'N' in FF, which is 5. New i=5i = 5.
      5. i=5i = 5. Prepend L[5]L[5] = 'A' to T. T = "ANANA". The 'A' at L[5]L[5] is the third 'A'. Jump to index of third 'A' in FF, which is 2. New i=2i = 2.
      6. i=2i = 2. Prepend L[2]L[2] = 'B' to T. T = "BANANA". The 'B' at L[2]L[2] is the first 'B'. Jump to index of first 'B' in FF, which is 3. New i=3i = 3.
      We have returned to our starting index I=3I=3, and we have reconstructed the full string.

The Complete Bzip2 Compression Pipeline

As mentioned, the BWT is just the first, crucial step in a multi-stage pipeline used by compressors like bzip2.

  • Stage 1: Burrows-Wheeler Transform (BWT): The input data is processed in blocks, and each block is transformed to group characters with similar contexts, producing the L-column and an index.
  • Stage 2: Move-to-Front (MTF) Transform: The output of the BWT, which now has long runs of identical characters, is processed by the MTF transform. MTF maintains a list of the alphabet. For each character it processes, it outputs the character's current position in the list and then moves that character to the front of the list. The result is a stream of small numbers (mostly 0s, 1s, and 2s), which is even more statistically redundant.
  • Stage 3: Run-Length Encoding (RLE): The stream of small numbers from MTF is now processed with RLE to encode the many runs of zeros that have been created.
  • Stage 4: Entropy Coding: The final stream of symbols from the RLE stage is compressed using Huffman coding (or sometimes Arithmetic coding) to produce the final compressed output.

Conclusion: An Elegant Transformation

The Burrows-Wheeler Transform is a profound and elegant algorithm that stands as a landmark in data compression. It demonstrates that by reversibly reordering data, we can expose a deeper layer of contextual redundancy that is inaccessible to simpler methods. While its computational cost, particularly the sorting step, makes it slower than LZ-based algorithms, its ability to achieve superior compression ratios on many types of large files ensures its continued relevance. The BWT pipeline represents a multi-stage, sophisticated approach to data reduction, where a clever transformation prepares the data for a cascade of simpler, highly effective compression stages.

    Burrows-Wheeler Transform Compression | Teleinf Edu