Решение задач на использование алгоритмов обработки данных
Алгоритмы сжатия данных
Характерной особенностью большинства типов данных является их избыточность. Степень избыточности данных зависит от типа данных. Например, для видеоданных степень избыточности в несколько раз больше, чем для графических данных, а степень избыточности графических данных, в свою очередь, больше чем степень избыточности текстовых данных. Другим фактором, влияющим на степень избыточности, является принятая система кодирования.
Существует много разных практических методов сжатия без потери информации, которые, как правило, имеют разную эффективность для разных типов данных и разных объемов. Однако в основе этих методов лежат три теоретических алгоритма:
Пример 3. Задача "Энтропийное кодирование"
Энтропийное кодирование – это метод кодирования данных, который обеспечивает компрессию данных за счет удаления избыточной информации. Например, английский текст, закодированный с помощью таблицы ASCII, является примером сообщения с высокой энтропией. В тоже время сжатые сообщения, например zip-архивы, имеют очень маленькую энтропию, и потому попытки их энтропийного кодирования не принесут пользы.
Английский текст, закодированный с помощью ASCII, имеет высокую степень энтропии, потому что для кодирования всех символов используется одно и тоже количество битов – восемь. В то же время известный факт состоит в том, что буквы E, L, N, R, S и T встречаются со значительно более высокой частотой, чем другие буквы английского алфавита. Если найдется способ закодировать только эти буквы четырьмя битами, то закодированный текст станет существенно меньше и при этом будет содержать всю исходную информацию и иметь меньшую энтропию. Однако, как различить при декодировании, четырьмя или восемью битами закодирован очередной символ? Эта проблема решается с помощью префиксного кодирования.
В такой схеме кодирования любое количество битов может быть использовано для конкретного символа. Однако для того, чтобы иметь возможность восстановить информацию, запрещено, чтобы последовательность битов, кодирующая некоторый символ, была префиксом битовой последовательности, используемой для кодирования любого другого символа. Это позволяет читать входную последовательность бит за битом, и как только встречено обозначение символа – его декодировать.
Рассмотрим текст AAAAABCD. Кодирование, использующее ASCII, требует 64 бита. Если же символ А будет кодироваться битовой последовательностью 00, символ В – последовательностью 01, символ С – последовательностью 10, a D – последовательностью 11, то для кодирования потребуется всего 16 битов. Результирующий поток битов будет такой: 0000000000011011.
Но это все еще кодирование с фиксированной длиной, здесь просто использовались для каждого символа два бита вместо восьми.
Символ А встречается чаще, тогда будем его кодировать с помощью меньшего количества битов. Следовательно, закодируем символы такими последовательностями битов:
А – 0 В –10 С – 110 D – 111
Используя такое кодирование, получим только 13 битов в закодированном сообщении: 0000010110111. Коэффициент сжатия в этом случае равен 4,9 к 1. Это означает, что каждый бит в последнем закодированном сообщении содержит столько же информации, сколько и 4,9 бит в первом закодированном сообщении (с помощью ASCII).
Попробуйте читать сообщение 0000010110111 слева направо – и убедитесь, что "префиксное" кодирование обеспечивает простое декодирование текста, даже несмотря на то, что символы кодируются различным количеством битов.
В качестве другого примера рассмотрим текст THE CAT IN THE HAT.
В этом тексте символы Т и пробел встречаются чаще других. Поэтому их нужно кодировать меньшим количеством битов. А символы C, I и N встречаются только по одному разу, потому будут кодироваться самыми длинными кодами. Например, так:
пробел – 00 А – 100 С – 1110 Е – 1111 Н – 110 I – 1010 N – 1011 Т – 01
При таком кодировании исходного предложения потребуется только 51 бит против 144, которые необходимы, чтобы закодировать исходное сообщение с помощью 8-битного ASCII-кодирования. Коэффициент сжатия равен 2,8 к 1.
Входной файл будет содержать список текстовых сообщений, по одному в строке. Сообщения будут состоять только из больших английских букв, цифр и символов подчеркивания (вместо пробелов). Конец файла обозначается строкой END. Эту строку не нужно обрабатывать.
В выходном файле будет содержаться для каждого входного сообщения количество битов в восьмибитовом ASCII-кодировании, количество битов при оптимальном префиксном кодировании и коэффициент сжатия с точность до одного знака после десятичной точки.
Пример.
Описание решения.
В данной задаче проведем кодирование текста алгоритмом Хаффмана. Отличиями являются представление входных и выходных данных. Каждую входную строку нужно кодировать по отдельности. Строка "END" обозначает конец ввода, ее кодировать не нужно.
На выходе нужно указать три числа:
- длину сообщения в битах при стандартном восьмибитовом кодировании;
- длину сообщения в битах при выполненном оптимальном кодировании;
- коэффициент сжатия.
Приведем программную реализацию данной задачи.
#include "stdafx.h" #include <iostream> using namespace std; void InputData(FILE *f); long MinK(); void SumUp(FILE *f); void BuildBits(); void OutputData(FILE *f); void Create(); void Clear(); void Destroy(); int MaxK = 1000; long *k, *a, *b; char **bits; char *sk; bool *Free; char **res; long i, j, n, m, kj, kk1, kk2; char str[256]; int _tmain(int argc, _TCHAR* argv[]){ FILE *in, *out; in = fopen("input.txt","r"); out = fopen("output.txt","w"); while ( !feof(in) ) { Create(); Clear(); InputData(in); cout << str << endl; SumUp(out); if (kj != 1) BuildBits(); if (kj != 1) OutputData(out); Destroy(); } fclose(out); fclose(in); return 0; } //описание функции выделения памяти void Create(){ if ( (k = new long[MaxK + 1]) == NULL ){ printf ("Memory for k no!\n"); system("pause"); exit(0); } if ( (a = new long[MaxK + 1]) == NULL ){ printf ("Memory for a no!\n"); system("pause"); exit(0); } if ( (b = new long[MaxK + 1]) == NULL ){ printf ("Memory for b no!\n"); system("pause"); exit(0); } if ( (bits = new char*[MaxK + 1]) == NULL ){ printf ("Memory for bits no!\n"); system("pause"); exit(0); } for (i = 0; i < MaxK + 1 ; i++) if ( (bits[i] = new char[40]) == NULL ){ printf ("Memory for bits[%d] no!\n",i); system("pause"); exit(0); } if ( (sk = new char[MaxK + 1]) == NULL ){ printf ("Memory for sk no!\n"); system("pause"); exit(0); } if ( (Free = new bool[MaxK + 1]) == NULL ){ printf ("Memory for Free no!\n"); system("pause"); exit(0); } if ( (res = new char*[256]) == NULL ){ printf ("Memory for res no!\n"); system("pause"); exit(0); } for (int i = 0; i < 256 ; i++) if ( (res[i] = new char[40]) == NULL ){ printf ("Memory for res[%d] no!\n",i); system("pause"); exit(0); } } //описание функции обнуления данных в массивах void Clear(){ for (i = 0; i < MaxK + 1; i++){ k[i] = a[i] = b[i] = 0; sk[i] = 0; Free[i] = true; for (j = 0; j < 40; j++) bits[i][j] = 0; } for (i = 0; i < 256 ; i++) for (j = 0; j < 40; j++) res[i][j] = 0; } //описание функции освобождения памяти void Destroy(){ delete [] res; delete [] Free; delete [] sk; delete [] bits; delete [] b; delete [] a; delete [] k; } //описание функции ввода данных void InputData(FILE *f){ char c; long *s = new long[256]; for ( i = 0; i < 256; i++) s[i] = 0; fscanf(f,"%s", str); if (strcmp(str,"END") == 0) { system("pause"); exit(0); } for ( n = 0; n < strlen(str); n++ ){ c = str[n]; s[c]++; } j = 0; for ( i = 0; i < 256; i++) if ( s[i] != 0 ){ j++; k[j] = s[i]; sk[j] = i; } kj = j; } /*описание функции нахождения минимальной частоты символа в исходном тексте*/ long MinK(){ long min; i = 1; while ( !Free[i] && i < MaxK) i++; min = k[i]; m = i; for ( i = m + 1; i <= kk2; i++ ) if ( Free[i] && k[i] < min ){ min = k[i]; m = i; } Free[m] = false; return min; } //описание функции посчета суммарной частоты символов void SumUp(FILE *f){ long s1, s2, m1, m2; if ( kj == 1 ){ fprintf(f,"%d %d %.1f\n",8*strlen(str),strlen(str),8); return; } for ( i = 1; i <= kj; i++ ){ Free[i] = true; a[i] = 0; b[i] = 0; } kk1 = kk2 = kj; while (kk1 > 2){ s1 = MinK(); m1 = m; s2 = MinK(); m2 = m; kk2++; k[kk2] = s1 + s2; a[kk2] = m1; b[kk2] = m2; Free[kk2] = true; kk1--; } } //описание функции формирования префиксных кодов void BuildBits(){ bits[kk2] = "1"; Free[kk2] = false; strcpy(bits[a[kk2]],bits[kk2]); strcat( bits[a[kk2]] , "0"); strcpy(bits[b[kk2]],bits[kk2]); strcat( bits[b[kk2]] , "1"); i = MinK(); bits[m] = "0"; Free[m] = true; strcpy(bits[a[m]],bits[m]); strcat( bits[a[m]] , "0"); strcpy(bits[b[m]],bits[m]); strcat( bits[b[m]] , "1"); for ( i = kk2 - 1; i > 0; i-- ) if ( !Free[i] ) { strcpy(bits[a[i]],bits[i]); strcat( bits[a[i]] , "0"); strcpy(bits[b[i]],bits[i]); strcat( bits[b[i]] , "1"); } } //описание функции вывода данных void OutputData(FILE *f){ long b8, bh; for ( i = 1; i <= kj; i++ ) res[sk[i]] = bits[i]; b8 = 8 * strlen(str); bh = 0; for (i = 0; i < strlen(str); i++) bh += strlen(res[str[i]]); double k = b8 * 1.0 / bh; fprintf(f,"%d %d %.1f\n",b8,bh,k); }Листинг .
Ключевые термины
Цифровая (поразрядная) сортировка – это упорядочивание данных по ключу, которое выполняется отдельно с каждым разрядом с последующим объединением результатов.
Энтропийное кодирование – это метод кодирования данных, который обеспечивает компрессию данных за счет удаления избыточной информации.
Краткие итоги
- При решении задач повышенной сложности рекомендовано придерживаться общей схемы решения задач по программированию.
- При решении различных задач повышенной сложности данные часто требуется упорядочить по некоторому признаку, а задача может требовать построения оптимального в смысле определенных требований или нестандартного алгоритма сортировки.
- Многие прикладные задачи и задачи повышенной сложности удобно сформулировать в терминах такой структуры данных как граф.
- Существует много разных практических методов сжатия без потери информации, которые имеют разную эффективность для разных типов данных и разных объемов.