Subido por Francisco Testa

Proyecto Final PCI Testa

Anuncio
PRINCIPIOS DE COMPUTADORAS I
Universidad Nacional del Sur
Departamento de Ingeniería Eléctrica y de Computadoras
Trabajo final de la materia
1er cuatrimestre - Ingeniería Electrónica
07/02/2022
Autores:
Testa, Francisco
fran.testa07@gmail.com
Destinatarios:
Andrea Silvetti
Cátedra de la materia
Alumno
Profesor
Resumen
Se presentan en este trabajo los resultados de la codificación de un compresor
de archivos de texto desarrollado en el lenguaje de programación ‘Pascal’, basado
en el algoritmo de Huffman.
1.
1.1.
Introducción
Algoritmo de Huffman
El algoritmo de Huffman consiste en reducir el espacio en memoria que ocupa
un archivo teniendo en cuenta cuántas veces se repite cada caracter, y asignando
mayor espacio de memoria para aquellos con mayor frecuencia. En realidad, lo
que hace es generar un árbol que contiene los caracteres con más frecuencia, más
cerca de la raíz del mismo. De esta manera, la codificación de esos caracteres
será más corta que los demás. Su primer funcionamiento es con archivos de
texto, aunque se pueden realizar adaptaciones del mismo.
Por ejemplo, en la palabra ‘Programa’, las letra ‘a’ y ‘r’ se repiten dos veces,
y las demás solo una. Entonces, podemos ver que una manera de optimizar la
memoria utilizada sería asignando más memoria a aquellos caracteres que se
repiten más. Entonces, el algoritmo primero debe generar una tabla de frecuencias para cada caracter. Luego, tiene que ordenar los elementos en una lista
enlazada según su frecuencia, y por último, generar un árbol binario ordenado
que optimice la memoria.
Figura 1: Palabra ‘Programa’ con frecuencia de aparición de caracter. Fuente:
[1]
Figura 2: Lista enlazada generada. Fuente: [1]
1
Figura 3: Arbol de Huffman con tabla de codificación y código final. Fuente: [1]
Ahora que se tiene el árbol deseado, debe pasarse a generar un nuevo archivo
codificado por el algoritmo. Este es el archivo binario que tiene tamaño reducido,
pero aún contiene toda la información del texto original.
Para descomprimir este archivo y poder leerlo nuevamente, debe usarse un
algoritmo descompresor basado en el mismo funcionamiento, con el detalle de
que se necesita una tabla con los valores de codificación para cada caracter que
se utilizaron al principio. De esta manera, se logra pasar de un archivo de texto, a
uno comprimido (práctico para su transferencia por medios de almacenamiento
reducido) que luego puede descomprimirse para obtener el original.
1.2.
Versiones implementadas
Para realizar un primer acercamiento a la codificación de dicho algoritmo,
se desarrolló una primera versión de consola. En esta versión sólo se generó el
codificador, ya que el objetivo era comprender el funcionamiento y resolver las
principales problemáticas.
Luego, se incorporó a la versión de consola una extensión para poder graficar
el árbol mediante una aplicación externa, y poder visualizar cómo se estaba
generando.
Por último, se desarrolló una versión con interfaz gráfica, mucho más amigable con el usuario, y que también incluye el descompresor necesario. Es totalmente funcional, comprime el archivo, y además sigue proporcionando una
imagen generada del árbol creado.
2
2.
2.1.
Versión de consola
Metodología y estructuras de datos
Para llevar a cabo esta versión, se tuvo en cuenta que se requeriría el manejo
de listas y árboles, y una nueva estructura definida que funcionara como ‘Lista
de árboles’.
Esto es necesario ya que el funcionamiento del algoritmo consiste en, primeramente, generar una lista enlazada con los caracteres de la cadena de texto y
su frecuencia. Luego, se ordena la lista por frecuencia de aparición, y finalmente
se genera el árbol de Huffman.
Por lo tanto, ya vemos que se requiere el uso de un árbol y una lista enlazada,
pero hay que tener en cuenta la manera en la que se generará el árbol. La misma
consiste en ir agrupando nodos, creando un nuevo nodo con una frecuencia igual
a la suma de los dos nodos hijos, de manera repetida hasta que se forma el árbol
completo. Vemos que en esta instancia debemos manejar conjuntamente listas
y árboles, y que debe desarrollarse un tipo de datos para este manejo.
Por lo tanto, se puede observar que hay tres unidades de TDAs (Tipos
de Datos Abstractos). La primera, llamada ‘TDAListas_Huffman’; la segunda, llamada ‘TDAListasdeArboles_Huffman’ y la tercera llamada ‘TDAArboles_Huffman’.
Además, se definió el tipo ‘TInfo’, que era un registro de tres campos. El
primero, llamado ‘letra’; el segundo, ‘frecuencia’, y el tercero ‘codigo_H’. El
primer campo se definió en un inicio como un caracter. La ampliación a ‘String’
fue necesaria para poder nombrar los nodos generados artificialmente, ya que
dicho nombre constaría de un asterisco más un número. Por otro lado, el campo
de frecuencia es un entero, y el último, es un String, que contiene el código
Huffman de esa letra (en un principio lógicamente está vacío).
El desarrollo de los TDA de árboles y listas no tuvo demasiadas complicaciones, pero al desarrollar el TDA de listas de árboles se encontró con una situación
con varios posibles desarrollos. Esta variación consistió en que podíamos operar
con una lista enlazada simple que contuviera dentro del registro, la información,
el puntero al siguiente y un puntero a un árbol, o directamente operar con una
lista enlazada que sólo tuviera dos campos: uno de tipo puntero a árbol y otro
puntero al siguiente.
Se comenzó el desarrollo de la primer manera mencionada, pero luego se
observó que sería mejor la segunda opción. Se llegó a esta conclusión al ver que
de la primera forma, tendríamos el nodo de la lista, con espacio en memoria
reservado para el campo de la letra, la frecuencia y el código Huffman, información que se repetiría luego en la raíz del árbol. De este modo, estaríamos
desperdiciando toda esa memoria. En cambio, de la segunda forma, en el peor
de los casos se desperdicia un puntero cuando se genera el árbol final (el campo
‘sig’ quedará en nil). La diferencia a tener en cuenta es que ahora, para acceder
a la información del caracter deseado de la lista, hay que ingresar al nodo, y
luego mirar la raíz del árbol. Es decir, no podremos acceder más de la manera
‘puntero.̂info’, sino que ahora será ‘puntero.̂arbol.̂info’.
3
2.2.
2.2.1.
Desarrollo
TDA de Listas
En las primeras líneas de este TDA podemos observar la definición de tipos
mencionada previamente: El tipo TInfo, con los campos ‘letra’,‘frecuencia’ y
‘codigo_H’, y luego la definición de tipos perteneciente a la lista enlazada.
u n i t TDAListas_Huffman ;
interface
type
TInfo = record
letra : String ;
f r e c u e n c i a : integer ;
codigo_H : S t r i n g ;
end ;
PTNodo = ^TNodo ;
TNodo = record
i n f o : TInfo ;
s i g : pTNodo ;
end ;
T l i s t a = pTNodo ;
Código 1: Primeras líneas del TDA de listas.
Esta definición no tiene variaciones respecto a una definición de tipos convencional para una lista enlazada. Se define el tipo ‘PTNodo’ como un puntero
a un elemento del tipo ‘TNodo’, que contiene dos campos: uno de tipo ‘TInfo’
(llamado info) y otro del tipo ‘PTNodo’, llamado ‘sig’. Finalmente, para más
comodidad, se define el tipo ‘TLista’, que es equivalente al tipo ‘PTNodo’.
Una vez analizada la definición de tipos a utilizar a lo largo del programa,
podemos pasar a analizar los distintos procedimientos y funciones del TDA. Lo
haremos observando el apartado ‘Interface’. No analizaremos en detalle todos
los procedimientos, sólo los que sean de importancia relevante.
procedure c r e a r L i s t a ( var l i s t a : T L i s t a ) ;
procedure m o s t r a r L i s t a ( l i s t a : T L i s t a ) ;
function b u s c a r _ c a r a c t e r ( l i s t a : T L i s t a ; c a r a c t e r : char ;
var r e s : pTNodo ) : boolean ;
procedure a n a d i r _ f i n a l ( var l i s t a : T L i s t a ; e l e m e n t o : TInfo ) ;
procedure i n s e r t a r _ c a r a c t e r ( var l i s t a : T L i s t a ; c a r a c t e r : char ) ;
procedure i n s e r t a r _ o r d e n a d o _ c o d i g o ( var l i s t a : pTNodo ;
dato : T i n f o ) ;
procedure o r d e n a r _ c o d i g o ( var l i s t a : T L i s t a ) ;
function codigo_Huffman_final ( l i s t a : T L i s t a ; t a b l a : T L i s t a )
: String ;
procedure b o r r a r L i s t a ( var l i s t a : T L i s t a ) ;
Código 2: Sección ‘interface’ del TDA de listas.
4
El primer procedimiento es bastante trivial; pide como parámetro por referencia una variable de tipo ‘TLista’. Lo que hace es inicializarla vacía.
El segundo procedimiento fue de gran utilidad para mostrar la lista al usuario
y controlar que se estuviera generando de manera correcta. A continuación se
muestra una captura de la consola luego de llamar a este procedimiento en
ejecución, cuando ya se habían codificado los caracteres:
Figura 4: Ejemplo de funcionamiento del procedimiento ‘MostrarLista’.
La función que sigue, llamada ‘buscar_caracter’, recibe como parámetros
una lista, un caracter, un puntero y devuelve una variable de tipo ‘boolean’.
Consiste en recorrer con un puntero auxiliar la lista pasada por valor (ya que
no se va a modificar), buscando el caracter también pasado por valor. Finaliza
cuando encuentra el caracter o cuando se termina la lista. Si encuentra el caracter, devuelve ‘True’, y a su vez el puntero ‘res’ apuntará a dicho elemento.
Si no lo encuentra, por el contrario, devolverá ‘False’, y ‘nil’ en el puntero. Esta
función es de gran utilidad para ordenar la lista más tarde.
El procedimiento ‘anadir_final’ es bastante simple: recorre la lista hasta el
úĺtimo elemento, e inserta allí el elemento de tipo ‘TInfo’ pasado por valor. Si
la lista está vacía no es un problema, ya que lo inserta como primer elemento.
El procedimiento ‘insertar_caracter’ recibe una lista por referencia (es posible que se modifique) y un caracter por valor. Funciona basándose en la función
‘buscar_caracter’. Primero hace un llamado a la misma, para ver si el caracter ya está en la lista. Si lo encuentra, le suma uno a su frecuencia; si no lo
encuentra, lo inserta al final con el procedimiento ‘anadir_final’.
El procedimiento ‘insertar_ordenado’ se desarrollará en detalle ya que engloba varias formas de manejar los punteros en el TDA.
5
procedure i n s e r t a r _ o r d e n a d o _ c o d i g o ( var l i s t a : pTNodo ; dato : T i n f o ) ;
var aux1 , aux2 : pTNodo ; i n s e r t a d o : pTNodo ; s a l i r : boolean ;
begin
new( i n s e r t a d o ) ;
i n s e r t a d o ^ . i n f o := dato ;
aux2 := n i l ;
aux1 := n i l ;
s a l i r := f a l s e ;
l i s t a <> n i l then begin
if
aux1 := l i s t a ;
while ( aux1 <> n i l ) and ( s a l i r = f a l s e ) do begin
i f l e n g t h ( aux1 ^ . i n f o . codigo_H ) <= l e n g t h ( dato . codigo_H ) then
s a l i r := true
e l s e begin
aux2 := aux1 ;
aux1 := aux1 ^ . s i g ;
end ;
end ;
i f ( aux2 = n i l ) and ( aux1 <> n i l ) then begin
i n s e r t a d o ^ . s i g := aux1 ;
l i s t a := i n s e r t a d o ;
end
e l s e begin
i n s e r t a d o ^ . s i g := aux1 ;
aux2 ^ . s i g := i n s e r t a d o ;
end ;
end
Código 3: Procedimiento ‘insertar_ordenado_codigo’ completo.
En este procedimiento se utilizarán dos punteros auxiliares, llamados ‘aux1’
y ‘aux2’. Al comienzo del programa, se pide un espacio de memoria para el
puntero ‘insertado’. Este puntero, apuntará a un nodo con la información que
se desea insertar. Por esto, se le asigna a su campo de información la variable
por valor ‘dato’.
A continuación, se inicializan en ‘nil’ los dos punteros auxiliares, y la variable
6
de control ‘salir’ se inicializa en ‘false’. Primero, es de interés si la lista está vacía.
Si lo está, se dirige el primer puntero al elemento a insertar, y al siguiente del
insertado, se le asigna nil. Si no es así, se le asigna a aux1 el primer elemento
de la lista, y se ingresa en un bucle. Dicho bucle de tipo ‘while’ tiene como
condiciones de corte que el puntero aux1 no sea nil y que la variable de control
‘salir’ sea falsa.
Es importante destacar que la sentencia ‘if’ requerida para establecer el
estado de la variable ‘salir’ podría incluirse en el mismo while directamente,
pero podría darse un error. Esto dependerá del compilador (si el mismo trabaja
en cortocircuito o no), y radica en que podría preguntarse por algo que no existe.
La variable ‘salir’ depende de una sentencia ‘if’ dentro del bucle. La misma
pregunta si el código Huffman del dato apuntado por nuestro puntero auxiliar
es más largo que el que se quiere insertar. De ser así, se sale del bucle; si no se
cumple la condición, se continúa modificando los dos punteros. Primero, a aux2
se le asigna aux1, y luego aux1 pasa a apuntar al siguiente. Esta es una manera
simple de recorrer listas enlazadas con dos punteros.
Al salir del bucle, se pregunta por otra condición: si el puntero aux1 es
distinto de nil y el puntero aux2 es nil. Esta situación implicaría que hay que
insertar el nodo apuntado por ‘insertado’ entre el puntero ‘lista’ y el primer
elemento. Es importante tener en cuenta esta situación particular para evitar
errores. En dicho caso, se inserta el elemento asignándole a aux1.̂sig el primer
elemento de la lista, y al puntero de la lista, el primer elemento. Es una forma
básica de insertar elementos en una lista enlazada.
De esta manera, se describen varios procedimientos básicos de manejo de
listas enlazadas utilizados en este TDA. Al final de este procedimiento se tiene
insertado de manera ordenada el dato deseado.
Ahora, siguiendo con el análisis de procedimientos y funciones, vemos el
procedimiento ‘ordenar_código’. Lo que hace este procedimiento es simplemente ordenar por longitud de código Huffman ascendente los elementos de una
lista. Consiste en crear una nueva lista e insertar de manera ordenada (con
el procedimiento anterior) los elementos en ella. Finalmente, se borra la lista
original y se modifica para devolver la segunda.
La función ‘Codigo_Huffman_final’ recibe una lista y una tabla como parámetros por valor, y devuelve el código Huffman completo final. La variable
‘tabla’ en realidad es una lista enlazada que contiene todos los caracteres utilizados con su código Huffman. De esta manera, podemos recorrer la frase original,
buscar el caracter e imprimir su código Huffman. Si hacemos esto sucesivamente
con toda la frase original, obtenemos el código Huffman final. Cabe destacar que
en este momento, se tomó una decisión en cuanto a cómo generar el código final.
Se podía recorrer la tabla, y por cada caracter, reemplazarlo en la lista enlazada
del código original, o podía recorrerse el código original (que podría ser muy
largo) una sola vez, buscando cada caracter en la tabla que estaría ordenada y
sería más corta. Se consideró que la segunda opción era más eficiente.
Por último, el procedimiento ‘borrarLista’ recibe como parámetro por referencia el puntero a una lista, para eliminarla completamente, dejando este
puntero en ‘nil’. Es importante que se desarrolle bien para no dejar basura en
7
memoria; consiste en apuntar ‘lista’ al siguiente, manteniendo con un auxiliar
el primer elemento de la lista. Se lo elimina, y así sucesivamente hasta que se
termina la lista.
2.2.2.
TDA de Árboles
Nuevamente, se analizará primero la declaración de tipos de la unidad.
u n i t TDAArboles_Huffman ;
interface
u s e s TDAListas_Huffman , win types , System . S y s U t i l s ;
type
pTArbol = ^TArbol ;
TArbol = record
i n f o : TInfo ;
i z q , d e r : pTArbol ;
end ;
TRaiz = pTArbol ;
Código 4: Primeras líneas del TDA de Árboles.
Podemos ver que tiene la estructura de datos típica de un árbol convencional.
Primero se declara el tipo ‘pTArbol’, que es un puntero al tipo ‘TArbol’. Este
tipo es un registro de tres campos: ‘info’,‘izq’ y ‘der’. En el primero, se contiene la
información del caracter, del tipo ‘TInfo’. Los otros dos campos son punteros al
tipo ‘TArbol’. Por último, se declara el tipo ‘TRaiz’ igualmente al tipo ‘pTArbol’
para trabajar con más claridad.
procedure
procedure
procedure
procedure
c r e a r A r b o l ( var r a i z : TRaiz ) ;
d e s t r u i r A r b o l ( var r a i z : TRaiz ) ;
PreOrden ( r a i z : TRaiz ; r e c o r r i d o : S t r i n g ) ;
c a r g a r _ c o d i g o s _ l i s t a ( r a i z : TRaiz ; var l i s t a
: TLista ) ;
Código 5: Sección ‘interface’ del TDA de Árboles.
Vemos que por parte de los procedimientos y funciones, son sólo cuatro y
no tienen gran complejidad. Como la mayoría de los procedimientos de árboles,
son casi todos recursivos.
El primero de todos no necesita gran aclaración; pasado un puntero del tipo
‘TRaiz’, se lo inicia en ‘nil’.
El procedimiento ‘destruirArbol’, pasado un puntero del tipo ‘TRaiz’, opera
de manera recursiva eliminando a todos los nodos del árbol, dejando nuevamente
el primer puntero en ‘nil’.
El procedimiento ‘PreOrden’ lo que hace es recorrer el árbol en, justamente,
pre-orden, pero además incluye una variable de tipo String que almacena el
8
recorrido. Esto es necesario para generar los códigos Huffman de cada caracter.
Cuando se llega a un nodo hoja (esto lo confirmamos si ambos hijos son ‘nil’) se
guarda el recorrido hasta ese lugar en el campo ‘info.codigo_H’. Además se lo
imprime por consola. Este será el código del caracter de la hoja. Se toma como
un cero si se avanza hacia la derecha, y un uno si se avanza a la izquierda. Para
más claridad se adjunta dicho subprograma:
procedure PreOrden ( r a i z : TRaiz ; r e c o r r i d o : S t r i n g ) ;
begin
i f r a i z <> n i l
then begin
i f ( r a i z ^ . i z q = n i l ) and ( r a i z ^ . d e r = n i l ) then begin
r a i z ^ . i n f o . codigo_H := r e c o r r i d o ;
writeln ( ' L e t r a : ␣ ' , r a i z ^ . i n f o . l e t r a , ' F r e c u e n c i a : ␣ ' ,
r a i z ^ . i n f o . f r e c u e n c i a , ' Codigo ' , r a i z ^ . i n f o . codigo_H ) ;
end ;
PreOrden ( r a i z ^ . i z q , r e c o r r i d o + ' 0 ' ) ;
PreOrden ( r a i z ^ . der , r e c o r r i d o + ' 1 ' ) ;
end ;
end ;
Código 6: Procedimiento para recorrer el árbol en Pre-Orden detallado.
Por último, el procedimiento ‘cargar_codigos_lista’ simplemente carga en
la lista pasada por referencia la letra y el código Huffman de la misma. Esto se
hace para generar una tabla de codificación, donde están todos los caracteres
con su código. Esta tabla facilita la creación del código Huffman final.
2.2.3.
TDA de Listas de Árboles
La definición de tipos muestra lo mencionado anteriormente acerca de esta
estructura de datos particular. Vemos que cada nodo es un registro de dos
campos: un puntero al nodo siguiente y un puntero a un árbol. La información
del nodo, por lo tanto, siempre se encuentra contenida en la raíz del árbol.
9
u n i t TDAListasdeArboles_Huffman ;
interface
u s e s TDAListas_Huffman , TDAArboles_Huffman , System . S y s U t i l s ;
type
pTNodo_A = ^TNodo_A ;
TNodo_A = record
s i g : pTNodo_A ;
a r b o l : pTArbol ;
end ;
TLista_A = pTNodo_A ;
Código 7: Primeras líneas del TDA de listas de Árboles.
Este TDA es el que más funciones y procedimientos tiene, debido a que es
el más utilizado a la hora de generar el árbol.
procedure c r e a r L i s t a D e A r b o l e s ( var l i s t a _ A : TLista_A ) ;
procedure mostrarLista_A ( l i s t a _ A : TLista_A ) ;
procedure i n s e r t a r _ o r d e n a d o ( var l i s t a _ A : TLista_A ; dato : TInfo ) ;
procedure i n s e r t a r N o d o _ o r d e n a d o ( var l i s t a _ A : TLista_A ;
var nodo : pTNodo_A ) ;
procedure o r d e n a m i e n t o _ L i s t a ( var l i s t a : T L i s t a ;
var r e s : TLista_A ) ;
procedure sacarNodo ( var l i s t a _ A : TLista_A ; nodo : pTNodo_A ) ;
procedure unirNodos ( var l i s t a _ A : TLista_A ;
nodo1 , nodo2 : pTNodo_A ) ;
procedure g e n e r a r _ A r b o l ( var l i s t a _ A : TLista_A ) ;
procedure e l i m i n a r L i s t a D e A r b o l e s ( var l i s t a _ A : TLista_A ) ;
Código 8: Sección ‘Interface’ del TDA de listas de Árboles.
El pimer procedimiento es para crear la lista de árboles. Con una variable
de tipo ‘TLista_A’ pasada por referencia, se la inicializa en ‘nil’.
El procedimiento ‘mostrarLista_A’ es práctico nuevamente para visualizar
de qué manera está generada la lista de árboles. Sólo muestra el primer elemento
del árbol vinculado al nodo de la lista.
El procedimiento ‘insertar_ordenado’ inserta en una lista de árboles pasada
por referencia, un dato de tipo ‘TInfo’ ordenado por frecuencia. Dentro del
programa simplemente se recorre la lista con un puntero auxiliar hasta encontrar
una frecuencia mayor a la del dato a insertar, y en éste lugar se insertará el
mismo.
El siguiente procedimiento es una extensión del anterior. En lugar de insertar
de manera ordenada por frecuencia un dato, se pide por referencia el nodo
completo. Es de utilidad en el procedimiento ‘unirNodos’.
El procedimiento ‘ordenamiento_lista’, recibe una lista de tipo ‘TLista’ por
referencia y devuelve el resultado en una lista de árboles. Lo que hace es ordenar
10
por frecuencia ascendente los datos de la lista original, utilizando la subrutina
‘insertar_ordenado’. El funcionamiento es simple: recorre la lista desde el principio hasta el final, y va insertando de manera ordenada los datos en la lista de
árboles. Por último, elimina la lista original, ya que a partir de aquí no tiene
sentido mantenerla.
El procedimiento ‘sacarNodo’ es necesario para que ‘unirNodos’ funcione
correctamente. Lo que hace este subproceso es simplemente sacar un nodo de
la lista. Es importante aclarar que no lo borra, simplemente lo saca de la lista,
pero si guardamos el puntero al nodo, el mismo seguirá ahí.
El procedimiento unirNodos simplemente realiza la tarea de unir dos nodos.
Dichos nodos se pasan por valor ya que no se modificarán, simplemente se genera
un nodo nuevo con una frecuencia igual a la suma de los dos anteriores. Además, este nuevo nodo tiene como hijos a los otros dos. Utiliza ‘sacarNodo’ para
sacar ambos nodos de la lista, generar el nuevo, y volver a insertar todo. Este
procedimiento es necesario para generar el árbol en el siguiente procedimiento.
Para generar el árbol, el procedimiento ‘generar_Arbol’ recibe simplemente
una lista de árboles por referencia, ya que se modificará. Aquí entra en juego
una parte fundamental del algoritmo de Huffman, que consiste en unir los dos
primeros nodos de la lista, e insertar el nuevo nodo ‘ficticio’ ordenadamente en
la lista. Esto se repite sucesivamente hasta que la lista tenga un sólo elemento,
con frecuencia igual a la suma de la frecuencia de todos los caracteres. Este
nodo será el nodo raíz del árbol completo.
Por último, el procedimiento ‘eliminarListadeArboles’ se ocupa de borrar
toda la lista de árboles, primero eliminando el árbol y luego la lista enlazada,
de manera de no dejar ningún tipo de basura en memoria.
2.3.
Funcionamiento del programa principal
Con la explicación del funcionamiento de todos los TDA, el código principal
no tiene muchas complicaciones. Primero analizaremos las variables declaradas
al principio del programa.
11
program Huffman_Consola ;
{$APPTYPE CONSOLE}
{$R ∗ . r e s }
uses
System . S y s U t i l s ,
TDAListas_Huffman in ' TDAListas_Huffman . pas ' ,
TDAListasdeArboles_Huffman in ' TDAListasdeArboles_Huffman . pas ' ,
TDAArboles_Huffman in ' TDAArboles_Huffman . pas ' ;
var l i s t a , t e x t o _ o r i g i n a l : T L i s t a ;
l i s t a _ A : TLista_A ;
i : integer ;
t e x t o , en co de : S t r i n g ;
elem : TInfo ;
Código 9: Primeras líneas del código principal.
Las variables ‘lista’ y ‘texto original’ son del tipo TLista. La primera, será
utilizada para generar la lista principal con todos los caracteres y su frecuencia.
La segunda, se utilizará para guardar la frase original y luego codificarla.
La variable ‘lista_A’ es la única lista de árboles que se utilizará para generar
el árbol.
La variable i nos sirve de contador global, que es necesario para una tarea
específica: nombrar a los nodos ‘ficticios’. Para identificar los nodos generados artificialmente, de los nodos hoja, los nodos ficticios tienen en el campo
‘info.letra’ a un asterisco seguido de un número. Este número depende de la
variable global, con el objetivo de no repetir nombres de nodos.
La variables ‘texto’, de tipo ‘String’ es necesaria para leer la cadena de texto
ingresada por el usuario con el comando ‘readln’.
Por último, la variable ‘elem’ del tipo ‘TInfo’ es necesaria para ir insertando
en nuestra lista enlazada los caracteres leídos por consola.
12
begin
writeln ( ' I n g r e s e ␣ una ␣ o r a c i o n ␣ t e r m i n a d a ␣ en ␣ punto ␣ para ␣ c o m p r i m i r l a ' ) ;
readln ( t e x t o ) ;
i := 1 ;
crearLista ( lista );
while t e x t o [ i ] <> ' . ' do begin
insertar_caracter ( l i s t a , texto [ i ] ) ;
elem . l e t r a := t e x t o [ i ] ;
a n a d i r _ f i n a l ( t e x t o _ o r i g i n a l , elem ) ;
i := i +1;
end ;
Código 10: Sección del bucle principal que genera la lista enlazada.
Sobre el comienzo del programa se escribe un mensaje en la consola pidiendo
que se ingrese una oración terminada en punto. Si no se tiene en cuenta este
detalle, el programa ingresa en un bucle infinito. No se trabajó en una solución
para este problema ya que es una primera implementación del algoritmo con el
fin de comprender su funcionamiento y luego extenderlo en otra versión.
A continuación, se inicializa el contador ‘i’ en 1, y se crea la lista a partir de la variable ‘lista’. Aquí comienza el bucle que recorrerá la cadena de
texto ingresada e insertará los caracteres en la lista enlazada, a partir del procedimiento ‘insertar_caracter’. Además, se utiliza otra lista enlazada, llamada
‘texto_original’, donde se guardará la frase original (esto es para evitar el uso
de una variable tipo ‘String’).
mostrarLista ( l i s t a ) ;
mostrarLista ( texto_original ) ;
readln ;
crearListaDeArboles ( lista_A ) ;
ordenamiento_Lista ( l i s t a , lista_A ) ;
mostrarLista_A ( l i s t a _ A ) ;
readln ;
Código 11: Se muestran las listas y se genera la lista de árboles.
Los dos primeros bloques a continuación tienen como objetivo mostrar las
listas enlazadas previamente, para asegurar que estén bien generadas. Por esto,
primero se muestra la lista enlazada con los caracteres y luego, la lista enlazada
con el texto original. Se espera a que el usuario presione ‘enter’, y luego se crea
la lista de árboles. Utilizando el procedimiento ‘ordenamiento_Lista’ se borra
la lista original y se ordena en la lista de árboles. Aquí se muestra la lista de
árboles y nuevamente se espera al usuario.
13
generar_Arbol ( lista_A ) ;
mostrarLista_A ( l i s t a _ A ) ;
readln ;
PreOrden ( l i s t a _ A ^ . a r b o l , ' ' ) ;
readln ;
Código 12: Generación del árbol y los códigos.
Las próximas dos sentencias generan el árbol a partir del procedimiento ‘generar_Arbol’, que une los nodos sucesivamente hasta que sólo quede un nodo
raíz y el árbol, y muestran la lista de árboles (sólo la raíz). Esto es para comprobar nuevamente que está todo funcionando correctamente. Por último, se llama
al procedimiento ‘PreOrden’, que recorre el árbol en pre-orden y además, codifica los códigos Huffman para cada caracter. También se muestra el recorrido
de las hojas por consola.
crearLista ( lista );
cargar_codigos_lista ( lista_A ^. arbol , l i s t a ) ;
mostrarLista ( l i s t a ) ;
readln ;
ordenar_codigo ( l i s t a ) ;
mostrarLista ( l i s t a ) ;
readln ;
writeln ( ' El ␣ c o d i g o ␣ Huffman ␣ e s : ␣ ' ) ;
writeln ( codigo_Huffman_Final ( t e x t o _ o r i g i n a l , l i s t a ) ) ;
readln ;
borrarLista ( l i s t a );
borrarLista ( texto_original );
eliminarListaDeArboles ( lista_A ) ;
Código 13: Finalización del programa.
En los últimos bloques, se reutiliza la variable ‘lista’ que fue borrada previamente (está vacía); ahora se la utilizará como tabla de codificación. Para realizar
esto se llama a ‘cargar_codigos_lista’ con la raíz del árbol y la tabla. Se muestra
la tabla, y se procede a ordenarla de menor a mayor según la longitud de dicho
código, con ‘ordenar_codigo’. Esto se realiza para que los caracteres que tienen menor longitud de código (que implica una mayor frecuencia de repetición)
estén más cerca a la hora de buscarlos.
Finalmente, se obtiene el código Huffman final en forma de String (poco deseable) y se imprime por consola, utilizando la función ‘código_Huffman_final’,
que genera dicho código a partir de la tabla de codificación y el texto original.
Se borran todas las listas y árboles y se finaliza el programa.
14
2.4.
Añadido de Graphviz
Para obtener una respuesta gráfica del programa y poder ver el árbol binario
completo, se extendió el algoritmo para poder generar dicha imagen a partir del
programa ‘Graphviz’.
2.4.1.
Funcionamiento básico de Graphviz
‘Graphviz es un software de visualización de grafos open source. La visualización de gráficos es una forma de representar información estructural como
diagramas de redes y gráficos abstractos. Tiene aplicaciones importantes en redes, bioinformática, ingeniería de software, diseño web y de bases de datos,
aprendizaje automático y en interfaces visuales para otros dominios técnicos.’
[2]
Más allá de la definición formal presentada del programa, lo que nos interesa
es cómo utilizarlo. Para ello, se genera un archivo de texto en lenguaje ‘dot’. No
se entrará en detalle en cómo funciona este lenguaje, pero es importante saber
que nos permite definir estructuras de nodos, que a su vez pueden dividirse
internamente.
Una vez definidas todas las estructuras de los nodos, se procede a apuntarlos
entre ellos. Para ello, a la hora de generar cada nodo, se etiquetó cada subpartición del mismo con una etiqueta, y en el programa se especifica que etiqueta
apunta a cual, mediante una flecha (‘->’).
Una vez generado y terminado el archivo de texto, se ejecuta por consola (en
Windows) la línea ‘dot -Tpng archivo.txt -o archivo_salida.png’. Esto compila el
archivo, genera el archivo de imagen (pueden especificarse otros formatos cambiando -Tpng por otra extensión) y lo guarda en el archivo de salida. También
se puede especificar la ruta donde estará cada archivo.
2.4.2.
Ejemplos de funcionamiento de Graphviz
En el primer ejemplo generaremos un sólo nodo que apunte a otro, y veremos
algunas propiedades básicas.
d i g r a p h G{
nodo1 [ shape = e l l i p s e , c o l o r = g r e e n , s t y l e = f i l l e d ] ;
nodo2 [ shape = s q u a r e , c o l o r = magenta ] ;
nodo1 −> nodo2 ;
}
Código 14: Primer ejemplo Graphviz.
Para comenzar, declaramos que el gráfico será de tipo dirigido (digraph).
Entre llaves, declararemos los nodos y cómo se apuntan entre ellos. Entre corchetes, podremos cambiar propiedades de los mismos, como colores, formas, etc.
El código presentado, genera una imagen como la siguiente:
15
Figura 5: Primer ejemplo de grafos dirigidos en Graphviz.
Como se dijo previamente, también se pueden declarar nodos con forma de
estructura. El siguiente ejemplo utiliza este recurso; veremos que el primer nodo
tendrá dos divisiones, una al lado de la otra, con sus respectivas etiquetas, que
apuntan a una subdivisión del segundo nodo. El segundo nodo tiene 3 divisiones;
las divisiones horizontales se aclaran con ‘|’, y las verticales con llaves, por eso
‘A’ y ‘B’ están uno encima del otro y a su vez en división horizontal con ‘Nodo2’.
d i g r a p h G{
node [ shape = r e c o r d ]
s t r u c t 1 [ l a b e l = "<f 0 >Nodo1 | ␣<f 1 >S i g u i e n t e " ] ;
s t r u c t 2 [ l a b e l = "<f 0 >Nodo2|{ < f 1 >A␣|< f 2 >␣B} " ] ;
s t r u c t 1 : f 1 −> s t r u c t 2 : f 1 ;
}
Código 15: Segundo ejemplo de Graphviz.
16
Figura 6: Segundo ejemplo de grafos con graphviz, ahora con estructuras.
Hay muchas más opciones para graficar usando este lenguaje de programación y su compilador. En el compresor Huffman desarrollado, no se utilizan más
recursos que los presentados. Se adjunta una guía de lenguaje ‘dot’ mucho más
desarrollada en las referencias.[2]
2.4.3.
Procedimientos extra utilizados para graficar usando Graphviz
En la primera versión de consola, ya se incluyeron los procedimientos necesarios para generar la imagen del árbol binario y ver si estaba generado correctamente. Los mismos se muestran a continuación. También se incluye una leve
modificación en la definición de tipos; se le agrega a cada nodo del árbol un
campo llamado ‘graf’ de tipo ‘TGraf’. El tipo TGraf tiene dos campos: ‘color’
y ‘num’. Los primeros dos son de tipo ‘String’ y el último es de tipo ‘integer’.
17
type
Tcolor = String ;
Tnum = integer ;
T g r a f = record
c o l o r : TColor ;
num : Tnum ;
end ;
pTArbol = ^TArbol ;
TArbol = record
i n f o : TInfo ;
g r a f : Tgraf ;
i z q , d e r : pTArbol ;
end ;
TRaiz = pTArbol ;
Código 16: Definición de tipos del TDA modificado.
El procedimiento que engloba toda la graficación se llama ‘mostrarArbol’, y
se encuentra desarrollado en el TDA de árboles. En el mismo TDA, se declararon dos procedimientos que no son visibles, es decir, son locales, y requeridos
únicamente por el procedimiento ‘mostrarArbol’. No se muestran en ‘interface’.
Estos procedimientos son ‘recorrer_graficar’ y ‘declarar_struct’.
El subprograma ‘declarar_struct’ se encarga de escribir en el archivo de
texto especificado toda la parte de estructuras de los nodos a graficar. También
se ocupa de establecer su color y nombre, según si es un nodo hoja o interno. Si
es un nodo interno, será de color negro, y su nombre el respectivo número del
contador global luego del asterisco.
El subprograma ‘recorrer_graficar’ recorre el árbol generado en forma recursiva, similar al Pre-Orden, y va escribiendo en el archivo de texto cómo se
apuntan los nodos entre sí.
Por último, el procedimiento ‘mostrarArbol’ primero crea una variable de
tipo ‘TextFile’, y le asigna su correspondiente nombre y ruta. Escribe las primeras dos líneas del archivo para inicializar el gráfico, y después llama a los
dos procedimientos mencionados previamente. De esta manera, queda generado
el archivo de texto llamado ‘Arbol_Huffman.txt’ listo para compilar y ejecutar
con Graphviz. Se genera en la carpeta donde corre la aplicación. Se verá claramente que, por la forma en la que se trabajó, este archivo se divide en dos
secciones bastante destacadas: en la primera, se declaran todos los nodos con
sus subdivisiones, y en la segunda, se apuntan correspondientemente.
18
struct13
struct14
struct15
struct16
struct17
struct18
struct19
[
[
[
[
[
[
[
color
color
color
color
color
color
color
=
=
=
=
=
=
=
green
green
black
black
green
green
green
,
,
,
,
,
,
,
s t r u c t 1 −> s t r u c t 2 : f 0 ;
s t r u c t 1 −> s t r u c t 1 5 : f 0 ;
s t r u c t 1 5 −> s t r u c t 1 6 : f 0
s t r u c t 1 5 −> s t r u c t 1 9 : f 0
s t r u c t 1 6 −> s t r u c t 1 7 : f 0
s t r u c t 1 6 −> s t r u c t 1 8 : f 0
s t r u c t 2 −> s t r u c t 3 : f 0 ;
label
label
label
label
label
label
label
=
=
=
=
=
=
=
"{{< f 0 >l |< f 1 >1}|< f 2 >1001} " ] ;
"{{< f 0 >H|< f 1 >1}|< f 2 >1000} " ] ;
"<f 0 >∗7| ␣<f 1 >6" ] ;
"<f 0 >∗4| ␣<f 1 >3" ] ;
"{{< f 0 >␣|< f 1 >2}|< f 2 >011}" ] ;
"{{< f 0 >s |< f 1 >1}|< f 2 >010}" ] ;
"{{< f 0 >a|< f 1 >3}|< f 2 >00}" ] ;
;
;
;
;
Código 17: Sección de código generado para compilar en Graphviz.
2.5.
Vulnerabilidades y limitaciones de la primera versión
La primera versión tenía el objetivo de realizar un primer acercamiento al
algoritmo de Huffman, con una implementación básica extendible a una versión
gráfica. Por lo tanto, tiene varias limitaciones.
La primera, observable a simple vista, es que no hay un control realizado
sobre la oración ingresada. Si no termina en punto, el programa entra en un
bucle infinito.
Otra limitación es que no podemos comprimir un archivo de texto completo;
debemos ingresar los caracteres por consola.
Por último, el código Huffman es prácticamente inútil si no se codifica un
descompresor. Por lo tanto, esta versión de desarrollo es muy básica y no sirve
para más que comprobar que el algoritmo base funciona correctamente, con
oraciones simples.
Cabe destacar el agregado del graficador de árboles en la segunda implementación de consola, aunque las limitaciones siguen siendo las mismas. Solamente
se amplía la información presentada para comprobar al detalle el árbol generado. Además, es muy poco práctico ya que para generar la imagen se debe
recurrir a la consola de comandos, para compilar y ejecutar el archivo de texto
con Graphviz.
3.
3.1.
Versión con interfaz gráfica
Metodología y estructuras de datos
Las estructuras de datos a utilizar en esta versión no tienen grandes modificaciones, ya que las implementadas en la primera versión funcionaron de la
manera esperada.
19
La metodología a seguir fue, primero, adaptar los TDA utilizados a la nueva
versión. Se eliminaron procedimientos y funciones que no se requerían en este
caso, y se agregaron algunos.
Luego, se diseñó el formulario que se utilizaría. El codificador constó de tres
botones: Uno para abrir el archivo a comprimir, otro para abrir el directorio de
destino de todos los archivos, y otro que permite visualizar el árbol generado en
el mismo formulario.
El diseño del decodificador fue similar; se crearon tres botones, uno para
abrir el archivo comprimido, otro para abrir la tabla de decodificación, y el
último para seleccionar el nombre y ubicación del archivo descomprimido.
3.2.
Problemas principales y su solución
Se encontraron varios problemas al implementar esta versión. El primero,
fue que se perdió de vista que una vez generado el código Huffman completo,
no era suficiente. Esto se explica por la forma en la que se guardan los archivos.
Si para generar el código Huffman reemplazamos cada caracter por su código
correspondiente, y lo almacenamos en forma de archivo de texto, tendremos un
problema; que ahora, cada caracter ocupa mucho más espacio, porque a cada
cero o uno se le están asignando 8 bits de memoria.
Se observó entonces que la manera de hacer que el algoritmo fuera útil y
realmente comprimiera el archivo, era necesario guardar el código Huffman final
en un archivo binario. De esta manera, se le asigna a cada uno o cero sólo un
bit, lo que genera que el archivo original se comprima.
Algo similar ocurrió con la generación de la tabla de codificación. Guardarla
en formato de archivo de texto ocupaba mucho espacio, por lo que se recurrió
nuevamente a la generación de un archivo binario, que optimizara mejor el
espacio.
Para trabajar con variables de tipo ‘byte’ se encontraron a su vez ciertas
dificultades. Estas consistieron en la longitud variable del código Huffman de
cada caracter. Para grabar un archivo de texto, se deben cargar byte por byte
los datos, no lo podemos hacer bit por bit. Por lo tanto, se tuvo que agrupar
toda la información y empaquetar en bytes, para luego guardarla en disco.
Para realizar esto, se hizo uso de varios conversores de binario a decimal y
viceversa. Esto es porque Pascal trabaja las variables tipo ‘Byte’ como números
enteros del 0 al 255. Por lo tanto, una vez que se obtenía cada byte, se debía
pasar a decimal, para poder cargarlo en el archivo binario.
Algo similar ocurrió en el proceso de decodificación: Los datos estaban agrupados en bytes, y debían separarse. Con cada byte expresado como un número
entero, debía pasarse a binario con otro conversor, para luego insertar en una
lista enlazada cada caracter, y poder decodificar el código a partir de la tabla.
Cabe destacar que aquí se consideró necesario el desarrollo de un nuevo TDA
para manejar listas enlazadas de caracteres.
Además, surgieron algunas complicaciones secundarias; el desarrollo se volvió
algo tedioso con las propiedades de cada elemento gráfico. Un problema que no
20
se pudo resolver es que la imagen mostrada en el primer formulario no se vé de
la manera esperada; pierde mucha definición respecto a la original.
3.3.
Desarrollo del codificador
Como se menionó previamente, los TDA se vieron levemente modificados;
ahora se mostrarán todas las modificaciones y nuevas subrutinas implementadas.
3.3.1.
TDA de Listas
Comenzando por la definición de tipos, se incluyó un nuevo tipo, llamado
‘TTabla’. Este tipo de dato fue útil para generar la tabla de codificación en
binario.
type
TTabla = record
l e t r a : char ;
codigo : String [ 2 0 ] ;
end ;
TSubStr = S t r i n g [ 8 ] ;
TInfo = record
letra : String ;
f r e c u e n c i a : integer ;
codigo_H : S t r i n g ;
end ;
PTNodo = ^TNodo ;
TNodo = record
i n f o : TInfo ;
s i g : pTNodo ;
end ;
T l i s t a = pTNodo ;
Código 18: Definición de tipos del TDA de listas.
El tipo mencionado tenía dos campos: uno llamado ‘letra’, de tipo caracter,
y otro llamado ‘codigo’, de tipo string con limitación de tamaño. La elección del
tamaño de este string es un punto a mejorar y se mencionará con más detalle
en las conclusiones.
También se declaró un tipo ‘TSubStr’, como un string de tamaño limitado
a 8 caracteres. Este tipo de dato es útil para trabajar con bytes, y decodificar
números enteros en un string de caracteres ceros y unos.
Por lo demás, vemos que no hay modificaciones respecto al TDA original.
21
procedure c r e a r L i s t a ( var l i s t a : T L i s t a ) ;
function b u s c a r _ c a r a c t e r ( l i s t a : T L i s t a ; c a r a c t e r : char ;
var r e s : pTNodo ) : boolean ;
procedure i n s e r t a r _ c a r a c t e r ( var l i s t a : T L i s t a ; c a r a c t e r : char ) ;
procedure i n s e r t a r _ o r d e n a d o _ c o d i g o ( var l i s t a : pTNodo ;
dato : T i n f o ) ;
procedure o r d e n a r _ c o d i g o ( var l i s t a : T L i s t a ) ;
procedure codigo_Huffman_final ( l i s t a : T L i s t a ;
t a b l a : TLista ; arc hivo_salida : S t r i n g ) ;
procedure c o d i f i c a r T a b l a ( t a b l a : T L i s t a ;
archivo_salida_tabla : String ) ;
procedure a n a d i r _ f i n a l ( var l i s t a : T L i s t a ;
e l e m e n t o : TInfo ) ;
procedure b o r r a r L i s t a ( var l i s t a : T L i s t a ) ;
Código 19: Sección ‘interface’ del TDA.
Por el lado de los procedimientos y funciones, se eliminó ‘mostrarLista’,
ya que no era útil en esta versión. Los demás, se mantuvieron, y se modificó
‘código_Huffman_final’. La modificación consiste en adaptar el subprograma
para cargar en el archivo binario especificado todo el código. También se agregó
‘codificarTabla’, que es donde ahora se genera la tabla en forma de binario. Los
analizaremos en detalle:
22
procedure codigo_Huffman_final ( l i s t a : T L i s t a ;
t a b l a : TLista ; arc hivo_salida : S t r i n g ) ;
var aux , r e s : pTNodo ;
F : F i l e of Byte ;
b : byte ;
S : TSubSTr ;
i : Integer ;
begin
A s s i g n F i l e (F , a r c h i v o _ s a l i d a ) ;
Rewrite (F ) ;
s := ' ' ;
aux := l i s t a ;
while aux <> n i l do begin
b u s c a r _ c a r a c t e r ( t a b l a , aux ^ . i n f o . l e t r a [ 1 ] , r e s ) ;
f o r i := 1 to l e n g t h ( r e s ^ . i n f o . codigo_H ) do begin
i f l e n g t h ( s ) = 8 then begin
b := c o n v e r s o r _ b i n a r i o ( s ) ;
write (F , b ) ;
s := ' ' ;
end ;
s := s + r e s ^ . i n f o . codigo_H [ i ] ;
end ;
aux := aux ^ . s i g ;
end ;
i f l e n g t h ( s ) < 8 then begin
b := c o n v e r s o r _ b i n a r i o ( s ) ;
write (F , b ) ;
end ;
c l o s e f i l e (F ) ;
end ;
Código 20: Modificación sobre el procedimiento que genera el código final
Se observa que primero se piden como parámetros por valor una lista y una
tabla. La lista debería contener el texto original con cada caracter, y la tabla de
codificación, cada caracter seguido de su código Huffman. Las variables locales
23
‘aux’ y ‘res’ son punteros, ‘F’ es de tipo ‘File of byte’, es decir, una variable a
la que se le asignará un archivo binario para escribir; ‘b’ es de tipo ‘byte’, ‘s’ de
tipo ‘TSubStr’, e ‘i’ de tipo entero.
Al comenzar el programa, se le asigna a F la ruta y el nombre del archivo
binario a generar, pasada por valor en los parámetros. Se utiliza ‘Rewrite(F)’
ya que escribiremos el archivo, y se inicializa el sub string en vacío.
Lo que hace el próximo bucle, es recorrer el texto original con el puntero
auxiliar ‘aux’, y busca el caracter en la tabla. Luego, en el siguiente bucle ‘for’,
se recorre el código Huffman de la letra, y cuando el substring se llena, se
transforma el byte del string en un número entero entre 0 y 255, y se carga en el
archivo binario. Se pone el string en blanco, y se continúa hasta que se termina
la lista del texto original.
Al final del código, se completa con ceros el último byte, ya que no podemos
cargar un byte incompleto. De esta manera, ya generamos el archivo binario
con el texto original comprimido, y podemos comprobar que pesa menos que el
archivo a comprimir.
procedure c o d i f i c a r T a b l a ( t a b l a : T L i s t a ;
archivo_salida_tabla : String ) ;
var F : F i l e of TTabla ; aux : pTNodo ; i n s e r t a d o : TTabla ;
begin
i f t a b l a <> n i l then begin
A s s i g n F i l e (F , a r c h i v o _ s a l i d a _ t a b l a ) ;
Rewrite (F ) ;
aux := t a b l a ;
while aux <> n i l do begin
i n s e r t a d o . l e t r a := aux ^ . i n f o . l e t r a [ 1 ] ;
i n s e r t a d o . c o d i g o := aux ^ . i n f o . codigo_H ;
write (F , i n s e r t a d o ) ;
aux := aux ^ . s i g ;
end ;
c l o s e f i l e (F ) ;
end ;
end ;
Código 21: Procedimiento donde se genera la tabla en un archivo binario
Vemos que este procedimiento recibe una tabla en forma de lista enlazada y
una ruta para el archivo de salida. Se declara a ‘F’ como variable para asignar
el archivo que será de tipo ‘TTabla’. Es decir, se cargarán los datos en el archivo
binario con el formato ‘TTabla’.
24
Ahora simplemente se recorre la lista enlazada hasta el final, asignando a la
variable temporal ‘insertado’ la respectiva letra y su código Huffman. Se escribe
la variable en el archivo y se repite el bucle. Por último se cierra el archivo y
termina el programa.
3.3.2.
TDA de árboles
Por parte de este TDA, no hay un desarrollo ya que no tiene ningún tipo de
modificación respecto del que se utilizó en la versión de consola con graficador
incluido.
3.3.3.
TDA de listas de Árboles
En este TDA tampoco hay grandes cambios; solamente se eliminó el procedimiento ‘Mostrar_Lista_A’, ya que, nuevamente, no se podrá mostrar información por consola.
3.4.
Programa principal
El programa principal no varía mucho en conceptos respecto a las versiones
de consola. Sí hay una gran variación en cómo funciona el código; ahora, los
eventos se verán controlados por los botones.
Al comienzo del programa, se declara un procedimiento llamado ‘codificar’.
Este procedimiento es lo más importante del programa, así que a continuación
se lo muestra en detalle:
25
procedure c o d i f i c a r ( var l i s t a : T L i s t a ;
var t e x t o _ o r i g i n a l : T L i s t a ; var l i s t a _ A : TLista_A ;
archivo_entrada : String ) ;
var c : char ; F : T e x t F i l e ; elem : TInfo ;
begin
crearLista ( lista );
A s s i g n F i l e (F , a r c h i v o _ e n t r a d a ) ;
Reset (F ) ;
while not EOf (F) do
begin
Read(F , c ) ;
insertar_caracter ( lista , c );
elem . l e t r a := c ;
a n a d i r _ f i n a l ( t e x t o _ o r i g i n a l , elem ) ;
end ;
i n s e r t a r _ c a r a c t e r ( l i s t a , chr ( 1 ) ) ;
crearListaDeArboles ( lista_A ) ;
o r d e n a m i e n t o _ L i s t a ( l i s t a , l i s t a _ A ) ; / / e s t o ya b o r r a l a l i s t a o r i g i n a l
generar_Arbol ( lista_A ) ;
PreOrden ( l i s t a _ A ^ . a r b o l , ' ' ) ; // ahora cada c a r a c t e r t i e n e su c o d i g o
crearLista ( lista );
// puedo r e u t i l i z a r l a v a r i a b l e l i s t a
cargar_codigos_lista ( lista_A ^. arbol , l i s t a ) ;
ordenar_codigo ( l i s t a ) ;
end ;
Código 22: Procedimiento principal de la versión de consola.
Se observa que esta subrutina no hace más que llamar a todos los procedimientos en el orden necesario para llevar a cabo la codificación completa de
manera similar a las primeras versiones de consola. Se tuvo que generar este
procedimiento para poder llamarlo más tarde con el accionar de los botones.
Recibe como parámetros una lista enlazada donde se guardarán los caracteres
con su frecuencia, otra lista donde se almacena el texto original, una lista de
árboles donde se generará el árbol principal y un String que tiene el nombre de
la ruta al archivo de texto que se quiere comprimir.
Luego, como variables locales, tenemos un caracter, una variable de tipo
‘TextFile’ para asignarla con la ruta especificada, y un elemento de tipo ‘TInfo’.
Primero, se crea la lista, y se asigna a F la ruta al archivo original. Se
utiliza un ‘Reset(F)’ para ubicar el cursor en el primer caracter del archivo de
texto. Luego, comienza el bucle principal que insertará los caracteres en la lista
enlazada de la misma manera que lo hacían los primeros programas, sólo que
ahora los caracteres se leen de un archivo de texto. Además, el bucle guarda el
26
texto original en otra lista enlazada.
La siguiente línea es importante; lo que hace, es asignar un caracter de
terminación del archivo. Es decir, al final de toda la lista enlazada generada, se
agrega un caracter no visible para el usuario (el que tiene asignado el código
ASCII ‘1’) para decodificar correctamente el archivo más tarde.
Por último, se crea la lista de árboles, se genera el árbol de la misma manera
que antes, se recorre en Pre-Orden para cargar los códigos y se reutiliza la lista
principal para usarla como tabla de codificación más tarde.
Es importante destacar que aquí no se encuentra la generación de la tabla
ni el archivo en binario ya que no se piden como parámetros los nombres de
dichos archivos. La invocación a los procedimientos que generan los dos archivos
binarios se da cuando el usuario presiona el botón de ‘codificar’.
El resto del código consiste en asignar las propiedades de cada elemento
gráfico, y realizar algunos controles con la visibilidad de los botones, para no
acceder a lugares de la memoria que no están asignados. Es importante destacar
la siguiente línea de código:
S h e l l E x e c u t e ( Handle , n i l , PChar ( ' dot ' ) ,
PChar ( '−Tpng␣ ' + d i r e c t o r i o E l e g i d o +
' \ Arbol_Huffman . t x t ␣−o ␣ " ' + d i r e c t o r i o E l e g i d o +
' / Arbol_Huffman . png " ' ) , n i l ,SW_SHOW) ;
Código 23: Se genera la imagen .png a partir del archivo de texto con Graphviz
Lo que hace este comando es ejecutar una aplicación externa, a partir de la
consola de comandos. Se utiliza en este caso para generar la imagen .png sin
que el usuario tenga que recurrir a hacerlo manualmente, y se muestre en el
formulario.[5]
Ejecuta en este caso por la consola el comando ‘(dot -TPNG Arbol_Huffman.txt
-o Arbol_Huffman.png)’ en el directorio especificado por el usuario. Si no se tiene instalado el graficador ‘Graphviz’ este comando no funcionará.
3.5.
Desarrollo del decodificador
El decodificador debió generarse desde cero ya que no se había implementado ninguna versión del mismo. El funcionamiento básico consiste en leer cada
byte del archivo binario, guardar cada cero o uno como un caracter en una lista enlazada, y luego trabajar sobre esa misma lista de caracteres para poder
decodificar el texto. Algo similar se llevó a cabo con la tabla binaria.
Por esto mismo, se consideró necesario el añadido de un TDA que manejara
listas de caracteres; el mismo se llama ‘TDAListas_char’. Además, fue necesaria
la inclusión del TDA de listas usado en el codificador, ya que para generar una
lista enlazada con los datos de la tabla se trabajó con este tipo de datos a fin
de no tener que generar uno nuevo.
27
3.5.1.
TDA de Listas de caracteres
u n i t TDAListas_char ;
interface
u s e s TDAListas_HuffmanV2 ;
type
PTNodoC = ^TNodoC ;
TNodoC = record
i n f o : char ;
s i g : pTNodoC ;
end ;
TListaC = pTNodoC ;
Código 24: Primeras líneas del TDA
Vemos que este TDA requiere del uso del TDA de listas. Esto es debido a
que algunos procedimientos vinculan ambos tipos de datos. Por la parte de la
definición de tipos, vemos que tenemos primero la definición del tipo puntero
a nodo, ‘PTNodoC’, que apuntará a un tipo ‘TNodoC’. Este nodo tendrá dos
campos; un caracter, y un puntero al próximo nodo. Por último, como ya es
costumbre, se define el tipo TListaC como un puntero a un nodo, para trabajar
con más comodidad.
procedure c r e a r L i s t a C ( var l i s t a C : TListaC ) ;
procedure a n a d i r _ f i n a l C ( var l i s t a C : TListaC ; e l e m e n t o : char ) ;
procedure g e n e r a r _ t e x t o ( var l i s t a C : TListaC ;
archivo _salida : S t r i n g ; t a b l a : TLista ) ;
procedure b o r r a r L i s t a C ( var l i s t a C : TListaC ) ;
Código 25: Sección ‘Interface’ del TDA
Por la parte de procedimientos y funciones, no hay mucho para decir; tenemos dos procedimientos para crear y borrar la lista de caracteres, y luego
tenemos otros dos correspondientes al manejo de la lista.
El procedimiento ‘anadir_finalC’ agrega al final de una lista enlazada el
caracter especificado por valor.
El procedimiento ‘generar_texto’ es el de mayor importancia ya que lleva
a cabo el objetivo principal que se tiene; generar el texto a partir de la lista
enlazada de caracteres proveniente de recorrer el archivo binario. Lo veremos en
detalle:
28
procedure g e n e r a r _ t e x t o ( var l i s t a C : TListaC ;
archivo _salida : S t r i n g ; t a b l a : TLista ) ;
var F : T e x t F i l e ;
aux : pTNodoC ;
s : String ;
r e s : pTNodo ;
s a l i r : boolean ;
begin
A s s i g n F i l e (F , a r c h i v o _ s a l i d a ) ;
Rewrite (F ) ;
s := ' ' ;
s a l i r := f a l s e ;
i f l i s t a C <> n i l then begin
aux := l i s t a C ;
while ( aux <> n i l ) and ( s a l i r = f a l s e ) do begin
s := s + aux ^ . i n f o ;
i f b u s c a r _ c o d i g o ( t a b l a , s , r e s ) = true then begin
i f r e s ^ . i n f o . l e t r a = c h r ( 1 ) then
s a l i r := true
e l s e begin
write (F , r e s ^ . i n f o . l e t r a ) ;
s := ' ' ;
end ;
end ;
aux := aux ^ . s i g ;
end ;
C l o s e F i l e (F ) ;
end ;
end ;
Código 26: Procedimiento para generar el archivo de texto original
Como en todos los subprogramas que trabajan con archivos de texto externos, primero se le asigna a una variable de tipo ‘TextFile’ el archivo donde se
va a escribir el texto original. Luego, se recorre la lista de caracteres generada
al principio (son caracteres pero en realidad sabemos que son códigos Huffman,
es decir, ceros y unos), y se busca a cada código en la tabla.
29
En el string ‘s’ se van acumulando los códigos; dada la longitud de código
variable, no sabemos el largo del código, entonces si no se encuentra el código,
se lo guarda en el string y se le agrega el siguiente cero o uno. Cuando se
encuentra el código en la tabla, se vacía el string y en el archivo de texto se
escribe el caracter correspondiente.
Además, es importane destacar que las condiciones de corte del bucle incluyen que se encuentre el caracter ‘chr(1)’, que sería el código de terminación no
imprimible que ingresamos en la cadena de texto en el codificador. Más allá de
esto, el código no muestra mucho más que el manejo típico de listas enlazadas.
3.6.
Programa principal
type
TByte = array [ 1 . . 8 ] of char ;
TTabla = record
l e t r a : char ;
codigo : String [ 2 0 ] ;
end ;
Código 27: Definición de tipos para el decodificador
Se declara el tipo de variable ‘TByte’ como un array de 8 caracteres. Esto se
hace para tener un manejo más cómodo de los bytes cuando se los transforma
del binario original a la lista enlazada. Por otro lado, vemos la definición del tipo
‘TTabla’, de manera exactamente igual a la del codificador. Esto es necesario
para poder leer correctamente el binario; cuando leemos un archivo binario,
debemos saber el formato en el que se escribió. De manera contraria, es casi
imposible decodificarlo.
De manera similar al codificador, todos los eventos son consecuencia del
accionar de los botones, pero hay un procedimiento principal. En este caso, lo
que hace es recibir como parámetros el archivo binario a descomprimir, la tabla
de descompresión y la ruta al archivo de salida.
Es importante observar primero el siguiente procedimiento, que se llamará
luego desde el programa principal:
30
procedure c r e a r L i s t a T a b l a ( var t a b l a : T L i s t a ;
archivo_tabla : String ) ;
var elem : TTabla ; F : f i l e of TTabla ; i n s e r t a r : TInfo ;
begin
A s s i g n F i l e (F , a r c h i v o _ t a b l a ) ;
Reset (F ) ;
while not eoF (F) do begin
read (F , elem ) ;
i n s e r t a r . l e t r a := elem . l e t r a ;
i n s e r t a r . codigo_H := elem . c o d i g o ;
anadir_final ( tabla , i n s e r t a r ) ;
end ;
end ;
Código 28: Creación de la lista enlazada de la tabla
Vemos que este procedimiento lee el archivo binario que contiene la tabla
con el formato correspondiente (cada dato escrito es de tipo ‘TTabla’). Cuando
lee cada uno de los datos, se almacena en ‘elem’. De esta manera, en la lista
enlazada siempre va añadiendo al final el elemento leído y va avanzando hasta
que recorre todo el archivo. Como la tabla ya fue ordenada en el codificador
de menor longitud de código a mayor, al leerla queda de la misma manera, de
forma óptima para recorrerla (más eficiente).
Ahora, a continuación ya podemos analizar el subprograma principal:
31
procedure d e c o d i f i c a r ( archivo_comprimido : S t r i n g ;
archivo_tabla : String ; archivo_salida : String ) ;
var t a b l a : T L i s t a ;
l i s t a C : TListaC ;
F1 : F i l e of Byte ;
c : char ;
s : TByte ;
r e s : pTNodo ;
b : byte ;
i : integer ;
elem : TInfo ;
begin
crearLista ( tabla ) ;
crearListaTabla ( tabla , archivo_tabla ) ;
crearListaC ( listaC ) ;
A s s i g n F i l e ( F1 , archivo_comprimido ) ;
Reset ( F1 ) ;
while not Eof ( F1 ) do begin
read ( F1 , b ) ;
s := d e c o d i f i c a d o r _ b ( b ) ;
f o r i := 1 to 8 do
anadir_finalC ( listaC , s [ i ] ) ;
end ;
c l o s e F i l e ( F1 ) ;
generar_texto ( listaC , archivo_salida , tabla ) ;
end ;
Código 29: Procedimiento principal del programa
Primero, crea dos listas; una lista enlazada normal y la otra de caracteres.
Luego, decodifica la tabla binaria y genera la lista enlazada correspondiente.
Recorre el archivo binario, decodifica cada byte en un array de 8 caracteres, e
ingresa en un bucle, que simplemente carga cada uno de los caracteres en la
lista de caracteres. Finalmente, llama al procedimiento ‘generar_texto’ con la
lista de caracteres, el archivo de salida y la tabla.
3.7.
Vulnerabilidades y limitaciones
Esta implementación del algoritmo de Huffman para comprimir archivos es
mucho más amigable con el usuario, y resuelve varias cuestiones para que sea
confiable y práctica. Aún así, tiene ciertas limitaciones.
La primera, radica en la longitud de la variable de tipo ‘String’ en el tipo
‘TTabla’. Se eligió una longitud de 20 ya que se probaron archivos de texto de
hasta 400 líneas y ninguno de los códigos superaba la longitud de 15 caracteres.
32
De todos modos, esta longitud es una clara limitación, y podría encontrarse
una manera de buscar el tamaño óptimo de manera dinámica. Si acortamos la
longitud de esta cadena, el archivo binario de la tabla ocupará menos espacio en
disco, pero debemos asegurarnos de que ninguno de los códigos generados supere
tal longitud. Si, por el contrario, agrandamos el tamaño del string, el archivo
de la tabla ocupará demasiado espacio y no tendrá sentido la compresión del
archivo.
También sería útil poder eliminar completamente el uso de cadenas de texto
de esta forma. Esto es para eliminar cualquier limitante a la hora de decodificar
archivos de gran tamaño. No se encontró una manera de hacer esto, ni tampoco de decodificar el texto sin la tabla. Cabe destacar, que esto último es una
limitación propia del algoritmo de Huffman.
Más allá de esta principal complicación, el manejo de la memoria es bastante
bueno; se trató de trabajar lo más posible con memoria dinámica y punteros
para no recurrir a variables estáticas. También se mejoró mucho en este aspecto
a comparación de la versión de consola, y la mayoría de las veces la codificación
es útil, ya que la suma del peso en bytes de la tabla con el binario codificado es
menor al del archivo de texto original.
Hay varios detalles que podrían mejorarse más allá del manejo de la memoria. Uno, por ejemplo, es la definición de la imagen que se muestra en el
codificador. Las propiedades de un ‘TImage’ en Delphi tienen sus limitaciones y
complicaciones para hacer esto. De todos modos, la imagen .png se genera igual
y se puede abrir luego con otro visor de imágenes.
4.
Conclusiones
Se concluye entonces que el algoritmo de Huffman es útil para comprimir
archivos de distinto tipo, aunque en este caso se trabajó solamente con archivos
de texto. Existen varias adaptaciones para trabajar con imágenes y otros tipos
de archivos.
Se pudieron alcanzar los objetivos principales e incluso se superaron en algunos casos, como por ejemplo con el añadido de ‘Graphviz’ para observar claramente la generación del árbol, que era un objetivo secundario.
Además, se repasaron conceptos vistos en la cursada y se comprendieron a
un mayor nivel. No se había trabajado aún con árboles binarios, sólo se habían
presentado en teoría, y el manejo de punteros y memoria dinámica en este
caso fue prácticamente constante. También se destaca el trabajo sobre archivos
externos, concepto que tampoco se había logrado desarrollar a este nivel en la
cursada.
Con este desarrollo finalizado, se presentan a continuación algunas pruebas
con la última versión implementada.
33
5.
Ensayos sobre la aplicación final
Para este ejemplo vamos a comprimir un TDA completo, de unas 400 líneas.
Figura 7: Archivo de texto a comprimir
Figura 8: Interfaz del codificador
Se presiona el botón para abrir el archivo de texto, que abre una ventana
que permite realizar esta tarea con el explorador de archivos de Windows.
Una vez elegido el archivo deseado, se presiona el siguiente botón que ahora
se encuentra habilitado, que abre esta ventana:
34
Figura 9: Ventana para elegir directorio de salida
En la carpeta que se elija se encontrarán todos los archivos necesarios; el
comprimido, la tabla y la imagen del árbol, así como el archivo de texto para
Graphviz.
35
Figura 10: Archivos generados en el directorio elegido
Al presionar el último botón, se muestra el árbol en el mismo formulario:
Figura 11: Árbol mostrado en el formulario
Dado el gran tamaño del árbol y la poca definición, no se puede apreciar la
imagen como se desearía.
Ahora, observaremos el tamaño de cada uno de los archivos:
36
Figura 12: Tamaño del archivo original a comprimir
Figura 13: Tamaño del archivo binario
37
Figura 14: Tamaño del archivo binario de la tabla
Vemos que la suma del archivo binario comprimido con la tabla es de 5776
bytes, cuando el archivo original tenía un tamaño de 7320 bytes. En este caso
se comprimió el archivo en un 22 % aproximadamente.
Ahora, decodificaremos el archivo. Ejecutamos el decodificador, que muestra
la siguiente interfaz:
38
Figura 15: Interfaz del decodificador
Una vez seleccionados los archivos de la tabla y del texto original, se habilita
el tercer botón, que nos permite elegir el directorio de destino y el nombre
del archivo. En este caso elegimos el mismo directorio donde estaba el archivo
original para compararlos:
39
Figura 16: Propiedades del archivo original (‘Prueba’) y el decodificado
Figura 17: Comparación de contenido de los archivos
Vemos que son exactamente iguales en contenido, y si recorremos las 400
líneas, serán todas iguales. Entonces, el compresor-descompresor Huffman funciona correctamente y de manera eficiente en este caso.
Referencias
[1] Código Huffman, Gaston Notte https://www.youtube.com/watch?v=
gYqwGJmyVjg
40
[2] Drawing graphs with dot, Emden R. Gansner and Eleftherios Koutsofios and
Stephen North https://www.graphviz.org/pdf/dotguide.pdf
[3] File Handling In Pascal , Pascal Wiki https://wiki.freepascal.
org/File_Handling_In_Pascal
[4] Programación con el lenguaje Pascal, F.J. Sanchis Llorca, A. Morales Lozano
[5] ShellExecute
in
Delphi
,
Jitendra
Kumar
delphiprogrammingdiary.blogspot.com/2014/07/
shellexecute-in-delphi.html
41
http://
Descargar