Está en la página 1de 6

Algoritmos y estructuras de datos

• ¿Cómo puede encontrar algoritmos eficientes?


- Es difícil de inventar otros nuevos
- Más fácil es reducir los problemas a las soluciones conocidas
• Comprender la complejidad inherente del problema
• Piense en cómo dividir un problema en subproblemas
• Relacionar subproblemas a otros problemas para los cuales ya existen algoritmos
eficientes

Algoritmos de búsqueda
• Algoritmo de búsqueda: método para encontrar un elemento o grupo de elementos
con propiedades específicas dentro de una colección de elementos.
• La colección es llamada espacio de búsqueda
• Ejemplos sierra – el hallar la raíz cuadrada como un problema de búsqueda
- Enumeración exhaustiva
- Búsqueda de bisección
- Newton- Raphson

La búsqueda lineal e indirección


• Método de Búsqueda simple

def busqueda(L, e):


for i in range(len(L)):
if L[i] == e:
return True
return False

• Complejidad?
- Si el elemento no está en la lista, de necesita O( len(L)) pruebas
- Así que la mejor búsqueda lineal es longitud de L

La búsqueda lineal y indirección


• ¿Por qué "el mejor lineal "?
- Se asume que cada prueba en bucle se puede hacer en tiempo constante
- ¿Pero Python recuperar el i-ésimo elemento de una lista en tiempo constante?

Indirección
• Caso sencillo: lista de enteros
- Cada elemento es del mismo tamaño (por ejemplo, cuatro unidades de la memoria - o
cuatro bytes de ocho bits)
- Luego la dirección en memoria del ith elemento es: inicio+ 4 * i donde inicio es la
dirección de inicio de la lista
- Así que puede llegar a ese punto de la memoria en tiempo constante

Indirección
• Pero, ¿y si la lista es de los objetos de tamaño arbitrario?
• Use la indirección
• Representar una lista como una combinación de una longitud (número de objetos), y
una secuencia de indicadores de tamaño fijo a objetos (o direcciones de memoria)
 Indirección
• Si la longitud de campo es de 4 unidades de memoria, y cada puntero ocupa 4
unidades de memoria
• Luego, la dirección del ith elemento se almacena en: inicio + 4 + 4 * i
• Esta dirección se puede encontrar en un tiempo constante, y el valor almacenado en la
dirección también encontrado en tiempo constante
• Así que la búsqueda es lineal
• Indirección: el acceso a algo accediendo primero algo que contiene una referencia a
lo solicitado.

Búsqueda binaria
• ¿Podemos hacer algo mejor que O(len(L)) para la búsqueda?
• Si no se sabe nada acerca de los valores de los elementos en la lista, entonces no.
• Peor caso, tendría que reconocer a cada elemento

¿Qué pasa si la lista está ordenada?


• Supongamos que los elementos están clasificados en orden ascendente

def busqueda(L, e):


for i in range(len(L)):
if L[i] == e:
return True
if L[i] > e:
return False
return False

• Mejora la complejidad media, pero peor de los casos todavía tienen que buscar cada
elemento.

Use la búsqueda binaria


1. Elige un índice, i, que divide la lista en mitades
2. Pregunta si L [i] == e
3. Si no, pregunte si L [i] es mayor o menor que e
4. Dependiendo de la respuesta, busca en la mitad izquierda de L o en la mitad derecha
de L para e

Una nueva versión del algoritmo divide - y - conquista


• Divida en versión más pequeña del problema (lista más pequeña), además de algunas
operaciones sencillas
• La respuesta a la versión más pequeña es la respuesta al problema original

Búsqueda binaria

def busqueda(L, e):


def busquedaBi(L, e, bajo, alto):
if e < L[bajo]:
return False
if e > L[alto-1]:
return False
if alto == bajo:
return L[bajo] == e
med = bajo + int((alto - bajo)//2)
if L[med] == e:
return True
if L[med] > e:
return busquedaBi(L, e, bajo, med - 1)
else:
return busquedaBi(L, e, med + 1, alto)

if len(L) == 0:
return False
else:
return busquedaBi(L, e, 0, len(L))

Analizando búsqueda binaria


• ¿La recursividad se detiene?
- Decrementando la función
1. Correspondencia de valores a que los parámetros formales están obligados a enteros
no negativos.
2. Cuando el valor es <= 0, termina recursividad
3. Para cada llamada recursiva, el valor de la función es estrictamente menor que el
valor de la entrada a la instancia de la función
- Aquí la función es alto-bajo
• Por lo menos 0 primera tiempo llama (1)
• ¿Cuándo exactamente 0, no hay ninguna llamada recursiva, devoluciones ( 2 )
• De lo contrario, detener o llamar de forma repetitiva con valor reducido a la mitad (3)
• Así termina

Analizando búsqueda binaria


• ¿Cuál es la complejidad?
- ¿Cuántas llamadas recursivas? (trabajo dentro de cada llamada es constante)
- ¿Cuántas veces podemos dividir alto - bajo a la mitad antes de llegar a 0?
- Log2 (alto - bajo)
- Por lo tanto buscar la complejidad es O(log(len(L)))

 Algoritmos de ordenación
• ¿Qué pasa con el costo de la ordenación?
• Asumir la complejidad de la ordenación de una lista es O(ordena( L))
• A continuación, si ordenar y buscar lo que queremos saber si:
ordena (L) + log(len(L)) < len (L)
- Es decir, debemos ordenar y buscar utilizando búsqueda binaria, o sólo se tiene que
utilizar la búsqueda lineal
• No se puede ordenar en menos tiempo que la búsqueda lineal!

Amortización de costos
• Sin embargo, supongamos que queremos buscar una lista de k veces?
• A continuación, es una especie (L) + k * log (len (L)) < k * len (L) ?
- Depende de k, pero uno espera que si una especie se puede hacer de manera eficiente,
entonces es mejor primero ordenar
- Coste amortizable de clasificación a través de múltiples búsquedas puede hacer que
esto valga la pena
- ¿Cómo podemos solucionar de manera eficiente?

Ordenación por slección

def selec(L):
for i in range(len(L) - 1):
minIndx = i
minVal= L[i]
j=i+1
while j < len(L):
if minVal > L[j]:
minIndx = j
minVal= L[j]
j += 1
temp = L[i]
L[i] = L[minIndx]
L[minIndx] = Temp.

El análisis de ordenación por selección


• Loop invariante
- Dado el prefijo de la lista L[0: i] y el sufijo L [ i +1: len ( L ) - 1], luego el prefijo se
ordena y no hay ningún elemento en el prefijo es más grande que el elemento más
pequeño del sufijo
1. Caso base: prefijo vacío, sufijo toda la lista entera - invariante verdad
2. Paso de inducción: mover el elemento mínimo del sufijo al final del prefijo. Desde
invariante cierto antes de mover, prefijo ordenada después de append
3. Al salir, el prefijo es lista completa, sufijo vacío, así fue ordenado

El análisis de ordenación por selección


• Complejidad de bucle interno es O(len(L))
• Complejidad de bucle exterior también O (len(L))
• Así que en general la complejidad es O(len(L) 2) o cuadrática
• Caro

Ordenación por mezcla


• Utilizar un enfoque divide y vencerás:
1. Si la lista es de longitud 0 o 1, está ya ordenados
2. Si la lista tiene más de un elemento, se divide en dos listas, y ordena cada una
3. Mezclar los resultados
1. Para mezcclar, basta con ver primer elemento de cada uno, mover el más pequeño
al final del resultado
2. Cuando uno lista está vacía, sólo tienes que copiar el resto de otra lista
Ejemplo de mezcla
Izquierda en la lista 1 Izquierda en la lista 2 Comparar Resultado
[ 1,5,12,18,19,20 ] [ 2,3,4,17 ] 1, 2 []
[ 5,12,18,19,20 ] [ 2,3,4,17 ] 5, 2 [1]
[ 5,12,18,19,20 ] [ 3,4,17 ] 5, 3 [1,2 ]
[ 5,12,18,19,20 ] [ 4,17 ] 5, 4 [1,2,3 ]
[ 5,12,18,19,20 ] [ 17 ] 5, 17 [ 1,2,3,4 ]
[ 12,18,19,20 ] [ 17 ] 12, 17 [ 1,2,3,4,5 ]
[ 18.19.20 ] [ 17 ] 18, 17 [ 1,2,3,4,5,12 ]
[ 18,19,20 ] [] 18, - [ 1,2,3,4,5,12,17 ]
[] [] [ 1,2,3,4,5,12,17,18,19,20 ]

Complejidad de Mezcla
• Comparación y copia son constantes
• El número de comparaciones - O(len(L)
• Número de copias - O(len(L1) + len(L2))
• Así que la mezcla es lineal en la longitud de las listas

def mezcla(izq, der, compara):


resultado = []
i,j = 0, 0
while i < len(izq) and j < len(der):
if compara(izq[i], der[j]):
resultado.append(izq[i])
i += 1
else:
resultado.append(der[j])
j += 1
while (i < len(izq)):
resultado.append(izq[i])
i += 1
while (j < len(der)):
resultado.append(der[j])
j += 1
return resultado

Colocando juntos

import operator

def mezclaOrd(L, compara = operator.lt):


if len(L) < 2:
return L[:]
else:
medio = int(len(L)/2)
izq = mezclaOrd(L[:medio], compara)
der = mezclaOrd(L[medio:], compara)
return mezcla(izq, der, compara)
Complejidad de ordenación por mezcla

• Mezcla es O(len(L))
• Por mezcla es O(len(L))*número de llamadas para fusionar
- O( len (L)) * número de llamadas a mezcla
- O(len(L) * log(len(L)))
• Registrar lineal - O(n log n), donde n es len (L)
• Viene con costo en el espacio, como lo hace la nueva copia de la lista

Mejorando la eficiencia
• Combinación de búsqueda binaria con la ordenación mezcla es muy eficiente
- Si buscamos lista k veces, entonces la eficiencia es n * log ( n) + k * log ( n)
• ¿Podemos hacerlo mejor?
• Diccionarios utilizan concepto de hash
- Operaciones de búsqueda se puede hacer en tiempo casi independiente del tamaño
del diccionario

Hashing
• Convertir la clave a un int
• Utilice int para indexar en una lista (tiempo constante)
• Conversión haciendo uso de la función hash
- Aplicación de gran espacio de entradas para un pequeño espacio de salidas
- Así, una aplicación de muchos-a - uno
- Cuando dos entradas van a la misma salida - una colisión
- Una buena función hash tiene una distribución uniforme- Minimiza la probabilidad de
una colisión

Complejidad
• Si no hay colisiones, entonces O(1)
• Si par todo hash al mismo cubo, entonces O(n)
• Pero, en general, se puede negociar el espacio para hacer tabla hash grande, y con
buena función que se acerque a la distribución uniforme, y reducir la complejidad a
cerca de O(1)

También podría gustarte