Arreglos Paralelos en Python: Ejemplo y Ordenamiento
Arreglos Paralelos en Python: Ejemplo y Ordenamiento
Ficha 17
sueldos
1100 2100 1300 750 800
0 1 2 3 4
Como se está pidiendo almacenar los datos en dos arreglos, sólo se debe cuidar que el
nombre y el importe del sueldo de una misma persona aparezcan en ambos arreglos en
casillas homólogas. Así, en nuestro ejemplo, la persona representada por la casilla 0 del
arreglo Nombres (o sea, nombres[0]) se llama "Juan", y el sueldo que percibe aparece en el
arreglo sueldos también en la casilla con índice 0 (o sea, sueldos[0] ) y es el valor 1100.
Cuando dos o más arreglos se usan de esta forma, se los suele designar como arreglos
correspondientes o paralelos [1].
El manejo de ambos arreglos es simple: sólo debe recordar el programador que hay que usar
el mismo índice en ambos arreglos para entrar a la información de una misma persona. El
siguiente esquema, muestra por pantalla el nombre y el sueldo de la persona en la casilla 2:
print('Nombre:', nombres[2]) # Alejandro
print('Sueldo:', sueldos[2]) # 1300
El siguiente problema servirá para aclarar la forma general de manejar arreglos paralelos:
Problema 40.) Desarrollar un programa que permita cargar tres arreglos con n nombres de
personas, sus edades y los sueldos que ganan. Luego de realizar la carga de todos los datos,
mostrar los nombres de las personas mayores de 18 años que ganan menos de 10000 pesos,
pero de forma que el listado salga ordenado en forma alfabética.
Discusión y solución: El programa usará una función read() cuyo objetivo es cargar por
teclado los tres arreglos. Cuando se trabaja con arreglos paralelos, en los que la componente
en la casilla i de cada arreglo contiene información relacionada al mismo objeto o entidad
del problema, es conveniente que la carga por teclado se haga de forma de barrer los tres
arreglos al mismo tiempo: se carga primero la componente 0 de todos los arreglos, luego la
componente 1 de todos ellos, y así sucesivamente. De esta forma, quien hace la carga de
datos ve facilitada su tarea pues carga los datos de una persona de una sola vez, sin tener
que volver atrás luego de cargar los nombres o las edades para cargar los sueldos (lo que
sería realmente incómodo). La función read() de nuestro programa sería la siguiente:
def read(nombres, edades, sueldos):
n = len(nombres)
for i in range(n):
nombres[i] = input('Nombre[' + str(i) + ']: ')
edades[i] = int(input('Edad: '))
sueldos[i] = int(input('Sueldo: '))
print()
Finalmente, la función display() usa un ciclo for para analizar cada componente del arreglo
de edades y del arreglo de sueldos. Si se encuentra que la edad en la posición i es mayor a 18
y al mismo tiempo el sueldo es menor a 10000, se muestran los tres datos que corresponden
a esa persona, que se encuentra en cada arreglo en la misma posición i. La función podría ser
la siguiente:
def display(nombres, edades, sueldos):
n = len(nombres)
print('ayores de 18 que ganan menos de 10000 pesos:')
for i in range(n):
if edades[i] > 18 and sueldos[i] < 10000:
print('Nombre:', nombres[i], 'Edad:', edades[i], 'Sueldo:', sueldos[i])
def validate(inf):
n = inf
while n <= inf:
n = int(input('Cantidad de elementos (> a ' + str(inf) + ' por favor): '))
if n <= inf:
print('Error: se pidio mayor a', inf, '... cargue de nuevo...')
return n
def test():
# cargar cantidad de personas...
n = validate(0)
# script principal...
if __name__ == '__main__':
test()
La instrucción anterior crea un arreglo de n = 6 componentes con valor inicial None. La idea
es que en cada uno se almacene luego una referencia a un registro de tipo Estudiante,
aunque todavía no se han creado esos registros. El aspecto que tendría este arreglo en este
momento en memoria sería como el que se muestra a continuación:
0 1 2 3 4 5
lo cual haría que cada casilla del arreglo contenga ahora una referencia a un registro de tipo
Estudiante, pero vacío (sin campos). Los valores None ya no están en cada casillero, y fueron
reemplazados por las direcciones de cada uno de lo registros. Una mejor forma de proceder,
podría ser que no sólo se creen los registros vacíos, sino que también se proceda a cargar
por teclado los datos a almacenar en cada uno, y luego crear los campos de esos registros
con la función init() que hemos mostrado más arriba. La secuencia que sigue muestra la
forma de hacerlo:
for i in range(n):
leg = int(input('Legajo[' + str(i) + ']: '))
nom = input('Nombre: ')
pro = float(input('Promedio: '))
print()
v[i] = Estudiante()
init(v[i], leg, nom, pro)
Está claro que puede lograrse el mismo resultado sin usar la función init(), creando y
asignando directamente los campos en cada casillero. Lo anterior es equivalente a:
for i in range(n):
v[i].legajo = int(input('Legajo[' + str(i) + ']: '))
v[i].nombre = input('Nombre: ')
v[i].promedio = float(input('Promedio: '))
print()
Como cada casillero v[i] del arreglo es un registro de tipo Estudiante, entonces el acceso a
cada campo de v[i] se hace con el operador punto combinado con el propio identificador
v[i]: v[i].legajo permite acceder al campo legajo del registro ubicado en v[i], y en forma
similar ocurre para el resto de los campos.
Las dos secuencias anteriores crean diferentes registros y los asignan en distintas casillas del
arreglo, que podría verse ahora como sigue (suponga que los datos cargados por teclado
fueron efectivamente los que se ven en la figura):
0 1 2 3 4 5
Sólo cuando cada registro haya sido creado se puede recorrer el arreglo y aplicar a cada uno
alguna tarea o proceso. Recuerde que v[i] es la referencia que apunta al registro en la casilla
i, y por lo tanto algo como v[i].legajo accede al número de legajo almacenado en el registro
v[i]. Pero esto sólo es válido si v[i] es efectivamente un registro de tipo Estudiante: si se
intenta acceder a un campo desde una referencia que no apunta a un registro de ese tipo, el
programa se interrumpirá lanzando un mensaje de error por campo inexistente.
Finamente, el arreglo se usa como se usaría a cualquier arreglo: si se sabe qué clase de
registros se almacenó dentro de él, se podrá cargarlos por teclado, visualizarlos, ordenarlos,
efectuar búsquedas, etc. Analice el siguiente ejercicio que completa el ejemplo que hemos
venido utilizando:
Problema 41.) Se desea almacenar en un arreglo la información de los n estudiantes que se
registraron para participar de un curso de programación. Por cada estudiante se tiene su
número de legajo, su nombre y su promedio en la carrera que cursa. Participarán del curso
los estudiantes cuyo promedio sea mayor o igual a x, siendo x un valor cargado por teclado.
Muestre los datos de los estudiantes que participarán del curso, pero ordenados de menor a
mayor por número de legajo.
Discusión y solución: El programa que resuelve este problema es el modelo test02.py
(incluido en el proyecto [F17] Arreglos – Registros – Ordenamiento que viene con esta Ficha),
y es el siguiente:
__author__ = 'Cátedra de AED'
class Estudiante:
pass
def write(est):
print('Legajo:', est.legajo, end=' ')
print('- Nombre:', est.nombre, end=' ')
print('- Promedio:', est.promedio)
def validate(inf):
n = inf
while n <= inf:
n = int(input('Cantidad de elementos (> a ' + str(inf) + ' por favor): '))
if n <= inf:
print('Error: se pidio mayor a', inf, '... cargue de nuevo...')
return n
def read(estudiantes):
n = len(estudiantes)
for i in range(n):
leg = int(input('Legajo[' + str(i) + ']: '))
nom = input('Nombre: ')
pro = float(input('Promedio: '))
print()
estudiantes[i] = Estudiante()
init(estudiantes[i], leg, nom, pro)
def sort(estudiantes):
n = len(estudiantes)
for i in range(n-1):
for j in range(i+1, n):
if estudiantes[i].legajo > estudiantes[j].legajo:
estudiantes[i], estudiantes[j] = estudiantes[j], estudiantes[i]
def test():
# cargar cantidad de estudiantes...
n = validate(0)
# script principal...
if __name__ == '__main__':
test()
La función test() define un arreglo referenciado por la variable estudiantes, que contendrá
referencias a registros de tipo Estudiante (tipo que fue definido al inicio del módulo). Este
arreglo se usará para almacenar los datos de todos los estudiantes que se hayan inscripto
para hacer el curso. La misma función test() carga por teclado la cantidad total de alumnos n,
y en base a ese valor crea el arreglo con n casilleros valiendo None. Recuerde que no se
debería comenzar a usar el arreglo hasta crear o asignar registros de tipo Estudiante en cada
componente (cosa que se hace en este caso la función read().
La función sort() ordena el arreglo de acuerdo a los legajos de los estudiantes, de menor a
mayor. Lo más relevante de esta función es la condición para determinar si debe hacerse un
intercambio o no:
if estudiantes[i].legajo > estudiantes[i].legajo:
estudiantes[i], estudiantes[j] = estudiantes[j], estudiantes[i]
Puede verse que en la condición se accede al campo legajo de cada registro, y se comparan
sus valores: ahora cada casillero contiene un registro y no un valor int, por lo que comparar
directamente estudiantes[i] con estudiantes[j] carece de sentido (¡y provocaría un error!).
Además, si la condición es verdadera puede verse que se procede a intercambiar
directamente las referencias a los registros, sin tener que hacer ese intercambio campo por
campo.
Finalmente, la función display() recorre el contenido del arreglo mostrando por consola los
datos de los alumnos cuyo promedio sea mayor o igual al valor x que se tomó como
parámetro. Como el arreglo está ordenado por legajo, los datos de los estudiantes que se
muestren saldrán a su vez ordenados por legajo.
En principio, los algoritmos clasificados como Simples o Directos son algoritmos sencillos e
intuitivos, pero de mal rendimiento si la cantidad n de elementos del arreglo es grande o
muy grande, o incluso si lo que se desea es ordenar un archivo en disco. Aquí, mal
rendimiento por ahora significa "demasiada demora esperada". En cambio, los métodos
presentados como Compuestos o Mejorados tienen un muy buen rendimiento en
comparación con los simples, y se aconsejan toda vez que el arreglo sea muy grande o se
quiera ordenar un conjunto en memoria externa. Si el tamaño n del arreglo es pequeño (por
ejemplo, no más de 100 o 150 elementos), los métodos simples siguen siendo una buena
elección por cuanto su escasa eficiencia no es notable con conjuntos pequeños. Note que la
idea final, es que cada algoritmo compuesto mostrado en esta tabla representa en realidad
una mejora en el planteo del algoritmo simple de la misma fila, y por ello se los llama
"algoritmos mejorados". Así, el algoritmo Quicksort es un replanteo del algoritmo de
intercambio directo (más conocido como ordenamiento de burbuja) para eliminar los
elementos que lo hacen ineficiente. Paradójicamente, el ordenamiento de burbuja o
bubblesort es en general el de peor rendimiento para ordenar un arreglo, pero ha dado lugar
al Quicksort, que en general es el de mejor rendimiento promedio conocido.
Directa. Para una descripción del algoritmo de Selección Directa, revise la Ficha 16, página
340. Más adelante, en una Ficha posterior, mostraremos un análisis de rendimiento formal
basado en calcular la cantidad de comparaciones que estos algoritmos realizan.
En todos los casos, suponemos un arreglo v de n elementos, y también suponemos que se
pretende ordenar de menor a mayor. Hemos incluido un modelo llamado test03.py en el
proyecto [F17] Arreglos – Registros – Ordenamiento, que acompaña a esta Ficha. En el
mismo proyecto se incluye el módulo ordenamiento.py, que contiene todas las funciones
que implementan estos algoritmos.
n=4
2 3 5 7 ordenado
0 1 2 3
Puede verse que si el arreglo tiene n elementos, serán necesarias a lo sumo n-1
pasadas para terminar de ordenarlo en el peor caso, y que en cada pasada se hace
cada vez una comparación menos que en la anterior. Otro hecho notable es que el
arreglo podría quedar ordenado antes de la última pasada. Por ejemplo, si el arreglo
original hubiese sido [2 – 3 – 7 – 5], entonces en la primera pasada el 7 cambiaría
con el 5 y dejaría al arreglo ordenado. Para detectar ese tipo de situaciones, en el
algoritmo se agrega una variable a modo de bandera de corte: si se detecta que en
una pasada no hubo ningún intercambio, el ciclo que controla la cantidad de pasadas
se interrumpe antes de llegar a la pasada n-1 y el ordenamiento se da por concluido.
Se ha denominado ordenamiento de burbuja porque los elementos parecen
primera pasada: El grupo ordenado contiene sólo a v[0] (con valor 7). Se inserta el
3 en ese grupo. Como el 3 es menor al 7, se intercambian y el grupo pasa a tener
7 3 5 2 ahora dos elementos, ordenados.
0 1 2 3
2 3 5 7 ordenado
0 1 2 3
La función que sigue (ver módulo ordenamiento.py del proyecto [F17] Arreglos –
Registros – Ordenamiento) lo implementa:
def insertion_sort(v):
n = len(v)
for j in range(1, n):
y = v[j]
k = j - 1
while k >= 0 and y < v[k]:
v[k+1] = v[k]
k -= 1
v[k+1] = y
0 1 4 3 2 5 8 10 7 6 9
0 1 2 3 4 5 6 7 8 9 10
0 1 4 3 2 5 6 9 7 8 10
0 1 2 3 4 5 6 7 8 9 10
0 1 2 3 4 5 6 7 8 9 10 Arreglo ordenado...
0 1 2 3 4 5 6 7 8 9 10
while h > 0:
for j in range(h, n):
y = v[j]
k = j - h
while k >= 0 and y < v[k]:
v[k+h] = v[k]
k -= h
v[k+h] = y
h //= 3
A modo de cierre, y para tener una visión completa, compacta y comparativa de estos
algoritmos, el proyecto [F17] Arreglos – Registros – Ordenamiento que acompaña a esta
Ficha, contiene un modelo test03.py en el cual se puede seleccionar por medio un menú la
técnica de ordenamiento a emplear para ordenar un vector. En ese programa hemos
aplicado los seis algoritmos citados en la tabla de la Figura 4, incluyendo el Heapsort y el
Quicksort. El programa además, calcula el tiempo que cada algoritmo demora en terminar de
ordenar el vector. Pruebe con valores grandes de n… y saque sus propias conclusiones. El
programa completo es el siguiente:
__author__ = 'Cátedra de AED'
import time
import ordenamiento
def validate(inf):
n = inf
while n <= inf:
n = int(input('Cantidad de elementos (> a ' + str(inf) + ' por favor): '))
if n <= inf:
print('Error: se pidio mayor a', inf, '... cargue de nuevo...')
return n
def test():
op = 0
while op != 9:
if op == 1:
print()
n = validate(0)
v = n * [0]
ordenamiento.generate_random(v)
print('Hecho... arreglo creado...')
print()
elif op == 2:
print()
if ordenamiento.check(v):
print('Está ordenado...')
else:
print('No está ordenado...')
print()
elif op == 3:
print()
print('Ordenamiento: Selección Directa.')
t1 = time.clock()
ordenamiento.selection_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
elif op == 4:
print()
print('Ordenamiento: Intercambio Directo (Burbuja).')
t1 = time.clock()
ordenamiento.bubble_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
elif op == 5:
print()
print('Ordenamiento: Inserción Directa.')
t1 = time.clock()
ordenamiento.insertion_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
elif op == 6:
print()
print('Ordenamiento: Heap Sort.')
t1 = time.clock()
ordenamiento.heap_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
elif op == 7:
print()
print('Ordenamiento: Quick Sort.')
t1 = time.clock()
ordenamiento.quick_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
elif op == 8:
print()
print('Ordenamiento: Shell Sort.')
t1 = time.clock()
ordenamiento.shell_sort(v)
t2 = time.clock()
tt = t2 - t1
print('Hecho... Tiempo total insumido:', tt, 'segundos')
print()
# script principal...
if __name__ == '__main__':
test()
El método de ordenamiento por Selección Directa realiza n-1 pasadas, y el objetivo de cada
una es seleccionar el menor de los elementos que sigan sin ordenar en el arreglo y
trasladarlo a la casilla designada como pivot. El problema es que la búsqueda del menor en
cada pasada se basa en un proceso secuencial, y exige demasiadas comparaciones. En 1964,
un estudiante de Ciencias de la Computación, llamado J. Williams, publicó una mejora para
el algoritmo de Selección Directa en Communicatons of the ACM, y llamó al mismo
Ordenamiento de Montículos o Heapsort (en realidad, debería traducirse como
Ordenamiento de Grupos de Ordenamiento, pero queda redundante...) Ese algoritmo reduce
de manera drástica el tiempo de búsqueda o selección del menor en cada pasada, usando
una estructura de datos conocida como grupo de ordenamiento (que no es otra cosa que
una cola de prioridades, pero optimizada en velocidad: los elementos se insertan
rápidamente, ordenados de alguna forma, pero cuando se extrae alguno, se extrae el menor
[o el mayor, según las necesidades]) Igual que antes, al seleccionar el menor (o el mayor) se
lo lleva a su lugar final del arreglo. Luego se extrae el siguiente menor (o mayor), se lo
reubica, y así se prosigue hasta terminar de ordenar [4].
En esencia, un grupo de ordenamiento es un árbol binario casi completo (todos los nodos
hasta el anteúltimo nivel tienen dos hijos, a excepción de los nodos más a la derecha en ese
nivel que podrían no tener los dos hijos) en el cual el valor de cada nodo es mayor que el
valor de sus dos hijos. La idea es comenzar con todos los elementos del arreglo, y formar un
árbol de este tipo, pero dentro del mismo arreglo (de otro modo, se usaría demasiado
espacio adicional para ordenar...). Es simple implementar un árbol completo o casi completo
en un arreglo: se ubica la raíz en el casillero v[0], y luego se hace que para nodo en el
casillero v[i], su hijo izquierdo vaya en v[2*i + 1] y su hijo derecho v[2*i + 2]. Así, el algoritmo
Heapsort primero lanza una fase de armado del grupo, en la cual los elementos del arreglo
se reubican para simular el árbol. La Figura 8 muestra un esquema general del proceso de
armado del grupo inicial para un pequeño arreglo de cuatro elementos.
Figura 8: Funcionamiento general del Algoritmo Heapsort - Fase 1: Armado del Grupo Inicial.
primera pasada: Se toma el valor en v[0], y se supone que es la raíz del grupo
de ordenamiento. Luego se toma v[1], y se lo considera como hijo izquierdo de
3 7 5 8 v[0]. Se pretende armar un grupo en el cual cada nodo sea mayor que sus dos
hijos, por lo se intercambia el 7 con el 3 para que el 7 quede como raíz. Los
0 1 2 3 elementos en verde, pertenecen ya al grupo.
7 v[0]
3 v[1]
0 1 2 3
7 v[0]
v[1] 3 5 v[2]
8 v[0]
v[1] 7 5 v[2]
3 v[3]
8 7 5 3 Grupo armado...
0 1 2 3
Una vez armado el grupo inicial, comienza la segunda fase del algoritmo: Se toma la raiz del
árbol (que es el mayor del grupo), y se lleva al último casillero del arreglo (donde quedará ya
ordenado). El valor que estaba en el último casillero se reinserta en el árbol (que ahora tiene
un elemento menos), y se repite este mecanismo, llevando el nuevo mayor al anteúltimo
casillero. Así se continúa, hasta que el árbol quede con sólo un elemento, que ya estará
ordenado en su posición correcta del arreglo. Gráficamente, la segunda fase se desarrolla
como se muestra en la Figura 9.
Figura 9: Funcionamiento general del Algoritmo Heapsort - Fase 2: Ordenamiento Final.
primer paso: Se toma la raíz del árbol, el 8, y se almacena su valor en v[3]. Con
el 3 que estaba en v[3] y ahora se intercambió con la raíz, se arma otra vez el
árbol, considerando ahora sólo los casilleros v[0], v[1] y v[2]. El 3 no puede
v[0] quedar como raíz: se intercambia con el 7, que es el mayor de sus hijos. Los
8 elementos pintados de azul, ya no están en el árbol: están ya ordenados en el
arreglo.
v[1] 7 5 v[2]
3 7 5 8
0 1 2 3
3 v[3]
7 v[0]
7 3 5 8
0 1 2 3
v[1] 3 5 v[2]
5 v[0]
5 3 7 8
0 1 2 3
v[1] 3
tercer paso: Se toma nueva la raíz, el 5, y se almacena su valor en v[1]. El 3 que
estaba en v[1] se intercambia con la raíz y el arreglo queda ordenado.
3 5 7 8 Arreglo ordenado...
0 1 2 3
for i in range(n):
e = v[i]
s = i
f = (s - 1) // 2
while s > 0 and v[f] < e:
v[s] = v[f]
s = f
f = (s - 1) // 2
v[s] = e
Créditos
El contenido general de esta Ficha de Estudio fue desarrollado por el Ing. Valerio Frittelli para ser
utilizada como material de consulta general en el cursado de la asignatura Algoritmos y Estructuras
de Datos – Carrera de Ingeniería en Sistemas de Información – UTN Córdoba, en el ciclo lectivo 2015.
Actuaron como revisores (indicando posibles errores, sugerencias de agregados de contenidos y
ejercicios, sugerencias de cambios de enfoque en alguna explicación, etc.) en general todos los
profesores de la citada asignatura como miembros de la Cátedra, y en especial las ing. Cynthia Corso
y Karina Ligorria, que realizaron aportes de contenidos, propuestas de ejercicios y sus soluciones,
sugerencias de estilo de programación, y planteo de enunciados de problemas y actividades
prácticas, entre otros elementos.
Bibliografía
[1] V. Frittelli, Algoritmos y Estructuras de Datos, Córdoba: Universitas, 2001.
[2] Python Software Foundation, "Python Documentation," 2015. [Online]. Available:
https://docs.python.org/3/. [Accessed 24 February 2015].
[3] M. Pilgrim, "Dive Into Python - Python from novice to pro," 2004. [Online]. Available:
http://www.diveintopython.net/toc/index.html. [Accessed 6 March 2015].
[4] Y. Langsam, M. Augenstein and A. Tenenbaum, Estructura de Datos con C y C++, México:
Prentice Hall, 1997.
[5] R. Sedgewick, Algoritmos en C++, Reading: Addison Wesley - Díaz de Santos, 1995.