917 lines
39 KiB
HTML
917 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<!-- https://www.carlbelle.com/Articles/Article/2016/Apr/6/ACloserLookAtHuffmanEncodingUsingCSharp/95 -->
|
|
<html>
|
|
<head>
|
|
<title>A Closer Look at Huffman Encoding Using C# | Articles - carlbelle.com</title>
|
|
<meta content="Articles about A Closer Look at Huffman Encoding Using C#." name="description"/>
|
|
<meta content="" name="keywords"/>
|
|
<meta charset="utf-8"/>
|
|
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
|
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
|
<link href="/Css/Version1.css" media="screen" rel="stylesheet" type="text/css"/>
|
|
<link href="/favicon.ico" rel="shortcut icon" type="image/x-icon">
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="LayoutContainer">
|
|
<div class="Header">
|
|
<div class="HeaderImage"><a href="/" title="Home - carlbelle.com">CARL.BELLE</a></div>
|
|
<div class="HeaderLinks">
|
|
<p>
|
|
<a class="NoUnderline" href="https://www.github.com/carlyman77" target="_blank" title="GitHub"><span
|
|
class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://www.linkedin.com/in/carlyman77" target="_blank"
|
|
title="LinkedIn"><span class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://www.twitter.com/carlbelle" target="_blank" title="Twitter"><span
|
|
class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://instagram.com/carlyman77" target="_blank" title="Instagram"><span
|
|
class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://open.spotify.com/artist/2kEspNx7mSo6Oe8BWpkGpj" target="_blank"
|
|
title="Spotify"><span class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://www.youtube.com/channel/UCEen0eKme7kKca_yDS7gDzw" target="_blank"
|
|
title="SoundCloud"><span class="Symbol"></span></a>
|
|
<a class="NoUnderline" href="https://soundcloud.com/carl-belle" target="_blank" title="SoundCloud"><span
|
|
class="Symbol"></span></a>
|
|
</p>
|
|
<p>
|
|
<a href="/" title="Home">Home</a> »
|
|
<a href="/Articles/Home" title="Articles">Articles</a> »
|
|
<a href="/QSharp" title="QSharp">QSharp</a> »
|
|
<a href="/Home/Contact" title="Contact">Contact</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="HorizontalSolidLine"></div>
|
|
<div class="HorizontalSpacer"></div>
|
|
|
|
<div class="Layout">
|
|
|
|
|
|
<h1>A Closer Look at Huffman Encoding Using C#</h1>
|
|
|
|
<p>06 Apr 2016
|
|
<p>
|
|
|
|
<p>
|
|
<a href="/Articles/Category/Algorithms/18" title="Algorithms">Algorithms</a>, <a
|
|
href="/Articles/Category/CSharp/11" title="C#">C#</a></p>
|
|
|
|
<br/>
|
|
|
|
<p>On this page:</p>
|
|
<ul>
|
|
<li><a href="#HowItWorks">How it Works</a></li>
|
|
<li><a href="#ConstructingTheBinaryTree">Constructing the Binary Tree</a></li>
|
|
<li><a href="#EncodingTheData">Encoding the Data</a></li>
|
|
<li><a href="#UnpackingTheData">Unpacking the Data</a></li>
|
|
<li><a href="#FinalThoughts">Final Thoughts</a></li>
|
|
<li><a href="#TheCode">The Code</a></li>
|
|
</ul>
|
|
|
|
<p>Huffman encoding is a compression technique developed by David Huffman and published in his 1952 paper 'A
|
|
Method for the Construction of Minimum-Redundancy Codes'. The technique centres on encoding characters as a
|
|
list of code words, which are then used to generate a coded version of the original data. Huffman encoding
|
|
can be very efficient, and should generally yield compression as low as 20% of the original data size.</p>
|
|
|
|
<a name="#HowItWorks"></a>
|
|
<p><strong>How it Works</strong></p>
|
|
|
|
<p>The input data is scanned, and a list of characters and their corresponding weight (the number of instances
|
|
the character appears in the data). The weights of each character are used to compute the length of the code
|
|
words that represent that character: larger weights should yield smaller code words, a key aspect of
|
|
compressing the data. Characters are then arranged into a binary tree, where the resulting layout is used to
|
|
determine the encoding table. The last step is using the encoding table to create the new file.</p>
|
|
|
|
<a name="#ConstructingTheBinaryTree"></a>
|
|
<p><strong>Constructing the Binary Tree</strong></p>
|
|
|
|
<p>Consider the following text:</p>
|
|
<pre>
|
|
abccccddddddefghijjjjj
|
|
</pre>
|
|
|
|
<p>Represented as a list of characters with corresponding weights:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
0 a 1
|
|
1 b 1
|
|
2 c 4
|
|
3 d 6
|
|
4 e 1
|
|
5 f 1
|
|
6 g 1
|
|
7 h 1
|
|
8 i 1
|
|
9 j 5
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>This list is now turned into a binary tree. A binary tree is a tree where each node may have a maximum of two
|
|
child nodes. The left child node is consider the 0 child node, whereas the right child node is considered
|
|
the 1 child node. The first two nodes are removed from the list, and a new parent node is constructed. This
|
|
parent node's weight is the combined weight of both children. In our example, after the first iteration our
|
|
list would look like this:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
00 null null <span class="Comment">// used to be a</span>
|
|
01 null null <span class="Comment">// used to be b</span>
|
|
02 e 1
|
|
03 f 1
|
|
04 g 1
|
|
05 h 1
|
|
06 i 1
|
|
07 c 4
|
|
08 j 5
|
|
09 d 6
|
|
10 a + b 2 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>The next two lowest weight nodes are added to a parent:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
00 null null <span class="Comment">// used to be a</span>
|
|
01 null null <span class="Comment">// used to be b</span>
|
|
02 null null <span class="Comment">// used to be e</span>
|
|
03 null null <span class="Comment">// used to be f</span>
|
|
04 g 1
|
|
05 h 1
|
|
06 i 1
|
|
07 c 4
|
|
08 j 5
|
|
09 d 6
|
|
10 a + b 2 <span class="Comment">// new parent node</span>
|
|
11 e + f 2 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>And so on. At the end of this round of processing, the list looks like this:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
00 null null <span class="Comment">// used to be a</span>
|
|
01 null null <span class="Comment">// used to be b</span>
|
|
02 null null <span class="Comment">// used to be e</span>
|
|
03 null null <span class="Comment">// used to be f</span>
|
|
04 null null <span class="Comment">// used to be g</span>
|
|
05 null null <span class="Comment">// used to be h</span>
|
|
06 null null <span class="Comment">// used to be i</span>
|
|
07 null null <span class="Comment">// used to be c</span>
|
|
08 null null <span class="Comment">// used to be j</span>
|
|
09 null null <span class="Comment">// used to be d</span>
|
|
10 a + b 2 <span class="Comment">// new parent node</span>
|
|
11 e + f 2 <span class="Comment">// new parent node</span>
|
|
12 g + h 2 <span class="Comment">// new parent node</span>
|
|
13 i + c 5 <span class="Comment">// new parent node</span>
|
|
14 j + d 11 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>Null nodes are removed from the list:</p>
|
|
<pre>
|
|
Index Char Weight
|
|
0 a + b 2
|
|
1 e + f 2
|
|
2 g + h 2
|
|
3 i + c 5
|
|
4 j + d 11
|
|
</pre>
|
|
|
|
<p>And the next round of processing begins, always taking the smallest weighted nodes and combining them under a
|
|
new parent. At the end of the next round of processing, the list looks like this:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
0 null null <span class="Comment">// a + b</span>
|
|
1 null null <span class="Comment">// e + f</span>
|
|
2 null null <span class="Comment">// g + h</span>
|
|
3 null null <span class="Comment">// i + c</span>
|
|
4 j + d 11
|
|
5 a + b (2), e + f (2) 4 <span class="Comment">// new parent node</span>
|
|
6 g + h (2), i + c (5) 7 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>Null nodes are removed from the list:</p>
|
|
<pre>
|
|
Index Char Weight
|
|
0 a + b (2), e + f (2) 4
|
|
1 g + h (2), i + c (5) 7
|
|
2 j + d 11
|
|
</pre>
|
|
|
|
<p>At the end of the next round of processing:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
0 null 4 <span class="Comment">// a + b (2), e + f (2)</span>
|
|
1 null 7 <span class="Comment">// g + h (2), i + c (5) </span>
|
|
2 j + d 11 <span class="Comment">// new parent node</span>
|
|
3 (a + b, e + f) - (4), (g + h, i + c) - (7) 11 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>After removing null nodes from the list, we are ready for the final round of processing:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
0 j + d 11 <span class="Comment">// new parent node</span>
|
|
1 (a + b, e + f) - (4), (g + h, i + c) - (7) 11 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>And finally, after all processing, there is only one node left:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char Weight
|
|
0 (j + d) - 11, (a + b, e + f, g + h, i + c) - (11) 22 <span class="Comment">// new parent node</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>This final node forms the root of the tree, and is used to generate a list of binary words that represent
|
|
each character. Starting at the root of the tree, following a left child node inserts a 0 into the binary
|
|
word, following a right child node inserts a 1 into the binary word. Using this method, the following list
|
|
of binary words is created:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Char Weight BinaryWord Int32Word
|
|
j 5 00 0 <span class="Comment">// This column is for sorting purposes only</span>
|
|
d 6 01 1
|
|
a 1 1000 8
|
|
b 1 1001 9
|
|
e 1 1010 10
|
|
f 1 1011 11
|
|
g 1 1100 12
|
|
h 1 1101 13
|
|
I 1 1110 14
|
|
c 4 1111 15
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>Note that nodes without a character value are not present in this list, but are able to be inferred from the
|
|
list sorting operations. For the moment, I'll leave you to construct the tree using the information
|
|
above.</p>
|
|
|
|
<a name="#EncodingTheData"></a>
|
|
<p><strong>Encoding the Data</strong></p>
|
|
|
|
<p>The next step is to encode the original data using the binary words created from the previous steps. Moving
|
|
forward through the data one byte at a time, each character is used to perform a lookup for its
|
|
corresponding binary word. This binary word is added into a binary word buffer. When the binary word buffer
|
|
reaches sufficient length, 8 values are read off and removed to construct a new byte. Consider the
|
|
following:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Char BinaryWord ByteBuffer
|
|
00 a 1000 <span class="Comment">// Only 4 bits here, not enough for a new byte</span>
|
|
01 b 1001 <span class="Comment">// Combined with the previous entry, there are enough bits here for a new byte: 10001001 => 137, remove 8 entries from the buffer</span>
|
|
02 c 1111 <span class="Comment">// Start a new byte using these 4 bits</span>
|
|
03 c 1111 <span class="Comment">// 11111111 => 255</span>
|
|
04 c 1111 <span class="Comment">// Start a new byte</span>
|
|
05 c 1111 <span class="Comment">// 11111111 => 255</span>
|
|
06 d 01 <span class="Comment">// Start a new byte</span>
|
|
07 d 01 <span class="Comment">// 0101 - the buffer is only half full</span>
|
|
08 d 01 <span class="Comment">// 010101</span>
|
|
09 d 01 <span
|
|
class="Comment">// 01010101 => 85 - note that this one byte actually represents 4 chars!</span>
|
|
10 d 01 <span class="Comment">// Start a new byte</span>
|
|
11 d 01 <span class="Comment">// 0101</span>
|
|
12 e 1010 <span class="Comment">// 01011010 => 90 - this one byte represents 3 chars</span>
|
|
13 f 1011 <span class="Comment">// Start a new byte</span>
|
|
14 g 1100 <span class="Comment">// 10111100 => 188</span>
|
|
15 h 1101 <span class="Comment">// New byte</span>
|
|
16 i 1110 <span class="Comment">// 11011110 => 222</span>
|
|
17 j 00 <span class="Comment">// New byte</span>
|
|
18 j 00 <span class="Comment">// 0000</span>
|
|
19 j 00 <span class="Comment">// 000000</span>
|
|
20 j 00 <span class="Comment">// 00000000 => 0</span>
|
|
21 j 00 <span class="Comment">// Start a new byte</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>With the processing of the final character, the byte buffer only contains 2 values, which is not enough to
|
|
construct a byte, however as the binary word is 00 it is simply converted to 0. Therefore, the final byte to
|
|
be constructed is 0. Processing of these 22 characters has yielded a list containing only 9 bytes, about 41%
|
|
of the original data size.</p>
|
|
|
|
<a name="#UnpackingTheData"></a>
|
|
<p><strong>Unpacking the Data</strong></p>
|
|
|
|
<p>Decompression (in this example at least) is relatively straight forward. The byte list is read, and each byte
|
|
converted into a binary word, which is added into a buffer. If the buffer is less than 8 values, then it is
|
|
'fixed' (more on that later). Reading each bit from the binary word indicates which path to take through the
|
|
binary tree (constructed above): starting at the root node, follow the left child for a 0 value, and follow
|
|
the right for a 1 value. The entire decompression process for the above data:</p>
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
Index Byte Characters
|
|
0 137 => 10001001 a, b
|
|
1 255 => 11111111 c, c
|
|
2 255 => 11111111 c, c
|
|
3 85 => 01010101 d, d, d, d
|
|
4 90 => 01011010 d, d, e
|
|
5 188 => 10111100 f, g
|
|
6 222 => 11011110 h, i
|
|
7 0 => 00000000 j, j, j, j
|
|
8 0 => 00 j <span class="Comment">// This is the last value, so rather than pad it out, we can just go directly to the char value</span>
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>The output data now contains 22 characters, the data has been successfully unpacked.</p>
|
|
|
|
<a name="#FinalThoughts"></a>
|
|
<p><strong>Final Thoughts</strong></p>
|
|
|
|
<p>Conveniently, this example ignores completely the size of the encoding table, as well as how storage and
|
|
retrieval mechanisms, so theoretically 9 bytes is probably as small as we can get. If this compressed data
|
|
were to be used outside this example, then the binary tree and code words would also need to be stored along
|
|
with the byte list in order to unpack the data at some other time.</p>
|
|
|
|
<a name="#TheCode"></a>
|
|
<p><strong>The Code</strong></p>
|
|
|
|
<p>I've included my somewhat rambling encoding and decoding process, in C# below.</p>
|
|
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">class</span> <span class="Type">HuffmanCompressor</span>
|
|
{
|
|
<span class="Keyword">private</span> <span class="Type">HuffmanNode</span> oRootHuffmanNode;
|
|
<span class="Keyword">private</span> <span class="Type">List</span><<span class="Type">HuffmanNode</span>> oValueHuffmanNodes;
|
|
|
|
<span class="Keyword">private</span> <span class="Type">List</span><<span class="Type">HuffmanNode</span>> BuildBinaryTree(<span
|
|
class="Keyword">string</span> Value)
|
|
{
|
|
<span class="Type">List</span><<span class="Type">HuffmanNode</span>> oHuffmanNodes = GetInitialNodeList();
|
|
|
|
<span class="Comment">// Update node weights</span>
|
|
Value.ToList().ForEach(m => oHuffmanNodes[m].Weight++);
|
|
|
|
<span class="Comment">// Select only nodes that have a weight</span>
|
|
oHuffmanNodes = oHuffmanNodes
|
|
.Where(m => (m.Weight > 0))
|
|
.OrderBy(m => (m.Weight))
|
|
.ThenBy(m => (m.Value))
|
|
.ToList();
|
|
|
|
<span class="Comment">// Assign parent nodes</span>
|
|
oHuffmanNodes = UpdateNodeParents(oHuffmanNodes);
|
|
|
|
oRootHuffmanNode = oHuffmanNodes[0];
|
|
oHuffmanNodes.Clear();
|
|
|
|
<span class="Comment">// Sort nodes into a tree</span>
|
|
SortNodes(oRootHuffmanNode, oHuffmanNodes);
|
|
|
|
<span class="Keyword">return</span> oHuffmanNodes;
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">void</span> Compress(<span class="Keyword">string</span> FileName)
|
|
{
|
|
<span class="Type">FileInfo</span> oFileInfo = <span class="Keyword">new</span> <span
|
|
class="Type">FileInfo</span>(FileName);
|
|
|
|
<span class="Keyword">if</span> (oFileInfo.Exists == <span class="Keyword">true</span>)
|
|
{
|
|
<span class="Keyword">string</span> sFileContents = <span class="Literal">""</span>;
|
|
|
|
<span class="Keyword">using</span> (<span class="Type">StreamReader</span> oStreamReader = <span
|
|
class="Keyword">new</span> <span class="Type">StreamReader</span>(<span class="Type">File</span>.OpenRead(FileName)))
|
|
{
|
|
sFileContents = oStreamReader.ReadToEnd();
|
|
}
|
|
|
|
<span class="Type">List</span><<span class="Type">HuffmanNode</span>> oHuffmanNodes = BuildBinaryTree(sFileContents);
|
|
|
|
<span class="Comment">// Filter to nodes we care about</span>
|
|
oValueHuffmanNodes = oHuffmanNodes
|
|
.Where(m => (m.Value.HasValue == <span class="Keyword">true</span>))
|
|
.OrderBy(m => (m.BinaryWord)) <span
|
|
class="Comment">// Not really required, for presentation purposes</span>
|
|
.ToList();
|
|
|
|
<span class="Comment">// Construct char to binary word dictionary for quick value to binary word resolution</span>
|
|
<span class="Type">Dictionary</span><char, <span class="Keyword">string</span>> oCharToBinaryWordDictionary = <span
|
|
class="Keyword">new</span> <span class="Type">Dictionary</span><char, <span
|
|
class="Keyword">string</span>>();
|
|
<span class="Keyword">foreach</span> (<span class="Type">HuffmanNode</span> oHuffmanNode <span
|
|
class="Keyword">in</span> oValueHuffmanNodes)
|
|
{
|
|
oCharToBinaryWordDictionary.Add(oHuffmanNode.Value.Value, oHuffmanNode.BinaryWord);
|
|
}
|
|
|
|
<span class="Type">StringBuilder</span> oStringBuilder = <span class="Keyword">new</span> <span
|
|
class="Type">StringBuilder</span>();
|
|
<span class="Type">List</span><<span class="Keyword">byte</span>> oByteList = <span class="Keyword">new</span> <span
|
|
class="Type">List</span><<span class="Keyword">byte</span>>();
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> i = 0; i < sFileContents.Length; i++)
|
|
{
|
|
<span class="Keyword">string</span> sWord = <span class="Literal">""</span>;
|
|
|
|
<span class="Comment">// Append the binary word value using the char located at the current file position</span>
|
|
oStringBuilder.Append(oCharToBinaryWordDictionary[sFileContents[i]]);
|
|
|
|
<span class="Comment">// Once we have at least 8 chars, we can construct a byte</span>
|
|
<span class="Keyword">while</span> (oStringBuilder.Length >= 8)
|
|
{
|
|
sWord = oStringBuilder.ToString().Substring(0, 8);
|
|
|
|
<span class="Comment">// Remove the word to be constructed from the buffer</span>
|
|
oStringBuilder.Remove(0, sWord.Length);
|
|
}
|
|
|
|
<span class="Comment">// Convert the word and add it onto the list</span>
|
|
<span class="Keyword">if</span> (<span class="Type">String</span>.IsNullOrEmpty(sWord) == <span
|
|
class="Keyword">false</span>)
|
|
{
|
|
oByteList.Add(Convert.ToByte(sWord, 2));
|
|
}
|
|
}
|
|
|
|
<span class="Comment">// If there is anything in the buffer, make sure it is retrieved</span>
|
|
<span class="Keyword">if</span> (oStringBuilder.Length > 0)
|
|
{
|
|
<span class="Keyword">string</span> sWord = oStringBuilder.ToString();
|
|
|
|
<span class="Comment">// Convert the word and add it onto the list</span>
|
|
<span class="Keyword">if</span> (<span class="Type">String</span>.IsNullOrEmpty(sWord) == <span
|
|
class="Keyword">false</span>)
|
|
{
|
|
oByteList.Add(Convert.ToByte(sWord, 2));
|
|
}
|
|
}
|
|
|
|
<span class="Comment">// Write compressed file</span>
|
|
<span class="Keyword">string</span> sCompressedFileName = Path.Combine(oFileInfo.Directory.FullName, <span
|
|
class="Type">String</span>.Format(<span class="Literal">"{0}.compressed"</span>, oFileInfo.Name.Replace(oFileInfo.Extension, <span
|
|
class="Literal">""</span>)));
|
|
<span class="Keyword">if</span> (<span class="Type">File</span>.Exists(sCompressedFileName) == <span
|
|
class="Keyword">true</span>)
|
|
{
|
|
<span class="Type">File</span>.Delete(sCompressedFileName);
|
|
}
|
|
|
|
<span class="Keyword">using</span> (<span class="Type">FileStream</span> oFileStream = <span class="Type">File</span>.OpenWrite(sCompressedFileName))
|
|
{
|
|
oFileStream.Write(oByteList.ToArray(), 0, oByteList.Count);
|
|
}
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">void</span> Decompress(<span class="Keyword">string</span> FileName)
|
|
{
|
|
<span class="Type">FileInfo</span> oFileInfo = <span class="Keyword">new</span> <span
|
|
class="Type">FileInfo</span>(FileName);
|
|
|
|
<span class="Keyword">if</span> (oFileInfo.Exists == <span class="Keyword">true</span>)
|
|
{
|
|
<span class="Keyword">string</span> sCompressedFileName = <span class="Type">String</span>.Format(<span
|
|
class="Literal">"{0}.compressed"</span>, oFileInfo.FullName.Replace(oFileInfo.Extension, <span
|
|
class="Literal">""</span>));
|
|
|
|
<span class="Keyword">byte</span>[] oBuffer = <span class="Keyword">null</span>;
|
|
<span class="Keyword">using</span> (<span class="Type">FileStream</span> oFileStream = <span class="Type">File</span>.OpenRead(sCompressedFileName))
|
|
{
|
|
oBuffer = <span class="Keyword">new</span> <span class="Keyword">byte</span>[oFileStream.Length];
|
|
oFileStream.Read(oBuffer, 0, oBuffer.Length);
|
|
}
|
|
|
|
<span class="Comment">// Find the zero node</span>
|
|
<span class="Type">HuffmanNode</span> oZeroHuffmanNode = oRootHuffmanNode;
|
|
<span class="Keyword">while</span> (oZeroHuffmanNode.Left != <span class="Keyword">null</span>)
|
|
{
|
|
oZeroHuffmanNode = oZeroHuffmanNode.Left;
|
|
}
|
|
|
|
<span class="Comment">// Unpack the file contents</span>
|
|
<span class="Type">HuffmanNode</span> oCurrentHuffmanNode = <span class="Keyword">null</span>;
|
|
<span class="Type">StringBuilder</span> oStringBuilder = <span class="Keyword">new</span> <span
|
|
class="Type">StringBuilder</span>();
|
|
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> i = 0; i < oBuffer.Length; i++)
|
|
{
|
|
<span class="Keyword">string</span> sBinaryWord = <span class="Literal">""</span>;
|
|
<span class="Keyword">byte</span> oByte = oBuffer[i];
|
|
|
|
<span class="Keyword">if</span> (oByte == 0)
|
|
{
|
|
sBinaryWord = oZeroHuffmanNode.BinaryWord;
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
sBinaryWord = Convert.ToString(oByte, 2);
|
|
}
|
|
|
|
<span class="Keyword">if</span> ((sBinaryWord.Length < 8) && (i < (oBuffer.Length - 1)))
|
|
{
|
|
<span class="Comment">// Pad binary word out to 8 places</span>
|
|
<span class="Type">StringBuilder</span> oBinaryStringBuilder = <span
|
|
class="Keyword">new</span> <span class="Type">StringBuilder</span>(sBinaryWord);
|
|
<span class="Keyword">while</span> (oBinaryStringBuilder.Length < 8)
|
|
{
|
|
oBinaryStringBuilder.Insert(0, <span class="Literal">"0"</span>);
|
|
}
|
|
|
|
sBinaryWord = oBinaryStringBuilder.ToString();
|
|
}
|
|
|
|
<span class="Comment">// Use the binary word to navigate the tree looking for the value</span>
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> j = 0; j < sBinaryWord.Length; j++)
|
|
{
|
|
<span class="Keyword">char</span> cValue = sBinaryWord[j];
|
|
|
|
<span class="Keyword">if</span> (oCurrentHuffmanNode == <span class="Keyword">null</span>)
|
|
{
|
|
oCurrentHuffmanNode = oRootHuffmanNode;
|
|
}
|
|
|
|
<span class="Keyword">if</span> (cValue == '0')
|
|
{
|
|
oCurrentHuffmanNode = oCurrentHuffmanNode.Left;
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
oCurrentHuffmanNode = oCurrentHuffmanNode.Right;
|
|
}
|
|
|
|
<span class="Keyword">if</span> ((oCurrentHuffmanNode.Left == <span class="Keyword">null</span>) && (oCurrentHuffmanNode.Right == <span
|
|
class="Keyword">null</span>))
|
|
{
|
|
<span class="Comment">// No more child nodes to choose from, so this must be a value node</span>
|
|
oStringBuilder.Append(oCurrentHuffmanNode.Value.Value);
|
|
oCurrentHuffmanNode = <span class="Keyword">null</span>;
|
|
}
|
|
}
|
|
}
|
|
|
|
<span class="Comment">// Write out file</span>
|
|
<span class="Keyword">string</span> sUncompressedFileName = Path.Combine(oFileInfo.Directory.FullName, <span
|
|
class="Type">String</span>.Format(<span class="Literal">"{0}.uncompressed"</span>, oFileInfo.Name.Replace(oFileInfo.Extension, <span
|
|
class="Literal">""</span>)));
|
|
|
|
<span class="Keyword">if</span> (<span class="Type">File</span>.Exists(sUncompressedFileName) == <span
|
|
class="Keyword">true</span>)
|
|
{
|
|
<span class="Type">File</span>.Delete(sUncompressedFileName);
|
|
}
|
|
|
|
<span class="Keyword">using</span> (<span class="Type">StreamWriter</span> oStreamWriter = <span
|
|
class="Keyword">new</span> <span class="Type">StreamWriter</span>(<span class="Type">File</span>.OpenWrite(sUncompressedFileName)))
|
|
{
|
|
oStreamWriter.Write(oStringBuilder.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">private</span> <span class="Keyword">static</span> <span class="Type">List</span><<span
|
|
class="Type">HuffmanNode</span>> GetInitialNodeList()
|
|
{
|
|
<span class="Type">List</span><<span class="Type">HuffmanNode</span>> oGetInitialNodeList = <span
|
|
class="Keyword">new</span> <span class="Type">List</span><<span class="Type">HuffmanNode</span>>();
|
|
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> i = Char.MinValue; i < Char.MaxValue; i++)
|
|
{
|
|
oGetInitialNodeList.Add(<span class="Keyword">new</span> <span class="Type">HuffmanNode</span>((<span
|
|
class="Keyword">char</span>)(i)));
|
|
}
|
|
|
|
<span class="Keyword">return</span> oGetInitialNodeList;
|
|
}
|
|
|
|
<span class="Keyword">private</span> <span class="Keyword">static</span> <span class="Keyword">void</span> SortNodes(<span
|
|
class="Type">HuffmanNode</span> Node, <span class="Type">List</span><<span
|
|
class="Type">HuffmanNode</span>> Nodes)
|
|
{
|
|
<span class="Keyword">if</span> (Nodes.Contains(Node) == <span class="Keyword">false</span>)
|
|
{
|
|
Nodes.Add(Node);
|
|
}
|
|
|
|
<span class="Keyword">if</span> (Node.Left != <span class="Keyword">null</span>)
|
|
{
|
|
SortNodes(Node.Left, Nodes);
|
|
}
|
|
|
|
<span class="Keyword">if</span> (Node.Right != <span class="Keyword">null</span>)
|
|
{
|
|
SortNodes(Node.Right, Nodes);
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">private</span> <span class="Keyword">static</span> <span class="Type">List</span><<span
|
|
class="Type">HuffmanNode</span>> UpdateNodeParents(<span class="Type">List</span><<span
|
|
class="Type">HuffmanNode</span>> Nodes)
|
|
{
|
|
<span class="Comment">// Assign parent nodes</span>
|
|
<span class="Keyword">while</span> (Nodes.Count > 1)
|
|
{
|
|
<span class="Keyword">int</span> iOperations = (Nodes.Count / 2);
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> iOperation = 0, i = 0, j = 1; iOperation < iOperations; iOperation++, i += 2, j += 2)
|
|
{
|
|
<span class="Keyword">if</span> (j < Nodes.Count)
|
|
{
|
|
<span class="Type">HuffmanNode</span> oParentHuffmanNode = <span class="Keyword">new</span> <span
|
|
class="Type">HuffmanNode</span>(Nodes[i], Nodes[j]);
|
|
Nodes.Add(oParentHuffmanNode);
|
|
|
|
Nodes[i] = <span class="Keyword">null</span>;
|
|
Nodes[j] = <span class="Keyword">null</span>;
|
|
}
|
|
}
|
|
|
|
<span class="Comment">// Remove null nodes</span>
|
|
Nodes = Nodes
|
|
.Where(m => (m != <span class="Keyword">null</span>))
|
|
.OrderBy(m => (m.Weight)) <span class="Comment">// Choose the lowest weightings first</span>
|
|
.ToList();
|
|
}
|
|
|
|
<span class="Keyword">return</span> Nodes;
|
|
}
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>Here's the code for the HuffmanNode type itself:</p>
|
|
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">class</span> <span class="Type">HuffmanNode</span>
|
|
{
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span>()
|
|
{
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span>(<span class="Keyword">char</span> Value)
|
|
{
|
|
cValue = Value;
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span>(<span class="Type">HuffmanNode</span> Left, <span
|
|
class="Type">HuffmanNode</span> Right)
|
|
{
|
|
oLeft = Left;
|
|
oLeft.oParent = <span class="Keyword">this</span>;
|
|
oLeft.bIsLeftNode = <span class="Keyword">true</span>;
|
|
|
|
oRight = Right;
|
|
oRight.oParent = <span class="Keyword">this</span>;
|
|
oRight.bIsRightNode = <span class="Keyword">true</span>;
|
|
|
|
iWeight = (oLeft.Weight + oRight.Weight);
|
|
}
|
|
|
|
<span class="Keyword">private</span> <span class="Keyword">string</span> sBinaryWord;
|
|
<span class="Keyword">private</span> <span class="Keyword">bool</span> bIsLeftNode;
|
|
<span class="Keyword">private</span> <span class="Keyword">bool</span> bIsRightNode;
|
|
<span class="Keyword">private</span> <span class="Type">HuffmanNode</span> oLeft;
|
|
<span class="Keyword">private</span> <span class="Type">HuffmanNode</span> oParent;
|
|
<span class="Keyword">private</span> <span class="Type">HuffmanNode</span> oRight;
|
|
<span class="Keyword">private</span> <span class="Keyword">char</span>? cValue;
|
|
<span class="Keyword">private</span> <span class="Keyword">int</span> iWeight;
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">string</span> BinaryWord
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">string</span> sReturnValue = <span class="Literal">""</span>;
|
|
|
|
<span class="Keyword">if</span> (<span class="Type">String</span>.IsNullOrEmpty(sBinaryWord) == <span
|
|
class="Keyword">true</span>)
|
|
{
|
|
<span class="Type">StringBuilder</span> oStringBuilder = <span class="Keyword">new</span> <span
|
|
class="Type">StringBuilder</span>();
|
|
|
|
<span class="Type">HuffmanNode</span> oHuffmanNode = <span class="Keyword">this</span>;
|
|
|
|
<span class="Keyword">while</span> (oHuffmanNode != <span class="Keyword">null</span>)
|
|
{
|
|
<span class="Keyword">if</span> (oHuffmanNode.bIsLeftNode == <span class="Keyword">true</span>)
|
|
{
|
|
oStringBuilder.Insert(0, <span class="Literal">"0"</span>);
|
|
}
|
|
|
|
<span class="Keyword">if</span> (oHuffmanNode.bIsRightNode == <span class="Keyword">true</span>)
|
|
{
|
|
oStringBuilder.Insert(0, <span class="Literal">"1"</span>);
|
|
}
|
|
|
|
oHuffmanNode = oHuffmanNode.oParent;
|
|
}
|
|
|
|
sReturnValue = oStringBuilder.ToString();
|
|
sBinaryWord = sReturnValue;
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
sReturnValue = sBinaryWord;
|
|
}
|
|
|
|
<span class="Keyword">return</span> sReturnValue;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span> Left
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">return</span> oLeft;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span> Parent
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">return</span> oParent;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Type">HuffmanNode</span> Right
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">return</span> oRight;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">char</span>? Value
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">return</span> cValue;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">int</span> Weight
|
|
{
|
|
<span class="Keyword">get</span>
|
|
{
|
|
<span class="Keyword">return</span> iWeight;
|
|
}
|
|
<span class="Keyword">set</span>
|
|
{
|
|
iWeight = <span class="Keyword">value</span>;
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">override</span> <span class="Keyword">string</span> ToString()
|
|
{
|
|
<span class="Type">StringBuilder</span> oStringBuilder = <span class="Keyword">new</span> <span class="Type">StringBuilder</span>();
|
|
|
|
<span class="Keyword">if</span> (cValue.HasValue == <span class="Keyword">true</span>)
|
|
{
|
|
oStringBuilder.AppendFormat(<span class="Literal">"'{0}' ({1}) - {2} ({3})"</span>, cValue.Value, iWeight, BinaryWord, BinaryWord.BinaryStringToInt32());
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
<span class="Keyword">if</span> ((oLeft != <span class="Keyword">null</span>) && (oRight != <span
|
|
class="Keyword">null</span>))
|
|
{
|
|
<span class="Keyword">if</span> ((oLeft.Value.HasValue == <span class="Keyword">true</span>) && (oRight.Value.HasValue == <span
|
|
class="Keyword">true</span>))
|
|
{
|
|
oStringBuilder.AppendFormat(<span class="Literal">"{0} + {1} ({2})"</span>, oLeft.Value, oRight.Value, iWeight);
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
oStringBuilder.AppendFormat(<span class="Literal">"{0}, {1} - ({2})"</span>, oLeft, oRight, iWeight);
|
|
}
|
|
}
|
|
<span class="Keyword">else</span>
|
|
{
|
|
oStringBuilder.Append(iWeight);
|
|
}
|
|
}
|
|
|
|
<span class="Keyword">return</span> oStringBuilder.ToString();
|
|
}
|
|
}
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>And finally, an extension method that is also required for the code to compile:</p>
|
|
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">static</span> <span class="Keyword">int</span> BinaryStringToInt32(<span
|
|
class="Keyword">this</span> <span class="Keyword">string</span> Value)
|
|
{
|
|
<span class="Keyword">int</span> iBinaryStringToInt32 = 0;
|
|
|
|
<span class="Keyword">for</span> (<span class="Keyword">int</span> i = (Value.Length - 1), j = 0; i >= 0; i--, j++)
|
|
{
|
|
iBinaryStringToInt32 += ((Value[j] == '0' ? 0 : 1) * (<span class="Keyword">int</span>)(<span
|
|
class="Type">Math</span>.Pow(2, i)));
|
|
}
|
|
|
|
<span class="Keyword">return</span> iBinaryStringToInt32;
|
|
}
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>To get this code running, it will need to be stitched together, perhaps in a console application:</p>
|
|
|
|
<!-- BEGIN AUTO FORMAT -->
|
|
<pre>
|
|
|
|
<span class="Keyword">public</span> <span class="Keyword">static</span> <span class="Keyword">void</span> Main(<span
|
|
class="Keyword">string</span>[] Arguments)
|
|
{
|
|
<span class="Type">HuffmanCompressor</span> oHuffmanCompressor = <span class="Keyword">new</span> <span
|
|
class="Type">HuffmanCompressor</span>();
|
|
oHuffmanCompressor.Compress(<span class="Literal">"Huffman.txt"</span>);
|
|
<span class="Type">Console</span>.WriteLine();
|
|
oHuffmanCompressor.Decompress(<span class="Literal">"Huffman.txt"</span>);
|
|
<span class="Type">Console</span>.WriteLine();
|
|
}
|
|
</pre>
|
|
|
|
<!-- END AUTO FORMAT -->
|
|
|
|
|
|
<p>Note that for this example, decompression is only possible if the binary tree has been constructed, i,e., the
|
|
data has already been compressed.</p>
|
|
|
|
<br/>
|
|
|
|
<p> </p>
|
|
|
|
</div>
|
|
|
|
<div class="HorizontalSpacer"></div>
|
|
<div class="HorizontalSolidLine"></div>
|
|
|
|
<p align="center">Copyright © 2024 <a href="/" title="carlbelle.com">carlbelle.com</a></p>
|
|
|
|
<div class="HorizontalSpacer"></div>
|
|
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
|
|
var sGuid = '3C9AAA06-9031-4B3E-8AFE-586AE8F6E17F';
|
|
|
|
var _gaq = _gaq || [];
|
|
_gaq.push(['_setAccount', 'UA-26910229-1']);
|
|
_gaq.push(['_trackPageview']);
|
|
|
|
(function () {
|
|
var ga = document.createElement('script');
|
|
ga.type = 'text/javascript';
|
|
ga.async = true;
|
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
|
var s = document.getElementsByTagName('script')[0];
|
|
s.parentNode.insertBefore(ga, s);
|
|
})();
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html> |