Фундаментальные алгоритмы и структуры данных в Delphi
Шрифт:
Не существует ли какой-либо способ исключения необходимости поставки дерева или двукратного считывания входных данных (желательно было бы избавиться от обоих этих недостатков)? Существует вариант сжатия Хаффмана, называемый адаптивным кодированием Хаффмана, который позволяет это сделать. Однако в этой главе мы рассмотрим малоизвестную адаптивную технологию, использующую скошенные деревья, с которыми мы впервые встретились в главе 8.
Дуглас В. Джонс (Douglas W. Jones) разработал сжатие с использованием скошенного дерева в 1988 году [8]. Если помните, в главе 8 говорилось, что скошенные деревья - это метод балансировки дерева бинарного поиска посредством скоса достигнутого узла к корневому узлу. Таким образом,
Следует признать, что скошенные деревья были разработаны для оптимизации деревьев бинарного поиска, т.е. упорядоченных деревьев. Частично их полезность обусловлена тем, что операции скоса поддерживают упорядоченность во время различных поворотов и трансформаций. Префиксные деревья не упорядочены, поэтому можно избежать большей части сложного манипулирования указателями, связного с поворотами. Кроме того, потребуется обеспечить, чтобы листья оставались листьями. В противном случае дерево перестало бы быть префиксным. Поэтому мы будем использовать скос, в результате которого родительский узел листа перемещается ближе к корневому узлу.
Код реализации базового алгоритма выполнения сжатия выглядит подобно приведенному в листинге 11.15.
Листинг 11.15. Базовый алгоритм сжатия с использованием скошенного дерева
procedure TDSplayCompress(aInStream, aOutStream : TStream);
var
STree : TSplayTree;
BitStrm : TtdOutputBitStream;
Signature : longint;
Size : longint;
begin
{вывести информацию заголовка сигнатуру и размер несжатых данных}
Signature := TDSplayHeader;
aOutStream.WriteBuffer(Signature, sizeof(longint));
Size := aInStream.Size;
aOutStream.WriteBuffer(Size, sizeof(longint));
{в случае отсутствия данных для сжатия выйти из подпрограммы}
if (Size = 0) then
Exit;
{подготовка}
STree := nil;
BitStrm := nil;
try
{создать сжатый поток битов}
BitStrm := TtdOutputBitStream.Create(aOutStream);
BitStrm.Name := 'Splay compressed stream';
{создать скошенное дерево}
STree := TSplayTree.Create;
{сжатье символы входного потока и поместить их в поток битов}
DoSplayCompression(aInStream, BitStrm, STree);
finally
BitStrm.Free;
STree.Free;
end;
end;
Для пометки выходного потока как сжатого с использованием скошенного дерева в выходной поток мы записываем сигнатуру типа длинного целого, а затем записываем размер несжатого потока. Если входной поток пуст, выполняется выход из подпрограммы, - в этом случае задача выполнена. В противном случае мы создаем выходной поток битов, который будет содержать выходной поток и скошенное дерево. Затем для выполнения реального сжатия мы вызываем метод DoSplayConapression. Код этой подпрограммы приведен в листинге 11.16.
Листинг 11.16. Цикл выполнения сжатия с использованием скошенного дерева
procedure DoSplayCompression(aInStream : TStream;
aBitStream : TtdOutputBitStream;
aTree : TSplayTree);
var
i : integer;
Buffer : PByteArray;
BytesRead : longint;
BitString : TtdBitString;
begin
GetMem(Buffer, SplayBufferSize);
try
{сбросить
aInStream.Position := 0;
{считать первый блок из входного потока}
BytesRead := aInStream.Read(Buffer^, SplayBufferSize);
while (BytesRead <> 0) do
begin
{записать строку битов для каждого символа в блоке}
for i := 0 to pred(BytesRead) do aTree.EncodeByte(aBitStream, Buffer^[i]);
{считать следующий блок из входного потока}
BytesRead := aInStream.Read(Buffer^, SplayBufferSize);
end;
finally
FreeMem(Buffer, SplayBufferSize);
end;
end;
Фактически эта подпрограмма представляется собой подпрограмму выполнения вложенного цикла. Во внешнем цикле выполняется поблочное считывание входного потока, а во внутреннем (через вызов метода EncodeByte скошенного дерева) -кодирование каждого байта текущего блока и запись результирующего кода в выходной поток битов.
Теперь пора рассмотреть внутренний класс TSplayTree, который выполняет основную часть работы по реализации алгоритма сжатия с использованием скошенного дерева. Код интерфейса этого класса показан в листинге 11.17.
Листинг 11.17. Класс сжатия с использованием скошенного дерева
type
PSplayNode = ^TSplayNode;
TSplayNode = packed record
hnParentInx: longint;
hnLeftInx : longint;
hnRightInx : longint;
hnIndex : longint;
end;
PSplayNodeArray = ^TSplayNodeArray;
TSplayNodeArray = array [0..510] of TSplayNode;
type
TSplayTree = class private
FTree : TSplayNodeArray;
FRoot : integer;
protected
procedure stConvertCodeStr(const aRevCodeStr : ShortString;
var aBitString : TtdBitString);
procedure stInitialize;
procedure stSplay(aNode!nx : integer);
public
constructor Create;
procedure EncodeByte(aBitStream : TtdOutputBitStream; aValue : byte);
function DecodeByte(aBitStream : TtdInputBitStream): byte;
end;
Хотя можно было бы воспользоваться ориентированным на узлы деревом, как это делалось в главе 8, поскольку нам известно количество символов в используемом алфавите (в общем случае используется алфавит, содержащий 256 символов), проще отдать предпочтение применению ориентированной на массивы системе, подобной структуре данных типа сортирующего дерева и дерева Хаффмана. Еще один аргумент в пользу перехода на использование других структур данных состоит в том, что в случае применения неадаптивных методов сжатия можно было строить таблицу кодов, так как они были статическими. При использовании сжатия с применением скошенного дерева битовый код символа зависит от состояния скошенного дерева и момента времени кодирования символа. В этом случае мы больше не можем использовать статическую таблицу. Следовательно, одно из выдвигаемых требований - возможность быстрого и эффективного поиска символа в дереве (предпочтительно при помощи алгоритма типа O(1) - мы не хотим его искать). Как только символ и его узел листа определены, можно легко выполнить обход вверх по дереву до корневого узла с целью вычисления кода символа (вообще говоря, мы получим битовый код с обратным порядком следования битов, но с помощью стека его легко можно изменить на противоположный).