Estructuras de Datos: Cola de prioridades

Una Cola  -en el contexto de las Ciencias de la Computación- es una estructura de datos FIFO. Esto significa que los primeros elementos que fueron ingresados en la cola serán primeros en ser removidos. Además, los nuevos elementos a ingresar a lo cola serán colocados en la parte posterior de la misma, como los clientes en las colas de los Bancos.

Eso lo sé desde la Universidad.

Sin embargo, en Data Structures and Algorithms in Java  descubrí un tipo de Cola llamada Colas de Prioridades; donde los elementos la posición de los elementos en la Cola está en función a un criterio predefinido; como en las colas de las discotecas en las que las jovencitas agraciadas siempre ingresan primero, sin importar la hora en la que hallan llegado.

Les presento el código que incluye el libro, para una Cola de Prioridades donde se procura que los elementos de menor valor numérico -nuestro criterio-sean removidos primero de la cola:

 package cgc.learning;  
 public class PriorityQ {  
      private int maxSize;  
      private long[] queArray;  
      private int nItems;  
      public PriorityQ(int s) {  
           maxSize = s;  
           queArray = new long[maxSize];  
           nItems = 0;  
      }  
      public void insert(long item) {  
           int j;  
           if (nItems == 0) {  
                queArray[nItems++] = item;  
           } else {  
                for (j = nItems - 1; j >= 0; j--) {  
                     if (item > queArray[j]) {  
                          queArray[j + 1] = queArray[j];  
                     } else {  
                          break;  
                     }  
                }  
                queArray[j + 1] = item;  
                nItems++;  
           }  
      }  
      public long remove() {  
           return queArray[--nItems];  
      }  
      public long peekMin() {  
           return queArray[nItems - 1];  
      }  
      public boolean isEmpty() {  
           return (nItems == 0);  
      }  
      public boolean isFull() {  
           return (nItems == maxSize);  
      }  
 }  

Nuestra Cola de prioridades está implementada en la clase PriorityQ, que para inicializarse requiere como parámetro el máximo número de elementos que puede soportar la cola. Usaremos este número para instanciar el arreglo de longs que contendrá la data.

Iremos agregando data en el arreglo mediante el método insert, que es también el encargado de que los elementos sean insertados en orden. Se debe procurar que al registrar elementos en el arreglo, los elementos mayores se encuentren a la izquierda -de modo que el mayor elemento insertado tenga índice 0 en el arreglo- mientras que los menores sean almacenados a la derecha, lo que implica que el menor elemento insertado se encuentre en la posición nItems -1 del arreglo. En la siguiente imagen:

La flecha Rear apunta hacia el elemento de mayor valor, que tiene el índice 0 en el arreglo; mientras que la flecha Front apunta hacia el elemento de menor valor con índice nItems -1 en el arreglo de PriorityQ. Al ingresar un elemento mediante el método insert, debemos procurar que este criterio se mantenga; por lo que realizamos lo siguiente:

  • En caso el arreglo no tenga elementos, el nuevo elemento ingresado ocupará la posición 0 del arreglo. Al realizar un registro, siempre se incrementa el valor de  nItems de modo que este atributo siempre contenga el número de elementos de la Cola.
  • En caso el arreglo no esté vacío, compararemos el elemento a ingresar con cada uno de los elementos del arreglo empezando por el menor-el de índice  (nItems  -1 )- desplazando los elementos existentes hacia la derecha hasta encontrar el lugar apropiado para el nuevo elemento de modo que el arreglo se mantenga ordenado.

Con esa lógica de inserción aseguramos que al invocar a remove siempre obtendremos el menor elemento de la lista, que es el primero de la Cola. Dado que realizamos comparaciones a lo largo del arreglo al momento de insertar, el tiempo de inserción es directamente proporcional al número de elementos de la Cola; sin embargo, el método remove tiene un tiempo de ejecución constante y no depende del número de elementos de la cola.

Para finalizar, las colas de prioridades se aplican -por ejemplo- en los sistemas operativos multi-tarea. Los procesos se colocan en una Cola de Prioridades y es un función a la prioridad/importancia relativa que les da el sistema operativo que pueden acceder a recursos como el CPU.

Código fuente e imágenes tomadas de Data Structures and Algorithms in Java.



Algoritmos: Ordenamiento por inserción

Si hay algún algoritmo que recuerdo de mi época universitaria es el Ordenamiento de Burbuja: lo recuerdo porque en todos los exámenes te solicitaban ordenar y era el algoritmo más fácil de memorizar. Ayer encontré a mi querido algoritmo de la Burbuja en el capítulo de ordenamiento simple de Data Structures and Algorithms in Java, dedicado a los algoritmos más sencillos - y lentos - de ordenamiento. De los tres algoritmos del capítulo, el de la Burbuja es el más lento e ineficiente de todos; por lo que a pesar de mi nostalgia decidí dedicarle el post al Ordenamiento por Inserción: el más rápido de todos los algoritmos fáciles.

La idea del algoritmo es que a través de las iteraciones construyamos dos partes diferenciadas con los elementos que tenemos que ordenar: una parte ordenada y una parte desordenada. Conforme pasan las iteraciones, iremos agregando elementos a la parte ordenada con elementos de la parte desordenada, hasta que la parte desordenada se quede sin elementos y nuestra parte ordenada sea el resultado del algoritmo. Imaginemos que estamos aplicando el Ordenamiento por Inserción para alinear un filar de personas por orden de talla. En una iteración intermedia, tendríamos este escenario:

El algoritmo ya ha hecho su trabajo y nos ha dividido al grupo de personas en dos partes: una ordenada y una que tenemos que ordenar. Lo que se hace en cada iteración es tomar al primer elemento de la izquierda de la parte desordenada (en el gráfico "Marked player") para  colocarlo en lista ordenada en el lugar que le corresponda. Entonces, sacamos a "Marked player" de la fila para ver donde lo colocamos.


Hemos retirado a "Marked player" y esto ha dejado un  espacio libre en la lista de personas. Lo que vamos a hacer ahora es desplazar a las personas de la lista ordenada hacia la derecha (aprovechando el nuevo espacio libre) hasta que encontremos el lugar que le corresponde a "Marked player". Después de mover a la derecha a tres grandulones, encontramos el espacio que le corresponde a "Marked player". Lo  colocamos en su sitio y nuestra fila india quedaría así:

Ahora la parte de la izquierda-que estaba ordenada- ha crecido en una persona y la parte desordenada de la derecha es una persona menor. Es momento de escoger un nuevo "Marked player" tomando a la primera persona de la izquierda de la lista desordenada. Repetimos el proceso tres veces más y hemos terminado.

Si en vez de un grupo de personas utilizamos un arreglo de enteros, el programa Java para ordenarlos mediante ordenamiento por inserción sería más o menos así:

 public void insertionSort() {  
           int in, out;  
           for (out = 1; out < nElems; out++) {  
                long temp = a[out];  
                in = out;  
                while (in > 0 && a[in - 1] >= temp) {  
                     a[in] = a[in - 1];  
                     --in;  
                }  
                a[in] = temp;  
           }  
      }  

Cuyo proceso es más o menos el que sigue:

  1. La variable nElems contiene la longitud del arreglo a[] que vamos a ordenar. La variable out la utilizamos para marcar el inicio de la porción no ordenada del arreglo. Nuestra parte ordenada al incio del algoritmo será el primer elemento de a[] (o sea a[0]) y la parte desordenada comenzará en a[1]; es por eso que la variable out se inicializa en 1: esta variable nos marcará siempre el inicio de la parte desordenada del arreglo.
  2. Colocamos el valor de a[out] (nuestro "Marked player") en la variable temp, dado que al desplazar los elementos de la parte ordenada hacia la derecha podríamos perder su valor.
  3. Inicializamos la variable in con el valor de out, y utilizaremos esta variable para desplazarnos a lo largo de la parte ordenada con el fin de encontrar el lugar que le corresponde a a[out] en la parte ordenada del arreglo. Para esto, comparamos si a[i-1] es mayor que el valor de a[out] almacenado en temp. Si es así, desplazamos a[i-1] a la derecha, y disminuimos i en una unidad para seguir recorriendo la parte ordenada del arreglo.
  4. Cuando a[i-1] sea menor que temp -que contiene a[out]- es que hemos encontrado al fin el lugar que le correspode a nuestro "Marked Player". El valor de a[out] ahora estará almacenado en a[i], y estamos listos para tomar el siguiente elemento de la parte desordenada del arreglo.
Como en el post anterior, nos toca determinar cúantos pasos le toma a nuestro algoritmo ordenar un arreglo arbitrario de longitud N, en el peor escenario posible (la gente de algoritmos siempre es pesimista). En la primera iteración, la parte ordenada tiene sólo un elemento, así que a lo más realizaríamos una comparación al ordenar. En la segunda iteración, como ya tendríamos dos elementos en la parte ordenada el algoritmo realizaría como máximo dos comparaciones; y en la última iteración, sólo nos quedaría realizar N-1 comparaciones en el peor escenario posible. Si sumamos todas las comparaciones, tendríamos:

1 + 2 + 3 + ... + N - 1 = N*(N-1)/2  

*De acuerdo a la fórmula que enseñan en secundaria

El tiempo de ejecución del algoritmo es proporcional al número de pasos; por lo que el tiempo de ejecución del algoritmo de ordenamiento por inserción para un arreglo de longitud N es proporcional a N al cuadrado.

Código fuente e imágenes tomadas de Data Structures and Algorithms in Java.


Orgullo de artesano (de Software)


"The whole problem with modern times is that there's no pride in craftsmanship. (...) Everyone's a slave of efficiency! No time for aesthetics! No love of things for their own sake!" -  Calvin & Hobbes de Watterson.

La frase que abre el post la encontré hoy en un tira de Calvin & Hobbes, y podría ser traducida como "El problema de estos tiempos es que se ha perdido el orgullo del artesano. ¡ Todos son esclavos de la eficiencia! ¡Ya no hay tiempo para la estética! No hay amor  para las cosas en sí mismas". Mientras todos los niños hacen bolas de nieve acumulándola entre las manos, Calvin se asegura que las suyas tengan la cantidad óptima de humedad y un diseño aerodinámico.

¿No creen que la frase de Calvin aplica también al mundo del desarrollo de software? Al Jefe de proyecto sólo le importa que el software esté listo a tiempo, al usuario sólo le preocupa que el software funcione y a los ingenieros sólo les interesa cobrar puntualmente. Eficiencia y productividad son los valores máximos de la industria de software hoy.

Creo que el software y su código fuente tienen un componente estético. Sí, yo creo que los programas pueden ser "bonitos".  Revisar código fuente ordenado, indentado, con nombres de variables y métodos adecuados puede llegar a emocionarme tanto como un buen cuento o una canción. Los programas con tipos desacoplados, capas definidas, estructuras de herencia elegantes y aplicación inteligente de patrones de diseño a  mi parecer pueden llegar al estatus de obras de arte.

Pero parece que la mayoría de ingenieros han perdido esos valores. Simplemente se limitan a digitar el código que haga que el software cumpla con los requerimientos del usuario. Si el código fuente es un caos lleno de nombres de variables initeligibles, clases de cientos de líneas de código y lógica de negocio dispersa pues es una lástima, pero no es su problema. Ya los ingenieros de mantenimiento arreglarán eso después del pase a producción.

Cuando codifico, trato que lo que estoy viendo en el monitor sea agradable para mi y para el próximo ingeniero que lo tendrá cargo. Trato siempre de aplicar las pocas técnicas que conozco para que el programa que estoy construyendo sea funcional, eficiente y , si se puede, elegante. Puede llevarme un poco más de tiempo y esfuerzo, pero creo que como profesional le debo calidad a la empresa que me ha contratado y a mis colegas que tendrán que lidiar con lo que he hecho.

Hace poco me enteré del movimiento de Artesanía de Software, que propone entre otras cosas considerar al proceso de desarrollo de software como un arte en sí mismo. El movimiento tiene un manifiesto con unos valores que a mi parecer todos los programadores deberíamos adoptar:


  • No sólo hacemos software que funciona, sino software bien hecho
  • No sólo respondemos al cambio, sino agregamos valor constantemente.
  • No sólo somos individuos e interactuamos, sino somos una comunidad de profesionales.
  • No sólo colaboramos con nuestros clientes, sino establecemos una sociedad productiva con ellos.
*Traducción libre

En la historieta, las bolas de nieve aerodinámicas de Calvin toman demasiado tiempo en hacerse. Para cuando tiene una lista, Susy ya lo ha acribillado con decenes de bolas de nieve convencionales. Mientras Calvin yace acribillado sobre la nieve, Hobbes sólo atina a decir:

"Artists always suffer" (Los artistas siempre sufren)

Algoritmos: Búsqueda dicotómica

Stoimen's Web Log desde hace un tiempo publica cada semana un post sobre Algoritmos. Habiendo llevado el curso de Algoritmos y Estructuras de Datos en la universidad -y habiéndolo aprobado con una nota decorosa- pensé en seguir los posts como una manera de refrescar mis algo oxidados conocimientos de algoritmia.


Ya van cinco algoritmos publicados y no he visto ninguno en la Universidad, así que parece que el profesor nos engañó al hacernos creer que sabíamos algoritmos. Me he propuesto remediar ese error, y he comenzado a leer Data Structures and Algorithms in Java para aprender lo que debí haber aprendido en la universidad. Esta serie de posts son una recopilación de conceptos producto de mi lectura.


Dicho esto, comenzamos:

¿Cómo localizarías un elemento en un arreglo ordenado? Una aproximación ingenua sería recorrer el arreglo desde la posición inicial (En Java, el índice 0) e ir recorriendo el arreglo elemento por elemento hasta localizar el elemento que estábamos buscando. Si asumimos el peor escenario posible -es decir, el elemento que buscábamos se encuentra al final del arreglo- este tipo de búsqueda requeriría N "pasos" par un arreglo de longitud N.

El enfoque anterior podría funcionar también para un arreglo desordenado, pero tratándose de un arreglo ordenado es ineficiente dado que no aprovechamos el hecho de tener el arreglo ordenado para reducir el número de pasos que le toma al algoritmo encontrar el elemento buscado. La búsqueda dicotómica nos permite aprovechar este atributo de nuestro arreglo para que -en el peor de los casos- la búsqueda tome sólamente lb (N) pasos (esto es el logaritmo en base 2 de N). Pongamos el algoritmo a prueba buscando un número X en un arreglo A de longitud 100 (N=100):

  1. Tomemos el elemento que se encuentra exactamente al medio del arreglo (A[50]) y verificamos si es el elemento que buscábamos. 
  2. Imaginemos que no lo es y que X es mayor que A[50]. Cómo se trata de un arreglo ordenado, ahora podemos afirmar con seguridad que X no se encuentre en A[49] ni en ninguno de los elementos que le preceden, por lo que sólo deberíamos buscar en los elementos entre A[51] y A[100]
  3. Tenemos ahora un nuevo arreglo en el que buscar, y según nuestro algoritmo debemos verificar si el elemento ubicado en la mitad del arreglo -o sea A[75] -es el elemento que buscábamos.
  4. Volvamos a pretender que no lo es,  pero que ahora X es menor que A[75]. Como este arreglo también se encuentra ordenado, concluimos que todos los elementos desde A[76] hasta A[100] son mayores que X, por lo que sólo deberíamos buscar entre A[51] hasta A[74]
  5. Y así hasta que encontremos el número que buscábamos. 

Si tanta palabrería la pasamos a Java, obtenemos algo como esto:

      public int find(long searchKey) {  
           int lowerBound = 0;  
           int upperBound = nElems - 1;  
           int curIn;  
           while (true) {  
                curIn = (lowerBound + upperBound) / 2;  
                if (a[curIn] == searchKey) {  
                     return curIn;  
                } else if (lowerBound > upperBound) {  
                     return nElems;  
                } else {  
                     if (a[curIn] < searchKey) {  
                          lowerBound = curIn + 1;  
                     } else {  
                          upperBound = curIn - 1;  
                     }  
                }  
           }  
      }  
Ese método pertenece a una clase donde a[] es un atributo que representa el arreglo ordenado sobre el que buscamos y nElems es un atributo que contiene la longitud del arreglo. El método devuelve la posición en a[] en la que se encuentra searchKey, y en caso no la encuentre devuelve nElems que es la longitud del arreglo. La variable curIn representa el índice del elemento que se encuentra en el medio del arreglo que estamos evaluando, siendo lowerBound el índice del menor elemento del arreglo y upperBound  el índice del mayor elemento del arreglo. En cada iteración del bucle while se compara si a[curIn] es mayor o menor que el elemento buscado searchKey y en base al resultado de esta comparación se recalculan los valores de  upperBound  y lowerBound. Con dibujitos, sería algo así:

El número de "pasos" que necesasitamos para encontrar un elemento en un arreglo de longitud N viene a estar dado por la cantidad de veces que podemos dividir N sobre 2 (antes que el producto de esta división sea menor que 1). Este número en matemáticas es igual a lb(N), que es considerablemente menor que N de nuestro enfoque ingenuo del primer párrafo: si buscamos secuencialmente un arreglo de 100 elementos nos tomaría 100 pasos, mientras que si usamos la búsqueda dicotómica nos tomaría sólo 7 (aproximadamente el valor de lb(100)).

Código fuente e imágenes tomadas de Data Structures and Algorithms in Java.

Arquitectura Java EE y Spring Framework

Hace unos meses -en mi anterior trabajo- realizé una presentación al equipo sobre fundamentos de arquitectura y su relación con Spring Framework. La presentación es fundamentalmente teórica así que van a encontrar poco código;  y consideré compartirla dado que estamos en campaña de resucitar el blog xD.

Saludos, y hasta otra!

GWT para novatos

GWT - Una introducción
View more presentations from Carlos Gavidia.

Me encargaron en el trabajo realizar una charla introductoria sobre GWT para el equipo. Después de leer durante algunos días -dado que soy un novato en la materia- preparé la presentación que adorna este post. La pongo a disposición de los interesados.

Saludos, y hasta otra!