Showing posts with label estructuras de datos. Show all posts
Showing posts with label estructuras de datos. Show all posts

Tuesday, May 29, 2018

El lenguaje parece que tiene estructura de árbol



Hace tiempo pensé que el lenguaje podría definirse como una lista simplemente ligada (http://la-morsa.blogspot.com/2013/08/el-lenguaje-es-una-lista-ligada.html). La idea era simple. Puedo escribir, por ejemplo: "la pelota es azul", pero no puedo poner: "azul es pelota la", es decir, hay un orden (las reglas gramaticales), que permiten saber qué va primero y qué va después. Eso hace que podamos entendernos. Los niños, cuando van empezando a hablar, cometen errores en su manera de decir las cosas porque simplemente desconocen que hay ciertas reglas o bien, que existen verbos irregulares cuya conjugación cambia. Es pues común corregir estos errores en los infantes.

Pero el otro día escribía un mensaje desde mi teléfono celular, y encontré que el sistema hace algo muy interesante: cuando escribo una palabra, por ejemplo "buenas", el sistema del teléfono me pone las siguientes palabras: "noches", "tardes", noticias", "madrugadas", "vibras", "intenciones", entre otras. Algunas de ellas no hacen sentido, por lo que las he omitido, pero si nos fijamos, el programa de predicción de texto busca finalmente las palabras que deben seguir a la palabra "buenas".

No sé de dónde habrá salido esta idea, pero es interesante. Vamos, no escribo: "buenas tránsito", ¿verdad? Y esto hace pensar que de nuevo, como en el artículo de este mismo blog que ya mencioné, el cerebro va armando un lenguaje con una estructura que s de un árbol de múltiples ramas y nodos, es decir, no es tan simple como un árbol binario, pero que no es tampoco imposible de crear con algún lenguaje de programación. De hecho, el sistema que tengo en mi teléfono hace precisamente esto.

Me parece que la idea es buena y voy a ver si puedo armar un subconjunto usando un diccionario que tengo de palabras, que viene de un proyecto en Linux. Si tengo algo pronto, lo presentaré en este mismo puedo armar un subconjunto usando un diccionario que tengo de palabras, que viene de un proyecto en Linux. Si tengo algo pronto, lo presentaré en este mismo

Sunday, July 27, 2014

¿Cuáles son las palabras diferentes en el Quijote?


Hace unos días se convocó al reto de hallar cuántas palabras diferentes tenía el Quijote, la obra de Cervantes, además de hallar la frecuencia de cada una de estas palabras. El reto, para un programador novicio puede parecer muy complicado, porque además se pide realizarlo en el menor tiempo posible. ¿Cómo se programa pues algo así?

Aquí la problemática se centra en la estructura de datos a usar. Por ejemplo, imaginemos que queremos usar un arreglo de palabras para irlas acomodando en la medida que las voy leyendo. El primero problema con esto es que no sabemos el total de palabras que hay por lo cual ¿De qué tamaño voy a hacer mi arreglo? Bueno, si el Quijote (como esá en el Proyecto Gutenberg) tiene unas 2 millones de letras, es razonable pensar que un 20% máximo serán palabras, más o menos 400 mil palabras.

Podemos crear un arreglo de 400,000 palabras y esperemos que esto sea suficiente, aunque en principio, sino sabemos cuantas palabras hay, estamos "adivinando" considerando que el promedio de letras de las palabras del español es menos de 10 letras por palabra. Pero esto último es una grosera aproximación a la cual estamos obligados si queremos usar la estructura de datos llamada arreglo.

Pero hay otra opción: usar estructuras de datos dinámicas. En este tipo de estructuras, lo que intentamos hacer es ir creando, en la medida de las necesidades, las estructuras necesarias, las cuales pueden crecer en la medida que lo vamos requiriendo. Dicho de otra manera, si necesitamos una nueva variable para alojar un dato, creamos esa variable en tiempo de ejecución, en el momento, de forma dinámica pues.

Una estructura de datos dinámica, muy poderosa, es la de los áerboles binarios. En una estructura de esta naturaleza tenemos un nodo y dos ramas. En el nodo tenemos la información y en las ramas tenemos apuntadores a otros nodos o bien a NIL, el vacío (es una manera en cómputo de no apuntar hacia la nada).

La técnica para ir creando un árbol binario es así: Consideremos que tenemos una lista de números desordenada: 60, 41, 74, 16, 53, 65, 25, 46, 55, 63, 70, etcétera. Tomemos el valor y pongámoslo en el nodo raíz. Ahora tomemos el siguiente valor, en este caso el 41. Si el valor es menor al que está en el nodo raíz, entonces lo colocamos en la rama izquierda, si es mayor, lo colocamos en la rama derecha., Así, ponemos el 41 en la rama izquierda. Acto seguido tenemos el 74. ¿Dónde lo pondremos? En la rama derecha del nodo raíz. Muy bien hasta aquí, pero de pronto tenemos un número más, el 16. ¿Dónde habremos de ponerlo? Tomamos el nodo raíz y vemos que es 60, así que debe ir en la rama izquierda, pero ésta ya tiene el nodo 41. De nuevo entonces vemos que el 16 es menor que el 41 y lo colocamos en la rama izquierda de éste. Con este procedimiento podemos llegar a llenar el árbol binario (porque solamente tiene dos ramas en cada nodo).



Nótese que un árbol de esta naturaleza además nos da una manera intrínseca de ordenar información. Si queremos desplegar los números que están en este árbol de manera que queden ordenados de menor a mayor, lo que hacemos es muy simple: Partimos del nodo raíz y nos movemos hasta la última rama izquierda posible. Esto nos dará, de acuerdo a la imagen que hemos puesto, en el número 16. Entonces vemos si este nodo tiene rama derecha. Sí, la tiene, y es el 25. Entonces ponemos como primer nímero el 16 y después el 25. Nos hacemos un nodo atrás y hallamos el 41. Lo ponemos. vemos si tiene rama derecha. Sí, la tiene, es el 53, pero éste tiene una rama izquierda, que es el 46 y este nodo a su vez tiene la rama izquierda cone l valor 42. Entonces ponemos el 42, 46, 53 y ahora revisamos la rama derecha y hallamos que hay un 55. Por lo tanto la lista va quedando así: 16, 25, 41, 42, 46, 53, 55... Y ahora ya no hay más ramas izquierdas que revisar. Pasamos a la rama derecha del nodo raíz y hallamos que hay un 74. Pero éste tiene un nodo izquierdo, el 65, que a su vez tiene otro, el 63, que a su vez tiene otro más, el 62, por lo que pondremos, 62, 63 y ahora vemos si hay nodo derecho. Y sí, lo hay, el 64. Ponemos ahora el 65 y como tiene rama derecha, ponemos el 70 y finalmente el 74.

Si queremos ordenar el árbol de los números mayores a los menores, lo que tenemos que hacer es recorrer el árbol al revés, primero empezar por la rama derecha. Esto es increíble. La misma estructura nos permite ya dejar ordenados literalmente los datos sin necesidad de otras estructuras.

Pues bien, una vez que tenemos esto aclarado, podemos crear un árbol binario con las palabras del Quijote. Si es una nueva palabra, creamos el primer nodo. Si es una palabra que es menor que la palabra que está en el nodo raíz, la ponemos en un nuevo nodo, que cuelga de la rama de la izquierda, en caso contrario, de la rama derecha.

Pero ¿cómo puedo crear una estuctura de un árbol binario? La manera más simple es crear una estructura dinámica, que crece en la medida que lo necesitamos. Por ejemplo, en Delphi, (Pascal) primero definiremos la estructura arbórea:


 nodoptr = ^nodo;  //el apuntador del nodo apunta a un nodo
       nodo = record  //esta es la definición del nodo
                algúndato: string; //palabra a guardar
                izquierda, derecha: nodoptr;
              end;


 Esta definición siempre parece costar trabajo leerla, y la razón es que es la única vez que Pascal permite usar algo que aún no hemos definido. Así, nodoptr = ^nodo; pero nodo no ha sido aún definido. De hecho, se define inmediatamente después.

Una vez teniendo la estructura dinámica, Delphi (Pascal), nos permite crear una nueva instancia de la misma si la necesitamos.

Por ejemplo, definamos

var
       arbol     : nodoptr;

Esto es precisamente el árbol en donde iremos colocando las palabras que vayamos leyendo del Quijote. Necesitamos para ello, un procedimiento para insertar nuevos nodos y poner los valores en los mismos. Esto lo podemos hacer así:

procedure insertaarbol(astr: string; var tree: nodoptr);
     { este procedimiento crea el árbol binario en memoria, 

        insertando un nodo en la posición correcta }

    begin
      if tree = nil then  //si el árbol esta vacío...
        begin
          new(tree);
          tree^.algundato := astr;
          tree^.left := nil;
          tree^.right := nil;
        end
      else
        begin
          if astr > tree^.algundato then
            insertaarbol(astr, tree^.right);


          if astr < tree^.algundato then
            insertaarbol(astr, tree^.left);
        end;
    end;


Con esto ya tenemos con qué trabajar. Es la rutina imprescindible para ir creando loas nodos (con sus respectivas ramas izquierda y derecha). Ahora hagamos la tarea: leamos el archivo del quijote, que está separado por líneas. Cada línea puede contener un número finito de palabras. He aquí el código (que se echa a andar cuando damos click al botón):

 procedure TForm1.Button1Click(Sender: TObject);
  var
    Linea   : ansistring;
    Palabra : ansistring;
    j       : integer;

   begin
   arbol := nil;
  
    assignfile(afile, Path+'palabras.txt'); 

//donde vamos a guardar las palabras halladas
    rewrite(afile);
    assignfile(infile, Path+'quijote-sin.txt'); 

//que archivo vamos a leer
    reset(infile);
    j := 0;

    repeat
      Readln(infile,Linea); //leo línea del texto
      for i := 1 to WordCnt(Linea) do
      begin
        Palabra := ExtractWords(i,1,Linea);
        Palabra := Lower(Palabra);
        inc(j);
        insertararbol(Palabra, arbol);
      end;
    until eof(infile); //hasta que leamos todo el texto

    writeouttree(tree);  //escribimos a disco el árbol
      //writeln;
      closefile(afile);
      closefile(infile);
      deletetree(tree); //borramos la estructufra binaria
      ShowMessage('Hay' + inttostr(j)+ ' palabras diferentes en el Quijote');
end;



Esto nos indica que el Quijote tiene 384,123 palabras en total (¿diferentes? unas 30 mil). El programa crea un archivo con todas las palabras (una por línea) y las despliega en orden alfabético, de menor a mayor, de la A a la Z, pues.

La rutina que crea este archivo es la siguiente:

procedure writeouttree(tree: nodoptr);
    {Copia a disco el árbol binario ya ordenado }
    begin
      if tree <> nil then
        begin
          writeouttree(tree^.left);
          writeln(afile, tree^.algundato) //escribe en archivo

          writeouttree(tree^.right);
        end;
    end;


Nótese que esta es una rutina recursiva (se llama a sí misma), que permite imprimir todas las palabras diferentes del Quijote en un archivo.

¿Cuánto creen que tarda en esta labor? ¡Alrededor de un segundo!

El reto propuesto es más interesante. No basta con saber cuáles son las palabras diferentes en total que tiene el Quijote, sino también su frencuencia. Aquí necesitamos entonces hacer dos cosas. La primera es que el nodo contenga no sólo la palabra del texto, sino las veces que la vamos encontrando, algo así como una variable que vaya incrementándose cada vez que encontremos dicha palabra. Por ello, requerimos de un procedimiento más: uno que busque un nodo y pueda modificar los valores del mismo.

No lo voy a hacer aquí. Si el amable lector me ha seguido hasta este punto, seguro puede hacer fácilmente esta rutina ¿verdad?

Wednesday, August 07, 2013

El lenguaje es una lista ligada


Una de las estructuras de datos que se aprenden en los cursos correspondientes de programación son las listas ligadas. Para entender de qué se trata esto, imaginemos que tenemos una serie de números que deben enlazarse en un orden unos con otros. Por ejemplo, en la siguiente imagen:



Aquí, por la razón particular que sea, el número 12 se enlaza al 99 éste al 37. El 37 se enlaza a un final que en general se denomina NIL, es decir, a un punto en donde ya no se apunta a ninguna parte.

Las listas ligadas nos permiten generar nuevos elementos sin alterar estos, sino que lo que ahcemos es generar un nuevo apuntador al siguiente número y así sucesivamente. Este es un clásico ejemplo de una estructura dinámica.

Cuando hacemos, por ejemplo, cálculos aritméticos en una computadora, normalmente el tamaño de palabra en números reales, definidos por los lenguajes de programación es suficiente para los usos cotidianos. Sin embargo, cuando queremos hacer, por ejemplo, un programa que permita hacer cálculos con una precisión mucho mayor (a unos 4 mil millones), entonces tenemos un problema, porque el tamaño definido para los números reales no nos da la capacidad adecuada. Es aquí donde se puede generar una lista dinámica de números, de dígitos, para generar un número tan grande como se deseé. Si queremos hacer cálculos sobre ese número, tendremos que finalmente, enseñarle a la computadora a hacer las operaciones de nodo en nodo y llevar los valores que se acarrean cuando estamos, por ejemplo, haciendo una larga suma.

Las listas ligadas (también llamadas, enlazadas), nos permiten de alguna manera trabajar con conjuntos de datos sin necesidad de tener que usar arreglos en una dimensión. De hecho, un problema con los arreglos es -en principio- que son estructuras de datos fijas, que además, no se pueden generar fácilmente en un programa de computadora, pues muchas veces tenemos que decirle a nuestro programa, a priori, el tamaño del arreglo. Dicho de otra manera, son estructuras estáticas y aunque son muy útiles en muchos casos, tienen sus propios inconvenientes.

Existen listas doblemente ligadas, es decir, cada elemento tiene dos ligas, una al siguiente elemento y otra al anterior. Un caso donde se puede usar esta idea es cuando se programa, por ejemplo, un sistema para desplegar partidas de ajedrez. Lo que se hace es leer la partida y guardar cada posición con dos apuntadores, uno a la posición anterior y otro a la posición siguiente. Eso permite entonces hacer un programa que simplemente nos permita movernos de jugada en jugadfa, hacia adelnate o hacia atrás, desplegando el tablero correspondiente.



Una de las ventajas de las listas ligadas es que permite, por ejemplo, eliminar elementos fácilmente, como se ve en la siguiente figura.



Aquí un elemento es dado de baja, lo borramos... ¿cómo? simplemente quitamos sus enlaces a la lista y ya no lo tenemos. Sin embargo, esto tiene un inconveniente. El elemento sigue en memoria, inaccesible, y utiliza memoria que no podemos usar porque no lo hemos literalmente borrado, solamente lo dimos de baja de nuestra lista ligada. Aquí tenemos que lidiar con esta dificultad y desde luego, hay mecanismos, como el del recolectro de basura, pero de eso hablaremos en otro momento.

Lo que he estado pensando es que el idioma, los lenguajes que hablamos los seres humanos son en realidad listas ligadas. Un ejemplo claro es la poesía. Quien se aprende un poema lo que hace es acordarse de qué palabra sigue a la otra. Así, si lo vemos como un problema de cómputo, podremos ver que se trata de una lista simplemente ligada. No lo es doblemente porque quien se aprende una poesía no la puede decir al revés. Solamente se dice de una manera.

Yendo más lejos aún entonces, nuestros cerebros han acomodado las palabras de manera tal que tienen asociado un apuntador *un puntero dirían los españoles) a una siguiente palabra. Podría ser, sin embargo, que cada palabra tuviese apuntadores no a una sola palabra siguiente (como en el caso de la poesía), sino a todo un conjunto de palabras que pueden ser ligadas directamente.

Y si vamos aún más lejos, la ortografía parece ser también un asunto de listas ligadas simples. Es claro que en muchos casos una letra sigue a la otra y sin embargo, hay unas que están vetadas. Por ejemplo:

e - > n - > b - > i - > a - > r

Podría considerarse una lista ligada ilegal o inválida, pues sabemos que después de una "n" nunca va una "b".

Quisiera pensar que el cerebro tiene un mecanismo que permite crear a través de las neuronas estas redes de listas ligadas. La pregunta entonces es: ¿podríamos desencriptar esta maraña de redes de neuronas como listas ligadas y ver cómo el cerebro está organizando la información? Por el momento me parece que estamos lejos de ello, pero bueno, es una reflexión nada más.

Saturday, May 07, 2011

Criptogramas aritméticos y Prolog



Hay una historia, evidentemente falsa, en la cual se dice que un joven estaba en un país extranjero estudiando, pero después de algunas semanas, se halló sin dinero. Contó su remanente y halló que podía mandar un telegrama a su padre, para que éste le enviará más dinero. Sin embargo, su capital sólo alcanzaba para poner tres palabras.

¿Cómo decirle qué cantidad necesitaba? En un acto de ingenio, decidió escribir SEND + MORE = MONEY, asumiendo que su padre entendería que en clave le estaba diciendo qué cantidad necesitaba.

Pues bien, este tipo de criptogramas no son muy complicados de resolver en Prolog, porque en este tipo de lenguajes, en donde se hace hincapié en dos temas, la recursión y el backtrack. Así, la técnica para resolver el criptograma aritmético implica simplemente pasar todos los posibles valores por las variables que se necesitan y ver si se cumplen las condiciones exigidas. A esto se le llama un árbol solución y si dibujáramos todas las posibles alternativas, tendríamos un nodo principal con una serie de ramas, que muchas veces se ramifican generando muchas pequeñas ramas o bien, muchas ramas cortas.




La recursión es otra técnica presente. En este caso, se trata de hacer una llamada de una función a la misma función, en donde nuevos parámetros son involucrados. Pero antes de procesar la llamada, podemos observar si se cumple una condición terminal. Si es así, salimos de la recursión y en caso contrario, entramos en un ciclo más de ella.


La recursión tiene su importancia porque a pesar de que mcuhos lenguajes de cuarta generación, como Pascal y C, lo tienen disponible, es claro que se requiere de más recursos de cómputo, memoria en particular. Para ilustrarlo, pensemos en una oficina en donde existen unos diez teléfonos. Suena el primero y una secretaria lo contesta. Se pone a atender a quien habla cuando suena el segundo teléfono. Entonces la secretaria le dice al interlocutor del teléfono 1: "espere un momento por favor". Entonces la mujer contesta el segundo teléfono. Mientras habla con el interlocutor 2, suena un tercer teléfono. de nuevo, la secretaria le dice al interlocutor 2 que espere. Atiende al tercer teléfono y cuando termina la comunicación con éste tercer interlocutor, regresa al segundo y continúa la conversación en donde se quedó. Cuelga con este segundo interlocutor y retoma la conversación con el primer interlocutor en el punto donde la dejó. ¿Cómo se acuerda en qué punto se quedó en cada llamada? Bien pues la eficiente secretaria lleva un stack interno, lo cual es una estructura en donde se van apilando las conversaciones. Cuando empieza la primera llamada, la secretaria habla, pero entonces, al sonar el segundo teléfono, la secretaria pone en la pila la conversación en el punto que está al contestar el segundo teléfono. Al sonar el tercer teléfono, la secretaria pone encima de la pila, el estado de la segunda llamada. Cuando cuelga el tercer teléfono, va al anterior y ve en qué momento de la conversación se quedó y saca ese dato de la pila. Hace lo mismo al colgar.

El stack/pila, es una estructura FILO - First In, Last Out, (el primero que entra es el último que se va). Solamente para comparar con otras estructuras, ¿qué tipo de estructura es FIFO (First In, First Out)? ¿Qué ejemplo de la vida cotidiana contiene una estructura FIFO? (*)

Pero regresando al punto del criptograma aritmético, he aquí el programa que resuelve el acertijo en Prolog. ¿Podrá escribirse una versión más corta que ésta en cualquier otro lenguaje? Tengo mis dudas.


smm :-
        X = [S,E,N,D,M,O,R,Y],
        Digits = [0,1,2,3,4,5,6,7,8,9],
        assign_digits(X, Digits),
        M > 0,
        S > 0,
                  1000*S + 100*E + 10*N + D +
                  1000*M + 100*O + 10*R + E =:=
        10000*M + 1000*O + 100*N + 10*E + Y,
        write(X).

select(X, [X|R], R).
select(X, [Y|Xs], [Y|Ys]):- select(X, Xs, Ys).

assign_digits([], _List).
assign_digits([D|Ds], List):-
        select(D, List, NewList),
        assign_digits(Ds, NewList).


No es de sorprenderse que el programa no sea una virtud de eficiencia. De hecho, requiere revisar 10!/2 posibilidades para ver en cuáles se cumplen las condiciones del problema. No obstante esto, hay que aclarar que en Prolog jamás se habla de eficiencia, o de uso de recursos en forma mínima.

A todo esto, una posible solución al criptograma es:


    [9,5,6,7]       SEND
    [1,0,8,5]     + MORE
                 ---------
  [1,0,6,5,2]      MONEY


________
(*) La cola de un banco es FIFO. La gente llega, se forma, un cajero atiende a un cliente. Hasta que no termina sus operaciones ese cliente, los demás deben esperar en la cola.