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 [email protected] 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://