Elección de estructuras - Departamento de Computación

Anuncio
Elección de estructuras
Algoritmos y Estructuras de Datos 2
Departamento de Computación,
Facultad de Ciencias Exactas y Naturales,
Universidad de Buenos Aires
15 de mayo de 2014
Elección y justificación de estructuras
Enunciado
Vamos a diseñar una base de datos de personas, de las cuales se guardan los siguientes datos:
Nombre (string, único)
DNI (entero, único)
Dı́a de Nacimiento (entero, 1 a 365, no hay bisiestos)
Año de Nacimiento (entero, 1 a M )
Aquı́ M es un parámetro que se decide al inaugurar la base de datos (pero no ahora).
Además, se debe dar la fecha actual como 2 enteros: Número de año y número de dı́a, con los mismos rangos que
para las personas.
Se requiere hacer las siguientes consultas, con los requerimientos de complejidad de peor caso indicados para cada
una. Llamamos n a la cantidad de personas que contiene la base de datos y ` al largo del nombre de la persona en
cuestión, cuando corresponde.
1. Dado un nombre de persona, encontrar todos sus datos O(`).
2. Dado el DNI de una persona, encontrar todos sus datos O(log n).
3. Dado el DNI de una persona, dar su edad actual en cantidad de años enteros O(log n).
4. Dada una cantidad de años, decir cuántas personas tienen exactamente esa cantidad en este momento O(1).
5. Decir cuantas personas estan en edad jubilatoria (es decir, tienen al menos 65 años cumplidos) O(1).
6. Agregar una persona nueva O(` + log n).
7. Dado un nombre, borrar a la persona que tiene ese nombre O(` + log n).
8. Consultar la fecha actual O(1).
9. Pasar de dı́a O(m) donde m es la cantidad de personas que cumplen años en el dı́a al que se llega luego de pasar.
1
Esquema de solución
Disclaimer: Lo que sigue NO es una resolución completa y detallada. Sin embargo, deberı́a ser suficiente para
transmitir la idea general. Ante cualquier duda, consultar.
Estructura
todos Lista enlazada de tuplas con nombre, DNI, dı́a y año de nacimiento de cada persona, en el orden en que se
ingresan. Para cada uno además agrego un iterador que la enlaza a su entrada correspondiente en porCumple.
porNombre Diccionario de nombre en iterador a todos, implementado sobre Trie.
porDNI Diccionario de DNI en iterador a todos, implementado sobre árbol balanceado (AVL).
cantPorEdad Arreglo de M posiciones, en la posición i guardamos cuántas personas tienen i años actualmente.
porCumple Arreglo de 365 posiciones, en cada posición tenemos una lista de iteradores a todos con las personas
que cumplen años ese dı́a.
dı́a Entero con el dı́a actual.
año Entero con el año actual.
jubilados Entero con la cantidad de jubilados.
base se representa con estr, donde
estr es tupla htodos: lista(persona),
porNombre: diccTrie(string, itLista(persona))
porDNI : diccAVL(nat, itLista(persona))
cantPorEdad : arreglo dimensionable(nat)
porCumple: arreglo dimensionable(lista(itLista(persona)))
dı́a: nat
año: nat
jubilados: nati
y
persona es tupla hnombre: string, dni : nat, dı́a: nat, año: nat, pCumple:itLista(itLista(persona))i
Idea de los algoritmos
1. Busco en porNombre (O(`)) y luego accedo a los datos mediante el iterador (O(1)).
2. Busco en porDNI (O(log n)) y luego accedo a los datos mediante el iterador (O(1)).
3. Accedo a la fecha actual y a los datos de la persona mediante las operaciones anteriores (O(1) + O(log n)) y
luego realizo una cuenta aritmética para ver la edad (O(1)).
4. Accedo al arreglo cantPorEdad en O(1) y tengo exactamente el dato.
5. Trivial O(1).
6.
agregar atrás de todos un nuevo elemento con los datos de la persona.
p ← iterador al elemento creado en el paso anterior
definir en porNombre el nombre de la persona con p como definición
definir en porDNI el DNI de la persona con p como definición
x ← año de nacimiento × 365 + dı́a de nacimiento
y ← edad actual de la persona
cantPorEdad [y] ← cantPorEdad [y] + 1
if y ≥ 65 then jubilados ← jubilados + 1 end if
agregar atrás de porCumple[x] un nuevo elemento con p como contenido
p → pCumple ← iterador al elemento creado en el paso anterior
El primer paso es O(`). Las actualizaciones de los diccionarios son O(`) y O(log n) en ese orden. Todos los demás
pasos son O(1).
7.
p ← buscar la definición del nombre en porNombre
2
Borrar el nombre de porNombre
Borrar p → dni de porDNI
y ← edad actual de ∗p
cantPorEdad [y] ← cantPorEdad [y] - 1
if y ≥ 65 then jubilados ← jubilados + 1 end if
Borrar p → pCumple de porCumple[p → dı́a]
Borrar p de todos
Los dos primeros pasos son O(`), el tercero es O(log n) y el resto son O(1).
8. Trivial O(1).
9.
actualizar fecha actual con operaciones aritméticas
x ← dı́a
for p en porCumple[x] do
y ← edad actual de ∗p
cantPorEdad [y − 1] ← cantPorEdad [y − 1] - 1
cantPorEdad [y] ← cantPorEdad [y] + 1
if y = 65 then jubilados ← jubilados + 1 end if
end for
Todas las operaciones son O(1). El for se ejecuta m veces e iterar una lista se puede hacer en tiempo lineal (sin
overhead), por lo tanto en total queda O(m).
Adicionales
Ejercicio 1. Agregar una operación para contar la cantidad de tocayos de un prefijo dado. La cantidad de tocayos es
la cantidad de personas cuyo nombre empieza exactamente con el prefijo recibido como parámetro. La operación debe
ser O(`) donde ` es la longitud del prefijo.
Solución 1. Se soluciona pidiendole al diccionario que ya tengo sobre trie (porNombre) que además me diga la
cantidad de claves que son continuación de un prefijo dado. Eso se puede solucionar agregandole a cada nodo del trie
la cantidad de palabras que tiene por debajo. Dicho campo es facil de actualizar sin costo adicional tanto al insertar
como al borrar del trie, ya que solo se actualiza la rama que se inserta/borra.
Ejercicio 2. Se quiere iterar las cantidades de años en las que hay al menos una persona con esa edad. Hacerlo
primero en O(M ) con la estructura ya propuesta en la sección anterior, y luego modificar la estructura para que sea
O(x), dónde M es el definido anteriormente y x es el número de edades para las cuáles hay una persona con esa edad.
Solución 2. Para hacerlo en O(M ) simplemente iteramos i entre 0 y M y nos fijamos si cantPorEdad [i] es mayor a
0, y en ese caso la reportamos, y si es 0 la salteamos.
Para poder hacerlo en O(x) tenemos que agregar una nueva lista enlazada con las posiciones de cantPorEdad [i] no
vacı́as, pero en cualquier orden. Cada vez que se suma a una posición de cantPorEdad que estaba en 0, se inserta en
la nueva lista. Cada vez que se borra algo de una posición de cantPorEdad, se elimina el elemento correspondiente de
la lista. Para que esta última operación se pueda hacer en O(1), se debe guardar un arreglo paralelo a cantPorEdad,
del mismo tamaño, que tenga el iterador al elemento que le corresponde de la nueva lista, para cada posición que no
sea 0.
Notar que lo que hicimos aca es representar un conjunto de enteros acotados entre 0 y M con un vector de booleanos,
una lista enlazada y un vector de iteradores a elementos de esa lista. De esta forma conseguimos inserción, consulta
y borrado del conjunto en O(1), pero adicionalmente podemos iterarlo en tiempo lineal en la cantidad de elementos
del conjunto, en lugar de tomar tiempo lineal en el tamaño del universo, que potencialmente es mucho mayor (en el
ejemplo, x ≤ M , por lo tanto O(x) es posiblemente mejor que O(M )).
Queda como tarea ver que efectivamente es mejor (es decir, que hay familias de ejemplos que muestran que
O(M ) 6= O(x)).
Ejercicio 3. Dado un DNI, encontrar cuantas personas tienen un DNI mayor o igual.
Solución 3. En cada nodo del AVL del diccionario porDNI se agrega un entero con la cantidad de nodos que tiene
a la derecha. Queda como tarea ver que eso se puede mantener en las operaciones de inserción y borrado de entradas
sin costo adicional (es fácil).
Una vez agregado ese campo, la solución al problema se encuentra bajando por el árbol como si estuviera buscando
el valor recibido como parámetro y sumando el valor del nuevo campo + 1 cada vez que bajamos hacia la izquierda.
3
Descargar