Elección de estructuras de datos

Anuncio
Elección de estructuras
Pablo Ariel Heiber
Departamento de Computación,
Facultad de Ciencias Exactas y Naturales,
Universidad de Buenos Aires
Algoritmos y Estructuras de Datos 2
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.
Esquema de solución
Lo aquı́ escrito no es una resolución completa y satisfactoria del ejercicio.
1
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 al nodo con todos los datos mediante el iterador (O(1)).
2. Busco en porDNI (O(log n)) y luego accedo al nodo con todos 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 nodo con los datos de la persona, dejando pCumple en NULL.
p ← iterador al nodo 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 nodo con p como contenido
p → pCumple ← iterador al nodo creado en el paso anterior
El primer paso es O(`) porque es el tamaño del nodo. Las actualizaciones de los diccionarios son O(`) y O(log n)
en ese orden. Todos los otros pasos son O(1).
7.
p ← buscar la definición del nombre en porNombre
Borrar el nombre de porNombre
Borrar p → dni de porDNI
2
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 borra el nodo 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 nodo 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 nodos 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. Esta solución es análoga a lo hecho con el ABB en el segundo ejemplo de la clase de introducción al
diseño. Repasarlo antes de seguir leyendo.
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 dicho 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. No reproduciremos aquı́ el detalle, si no quedó claro de la clase, consultar.
3
Descargar