viernes, 11 de noviembre de 2016

Programación del Sinclair QL (VIII): Ordenación por intercambio iterativo, Burbuja



Ordenación por burbuja

Este sistema, denominado en inglés Bubblesort, es el sistema mas sencillo de entender aunque también es el algoritmo mas lento e ineficiente de todos, por lo que en muchos libros se recomienda ni siquiera enseñarlo en los cursos de programación. A pesar de esto comienzo por el que todos hemos empezado, aunque recomiendo usar Inserción como algoritmo iterativo y Quicksort como recursivo que son mucho mas eficientes.

La idea del algoritmo es sencilla, se recorre la lista comparando cada elemento con el siguiente, si este es menor se intercambian ambos elementos. Este proceso se repite hasta que la lista esté ordenada, con cada pasada los elementos mayores se hunden en la lista rápidamente, moviendo hacia arriba los elementos ligeros mas lentamente, como burbujas en un líquido, de ahi su nombre. Vamos con el algoritmo en seudo-código que orden por burbuja una lista, indicando el primer y ultimo elemento de la misma que deseamos ordenar

ALGORITMO Burbuja (lista,primero,ultimo) 
 
  REPETIR
    BUCLE i DESDE primero HASTA ultimo-1
      SI lista(i) > lista(i+1) ENTONCES
        INTERCAMBIA(i, i+1)
      FIN SI
    FIN BUCLE
  HASTA QUE lista_ordenada 

Como vemos es un bucle que se repite recorriendo la lista de elementos, intercambiándolos si es necesario, hasta que la lista esté ordenada. ¿Como sabemos cuando lo está? hay dos sistemas complementarios. El primero es darse cuenta de que cuando en una pasada no sean necesarios mas intercambios, la lista está ordenada. Se usa una variable auxiliar para ello a la que se le denomina centinela, y al algoritmo en general Burbuja con centinela.

ALGORITMO Burbuja (lista,primero,ultimo) 
 
  REPETIR
    ordenada ← cierto
    BUCLE i DESDE primero HASTA ultimo-1
      SI lista(i) > lista(i+1) ENTONCES
        INTERCAMBIA(i, i+1)
        ordenada ← falso
      FIN SI
    FIN BUCLE
  HASTA QUE ordenada 


La otra forma es conocer cuantas pasadas necesitamos, lo que es también sencillo aunque en principio no lo parezca. Sabemos que en cada pasada el último elemento está siempre posicionado, por lo que debemos hacer tantas pasadas como elementos en la lista. Pero como sabmos que tras una pasada el último elemento está ordenado, también podemos reducir el recorrido en uno. Debemos repetir hasta que hayamos reducido la lista a un elemento. Como el método es complementario al anterior, usaremos ambos a la vez.

ALGORITMO Burbuja (lista,primero,ultimo) 
 
  REPETIR
    ordenada ← cierto
    BUCLE i DESDE primero HASTA ultimo-1
      SI lista(i) > lista(i+1) ENTONCES
        INTERCAMBIA(i, i+1)
        ordenada ← falso
      FIN SI
    FIN BUCLE
    ultimo ← ultimo - 1
  HASTA QUE (ordenada) O BIEN (ultimo=primero) 


Aun podemos añadir una optimización adicional. Cuando movemos un elemento puede que llegue a un punto en que los elementos a su derecha sean todos mayores que él, por lo que sabemos que esa parte ya está ordenada. Así por ejemplo si tenemos 30,12,10,40,50 y lanzamos el proceso, el 30 solo lo movemos 2 veces, quedando la lista como 12,10,30,40,50 y sabemos que en la siguiente pasada en lugar de reducir solo en uno podemos reducir en tres directamente. Como esto es nuevamente complementario lo usamos todo a la vez. No podemos alterar directamente el valor de la variable que marca el último directamente, ya que es la que se usa de compración en el bucle. Hay lenguajes como el C en que este cambio altera el bucle, y otros como el SuperBASIC en que no, como esto es un algoritmo genérico debemos pensar que puede ser alterado, y usaremos una variable auxiliar.

ALGORITMO Burbuja (lista,primero,ultimo) 
 
  REPETIR
    ordenada ← cierto
    derecha ← primero
    BUCLE i DESDE primero HASTA ultimo-1
      SI lista(i) > lista(i+1) ENTONCES
        INTERCAMBIA(i, i+1)
        ordenada ← falso
        derecha ← i
      FIN SI
    FIN BUCLE
    ultimo ← derecha
  HASTA QUE (ordenada) O BIEN (ultimo=primero) 


Se puede reempazar el bucle REPETIR por un FOR, lo que hace este algoritmo compatible con otras versiones de BASIC que no soportan esta sentencia, con dos variantes equivalentes:

FOR i = ultimo-1 TO primero STEP -1
  FOR j = primero TO i-1

O bien

FOR i = primero+1 TO ultimo
  FOR j = primero TO ultimo-i

Solo resta un detalle, el algoritmo puede ordenar de menor a mayor o al contrario, solo es necesario cambiar el sentido de la comparación, para ello añadimos como parámetro el sentido en que deseamos ordenar, y cambiamos la comparación.

ALGORITMO Burbuja (lista,sentido[Ascendente|Descendente],primero,ultimo) 
 
  REPETIR
    ordenada ← cierto
    derecha ← primero
    BUCLE i DESDE primero HASTA ultimo-1
      SI (sentido = Ascendente)  Y (lista(i) > lista(i+1)) O BIEN
         (sentido = Descendente) Y (lista(i) < lista(i+1)) ENTONCES
        INTERCAMBIA(i, i+1)
        ordenada ← falso
        derecha ← i
      FIN SI
    FIN BUCLE
    ultimo ← derecha
  HASTA QUE (ordenada) O BIEN (ultimo=primero) 

Con esto ya podemos transcribir el algoritmo a SuperBASIC. Un tema que ocurre demasiado a los programadores es que una vez tienen su algoritmo, en lugar de transcribirlo en su lenguaje, lo escriben de nuevo, lo que es un esfuerzo inútil cuando todo ha sido estudiado de antemano. Aquí está mi transcripción del programa, en el que lo único que añado es que por temas de hacer las líneas mas cortas, como SuperBASIC no admite sentencias en varias líneas, uso una variable auxiliar para saber si tengo que intercambiar los elementos, y la asigno según desee que la lista sea ascendente o descendente, y que si no indico cual es el último elemento que deseo ordenar, lo calculo en función de los elementos del arreglo:

4000 REMark ---------------------------------------------
4010 REMark -- ALGORITMOS DE ORDENACION                --
4020 REMark --   Modulo....: Burbuja                   --
4030 REMark --   Objetivo..: Ordena un arreglo por el  --
4040 REMark --               algoritmo de burbuja      --
4050 REMark --   Autor.....: javu61, 11/2016           --
4060 REMark --   Parametros:                           --
4070 REMark --     arreglo -> Arreglo a ordenar        --
4080 REMark --     sentido -> FALSO = Ascendente       --
4090 REMark --     primero -> primer elemento          --
4100 REMark --     ultimo  -> ultimo elemento          --
4110 REMark --                Si ultimo=0 -> Todos     --
4120 REMark ---------------------------------------------
4130 DEFine PROCedure Burbuja (arreglo,sentido,primero,ultimo)
4140   LOCal derecha,cambiar,ordenada,repetir,i
4150   :
4160   IF ultimo=0 THEN ultimo=DIMN(arreglo)
4170   :
4180   REPeat repetir
4190     ordenada=Si
4200     derecha=primero
4210     FOR i=primero TO ultimo-1
4220       cambiar=No
4230       :: nrocomp(nrutina,nropaso)=nrocomp(nrutina,nropaso)+1
4240       IF sentido=Ascendente THEN 
4250         IF arreglo(i) > arreglo(i+1) THEN cambiar=Si
4260       ELSE 
4270         IF arreglo(i) < arreglo(i+1) THEN cambiar=Si
4280       END IF 
4290       IF cambiar THEN 
4300         :: nromovi(nrutina,nropaso)=nromovi(nrutina,nropaso)+3
4310         temp         = arreglo(i)
4320         arreglo(i)   = arreglo(i+1)
4330         arreglo(i+1) = temp
4340         :
4350         ordenada=No
4360         derecha=i
4370       END IF 
4380     END FOR i
4390     ultimo=derecha
4400     IF ordenada OR (primero=ultimo) THEN EXIT repetir
4410   END REPeat repetir
4420 END DEFine 


Las líneas que comienzan por :: son solo para contar comparaciones e intercambios, necesario solo para las pruebas, en el algoritmo definitivo hay que eliminar estas líneas para poder usarlo en otros programas.

jueves, 10 de noviembre de 2016

Programación del Sinclair QL (VII): Algoritmos de ordenación, planteamiento para las pruebas



Nota importante sobre los programas que se presenten

Incluiré el código completo del programa siempre al final de la entrada, pero a lo mejor veis que no coinciden los números de línea, e incluso que en la misma entrada los números de línea se solapan entre trozos del código.

Salvo en el módulo de carga en el resto del módulos no usan GOTO o GOSUB, aunque nunca es problema renumerarlos, ya que los números de línea solo sirven como referencia para la edición y los comentarios que haga, incluso puede que en algunos casos veáis que se solapan dentro del mismo articulo. Al final siempre encontrareis el código completo del mismo, podéis usarlo para vuestro QL o con vuestro emulador.

La base para las pruebas de algoritmos con datos en memoria


Empezaremos con lo básico para las pruebas de los algoritmos que es disponer de una estructura común para todos, que los llame con las mismas condiciones iniciales y guarde los resultados del proceso. En el QL hay un Reloj que podemos explotar bien desde código máquina, pero desde SuperBASIC solo tenemos posibilidad de contar segundos, es un poco limitado pero creo que bastará para las pruebas.

Voy a ordenar un arreglo de números de la longitud que le indiquemos, un procedimiento o función acepta un arreglo como parámetro de entrada, pero yo el arreglo no se lo puedo pasar al algoritmo como parámetro pues las variables se pasan por valor y lo que se ordenaría sería el arreglo copiado y no el original, es una limitación importante de no disponer de punteros.

El proceso lo voy a dividir en ficheros separados que cargaré usando MERGE, de esta forma es mas sencillo el mantenimiento, y tiene la ventaja de que cada módulo de ordenación es un fichero independiente, por lo que se puede usar en otros programas.

Módulo de carga


Este es el modulo que realizará la carga del resto, de forma que no hay que hacerlo manualmente nunca. Es muy sencillo pero hay que eliminarlo si se desea ejecutar el programa para que no lo vuelva a ejecutar, acuérdate de usar DLINE TO 999 cuando termine de cargar. Cada vez que incluyo un módulo debo cambiar la línea 170, y añadir dos líneas mas tras la 190 por cada módulo:

100 REMark ----------------------------------------
110 REMark -- ALGORITMOS DE ORDENACION --
120 REMark -- Modulo: Cargador de modulos --
130 REMark -- Autor.: javu61, noviembre 2016 --
140 REMark ----------------------------------------
150 :
160 :
170 DATA "mdv1_"
180 DATA "o_base" , "Modulo base"
190 DATA "o_selector" , "Modulo Selector"
200 DATA "o_burbuja" , "Modulo Burbuja"
210 DATA "o_sacudida" , "Modulo Sacudida"
220 DATA ""
230 :
240 actual = 0 : total = 0
250 DIM p$(20,2,20)
260 :
270 RESTORE 
280 READ u$
290 REPeat leer_data
300 READ p$(total,0) : IF p$(total,0)="" THEN EXIT leer_data
310 READ p$(total,1)
320 p$(total,0)=u$ & p$(total,0)
330 total=total+1
340 END REPeat leer_data
350 :
360 MODE 4 : PAPER 0 : CLS
370 PRINT "Cargando modulos del programa"
380 :
390 PRINT " (";actual;"/";total;") ";p$(actual,1)
400 MERGE p$(actual,0)
410 actual=actual+1
420 IF p$(actual,0) <> "" THEN GO TO 390
430 :
440 :
450 PRINT "Ejecute DLINE TO 999 antes de ejecutar"
460 STOP
En la línea 420 hay un GOTO, el único que encontrarás en el programa, está ya que al ejecutar la sentencia MERGE dentro de un bucle se pierden los punteros, hay que recurrir al método tradicional para que funcione.

Módulo base

Este es el modulo que realizará las pruebas de los algoritmos. Como veréis todo lo empiezo por una cabecera general con lo que es, no es necesario pero si una practica de buen programador. Una técnica para acelerar los programas en BASIC es eliminar comentarios, ya que el programa pasa por ellos y los debe analizar, esto no es necesario en SuperBASIC pues monta una tabla de procedimientos y funciones, por lo que casi nunca pasará por los comentarios.
1000 REMark ----------------------------------------
1010 REMark -- ALGORITMOS DE ORDENACION           --
1020 REMark --   Modulo: Base para las pruebas    --
1030 REMark --   Autor.: javu61, noviembre 2016   --
1040 REMark ----------------------------------------

Primero creo un manejador de errores, es una muy buena costumbre tener al menos una estándar, debe estar al principio para que el programa sepa que existe esa rutina de errores.

1060 REMark ----------------------------------------
1070 REMark -- Manejo de errores                  --
1080 REMark ----------------------------------------
1090 WHEN ERRor 
1100   PRINT "En la linea ";ERLIN;" se ha producido un error ";ERNUM;": ";
1110   REPORT #1,ERNUM
1120   STOP
1130 END WHEN 
 
Ahora voy lanzando procesos que hagan las cosas, contra mas modular mejor, pues es difícil seguir un programa largo y al dividirlo en partes pequeñas se simplifica su lectura, lo que para el mantenimiento es fundamental. Creo que fue Ole-Johan Dahl, uno de los creadores de la programación orientada a objetos, el que dijo que una función no debía ocupar mas de una página (supongo que de papel y no de pantalla). Empiezo borrando pantalla y pidiendo los elementos a ordenar, contra mas tenga mas tardará, y mejor resultado darán las prueba, pero el QL no es un veloz galgo, no os paséis.

1160 REMark ----------------------------------------
1170 REMark -- Pedir elementos a procesar         --
1180 REMark ----------------------------------------
1190 LET nroelem = 0    : REMark Nro elementos a ordenar
1200 :
1210 MODE 4 : PAPER 0 : CLS
1220 INPUT "Numero de elementos a ordenar: ";nroelem
1230 nroelem=nroelem - 1  : REMark Empieza en cero
 
A mi me gusta definir las variables siempre, ahorra errores, pero en SuperBASIC no es necesario ni posible mas que para los arreglos, pero lo simulo inicializándolas a un valor, y uso LET para distinguir que solo es la primera declaración. En SuperBASIC no existen las constates que hacen el código mas legible, las voy a simular con variables, todas son booleanas, en SuperBASIC cero equivale a falso y distinto de cero equivale a cierto.

1260 REMark ----------------------------------------
1270 REMark -- Definir Seudo-Contantes            --
1280 REMark ----------------------------------------
1290 LET No = 0          : REMark Valor para falso
1300 LET Si = 1          : REMark Valor para cierto
1310 LET Ascendente  = 0 : REMark Orden a montar
1320 LET Descendente = 1 : REMark Orden a montar
1330 :
1340 :
1350 REMark ----------------------------------------
1360 REMark -- Definir variables globales         --
1370 REMark ----------------------------------------
1380 LET m = 20         : REMark Nro maximo de rutinas a probar
1390 LET nrutina = 0    : REMark Algoritmo en prueba
1400 LET nropaso = 0    : REMark Paso en la prueba
1410 DIM nombres$(m,18) : REMark Nombres de algoritmos
1420 DIM tiempoi(m,3)   : REMark Guardar tiempos inicio
1430 DIM tiempof(m,3)   : REMark Guardar tiempos fin
1440 DIM nrocomp(m,3)   : REMark Numero de comparaciones
1450 DIM nromovi(m,3)   : REMark Numero de movimientos
1460 :
1470 DIM Abase(nroelem) : REMark Arreglo base aleatorio
1480 DIM Atemp(nroelem) : REMark Arreglo a ordenar
  
Solo queda ir llamando a los tres procesos que voy a utilizar, inicializar todo, llamar a la pruebas y presentar el resultado.

1510 REMark ----------------------------------------
1520 REMark -- Lanzar por procesos                --
1530 REMark ----------------------------------------
1540 Inicializar
1550 Probar
1560 INPUT "Finalizado",fin$
1570 Resultados
1580 STOP

El proceso de inicio crea un arreglo para los valores aleatorios y lo muestra en pantalla como referencia, para que veamos el origen.

1610 REMark ----------------------------------------
1620 REMark -- Inicializar                        --
1630 REMark ----------------------------------------
1640 DEFine PROCedure Inicializar
1650   Montar_arreglo Abase
1660   PRINT "----- Elementos a ordenar"
1670   Presentar_arreglo Abase
1680 END DEFine 
 
Paso al proceso que lanza las pruebas. En lenguajes que soporten punteros como el C o los orientados a objeto se puede crear un arreglo con los enlaces a las rutinas que definamos, no en SuperBASIC, por lo que empezamos un bucle indefinido, guiado por la variable nrutina en la que llevamos un contador de rutinas a probar, según el número seleccionamos el nombre del algoritmo o salimos del bucle. Luego hacemos tres pasadas por un bucle, la primera ordena el arreglo base aleatorio, la segunda con el mejor caso que es con el arreglo ya ordenado, y la tercera con el peor caso lo que invierte el arreglo completamente.

1710 REMark ----------------------------------------
1720 REMark -- Proceso de pruebas                 --
1730 REMark ----------------------------------------
1740 DEFine PROCedure Probar
1750   REPeat prueba_rutina
1760     Seleccion No
1770     IF nombres$(nrutina)="" THEN EXIT prueba_rutina
1780     PRINT "** Probando rutina ";nrutina;" ";nombres$(nrutina)
1790     :
1800     Copiar_arreglo Abase,Atemp
1810     FOR nropaso=0 TO 2
1820       IF nropaso <> 2 THEN 
1830         orden = Ascendente
1840       ELSE 
1850         orden = Descendente
1860       END IF 
1870       tiempoi(nrutina,nropaso)=DATE
1880       Seleccion Si,Atemp,orden
1890       tiempof(nrutina,nropaso)=DATE
1900       :
1910       Verifica_arreglo Atemp, orden
1920       :
1930       PRINT "-- ";nombres$(nrutina);
1940       SELect ON nropaso
1950         ON nropaso=0 : PRINT !" Aleatorio";
1960         ON nropaso=1 : PRINT !" Ordenado ";
1970         ON nropaso=2 : PRINT !" Inverso  ";
1980       END SELect 
1990       REMark PRINT !" (";DATE$;") "
2000       t=tiempof(nrutina,nropaso)-tiempoi(nrutina,nropaso)
2010       PRINT !"T: ";t;" seg";
2020       PRINT !"C: ";nrocomp(nrutina,nropaso);
2030       PRINT !"I: ";nromovi(nrutina,nropaso);
2040       PRINT
2050     END FOR nropaso
2060     :
2070     nrutina=nrutina+1
2080     IF nrutina > m THEN EXIT prueba_rutina
2090     :
2100   END REPeat prueba_rutina
2110 END DEFine 

Terminamos este grupo con la parte que presenta los resultados en la pantalla para poder compararlos todos entre sí. Para evitar líneas largas uso variables auxiliares con lo que mostrar.

2140 REMark ----------------------------------------
2150 REMark -- Presentar los resultados           --
2160 REMark ----------------------------------------
2170 DEFine PROCedure Resultados
2180   LOCal e1$,g1$,g2$
2190   :
2200   e1$=FILL$(" ",25)
2210   g1$=FILL$("-",24)
2220   g2$=FILL$("-",15)
2230   :
2240   CLS
2250   PRINT e1$;"+";g2$;"+";g2$;"+";g2$;"+"
2260   PRINT "       Con "; : Pnumero nroelem+1,3: PRINT " elementos";
2270   PRINT " |  Caso medio   |  Caso mejor   |   Caso peor   |"
2280   PRINT "+";g1$;"+";g2$;"+";g2$;"+";g2$;"+"
2290   FOR i=0 TO DIMN(nombres$)
2300     IF nombres$(i)="" THEN EXIT i
2310     t0=tiempof(i,0)-tiempoi(i,0)
2320     t1=tiempof(i,1)-tiempoi(i,1)
2330     t2=tiempof(i,2)-tiempoi(i,2)
2340     PRINT "| "; : Pnumero i,2
2350     PRINT " "; : PRINT nombres$(i);
2360     lon = 19-LEN(nombres$(i))
2370     IF lon > 0 THEN PRINT FILL$(" ",lon);
2380     PRINT " | "; : Pnumero t0,3
2390     PRINT " "; : Pnumero nrocomp(i,0),4
2400     PRINT " "; : Pnumero nromovi(i,0),4
2410     PRINT " | "; : Pnumero t1,3
2420     PRINT " "; : Pnumero nrocomp(i,1),4
2430     PRINT " "; : Pnumero nromovi(i,1),4
2440     PRINT " | "; : Pnumero t2,3
2450     PRINT " "; : Pnumero nrocomp(i,2),4
2460     PRINT " "; : Pnumero nromovi(i,2),4
2470     PRINT " |"
2480   END FOR i
2490   PRINT "+";g1$;"+";g2$;"+";g2$;"+";g2$;"+"
2500 END DEFine 
 
Luego tenemos rutinas auxiliares, la primera rellena un arreglo de un tamaño dado con valores aleatorios entre 10 y 99, de esta forma con un PRINT salen en columnas homogéneas.

2530 REMark ----------------------------------------
2540 REMark -- Rellena un arreglo con numeros     --
2550 REMark -- aleatorios de dos cifras           --
2560 REMark ----------------------------------------
2570 DEFine PROCedure Montar_arreglo(arreglo)
2580   LOCal i
2590   :
2600   FOR i=0 TO DIMN(arreglo)
2610     arreglo(i)=RND(10 TO 99)
2620   END FOR i
2630 END DEFine 
 
La segunda copia un arreglo sobre otro, es muy sencilla.

2660 REMark ----------------------------------------
2670 REMark -- Copiar un arreglo sobre otro       --
2680 REMark ----------------------------------------
2690 DEFine PROCedure Copiar_arreglo(origen,destino)
2700   LOCal i
2710   :
2720   IF DIMN(destino) < DIMN(origen) THEN 
2730     PRINT "ERROR copiando arreglo: Destino menor que origen"
2740     STOP
2750   END IF 
2760   :
2770   FOR i=0 TO DIMN(origen)
2780     destino(i)=origen(i)
2790   END FOR i
2800 END DEFine 

Luego otra para presentar por pantalla un arreglo, como todos los valores son de 2 cifras, mas el espacio intermedio, caben 25 valores por línea en modo 4.

2830 REMark ----------------------------------------
2840 REMark -- Presenta en pantalla un arreglo    --
2850 REMark ----------------------------------------
2860 DEFine PROCedure Presentar_arreglo(arreglo)
2870   LOCal i
2880   :
2890   FOR i=0 TO DIMN(arreglo)
2900     PRINT !arreglo(i);
2910   END FOR i
2920   PRINT
2930 END DEFine 
 
Para poder asegurarme de que las rutinas funciona bien, este proceso verifica que están bien ordenados los elementos del arreglo según el orden indicado.

2960 REMark ----------------------------------------
2970 REMark -- Ver si el arreglo esta ordenado    --
2980 REMark ----------------------------------------
2990 DEFine PROCedure Verifica_arreglo(arreglo, orden)
3000   LOCal i,hayerror
3010   :
3020   hayerror = No
3030   FOR i=0 TO DIMN(arreglo) - 1
3040     IF orden = Ascendente THEN 
3050       IF arreglo(i) > arreglo(i+1) THEN hayerror=Si
3060     ELSE 
3070       IF arreglo(i) < arreglo(i+1) THEN hayerror=Si
3080     END IF 
3090     IF hayerror THEN 
3100       PRINT "ERROR: Mal en orden";
3110       SELect ON orden
3120         = Ascendente  : PRINT !"ascendente";
3130         = Descendente : PRINT !"descendente";
3140       END SELect 
3150       PRINT !"Posicion ";i;"(";arreglo(i);")";
3160       PRINT !"Posicion ";i+1;"(";arreglo(i+1);")"
3170       Presentar_arreglo arreglo
3180       STOP
3190     END IF 
3200   END FOR i
3210 END DEFine   

Esta toma un número cualquiera y lo presenta relleno a ceros por la izquierda si es de menos de tres dígitos. Es sencillo ampliarla para que acepte como parámetro la longitud a rellenar.

3240 REMark ----------------------------------------
3250 REMark -- Presenta en pantalla un numero     --
3260 REMark ----------------------------------------
3270 DEFine PROCedure Pnumero(valor$,long)
3280   LOCal lon,aux$
3290   aux$=valor$
3300   IF aux$="" THEN aux$="0"
3310   lon = long - LEN(valor$)
3320   IF lon > 0 THEN aux$=FILL$(" ",lon) & aux$
3330   PRINT aux$;
3340 END DEFine 

Aquí terminan las rutinas de módulo base.

Módulo Selector

Este módulo contiene el procedimiento que llama al algoritmo en función del número, si no procesa monta la tabla de nombres de algoritmos, seguido por la línea que llama al algoritmo real. Hay que ir ampliándolo con los algoritmos que se desarrollen:

3500 REMark ----------------------------------------
3510 REMark -- ALGORITMOS DE ORDENACION           --
3520 REMark --   Modulo: Selector de algoritmos   --
3530 REMark --   Autor.: javu61, noviembre 2016   --
3540 REMark ----------------------------------------
3550 :
3560 :
3570 REMark ----------------------------------------
3580 REMark -- SELECCION DEL ALGORITMO            --
3590 REMark ----------------------------------------
3600 DEFine PROCedure Seleccion(procesar,arreglo,ordenacion)
3610   SELect ON nrutina
3620     ON nrutina=0
3630       nombres$(nrutina)="Burbuja"
3640       IF procesar THEN Burbuja arreglo,ordenacion,0,0
3650     ON nrutina=1
3660       nombres$(nrutina)="Burbuja Bidireccional"
3670       IF procesar THEN Sacudida arreglo,ordenacion,0,0
3680     :
3690     : REMark Aqui el resto de rutinas
3700     :
3710   END SELect 
3720 END DEFine 
 

Módulos de ordenación


Y por fin tenemos los métodos de ordenación, pongo la base de los dos primeros, la ordenación por burbuja y la ordenación por sacudida, solo para que el programa pueda ejecutarse y podemos ver cosas en pantalla. 

Modulo burbuja

4000 REMark ---------------------------------------------
4010 REMark -- ALGORITMOS DE ORDENACION                --
4020 REMark --   Modulo....: Burbuja                   --
4030 REMark --   Objetivo..: Ordena un arreglo por el  --
4040 REMark --               algoritmo de burbuja      --
4050 REMark --   Autor.....: javu61, 11/2016           --
4060 REMark --   Parametros:                           --
4070 REMark --     arreglo -> Arreglo a ordenar        --
4080 REMark --     sentido -> FALSO = Ascendente       --
4090 REMark --     primero -> primer elemento          --
4100 REMark --     ultimo  -> ultimo elemento          --
4110 REMark --                Si ultimo=0 -> Todos     --
4120 REMark ---------------------------------------------
4130 DEFine PROCedure Burbuja (arreglo,sentido,primero,ultimo)
4140   REMark Aqui va el codigo
4150 END DEFine 

Modulo sacudida

4500 REMark ---------------------------------------------
4510 REMark -- ALGORITMOS DE ORDENACION                --
4520 REMark --   Modulo....: Sacudida                  --
4530 REMark --   Objetivo..: Ordena un arreglo por el  --
4540 REMark --               algoritmo de burbuja      --
4550 REMark --               doble o sacudida          --
4560 REMark --   Autor.....: javu61, 11/2016           --
4570 REMark --   Parametros:                           --
4580 REMark --     arreglo -> Arreglo a ordenar        --
4590 REMark --     sentido -> FALSO = Ascendente       --
4600 REMark --     primero -> primer elemento          --
4610 REMark --     ultimo  -> ultimo elemento          --
4620 REMark --                Si ultimo=0 -> Todos     --
4630 REMark ---------------------------------------------
4640 DEFine PROCedure Sacudida (arreglo,sentido,primero,ultimo)
4650   REMark Aqui va el codigo
4660 END DEFine 


A partir de ahora solo presentaré los algoritmos que desarrolle en el QL, se añaden al final de todo, junto a código para manejarlos en el procedimiento anterior.

Podéis descargar el programa completo desde aquí, abrir los programas con un editor de textos (no el notepad que solo soporta textos con CR+LF) o cargarlo en un QL real o emulado.

martes, 8 de noviembre de 2016

Programación del Sinclair QL (VI): Procedimientos y funciones, paso de parámetros



Parámetros por referencia y por valor


Tras tantos años acabo de descubrir un tema que no he visto reflejado en otros lugares, supongo que buscando un poco mas aparecerá, pero usando Q-Emulator con versión del SuperBASIC inglesa JS he probado este programa:

100 CLS
110 a=10
120 b(a)  : PRINT a
130 b a   : PRINT a
140 :
150 DEFine PROCedure b(c)
160   c=c+1
170 END DEFine


Si el paso de parámetros fuera siempre por valor, el resultado debían ser 10 y 10, pero el resultado es  10 y 11. Esto quiere decir que cuando llamamos a un procedimiento o función pasando los parámetros entre paréntesis pasan por valor, pero al pasarlos sin ellos lo hacen por referencia. Esto sería muy bueno si no fuera por que cuando llamamos a un procedimiento o función con mas de un parámetro, no podemos usar los paréntesis en la llamada pues da un error. Seguramente es uno de los famosos errores de la ROM del QL.

Parámetros opcionales


Otro tema que tampoco conocía y también acabo de descubrir es que se pueden usar parámetros opcionales de cierta manera, ya que mientras no los uses en tu programa no dará error:

100 CLS
110 x=5:y=5: b x   :PRINT x,y
120 x=5:y=5: b x,y :PRINT x,y
130 x=7:y=5: b x   :PRINT x,y
140 x=7:y=5: b x,y :PRINT x,y
150 :
160 DEFine PROCedure b(c,d)
170   c=c+1
180   IF c > 6 THEN d=d+1
190 END DEFine 

Esto presentará en pantalla por la línea 110 (6,5),  la 120 presenta (6,5), la 130 muestra (8,6), y al final un error de variable no definida al querer usar la línea 140.

lunes, 7 de noviembre de 2016

Programación del Sinclair QL (V): Algoritmos de ordenación, clasifición y análisis



Consideraciones sobre los algoritmos de ordenación


Los algoritmos de ordenación son vitales en informática, no solo para poder presentar los datos ordenados sino para poder optimizar las búsquedas en ellos, como demuestra que el volumen 3 de la serie de 7 de "El arte de programar ordenadores" de Donald Knuth se dedique a "Ordenación y búsqueda". Se considera que el primer algoritmo de ordenación fue desarrollado en 1951 por Betty Holberton, una de las 6 programadoras del ENIAC.

Los primeros ordenadores disponían de muy pocos recursos, por lo que los algoritmos iterativos que no requieren recursos podían usarse, pero si los datos eran numerosos había que recurrir a memoria externa como las cintas magnéticas y en esos casos se usaban la clasificación por mezcla de datos que eran mas efectivas. Cuando la potencia de calculo, la cantidad de memoria y los recursos externos como los discos duros fueron habituales, se crearon los algoritmos recursivos. Aparte de estos hay un cuarto grupo que no son estrictamente de ordenación, mas bien hacen lo contrario y en lugar de ordenar desordenan, que se emplean para estudiar los otros algoritmos.

Para elegir un algoritmo de ordenación hay que evaluar los varios puntos del algoritmo, que son los que utilizan más recursos del ordenador:
  • Comparaciones: Para poder ordenar hay que comparar valores, el tiempo empleado en una comparación es proporcional a tamaño del dato, comparar enteros es rápido, comparar cadenas es mas lento. Evidentemente contra menos comparaciones mas rápido, y siempre el que menos necesite será de los más rápidos.
  • Intercambios: Para ordenar hay que cambiar el orden de los registros, este tiempo puede ser proporcional al tamaño del dato si se mueve físicamente, o ser lineal si se cambian punteros nada mas. Los algoritmos iterativos mueven muchos datos intercambiándolos en memoria, los recursivos mueven una cantidad pequeña de datos también intercambiándolos en memoria, mientras que los de mezcla los mueven menos pero lo hacen a otras zonas de memoria ocupando mucha mas.
  • Memoria auxiliar: Los algoritmos iterativos necesitan muy poco memoria auxiliar para su proceso, los recursivos requieren una cantidad proporcional a los datos que van a manejar, mientras que los de mezcla requieren una cantidad de memoria igual o mayor a la que ocupan los datos.
  • Ubicación: Otro aspecto importante es donde de encuentran los datos, si estos están en memoria o en una unidad externa como un fichero en disco, los tiempos de acceso son mucho mas elevados, por lo que minimizando la cantidad de accesos se pueden lograr aumentos importantes de velocidad.

Clasificación según su estructura y análisis general

Como ingeniero informático no puedo dejar de mencionar los ordenes de tiempo y ocupación de memoria de los algoritmos, pero no voy a entrar mucho más, el análisis de algoritmos es un tema muy bonito y complejo por lo que si alguno lo desea puedo discutirlo en otras entradas.

  • Algoritmos por intercambio: Se basan en ir intercambiando los elementos de la lista unos con otros ubicándolos o al menos acercándolos a su lugar correcto. Hay dos variantes generales:
    • Algoritmos por intercambio iterativos Son los que menos recursos de espacio necesitan, normalmente solo espacio para guardar uno de los datos a clasificar temporalmente, a cambio requieren mas pasadas entre los datos para ordenarlos. Son mas lentos, pero muy sencillos de programar, por lo que para cantidades pequeñas de datos son los preferidos, el mas conocido es el de Burbuja, que son 4 líneas de programa, en general es también el más lento pero si en un PC moderno ordenar por burbuja 100 datos cuesta menos de un segundo, ¿para que emplear tiempo en hacer otros algoritmos mas complejos? En general tardan un tiempo proporcional al cuadrado del número de registros a ordenar por tanto Velocidad Θ(n2), y requieren solo de un solo dato en memoria por tanto Ocupación Θ(1), por lo que en caso de poca memoria son los mas recomendables, por ejemplo si queremos ordenar 1000 bytes en una máquina con 1024 posiciones de memoria, con esos 24 bytes podemos programar uno de estos algoritmos.
    • Algoritmos por intercambio recursivos o por partición: Consiguen mucha mejor velocidad ya que ubican mas elementos en su lugar o muy cerca a la primera minimizando los intercambios. Van dividiendo la lista en sublistas a partir de un pivote, y las ordenan de forma que todos los elementos de la sublista anterior al pivote sean menores que él y los posteriores todos mayores, pasando luego a realizar el mismo proceso para cada una de las sublistas que se generan. Requieren mas memoria para la recursividad, pero reducen drásticamente el número de intercambios sobre los recursivos. El famoso QuickSort es el considerado mas rápido de los algoritmos de ordenación en casi todos los casos. Su tiempo en general es del orden de Velocidad Θ(n log n), igual que para los de mezcla, pero al ser recursivo Ocupación Θ(log n). Si dispones de memoria suficiente y de un lenguaje recursivo no dudes en usarlo, es sencillo de programar y el mas rápido casi siempre.
  • Algoritmos por inserción: Variante de los de intercambio que buscan el lugar adecuado para un elemento y lo ubican en su lugar, desplazando el resto de elementos y minimizando así el número de intercambios. El mas conocido es el de Ordenación por Inserción. También los hay iterativos y recursivos, y su tiempo y ocupación en general es similar a los de intercambio, iterativos Velocidad Θ(n2) y recursivos Velocidad Θ(n log n), aunque al reducir el número de intercambios siempre son un poco mas rápidos. La ocupación en memoria es también similar, los iterativos Ocupación Θ(1) y los recursivos Ocupación Θ(log n).
  • Algoritmos por selección: Estos algoritmos funcionan en dos pasos, primero utilizan estructuras auxiliares para clasificar los datos (usualmente creando un árbol), y luego lo recorren montando la lista ordenada. El mas usado es el de Ordenación por Montones (un montón es una forma de arbol binario que usa una lista) Al recorrer la lista una sola vez son rápidos y su velocidad es Velocidad Θ(n log n), pero requieren montar una estructura auxiliar por lo que necesitan tanta memoria como datos a ordena, y por tanto Ocupación Θ(n)
  • Algoritmos de mezcla: Son los preferidos para ordenar cuando hay ubicaciones suficientes para ello, y siempre que se realiza en medios de almacenamiento auxiliares y no en memoria. Son mas rápidos pero requieren en general mucho espacio, habitualmente el mismo que ocupan los datos a ordenar si se realizan en memoria, pero se reduce mucho si es en disco. El mas popular entre estos es Merge Sort. Su tiempo varía mucho pero suelen estar en el orden de Velocidad Θ(n log n), y ocupan normalmente lo mismo que los datos a clasificar, por tanto Ocupación Θ(n) trabajando en memoria, pero pueden trabajar muy bien con memoria externa y en este caso Ocupación Θ(1). Apoyados en un medio de almacenamiento masivo son los mejores si estos ya están en memoria externa, así podemos ordenar en poco tiempo una gran cantidad de datos que no quepan en memoria.
  • Algoritmos de distribución: Requieren un conocimiento previo de las claves a ordenar, ya que los van clasificando en grupos según un criterio, de forma similar a como se reparte el correo clasificándolo por códigos postales, luego por calles, por números de calle y finalmente por viviendas. El mas popular entre estos es el de Ordenación por Casilleros. Su tiempo es bueno ya que suelen ser Velocidad Θ(nk) , pero ocupan normalmente más que los datos a clasificar, por tanto Ocupación Θ(nk), con una k pequeña que puede llegar a ser 1 en algunos casos, pero con la limitación de ser muy dependientes del conocimiento de los datos a ordenar.
  • Algoritmos concurrentes: En este grupo se incluyen algoritmos que se han diseñado específicamente para máquinas con varios procesadores (o varios núcleos) que trabajan a la vez de manera coordinada ordenando los datos. El mas usado es el Cube para ordenar los famosos "cubos" de análisis de datos, realmente arreglos multidimensionales.
  • Algoritmos Híbridos: Es usual emplear varios algoritmos a la vez, por ejemplo Quicksort trabaja bien en listas grandes, pero mal cuando son ya pequeñas, por lo que se suele llamar a otro cuando las sublistas tienen ya pocos elementos. Este grupo de algoritmos hacen lo mismo, emplean dos métodos de ordenación complementarios para alcanzar el objetivo.
  • Otros algoritmos: En este grupo se incluyen algoritmos especializados en un tipo de datos concreto, como grafos o qbits, o algoritmos muy imaginativos que son solo adecuados para el análisis de algoritmos ya que no son nada eficientes.
  • Algoritmos de desordenación: Estos algoritmos son muy usados para producir resultados aleatorios en juegos. El mas antíguo es el Algoritmo del Sombrero. Estos algoritmos ordenan aleatoriamente los datos y repite el proceso hasta que por casualidad estén ordenados, por tanto no se puede usar para ordenar ya que su tiempo es del orden de los algoritmos intratables, con pocos datos puede ser del orden de Velocidad Θ(n!) aunque lo normal es que no acaben nunca. A cambio es muy usado para simular en un programa el barajar las cartas.

Algoritmos estables e inestables

Un registro puede contener claves primarias y secundarias de ordenación, por ejemplo cuando se ordena por Apellidos y Nombre una lista. Se considera un algoritmo estable cuando ordenan siempre los registros con claves primarias iguales en su orden por las secundarias, y se consideran inestables cuando no consiguen mantener el orden dentro de las secundarias. Esta inestabilidad se puede eliminar si unimos las claves primarias y secundarias en una clave única, al desaparecer las claves secundarias desaparece el problema.

Orden y desorden

Un punto importante es cuan desordenados están los datos inicialmente, esto nos puede ayudar a elegir uno u otro algoritmo, hay alguno especializado en listas casi ordenadas. Una forma de añadir un elemento a una lista es ubicarlo al final y llamar a la ordenación para que lo ponga en su lugar, por eso se distinguen tres casos que son los que usaré en las pruebas:

  • Caso medio: Los datos están ordenados aleatoriamente, siendo este es el caso genérico que se usa para probar los algoritmos.
  • Caso mejor: Los datos ya están ordenados, en este caso los últimos serán los primeros ya que la lenta Burbuja es el que mas rápido acaba normalmente. La comparación de este tiempo con el anterior nos proporciona una idea de la velocidad cuando los datos están casi ordenados.
  • Caso peor: Los datos están en orden inverso, que es una buena prueba en la que los mejores algoritmos deben tardar mas o menos lo mismo que en el caso medio.
Las pruebas de los algoritmos de ordenación en memoria las voy a hacer de la siguiente forma para que sean comparables:

  1. Genero un arreglo con el tamaño que se indique, y lo relleno de números enteros aleatorios, que pueden estar repetidos. Será el que use luego con todos los algoritmos y así no hay diferencias de partida.
  2. Para cada algoritmo que deseo probar:
    1. Caso medio: Copio el arreglo sobre uno temporal, y llamo al algoritmo para ordenar el arreglo temporal de menor a mayor. Cuento la cantidad de comparaciones, la cantidad de intercambios y el tiempo en segundos que tarda en total.
    2. Caso mejor: Vuelvo a llamar al algoritmo con el arreglo temporal ya ordenado para que lo intente ordenar de menor a mayor, no debe mover ningún elemento del arreglo. Cuento la cantidad de comparaciones, la cantidad de intercambios y el tiempo en segundos que tarda en total
    3. Caso peor: Vuelvo a llamar al algoritmo con el arreglo temporal ya ordenado para que lo intente ordenar de mayor a menor, lo que debe cambiar todos los elementos del arreglo. Cuento la cantidad de comparaciones, la cantidad de intercambios, y el tiempo en segundos que tarda en total.
  3. Presento los resultados de todos los algoritmos.
Mas adelante lo volveré a hacer con los algoritmos de ordenación externos, que ordenen los datos almacenados en un fichero guardado en el microdriver, haciendo las modificaciones oportunas en el programa base.

Clasificación según su método de ordenación

Según el método que empleen para ordenar los datos hay una serie de familias de algoritmos. Los hay mas o menos rápidos, pero también se han desarrollado algoritmos impracticables, ya que el tiempo que tardan los hacen no abordables, y solo se han usado para el estudio de los algoritmos. No voy a desarrollar todos en superBASIC solo los mas habituales pues la lista es muy extensa, solo pongo aquí los algoritmos las más conocidos y ya son mas de 50:


Por Intercambio
Estos algoritmos son buenos trabajando sobre datos en memoria, trabajan intercambiando elementos de la lista a ordenar para ir acercándolos a su posición definitiva. Todos son variantes del denostado método de la Burbuja, y en general Inestables.
  • Métodos iterativos
    • Ordenación por Burbuja (Bubble sort)
    • Ordenación por Sacudida o Burbuja Bidireccional (Cocktail shaker sort)
    • Ordenación Par-Impar o de Pares y Nones (Odd–even sort)
    • Ordenación de peine (Comb sort) Inestable
    • Ordenación de vaivén o cremallera (Gnome sort o Stupid sort)
    • Ordenación de Algunos Únicos (Several unique sort). Inestable. Algoritmo ineficiente ya que no mejora apenas la velocidad de la burbuja y por tanto solo es usado en análisis de algoritmos.
  • Métodos recursivos
    • Ordenación Rápida (Quicksort) Inestable
    • Ordenación con Ayudante (Stooge sort o Trippel sort). Inestable e impracticable por su baja velocidad
    • Ordenación Tonta (Sillysort). No mejora más que muy ligeramente la velocidad de la burbuja por lo que es un algoritmo ineficiente, solo usado en análisis de algoritmos.

Por Inserción
Se busca el lugar donde ubicar el elemento y se inserta en el, desplazando el resto de elementos si es necesario.
  • Ordenación por Inserción (Insertion sort)
  • Ordenación Shell (Shellsort) Por el nombre de su creador. Inestable
  • Ordenación por Árbol Binario (Tree sort o Binary Tree sort)
  • Ordenación por Árbol Biselado (Splaysort o Splay Tree sort)
  • Algoritmo de la librería o de huecos (Library sort o Gapped insertion sort)
  • Algoritmo de la Paciencia (Patience sorting), por un juego de cartas del solitario de ese nombre

Por Selección
Estos algoritmos utilizan estructuras auxiliares en memoria para aumentar la velocidad de proceso.
  • Ordenación por montículos (Heapsort). Inestable, se considera poco eficiente por lo que no se recomienda su empleo.
  • Ordenación por selección (Selection sort), inestable
  • Ordenación de Dijkstra (Smoothsort) Por el nombre de su creador, muy bueno para listas casi ordenadas. Inestable.
  • Ordenación por Árbol Cartesiano (Cartesian tree sort)
  • Ordenación por torneo (Tournament sort)
  • Ordenación por Ciclos (Cycle sort)
  • Ordenación por Trozos (Strand sort)

Por Mezcla
Dividen los datos en grupos y van mezclándolos de forma ordenada. Su época dorada vino con las cintas magnéticas, y hoy son buenos para ordenación en disco.
  • Ordenación por Mezcla (Merge sort), con su variante del Algoritmo de McCarthy (McCarthy's Algorithm)
  • Ordenación Oscilante (Oscillating merge sort u Oscillating sort)
  • Ordenación Polifásica (Polyphase merge sort o Polyphase sort), y su variante Ordenación por Cascada (Cascade merge sort o Cascade sort)

Por Distribución
Basados en la distribución de los elementos en varios casilleros, de forma similar a como se reparte el correo agrupándolo por códigos postales. Se diferencian principalmente en como se montan los casilleros.
  • Algoritmos de distribución usables
    • Ordenación por Casilleros o por Cajones (Bucket sort o Bin sort), con sus variantes del Algoritmo del Cartero (Postman sort), Ordenación por Huecos de Casillero (Pigeonhole sort), y Ordenación de las Barras o de la Bandera (American flag sort)
    • Ordenación por Raíz o por Base (Radix sort), con variantes de Dígito Mas Significativo (MSD) y Dígito Menos Significativo (LSD)
    • Ordenación por Ráfaga (Burstsort)
    • Ordenación por Particiones (Proxmap sort)
    • Ordenación por Destello (Flashsort)
    • Ordenación por Contero o por Cuentas (Counting sort) Usa una técnica original
  • Algoritmos impracticables
    • Ordenación por Ábaco, por cuentas o por gravedad (Bead sort)

Algoritmos
Concurrentes
Diseñados para el procesamiento en paralelo de los datos por varios procesadores simultáneamente.
  • Algoritmos usables
    • Ordenación de Batcher (Batcher odd–even mergesort). Por el nombre de su creador
    • Ordenación en Parejas (Pairwise sorting network). Versión del Impar-Par para varios procesadors en paralelo.
    • Ordenación del cubo (Cubesort). Bueno para arreglos multidimensionales
    • Ordenación por Muestras (Samplesort)
  • Algoritmos impracticables
    • Ordenación por redes (Sorting network) y su variante Ordenación Bitónica (Bitonic sorter)

Algoritmos
Híbridos
Es habitual usar varios algoritmos diferentes para acelerar el proceso, por ejemplo se suele usar mucho Quicksort que es bueno para listas grandes, las va reduciendo de tamaño cada vez hasta que son pequeñas, momento en que se usa otro algoritmo mas rápido con tamaños pequeños. Estos algoritmos hacen algo similar
  • Ordenación por bloques (Block merge sort)
  • Algoritmo de Tim (Timsort). Por el nombre del primero que lo programó
  • Ordenación Introspectiva (Introsort or introspective sort)
  • Ordenación por Propagación (Spreadsort)
  • Ordenación sin Arrastre (UnShuffle Sort)

Otros
Algoritmos
  • Algoritmos especializados
    • Ordenación Topológica o de Grafos (Topological sort, Topological ordering, Topsort o Toposort)
  • Algoritmos impracticables solo usados para análisis de algoritmos
    • Ordenación de Tortitas, de Panqueques o de panecillos (Pancake sorting)
    • Ordenación del Espagueti (Spaghetti sort)
    • Ordenación del Diablo (EvilSort)
  • Algoritmos para ordenadores cuánticos
    • Ordenación Cuántica (Quantum sort)

Algoritmos
desordenadores
Estos algoritmos desordenan las listas aleatoriamente.
  • Por intercambio
    • Falsa ordenación u Ordenación tonta (Bogosort, Slowsort, Stupid sort, Monkey sort). Impracticable para ordenar pero se usa para desordenar una lista ordenada
  • Por inserción
    • Algoritmo de Fisher-Yates, Ordenación Aleatoria o Algoritmo del Sombrero (Fisher–Yates shuffle, Random Sort o Hat Sort). Solo sirve para desordenar, por lo que es muy usado para barajar las cartas en los juegos. Existe una variante llamada Algoritmo de Sattollo (Sattolo's algorithm) por el nombre de su creador.

viernes, 4 de noviembre de 2016

Programación del Sinclair QL (IV): Programando en SuperBASIC con un emulador



Emulación, ventaja o sacrilegio

No tengo operativo mi querido QL actualmente, por tanto para poder programar usaré un emulador en un PC. Los puristas se echarán las manos a la cabeza, pero es el mejor sistema de mantener operativo un sistema hoy día, y además es mucho mas cómodo de manejar para hacer programas, ya que puedes usar un editor de textos normal y no el limitado sistema de edición del QL.

Todos los emuladores disponen de la posibilidad de simular los Microdrivers con directorios en el ordenador, y eso permite escribir los programas en uno y leerlos en el otro de forma cómoda y sencilla. Con la edad me vuelvo cómodo y prefiero usar un editor de textos para crear los programas, luego los leo en el QL, los corrijo, los ejecuto y los depuro de forma similar a como usaría un compilador, lo que para mi es mas cómodo y el desarrollo lo hago mas rápido.

Actualmente los emuladores de QL están un poco parados, aunque se mantienen varios actualizados periódicamente, pero hace tiempo que no salen novedades importantes. En general son bastante buenos y funcionan de maravilla, los hay gratuitos y de pago, hay versiones para MS.DOS, para Windows, para Linux, para Mac, etc.

Particularmente uso Q-Emulator de Daniele Terdina que me ha funcionado bien cuando he querido recordar mi viejo sistema, actualmente lo ejecuto sin problemas bajo Windows 10. Tiene dos versiones, la básica es gratuita y emula un QL estándar sin ampliaciones, a velocidad aproximada del QL, la avanzada es de pago pero funciona a mas velocidad, soporta mas memoria, mas cantidad de periféricos y otros S.O. Yo uso la versión gratuita que es suficiente para seguir estas entradas. Tiene versión para PC y versión para Mac. Ir a su web.

El emulador QPC desarrollado por Marcel Kilgus es también muy buena opción, era de pago pero en 2014 lo pasó a libre, por lo que os recomiendo probarlo y decidir luego. Funciona bien y rápido, ha sido el que tenia la mejor fama hasta hace poco, su versión QPC1 funciona bajo MS.DOS, mientras que la versión QPC2 trabaja tanto en PC bajo Windows como bajo Mac. Ir a su web.

El emulador mas clásico es QLay, el veterano desarrollado por JawVenema emula perfectamente bajo MS.DOS, Windows 95 y Linux, y tuvo versiones mejoradas desarrolladas por otros usuarios bajo versiones mas avanzadas de Windows denominadas QLay2 y QLay2K. Hoy día están superadas, pero para los amantes del retro mas retro podéis buscar aquí la original y aquí las nuevas.

Bajo Linux nació la versión uQLx desarrollada por Richard Zidlicky y basado en Q-emuLator. Ya no se desarrolla, y tuvo ports para Windows y Mac. Esta es la copia de la web original.

Existen emuladores desarrollados para correr en los otros 68000 de la época, si dispones de uno de esos sistemas puedes buscar QDOS4Amiga o QDOS Classic para Amiga, o QLem para ST.

Si buscáis hardware para usar el QL en otros sistemas, atrás quedaron la tarjeta QXL para ordenador PC con bus ISA, y la ST-QL para Atari ST, difíciles de conseguir, o las placas Q40 y Q60, caras pero muy buenas opciones. Hoy día es la época de las FPGA, ya hay una versión disponible para la FPGA MiST que nos permite disponer de un QL con 640Kb de RAM, y con posibilidad de usar tarjetas de memoria para simular disqueteras o microdrivers, mirar esta entrada en los QLforum ingleses.

jueves, 3 de noviembre de 2016

Programación del Sinclair QL (III): Procedimientos y funciones

Procedimientos y Funciones 

Hay lenguajes como el C en que estas dos estructuras de programación no se diferencian entre sí, mientras que en el superBASIC diferenciamos entre ambas según retornen o no un valor, siendo equivalentes el resto de sus características.

Ambas son estructuras recursivas, algo que en el BASIC estándar no existe como tal, aunque se puede hacer uso del GOSUB para conseguir cierta recursividad, es complejo por carecer de variables locales y las limitaciones del tamaño de la Pila de retorno lo hacen inviable en la mayoría de sistemas. Vemos un ejemplo muy sencillo de ambas:


BASIC     SuperBASIC
100 CLS
110 GOSUB 180
120 LET A=5:GOSUB 230:PRINT A
130 STOP
150 REM ---------------------
160 REM SALUDAR
170 REM ---------------------
180 PRINT "Hola a todos"
190 RETURN
200 REM ---------------------
210 REM INCREMENTAR
220 REM ---------------------
230 LET A=A+1
240 RETURN 
   
100 CLS
110 saluda
120 PRINT incremento(5)
130 REMark ------------------------
140 REMark PROCEDIMIENTO: Saludar
150 REMark ------------------------
160 DEFine PROCedure saluda
170   PRINT "Hola a todos"
180 END DEFine 
190 REMark ------------------------
200 REMark FUNCION:Incrementar
210 REMark ------------------------
220 DEFine FuNction incremento(a)
230   RETurn a+1
240 END DEFine 


En BASIC estándar hay que usar GOSUB para ir a otras partes del programa, para lo que hay que conocer el número de línea en que se encuentra nuestra rutina, y no olvidar el RETURN final pues si no el programa se liará bastante, y como todas las instrucciones son ejecutables, es necesario parar el programa para que no vuelva a ejecutar la primera rutina. En SuperBASIC podemos definir rutinas con nombre, y llamarlas desde cualquier punto del programa por ese nombre, esos trozos no se ejecutarán nunca por si solos, lo que hace que sean mas sencillos de usar.

Además opcionalmente podemos usar todos los parámetros de entrada que deseemos, cuando se llama se deben usar valores o variables para estos parámetros, que son copiados por valor a la rutina, estos parámetros ya no son variables globales sino que se comportan como variables locales en la rutina, por lo que se simplifica mucho el manejo de las variables, en nuestro ejemplo pasamos una variable a la función incremento, esta recibe el valor en una variable que siempre es local, lo que simplifica mucho el diseño de buenas rutinas, que no afecten al resto del programa.

Vemos que el procedimiento no tiene valor alguno de retorno, mientras que la función lo tiene siempre, es importante tenerlo en cuenta, ya que no podemos llamar por ejemplo a PRINT saluda pues no hay valor de retorno, ni llamar directamente a incrementa si no usamos el valor retornado en ningún lugar. En esto se diferencia de C en que se puede no usar el valor de retorno.

El paso de parámetro a nuestras rutinas siempre se realiza por valor, si deseamos modificar el valor de una variable debemos recurrir a usar variables globales para ello. Una función puede retornar solo un valor, pero el tipo de este valor no se define por lo que podemos asignar una variable de cadena a una entera, y esperar que el SuperBASIC nos resuelva la conversión del tipo de la variable. A mi particularmente me gustan los lenguajes fuertemente tipados que me evitan errores, pero en nuestro querido QL no hay otra opción.

Vamos con la recursividad, usando el ejemplo típico del factorial. Ritchie, uno de los creadores del C, dijo que era muy mal ejemplo pues la solución iterativa del factorial es mas rápida y eficiente que la recursiva, y prefería poner como ejemplo la impresión de un número en pantalla, ya entraremos en eso.

BASIC iterativo     SuperBASIC recursivo
100 CLS
110 INPUT "Valor: ";V
120 GOSUB 180
130 PRINT "El factorial es: ";F
140 END
150 REM ---------------------
160 REM FACTORIAL
170 REM ---------------------
180 LET F=1
190 FOR I= V TO 1 STEP -1
200   F=F*I
210 NEXT I
220 RETURN
   
100 CLS
110 INPUT "Valor: ";v
120 PRINT "El factorial es: ";factorial(v)
190 REMark ------------------------
200 REMark FUNCION: Factorial
210 REMark ------------------------
220 DEFine FuNction factorial(a)
230   IF (a = 0) THEN 
231     RETurn 1
232   ELSE 
233     RETurn a * factorial(a-1)
234   END IF 
240 END DEFine


Este ejemplo pone en claro que en caso del BASIC estándar, debemos conocer la dirección del programa en que se encuentra la rutina y los valores de variables que usará, mientras que en SuperBASIC nos vale con saber el nombre de la rutina. Si tenéis curiosidad y ejecutáis en un QL el programa para el valor 200, el resultado tardará un poco pero será 3.060575E614 presentando el resultado en notación científica, si ponéis en marcha un GWBASIC por ejemplo con el DOSBOX, el proceso iterativo dará un error de OverFlow para valores mayores a 33.

Otra característica muy útil es la posibilidad de usar variables locales, lo que evita muchos errores de programación, simplemente definiéndolas como primera instrucción de la función o el procedimiento:

100 b=10 : calculos: PRINT b
200 DEFine PROCedure calculos
210   LOCAL b
220   b = 20
230 END DEFine
 
Si ejecuta este programa tal cual, el valor que imprime es 10, pero si elimina la línea 210, el valor que mostrará será 20. Siempre todos los parámetros que reciba serán tratados como variables locales.

miércoles, 2 de noviembre de 2016

Programación del Sinclair QL (II): Formas estructuradas de intrucciones básicas

Añadido el 30/06/24 ampliaciones y mejoras

Añadido el 19/07/24 ampliación del On Select


Arreglos, mas conocidos como arrays, vectores o matrices

Como ingeniero informático llamaré a las cosas por su nombre. Array es una palabra inglesa que no existe en Español, en su lugar hay que usar la palabra arreglo. Se llama vector a un arreglo de una dimensión, y matriz a un arreglo de dos dimensiones, estos nombres se usan por similitud con las estructuras matemáticas del mismo nombre, pero el nombre que debemos usar debe ser arreglo unidimensional o arreglo bidimensional.


SuperBASIC admite arreglos de una y de dos dimensiones, pero con varias diferencia sobre los del  BASIC estándar.

En superBASIC es posible usar mas de dos dimensiones, no hay más límite que la memoria total disponible, pero en mi larga vida de programador muy pocas veces he usado arreglos de 3 dimensiones (solo recuerdo un caso), podemos pensar que esto se refiere a una colección de matrices, como sería por ejemplo una hoja de cálculo con un conjunto de hojas con sus filas y columnas. Usar mas de 3 es muy complicado de imaginar siquiera su uso, y personalmente nunca lo he necesitado, aunque yo me dedico profesionalmente a la gestión empresarial, quizá en la rama científica sea más factible usarlo.

En un arreglo numérico se incluye siempre el elemento cero, pero por compatibilidad se mantiene que el número sea el máximo elemento del arreglo, de forma que cuando se define un arreglo de 10 elementos estos van del 0 al 10, y no del 1 al 10 como en BASIC estándar, o del 0 al 9 como en C.


Los arreglos de caracteres son siempre de elementos de un solo carácter con la longitud máxima indicada, y guardando en el elemento cero la longitud real de la cadena, de forma que si creamos un arreglo con DIM a$(10), b$(10,5) lo que hacemos es crear en a$ una variable de cadena de diez posiciones máximo, y en b$ creamos ONCE cadenas (recordar que el elemento cero existe) de cinco posiciones máximo. En SuperBASIC el nombre del arreglo es una referencia a su contenido, de esta manera si hacemos a$="Hola" o b$(3)="Otras cadenas", lo que hacemos es usar cadenas de longitud máxima predefinida. 

  • Si hacemos PRINT a$;".";b$(3) en pantalla aparece Hola.Otras sin incluir espacios.
  • Si hacemos PRINT a$(0), en pantalla aparece 4 
  • Si hacemos PRINT a$(1), en pantalla aparece H 

Las cadenas se puede tratar como arreglos unidimensionales, pero en este caso no existe el elemento 0 con su longitud, así podemos hacer LET c$="Hola", si hacemos PRINT c$(1) aparece en pantalla H, si hacemos PRINT c$(0) o si hacemos PRINT c$(5) dará un error en ambos casos.

Podemos usar esto para imprimir todos los elementos de un arreglo numérico o de cadena en un PRINT, o para igualar entre si elementos de arreglos de cadena y usar por ejemplo b$(1)=a$, pero esta asignación no funciona con arreglos numéricos. Podemos dar valor a un arreglo de cadena usando por ejemplo INPUT a$ pero esto no funciona con numéricos.

 

Formas corta y larga de las instrucciones, uso del IF

En el superBASIC hay dos formas de escribir una instrucción, la que denominan forma corta abarca hasta el final de la línea en curso y no necesita cerrarse por tanto, mientras que la forma larga abarca varias líneas. Un ejemplo típico será un pequeño IF que podemos escribir de dos maneras

Forma LARGA     Forma CORTA
130 IF columna > 10 THEN 
140   columna=1
150   linea=linea+1
160 END IF 
   
130 IF columna > 10 THEN columna=1 : linea=linea+1

La forma corta no necesita cierre pues siempre termina en la misma línea en la que está, mientras que la forma larga si necesita siempre cierre. Esto es válido para cualquier instrucción.

En superBASIC existe la forma IF ... THEN ... ELSE ... END IF normal en los BASIC, con la parte ELSE opcional, pero con la particularidad de que el THEN es opcional, por lo que para la forma corta debe reemplazarse por dos puntos, pudiendo escribirse por ejemplo cualquiera de estas dos líneas

IF linea=23 THEN linea=1 : ELSE linea=linea+1
IF linea=23 : linea=1 : ELSE linea=linea+1

Bucle FOR

Los bucles FOR del QL mantienen la compatibilidad con el BASIC estándar pero añaden características adicionales. Veamos un bucle muy básico, presentaremos en pantalla una lista de números del 1 al 20:

BASIC     SuperBASIC
10 FOR i=1 TO 20 
20   PRINT i 
30 NEXT i
   
10 FOR i=1 TO 20
20   PRINT i 
30 END FOR i

Vemos que la única diferencia es como terminamos el bucle, y ya que la instrucción NEXT sigue funcionando en SuperBASIC, aunque añade otro sentido, podemos escribir ambos programas en el QL obteniendo el mismo resultado.

Los bucles en SuperBASIC disponen de dos instrucciones que podemos usar en su interior, la primera es NEXT, que hace que el bucle pase directamente al siguiente ciclo sin pasar por el resto de instrucciones. Si deseamos por ejemplo que el bucle se salte los elementos que sean múltiplos de 3, podemos escribir el código así:

BASIC     SuperBASIC
10 FOR i=1 TO 20 
20   IF (i MOD 3) = 0 THEN GOTO 40
30   PRINT i 
40 NEXT i
   
10 FOR i=1 TO 20
20   IF (i MOD 3) = 0 THEN NEXT i
30   PRINT i 
40 END FOR i

Como vemos ahora se imprimen solo los números 1,2,4,5,7,8,10,11,13,14,16,17,19,20 en ambos casos, pero nos hemos ahorrado el GOTO, y sobre todo, podemos incluir todas las líneas que deseemos en el bucle, sin importarnos cual es la línea que lo termina. Nuevamente podemos teclear ambas variantes del programa en SuperBASIC y funcionan ambas.

La otra instrucción que se puede usar en los bucles es EXIT, que como podemos imaginar hace que nos salgamos del bucle en el lugar en que nos encontremos. Supongamos que deseamos hacer que el bucle termine aleatoriamente, para lo que podemos escribir esto:
BASIC     SuperBASIC
100 CLS
110 FOR i=1 TO 20
120   IF (i MOD 3) = 0 THEN GOTO 160
130   LET a = INT(RND*11)
140   PRINT i, a
150   IF a = 10 THEN GOTO 170
160 NEXT i
170 REM Fin
   
100 CLS
110 FOR i=1 TO 20
120   IF (i MOD 3) = 0 THEN NEXT i
130   a = RND(0 TO 10)
140   PRINT i, a
150   IF a = 10 THEN EXIT i
160 END FOR i

Nuevamente, podemos escribir ambos programas en SuperBASIC con el mismo resultado, pero la segunda forma tiene la ventaja de que no necesitamos conocer donde acaba el bucle para salir de el, ni debe existir siquiera una sentencia posterior. Podemos ver otra característica del lenguaje interesante, la instrucción LET es opcional. Y por último, la instrucción RND es mas completa, si no decimos nada genera un número real entre 0 y 1, pero podemos darle un rango y generará números en ese rango, evitando calcular la parte entera de la multiplicación.

Además el bucle FOR también admite la forma corta en una sola línea, que no requiere añadir al final el End For:

                                FOR i=1 to 10 : PRINT i, : LET j=i*4 : PRINT j


Como característica adicional muy interesante, un bucle FOR puede contener dos formas de marcar los límites, la tradicional del TO-STEP o la posibilidad de indicarle una lista de valores sobre los que iterar, así podemos escribir:


10 FOR i = 1 TO 20 STEP 3
20 FOR i = 1 TO 3, 11 TO 13, 21 TO 23
30 FOR i = 1,3,7,10,20 TO 25,40,50
 

Y el bucle iterará por los valores 1, 4, 7, 10, 13, 16 y 19 en el primer caso, por 1, 2, 3, 11, 12, 13, 21, 22, 23 en sel segundo, y por 1, 3, 7, 10, 20, 21, 22, 23, 24, 25, 40, 50 en el tercero.

Bucle REPeat

En otros lenguajes hay dos construcciones estructuradas para el manejo de los bucles llamadas normalmente:
 
 
                            
WHILE (condición)                 
   ....
   ....
WEND
REPEAT
   .....
   .....
UNTIL (condición)

 
que se diferencian en cuando se verifica que se cumpla la condición, antes de empezar o tras la primera iteración. En SuperBASIC se dispone del REPeat como la otra forma estructurada de iterar en un bucle, que no verifica las condiciones nunca, debemos añadir un EXIT para poder salir del mismo siempre. Cuando tecleamos el programa, solo es necesario escribir la parte en mayúsculas de comando para que el sistema la complete. Este es un bucle básico que cuenta del 1 al 20:

BASIC     SuperBASIC
100 CLS
110 LET I=0
120   LET I=I+1
130   PRINT I
140 IF (I < 20) THEN GOTO 120
   
100 CLS
110 i=0
120 REPeat bucle
130   i = i + 1
140   PRINT i
150   IF i = 20 THEN EXIT bucle
160 END REPeat bucle


Ambas versiones funcionan en SuperBasic nuevamente. Estos bucles son útiles, pero se echa de menos una condición de entrada o de salida que nos ayude un poco mas, aunque podemos poner el EXIT como la primera instrucción del bucle para simular el WHILE, o como la última para el REPEAT. En estos bucles, la instrucción NEXT funciona pero no sirve para seguir iterando sino que es un sinónimo del EXIT para salir de bucle.  

Podemos escribir estas instrucciones en Super-BASIC de la siguiente manera, pero hay que tener en cuenta que el Repeat de otros lenguajes se repite MIENTRAS se cumpla la condición, por tanto para salir en Super-BASIC hay que verificar que NO se cumpla la condición. En caso del Repeat de otros lenguajes, se repite HASTA que se cumpla la condición, por tanto es lo mismo en Super-BASIC.


WHILE (condición)
  ....
  ....
WEND
Repeat bucle
  IF NO_Condición : EXIT bucle
  ....
END REPeat bucle
REPEAT
  ....
  ....
UNTIL (condición)
Repeat bucle
  ....
   IF condición : EXIT bucle
END REPeat bucle

SELect ON

Mientras en BASIC clásico se debe usar ON...GOTO / ON...GOSUB o instrucciones IF, en SuperBASIC se dispone del comando SELect como la forma estructurada de ejecutarlo, con la limitación de que solo se admite para valores numéricos y no de cadena. Veamos un ejemplo en el que decidimos que hace según el valor de una variable

BASIC     SuperBASIC
100 INPUT "Valor: ";VAR
110 ON VAR GOTO 140,160,180
120 PRINT "Valor no válido ";VAR
130 GOTO 200
140 PRINT "Valor 1"
150 GOTO 200
160 PRINT "Valor 2"
170 GOTO 200
180 PRINT "Valor 3"
190 GOTO 200
200 REM fin
   
100 INPUT "Valor: ";valor
110 SELect ON valor
120   ON valor=1
130     PRINT "Valor 1"
140   ON valor=2
150     PRINT "Valor 2"
160   ON valor=3
170     PRINT "Valor 3"
180   ON valor=REMAINDER 
190     PRINT "Valor no válido: ";valor
200 END SELect 

Ambas versiones funcionan en SuperBasic nuevamente, aunque la primera dará un error si el valor introducido no es 1, 2 o 3 dependiendo de la versión del BASIC que usemos, y lo dará en SuperBASIC. El uso de ON en la primera línea es opcional, y el de REMAINDER nos posibilita tratar cualquier caso no contemplado entre los seleccionados. Además, el sistema puede tratar con listas de valores, por tanto podemos poner por ejemplo:

110 SELect valor
120   ON valor=1,3,5
130     PRINT "Valor impar"
140   ON valor=2,4,6
150     PRINT "Valor par"
180   ON valor=REMAINDER 
190     PRINT "Valor fuera de rango"
200 END SELect 


Es posible compactar mas todavía las líneas, eliminando el ON (aunque por costumbre de otros lenguajes lo seguiré usando) y usar la forma corta de las instrucciones:

110 SELect valor
120   =1,3,5     : PRINT "Valor impar"
130   =2,4,6     : PRINT "Valor par"
140   =REMAINDER : PRINT "Valor fuera de rango"
150 END SELect 

Se puede usar la forma corta en una sola línea, lo que usando rangos nos permite reemplazar un IF por un SELect

110 SELect valor=1 TO 5, 11 TO 15 : PRINT "Valor fuera de rango"
Importante el que en el select el valor de control debe ser numérico, NO ES POSIBLE USAR VALORES DE CADENA, así NO podemos hacer un select de esta manera, da un error:

110 SELect valor$
120   ON valor$="1" : PRINT "UNO"
130   ON valor$="2" : PRINT "DOS"
140   ON valor$=REMAINDER : PRINT "OTRO"
150 END SELect