04 Grokking Algorithms Illustrated Programmers Curious ESP

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 258

Machine Translated by Google

Machine Translated by Google

asimilando
algoritmos
Machine Translated by Google
Machine Translated by Google

asimilando
algoritmos
Una guía ilustrada
para programadores y otros curiosos

Aditya Y. Bhargava

MANEJO
Isla Refugio
Machine Translated by Google

Para obtener información en línea y solicitar este y otros libros de Manning, visite
www.manning.com. La editorial ofrece descuentos en este libro cuando se pide en cantidad. Para
obtener más información, póngase en contacto

Departamento de Ventas Especiales

Manning Publications Co.


20 Baldwin Road, PO Box 761
Shelter Island, NY 11964

Correo electrónico: [email protected]

©2016 por Manning Publications Co. Todos los derechos reservados.

Ninguna parte de esta publicación puede reproducirse, almacenarse en un sistema de


recuperación o transmitirse de ninguna forma o por medios electrónicos, mecánicos, fotocopiados
o de otro modo, sin el permiso previo por escrito del editor.

Muchas de las designaciones utilizadas por los fabricantes y vendedores para distinguir sus
productos se reclaman como marcas comerciales. Donde esas designaciones aparecen en el libro, y
Manning Publications estaba al tanto de un reclamo de marca registrada, las designaciones se han
impreso en mayúsculas iniciales o en mayúsculas.

ÿ Reconociendo la importancia de preservar lo que ha sido escrito, es el objetivo de Manning


política de tener los libros que publicamos impresos en papel libre de ácido, y hacemos nuestros mejores
esfuerzos con ese fin. Reconociendo también nuestra responsabilidad de conservar los recursos de
nuestro planeta, los libros de Manning se imprimen en papel que es al menos un 15 por ciento reciclado
y procesado sin el uso de cloro elemental.

Editora de desarrollo de Manning Publications Co.: Jennifer Stout


20 Baldwin Road Editor de desarrollo técnico: Damien White

Isla del refugio, NY 11964 Gerente de proyecto: Tiffany Taylor


Correctora: Tiffany Taylor
Corrector técnico: Jean-François Morin

Tipógrafo: Leslie Haimes


Diseño de portada e interior: Leslie Haimes
Ilustraciones del autor

ISBN: 9781617292231
Impreso en los Estados Unidos de América
1 2 3 4 5 6 7 8 9 10 – MBE – 21 20 19 18 17 16
Machine Translated by Google

Para mis padres, Sangeeta y Yogesh


Machine Translated by Google
Machine Translated by Google

viii

contenido
prefacio XIII

agradecimientos xiv

sobre este libro XV

1 Introducción a los algoritmos 1

Introducción 1

Lo que aprenderá sobre el rendimiento 2

Lo que aprenderás sobre la resolución de problemas 2

Búsqueda binaria 3

Una mejor manera de buscar 5

Tiempo de ejecución 10

Notación O grande 10

Los tiempos de ejecución del algoritmo crecen a diferentes tasas 11

Visualización de diferentes tiempos de ejecución de Big O 13

Big O establece un tiempo de ejecución en el peor de los casos 15

Algunos tiempos de ejecución comunes de Big O 15

El vendedor ambulante 17

Resumen 19

2 Clasificación de selección 21

como funciona la memoria 22

Matrices y listas enlazadas 24


listas enlazadas 25

arreglos 26

Terminología 27

Insertar en medio de una lista 29


Eliminaciones 30
Machine Translated by Google

viii contenido

Clasificación de selección 32
Resumen 36

3 recursividad 37

recursividad 38
Caso base y caso recursivo 40
La pila 42
La pila de llamadas 43
La pila de llamadas con recursividad 45
Resumen 50

4 Clasificación rápida 51

Divide y vencerás 52
Ordenación rápida 60
Notación Big O revisada 66

Combinar ordenación frente a ordenación 67

rápida Caso promedio frente al peor de los casos 68

Recapitulación 72

5 tablas hash 73

Funciones hash 76
casos de uso 79

Uso de tablas hash para búsquedas 79

Prevención de entradas duplicadas 81

Uso de tablas hash como caché 83

Resumen 86
Colisiones 86
Rendimiento 88
Factor de carga 90

Una buena función hash 92


Resumen 93

6 Búsqueda en amplitud 95

Introducción a los gráficos ¿Qué 96


es un gráfico? 98
Búsqueda en amplitud 99

Encontrar el camino más corto 102


Machine Translated by Google

contenido ix

Colas 103
Implementando el gráfico 105
Implementando el algoritmo 107
Tiempo de ejecución 111
Resumen 114

7 Algoritmo de Dijkstra 115

Trabajando con el algoritmo de Dijkstra 116


Terminología 120
cambio por un piano 122
Bordes de peso negativo 128
Implementación 131
Resumen 140

8 algoritmos codiciosos 141

El problema de la programación del aula. 142


El problema de la mochila 144
El problema de la cobertura del conjunto 146
Algoritmos de aproximación 147
Problemas NP-completos Vendedor 152
viajero, paso a paso ¿Cómo saber si un 153
problema es NP-completo? 158
Resumen 160

9 Programación dinámica 161

El problema de la mochila 161


la solución sencilla 162
Programación dinámica 163
Preguntas frecuentes sobre el 171
problema de la mochila ¿Qué pasa si agregas un artículo? 171
¿Qué pasa si cambias el orden de las filas? 174
¿Puedes completar la cuadrícula por columnas en
lugar de por filas? 174
¿Qué pasa si agregas un artículo más pequeño? 174
¿Puedes robar fracciones de un artículo? 175
Optimización de su itinerario de viaje 175
Manejo de elementos que dependen unos de otros 177
Machine Translated by Google

X contenido

¿Es posible que la solución requiera más de dos


submochilas? 177
¿Es posible que la mejor solución no llene la mochila
por completo? 178
Subcadena común más larga 178
Haciendo la grilla Llenando 179
la grilla La solución 180
182
Subsecuencia común más larga 183
Subsecuencia común más larga: solución 184
Resumen 186

10 K-vecinos más cercanos 187

Clasificación de naranjas frente a pomelos 187


Construcción de un sistema de recomendaciones 189
Extracción de características 191
Regresión 195
Elegir buenas características 198
Introducción al aprendizaje automático 199
LOC 199
Construyendo un filtro de spam 200
Predecir el mercado de valores 201
Resumen 201

11 Dónde ir después 203

Árboles 203
Índices invertidos 206
La transformada de Fourier 207
Algoritmos paralelos 208
Mapa reducido 209
¿Por qué son útiles los algoritmos distribuidos? 209
La función de mapa 209
La función de reducción 210
Filtros Bloom y HyperLogLog 211
Filtros de floración 212
Machine Translated by Google

contenido xi

HyperLogLog 213
Los algoritmos SHA 213
Comparando archivos 214
Comprobación de contraseñas 215
Hashing sensible a la localidad 216
Intercambio de claves Diffie-Hellman 217
Programación lineal 218
Epílogo 219
respuestas a ejercicios 221
índice 235
Machine Translated by Google
Machine Translated by Google

prefacio
Primero me metí en la programación como un hobby. Visual Basic 6 for Dummies me
enseñó los conceptos básicos y seguí leyendo libros para aprender más. Pero el tema
de los algoritmos era impenetrable para mí. Recuerdo saborear la tabla de contenido
de mi primer libro de algoritmos, pensando "¡Por fin voy a entender estos temas!" Pero
era algo denso, y me di por vencido después de unas semanas. No fue hasta que tuve
mi primer buen profesor de algoritmos que me di cuenta de lo simples y elegantes que
eran estas ideas.

Hace unos años, escribí mi primera entrada de blog ilustrada. Soy un aprendiz
visual y me gustó mucho el estilo ilustrado. Desde entonces, he escrito algunas
publicaciones ilustradas sobre programación funcional, Git, aprendizaje automático y
concurrencia. Por cierto: yo era un escritor mediocre cuando empecé. Explicar
conceptos técnicos es difícil. Proponer buenos ejemplos lleva tiempo, y explicar un
concepto difícil lleva tiempo.
Así que es más fácil pasar por alto las cosas difíciles. Pensé que estaba haciendo un
buen trabajo, hasta que una de mis publicaciones se hizo popular, un compañero de
trabajo se me acercó y me dijo: "Leí tu publicación y todavía no entiendo esto". Todavía
tenía mucho que aprender acerca de la escritura.

En algún momento mientras escribía estas publicaciones de blog, Manning se acercó a mí


y me preguntó si quería escribir un libro ilustrado. Bueno, resulta que los editores de
Manning saben mucho sobre cómo explicar conceptos técnicos y me enseñaron cómo
enseñar. Escribí este libro para rascarme un picor en particular: quería escribir un libro que
explicara bien los temas técnicos difíciles, y quería un libro de algoritmos fácil de leer. Mi
escritura ha recorrido un largo camino desde esa primera publicación de blog, y espero
que encuentre este libro una lectura fácil e informativa.

XIII
Machine Translated by Google

expresiones de gratitud
Felicitaciones a Manning por darme la oportunidad de escribir este libro
y permitirme tener mucha libertad creativa con él. Gracias a la editora
Marjan Bace, Mike Stephens por ayudarme, a Bert Bates por enseñarme
a escribir y a Jennifer Stout por ser una editora increíblemente receptiva
y útil. Gracias también a la gente del equipo de producción de Manning:
Kevin Sullivan, Mary Piergies, Tiffany Taylor, Leslie Haimes y todos los
demás detrás de escena. Además, quiero agradecer a las muchas
personas que leyeron el manuscrito y ofrecieron sugerencias: Karen
Bensdon, Rob Green, Michael Hamrah, Ozren Harlovic, Colin Hastie,
Christopher Haupt, Chuck Henderson, Pawel Kozlowski, Amit Lamba, Jean-
François Morin, Robert Morrison, Sankar Ramanathan, Sander Rossel,
Doug Sparling y Damien White.
Gracias a las personas que me ayudaron a llegar a este punto: la gente
del tablero de Flaskhit, por enseñarme a codificar; los muchos amigos que
me ayudaron revisando capítulos, dándome consejos y permitiéndome
probar diferentes explicaciones, incluidos Ben Vinegar, Karl Puzon, Alex
Manning, Esther Chan, Anish Bhatt, Michael Glass, Nikrad Mahdi, Charles
Lee, Jared Friedman, Hema Manickavasagam , Hari Raja, Murali Gudipati,
Srinivas Varadan y otros; y Gerry Brady, por enseñarme algoritmos. Otro
gran agradecimiento a los académicos de algoritmos como CLRS, Knuth y
Strang. Realmente estoy de pie sobre los hombros de gigantes.
Papá, mamá, Priyanka y el resto de la familia: gracias por su constante
apoyo. Y muchas gracias a mi esposa Maggie. Tenemos muchas
aventuras por delante, y algunas de ellas no implican quedarse en casa
un viernes por la noche reescribiendo párrafos.
Finalmente, muchas gracias a todos los lectores que se arriesgaron con
este libro y a los lectores que me dieron su opinión en el foro del libro.
Realmente ayudaste a mejorar este libro.

xiv
Machine Translated by Google

sobre este libro


Este libro está diseñado para ser fácil de seguir. Evito los grandes saltos de pensamiento.
Cada vez que se introduce un nuevo concepto, lo explico de inmediato o le digo
cuándo lo explicaré. Los conceptos básicos se refuerzan con ejercicios y múltiples
explicaciones para que pueda verificar sus suposiciones y asegurarse de que está
siguiendo las instrucciones.

Dirijo con ejemplos. En lugar de escribir una sopa de símbolos, mi objetivo es


facilitarle la visualización de estos conceptos. También creo que aprendemos mejor si
podemos recordar algo que ya sabemos, y los ejemplos hacen que recordar sea más
fácil. Entonces, cuando intente recordar la diferencia entre matrices y listas enlazadas
(explicadas en el capítulo 2), puede pensar en sentarse para ver una película. Además,
a riesgo de decir lo obvio, aprendo visualmente. Este libro está repleto de imágenes.

El contenido del libro está cuidadosamente seleccionado. No hay necesidad de


escribir un libro que cubra todos los algoritmos de clasificación, por eso tenemos
Wikipedia y Khan Academy. Todos los algoritmos que he incluido son prácticos. Los he
encontrado útiles en mi trabajo como ingeniero de software y proporcionan una buena
base para temas más complejos.
¡Feliz lectura!

Mapa vial
Los tres primeros capítulos de este libro sientan las bases:

• Capítulo 1—Aprenderá su primer algoritmo práctico: búsqueda binaria.


También aprende a analizar la velocidad de un algoritmo utilizando la notación
Big O. La notación Big O se usa a lo largo del libro para analizar qué tan lento o rápido
es un algoritmo.

XV
Machine Translated by Google

xvi sobre este libro

• Capítulo 2: aprenderá sobre dos estructuras de datos fundamentales:


matrices y listas enlazadas. Estas estructuras de datos se usan a lo largo del libro y
se usan para crear estructuras de datos más avanzadas, como tablas hash (capítulo
5).

• Capítulo 3: aprenderá sobre la recursividad, una técnica útil utilizada por muchos
algoritmos (como el ordenamiento rápido, que se trata en el capítulo 4).

En mi experiencia, la notación Big O y la recursividad son temas desafiantes para los


principiantes. Así que reduje la velocidad y pasé más tiempo en estas secciones.

El resto del libro presenta algoritmos con amplias aplicaciones:

• Técnicas de resolución de problemas: cubiertas en los capítulos 4, 8 y 9. Si se


encuentra con un problema y no está seguro de cómo resolverlo de manera eficiente,
intente dividir y vencer (capítulo 4) o programación dinámica (capítulo 9). O puede
darse cuenta de que no hay una solución eficiente y obtener una respuesta aproximada
utilizando un algoritmo codicioso (capítulo 8).

• Tablas hash : cubiertas en el capítulo 5. Una tabla hash es una estructura de datos
muy útil. Contiene conjuntos de pares de clave y valor, como el nombre de una
persona y su dirección de correo electrónico, o un nombre de usuario y la contraseña asociada.
Es difícil exagerar la utilidad de las tablas hash. Cuando quiero resolver un
problema, los dos planes de ataque con los que empiezo son "¿Puedo usar una
tabla hash?" y "¿Puedo modelar esto como un gráfico?"

• Algoritmos de gráficos: se tratan en los capítulos 6 y 7. Los gráficos son una forma de
modelar una red: una red social, una red de caminos, neuronas o cualquier otro
conjunto de conexiones. La búsqueda primero en amplitud (capítulo 6) y el algoritmo
de Dijkstra (capítulo 7) son formas de encontrar la distancia más corta entre dos puntos
en una red: puede usar este enfoque para calcular los grados de separación entre dos
personas o la ruta más corta a un destino .

• K-vecinos más cercanos (KNN): cubierto en el capítulo 10. Este es un


Algoritmo de aprendizaje automático simple. Puede usar KNN para crear un
sistema de recomendaciones, un motor OCR, un sistema para predecir valores de
acciones, cualquier cosa que implique predecir un valor ("Creemos que Adit calificará
esta película con 4 estrellas") o clasificar un objeto ("Esa letra es un Q”).

• Próximos pasos: el Capítulo 11 repasa 10 algoritmos que serían una buena lectura
adicional.
Machine Translated by Google

sobre este libro xvii

Como usar este libro


El orden y el contenido de este libro han sido cuidadosamente diseñados. Si está interesado en un
tema, no dude en adelantarse. De lo contrario, lea los capítulos en orden: se complementan entre sí.

Recomiendo encarecidamente ejecutar el código de los ejemplos usted mismo. No puedo enfatizar esta
parte lo suficiente. Simplemente escriba mis ejemplos de código palabra por palabra (o descárguelos
de www.manning.com/books/grokking algoritmos o https://github.com/egonschiele/grokking_algorithms)
y ejecútelos. Retendrá mucho más si lo hace.

También recomiendo hacer los ejercicios de este libro. Los ejercicios son breves, generalmente de
uno o dos minutos, a veces de 5 a 10 minutos. Le ayudarán a revisar su forma de pensar, para que
sepa cuándo se está desviando antes de que haya ido demasiado lejos.

Quién debería leer este libro


Este libro está dirigido a cualquier persona que conozca los conceptos básicos de la codificación
y quiera comprender los algoritmos. Tal vez ya tenga un problema de codificación y esté tratando
de encontrar una solución algorítmica. O tal vez quieras entender para qué son útiles los algoritmos.
Aquí hay una lista breve e incompleta de personas que probablemente encontrarán útil este libro:

• Codificadores aficionados

• Estudiantes del campo de entrenamiento de codificación

• Graduados de informática que buscan un repaso

• Física/matemáticas/otros graduados que estén interesados en la programación

Convenciones de código y descargas


Todos los ejemplos de código de este libro usan Python 2.7. Todo el código del libro se presenta
en una fuente de ancho fijo como esta para separarlo del texto ordinario. Las anotaciones de
código acompañan a algunos de los listados, destacando conceptos importantes.

Puede descargar el código de los ejemplos del libro desde el sitio web de la editorial en
www.manning.com/books/grokking-algorithms o desde https://github.com/egonschiele/grokking_algorithms.

Creo que aprendes mejor cuando realmente disfrutas aprendiendo, ¡así que diviértete y ejecuta los
ejemplos de código!
Machine Translated by Google

xviii sobre este libro

Sobre el Autor
Aditya Bhargava es ingeniero de software en Etsy, un mercado en línea
para productos hechos a mano. Tiene una maestría en informática de la
Universidad de Chicago. También dirige un popular blog tecnológico
ilustrado en adit.io.

Autor en línea
La compra de Grokking Algorithms incluye acceso gratuito a un foro web
privado administrado por Manning Publications donde puede hacer
comentarios sobre el libro, hacer preguntas técnicas y recibir ayuda del
autor y de otros usuarios. Para acceder al foro y suscribirse, apunte su
navegador web a www.manning.com/books/grokking algoritmos. Esta
página brinda información sobre cómo ingresar al foro una vez que está
registrado, qué tipo de ayuda está disponible y las reglas de conducta en
el foro.

El compromiso de Manning con nuestros lectores es proporcionar un


lugar donde pueda tener lugar un diálogo significativo entre lectores
individuales y entre lectores y el autor. No es un compromiso de participación
específica por parte del autor, cuya contribución a Author Online sigue
siendo voluntaria (y no remunerada). ¡Le sugerimos que intente hacerle
algunas preguntas desafiantes al autor para que su interés no se desvíe! El
foro Author Online y los archivos de debates anteriores estarán accesibles
desde el sitio web del editor siempre que el libro esté impreso.
Machine Translated by Google

introducción
a los algoritmos 1

En este capítulo
• Obtienes una base para el resto del libro.

• Escribes tu primer algoritmo de búsqueda (búsqueda


binaria).

• Aprende a hablar sobre el tiempo de ejecución de un


algoritmo (notación Big O).

• Se le presenta una técnica común para diseñar


algoritmos (recursión).

Introducción
Un algoritmo es un conjunto de instrucciones para realizar una tarea. Cada
pieza de código podría llamarse algoritmo, pero este libro cubre las partes
más interesantes. Elegí los algoritmos de este libro para incluirlos porque son
rápidos o resuelven problemas interesantes, o ambas cosas. Aquí hay algunos
aspectos destacados:

• El Capítulo 1 habla sobre la búsqueda binaria y muestra cómo un algoritmo


puede acelerar su código. En un ejemplo, ¡la cantidad de pasos necesarios
va de 4 mil millones a 32!

1
Machine Translated by Google

2 Capítulo 1 I Introducción a los algoritmos

• Un dispositivo GPS usa algoritmos gráficos (como aprenderá en los capítulos 6, 7 y


8) para calcular la ruta más corta a su destino.

• Puede usar la programación dinámica (discutida en el capítulo 9) para escribir


un algoritmo de IA que juega a las damas.

En cada caso, describiré el algoritmo y te daré un ejemplo. Luego hablaré sobre el


tiempo de ejecución del algoritmo en notación Big O.
Finalmente, exploraré qué otros tipos de problemas podrían resolverse con el mismo
algoritmo.

Lo que aprenderá sobre el rendimiento


La buena noticia es que probablemente haya disponible una implementación de cada
algoritmo en este libro en su idioma favorito, ¡así que no tiene que escribir cada algoritmo
usted mismo! Pero esas implementaciones son inútiles si no comprende las
compensaciones. En este libro, aprenderá a comparar las ventajas y desventajas entre
diferentes algoritmos: ¿debería usar la ordenación por combinación o la ordenación
rápida? ¿Deberías usar una matriz o una lista? El simple hecho de usar una estructura
de datos diferente puede marcar una gran diferencia.

Lo que aprenderás sobre la resolución de problemas


Aprenderá técnicas para resolver problemas que podrían haber estado fuera de su
alcance hasta ahora. Por ejemplo:

• Si te gusta hacer videojuegos, puedes escribir un sistema de IA que siga al


usuario usando algoritmos gráficos.

• Aprenderás a hacer un sistema de recomendaciones usando k-nearest


vecinos

• ¡Algunos problemas no se pueden resolver a tiempo! La parte de este libro que


trata sobre problemas NP-completos le muestra cómo identificar esos problemas
y generar un algoritmo que le dé una respuesta aproximada.

En términos más generales, al final de este libro, conocerá algunos de los algoritmos
más ampliamente aplicables. Luego puede usar su nuevo conocimiento para aprender
sobre algoritmos más específicos para IA, bases de datos, etc. O puede asumir desafíos
más grandes en el trabajo.
Machine Translated by Google

Búsqueda binaria 3

Lo que necesitas saber


Necesitará saber álgebra básica antes de comenzar este libro. En
particular, tome esta función: f(x) = x × 2. ¿Cuánto es f(5)? Si respondió
10, está listo.
Además, este capítulo (y este libro) será más fácil de seguir si está
familiarizado con un lenguaje de programación. Todos los ejemplos en
este libro están en Python. Si no conoce ningún lenguaje de
programación y desea aprender uno, elija Python, es ideal para
principiantes. Si sabes otro idioma, como Ruby, estarás bien.

Búsqueda binaria
Supongamos que está buscando a una persona en la guía telefónica (¡qué frase tan
anticuada!). Su nombre comienza con K. Puede comenzar desde el principio y seguir
pasando las páginas hasta llegar a las K. Pero es más probable que comience en una
página en el medio, porque sabe que las K estarán cerca del medio de la guía telefónica.

O suponga que está buscando una palabra en un diccionario y comienza con


O. Nuevamente, comenzará cerca del medio.

Ahora suponga que inicia sesión en Facebook. Cuando lo haga, Facebook tiene
que verificar que tiene una cuenta en el sitio. Por lo tanto, necesita buscar su
nombre de usuario en su base de datos. Supongamos que su nombre de usuario es
karlmageddon. Facebook podría comenzar desde As y buscar su nombre, pero tiene
más sentido que comience en algún punto intermedio.

Este es un problema de búsqueda. Y todos estos casos utilizan el mismo algoritmo


para resolver el problema: la búsqueda binaria.

La búsqueda binaria es un algoritmo; su entrada es una lista ordenada de


elementos (explicaré más adelante por qué debe ordenarse). Si un elemento que
está buscando está en esa lista, la búsqueda binaria devuelve la posición donde
se encuentra. De lo contrario, la búsqueda binaria devuelve nulo.
Machine Translated by Google

4 capitulo 1 yo Introducción a los algoritmos

Por ejemplo:

Buscando empresas en
una guía telefónica con
búsqueda binaria

Aquí hay un ejemplo de cómo funciona la búsqueda binaria. Estoy pensando en


un número entre 1 y 100.

Tienes que intentar adivinar mi número en el menor número de intentos posible. Con
cada suposición, te diré si tu suposición es demasiado baja, demasiado alta o correcta.

Supongamos que empiezas a adivinar así: 1, 2, 3, 4…. Así es como sería.


Machine Translated by Google

Búsqueda binaria 5

Un mal enfoque
para adivinar números

Esta es una búsqueda simple (tal vez la búsqueda estúpida sería un mejor término). Con
cada suposición, estás eliminando solo un número. Si mi número fuera 99, ¡podría tomar 99
intentos para llegar allí!

Una mejor manera de buscar


Aquí hay una mejor técnica. Comience con 50.

¡Demasiado bajo, pero acabas de eliminar la mitad de los números! Ahora sabes que 1–50
son demasiado bajos. Próxima suposición: 75.
Machine Translated by Google

6 capitulo 1 yo Introducción a los algoritmos

¡Demasiado alto, pero de nuevo recortas la mitad de los números restantes!


Con la búsqueda binaria, adivinas el número del medio y eliminas la mitad de
los números restantes cada vez. El siguiente es 63 (a medio camino entre 50 y 75).

Esta es la búsqueda binaria. ¡Acabas de aprender tu primer algoritmo! He aquí


cuántos números puedes eliminar cada vez.

Eliminar la mitad de la
números cada vez con
búsqueda binaria.

Cualquiera que sea el número en el que esté pensando, puede adivinar en un


máximo de siete intentos, ¡porque elimina tantos números con cada intento!

Suponga que está buscando una palabra en el diccionario. El diccionario tiene


240.000 palabras. En el peor de los casos, ¿cuántos pasos crees que llevará cada
búsqueda?

La búsqueda simple podría tomar 240,000 pasos si la palabra que está buscando
es la última del libro. Con cada paso de la búsqueda binaria, reduce el número de
palabras a la mitad hasta que solo le queda una palabra.
Machine Translated by Google

Búsqueda binaria 7

Entonces, la búsqueda binaria tomará 18 pasos, ¡una gran diferencia! En general, para cualquier
lista de n, la búsqueda binaria tomará log2 n pasos para ejecutarse en el peor de los casos,
mientras que la búsqueda simple tomará n pasos.

logaritmos
Puede que no recuerdes qué son los logaritmos, pero probablemente sepas qué son los
exponenciales. log10 100 es como preguntar: "¿Cuántos 10 multiplicamos para obtener
100?" La respuesta es 2: 10 × 10. Así que log10 100 = 2. Los logaritmos son la inversión de
exponenciales.

Los registros son el cambio de exponenciales.

En este libro, cuando hablo sobre el tiempo de ejecución en notación Big O (explicado un
poco más adelante), log siempre significa log2 . Cuando busca un elemento mediante la
búsqueda simple, en el peor de los casos, es posible que tenga que mirar cada uno de los
elementos. Entonces, para una lista de 8 números, tendrías que marcar 8 números como máximo.
Para la búsqueda binaria, debe verificar los elementos log n en el peor de los casos. Para
una lista de 8 elementos, log 8 == 3, porque 23 == 8. Entonces, para una lista de 8 números,
tendrías que marcar 3 números como máximo. Para una lista de 1024 elementos, log 1024
= 10, porque 210 == 1024. Entonces, para una lista de 1024 números, tendrías que marcar
10 números como máximo.

Nota

Hablaré mucho sobre el tiempo de registro en este libro, por lo que debe
comprender el concepto de logaritmos. Si no es así, Khan Academy
(khanacademy.org) tiene un buen video que lo deja claro.
Machine Translated by Google

8 capitulo 1 yo Introducción a los algoritmos

Nota
La búsqueda binaria solo funciona cuando su lista está ordenada. Por ejemplo, los
nombres de una guía telefónica se ordenan alfabéticamente, por lo que puede
utilizar la búsqueda binaria para buscar un nombre. ¿Qué pasaría si los nombres no
estuvieran ordenados?

Veamos cómo escribir una búsqueda binaria en Python. El ejemplo de código aquí usa
matrices. Si no sabe cómo funcionan las matrices, no se preocupe; se tratan en el próximo
capítulo. Solo necesita saber que puede almacenar una secuencia de elementos en una
fila de cubos consecutivos llamada matriz.
Los cubos están numerados comenzando con 0: el primer cubo está en la posición #0, el
segundo es el #1, el tercero es el #2 y así sucesivamente.

La función binary_search toma una matriz ordenada y un elemento. Si el elemento está


en la matriz, la función devuelve su posición. Realizará un seguimiento de qué parte de la
matriz tiene que buscar. Al principio, esta es la matriz completa:

bajo = 0
alto = len(lista) - 1

Cada vez, marca el elemento del medio:


Python redondea hacia abajo
medio = (bajo + alto) / 2 conjetura =
automáticamente si (bajo + alto)
lista[medio]
no es un número par.

Si la conjetura es demasiado baja, actualice bajo en consecuencia:

si adivinar < artículo:


bajo = medio + 1
Machine Translated by Google

Búsqueda binaria 9

Y si la conjetura es demasiado alta, actualiza alto. Aquí está el código completo:

def binary_search(lista, elemento):


bajo y alto controle en qué parte de la lista
bajo = 0 alto
buscará.
= len(lista)—1
Si bien no lo ha reducido a un solo elemento...
mientras que bajo <= alto:
medio = (bajo + alto)
… verifique el elemento central.
adivinar = lista[mid]
si adivinar == elemento: Encontré el artículo.
volver a la mitad
si adivinar> artículo: alto =
La conjetura era demasiado alta.
medio - 1
más: La conjetura era demasiado baja.
bajo = medio + 1
El artículo no existe.
volver Ninguno
¡Vamos a probarlo!

mi_lista = [1, 3, 5, 7, 9]

Recuerde, las listas comienzan en 0.


La segunda ranura tiene el índice 1.
imprimir búsqueda_binaria(mi_lista, 3) # => 1
print binary_search(my_list, -1) # => Ninguno
"Ninguno" significa cero en Python.
Indica que no se encontró el artículo.

EJERCICIOS
1.1 Suponga que tiene una lista ordenada de 128 nombres y la está buscando
utilizando una búsqueda binaria. ¿Cuál es el número máximo de pasos que
daría?

1.2 Suponga que duplica el tamaño de la lista. ¿Cuál es el número máximo de


pasos ahora?
Machine Translated by Google

10 capitulo 1 yo Introducción a los algoritmos

Tiempo de ejecución
Cada vez que hablo de un algoritmo, hablaré de su tiempo de ejecución.
Por lo general, desea elegir el algoritmo más eficiente:
ya sea que esté tratando de optimizar por tiempo o espacio.

Volver a la búsqueda binaria. ¿Cuánto tiempo ahorras usándolo? Bueno, el primer


enfoque fue verificar cada número, uno por uno. Si esta es una lista de 100 números,
se necesitan hasta 100 intentos.
Si es una lista de 4 mil millones de números, se necesitan hasta 4 mil millones de intentos. Entonces,
el número máximo de conjeturas es el mismo que el tamaño de la lista. Esto se llama tiempo lineal.

La búsqueda binaria es diferente. Si la lista tiene 100 elementos, se necesitan como máximo 7
intentos. Si la lista es de 4 mil millones de elementos, se necesitan como máximo 32 conjeturas.
Potente, ¿eh? La búsqueda binaria se ejecuta en tiempo logarítmico (o tiempo de registro, como
lo llaman los nativos). Aquí hay una tabla que resume nuestros hallazgos de hoy.

Tiempos de ejecución

de los algoritmos de búsqueda

Notación O grande
La notación Big O es una notación especial que te dice qué tan rápido es un algoritmo.
¿A quien le importa? Bueno, resulta que usarás los algoritmos de otras personas con frecuencia,
y cuando lo haces, es bueno entender cuán rápidos o lentos son. En esta sección, explicaré qué
es la notación Big O y le daré una lista de los tiempos de ejecución más comunes para los algoritmos
que la usan.
Machine Translated by Google

Notación O grande 11

Los tiempos de ejecución del algoritmo crecen a diferentes tasas


Bob está escribiendo un algoritmo de búsqueda para la NASA. Su algoritmo se activará
cuando un cohete esté a punto de aterrizar en la Luna y ayudará a calcular dónde aterrizar.

Este es un ejemplo de cómo el tiempo de ejecución de dos algoritmos puede crecer a


ritmos diferentes. Bob está tratando de decidir entre la búsqueda simple y la búsqueda
binaria. El algoritmo debe ser rápido y correcto. Por un lado, la búsqueda binaria es más
rápida. Y Bob tiene solo 10 segundos para averiguar dónde aterrizar; de lo contrario, el
cohete se desviará. Por otro lado, la búsqueda simple es más fácil de escribir y hay menos
posibilidades de que se introduzcan errores. ¡Y Bob realmente no quiere errores en el
código para hacer aterrizar un cohete! Para ser más cuidadoso, Bob decide cronometrar
ambos algoritmos con una lista de 100 elementos.

Supongamos que se tarda 1 milisegundo en comprobar un elemento. Con la búsqueda


simple, Bob tiene que comprobar 100 elementos, por lo que la búsqueda tarda 100 ms
en ejecutarse. Por otro lado, solo tiene que verificar 7 elementos con la búsqueda binaria
(log2 100 es aproximadamente 7), por lo que la búsqueda tarda 7 ms en ejecutarse.
Pero siendo realistas, la lista tendrá más de mil millones de elementos. Si es así, ¿cuánto
tiempo llevará la búsqueda simple? ¿Cuánto tiempo llevará la búsqueda binaria?
Asegúrese de tener una respuesta para cada pregunta antes de seguir leyendo.

Tiempo de ejecución
para búsqueda simple
frente a búsqueda
binaria, con una lista de 100
elementos

Bob ejecuta una búsqueda binaria con mil millones de elementos y tarda 30 ms
(log2 1,000,000,000 es aproximadamente 30). “¡32 ms!” él piensa. “La búsqueda binaria
es aproximadamente 15 veces más rápida que la búsqueda simple, porque la búsqueda
simple tomó 100 ms con 100 elementos y la búsqueda binaria tomó 7 ms. Entonces, la
búsqueda simple tomará 30 × 15 = 450 ms, ¿verdad? Muy por debajo de mi umbral de 10
segundos”. Bob decide ir con la búsqueda simple. ¿Es esa la elección correcta?
Machine Translated by Google

12 capitulo 1 yo Introducción a los algoritmos

No. Resulta que Bob está equivocado. Completamente equivocado. El tiempo de ejecución para
una búsqueda simple con 1000 millones de elementos será de 1000 millones de ms, ¡lo que
equivale a 11 días! El problema es que los tiempos de ejecución de la búsqueda binaria y la
búsqueda simple no crecen al mismo ritmo.

¡Los tiempos de ejecución


crecen a velocidades muy diferentes!

Es decir, a medida que aumenta el número de elementos, la búsqueda binaria tarda un poco más
en ejecutarse. Pero la búsqueda simple requiere mucho más tiempo para ejecutarse. Entonces, a
medida que la lista de números crece, la búsqueda binaria de repente se vuelve mucho más rápida
que la búsqueda simple. Bob pensó que la búsqueda binaria era 15 veces más rápida que la
búsqueda simple, pero eso no es correcto. Si la lista tiene mil millones de elementos, es más como
33 millones de veces más rápido. Es por eso que no es suficiente saber cuánto tarda en ejecutarse
un algoritmo: debe saber cómo aumenta el tiempo de ejecución a medida que aumenta el tamaño de
la lista. Ahí es donde Big O
entra la notación.

La notación Big O te dice qué tan rápido es un algoritmo. Por ejemplo, suponga que tiene una lista de
tamaño n. La búsqueda simple necesita verificar cada elemento, por lo que tomará n operaciones. El
tiempo de ejecución en notación Big O es O(n). ¿Dónde están los segundos? No hay ninguno: Big O no
te dice la velocidad en segundos. La notación Big O le permite comparar el número de operaciones. Te
dice qué tan rápido crece el algoritmo.
Machine Translated by Google

Notación O grande 13

Aquí hay otro ejemplo. La búsqueda binaria necesita operaciones de registro n para verificar
una lista de tamaño n. ¿Cuál es el tiempo de ejecución en notación Big O? Es O (registro n).
En general, la notación Big O se escribe de la siguiente manera.

Cómo se ve la
notación Big O

Esto le indica el número de operaciones que realizará un algoritmo. Se llama notación Big
O porque pones una "gran O" delante del número de operaciones (suena como una broma, ¡pero
es verdad!).

Ahora veamos algunos ejemplos. Vea si puede calcular el tiempo de ejecución de estos
algoritmos.

Visualización de diferentes tiempos de ejecución de Big O

Aquí tienes un ejemplo práctico que puedes seguir en casa


con unas cuantas hojas de papel y un lápiz.
Suponga que tiene que dibujar una cuadrícula de 16 casillas.
¿Cuál es un buen
Algoritmo 1 algoritmo para
dibujar esta cuadrícula?
Una forma de hacerlo es dibujar 16 cajas, una a la vez.
Recuerde, la notación Big O cuenta el número de
operaciones. En este ejemplo, dibujar un cuadro es una
operación. Tienes que dibujar 16 cajas. ¿Cuántas
operaciones tomará dibujar un cuadro a la vez?

Dibujar una
cuadrícula un cuadro a la vez

Se necesitan 16 pasos para dibujar 16 cajas. ¿Cuál es el tiempo de ejecución de este


algoritmo?
Machine Translated by Google

14 capitulo 1 yo Introducción a los algoritmos

Algoritmo 2
Pruebe este algoritmo en su lugar. Dobla el papel.

En este ejemplo, doblar el papel una vez es una operación. ¡Acabas de hacer dos
cajas con esa operación!

Dobla el papel una y otra y otra vez.

¡Desdóblalo después de cuatro pliegues y tendrás una cuadrícula hermosa!


Cada pliegue duplica el número de cajas. ¡Hiciste 16 cajas con 4 operaciones!

Dibujar una cuadrícula


en cuatro pliegues

Puede "dibujar" el doble de cajas con cada pliegue, por lo que puede dibujar 16
cajas en 4 pasos. ¿Cuál es el tiempo de ejecución de este algoritmo? Proponga
tiempos de ejecución para ambos algoritmos antes de continuar.

Respuestas: El algoritmo 1 toma el tiempo O(n) y el algoritmo 2 toma el


tiempo O(log n).
Machine Translated by Google

Notación O grande 15

Big O establece un tiempo de ejecución en el peor de los casos

Suponga que está utilizando la búsqueda simple para buscar a una persona en la guía
telefónica. Sabe que la búsqueda simple tarda O(n) tiempo en ejecutarse, lo que significa que,
en el peor de los casos, tendrá que revisar todas las entradas de su directorio telefónico. En este
caso, estás buscando a Adit. Este tipo es la primera entrada en tu guía telefónica. Por lo tanto,
no tuvo que mirar todas las entradas: las encontró en el primer intento. ¿Este algoritmo tomó
O(n) tiempo?
¿O tomó O (1) tiempo porque encontró a la persona en el primer intento?

La búsqueda simple aún requiere tiempo O(n). En este caso, encontraste lo que buscabas al
instante. Ese es el mejor de los casos. Pero la notación Big O se trata del peor de los casos.
Así que puedes decir que, en el peor de los casos, tendrás que mirar cada entrada en la guía
telefónica una vez.
Eso es O(n) tiempo. Es una garantía: sabe que la búsqueda simple nunca será más lenta que
el tiempo O(n).

Nota
Junto con el tiempo de ejecución del peor de los casos, también es importante observar el
tiempo de ejecución del caso promedio. El peor de los casos versus el caso promedio se analiza
en el capítulo 4.

Algunos tiempos de ejecución comunes de Big O

Aquí hay cinco tiempos de ejecución de Big O que encontrará mucho, ordenados del más
rápido al más lento:

• O(log n), también conocido como tiempo de registro. Ejemplo: búsqueda binaria.

• O(n), también conocido como tiempo lineal. Ejemplo: Búsqueda simple.

• O(n * registro n). Ejemplo: un algoritmo de clasificación rápida, como quicksort


(próximamente en el capítulo 4).

• O(n2 ). Ejemplo: un algoritmo de ordenación lenta, como la ordenación por selección


(en el capítulo 2).

• ¡Sobre!). Ejemplo: un algoritmo realmente lento, como el vendedor ambulante (¡a


continuación!).

Suponga que está dibujando una cuadrícula de 16 cuadros nuevamente y puede elegir entre
5 algoritmos diferentes para hacerlo. Si usa el primer algoritmo, le llevará O (log n) tiempo
dibujar la cuadrícula. Puedes hacer 10 operaciones.
Machine Translated by Google

dieciséis
capitulo 1 yo Introducción a los algoritmos

por segundo. Con el tiempo O (log n), le tomará 4 operaciones dibujar una cuadrícula de
16 cajas (log 16 es 4). Por lo tanto, le llevará 0,4 segundos dibujar la cuadrícula. ¿Qué
pasa si tienes que dibujar 1.024 cajas? Le tomará registrar 1024 = 10 operaciones, o 1
segundo para dibujar una cuadrícula de 1024 cajas.
Estos números están usando el primer algoritmo.

El segundo algoritmo es más lento: toma O(n) tiempo. Se necesitarán 16 operaciones


para sacar 16 cajas, y se necesitarán 1024 operaciones para sacar 1024 cajas. ¿Cuánto
tiempo es eso en segundos?

Este es el tiempo que llevaría dibujar una cuadrícula para el resto de los
algoritmos, del más rápido al más lento:

También hay otros tiempos de ejecución, pero estos son los cinco más comunes.

Esta es una simplificación. En realidad, no se puede convertir de un tiempo de ejecución


de Big O a una serie de operaciones con tanta claridad, pero esto es lo suficientemente
bueno por ahora. Volveremos a la notación Big O en el capítulo 4, después de que haya
aprendido algunos algoritmos más. Por ahora, las conclusiones principales son las
siguientes:

• La velocidad del algoritmo no se mide en segundos, sino en el crecimiento de la


número de operaciones.

• En cambio, hablamos de qué tan rápido aumenta el tiempo de ejecución de un


algoritmo a medida que aumenta el tamaño de la entrada.

• El tiempo de ejecución de los algoritmos se expresa en notación Big O.

• O(log n) es más rápido que O(n), pero se vuelve mucho más rápido a medida que la lista de elementos
estás buscando crece.
Machine Translated by Google

Notación O grande 17

EJERCICIOS
Proporcione el tiempo de ejecución para cada uno de estos escenarios en términos de Big O.

1.3 Tiene un nombre y desea encontrar el número de teléfono de la persona en la


guía telefónica.

1.4 Tiene un número de teléfono y desea encontrar el nombre de la persona en la


guía telefónica. (Pista: ¡Tendrás que buscar en todo el libro!)

1.5 Quiere leer los números de cada persona en la guía telefónica.

1.6 Quiere leer los números de solo el As. (¡Esto es complicado!


Se trata de conceptos que se cubren más en el capítulo 4. Lea la respuesta,
¡puede que se sorprenda!)

El vendedor ambulante
Es posible que haya leído la última sección y haya pensado: "No hay forma de que
me encuentre con un algoritmo que tome O (n!) tiempo". Bueno, ¡déjame intentar
demostrarte que estás equivocado! Aquí hay un ejemplo de un algoritmo con un
tiempo de ejecución realmente malo. Este es un problema famoso en informática,
porque su crecimiento es terrible y algunas personas muy inteligentes piensan que
no se puede mejorar. Se llama el problema del viajante de comercio.

Tienes un vendedor.
Machine Translated by Google

18 capitulo 1 yo Introducción a los algoritmos

El vendedor tiene que ir a cinco ciudades.

Este vendedor, a quien llamaré Opus, quiere llegar a las cinco ciudades
recorriendo la distancia mínima. He aquí una forma de hacerlo: mire todos los
órdenes posibles en los que podría viajar a las ciudades.

Suma la distancia total y luego elige el camino con la distancia más


baja. Hay 120 permutaciones con 5 ciudades, por lo que se necesitarán
120 operaciones para resolver el problema de 5 ciudades. Para 6 ciudades,
se necesitarán 720 operaciones (hay 720 permutaciones). ¡Para 7 ciudades,
se necesitarán 5,040 operaciones!

El número de
operaciones
aumenta drásticamente.
Machine Translated by Google

Resumen 19

En general, para n elementos, se necesitarán n! (n factorial) operaciones para calcular


el resultado. Este es el tiempo O(n!), o tiempo factorial. Se necesitan muchas operaciones para
todo, excepto para los números más pequeños. Una vez que se trata de más de 100 ciudades,
es imposible calcular la respuesta a tiempo: el Sol colapsará primero.

¡Este es un algoritmo terrible! Opus debería usar uno diferente, ¿verdad? Pero no puede. Este es
uno de los problemas no resueltos en informática.
No existe un algoritmo rápido conocido para ello, y las personas inteligentes piensan que es
imposible tener un algoritmo inteligente para este problema. Lo mejor que podemos hacer es llegar
a una solución aproximada; consulte el capítulo 10 para obtener más información.

Una nota final: si eres un lector avanzado, ¡echa un vistazo a los árboles de búsqueda binarios!
Hay una breve descripción de ellos en el último capítulo.

Resumen

• La búsqueda binaria es mucho más rápida que la búsqueda simple.

• O(log n) es más rápido que O(n), pero se vuelve mucho más rápido una vez que la lista de
elementos que está buscando a través de crece.

• La velocidad del algoritmo no se mide en segundos.

• Los tiempos de los algoritmos se miden en términos de crecimiento de un algoritmo.

• Los tiempos de los algoritmos se escriben en notación Big O.


Machine Translated by Google
Machine Translated by Google

clasificación de

selección
2

En este capítulo
• Aprende sobre arreglos y listas enlazadas, dos de las estructuras
de datos más básicas. Se utilizan absolutamente en todas
partes. Ya usó arreglos en el capítulo 1 y los usará en casi
todos los capítulos de este libro. Las matrices son un tema
crucial, ¡así que presta atención!
Pero a veces es mejor usar una lista enlazada en lugar de una
matriz. Este capítulo explica los pros y los contras de ambos
para que pueda decidir cuál es el adecuado para su algoritmo.

• Aprendes tu primer algoritmo de clasificación. Muchos ritmos de


algoritmos solo funcionan si sus datos están ordenados.
¿Recuerdas la búsqueda binaria? Puede ejecutar la búsqueda
binaria solo en una lista ordenada de elementos. Este capítulo
le enseña a clasificar por selección. La mayoría de los idiomas
tienen un algoritmo de clasificación incorporado, por lo que
rara vez necesitará escribir su propia versión desde cero. Pero
la ordenación por selección es un trampolín para la ordenación
rápida, que trataré en el próximo capítulo. Quicksort es un
algoritmo importante, y será más fácil de entender si ya conoce
un algoritmo de clasificación.

21
Machine Translated by Google

22 Capítulo 2 I Clasificación por selección

Lo que necesitas saber


Para comprender los bits de análisis de rendimiento de este capítulo,
debe conocer la notación Big O y los logaritmos. Si no los conoce, le
sugiero que regrese y lea el capítulo 1. La notación Big O se usará
en el resto del libro.

como funciona la memoria


Imagina que vas a un espectáculo y necesitas revisar tus cosas. Una
cómoda está disponible.

Cada cajón puede contener un elemento. Quieres guardar dos cosas,


entonces pides dos cajones.
Machine Translated by Google

como funciona la memoria 23

Guardas tus dos cosas aquí.

¡Y estás listo para el espectáculo! Básicamente, así es como funciona la memoria de


su computadora. Su computadora parece un conjunto gigante de cajones, y cada
cajón tiene una dirección.

/
fe0ffeeb es la dirección de una ranura en la memoria.

Cada vez que desea almacenar un elemento en la memoria, le pide espacio a la


computadora y le proporciona una dirección donde puede almacenar su elemento. Si
desea almacenar varios elementos, hay dos formas básicas de hacerlo: matrices y
listas. A continuación, hablaré de matrices y listas, así como de las ventajas y desventajas
de cada una. No existe una forma correcta de almacenar elementos para cada caso de
uso, por lo que es importante conocer las diferencias.
Machine Translated by Google

24 Capítulo 2 I Clasificación por selección

Matrices y listas enlazadas


A veces es necesario almacenar una lista de elementos en la memoria. Suponga que está
escribiendo una aplicación para administrar sus tareas pendientes. Querrá almacenar todos
los todos como una lista en la memoria.

¿Deberías usar una matriz o una lista enlazada? Primero almacenemos todos en una matriz,
porque es más fácil de entender. El uso de una matriz significa que todas sus tareas se
almacenan de forma contigua (una al lado de la otra) en la memoria.

Ahora suponga que desea agregar una cuarta tarea. ¡Pero el siguiente cajón está ocupado
por las cosas de otra persona!

Es como ir al cine con tus amigos y encontrar un lugar para sentarte—


pero otro amigo se une a ti, y no hay lugar para ellos. Tienes que moverte a un nuevo lugar donde
quepan todos. En este caso, debe pedirle a su computadora una porción diferente de memoria
que pueda adaptarse a cuatro tareas. Entonces necesitas mover todas tus tareas allí.
Machine Translated by Google

Matrices y listas enlazadas 25

Si viene otro amigo, se quedarán sin espacio otra vez, ¡y tendrán que mudarse por segunda vez! Que dolor.

Del mismo modo, agregar nuevos elementos a una matriz puede ser un gran dolor. Si no tiene espacio y

necesita moverse a un nuevo lugar en la memoria cada vez, agregar un nuevo elemento será muy lento.

Una solución fácil es "reservar asientos": incluso si solo tiene 3 elementos en su lista de tareas, puede pedirle

a la computadora 10 espacios, por si acaso. Luego, puede agregar 10 elementos a su lista de tareas sin tener

que moverse. Esta es una buena solución, pero debe tener en cuenta un par de inconvenientes:

• Es posible que no necesite las ranuras adicionales que solicitó, y entonces esa memoria se

desperdiciará. No lo estás usando, pero nadie más puede usarlo tampoco.

• Puede agregar más de 10 elementos a su lista de tareas y debe

muévete de todos modos.

Así que es una buena solución, pero no es una solución perfecta. Las listas enlazadas resuelven este

problema de agregar elementos.

listas enlazadas
Con las listas vinculadas, sus elementos pueden estar en cualquier lugar de la memoria.

Cada elemento almacena la dirección del siguiente elemento de la lista. Un montón de direcciones de

memoria aleatorias están vinculadas entre sí.


Machine Translated by Google

26 Capítulo 2 I Clasificación por selección

Direcciones de
memoria enlazadas

Es como una búsqueda del tesoro. Vas a la primera dirección y dice: "El siguiente elemento se puede

encontrar en la dirección 123". Así que vas a la dirección 123 y dice: "El siguiente elemento se puede encontrar

en la dirección 847", y así sucesivamente. Agregar un elemento a una lista vinculada es fácil: lo coloca en

cualquier lugar de la memoria y almacena la dirección con el elemento anterior.

Con las listas vinculadas, nunca tendrá que mover sus elementos. También te evitas otro problema.

Digamos que vas a una película popular con cinco de tus amigos. Los seis están tratando de encontrar un

lugar para sentarse, pero el teatro está lleno. No hay seis asientos juntos. Bueno, a veces esto sucede con

las matrices. Digamos que está tratando de encontrar 10 000 ranuras para una matriz. Su memoria tiene 10

000 ranuras, pero no tiene 10 000 ranuras juntas. ¡No puedes conseguir espacio para tu matriz! Una lista

enlazada es como decir: "Separémonos y veamos la película". Si hay espacio en la memoria, tiene espacio

para su lista enlazada.

Si las listas enlazadas son mucho mejores en las inserciones, ¿para qué sirven las matrices?

arreglos
Los sitios web con listas de los 10 principales utilizan una táctica sucia para obtener más visitas a la página.

En lugar de mostrarle la lista en una página, colocan un elemento en cada página y le hacen hacer clic en

Siguiente para pasar al siguiente elemento de la lista. Por ejemplo, Top 10 Best TV Villains no le mostrará

la lista completa en una sola página. En cambio, comienza en el n. ° 10 (Newman) y debe hacer clic en

Siguiente en cada página para llegar al n. ° 1 (Gustavo Fring). Esta técnica le da a los sitios web 10 páginas

completas en las que mostrar anuncios, pero es aburrido hacer clic en Siguiente 9 veces para llegar al n.° 1.

Sería mucho mejor si la lista completa estuviera en una página y pudiera hacer clic en el nombre de cada

persona para obtener más información.

Las listas enlazadas tienen un problema similar. Suponga que desea leer el último elemento de una lista

enlazada. No puedes simplemente leerlo, porque no sabes en qué dirección está. En su lugar, debe ir al

elemento n.º 1 para obtener la dirección de


Machine Translated by Google

Matrices y listas enlazadas 27

artículo #2. Luego, debe ir al elemento n. ° 2 para obtener la dirección del elemento n. ° 3.
Y así sucesivamente, hasta llegar al último elemento. Las listas vinculadas son excelentes
si va a leer todos los elementos de uno en uno: puede leer un elemento, seguir la dirección
hasta el siguiente elemento, etc. Pero si vas a seguir dando vueltas, las listas enlazadas son
terribles.

Las matrices son diferentes. Conoce la dirección de cada elemento de su matriz.


Por ejemplo, suponga que su matriz contiene cinco elementos y sabe que comienza en la
dirección 00. ¿Cuál es la dirección del elemento #5?

Las matemáticas simples te dicen: es 04. Las matrices son geniales si quieres leer
elementos aleatorios, porque puedes buscar cualquier elemento en tu matriz al instante.
Con una lista enlazada, los elementos no están uno al lado del otro, por lo que no puede
calcular instantáneamente la posición del quinto elemento en la memoria: debe ir al primer
elemento para obtener la dirección del segundo elemento, luego ir al segundo elemento
para obtener la dirección del tercer elemento, y así sucesivamente hasta llegar al quinto
elemento.

Terminología
Los elementos de una matriz están numerados. Esta numeración comienza desde 0, no
desde 1. Por ejemplo, en esta matriz, 20 está en la posición 1.

Y el 10 está en la posición 0. Esto suele hacer que los programadores nuevos se den
una vuelta. Comenzar en 0 hace que todo tipo de código basado en matrices sea más fácil
de escribir, por lo que los programadores se han quedado con él. Casi todos los lenguajes
de programación que utilice enumerarán los elementos de la matriz a partir de 0. Pronto se
acostumbrará.
Machine Translated by Google

28 Capítulo 2 I Clasificación por selección

La posición de un elemento se llama su índice. Entonces, en lugar de decir "20 está en la posición
1", la terminología correcta es "20 está en el índice 1". Usaré índice para indicar posición a lo largo
de este libro.

Estos son los tiempos de ejecución de operaciones comunes en matrices y listas.

Pregunta: ¿Por qué lleva O(n) tiempo insertar un elemento en una matriz? Suponga que
desea insertar un elemento al comienzo de una matriz. ¿Como lo harias? ¿Cuanto tiempo
tardaría? ¡Encuentre las respuestas a estas preguntas en la siguiente sección!

EJERCICIO
2.1 Suponga que está creando una aplicación para realizar un seguimiento de sus finanzas.

Todos los días, escribes todo en lo que gastaste dinero. Al final del mes, revisas tus gastos
y sumas cuánto gastaste. Entonces, tienes muchas inserciones y algunas lecturas. ¿Deberías
usar una matriz o una lista?
Machine Translated by Google

Matrices y listas enlazadas 29

Insertar en medio de una lista


Suponga que desea que su lista de tareas pendientes funcione más como un calendario.
Anteriormente, estaba agregando cosas al final de la lista.

Ahora desea agregarlos en el orden en que deben hacerse.

desordenado Ordenado

¿Qué es mejor si desea insertar elementos en el medio: matrices o listas? Con las
listas es tan fácil como cambiar a qué apunta el elemento anterior.

Pero para las matrices, debe desplazar el resto de los elementos hacia abajo.

Y si no hay espacio, es posible que deba copiar todo en una nueva ubicación. Las listas
son mejores si desea insertar elementos en el medio.
Machine Translated by Google

30 Capítulo 2 I Clasificación por selección

Eliminaciones
¿Qué sucede si desea eliminar un elemento? Nuevamente, las listas son mejores, porque
solo necesita cambiar lo que apunta el elemento anterior. Con las matrices, todo debe
moverse hacia arriba cuando elimina un elemento.

A diferencia de las inserciones, las eliminaciones siempre funcionarán. Las


inserciones pueden fallar a veces cuando no queda espacio en la memoria. Pero siempre
puedes eliminar un elemento.

Estos son los tiempos de ejecución de las operaciones comunes en matrices y


listas vinculadas.

Vale la pena mencionar que las inserciones y eliminaciones son O (1) tiempo solo si puede
acceder instantáneamente al elemento que se eliminará. Es una práctica común realizar un
seguimiento del primer y último elemento de una lista vinculada, por lo que solo se necesitaría
O (1) tiempo para eliminarlos.

¿Cuáles se usan más: arrays o listas? Obviamente, depende del caso de uso. Pero las
matrices tienen mucho uso porque permiten el acceso aleatorio. Hay dos tipos diferentes de
acceso: acceso aleatorio y acceso secuencial.
El acceso secuencial significa leer los elementos uno por uno, comenzando por el
primer elemento. Las listas enlazadas solo pueden hacer acceso secuencial. Si desea
leer el décimo elemento de una lista enlazada, debe leer los primeros 9 elementos y seguir
los enlaces hasta el décimo elemento. El acceso aleatorio significa que puede saltar

directamente al décimo elemento. Con frecuencia me escuchará decir que las matrices son
más rápidas en las lecturas. Esto se debe a que proporcionan acceso aleatorio. Muchos
casos de uso requieren acceso aleatorio, por lo que las matrices se usan mucho. Las matrices
y las listas también se utilizan para implementar otras estructuras de datos (más adelante en
el libro).
Machine Translated by Google

Matrices y listas enlazadas 31

EJERCICIOS
2.2 Suponga que está creando una aplicación para que los restaurantes lleven a los clientes
pedidos. Su aplicación necesita almacenar una lista de pedidos. Los meseros
continúan agregando pedidos a esta lista, y los chefs quitan pedidos de la lista y los preparan.
Es una cola de pedidos: los servidores agregan pedidos al final de la cola y el chef
toma el primer pedido de la cola y lo cocina.

¿Usaría una matriz o una lista enlazada para implementar esta cola?
(Sugerencia: las listas vinculadas son buenas para inserciones/eliminaciones, y las
matrices son buenas para el acceso aleatorio. ¿Cuál va a hacer aquí?)

2.3 Hagamos un experimento mental. Supongamos que Facebook mantiene una lista de
nombres de usuario. Cuando alguien intenta iniciar sesión en Facebook, se realiza una
búsqueda de su nombre de usuario. Si su nombre está en la lista de nombres de usuario,

pueden iniciar sesión. Las personas inician sesión en Facebook con bastante frecuencia,
por lo que hay muchas búsquedas a través de esta lista de nombres de usuario.
Supongamos que Facebook usa la búsqueda binaria para buscar en la lista. La búsqueda
binaria necesita acceso aleatorio: debe poder llegar al medio de la lista de nombres de
usuario al instante. Sabiendo esto, ¿implementaría la lista como una matriz o como una
lista enlazada?

2.4 Las personas también se registran en Facebook con bastante frecuencia. Suponga que decide
usar una matriz para almacenar la lista de usuarios. ¿Cuáles son las desventajas de una
matriz para inserciones? En particular, suponga que está utilizando la búsqueda binaria para
buscar inicios de sesión. ¿Qué sucede cuando agrega nuevos usuarios a una matriz?

2.5 En realidad, Facebook no utiliza ni una matriz ni una lista enlazada para almacenar la
información del usuario. Consideremos una estructura de datos híbrida: una matriz de
listas enlazadas. Tienes una matriz con 26 ranuras. Cada ranura apunta a una lista
enlazada. Por ejemplo, el primer espacio en la matriz apunta a una lista vinculada que
contiene todos los nombres de usuario que comienzan con a. El segundo espacio apunta a
una lista enlazada que contiene todos los nombres de usuario que comienzan con b, y así
sucesivamente.
Machine Translated by Google

32 Capítulo 2 I Clasificación por selección

Suponga que Adit B se registra en Facebook y desea agregarlos a la lista. Vaya a la


ranura 1 en la matriz, vaya a la lista vinculada para la ranura 1 y agregue Adit B al final.
Ahora, suponga que desea buscar a Zakhir H. Vaya al espacio 26, que apunta a una lista
vinculada de todos los nombres de Z. Luego busca en esa lista para encontrar a Zakhir H.

Compare esta estructura de datos híbrida con arreglos y listas enlazadas. ¿Es más lento
o más rápido que cada uno para buscar e insertar? No tiene que dar tiempos de ejecución
de Big O, solo si la nueva estructura de datos sería más rápida o más lenta.

Clasificación de selección

Pongamos todo junto para aprender su segundo algoritmo: clasificación


por selección. Para seguir esta sección, debe comprender las matrices y
las listas, así como la notación Big O.

Suponga que tiene un montón de música en su computadora.


Para cada artista, tienes un conteo de reproducción.

Desea ordenar esta lista de más a menos reproducidos, de modo que pueda clasificar a sus artistas
favoritos. ¿Cómo puedes hacerlo?
Machine Translated by Google

Clasificación de selección 33

Una forma es revisar la lista y encontrar el artista más reproducido. Agregue ese artista a una nueva
lista.

Hazlo de nuevo para encontrar al siguiente artista más reproducido.

Sigue haciendo esto y terminarás con una lista ordenada.


Machine Translated by Google

34 Capítulo 2 I Clasificación por selección

Pongámonos el sombrero de informáticos y veamos cuánto tardará en ejecutarse. Recuerde que


el tiempo O(n) significa que toca todos los elementos de una lista una vez. Por ejemplo, ejecutar
una búsqueda simple en la lista de artistas significa mirar a cada artista una vez.

Para encontrar al artista con el mayor número de reproducciones, debe verificar cada
elemento de la lista. Esto toma O(n) tiempo, como acabas de ver. Así que tienes una
operación que toma O(n) tiempo, y tienes que hacer eso n veces:

Esto requiere un tiempo O(n × n) o un tiempo O(n2 ).

Los algoritmos de clasificación son muy útiles. Ahora puedes ordenar

• Nombres en una guía telefónica

• Fechas de viaje

• Correos electrónicos (del más reciente al más antiguo)


Machine Translated by Google

Clasificación de selección 35

Comprobación de menos elementos cada vez


Tal vez te estés preguntando: a medida que avanzas en las operaciones, la cantidad
de elementos que tienes que verificar sigue disminuyendo. Eventualmente, tendrá que
marcar solo un elemento. Entonces, ¿cómo puede el tiempo de ejecución seguir siendo
O(n2 )? Esa es una buena pregunta, y la respuesta tiene que ver con constantes en
notación Big O. Me adentraré más en esto en el capítulo 4, pero aquí está la esencia.

Tiene razón en que no tiene que verificar una lista de n elementos cada vez.
Verifica n elementos, luego n – 1, n - 2 … 2, 1. En promedio, verifica una lista que tiene
1/2 × n elementos. El tiempo de ejecución es O(n × 1/2 × n). Pero las constantes como
1/2 se ignoran en la notación Big O (nuevamente, vea el capítulo 4 para la discusión
completa), así que simplemente escriba O(n × n) u O(n2 ).

La ordenación por selección es un buen algoritmo, pero no es muy rápido. Quicksort es un


algoritmo de clasificación más rápido que solo toma el tiempo O (n log n). ¡Saldrá en el próximo
capítulo!

LISTA DE CÓDIGOS DE EJEMPLO


No le mostramos el código para ordenar la lista de música, pero a continuación hay un código
que hará algo muy similar: ordenar una matriz de menor a mayor. Escribamos una función
para encontrar el elemento más pequeño en una matriz:

def findSmallest(arr): más pequeño


= arr[0] índice_más pequeño Almacena el valor más pequeño
= 0 for i in range(1, len(arr)): Almacena el índice del valor más pequeño.

si arr[i] < más pequeño:


menor = arr[i]
índice_más pequeño = yo
devolver el índice_más pequeño

Ahora puede usar esta función para escribir el ordenamiento por selección:

def selecciónOrdenar(arr): nuevaArr Ordena una matriz


= []
para i en el rango (len (arr)):
más pequeño = encontrarMásPequeño(arr) Encuentra el elemento más pequeño en el
newArr.append(arr.pop(menor)) matriz, y lo agrega a la nueva matriz
volver nuevoArr

imprimir selección Ordenar ([5, 3, 6, 2, 10])


Machine Translated by Google

36 Capítulo 2 I Clasificación por selección

Resumen

• La memoria de su computadora es como un conjunto gigante de cajones.

• Cuando desee almacenar varios elementos, utilice una matriz o una lista.

• Con una matriz, todos sus elementos se almacenan uno al lado del otro.

• Con una lista, los elementos están esparcidos por todas partes y un elemento
almacena la dirección del siguiente.

• Las matrices permiten lecturas rápidas.

• Las listas vinculadas permiten inserciones y eliminaciones rápidas.

• Todos los elementos de la matriz deben ser del mismo tipo (todos enteros,
todos dobles, etc.).
Machine Translated by Google

recursión 3

En este capítulo
• Aprendes sobre la recursividad. La recursividad es una codificación.
técnica utilizada en muchos algoritmos. Es un elemento básico
para comprender los capítulos posteriores de este libro.

• Aprende cómo dividir un problema en un caso base y un caso


recursivo. La estrategia divide y vencerás (capítulo 4) utiliza

este concepto simple para resolver problemas difíciles.

Estoy entusiasmado con este capítulo porque cubre la recursividad, una


forma elegante de resolver problemas. La recursividad es uno de mis temas
favoritos, pero es divisivo. La gente lo ama o lo odia, o lo odia hasta que aprende a
amarlo unos años más tarde. Yo personalmente estaba en ese tercer campo. Para
facilitarte las cosas, te doy algunos consejos:

• Este capítulo tiene muchos ejemplos de código. Ejecute el código usted mismo para
ver cómo funciona.

• Hablaré de funciones recursivas. Al menos una vez, pase por un


función recursiva con lápiz y papel: algo como, “Veamos, paso 5 a factorial, y luego
vuelvo 5 veces pasando 4 a factorial, que es…”, y así sucesivamente. Recorrer una
función como esta le enseñará cómo funciona una función recursiva.

37
Machine Translated by Google

38 Capítulo 3 I Recurrencia

Este capítulo también incluye una gran cantidad de pseudocódigo. El pseudocódigo


es una descripción de alto nivel del problema que está tratando de resolver, en código.
Está escrito como un código, pero está destinado a estar más cerca del habla humana.

recursividad
Suponga que está excavando en el ático de su abuela y se encuentra con una misteriosa maleta
cerrada con llave.

La abuela te dice que la llave de la maleta probablemente esté en esta otra caja.

Esta caja contiene más cajas, con más cajas dentro de esas cajas. La llave está en una caja
en alguna parte. ¿Cuál es tu algoritmo para buscar la clave?
Piense en un algoritmo antes de seguir leyendo.
Machine Translated by Google

recursividad 39

Aquí hay un enfoque.

1. Haz una pila de cajas para mirar.

2. Tome una caja y mire a través de ella.

3. Si encuentra una caja, agréguela a la pila para revisarla más tarde.

4. Si encuentra una llave, ¡ya está!

5. Repita.

Aquí hay un enfoque alternativo.

1. Mire a través de la caja.

2. Si encuentra una casilla, vaya al paso 1.

3. Si encuentra una llave, ¡ya está!


Machine Translated by Google

40 Capítulo 3 I Recurrencia

¿Qué enfoque te parece más fácil? El primer enfoque utiliza un tiempo


círculo. Si bien la pila no está vacía, tome una caja y mire a través de ella:

def buscar_clave(caja_principal):
pila = main_box.make_a_pile_to_look_through()
mientras que la pila no está vacía:
caja = pila.grab_a_box()
para artículo en caja:
si item.is_a_box():
pile.append(elemento)
elif elemento.es_una_clave():
imprimir "¡encontré la llave!"

La segunda forma utiliza la recursividad. La recursividad es donde una función se llama a sí misma.
Aquí está la segunda forma en pseudocódigo:

def buscar_clave(caja):
para artículo en caja:
si item.is_a_box():
buscar_clave(elemento) elif ¡Recursión!
elemento.es_una_clave():
imprimir "¡encontré la llave!"

Ambos enfoques logran lo mismo, pero el segundo enfoque es más claro para mí. La recursividad
se usa cuando aclara la solución.

No hay ningún beneficio de rendimiento en el uso de la recursividad; de hecho, los bucles a


veces son mejores para el rendimiento. Me gusta esta cita de Leigh Caldwell sobre Stack
Overflow: “Los bucles pueden lograr una mejora en el rendimiento de su programa. La
recursividad puede lograr una ganancia de rendimiento para su programador. ¡Elige cuál es
más importante en tu situación!”1

Muchos algoritmos importantes usan recursividad, por lo que es importante


comprender el concepto.

Caso base y caso recursivo

Debido a que una función recursiva se llama a sí misma, es fácil escribir una
función incorrectamente que termine en un bucle infinito. Por ejemplo, suponga
que desea escribir una función que imprima una cuenta regresiva, como esta:

> 3...2...1

1 http://stackoverflow.com/a/72694/139117.
Machine Translated by Google

Caso base y caso recursivo 41

Puedes escribirlo recursivamente, así:


def cuenta regresiva(i):
imprimir yo
cuenta regresiva (i-1)

Escriba este código y ejecútelo. Notarás un problema: ¡esta función se


ejecutará para siempre!

Bucle infinito

> 3...2...1...0...-1...-2...

(Presione Ctrl-C para eliminar su secuencia de comandos).

Cuando escribes una función recursiva, tienes que decirle cuándo dejar de
recurrir. Es por eso que cada función recursiva tiene dos partes: el caso base
y el caso recursivo. El caso recursivo es cuando la función se llama a sí misma.
El caso base es cuando la función no se vuelve a llamar a sí misma... por lo que
no entra en un bucle infinito.
Agreguemos un caso base a la función de cuenta regresiva:

def cuenta regresiva(i):


imprimir yo
si i <= 0: volver Caso base

demás: caso recursivo


cuenta regresiva (i-1)

Ahora la función funciona como se esperaba. Es algo parecido a esto.


Machine Translated by Google

42 Capítulo 3 I Recurrencia

La pila

Esta sección cubre la pila de llamadas. Es un concepto importante en la


programación. La pila de llamadas es un concepto importante en la programación
general, y también es importante entenderlo cuando se usa la recursividad.

Supongamos que estás organizando una barbacoa. Mantienes una lista de cosas por
hacer para la barbacoa, en forma de una pila de notas adhesivas.

¿Recuerdas cuando hablamos de arreglos y listas, y tenías una lista de


tareas pendientes? Puede agregar elementos pendientes en cualquier
lugar de la lista o eliminar elementos aleatorios. La pila de notas adhesivas
es mucho más simple. Cuando inserta un elemento, se agrega a la parte
superior de la lista. Cuando lee un elemento, solo lee el elemento superior y se
elimina de la lista. Por lo tanto, su lista de tareas tiene solo dos acciones: empujar (insertar) y
hacer estallar (eliminar y leer).

Veamos la lista de tareas pendientes en acción.

Esta estructura de datos se llama pila. La pila es una estructura de datos simple.
¡Has estado usando una pila todo este tiempo sin darte cuenta!
Machine Translated by Google

La pila 43

La pila de llamadas

Su computadora usa una pila internamente llamada pila de llamadas. Veámoslo en


acción. Aquí hay una función simple:

def saludar(nombre):
imprimir "hola", saludar2 + nombre + “!”
(nombre)
print "preparándome para decir adiós..."
adiós()

Esta función lo saluda y luego llama a otras dos funciones. Aquí están esas dos
funciones:

def saludo2(nombre):
“ + nombre + “?”
escribe “¿cómo estás,

definitivamente adios():

escribe “ok adiós!”

Veamos qué sucede cuando llamas a una función.

Nota
print es una función en Python, pero para facilitar las cosas en este ejemplo,
pretendemos que no lo es. Solo sigue el juego.

Supongamos que llamas saludo ("maggie"). Primero, su computadora asigna una caja
de memoria para esa llamada de función.

Ahora usemos la memoria. El nombre de la variable se establece en "maggie". Eso


hay que guardarlo en la memoria.
Machine Translated by Google

44 Capítulo 3 I Recurrencia

Cada vez que realiza una llamada de función, su computadora guarda los valores de
todas las variables para esa llamada en la memoria de esta manera. A continuación, imprime
hola, maggie! Luego llamas a greeting2 ("maggie"). Nuevamente, su
computadora asigna una caja de memoria para esta llamada de función.

Su computadora está usando una pila para estas cajas. El segundo cuadro se agrega encima
del primero. Imprimes ¿cómo estás, maggie? Luego regresa de la llamada a la función.
Cuando esto sucede, la caja en la parte superior de la pila se abre.

Ahora, el cuadro superior de la pila es para la función de saludo , lo que significa que
regresó a la función de saludo . Cuando llamó a la función de saludo2 , la función de saludo
se completó parcialmente. Esta es la gran idea detrás de esta sección: cuando llama a una
función desde otra función, la función que llama se detiene en un estado parcialmente
completado. Todos los valores de las variables para esa función aún se almacenan en la
memoria.
Ahora que ha terminado con la función saludar2 , ha vuelto a la función saludar y
continúa donde la dejó. Primero imprimes preparándote para decir adiós…. Llamas a
la función bye .
Machine Translated by Google

La pila 45

Se agrega un cuadro para esa función en la parte superior de la pila. Entonces imprimes ok bye!
y regresa de la llamada a la función.

Y vuelves a la función de saludo . No hay nada más que hacer, por lo que también regresa de la
función de saludo . Esta pila, que se usa para guardar las variables para varias funciones, se
denomina pila de llamadas.

EJERCICIO
3.1 Suponga que le muestro una pila de llamadas como esta.

¿Qué información me puede dar, solo en función de esta pila de llamadas?

Ahora veamos la pila de llamadas en acción con una función recursiva.

La pila de llamadas con recursividad


¡Las funciones recursivas también usan la pila de llamadas! Veamos esto en acción con la

función factorial . factorial(5) se escribe como 5!, y se define así: 5! = 5 * 4 * 3 * 2 * 1. De

manera similar, factorial(3) es 3 * 2 * 1. Aquí hay una función recursiva para calcular el factorial
de un número:

def hecho(x):
si x == 1:
volver 1
demás:
volver x * hecho(x-1)

Ahora llamas fact(3). Repasemos esta llamada línea por línea y veamos cómo cambia la pila.
Recuerde, el cuadro superior de la pila le indica en qué llamada al hecho se encuentra
actualmente.
Machine Translated by Google

46 Capítulo 3 I Recurrencia
Machine Translated by Google

La pila 47

Observe que cada llamada al hecho tiene su propia copia de x. No puede acceder a
la copia de una función diferente de x.

La pila juega un papel importante en la recursividad. En el ejemplo inicial, hubo dos


enfoques para encontrar la clave. Aquí está la primera forma de nuevo.

De esta manera, crea una pila de cajas para buscar, para que siempre sepa qué
cajas aún necesita buscar.
Machine Translated by Google

48 Capítulo 3 I Recurrencia

Pero en el enfoque recursivo, no hay pila.

Si no hay pila, ¿cómo sabe su algoritmo qué cajas aún tiene


que revisar? Aquí hay un ejemplo.
Machine Translated by Google

La pila 49

En este punto, la pila de llamadas se ve así.

¡La "pila de cajas" se guarda en la pila! Esta es una pila de llamadas de


función a medio completar, cada una con su propia lista de cuadros a medio completar
para revisar. El uso de la pila es conveniente porque no tiene que realizar un seguimiento
de una pila de cajas, la pila lo hace por usted.

Usar la pila es conveniente, pero tiene un costo: guardar toda esa información puede
consumir mucha memoria. Cada una de esas llamadas de función ocupa algo de
memoria, y cuando su pila es demasiado alta, eso significa que su computadora está
guardando información para muchas llamadas de función. En ese momento, tienes
dos opciones:

• Puede reescribir su código para usar un bucle en su lugar.

• Puede usar algo llamado recursividad de cola. Ese es un tema de recursión


avanzada que está fuera del alcance de este libro. Además, solo es compatible
con algunos idiomas, no con todos.

EJERCICIO
3.2 Suponga que accidentalmente escribe una función recursiva que se
ejecuta para siempre. Como vio, su computadora asigna memoria en
la pila para cada llamada de función. ¿Qué sucede con la pila cuando su
función recursiva se ejecuta para siempre?
Machine Translated by Google

50 Capítulo 3 I Recurrencia

Resumen

• La recursividad es cuando una función se llama a sí misma.

• Toda función recursiva tiene dos casos: el caso base y el caso recursivo.

• Una pila tiene dos operaciones: empujar y sacar.

• Todas las llamadas a funciones van a la pila de llamadas.

• La pila de llamadas puede volverse muy grande, lo que consume mucha memoria.
Machine Translated by Google

ordenación rápida 4

En este capítulo
• Aprendes sobre divide y vencerás. A veces te encontrarás con
un problema que no se puede resolver con ningún algoritmo
que hayas aprendido. Cuando un buen algoritmo se encuentra
con un problema así, no se da por vencido. Tienen una caja
de herramientas llena de técnicas que utilizan en el problema,
tratando de encontrar una solución. Divide y vencerás es la
primera técnica general que aprendes.

• Aprendes sobre quicksort, una clasificación elegante


algoritmo que se utiliza a menudo en la práctica. Quicksort
utiliza divide y vencerás.

Aprendiste todo sobre la recursividad en el último capítulo. Este capítulo se


centra en el uso de su nueva habilidad para resolver problemas. Exploraremos
divide y vencerás (D&C), una conocida técnica recursiva para resolver
problemas.

Este capítulo realmente entra en la carne de los algoritmos. Después de


todo, un algoritmo no es muy útil si solo puede resolver un tipo de
problema. En cambio, D&C le brinda una nueva forma de pensar acerca de cómo resolver

51
Machine Translated by Google

52 Capítulo 4 I Clasificación rápida

problemas. D&C es otra herramienta en su caja de herramientas. Cuando tienes un nuevo


problema, no tienes que estar perplejo. En su lugar, puedes preguntar: "¿Puedo resolver esto si
uso divide y vencerás?"

Al final del capítulo, aprenderá su primer algoritmo importante de D&C: clasificación rápida. Quicksort
es un algoritmo de clasificación, mucho más rápido que la clasificación por selección (que aprendiste
en el capítulo 2). Es un buen ejemplo de código elegante.

Divide y vencerás
D&C puede tomar algún tiempo para comprender. Entonces, haremos
tres ejemplos. Primero les mostraré un ejemplo visual. Luego haré un
ejemplo de código que es menos bonito pero quizás más fácil. Finalmente,
revisaremos Quicksort, un algoritmo de clasificación que usa D&C.

Supongamos que eres un agricultor con una parcela de tierra.

Desea dividir esta granja de manera uniforme en parcelas cuadradas. Quieres que las parcelas sean
lo más grandes posible. Así que ninguno de estos funcionará.
Machine Translated by Google

Divide y vencerás 53

¿Cómo calculas el tamaño cuadrado más grande que puedes usar para un terreno? ¡Usa
la estrategia D&C! Los algoritmos D&C son algoritmos recursivos.
Para resolver un problema usando D&C, hay dos pasos:

1. Averigüe el caso base. Este debería ser el caso más simple posible.

2. Divida o disminuya su problema hasta que se convierta en el caso base.

Usemos D&C para encontrar la solución a este problema. ¿Cuál es el tamaño de cuadrado
más grande que puedes usar?

Primero, averigüe el caso base. El caso más fácil sería si un lado fuera múltiplo del otro lado.

Supongamos que un lado mide 25 metros (m) y el otro mide 50 m. Entonces, la caja más
grande que puedes usar es de 25 m × 25 m. Necesitas dos de esas cajas para dividir la
tierra.

Ahora necesitas averiguar el caso recursivo. Aquí es donde entra D&C. De acuerdo
con D&C, con cada llamada recursiva, debe reducir su problema. ¿Cómo se reduce el
problema aquí? Comencemos marcando las cajas más grandes que puedes usar.
Machine Translated by Google

54 Capítulo 4 I Clasificación rápida

Puede colocar dos cajas de 640 × 640 allí, y aún queda algo de tierra por dividir.
Ahora aquí viene el "¡Ajá!" momento. Queda un segmento de granja por dividir.
¿Por qué no aplicas el mismo algoritmo a este segmento?

Entonces, comenzó con una granja de 1680 × 640 que necesitaba dividirse.
Pero ahora necesita dividir un segmento más pequeño, 640 × 400. Si encuentra el
cuadro más grande que funcionará para este tamaño, ese será el cuadro más grande
que funcionará para toda la granja. ¡Acaba de reducir el problema de una granja de
1680 × 640 a una granja de 640 × 400!

Algoritmo de Euclides

“Si encuentra la caja más grande que funcionará para este tamaño, esa
será la caja más grande que funcionará para toda la granja”. Si no le resulta
obvio por qué esta afirmación es cierta, no se preocupe. No es obvio.
Desafortunadamente, la prueba de por qué funciona es demasiado larga para
incluirla en este libro, por lo que tendrá que creerme que funciona. Si quieres
entender la prueba, busca el algoritmo de Euclides. La academia Khan tiene
una buena explicación aquí: https://www.khanacademy.org/computing/computer-science/
criptografía/modaritmética/a/el-algoritmo-euclidiano.

Apliquemos el mismo algoritmo de nuevo. Comenzando


con una granja de 640 × 400 m, la caja más grande que
puede crear es de 400 × 400 m.
Machine Translated by Google

Divide y vencerás 55

Y eso te deja con un segmento más pequeño, 400 × 240 m.

Y puedes dibujar un cuadro en eso para obtener un segmento aún más pequeño,
240 × 160 m.

Y luego dibujas un cuadro sobre eso para obtener un segmento aún más pequeño.

Oye, estás en el caso base: 80 es un factor de 160. Si divides este segmento usando
cajas, ¡no te sobra nada!
Machine Translated by Google

56 Capítulo 4 I Clasificación rápida

Entonces, para la granja original, el tamaño de parcela más grande que puede usar es de 80 × 80 m.

En resumen, así es como funciona D&C:

1. Calcule un caso simple como caso base.

2. Averigüe cómo reducir su problema y llegue al caso base.

D&C no es un algoritmo simple que pueda aplicar a un problema. En cambio, es una


manera de pensar en un problema. Hagamos un ejemplo más.

Te dan una serie de números.

Tienes que sumar todos los números y devolver el total. Es bastante fácil hacer esto
con un bucle:
def suma(arr):
totales = 0
para x en arr:
total += x
devolución total

imprimir suma ([1, 2, 3, 4])

Pero, ¿cómo harías esto con una función recursiva?

Paso 1: Averiguar el caso base. ¿Cuál es la matriz más simple que podría
obtener? Piense en el caso más simple y luego siga leyendo. Si obtiene una
matriz con 0 o 1 elemento, es bastante fácil de resumir.
Machine Translated by Google

Divide y vencerás 57

Así que ese será el caso base.

Paso 2: debe acercarse a una matriz vacía con cada llamada recursiva.
¿Cómo reduce el tamaño de su problema? Aquí hay una forma.

Es lo mismo que esto.

En cualquier caso, el resultado es 12. Pero en la segunda versión, está pasando


una matriz más pequeña a la función de suma . Es decir, ¡disminuyó el tamaño de
su problema!
Su función de suma podría funcionar así.
Machine Translated by Google

58 Capítulo 4 I Clasificación rápida

Aquí está en acción.

Recuerde, la recursividad realiza un seguimiento del estado.

Propina

Cuando está escribiendo una función recursiva que involucra una matriz, el caso base suele
ser una matriz vacía o una matriz con un elemento. Si estás atascado, prueba eso primero.
Machine Translated by Google

Divide y vencerás 59

Un vistazo a la programación funcional


"¿Por qué haría esto recursivamente si puedo hacerlo fácilmente con un ciclo?"
usted puede estar pensando. Bueno, ¡este es un adelanto de la programación funcional!
Los lenguajes de programación funcional como Haskell no tienen bucles, por lo que
debe usar la recursividad para escribir funciones como esta. Si tiene una buena
comprensión de la recursividad, los lenguajes funcionales serán más fáciles de aprender.
Por ejemplo, así es como escribiría una función de suma en Haskell:

suma [] = 0 Caso base


suma (x:xs) = x + (suma xs) caso recursivo

Observe que parece que tiene dos definiciones para la función. La primera definición
se ejecuta cuando llega al caso base. La segunda definición se ejecuta en el caso
recursivo. También puede escribir esta función en Haskell usando una declaración if:

suma arr = si arr == []


entonces 0
else (cabeza arr) + (suma (cola arr))

Pero la primera definición es más fácil de leer. Debido a que Haskell hace un uso
intensivo de la recursividad, incluye todo tipo de sutilezas como esta para facilitar la
recursividad. Si le gusta la recursividad o está interesado en aprender un nuevo
idioma, consulte Haskell.

EJERCICIOS
4.1 Escriba el código de la función de suma anterior .

4.2 Escriba una función recursiva para contar el número de elementos en una lista.

4.3 Encuentra el número máximo en una lista.

4.4 ¿Recuerdas la búsqueda binaria del capítulo 1? También es un algoritmo de divide


y vencerás. ¿Puedes encontrar el caso base y el caso recursivo para la búsqueda
binaria?
Machine Translated by Google

60 Capítulo 4 I Clasificación rápida

Ordenación rápida

Quicksort es un algoritmo de clasificación. Es mucho más rápido que el ordenamiento


por selección y se usa con frecuencia en la vida real. Por ejemplo, la biblioteca estándar
de C tiene una función llamada qsort, que es su implementación de quicksort.
Quicksort también usa D&C.

Usemos quicksort para ordenar una matriz. ¿Cuál es la matriz más simple que puede
manejar un algoritmo de clasificación (recuerda mi consejo de la sección anterior)?
Bueno, algunas matrices no necesitan ordenarse en absoluto.

Las matrices vacías y las matrices con un solo elemento serán el caso base. Puede devolver
esas matrices tal como están; no hay nada que ordenar:

def ordenación rápida (matriz):


si len(matriz) < 2:
matriz de retorno

Veamos arreglos más grandes. Una matriz con dos elementos es bastante fácil de
ordenar, también.

¿Qué pasa con una matriz de tres elementos?

Recuerde, está usando D&C. Entonces, desea desglosar esta matriz hasta que esté en
el caso base. Así es como funciona Quicksort. Primero, elija un elemento de la matriz.
Este elemento se llama pivote.

Hablaremos sobre cómo elegir un buen pivote más adelante. Por ahora,
digamos que el primer elemento de la matriz es el pivote.
Machine Translated by Google

Ordenación rápida 61

Ahora encuentre los elementos más pequeños que el pivote y los elementos más
grandes que el pivote.

Esto se llama partición. Ahora tu tienes

• Un subarreglo de todos los números menores que el pivote

• El pivote

• Un subarreglo de todos los números mayores que el pivote

Los dos subarreglos no están ordenados. Solo están particionados. Pero si estuvieran
ordenados, ordenar toda la matriz sería bastante fácil.

Si los subconjuntos están ordenados, puede combinar todo de esta manera: conjunto
izquierdo + pivote + conjunto derecho, y obtendrá un conjunto ordenado. En este caso,
es [10, 15] + [33] + [] = [10, 15, 33], que es una matriz ordenada.

¿Cómo se ordenan los sub-arreglos? Bueno, el caso base de ordenación rápida ya


sabe cómo ordenar arreglos de dos elementos (el subarreglo izquierdo) y arreglos
vacíos (el subarreglo derecho). Entonces, si llama a la ordenación rápida en las dos
sub-matrices y luego combina los resultados, ¡obtiene una matriz ordenada!

clasificación rápida ([15, 10]) + [33] + clasificación rápida ([])


> [10, 15, 33] Una matriz ordenada
Machine Translated by Google

62 Capítulo 4 I Clasificación rápida

Esto funcionará con cualquier pivote. Suponga que elige 15 como pivote.

Ambos subconjuntos tienen un solo elemento, y usted sabe cómo ordenarlos. Así
que ahora sabes cómo ordenar una matriz de tres elementos. Aquí están los pasos:

1. Elija un pivote.

2. Divida la matriz en dos sub-matrices: elementos menores que el pivote


y elementos mayores que el pivote.

3. Llame a quicksort de forma recursiva en los dos subarreglos.

¿Qué pasa con una matriz de cuatro elementos?

Suponga que elige 33 como pivote nuevamente.

La matriz de la izquierda tiene tres elementos. Ya sabe cómo ordenar una matriz de tres
elementos: llame a Quicksort recursivamente.
Machine Translated by Google

Ordenación rápida 63

Entonces puede ordenar una matriz de cuatro elementos. Y si puede ordenar una matriz
de cuatro elementos, puede ordenar una matriz de cinco elementos. ¿Porqué es eso?
Suponga que tiene esta matriz de cinco elementos.

Estas son todas las formas en que puede particionar esta matriz, según el pivote que
elija.

Tenga en cuenta que todos estos subconjuntos tienen entre 0 y 4 elementos. ¡Y ya


sabe cómo ordenar una matriz de 0 a 4 elementos usando Quicksort! Por lo tanto,
independientemente del pivote que elija, puede realizar una ordenación rápida de forma
recursiva en los dos subconjuntos.
Machine Translated by Google

64 Capítulo 4 I Clasificación rápida

Por ejemplo, suponga que elige 3 como pivote. Llamas a quicksort en los sub-arreglos.

Las sub-matrices se ordenan y luego se combina todo para obtener una matriz ordenada. Esto
funciona incluso si elige 5 como pivote.

Esto funciona con cualquier elemento como pivote. Entonces puede ordenar una matriz
de cinco elementos. Usando la misma lógica, puede ordenar una matriz de seis
elementos, y así sucesivamente.
Machine Translated by Google

Ordenación rápida sesenta y cinco

Pruebas inductivas
¡Acabas de obtener un adelanto de las pruebas inductivas! Las pruebas inductivas
son una forma de probar que su algoritmo funciona. Cada prueba inductiva tiene dos
pasos: el caso base y el caso inductivo. ¿Suena familiar? Por ejemplo, supongamos
que quiero demostrar que puedo subir a lo alto de una escalera. En el caso inductivo,
si mis piernas están en un peldaño, puedo poner mis piernas en el siguiente peldaño.
Entonces, si estoy en el peldaño 2, puedo subir al peldaño 3. Ese es el caso inductivo.
Para el caso base, diré que mis piernas están en el peldaño 1. Por lo tanto, puedo
subir toda la escalera, subiendo un peldaño a la vez.

Utiliza un razonamiento similar para quicksort. En el caso base, mostré que el algoritmo
funciona para el caso base: matrices de tamaño 0 y 1. En el caso inductivo, mostré
que si Quicksort funciona para una matriz de tamaño 1, funcionará para una matriz de
tamaño 2 Y si funciona para arreglos de tamaño 2, funcionará para arreglos de tamaño
3, y así sucesivamente. Entonces puedo decir que Quicksort funcionará para todas las
matrices de cualquier tamaño. No profundizaré en las pruebas inductivas aquí, pero
son divertidas y van de la mano con D&C.

Aquí está el código para Quicksort:

def ordenación rápida (matriz):


si len(matriz) < 2:
matriz de retorno Caso base: las matrices con 0 o 1 elemento ya están "ordenadas".
más:
pivote = matriz [0] caso recursivo
menos = [i for i in array[1:] if i <= pivote] Sub-arreglo de todos los
elementos menos que el pivote
mayor = [i for i in array[1:] if i > pivot] Sub-arreglo de todos los
elementos mayores que el pivote
return clasificación rápida (menos) + [pivote] + clasificación rápida (mayor)

imprimir clasificación rápida ([10, 5, 2, 3])


Machine Translated by Google

66 Capítulo 4 I Clasificación rápida

Notación Big O revisada


Quicksort es único porque su velocidad depende del pivote que elija.

Antes de hablar sobre ordenación rápida, echemos un vistazo a los tiempos de ejecución más comunes de

Big O nuevamente.

Estimaciones basadas en una computadora lenta que realiza 10 operaciones por segundo

Los tiempos de ejemplo en este gráfico son estimaciones si realiza 10 operaciones por segundo.

Estos gráficos no son precisos, solo están ahí para darle una idea de cuán diferentes son estos tiempos

de ejecución. En realidad, su computadora puede hacer más de 10 operaciones por segundo.

Cada tiempo de ejecución también tiene adjunto un algoritmo de ejemplo. Revisa el ordenamiento

por selección, que aprendiste en el capítulo 2. Es O(n2 ). Ese es un algoritmo bastante lento.

Hay otro algoritmo de clasificación llamado merge sort, que es O(n log n). ¡Mucho mas

rápido! Quicksort es un caso complicado. En el peor de los casos, quicksort toma O(n2 ) tiempo.

¡Es tan lento como el tipo de selección! Pero ese es el peor de los casos. En el caso promedio, quicksort

toma O(n log n) tiempo. Así que te estarás preguntando:

• ¿Qué significan aquí el peor de los casos y el caso promedio?

• Si la ordenación rápida es O(n log n) en promedio, pero la ordenación por fusión es O(n log n)

siempre, ¿por qué no usar la ordenación por fusión? ¿No es más rápido?
Machine Translated by Google

Notación Big O revisada 67

Combinar ordenación vs. ordenación rápida

Supongamos que tiene esta función simple para imprimir todos los elementos de una lista:

def imprimir_elementos(lista):
para el artículo en la lista:
elemento de impresión

Esta función revisa todos los elementos de la lista y los imprime.


Debido a que recorre toda la lista una vez, esta función se ejecuta en tiempo O(n). Ahora,
suponga que cambia esta función para que duerma durante 1 segundo antes de imprimir un
elemento:

desde el tiempo de importación del sueño


def print_items2(lista):
para el artículo en la lista:
dormir(1)
elemento de impresión

Antes de imprimir un artículo, hará una pausa de 1 segundo. Suponga que imprime una
lista de cinco elementos utilizando ambas funciones.

Ambas funciones recorren la lista una vez, por lo que ambas son tiempo O(n).
¿Cuál crees que será más rápido en la práctica? Creo que print_items será mucho más rápido
porque no hace una pausa de 1 segundo antes de imprimir un elemento. Entonces, aunque ambas
funciones tienen la misma velocidad en notación Big O, print_items es más rápido en la práctica.
Cuando escribes la notación Big O como O(n), realmente significa esto.

c es una cantidad fija de tiempo que toma su algoritmo. Se llama la constante. Por ejemplo, podría
ser 10 milisegundos * n para print_
*
elementos versus 1 segundo n para imprimir_elementos2.
Machine Translated by Google

68 Capítulo 4 I Clasificación rápida

Por lo general, ignora esa constante, porque si dos algoritmos tienen diferentes
tiempos de Big O, la constante no importa. Tome la búsqueda binaria y la búsqueda simple,
por ejemplo. Supongamos que ambos algoritmos tuvieran estas constantes.

Podrías decir: “¡Guau! La búsqueda simple tiene una constante de 10 milisegundos, pero la
búsqueda binaria tiene una constante de 1 segundo. ¡La búsqueda simple es mucho más
rápida!” Ahora suponga que está buscando en una lista de 4 mil millones de elementos. Aquí
están los tiempos.

Como puede ver, la búsqueda binaria sigue siendo mucho más rápida. Esa constante no
hizo una diferencia en absoluto.

Pero a veces la constante puede marcar la diferencia. Quicksort versus merge sort es un
ejemplo. Quicksort tiene una constante más pequeña que merge sort. Entonces, si ambos
tienen el tiempo O (n log n), la ordenación rápida es más rápida. Y quicksort es más rápido
en la práctica porque golpea el caso promedio con más frecuencia que el peor de los casos.

Así que ahora te estás preguntando: ¿cuál es el caso promedio versus el peor de los casos?

Caso promedio frente al peor de los casos


El rendimiento de Quicksort depende en gran medida del pivote que elija.
Suponga que siempre elige el primer elemento como pivote. Y llama a quicksort con una
matriz que ya está ordenada. Quicksort no verifica si la matriz de entrada ya está
ordenada. Así que todavía intentará ordenarlo.
Machine Translated by Google

Notación Big O revisada 69

Observe cómo no está dividiendo la matriz en dos mitades. En cambio, uno de los
subconjuntos siempre está vacío. Entonces, la pila de llamadas es realmente larga.
Ahora, en cambio, suponga que siempre eligió el elemento central como pivote.
Mire la pila de llamadas ahora.

¡Es tan corto! Debido a que divide la matriz por la mitad cada vez, no necesita
realizar tantas llamadas recursivas. Alcanza el caso base antes y la pila de llamadas
es mucho más corta.
Machine Translated by Google

70 Capítulo 4 I Clasificación rápida

El primer ejemplo que vio es el peor de los casos, y el segundo ejemplo es el


mejor de los casos. En el peor de los casos, el tamaño de la pila es O(n). En
el mejor de los casos, el tamaño de la pila es O(log n).

Ahora mire el primer nivel en la pila. Eliges un elemento como pivote y el


resto de los elementos se dividen en subarreglos. Tocas los ocho elementos
de la matriz. Entonces esta primera operación toma tiempo O(n). Tocó los ocho
elementos en este nivel de la pila de llamadas. Pero en realidad, toca elementos
O(n) en cada nivel de la pila de llamadas.
Machine Translated by Google

Notación Big O revisada 71

Incluso si divide la matriz de manera diferente, todavía está tocando elementos O (n) cada
vez.

Entonces, cada nivel toma O (n) tiempo para completarse.

En este ejemplo, hay niveles O(log n) (la forma técnica de decirlo es: “La altura de la pila
de llamadas es O(log n)”). Y cada nivel toma O(n) tiempo. Todo el algoritmo tomará el tiempo
O(n) * O(log n) = O(n log n). Este es el mejor de los casos.

En el peor de los casos, hay niveles O(n), por lo que el algoritmo tomará el tiempo O(n) *
O(n) = O(n2 ).

¿Bien adivina que? Estoy aquí para decirles que el mejor caso es también el caso
promedio. Si siempre elige un elemento aleatorio en la matriz como pivote, la ordenación rápida
se completará en un tiempo promedio de O (n log n). Quicksort es uno de los algoritmos de
clasificación más rápidos que existen y es un muy buen ejemplo de D&C.
Machine Translated by Google

72 Capítulo 4 I Clasificación rápida

EJERCICIOS
¿Cuánto tiempo tomaría cada una de estas operaciones en notación Big O?

4.5 Imprimiendo el valor de cada elemento en un arreglo.

4.6 Duplicar el valor de cada elemento de un arreglo.

4.7 Duplicar el valor del primer elemento de un arreglo.

4.8 Crear una tabla de multiplicar con todos los elementos del arreglo. Entonces, si
tu arreglo es [2, 3, 7, 8, 10], primero multiplicas cada elemento por 2, luego
multiplicas cada elemento por 3, luego por 7, y así sucesivamente.

Resumen

• D&C funciona dividiendo un problema en partes cada vez más pequeñas. Si usa
D&C en una lista, el caso base probablemente sea una matriz vacía o una
matriz con un elemento.

• Si está implementando ordenación rápida, elija un elemento aleatorio como


pivote. ¡El tiempo de ejecución promedio de quicksort es O (n log n)!

• La constante en la notación Big O puede ser importante a veces. Es por eso


quicksort es más rápido que merge sort.

• La constante casi nunca importa para la búsqueda simple versus la búsqueda


binaria, porque O(log n) es mucho más rápido que O(n) cuando su lista se vuelve
grande.
Machine Translated by Google

tablas hash 5

En este capítulo

• Aprendes sobre tablas hash, una de las más


estructuras de datos básicos útiles. Las tablas hash tienen
muchos usos; este capítulo cubre los casos de uso comunes.

• Aprende sobre los aspectos internos de las tablas hash:


implementación, colisiones y funciones hash.
Esto lo ayudará a comprender cómo analizar el rendimiento
de una tabla hash.

Supongamos que trabajas en una tienda de comestibles. Cuando


un cliente compra productos, tienes que buscar el precio en un libro.
Si el libro no está alfabetizado, puede llevarle mucho tiempo buscar
manzana en cada línea. Estaría haciendo una búsqueda simple desde
el capítulo 1, donde tiene que mirar cada línea. ¿Recuerdas cuánto
tiempo tomaría eso?
A tiempo. Si el libro está en orden alfabético, puede ejecutar una
búsqueda binaria para encontrar el precio de una manzana. Eso
solo tomaría tiempo O (log n).

73
Machine Translated by Google

74 Capítulo 5 I Tablas hash

Como recordatorio, ¡hay una gran diferencia entre el tiempo O(n) y O(log n)!
Suponga que puede mirar 10 líneas del libro por segundo. Esto es lo que
tardaría la búsqueda simple y la búsqueda binaria.

Ya sabes que la búsqueda binaria es muy rápida. Pero como cajera,


buscar cosas en un libro es una molestia, incluso si el libro está ordenado.
Puedes sentir al cliente animándose mientras buscas artículos en el libro. Lo
que realmente necesitas es un amigo que tenga todos los nombres y precios memorizados.
Entonces no necesitas buscar nada: le preguntas y ella te dice la respuesta al
instante.
Machine Translated by Google

tablas hash 75

Tu amiga Maggie puede darte el precio en O(1) tiempo para cualquier artículo, sin
importar qué tan grande sea el libro. Es incluso más rápida que la búsqueda binaria.

¡Qué maravillosa persona! ¿Cómo se obtiene un "Maggie"?

Pongámonos nuestros sombreros de estructura de datos. Conoces dos estructuras


de datos hasta ahora: arreglos y listas (no hablaré de pilas porque realmente no
puedes "buscar" algo en una pila). Podría implementar este libro como una matriz.

Cada elemento de la matriz es en realidad dos elementos: uno es el nombre de un


tipo de producto y el otro es el precio. Si ordena esta matriz por nombre, puede
ejecutar una búsqueda binaria para encontrar el precio de un artículo. Entonces puede
encontrar elementos en tiempo O (log n). Pero desea encontrar elementos en el tiempo
O (1). Es decir, quieres hacer una "Maggie". Ahí es donde entran las funciones hash.
Machine Translated by Google

76 Capítulo 5 I Tablas hash

Funciones hash
Una función hash es una función en la que ingresas una cadena1 y obtienes un
número.

En terminología técnica, diríamos que una función hash "asigna cadenas a números".
Puede pensar que no hay un patrón discernible sobre el número que obtiene cuando
ingresa una cadena. Pero hay algunos requisitos para una función hash:

• Tiene que ser consistente. Por ejemplo, suponga que ingresa "manzana" y obtiene "4".
Cada vez que ingrese "manzana", debe obtener "4" de regreso.
Sin esto, su tabla hash no funcionará.

• Debe asignar diferentes palabras a diferentes números. por ejemplo, un


La función hash no es buena si siempre devuelve "1" para cualquier palabra que
ingrese. En el mejor de los casos, cada palabra diferente debe asignarse a un número
diferente.

Entonces, una función hash asigna cadenas a números. ¿Para qué sirve eso?
Bueno, ¡puedes usarlo para hacer tu "Maggie"!

Comience con una matriz vacía:

Almacenará todos sus precios en esta matriz. Agreguemos el precio de una manzana.
Introduzca "manzana" en la función hash.

1
Cadena aquí significa cualquier tipo de datos, una secuencia de bytes.
Machine Translated by Google

Funciones hash 77

La función hash genera "3". Así que almacenemos el precio de una manzana
en el índice 3 de la matriz.

Agreguemos leche. Introduzca


"leche" en la función hash.

La función hash dice "0". Guardemos el precio de la leche en el índice 0.

Continúe, y eventualmente toda la gama estará llena de precios.

Ahora preguntas: "Oye, ¿cuál es el precio de un aguacate?" No necesita buscarlo


en la matriz. Solo introduce "aguacate" en la función hash.

Te dice que el precio está almacenado en el índice 4. Y efectivamente, ahí


está.
Machine Translated by Google

78 Capítulo 5 I Tablas hash

La función hash le dice exactamente dónde se almacena el precio, ¡así que no tiene
que buscar en absoluto! Esto funciona porque

• La función hash asigna consistentemente un nombre al mismo índice. Cada vez que
ingrese "aguacate", obtendrá el mismo número. Entonces puede usarlo la primera vez
para encontrar dónde almacenar el precio de un aguacate, y luego puede usarlo para
encontrar dónde almacenó ese precio.

• La función hash asigna diferentes cadenas a diferentes índices.


"Aguacate" se asigna al índice 4. "Leche" se asigna al índice 0. Todo se asigna a una
ranura diferente en la matriz donde puede almacenar su precio.

• La función hash sabe qué tan grande es su matriz y solo devuelve índices válidos.
Entonces, si su matriz tiene 5 elementos, la función hash no devuelve 100... ese no sería
un índice válido en la matriz.

¡Acabas de construir una "Maggie"! Ponga una función hash y una matriz juntas, y
obtendrá una estructura de datos llamada tabla hash. Una tabla hash es la primera
estructura de datos que aprenderá que tiene algo de lógica adicional detrás. Las matrices
y las listas se asignan directamente a la memoria, pero las tablas hash son más inteligentes.
Utilizan una función hash para averiguar de forma inteligente dónde almacenar elementos.

Las tablas hash son probablemente la estructura de datos compleja más útil que
aprenderá. También se conocen como mapas hash, mapas, diccionarios y matrices
asociativas. ¡Y las tablas hash son rápidas! ¿Recuerda nuestra discusión sobre arreglos
y listas enlazadas en el capítulo 2? Puede obtener un elemento de una matriz al instante. Y
las tablas hash usan una matriz para almacenar los datos, por lo que son igualmente rápidas.

Probablemente nunca tendrá que implementar tablas hash usted mismo. Cualquier buen
lenguaje tendrá una implementación para tablas hash. Python tiene tablas hash; se llaman
diccionarios. Puede crear una nueva tabla hash usando la función dict :

>>> libro = dict()

book es una nueva tabla hash. Agreguemos algunos precios para reservar:

>>> libro[“manzana”] = 0.67 >>> Una manzana cuesta 67 centavos.


libro[“leche”] = 1.49 >>> La leche cuesta $1.49.
libro[“aguacate”] = 1.49
>>> imprimir libro
{'aguacate': 1,49, 'manzana': 0,67, 'leche': 1,49}
Machine Translated by Google

casos de uso 79

¡Muy fácil! Ahora vamos a preguntar por el precio de un aguacate:

>>> imprimir libro[“aguacate”]


1.49 El precio de un aguacate

Una tabla hash tiene claves y valores. En el libro hash, los nombres de los
productos son las claves y sus precios son los valores. Una tabla hash asigna claves a
valores.

En la siguiente sección, verá algunos ejemplos en los que las tablas hash son
realmente útiles.

EJERCICIOS
Es importante que las funciones hash devuelvan constantemente la misma salida para
la misma entrada. ¡Si no lo hacen, no podrás encontrar tu artículo después de ponerlo
en la tabla hash!
¿Cuáles de estas funciones hash son consistentes?

5.1 f(x) = 1 Devuelve "1" para todas las entradas

5.2 f(x) = rand() Devuelve un número aleatorio cada vez

5.3 f(x) = siguiente_ranura_vacia() Devuelve el índice de la


siguiente ranura vacía en la tabla hash
5.4 f(x) = largo(x) Utiliza la longitud de la
cadena como índice.

casos de uso

Las tablas hash se utilizan en todas partes. Esta sección le mostrará algunos
casos de uso.

Uso de tablas hash para búsquedas


Su teléfono tiene una práctica agenda integrada.

Cada nombre tiene un número de teléfono asociado.


Machine Translated by Google

80 Capítulo 5 I Tablas hash

Suponga que desea crear una guía telefónica como esta. Está asignando nombres
de personas a números de teléfono. Su directorio telefónico debe tener esta funcionalidad:

• Agregue el nombre de una persona y el número de teléfono asociado


con esa persona.

• Ingrese el nombre de una persona y obtenga el número de teléfono asociado


con ese nombre

¡Este es un caso de uso perfecto para tablas hash! Las tablas hash son
geniales cuando quieres

• Crear un mapeo de una cosa a otra cosa

• Busca algo

Crear una guía telefónica es bastante fácil. Primero, crea una nueva tabla hash:

>>> agenda_teléfono = dict()

Por cierto, Python tiene un atajo para crear una nueva tabla hash. Puedes usar dos llaves:

>>> guía_teléfono = {} Igual que phone_book = dict()

Agreguemos los números de teléfono de algunas personas en esta guía telefónica:

>>> guía_teléfono[“jenny”] = 8675309


>>> agenda_teléfono[“emergencia”] = 911

¡Eso es todo al respecto! Ahora, suponga que desea encontrar el


número de teléfono de Jenny. Simplemente pase la clave al hash:

>>> imprimir agenda_teléfono[“jenny”]


8675309 número de teléfono de Jenny

Imagínese si tuviera que hacer esto usando una matriz en su lugar.


¿Como lo harias? Las tablas hash facilitan el modelado de una relación de un elemento a
otro.

Las tablas hash se utilizan para búsquedas a una escala mucho mayor. Por ejemplo,
suponga que va a un sitio web como http://adit.io. Su computadora tiene que traducir
adit.io a una dirección IP.
Machine Translated by Google

casos de uso 81

Para cualquier sitio web al que vaya, la dirección debe traducirse a una dirección
IP.

Guau, ¿asignar una dirección web a una dirección IP? ¡Suena como un caso de uso
perfecto para tablas hash! Este proceso se denomina resolución de DNS. Las tablas
hash son una forma de proporcionar esta funcionalidad.

Prevención de entradas duplicadas


Supongamos que está ejecutando una cabina de votación. Naturalmente, cada
persona puede votar una sola vez. ¿Cómo te aseguras de que no hayan votado antes?
Cuando alguien viene a votar, le pides su nombre completo. Luego lo compara con la
lista de personas que han votado.

Si su nombre está en la lista, esta persona ya ha votado, ¡échalo! De lo contrario,


agrega su nombre a la lista y déjalos votar. Ahora supongamos que mucha gente ha
venido a votar y la lista de personas que han votado es realmente larga.
Machine Translated by Google

82 Capítulo 5 I Tablas hash

Cada vez que alguien nuevo entra a votar, debe escanear esta lista gigante
para ver si ya votó. Pero hay una mejor manera: ¡usa un hash!

Primero, haga un hash para realizar un seguimiento de las personas que han votado:
>>> votado = {}

Cuando alguien nuevo entre a votar, verifique si ya está en el hash:

>>> valor = votado.obtener(“tom”)

La función get devuelve el valor si "tom" está en la tabla hash.


De lo contrario, devuelve Ninguno. ¡Puedes usar esto para comprobar si
alguien ya ha votado!

Aquí está el código:

votado = {}

def check_voter(nombre):
si votado.obtener(nombre):
imprimir "¡échalos!"
demás:
votado[nombre] = Verdadero
escribe “¡que voten!”

Probemos varias veces:


>>> verificar_votante(“tom”)
¡que voten!
>>> verificar_votante(“mike”)
¡que voten!
>>> verificar_votante(“mike”)
¡echarlos!

La primera vez que Tom entre, se imprimirá "¡déjalos votar!" Entonces Mike
entra y se imprime, "¡déjalos votar!" Luego, Mike intenta ir por segunda vez, y
se imprime, "¡échalos!"
Machine Translated by Google

casos de uso 83

Recuerde, si estuviera almacenando estos nombres en una lista de personas que votaron, esta
función eventualmente se volvería muy lenta, ya que tendría que realizar una búsqueda simple en
toda la lista. Pero en su lugar, está almacenando sus nombres en una tabla hash, y una tabla hash
le dice instantáneamente si el nombre de esta persona está en la tabla hash o no. La comprobación
de duplicados es muy rápida con una tabla hash.

Uso de tablas hash como caché


Un último caso de uso: almacenamiento en caché. Si trabaja en un sitio web, es
posible que haya oído hablar del almacenamiento en caché como algo bueno.
Aquí está la idea. Suponga que visita facebook.com:

1. Haces una solicitud al servidor de Facebook.

2. El servidor piensa por un segundo y se le ocurre


la página web para enviar a usted.

3. Obtienes una página web.

Por ejemplo, en Facebook, el servidor puede recopilar toda la actividad de sus amigos para
mostrársela. Tarda un par de segundos en recopilar toda esa actividad y te la muestra. Ese par
de segundos puede parecer mucho tiempo como usuario. Podrías pensar: "¿Por qué Facebook
es tan lento?"
Por otro lado, los servidores de Facebook tienen que atender a millones de personas, y ese par de
segundos les suma. Los servidores de Facebook realmente están trabajando duro para servir a
todos esos sitios web. ¿Hay alguna manera de hacer que Facebook sea más rápido y que sus
servidores trabajen menos al mismo tiempo?

Suponga que tiene una sobrina que no deja de preguntarle acerca de los planetas. “¿A qué distancia
está Marte de la Tierra?” “¿Qué tan lejos está la Luna?” "¿A qué distancia está Júpiter?" Cada vez,
tienes que hacer una búsqueda en Google y darle una respuesta. Se necesita
Machine Translated by Google

84 Capítulo 5 I Tablas hash

un par de minutos. Ahora, supongamos que ella siempre preguntaba: "¿Qué tan lejos está la
Luna?" Muy pronto, memorizarías que la Luna está a 238,900 millas de distancia. No tendrías
que buscarlo en Google… simplemente recordarías y responderías. Así es como funciona el
almacenamiento en caché: los sitios web recuerdan los datos en lugar de volver a calcularlos.

Si ha iniciado sesión en Facebook, todo el contenido que ve está diseñado solo para usted.
Cada vez que ingresa a facebook.com, sus servidores tienen que pensar en qué contenido le
interesa. Pero si no ha iniciado sesión en Facebook, verá la página de inicio de sesión. Todos
ven la misma página de inicio de sesión.
A Facebook se le pregunta lo mismo una y otra vez: "Dame la página de inicio cuando esté
desconectado". Por lo tanto, deja de hacer que el servidor funcione para descubrir cómo se ve
la página de inicio. En su lugar, memoriza cómo se ve la página de inicio y te la envía.

Esto se llama almacenamiento en caché. Tiene dos ventajas:

• Accedes a la página web mucho más rápido, como cuando memorizaste la distancia de la Tierra
a la Luna. La próxima vez que tu sobrina te pregunte, no tendrás que buscarlo en Google.
Puedes responder al instante.

• Facebook tiene que hacer menos trabajo.

El almacenamiento en caché es una forma común de hacer las cosas más rápido. Todos los grandes sitios web utilizan el

almacenamiento en caché. ¡Y esos datos se almacenan en caché en un hash!


Machine Translated by Google

casos de uso 85

Facebook no solo almacena en caché la página de inicio. También almacena en caché


la página Acerca de, la página Contacto, la página Términos y condiciones y mucho más.
Por lo tanto, necesita una asignación de la URL de la página a los datos de la página.

Cuando visitas una página en Facebook, primero verifica si la página está almacenada
en el hash.

Aquí está en código:

caché = {}

def get_page(url):
si cache.get(url):
devolver caché [url] más: Devuelve datos almacenados en caché

datos = get_data_from_server(url)
cache[url] = datos de Primero guarda estos datos en su caché
retorno de datos

Aquí, haces que el servidor funcione solo si la URL no está en el caché.


Sin embargo, antes de devolver los datos, los guarda en el caché. La próxima vez que
alguien solicite esta URL, puede enviar los datos desde el caché en lugar de hacer que
el servidor haga el trabajo.
Machine Translated by Google

86 Capítulo 5 I Tablas hash

Resumen

En resumen, los hashes son buenos para

• Modelado de relaciones de una cosa a otra cosa

• Filtrado de duplicados

• Almacenamiento en caché/memorización de datos en lugar de hacer que su servidor funcione

Colisiones
Como dije antes, la mayoría de los idiomas tienen tablas hash. No es necesario
que sepa escribir el suyo propio. Por lo tanto, no hablaré demasiado sobre los aspectos
internos de las tablas hash. ¡Pero todavía te preocupas por el rendimiento! Para
comprender el rendimiento de las tablas hash, primero debe comprender qué son las
colisiones. Las siguientes dos secciones cubren las colisiones y el rendimiento.

Primero, te he estado diciendo una mentira piadosa. Te dije que una función hash
siempre asigna diferentes claves a diferentes ranuras en la matriz.

En realidad, es casi imposible escribir una función hash que haga esto.
Tomemos un ejemplo simple. Suponga que su matriz contiene 26 ranuras.

Y su función hash es realmente simple: asigna un lugar en la matriz alfabéticamente.


Machine Translated by Google

Colisiones 87

Tal vez ya puedas ver el problema. Quieres poner


el precio de las manzanas en tu hachís.
Te asignan el primer espacio.

Entonces quieres poner el precio de los plátanos en el hash. Te asignan la segunda


ranura.

¡Todo va tan bien! Pero ahora quieres poner el precio de los aguacates en
tu hachís. Te asignan el primer espacio nuevamente.

¡Oh, no! ¡Las manzanas ya tienen esa ranura! ¿Qué hacer? A esto se le
llama colisión: a dos llaves se les ha asignado la misma ranura. Esto es un problema.
Si almacena el precio de los aguacates en esa ranura, sobrescribirá el precio de
las manzanas. Entonces, la próxima vez que alguien pregunte por el precio de las
manzanas, ¡obtendrá el precio de los aguacates! Las colisiones son malas y debe
evitarlas. Hay muchas maneras diferentes de lidiar con las colisiones. La más
simple es esta: si varias claves se asignan a la misma ranura, comience una lista
vinculada en esa ranura.
Machine Translated by Google

88 Capítulo 5 I Tablas hash

En este ejemplo, tanto "manzana" como "aguacate" se asignan al mismo espacio.


Así que empiezas una lista enlazada en ese espacio. Si necesita saber el precio de
los plátanos, todavía es rápido. Si necesita saber el precio de las manzanas, es un
poco más lento. Tienes que buscar a través de esta lista enlazada para encontrar "apple".
Si la lista enlazada es pequeña, no importa: debe buscar entre tres o cuatro elementos.
Pero suponga que trabaja en una tienda de abarrotes donde solo vende productos que
comienzan con la letra A.

¡Oye, espera un minuto! Toda la tabla hash está totalmente vacía a excepción de una
ranura. ¡Y esa tragamonedas tiene una lista enlazada gigante! Cada elemento de esta
tabla hash está en la lista enlazada. Para empezar, eso es tan malo como poner todo en
una lista enlazada. Va a ralentizar tu tabla hash.
Hay dos lecciones aquí:

• Su función hash es realmente importante. Su función hash asignó todas las claves a
una sola ranura. Idealmente, su función hash asignaría claves uniformemente en
todo el hash.

• Si esas listas enlazadas se hacen largas, se ralentiza mucho la tabla hash. ¡Pero
no durarán mucho si usas una buena función hash!

Las funciones hash son importantes. Una buena función hash le dará muy pocas
colisiones. Entonces, ¿cómo eliges una buena función hash? ¡Eso viene en la siguiente
sección!

Rendimiento
Comenzaste este capítulo en el supermercado. Quería construir algo que le diera
los precios de los productos al instante. Bueno, las tablas hash son realmente rápidas.

En el caso promedio, las tablas hash toman O(1) para todo. O(1) se llama tiempo
constante. No has visto el tiempo constante antes. no significa
Machine Translated by Google

Rendimiento 89

instante. Significa que el tiempo necesario seguirá siendo el mismo,


independientemente del tamaño de la tabla hash. Por ejemplo, sabe que la búsqueda
simple lleva un tiempo lineal.

La búsqueda binaria es más rápida, lleva tiempo de registro:

Buscar algo en una tabla hash lleva un tiempo constante.

¿Ves cómo es una línea plana? Eso significa que no importa si su tabla hash tiene 1
elemento o mil millones de elementos: sacar algo de una tabla hash llevará la misma
cantidad de tiempo. En realidad, has visto el tiempo constante antes. Obtener un
elemento de una matriz lleva un tiempo constante. No importa cuán grande sea su
matriz; se necesita la misma cantidad de tiempo para obtener un elemento. En el caso
promedio, las tablas hash son realmente rápidas.
Machine Translated by Google

90 Capítulo 5 I Tablas hash

En el peor de los casos, una tabla hash toma O(n) —tiempo lineal— para todo, lo cual es
realmente lento. Comparemos tablas hash con arreglos y listas.

Mire el caso promedio de las tablas hash. Las tablas hash son tan rápidas como las matrices
en la búsqueda (obtener un valor en un índice). Y son tan rápidos como las listas enlazadas
en las inserciones y eliminaciones. ¡Es lo mejor de ambos mundos! Pero en el peor de los

casos, las tablas hash son lentas en todo eso. Por lo tanto, es importante que no alcance el
peor rendimiento con tablas hash. Y para hacer eso, necesitas evitar colisiones. Para evitar
colisiones, necesita

• Un factor de carga bajo

• Una buena función hash

Nota
Antes de comenzar con la siguiente sección, sepa que no es una lectura
obligatoria. Voy a hablar sobre cómo implementar una tabla hash, pero nunca
tendrá que hacerlo usted mismo. Cualquiera que sea el lenguaje de programación
que use, tendrá una implementación de tablas hash incorporadas. Puede usar la
tabla hash incorporada y asumir que tendrá un buen rendimiento. La siguiente
sección le da un vistazo debajo del capó.

Factor de carga
El factor de carga de una tabla hash

es fácil de calcular.

Las tablas hash usan una matriz para el almacenamiento, por lo que cuenta la
cantidad de ranuras ocupadas en una matriz. Por ejemplo, esta tabla hash tiene un factor de
2 de
carga
/5, o 0,4.
Machine Translated by Google

Rendimiento 91

¿Cuál es el factor de carga de esta tabla hash?

1
Si dijiste /3, tienes razón. El factor de carga mide cuántos espacios vacíos
permanecer en tu tabla hash.

Suponga que necesita almacenar el precio de 100 productos en su tabla hash, y su tabla
hash tiene 100 ranuras. En el mejor de los casos, cada elemento tendrá su propio
espacio.

Esta tabla hash tiene un factor de carga de 1. ¿Qué sucede si su tabla hash tiene solo
50 ranuras? Entonces tiene un factor de carga de 2. No hay forma de que cada elemento
tenga su propio espacio, ¡porque no hay suficientes espacios! Tener un factor de carga
superior a 1 significa que tiene más elementos que ranuras en su matriz.
Una vez que el factor de carga comienza a crecer, debe agregar más espacios a su tabla
hash. Esto se llama cambiar el tamaño. Por ejemplo, suponga que tiene esta tabla hash
que se está llenando bastante.

Necesita cambiar el tamaño de esta tabla hash. Primero creas una nueva matriz que
es más grande. La regla general es hacer una matriz que tenga el doble de tamaño.
Machine Translated by Google

92 Capítulo 5 I Tablas hash

Ahora debe volver a insertar todos esos elementos en esta nueva tabla hash usando la
función hash :

Esta nueva tabla tiene un factor de carga de 3/8. ¡Mucho mejor! Con una carga menor
factor, tendrá menos colisiones y su tabla funcionará mejor. Una buena regla general es cambiar
el tamaño cuando el factor de carga sea superior a 0,7.

Tal vez estés pensando: “¡Este negocio de redimensionamiento lleva mucho tiempo!” Y tienes
razón. Cambiar el tamaño es costoso y no desea cambiar el tamaño con demasiada frecuencia.
Pero en promedio, las tablas hash toman O (1) incluso con el cambio de tamaño.

Una buena función hash


Una buena función hash distribuye los valores en la matriz de manera uniforme.

Una mala función hash agrupa valores y produce muchas colisiones.

¿Qué es una buena función hash? Eso es algo de lo que nunca tendrás que preocuparte:
los hombres (y mujeres) mayores con grandes barbas se sientan en cuartos oscuros y se
preocupan por eso. Si tiene mucha curiosidad, busque la función SHA (hay una breve descripción
en el último capítulo). Podrías usar eso como tu función hash.
Machine Translated by Google

Resumen 93

EJERCICIOS
Es importante que las funciones hash tengan una buena distribución. Deben mapear los
elementos de la manera más amplia posible. El peor de los casos es una función hash
que asigna todos los elementos al mismo espacio en la tabla hash.

Suponga que tiene estas cuatro funciones hash que funcionan con cadenas:

una. Devuelve "1" para todas las entradas.

B. Utilice la longitud de la cadena como índice.

C. Utilice el primer carácter de la cadena como índice. Entonces, todas las cadenas
que comienzan con a se codifican juntas, y así sucesivamente.

D. Asigna cada letra a un número primo: a = 2, b = 3, c = 5, d = 7,


e = 11, y así sucesivamente. Para una cadena, la función hash es la suma de todos
los caracteres módulo el tamaño del hash. Por ejemplo, si su tamaño de hash es 10
y la cadena es "bolsa", el índice es 3 + 2 + 17 %
10 = 22 % 10 = 2.

Para cada uno de estos ejemplos, ¿qué funciones hash proporcionarían una buena distribución?
Suponga un tamaño de tabla hash de 10 ranuras.

5.5 Una agenda donde las claves son nombres y los valores son números de
teléfono. Los nombres son los siguientes: Esther, Ben, Bob y Dan.

5.6 Un mapeo del tamaño de la batería a la potencia. Los tamaños son A, AA, AAA,
y AAAA.

5.7 Un mapeo de títulos de libros a autores. Los títulos son Maus, Fun
Hogar y Vigilantes.

Resumen

Casi nunca tendrá que implementar una tabla hash usted mismo. El lenguaje de
programación que utilice debería proporcionarle una implementación. Puede usar las tablas
hash de Python y asumir que obtendrá el rendimiento promedio del caso: tiempo constante.

Las tablas hash son una estructura de datos poderosa porque son muy rápidas y le
permiten modelar los datos de una manera diferente. Es posible que pronto descubras que
los estás usando todo el tiempo:
Machine Translated by Google

94 Capítulo 5 I Tablas hash

• Puede crear una tabla hash combinando una función hash con una matriz.

• Las colisiones son malas. Necesita una función hash que


minimice las colisiones.

• Las tablas hash tienen funciones de búsqueda, inserción y eliminación realmente rápidas.

• Las tablas hash son buenas para modelar relaciones de una


elemento a otro elemento.

• Una vez que su factor de carga sea mayor a .07, es hora de cambiar el tamaño
de su tabla hash.

• Las tablas hash se utilizan para almacenar datos en caché (por ejemplo,
con un servidor web).

• Las tablas hash son excelentes para detectar duplicados.


Machine Translated by Google

primero en anchura
búsqueda 6

En este capítulo

• Aprende a modelar una red utilizando una estructura de datos nueva y


abstracta: gráficos.

• Aprende la búsqueda primero en amplitud, un algoritmo que puede


ejecutar en gráficos para responder preguntas como "¿Cuál es el
camino más corto para ir a X?"

• Aprendes sobre gráficas dirigidas versus no dirigidas.

• Aprende clasificación topológica, un tipo diferente de algoritmo de


clasificación que expone dependencias entre nodos.

Este capítulo presenta los gráficos. Primero, hablaré sobre qué son los gráficos (no
involucran un eje X o Y). Luego te mostraré tu primer algoritmo gráfico. Se llama búsqueda
en amplitud (BFS).

La búsqueda primero en amplitud le permite encontrar la distancia más corta entre


dos cosas. ¡Pero la distancia más corta puede significar muchas cosas!
Puede utilizar la búsqueda en anchura para

• Escriba una IA de damas que calcule la menor cantidad de movimientos hacia la victoria

95
Machine Translated by Google

96 Capítulo 6 I Búsqueda en amplitud

• Escribir un corrector ortográfico (la menor cantidad de ediciones de su error


ortográfico a una palabra real, por ejemplo, LEÍDO -> LECTOR es una edición)

• Encuentre el médico más cercano a usted en su red

Los algoritmos gráficos son algunos de los algoritmos más útiles que conozco. Asegúrese
de leer detenidamente los siguientes capítulos: estos son algoritmos que podrá aplicar una
y otra vez.

Introducción a los gráficos

Supongamos que estás en San Francisco y quieres ir de Twin Peaks al puente Golden
Gate. Quieres llegar en autobús, con el mínimo número de transbordos. Aquí están sus
opciones.
Machine Translated by Google

Introducción a los gráficos 97

¿Cuál es tu algoritmo para encontrar el camino con la menor cantidad de pasos?

Bueno, ¿puedes llegar allí en un solo paso? Aquí están todos los lugares a los que puedes
llegar en un solo paso.

El puente no está resaltado; no se puede llegar en un solo paso. ¿Puedes llegar en dos pasos?

Nuevamente, el puente no está ahí, así que no puedes llegar al puente en dos pasos.
¿Qué hay de tres pasos?
Machine Translated by Google

98 Capítulo 6 I Búsqueda en amplitud

¡Ajá! Ahora aparece el puente Golden Gate. Entonces se necesitan tres pasos para llegar desde
Twin Peaks al puente usando esta ruta.

Hay otras rutas que también te llevarán al puente, pero son más largas (cuatro pasos). El
algoritmo encontró que la ruta más corta al puente es de tres pasos. Este tipo de problema se
llama problema del camino más corto. Siempre estás tratando de encontrar algo más corto. Podría
ser la ruta más corta a la casa de tu amigo. Podría ser el número más pequeño de movimientos
para dar jaque mate en un juego de ajedrez. El algoritmo para resolver un problema de ruta más
corta se llama búsqueda primero en anchura.

Para averiguar cómo llegar desde Twin Peaks al puente Golden Gate, hay dos pasos:

1. Modele el problema como un gráfico.

2. Resuelva el problema usando la búsqueda primero en amplitud.

A continuación, cubriré lo que son los gráficos. Luego entraré en la búsqueda primero en amplitud
con más detalle.

¿Qué es un gráfico?
Un gráfico modela un conjunto de conexiones. Por
ejemplo, suponga que usted y sus amigos están jugando al
póquer y desea modelar quién le debe dinero a quién. Así es como
podrías decir: “Alex le debe dinero a Rama”.
Machine Translated by Google

Búsqueda en amplitud 99

El gráfico completo podría verse así.

Gráfico de personas que deben


dinero de póquer a otras personas

Alex le debe dinero a Rama, Tom le debe dinero a Adit, y así sucesivamente. Cada
gráfico se compone de nodos y aristas.

¡Eso es todo al respecto! Los gráficos están formados por nodos y aristas. Un nodo se
puede conectar directamente a muchos otros nodos. Esos nodos se llaman sus vecinos.
En este gráfico, Rama es la vecina de Alex. Adit no es vecino de Alex, porque no están
conectados directamente. Pero Adit es vecino de Rama y Tom.

Los gráficos son una forma de modelar cómo las diferentes cosas están conectadas
entre sí. Ahora veamos la búsqueda primero en amplitud en acción.

Búsqueda en amplitud

Vimos un algoritmo de búsqueda en el capítulo 1: búsqueda binaria. La búsqueda


primero en amplitud es un tipo diferente de algoritmo de búsqueda: uno que se ejecuta
en gráficos. Puede ayudar a responder dos tipos de preguntas:

• Pregunta tipo 1: ¿Existe un camino desde el nodo A hasta el nodo B?

• Pregunta tipo 2: ¿Cuál es el camino más corto del nodo A al nodo B?


Machine Translated by Google

100 Capítulo 6 I Búsqueda en amplitud

Ya vio la búsqueda primero en anchura una vez, cuando calculó la ruta más corta
desde Twin Peaks hasta el puente Golden Gate. Esa era una pregunta de tipo 2: "¿Cuál
es el camino más corto?" Ahora veamos el algoritmo con más detalle. Hará una pregunta
de tipo 1: "¿Hay un camino?"

Suponga que es el orgulloso propietario de una granja de mangos. Está buscando un


vendedor de mangos que pueda vender sus mangos. ¿Está conectado con un vendedor de
mango en Facebook? Bueno, puedes buscar a través de tus amigos.

Esta búsqueda es bastante sencilla.


Primero, haga una lista de amigos para buscar.
Machine Translated by Google

Búsqueda en amplitud 101

Ahora, vaya a cada persona en la lista y verifique si esa persona vende


mangos.

Suponga que ninguno de sus amigos es vendedor de mangos. Ahora tienes


que buscar entre los amigos de tus amigos.

Cada vez que busque a alguien de la lista, agregue todos sus amigos a la lista.
Machine Translated by Google

102 Capítulo 6 I Búsqueda en amplitud

De esta manera, no solo busca a sus amigos, sino que también busca a sus amigos. Recuerde, el
objetivo es encontrar un vendedor de mango en su red.
Entonces, si Alice no vende mangos, también agrega a sus amigos a la lista. Eso significa que
eventualmente buscará a sus amigos, y luego a sus amigos, y así sucesivamente. Con este algoritmo,
buscará en toda su red hasta que se encuentre con un vendedor de mango. Este algoritmo es una
búsqueda primero en amplitud.

Encontrar el camino más corto


Como resumen, estas son las dos preguntas que la búsqueda primero en amplitud puede
responder por usted:

• Pregunta tipo 1: ¿Existe un camino desde el nodo A hasta el nodo B? (¿Hay algún vendedor de
mango en su red?)

• Pregunta tipo 2: ¿Cuál es el camino más corto del nodo A al nodo B?


(¿Quién es el vendedor de mango más cercano?)

Viste cómo responder la pregunta 1; ahora tratemos de responder la pregunta 2. ¿Puede


encontrar el vendedor de mango más cercano? Por ejemplo, tus amigos son conexiones de
primer grado y sus amigos son conexiones de segundo grado.
Machine Translated by Google

Búsqueda en amplitud 103

Preferiría una conexión de primer grado a una conexión de segundo grado, y una conexión
de segundo grado a una conexión de tercer grado, y así sucesivamente. Por lo tanto, no
debe buscar conexiones de segundo grado antes de asegurarse de que no tiene una
conexión de primer grado que sea un vendedor de mango. Bueno, ¡la búsqueda en amplitud
ya hace esto! La forma en que funciona la búsqueda primero en amplitud, la búsqueda se
irradia desde el punto de partida. Por lo tanto, verificará las conexiones de primer grado antes
que las conexiones de segundo grado. Examen sorpresa: ¿quién será revisado primero,
Claire o Anuj? Respuesta: Claire es una conexión de primer grado y Anuj es una conexión
de segundo grado. Entonces Claire será revisada antes que Anuj.

Otra forma de ver esto es que las conexiones de primer grado se


agregan a la lista de búsqueda antes que las de segundo grado.
conexiones

Simplemente recorre la lista y verifica a las personas para ver


si cada una es vendedora de mango. Las conexiones de primer
grado se buscarán antes que las conexiones de segundo grado,

por lo que encontrará el vendedor de mango más cercano a usted.


La búsqueda primero en amplitud no solo encuentra un camino de
A a B, sino que también encuentra el camino más corto.

Tenga en cuenta que esto solo funciona si busca personas en el mismo orden en que se
agregan. Es decir, si Claire se agregó a la lista antes que Anuj, Claire debe buscarse antes
que Anuj. ¿Qué pasa si buscas a Anuj antes que a Claire y ambos son vendedores de
mangos? Bueno, Anuj es un contacto de segundo grado y Claire es un contacto de primer
grado. Termina con un vendedor de mango que no es el más cercano a usted en su red. Por
lo tanto, debe buscar personas en el orden en que se agregan. Hay una estructura de datos
para esto: se llama cola.

Colas
Una cola funciona exactamente igual que en la
vida real. Suponga que usted y su amigo están
haciendo fila en la parada del autobús. Si estás
antes que él en la cola, te subes primero al autobús.
Una cola funciona de la misma manera.
Las colas son similares a las pilas. No puede
acceder a elementos aleatorios en la cola.
En su lugar, hay dos únicas operaciones, enqueue
y dequeue.
Machine Translated by Google

104 Capítulo 6 I Búsqueda en amplitud

Si pone en cola dos elementos en la lista, el primer elemento que agregó se quitará de la cola
antes que el segundo elemento. ¡Puedes usar esto para tu lista de búsqueda!
Las personas que se agreguen primero a la lista serán eliminadas y buscadas primero.

La cola se denomina estructura de datos FIFO: primero en entrar, primero en salir. Por el
contrario, una pila es una estructura de datos LIFO: último en entrar, primero en salir.

Ahora que sabe cómo funciona una cola, ¡implementemos la búsqueda en amplitud!

EJERCICIOS
Ejecute el algoritmo de búsqueda primero en amplitud en cada uno de estos gráficos para encontrar
la solución.

6.1 Encuentra la longitud del camino más corto de


principio a fin.

6.2 Encuentre la longitud del camino más corto desde


"taxi" hasta "murciélago".
Machine Translated by Google

Implementando el gráfico 105

Implementando el gráfico
Primero, debe implementar el gráfico en el código. Un gráfico consta
de varios nodos.

Y cada nodo está conectado a los nodos vecinos.


¿Cómo expresas una relación como “tú -> bob”?
Afortunadamente, conoce una estructura de datos que le permite
expresar relaciones: ¡una tabla hash!

Recuerde, una tabla hash le permite asignar una clave a un valor.


En este caso, desea asignar un nodo a todos sus vecinos.

Así es como lo escribirías en Python:

gráfico = {}
gráfico[“tú”] = [“alice”, “bob”, “claire”]

Observe que "usted" está asignado a una matriz. Entonces graph[“you”] le dará una
matriz de todos los vecinos de “usted”.

Un gráfico es solo un montón de nodos y bordes, por lo que esto es todo lo que
necesita para tener un gráfico en Python. ¿Qué tal un gráfico más grande, como este?
Machine Translated by Google

106 Capítulo 6 I Búsqueda en amplitud

Aquí está como código de Python:


gráfico = {}
gráfico[“tú”] = [“alice”, “bob”, “claire”]
gráfico[“bob”] = [“anuj”, “peggy”]
gráfica[“alicia”] = [“peggy”]
gráfico[“claire”] = [“thom”, “jonny”]
gráfico[“anuj”] = []
gráfico[“peggy”] = []
gráfico[“thom”] = []
gráfico[“jonny”] = []

Prueba sorpresa: ¿importa en qué orden agregas los pares clave/valor?


¿Importa si escribes?

gráfico[“claire”] = [“thom”, “jonny”]


gráfico[“anuj”] = []

en vez de

gráfico[“anuj”] = []
gráfico[“claire”] = [“thom”, “jonny”]

Piense en el capítulo anterior. Respuesta: No importa. Las tablas hash


no tienen orden, por lo que no importa en qué orden agregue pares
clave/valor.
Anuj, Peggy, Thom y Jonny no tienen vecinos. Tienen flechas
apuntándoles, pero ninguna flecha de ellos a otra persona.
Esto se llama un gráfico dirigido: la relación es de una sola manera. Entonces
Anuj es vecino de Bob, pero Bob no es vecino de Anuj. Un gráfico no dirigido
no tiene flechas y ambos nodos son vecinos entre sí. Por ejemplo, ambos
gráficos son iguales.
Machine Translated by Google

Implementando el algoritmo 107

Implementando el algoritmo
En resumen, así es como funcionará la implementación.

Nota
Cuando actualizo las colas,
utilizo los términos enqueue y
dequeue. También encontrará
los términos empujar y hacer estallar.
Push es casi siempre lo mismo
que poner en cola, y pop casi
siempre es lo mismo que quitar
de la cola.

Haz cola para empezar. En Python, utiliza la función de cola doble


(deque) para esto:

from collections import deque cola_búsqueda


= deque() cola_búsqueda += gráfico[“tú”] Crea una nueva cola
Agrega a todos sus vecinos a la cola de búsqueda

Recuerda, graph[“you”] te dará una lista de todos tus


vecinos, como [“alice”, “bob”, “claire”]. Todos esos se agregan a
la cola de búsqueda.
Machine Translated by Google

108 Capítulo 6 I Búsqueda en amplitud

Veamos el resto:

while search_queue: Mientras la cola no esté vacía...


person = search_queue.popleft() … toma a la primera persona de la cola
if person_is_seller(person): Comprueba si la persona es vendedora de mangos
escriba persona + “¡es un vendedor de mangos!” Sí, son un vendedor de mango.
volver verdadero
else:
search_queue += gráfico[persona] return False No, no lo son. Agregue todos los
Si llegaste hasta aquí, nadie en amigos de esta persona a la cola de búsqueda.
la cola era vendedor de mangos.

Una última cosa: aún necesita una función person_is_seller para saber cuándo alguien
es vendedor de mango. Aquí hay uno:

def persona_es_vendedor(nombre):
devolver nombre[-1] == 'm'

Esta función verifica si el nombre de la persona termina con la letra m.


Si lo hace, es un vendedor de mango. Una forma algo tonta de hacerlo, pero servirá
para este ejemplo. Ahora veamos la búsqueda primero en amplitud en acción.
Machine Translated by Google

Implementando el algoritmo 109

Y así. El algoritmo continuará hasta que cualquiera

• Se encuentra un vendedor de mango, o

• La cola se vacía, en cuyo caso no hay vendedor de mango.

Alice y Bob comparten una amiga: Peggy. Por lo tanto, Peggy se agregará a
la cola dos veces: una cuando agregue a los amigos de Alice y otra vez cuando
agregue a los amigos de Bob. Terminarás con dos Peggys en la cola de búsqueda.

Pero solo necesita verificar a Peggy una vez para ver si vende mango. Si la
revisa dos veces, está haciendo un trabajo extra innecesario. Entonces, una vez
que busque a una persona, debe marcar a esa persona como buscada y no buscarla
nuevamente.

Si no hace esto, también podría terminar en un bucle infinito. Supongamos que el


gráfico del vendedor de mango se ve así.

Para empezar, la cola de búsqueda contiene todos tus vecinos.

Ahora revisa a Peggy. Ella no vende mangos, por lo que agrega a todos sus
vecinos a la cola de búsqueda.
Machine Translated by Google

110 Capítulo 6 I Búsqueda en amplitud

A continuación, te revisas a ti mismo. No vende mangos, por lo que agrega a


todos sus vecinos a la cola de búsqueda.

Y así. Este será un bucle infinito, porque la cola de búsqueda seguirá yendo de
ti a Peggy.

Antes de revisar a una persona, es importante asegurarse


de que no haya sido revisada ya. Para hacer eso, mantendrá
una lista de personas que ya ha verificado.

Aquí está el código final para la búsqueda en amplitud, teniendo eso en cuenta:

def buscar(nombre):
cola_busqueda = deque()
search_queue += gráfico[nombre] buscado =
[] while search_queue: Esta matriz es la forma en que realiza un
seguimiento de las personas que ha buscado antes.

person = search_queue.popleft() si no es persona


Solo busca a esta persona si aún
en la búsqueda:
no la has buscado.
si person_is_seller(persona):
imprimir persona + “¡es un vendedor de mangos!”
volver verdadero
demás:
search_queue += gráfico[persona]
Marca a esta persona como buscada
buscado.append(persona)
volver falso

buscar ("tú")
Machine Translated by Google

Implementando el algoritmo 111

Intente ejecutar este código usted mismo. Tal vez intente cambiar la persona_es_
función de vendedor a algo más significativo, y vea si imprime lo que espera.

Tiempo de ejecución

Si busca en toda su red un vendedor de mango, eso significa que seguirá cada borde
(recuerde, un borde es la flecha o la conexión de una persona a otra). Entonces, el
tiempo de ejecución es al menos O (número de aristas).

También mantiene una cola de cada persona para buscar. Añadir una persona a la cola
lleva un tiempo constante: O(1). Hacer esto para cada persona tomará O (número de
personas) en total. La búsqueda en amplitud toma O (número de personas + número de
bordes), y se escribe más comúnmente como O (V + E)
(V para número de vértices, E para número de aristas).

EJERCICIO
Aquí hay un pequeño gráfico de mi rutina matutina.

Te dice que no puedo desayunar hasta que me haya cepillado los dientes. Así que
“desayunar” depende de “cepillarse los dientes”.

Por otro lado, ducharme no depende de cepillarme los dientes, porque puedo
ducharme antes de cepillarme los dientes. A partir de este gráfico, puedes hacer una
lista del orden en el que debo hacer mi rutina matutina:

1. Despierta.
2. Ducha.

3. Cepíllese los dientes.

4. Desayuna.
Machine Translated by Google

112 Capítulo 6 I Búsqueda en amplitud

Tenga en cuenta que "ducha" se puede mover, por lo que esta lista también es válida:

1. Despierta.
2. Cepíllese los dientes.

3. Ducha.

4. Desayuna.

6.3 Para estas tres listas, marque si cada una es válida o no.

6.4 Aquí hay un gráfico más grande. Haz una lista válida para este gráfico.

Se podría decir que esta lista está ordenada, en cierto modo. Si la tarea A depende
de la tarea B, la tarea A aparece más adelante en la lista. Esto se denomina clasificación
topológica y es una forma de hacer una lista ordenada a partir de un gráfico. Suponga
que está planeando una boda y tiene un gran gráfico lleno de tareas por hacer, y no
está seguro por dónde empezar. Podría ordenar topológicamente el gráfico y obtener
una lista de tareas por hacer, en orden.
Machine Translated by Google

Implementando el algoritmo 113

Supongamos que tienes un árbol genealógico.

Este es un gráfico, porque tiene nodos (las personas) y bordes.


Los bordes apuntan a los padres de los nodos. Pero todos los bordes bajan, ¡no
tendría sentido que un árbol genealógico tuviera un borde apuntando hacia arriba!
Eso no tendría sentido: ¡tu papá no puede ser el papá de tu abuelo!

Esto se llama un árbol. Un árbol es un tipo especial de gráfico, donde ningún borde
apunta hacia atrás.

6.5 ¿Cuáles de los siguientes gráficos también son árboles?


Machine Translated by Google

114 Capítulo 6 I Búsqueda en amplitud

Resumen

• La búsqueda primero en amplitud le dice si hay una ruta de A a B.

• Si hay una ruta, la búsqueda primero en anchura encontrará la ruta más corta.

• Si tiene un problema como “encontrar la X más corta”, trate de modelar su problema


como un gráfico y use la búsqueda primero en anchura para resolverlo.

• Un gráfico dirigido tiene flechas y la relación sigue la


dirección de la flecha (rama -> adit significa “rama debe dinero adit”).

• Los gráficos no dirigidos no tienen flechas y la relación va en ambos sentidos (ross -


rachel significa “ross salió con rachel y rachel salió con ross”).

• Las colas son FIFO (primero en entrar, primero en salir).

• Las pilas son LIFO (último en entrar, primero en salir).

• Debe verificar las personas en el orden en que se agregaron a la lista de búsqueda,


por lo que la lista de búsqueda debe ser una cola. De lo contrario, no obtendrá el
camino más corto.

• Una vez que revise a alguien, asegúrese de no volver a revisarlo.


De lo contrario, podría terminar en un bucle infinito.
Machine Translated by Google

7 de Dijkstra
algoritmo

En este capítulo

• Continuamos la discusión de los gráficos y aprenderá


sobre los gráficos ponderados: una forma de asignar
más o menos peso a algunos bordes.

• Aprendes el algoritmo de Dijkstra, que te permite


responder "¿Cuál es el camino más corto a X?"
para gráficos ponderados.

• Aprendes sobre ciclos en gráficos, donde el


algoritmo de Dijkstra no funciona.

115
Machine Translated by Google

116 Capítulo 7 I Algoritmo de Dijkstra

En el último capítulo, descubriste una forma de ir del punto A al punto B.

No es necesariamente el camino más rápido. Es el camino más corto, porque tiene el


menor número de segmentos (tres segmentos). Pero suponga que agrega tiempos de
viaje a esos segmentos. Ahora ves que hay un camino más rápido.

Utilizó la búsqueda primero en amplitud en el último capítulo. La búsqueda primero en


amplitud le encontrará la ruta con la menor cantidad de segmentos (el primer gráfico
que se muestra aquí). ¿Qué sucede si desea la ruta más rápida en su lugar (el segundo
gráfico)? Puede hacerlo más rápido con un algoritmo diferente llamado algoritmo de Dijkstra.

Trabajando con el algoritmo de Dijkstra


Veamos cómo funciona con este gráfico.

Cada segmento tiene un tiempo de viaje en minutos. Usarás el algoritmo de


Dijkstra para ir de principio a fin en el menor tiempo posible.
Machine Translated by Google

Trabajando con el algoritmo de Dijkstra 117

Si ejecutó la búsqueda primero en anchura en este gráfico, obtendría


esta ruta más corta.

Pero ese camino toma 7 minutos. ¡Veamos si puedes encontrar un camino que lleve
menos tiempo! Hay cuatro pasos para el algoritmo de Dijkstra:

1. Encuentra el nodo "más barato". Este es el nodo al que menos puedes llegar
cantidad de tiempo.

2. Actualizar los costos de los vecinos de este nodo. voy a explicar lo que yo
quiero decir con esto en breve.

3. Repita hasta que haya hecho esto para cada nodo en el gráfico.

4. Calcular la ruta final.

Paso 1: Encuentra el nodo más barato. Está parado al principio, preguntándose si debe
ir al nodo A o al nodo B. ¿Cuánto tiempo se tarda en llegar a cada nodo?

Se necesitan 6 minutos para llegar al nodo A y 2 minutos para llegar al nodo B.


El resto de los nodos, aún no lo sabes.

Como aún no sabes cuánto tiempo lleva llegar a la meta,


pones infinito (verás por qué pronto). El nodo B es el nodo
más cercano... está a 2 minutos.
Machine Translated by Google

118 Capítulo 7 I Algoritmo de Dijkstra

Paso 2: calcule cuánto tiempo lleva llegar a todos los vecinos del nodo B siguiendo un borde
desde B.

¡Oye, acabas de encontrar una ruta más corta al nodo A! Solía tardar 6 minutos en
llegar al nodo A.

Pero si pasa por el nodo B, ¡hay un camino que solo toma 5 minutos!

Cuando encuentre una ruta más corta para un vecino de B, actualice su costo. En este
caso, usted encontró

• Un camino más corto a A (de 6 minutos a 5 minutos)

• Un camino más corto hasta la meta (de infinito a 7 minutos)

Paso 3: ¡Repite!

Paso 1 de nuevo: encuentre el nodo al que le lleve la menor cantidad de tiempo


llegar. Has terminado con el nodo B, por lo que el nodo A tiene el siguiente más pequeño
tiempo estimado.
Machine Translated by Google

Trabajando con el algoritmo de Dijkstra 119

Paso 2 nuevamente: actualice los costos para los vecinos del nodo A.

Woo, ¡tarda 6 minutos en llegar a la meta ahora!

Ha ejecutado el algoritmo de Dijkstra para cada nodo (no necesita ejecutarlo para el nodo
final). En este punto, ya sabes

• Se tarda 2 minutos en llegar al nodo B.

• Se tarda 5 minutos en llegar al nodo A.

• Se tarda 6 minutos en llegar a la meta.

Guardaré el último paso, calcular la ruta final, para la siguiente sección. Por ahora, solo te
mostraré cuál es el camino final.

La búsqueda primero en amplitud no habría encontrado esto como el camino más


corto, porque tiene tres segmentos. Y hay una manera de llegar desde el principio hasta el
final en dos segmentos.
Machine Translated by Google

120 Capítulo 7 I Algoritmo de Dijkstra

En el último capítulo, utilizó la búsqueda primero en anchura para encontrar el camino más
corto entre dos puntos. En aquel entonces, "camino más corto" significaba el camino con la
menor cantidad de segmentos. Pero en el algoritmo de Dijkstra, asignas un número o peso a
cada segmento. Luego, el algoritmo de Dijkstra encuentra el camino con el peso total más
pequeño.

En resumen, el algoritmo de Dijkstra consta de cuatro pasos:

1. Encuentra el nodo más barato. Este es el nodo al que menos puedes llegar
cantidad de tiempo.

2. Verifique si hay una ruta más barata a los vecinos de este nodo.
De ser así, actualice sus costos.

3. Repita hasta que haya hecho esto para cada nodo en el gráfico.

4. Calcular la ruta final. (¡Próximamente en la siguiente sección!)

Terminología
Quiero mostrarles algunos ejemplos más del algoritmo de Dijkstra en acción. Pero primero
permítanme aclarar algo de terminología.

Cuando trabaja con el algoritmo de Dijkstra, cada borde del gráfico tiene un número asociado.
Estos se llaman pesos.

Un gráfico con pesos se llama gráfico ponderado. Un gráfico sin pesos se llama gráfico
no ponderado.
Machine Translated by Google

Terminología 121

Para calcular la ruta más corta en un gráfico no ponderado, utilice la búsqueda en


anchura. Para calcular la ruta más corta en un gráfico ponderado, utilice el algoritmo
de Dijkstra. Los gráficos también pueden tener ciclos. Un ciclo se ve así.

Significa que puede comenzar en un nodo, viajar y terminar en el mismo nodo.


Suponga que está tratando de encontrar el camino más corto en este gráfico que
tiene un ciclo.

¿Tendría sentido seguir el ciclo? Bueno, puedes usar el camino que evita el ciclo.

O puede seguir el ciclo.


Machine Translated by Google

122 Capítulo 7 I Algoritmo de Dijkstra

Terminas en el nodo A de cualquier manera, pero el ciclo agrega más peso. Incluso podrías
seguir el ciclo dos veces si quisieras.

Pero cada vez que sigues el ciclo, solo sumas 8 al peso total. Así que seguir el ciclo
nunca te dará el camino más corto.

Finalmente, ¿recuerda nuestra conversación sobre gráficas dirigidas versus no dirigidas


del capítulo 6?

Un gráfico no dirigido significa que ambos nodos apuntan entre sí. ¡Eso es un ciclo!

Con un gráfico no dirigido, cada borde agrega otro ciclo.


El algoritmo de Dijkstra solo funciona con gráficos acíclicos dirigidos, llamados
DAG para abreviar.

cambio por un piano


¡Basta de terminología, veamos otro ejemplo! Este es Rama.

Rama está tratando de cambiar un libro de música por un piano.


Machine Translated by Google

cambio por un piano 123

“Te daré este póster para tu libro”, dice Alex. “Es un póster de mi banda
favorita, Destroyer. O te daré este LP raro de Rick Astley para tu libro y $5
más”. “Ooh, he oído que LP tiene una canción realmente genial”, dice Amy.
“Te cambio mi guitarra o batería por el poster o el LP.”

“¡He tenido la intención de entrar en la guitarra!” exclama Beethoven. "Oye,


te cambio mi piano por cualquiera de las cosas de Amy".

¡Perfecto! Con un poco de dinero, Rama puede pasar de un libro de piano a un


piano de verdad. Ahora solo necesita descubrir cómo gastar la menor cantidad de
dinero para realizar esos intercambios. Hagamos un gráfico de lo que le han
ofrecido.

En este gráfico, los nodos son todos los artículos que Rama puede
intercambiar. Los pesos en los bordes son la cantidad de dinero que tendría
que pagar para realizar el intercambio. Entonces puede cambiar el poster por la
guitarra por $30, o cambiar el LP por la guitarra por $15. ¿Cómo va a encontrar
Rama el camino del libro al piano donde gasta menos dinero?
¡El algoritmo de Dijkstra al rescate! Recuerde, el algoritmo de Dijkstra tiene
cuatro pasos. En este ejemplo, realizará los cuatro pasos, por lo que también
calculará la ruta final al final.

Antes de comenzar, necesita algunos


ajustes. Haz una tabla del costo de
cada nodo. El costo de un nodo es lo caro
que es llegar.
Machine Translated by Google

124 Capítulo 7 I Algoritmo de Dijkstra

Seguirá actualizando esta tabla a medida que avance el algoritmo. Para calcular la
ruta final, también necesita una columna principal en esta tabla.

Pronto les mostraré cómo funciona esta columna. Empecemos el algoritmo.

Paso 1: Encuentra el nodo más barato. En este caso, el cartel es el comercio


más barato, a $0. ¿Hay alguna forma más económica de cambiar el póster? Este
es un punto muy importante, así que piénsalo. ¿Puedes ver una serie de
intercambios que le darán a Rama el póster por menos de $0? Sigue leyendo
cuando estés listo. Respuesta: No. Debido a que el cartel es el nodo más barato al
que Rama puede llegar, no hay manera de hacerlo más barato. Aquí hay una
manera diferente de verlo. Supongamos que viaja de su casa al trabajo.

Si tomas el camino hacia la escuela, tardas 2 minutos. Si tomas el camino hacia


el parque, tardas 6 minutos. ¿Hay alguna manera de tomar el camino hacia el
parque y terminar en la escuela en menos de 2 minutos? Es imposible, porque se
tarda más de 2 minutos en llegar al parque. Por otro lado, ¿puedes encontrar un
camino más rápido hacia el parque? Sip.
Machine Translated by Google

cambio por un piano 125

Esta es la idea clave detrás del algoritmo de Dijkstra: mira el nodo más barato en tu
gráfico. ¡No hay forma más económica de llegar a este nodo!

Volvamos al ejemplo de la música. El cartel es el comercio más barato.

Paso 2: Calcule cuánto tiempo se tarda en llegar a sus vecinos (el costo).

Tienes precios para el bajo y la batería en la tabla. Su valor se estableció cuando


revisó el cartel, por lo que el cartel se establece como su padre. Eso significa que,
para llegar al bajo, sigues el borde del cartel, y lo mismo para la batería.

Paso 1 nuevamente: El LP es el siguiente nodo más barato a $5.

Paso 2 nuevamente: actualice los valores de todos sus vecinos.

¡Oye, actualizaste el precio de la batería y la guitarra! Eso significa que es más


barato llegar a la batería y la guitarra siguiendo la línea del LP. Así que establece el LP
como el nuevo padre para ambos instrumentos.
Machine Translated by Google

126 Capítulo 7 I Algoritmo de Dijkstra

El bajo es el siguiente artículo más barato. Actualizar a sus vecinos.

Ok, finalmente tienes un precio para el piano, cambiando la guitarra por el piano. Así
que estableces la guitarra como padre. Finalmente, el último nodo, la batería.

Rama puede conseguir el piano aún más barato cambiando la batería por el piano.
Entonces, el conjunto de intercambios más barato le costará a Rama $ 35.

Ahora, como te prometí, necesitas descubrir el camino. Hasta ahora, sabes que el
camino más corto cuesta $35, pero ¿cómo calculas el camino? Para empezar, mira el
padre para piano.

El piano tiene la batería como padre. Eso significa que Rama cambia la batería por el
piano. Entonces sigues este borde.
Machine Translated by Google

cambio por un piano 127

Veamos cómo seguirías los bordes. El piano tiene la batería como padre.

Y la batería tiene el LP como padre.

Así que Rama cambiará el LP por la batería. Y por supuesto, cambiará el libro por el
LP. Al seguir a los padres hacia atrás, ahora tiene el camino completo.

Esta es la serie de intercambios que Rama necesita hacer.


Machine Translated by Google

128 Capítulo 7 I Algoritmo de Dijkstra

Hasta ahora, he estado usando el término ruta más corta literalmente: calcular la
ruta más corta entre dos ubicaciones o entre dos personas. Espero que este ejemplo
te haya mostrado que el camino más corto no tiene que estar relacionado con la
distancia física. Puede tratarse de minimizar algo. En este caso, Rama quería
minimizar la cantidad de dinero que gastaba.
¡Gracias, Dijkstra!

Bordes de peso negativo


En el ejemplo de comercio, Alex ofreció cambiar el libro por dos
artículos.

Supongamos que Sarah se ofrece a cambiar el LP por el póster y


ella le dará a Rama $7 adicionales. A Rama no le cuesta nada
hacer este intercambio; en cambio, recibe $ 7 de vuelta.
¿Cómo mostrarías esto en el gráfico?

¡El borde del LP al cartel tiene un peso negativo! Rama recupera $7 si hace
ese intercambio. Ahora Rama tiene dos formas de llegar al cartel.
Machine Translated by Google

Bordes de peso negativo 129

Así que tiene sentido hacer el segundo intercambio: ¡Rama recibe $2 de vuelta de esa manera!
Ahora, si recuerdas, Rama puede cambiar el cartel por los tambores. Hay dos caminos que
podría tomar.

El segundo camino le cuesta $2 menos, así que debería tomar ese camino, ¿verdad?
¿Bien adivina que? Si ejecuta el algoritmo de Dijkstra en este gráfico, Rama tomará el camino
equivocado. Él tomará el camino más largo. No puede usar el algoritmo de Dijkstra si tiene
bordes de peso negativo. Los bordes de peso negativo rompen el algoritmo. Veamos qué
sucede cuando ejecutas el algoritmo de Dijkstra en esto. Primero, haz la tabla de costos.

A continuación, busque el nodo de menor costo y actualice los costos de sus vecinos.
En este caso, el cartel es el nodo de menor costo. Entonces, de acuerdo con el
algoritmo de Dijkstra, no hay una forma más barata de llegar al cartel que pagando $0
(¡sabes que eso está mal!). De todos modos, actualicemos los costos para sus vecinos.

Ok, los tambores tienen un costo de $35 ahora.


Machine Translated by Google

130 Capítulo 7 I Algoritmo de Dijkstra

Obtengamos el siguiente nodo más barato que aún no se haya procesado.

Actualizar los costos para sus vecinos.

Ya procesó el nodo del póster, pero está actualizando el costo. Esta es una gran
bandera roja. Una vez que procesa un nodo, significa que no hay una forma más
económica de llegar a ese nodo. ¡Pero acabas de encontrar una forma más
barata de llegar al cartel! Drums no tiene vecinos, así que ese es el final del
algoritmo. Aquí están los costos finales.

Cuesta $35 llegar a la batería. Sabes que hay un camino que cuesta solo $33,
pero el algoritmo de Dijkstra no lo encontró. El algoritmo de Dijkstra asumió que
debido a que estaba procesando el nodo del póster, no había una forma más
rápida de llegar a ese nodo. Esa suposición solo funciona si no tiene bordes de
peso negativo. Por lo tanto, no puede usar bordes de peso negativo con el
algoritmo de Dijkstra. Si desea encontrar la ruta más corta en un gráfico que tiene
bordes de peso negativo, ¡hay un algoritmo para eso! Se llama el algoritmo de
Bellman-Ford. Bellman-Ford está fuera del alcance de este libro, pero puede
encontrar excelentes explicaciones en línea.
Machine Translated by Google

Implementación 131

Implementación
Veamos cómo implementar el algoritmo de Dijkstra en el código. Aquí está
el gráfico que usaré para el ejemplo.

Para codificar este ejemplo, necesitará tres tablas hash.

Actualizará los costes y las tablas hash principales a medida que avance
el algoritmo. Primero, necesitas implementar el gráfico. Usará una tabla hash
como lo hizo en el capítulo 6:
gráfico = {}

En el último capítulo, almacenó todos los vecinos de un nodo en la tabla hash,


así:
gráfico[“tú”] = [“alice”, “bob”, “claire”]

Pero esta vez, debe almacenar a los vecinos y el costo de llegar a ese vecino. Por
ejemplo, Start tiene dos vecinos, A y B.
Machine Translated by Google

132 Capítulo 7 I Algoritmo de Dijkstra

¿Cómo representas los pesos de esos bordes? ¿Por qué no usar


otra tabla hash?

gráfico[“inicio”] = {}
gráfico[“inicio”][“a”] = 6
gráfico[“inicio”][“b”] = 2

Entonces , el gráfico [“inicio”] es una tabla hash. Puede obtener todos los vecinos para el
inicio de esta manera:

>>> imprimir gráfico[“inicio”].teclas() [“a”, “b”]

Hay una arista de Inicio a A y una arista de Inicio a B. ¿Qué sucede si


desea encontrar los pesos de esas aristas?
>>> imprimir gráfico[“inicio”][“a”] 2

>>> imprimir gráfico[“inicio”][“b”] 6

Agreguemos el resto de los nodos y sus vecinos al gráfico:


gráfico[“a”] = {}
gráfico[“a”][“aleta”] = 1

gráfico[“b”] = {}
gráfico[“b”][“a”] = 3 gráfico[“b”]
[“aleta”] = 5

gráfico[“aleta”] = {} El nodo final no tiene vecinos.


Machine Translated by Google

Implementación 133

La tabla hash del gráfico completo tiene este aspecto.

A continuación, necesita una tabla hash para almacenar los costos de cada nodo.

El costo de un nodo es el tiempo que se tarda en llegar a ese nodo


desde el principio. Sabe que se tarda 2 minutos desde el inicio hasta el

nodo B. Sabe que se tarda 6 minutos en llegar al nodo A (aunque puede


encontrar una ruta que le lleve menos tiempo). No sabes cuánto tiempo se
tarda en llegar a la meta. Si aún no sabes el costo, pones infinito. ¿Puedes
representar el infinito en Python? Resulta que puedes:

infinito = float(“inf”)

Aquí está el código para hacer la tabla de costos:

infinito = float(“inf”)
costos = {}
costos[“a”] = 6
costos[“b”] = 2
costos[“fin”] = infinito

También necesita otra tabla hash para los padres:


Machine Translated by Google

134 Capítulo 7 I Algoritmo de Dijkstra

Aquí está el código para hacer la tabla hash para los padres:
padres = {}
padres[“a”] = “inicio”
padres[“b”] = “inicio”
padres[“fin”] = Ninguno

Finalmente, necesita una matriz para realizar un seguimiento de todos los


nodos que ya procesó, porque no necesita procesar un nodo más de una vez:

procesado = []

Esa es toda la configuración. Ahora veamos el algoritmo.

Primero le mostraré el código y luego lo revisaré. Aquí está el código:


Encuentre el nodo de menor
nodo = find_lowest_cost_node(costos) mientras que el costo que aún no haya procesado.
nodo no es Ninguno: costo = costos[nodo] Si ha procesado todos los nodos, este ciclo while está terminado.

vecinos = gráfico[nodo] para n en


vecinos.claves(): Revise todos los vecinos de este nodo.
new_cost = costo + vecinos[n] Si es más barato llegar a este vecino
si cuesta[n] > nuevo_costo: pasando por este nodo...
costos[n] = nuevo_costo … actualice el costo de este nodo.
padres[n] = nodo Este nodo se convierte en el nuevo padre de este vecino.
procesado.append(nodo) Marque el nodo como procesado.
nodo = find_lowest_cost_node(costos) Encuentre el siguiente nodo para procesar y haga un bucle.

¡Ese es el algoritmo de Dijkstra en Python! Te mostraré el código para la


función más tarde. Primero, veamos este código de algoritmo
find_lowest_cost_node en acción.
Machine Translated by Google

Implementación 135

Encuentre el nodo con el costo más bajo.

Obtenga el costo y los vecinos de ese nodo.

Bucle a través de los vecinos.

Cada nodo tiene un costo. El costo es el tiempo que se tarda en llegar a


ese nodo desde el principio. Aquí, está calculando cuánto tardaría en llegar
al nodo A si fuera Inicio > nodo B > nodo A, en lugar de Inicio > nodo A.

Comparemos esos costos.


Machine Translated by Google

136 Capítulo 7 I Algoritmo de Dijkstra

¡Encontraste una ruta más corta al nodo A! Actualizar el costo.

La nueva ruta pasa por el nodo B, así que establezca B como el nuevo padre.

Ok, estás de vuelta en la parte superior del ciclo. El siguiente vecino es el nodo
Finalizar.

¿Cuánto tiempo se tarda en llegar a la meta si pasa por el nodo B?

Se tarda 7 minutos. El costo anterior era infinitos minutos, y 7 minutos es


menos que eso.
Machine Translated by Google

Implementación 137

Establezca el nuevo costo y el nuevo padre para el nodo Finalizar.

Ok, actualizó los costos para todos los vecinos del nodo B. Márquelo como procesado.

Encuentre el siguiente nodo para procesar.

Obtenga el costo y los vecinos del nodo A.


Machine Translated by Google

138 Capítulo 7 I Algoritmo de Dijkstra

El nodo A solo tiene un vecino: el nodo Finalizar.

Actualmente se tarda 7 minutos en llegar al nodo Finalizar. ¿Cuánto tardaría en


llegar allí si pasara por el nodo A?

¡Es más rápido llegar a Finalizar desde el nodo A! Actualicemos el


costo y el padre.
Machine Translated by Google

Implementación 139

Una vez que haya procesado todos los nodos, el algoritmo habrá terminado.
Espero que el tutorial te haya ayudado a comprender un poco mejor el algoritmo.
Encontrar el nodo de menor costo es bastante fácil con find_lowest_
función cost_node . Aquí está en código:
def find_lowest_cost_node(costos):
coste_mínimo = float(“inf”)
low_cost_node = Ninguno
para nodo en costes: coste = Ir a través de cada nodo. Si es el costo más bajo
costes[nodo] hasta ahora y no ha sido
si el costo <costo_más bajo y el nodo no está procesado: procesado todavía…
costo_bajo = costo … configurarlo como el nuevo nodo de menor costo.
costo_bajo_nodo = nodo
devolver el nodo_de_coste_más_bajo

EJERCICIO
7.1 En cada uno de estos gráficos, ¿cuál es el peso del camino más corto
de principio a fin?
Machine Translated by Google

140 Capítulo 7 I Algoritmo de Dijkstra

Resumen

• La búsqueda en amplitud se utiliza para calcular la ruta más corta para


un gráfico no ponderado.

• El algoritmo de Dijkstra se usa para calcular la ruta más corta para un


gráfico ponderado.

• El algoritmo de Dijkstra funciona cuando todos los pesos son positivos.

• Si tiene pesos negativos, use el algoritmo de Bellman-Ford.


Machine Translated by Google

8
algoritmos codiciosos

En este capítulo
• Aprendes a hacer frente a lo imposible:
problemas que no tienen solución algorítmica rápida
(problemas NP-completos).

• Aprende a identificar tales problemas cuando los ve, por lo


que no pierde el tiempo tratando de encontrar un
algoritmo rápido para ellos.

• Aprende sobre algoritmos de aproximación, que puede


usar para encontrar rápidamente una solución
aproximada a un problema NP-completo.

• Aprendes sobre la estrategia codiciosa, una muy simple


estrategia de resolución de problemas.

141
Machine Translated by Google

142 Capítulo 8 I Algoritmos codiciosos

El problema de la programación del aula.


Suponga que tiene un salón de clases y desea realizar tantas clases aquí
como sea posible. Obtienes una lista de clases.

No puede contener todas estas clases allí, porque algunas de ellas se


superponen.

Desea realizar tantas clases como sea posible en este salón de clases. ¿Cómo
eliges qué conjunto de clases realizar, de modo que obtengas el mayor conjunto de
clases posible?

Suena como un problema difícil, ¿verdad? En realidad, el algoritmo es tan fácil que
podría sorprenderte. Así es como funciona:

1. Elige la clase que termine antes. Esta es la primera clase que tendrá en este salón
de clases.

2. Ahora, debe elegir una clase que comience después de la primera clase.
Nuevamente, elija la clase que termine antes. Esta es la segunda clase
que tendrá.
Machine Translated by Google

El problema de la programación del aula. 143

¡Sigue haciendo esto y terminarás con la respuesta! Probémoslo. Arte termina lo antes posible, a las 10:00 am,

así que esa es una de las clases que elijas.

Ahora necesita la próxima clase que comienza después de las 10:00 a. m. y termina lo antes posible.

El inglés está descartado porque entra en conflicto con el arte, pero las matemáticas funcionan.

Finalmente, CS entra en conflicto con Math, pero Music funciona.

Así que estas son las tres clases que tendrá en este salón de clases.
Machine Translated by Google

144 Capítulo 8 I Algoritmos codiciosos

Mucha gente me dice que este algoritmo parece fácil. Es demasiado obvio, así que
debe estar mal. Pero esa es la belleza de los algoritmos codiciosos: ¡son fáciles! Un
algoritmo codicioso es simple: en cada paso, elija el movimiento óptimo.
En este caso, cada vez que eliges una clase, eliges la clase que termina antes. En
términos técnicos: en cada paso eliges la solución óptima localmente y al final te
quedas con la solución óptima globalmente.
Lo crea o no, ¡este simple algoritmo encuentra la solución óptima a este problema de
programación!

Obviamente, los algoritmos codiciosos no siempre funcionan. ¡Pero son fáciles de


escribir! Veamos otro ejemplo.

El problema de la mochila
Supongamos que eres un ladrón codicioso. Estás en una tienda
con una mochila y hay todos estos artículos que puedes robar.
Pero solo puedes llevar lo que cabe en tu mochila.
La mochila puede contener 35 libras.

Estás tratando de maximizar el valor de los artículos que pones en


tu mochila. ¿Qué algoritmo usas?

Nuevamente, la estrategia codiciosa es bastante simple:

1. Elige lo más caro que quepa en tu mochila.

2. Elija la siguiente cosa más cara que quepa


tu mochila Y así.

Excepto que esta vez, ¡no funciona! Por ejemplo, suponga que hay tres elementos
que puede robar.
Machine Translated by Google

El problema de la mochila 145

Su mochila puede contener 35 libras de artículos. El sistema estéreo es


el más caro, así que te lo robas. Ahora no tienes espacio para nada más.

Tienes $3,000 en bienes. ¡Pero espera! Si hubiera elegido la computadora


portátil y la guitarra, ¡podría haber obtenido un botín de $ 3,500!

Claramente, la estrategia codiciosa no te da la solución óptima aquí.


Pero te acerca bastante. En el próximo capítulo, explicaré cómo calcular
la solución correcta. Pero si eres un ladrón en un centro comercial, no te
importa la perfección. "Bastante bien" es lo suficientemente bueno.
Esta es la conclusión de este segundo ejemplo: a veces, lo perfecto es enemigo
de lo bueno. A veces todo lo que necesitas es un algoritmo que resuelva el
problema bastante bien. Y ahí es donde brillan los algoritmos codiciosos, porque
son fáciles de escribir y por lo general se acercan bastante.

EJERCICIOS
8.1 Trabajas para una empresa de muebles y tienes que enviar muebles a
todo el país. Necesitas empacar tu camión con cajas. Todas las cajas
son de diferentes tamaños y estás tratando de maximizar el espacio que
usas en cada camión. ¿Cómo escogerías las cajas para maximizar el
espacio? Piensa en una estrategia codiciosa. ¿Eso le dará la solución
óptima?

8.2 Te vas a Europa y tienes siete días para ver todo lo que puedas. Asignas
un valor en puntos a cada artículo (cuánto quieres
Machine Translated by Google

146 Capítulo 8 I Algoritmos codiciosos

para verlo) y estimar cuánto tiempo lleva. ¿Cómo puede maximizar el total de
puntos (ver todas las cosas que realmente quiere ver) durante su estadía? Piensa
en una estrategia codiciosa. ¿Eso le dará la solución óptima?

Veamos un último ejemplo. Este es un ejemplo donde los algoritmos


codiciosos son absolutamente necesarios.

El problema de la cobertura del conjunto

Suponga que está comenzando un programa de radio.


Quiere llegar a los oyentes en los 50 estados. Tienes que decidir
en qué estaciones reproducir para llegar a todos esos oyentes.
Cuesta dinero estar en cada estación, por lo que está tratando de minimizar
la cantidad de estaciones en las que juega. Tienes una lista de estaciones.

Cada estación cubre una región y hay


superposición.

¿Cómo calculas el conjunto más pequeño de


estaciones en las que puedes jugar para cubrir
los 50 estados? Suena fácil, ¿no? Resulta que es
extremadamente difícil. Aquí está cómo hacerlo:

1. Haga una lista de todos los subconjuntos posibles de estaciones.

Esto se llama el conjunto de potencia. Hay


2^n subconjuntos posibles.
Machine Translated by Google

El problema de la cobertura del conjunto 147

2. De estos, elija el conjunto con el menor número de estaciones que cubra los
50 estados.

El problema es que lleva mucho tiempo calcular cada posible subconjunto de


estaciones. Toma tiempo O(2^n), porque hay 2^n estaciones. Es posible hacerlo
si tiene un conjunto pequeño de 5 a 10 estaciones. Pero con todos los ejemplos
aquí, piensa en lo que sucederá si tienes muchos artículos. Se tarda mucho más
si tiene más estaciones. Suponga que puede calcular 10 subconjuntos por segundo.

¡No hay ningún algoritmo que lo resuelva lo suficientemente rápido! ¿Qué puedes hacer?

Algoritmos de aproximación
¡Algoritmos codiciosos al rescate! Aquí hay un algoritmo codicioso que se acerca
bastante:
1. Elija la estación que cubra la mayor cantidad de estados que aún no han sido
cubiertos. Está bien si la estación cubre algunos estados que ya han sido
cubiertos.

2. Repita hasta cubrir todos los estados.

Esto se llama un algoritmo de aproximación. Cuando calcular la solución exacta


llevará demasiado tiempo, funcionará un algoritmo de aproximación. Los algoritmos
de aproximación se juzgan por

• Qué tan rápidos son

• Qué tan cerca están de la solución óptima

Los algoritmos codiciosos son una buena opción porque no solo son fáciles de
crear, sino que esa simplicidad significa que generalmente también se ejecutan rápido.
En este caso, el algoritmo codicioso se ejecuta en tiempo O(n^2), donde n es el
número de estaciones de radio.
Machine Translated by Google

148 Capítulo 8 I Algoritmos codiciosos

Veamos cómo se ve este problema en el código.

Código para la configuración

Para este ejemplo, usaré un subconjunto de estados y estaciones para simplificar las cosas.

Primero, haga una lista de los estados que desea cubrir:

estados_necesarios = set([“mt”, “wa”, “o”, “id”, “nv”, “ut”,


“ca”, “az”]) Pasas una matriz y se convierte en un conjunto.

Usé un conjunto para esto. Un conjunto es como una lista, excepto que cada elemento puede aparecer
solo una vez en un conjunto. Los conjuntos no pueden tener duplicados. Por ejemplo, suponga que
tiene esta lista:

>>> matriz = [1, 2, 2, 3, 3, 3]

Y lo convertiste en un conjunto:

>>> establecer(arr)
conjunto ([1, 2, 3])

1, 2 y 3 aparecen solo una vez en un conjunto.

También necesita la lista de estaciones que está eligiendo. Elegí usar un hash para esto:

estaciones = {}
estaciones[“kone”] = conjunto([“id”, “nv”, “ut”])
estaciones[“kdos”] = conjunto([“wa”, “id”, “mt”])
estaciones[“ktres”] = conjunto([“o”, “nv”, “ca”])
estaciones[“kcuatro”] = conjunto([“nv”, “ut”])
estaciones[“kcinco”] = conjunto([“ca”, “az”])

Las claves son los nombres de las estaciones y los valores son los estados que cubren.
Entonces, en este ejemplo, la estación kone cubre Idaho, Nevada y Utah.
Todos los valores también son conjuntos. Hacer que todo sea un conjunto te hará la vida más fácil,
como verás pronto.

Finalmente, necesita algo para contener el conjunto final de estaciones que usará:

estaciones_finales = establecer()
Machine Translated by Google

El problema de la cobertura del conjunto 149

Calculando la respuesta
Ahora necesitas calcular qué estaciones usarás. Mire la imagen de la derecha
y vea si puede predecir qué estaciones debe usar.

Puede haber más de una solución correcta. Debe pasar por todas las
estaciones y elegir la que cubra la mayor cantidad de estados descubiertos.
Llamaré a esta best_station:

mejor_estacion = Ninguno
estados_cubiertos = set()
para estación, estados_para_estación en estaciones.elementos():

estados_cubiertos es un conjunto de todos los estados que cubre esta estación


que aún no han sido cubiertos. El ciclo for le permite recorrer cada estación
para ver cuál es la mejor estación. Veamos el cuerpo del bucle for :

cubierto = estados_necesarios & estados_para_estación si len(cubierto)


> len(estados_cubiertos): ¡Nueva sintaxis! A esto
mejor_estación = estación se le llama intersección de conjuntos.

estados_cubiertos = cubiertos

Hay una línea de aspecto divertido aquí:

cubierto = estados_necesarios & estados_para_estación

¿Que esta pasando?

Conjuntos

Supongamos que tienes un conjunto de frutas.

También tienes un conjunto de verduras.

Cuando tienes dos conjuntos, puedes hacer algunas cosas divertidas con ellos.
Machine Translated by Google

150 Capítulo 8 I Algoritmos codiciosos

Aquí hay algunas cosas que puede hacer con conjuntos.

• Una unión de conjuntos significa “combinar ambos conjuntos”.

• Una intersección de conjuntos significa “buscar los elementos que aparecen en ambos conjuntos” (en

este caso, solo el tomate).

• Una diferencia de conjuntos significa "restar los elementos de un conjunto de los elementos

en el otro conjunto.”

Por ejemplo:

>>> frutas = conjunto([“aguacate”, “tomate”, “plátano”])


>>> verduras = conjunto([“remolacha”, “zanahorias”, “tomate”])
>>> frutas | conjunto de verduras Esta es una unión establecida.

([“aguacate”, “remolacha”, “zanahorias”, “tomate”, “plátano”])


>>> conjunto de frutas y Esta es una intersección establecida.

verduras([“tomate”])
>>> conjunto frutas – Esta es una diferencia establecida.

verduras([“aguacate”, “banana”])
>>> verduras – frutas ¿Qué crees que hará esto?
Machine Translated by Google

El problema de la cobertura del conjunto 151

Recordar:

• Los conjuntos son como listas, excepto que los conjuntos no pueden tener duplicados.

• Puedes hacer algunas operaciones interesantes en conjuntos, como unión,


intersección y diferencia.

volver al código

Volvamos al ejemplo original.


Esta es una intersección de conjuntos:

cubierto = estados_necesarios & estados_para_estación

cubierto es un conjunto de estados que estaban en ambos


estados_necesario y estados_para_estación. ¡Tan cubierto es el conjunto
de estados descubiertos que cubre esta estación! A continuación,
comprueba si esta estación cubre más estados que la actual best_station:

if len(cubierto) > len(estados_cubiertos): mejor_estación =


estación estados_cubiertos = cubierto

Si es así, esta estación es la nueva best_station. Finalmente, después de que


finaliza el bucle for , agrega best_station a la lista final de estaciones:

estaciones_finales.add(mejor_estación)

También necesita actualizar los estados_necesitados. Debido a que esta estación


cubre algunos estados, esos estados ya no son necesarios:
estados_necesarios -= estados_cubiertos

Y recorres hasta que estados_necesitados esté vacío. Aquí está el código completo
para el ciclo:

mientras estados_necesarios:
mejor_estación = Ninguno
estados_cubiertos = set() para
estación, estados en estaciones.elementos(): cubierto =
estados_necesarios & estados si len(cubierto) >
len(estados_cubiertos): mejor_estación = estación
estados_cubiertos = cubierto

estados_necesarios -= estados_cubiertos
estaciones_finales.add(mejor_estación)
Machine Translated by Google

152 Capítulo 8 I Algoritmos codiciosos

Finalmente, puede imprimir estaciones_finales, y debería ver esto:

>>> imprimir estaciones_finales


conjunto(['kdos', 'ktres', 'kone', 'kcinco'])

¿Es eso lo que esperabas? En lugar de las estaciones 1, 2, 3 y 5, podría haber elegido las
estaciones 2, 3, 4 y 5. Comparemos el tiempo de ejecución del algoritmo voraz con el algoritmo
exacto.

EJERCICIOS
Para cada uno de estos algoritmos, diga si es un algoritmo codicioso o no.

8.3 Clasificación rápida

8.4 Búsqueda en amplitud

8.5 Algoritmo de Dijkstra

Problemas NP-completos
Para resolver el problema de cobertura de conjuntos, tenía que calcular todos los
conjuntos posibles.
Machine Translated by Google

Problemas NP-completos 153

Tal vez te acordaste del problema del vendedor ambulante del capítulo 1.
En este problema, un vendedor tiene que visitar cinco ciudades diferentes.

Y está tratando de encontrar la ruta más corta que lo lleve a las cinco
ciudades. Para encontrar la ruta más corta, primero debe calcular todas las
rutas posibles.

¿Cuántas rutas tienes que calcular para cinco ciudades?

Vendedor viajero, paso a paso


Comencemos poco a poco. Supongamos que solo tiene dos ciudades. Hay
dos rutas a elegir.
Machine Translated by Google

154 Capítulo 8 I Algoritmos codiciosos

¿Misma ruta o diferente?

Usted puede pensar que esta debería ser la misma ruta. Después de todo, ¿no es SF > Marin la
misma distancia que Marin > SF? No necesariamente. Algunas ciudades (como San Francisco)
tienen muchas calles de sentido único, por lo que no puedes volver por donde viniste. También es
posible que tenga que desviarse 1 o 2 millas para encontrar una rampa de acceso a una autopista.
Así que estas dos rutas no son necesariamente las mismas.

Quizás se esté preguntando: "En el problema del vendedor ambulante, ¿hay una
ciudad específica desde la que deba comenzar?" Por ejemplo, digamos que soy el
vendedor ambulante. Vivo en San Francisco y necesito ir a otras cuatro ciudades. San
Francisco sería mi ciudad de inicio.

Pero a veces la ciudad de inicio no está establecida. Suponga que es FedEx e


intenta entregar un paquete en el Área de la Bahía. El paquete se envía por avión
desde Chicago a una de las 50 ubicaciones de FedEx en el Área de la Bahía.
Luego ese paquete irá en un camión que viajará a diferentes lugares entregando
paquetes. ¿A qué lugar debería volar? Aquí se desconoce la ubicación de inicio.
Depende de usted calcular la ruta óptima y la ubicación de inicio para el vendedor
ambulante.

El tiempo de ejecución para ambas versiones es el mismo. Pero es un ejemplo


más fácil si no hay una ciudad de inicio definida, así que optaré por esa versión.

Dos ciudades = dos rutas posibles.

3 ciudades

Ahora suponga que agrega una ciudad más. ¿Cuántas rutas posibles hay?

Si empiezas en Berkeley, tienes dos ciudades más para visitar.


Machine Translated by Google

Problemas NP-completos 155

Hay seis rutas en total, dos por cada ciudad en la que puede comenzar.

Así que tres ciudades = seis rutas posibles.

4 ciudades

Agreguemos otra ciudad, Fremont. Ahora suponga que comienza en Fremont.


Machine Translated by Google

156 Capítulo 8 I Algoritmos codiciosos

Hay seis rutas posibles a partir de Fremont. ¡Y oye! Se parecen mucho a las
seis rutas que calculó anteriormente, cuando solo tenía tres ciudades. ¡Excepto
que ahora todas las rutas tienen una ciudad adicional, Fremont!
Hay un patrón aquí. Suponga que tiene cuatro ciudades y elige una ciudad de inicio,
Fremont. Quedan tres ciudades. Y sabes que si hay tres ciudades, hay seis rutas
diferentes para llegar entre esas ciudades.
Si comienza en Fremont, hay seis rutas posibles. También puede comenzar en una
de las otras ciudades.

Cuatro ciudades de inicio posibles, con seis rutas posibles para cada ciudad de
inicio = 4 * 6 = 24 rutas posibles.

¿Ves un patrón? Cada vez que agrega una nueva ciudad, aumenta la cantidad de
rutas que tiene que calcular.

¿Cuántas rutas posibles hay para seis ciudades? Si adivinó 720, tiene razón.
5.040 para 7 ciudades, 40.320 para 8 ciudades.

Esto se llama la función factorial (¿recuerdas haber leído sobre esto en el


capítulo 3?). Entonces 5! = 120. Supón que tienes 10 ciudades. ¿Cuántas rutas
posibles hay? 10! = 3.628.800. Tienes que calcular más de 3 millones de rutas
posibles para 10 ciudades. Como puede ver, el número de posibles
Machine Translated by Google

Problemas NP-completos 157

¡Las rutas se vuelven grandes muy rápido! Esta es la razón por la que es imposible
calcular la solución "correcta" para el problema del vendedor ambulante si tiene una
gran cantidad de ciudades.

El problema del vendedor ambulante y el problema de la cobertura del set tienen


algo en común: calculas todas las soluciones posibles y eliges la más pequeña o la
más corta. Ambos problemas son NP-completos.

aproximando
¿Cuál es un buen algoritmo de aproximación para el vendedor ambulante?
Algo simple que encuentra un camino corto. Vea si puede encontrar una respuesta antes de
seguir leyendo.

Así es como lo haría: elegir arbitrariamente una ciudad de inicio. Luego, cada vez que el
vendedor tiene que elegir la siguiente ciudad para visitar, elige la ciudad no visitada más
cercana. Supongamos que comienzan en Marin.

Distancia total: 71 millas. Tal vez no sea el camino más corto, pero sigue siendo bastante
corto.

Aquí está la breve explicación de la completitud de NP: algunos problemas son


famosos por ser difíciles de resolver. El vendedor ambulante y el problema de la
cobertura de decorados son dos ejemplos. Mucha gente inteligente piensa que no
es posible escribir un algoritmo que resuelva estos problemas rápidamente.
Machine Translated by Google

158 Capítulo 8 I Algoritmos codiciosos

¿Cómo saber si un problema es NP-completo?


Jonah está eligiendo jugadores para su equipo de fútbol de fantasía. Tiene una lista de
habilidades que quiere: buen mariscal de campo, buen corredor, bueno bajo la lluvia, bueno
bajo presión, etc. Tiene una lista de jugadores, donde cada jugador cumple con unas habilidades.

Jonah necesita un equipo que cumpla con todas sus habilidades y el tamaño del
equipo es limitado. “Espera un segundo”, se da cuenta Jonah. “¡Este es un problema
de cobertura del set!”

Jonah puede usar el mismo algoritmo de aproximación para crear su equipo:

1. Encuentra el jugador que cumple más habilidades que aún no se han cumplido.

2. Repita hasta que el equipo cumpla con todas las habilidades (o se quede sin espacio en
el equipo).

¡Los problemas NP-completos aparecen en todas partes! Es bueno saber si el problema


que está tratando de resolver es NP-completo. En ese punto, puede dejar de intentar
resolverlo perfectamente y resolverlo utilizando un algoritmo de aproximación. Pero es difícil
saber si un problema en el que está trabajando es NP-completo. Por lo general, hay una
diferencia muy pequeña entre un problema que es fácil de resolver y un problema NP-completo.
Por ejemplo, en los capítulos anteriores, hablé mucho sobre los caminos más cortos. Sabes
calcular el camino más corto para ir del punto A al punto B.
Machine Translated by Google

Problemas NP-completos 159

Pero si quieres encontrar el camino más corto que conecta varios puntos, ese es el problema
del vendedor ambulante, que es NP-completo. La respuesta corta: no hay una manera fácil de
saber si el problema en el que está trabajando es NP-completo. Aquí hay algunos obsequios:

• Su algoritmo se ejecuta rápidamente con un puñado de elementos, pero realmente se ralentiza


abajo con más artículos.

• “Todas las combinaciones de X” generalmente apuntan a un problema NP-completo.

• ¿Tiene que calcular “todas las versiones posibles” de X porque no puede dividirlo en
subproblemas más pequeños? Podría ser NP-completo.

• Si su problema involucra una secuencia (como una secuencia de ciudades, como un vendedor
ambulante) y es difícil de resolver, podría ser NP-completo.

• Si su problema involucra un conjunto (como un conjunto de estaciones de radio) y es difícil de


resolver, podría ser NP-completo.

• ¿Puede volver a plantear su problema como el problema de cubrir el juego o el problema


del vendedor ambulante? Entonces su problema es definitivamente NP-completo.

EJERCICIOS
8.6 Un cartero necesita hacer entregas en 20 hogares. Necesita encontrar la ruta
más corta que vaya a las 20 casas. ¿Es este un problema NP-completo?

8.7 Encontrar la camarilla más grande en un conjunto de personas (una camarilla es un


conjunto de personas que se conocen). ¿Es este un problema NP-completo?

8.8 Estás haciendo un mapa de los EE. UU. y necesitas colorear los adyacentes
estados con diferentes colores. Tienes que encontrar el número mínimo de colores que

necesitas para que no haya dos estados adyacentes del mismo color.
¿Es este un problema NP-completo?
Machine Translated by Google

160 Capítulo 8 I Algoritmos codiciosos

Resumen

• Los algoritmos codiciosos se optimizan localmente, con la esperanza de terminar con un global
óptimo.

• Los problemas NP-completos no tienen una solución rápida conocida.

• Si tiene un problema NP-completo, lo mejor que puede hacer es usar un algoritmo


de aproximación.

• Los algoritmos codiciosos son fáciles de escribir y rápidos de ejecutar, por lo que son
buenos algoritmos de aproximación.
Machine Translated by Google

dinámica
programación 9

En este capítulo
• Aprendes programación dinámica, un
técnica para resolver un problema difícil
dividiéndolo en subproblemas y resolviendo esos
subproblemas primero.

• Usando ejemplos, aprenderá a encontrar una solución de


programación dinámica para un nuevo problema.

El problema de la mochila
Repasemos el problema de la mochila del capítulo 8.
Eres un ladrón con una mochila que puede llevar 4 libras de
mercancías.

161
Machine Translated by Google

162 Capítulo 9 I Programación dinámica

Tienes tres artículos que puedes poner en la mochila.

¿Qué artículos debe robar para robar el máximo valor de dinero en bienes?

La solución simple El
algoritmo más simple es este: prueba todos los conjuntos posibles de
bienes y encuentra el conjunto que le da el mayor valor.

Esto funciona, pero es muy lento. Para 3 elementos, debe calcular 8


conjuntos posibles. Para 4 artículos, debe calcular 16 conjuntos. ¡Con cada
elemento que agrega, la cantidad de conjuntos que tiene que calcular se
duplica! Este algoritmo toma un tiempo O(2^n), que es muy, muy lento.
Machine Translated by Google

El problema de la mochila 163

Eso no es práctico para cualquier número razonable de bienes. En el capítulo 8,


viste cómo calcular una solución aproximada. Esa solución estará cerca de la
solución óptima, pero puede que no sea la solución óptima.

Entonces, ¿cómo se calcula la solución óptima?

Programación dinámica
Respuesta: ¡Con programación dinámica! Veamos cómo funciona aquí el
algoritmo de programación dinámica. La programación dinámica comienza
resolviendo subproblemas y avanza hasta resolver el gran problema.

Para el problema de la mochila, comenzará resolviendo el problema de las


mochilas más pequeñas (o “sub-mochilas”) y luego trabajará hasta resolver el
problema original.

La programación dinámica es un concepto difícil, así que no se preocupe si no lo


entiende de inmediato. Vamos a ver muchos ejemplos.

Comenzaré mostrándoles el algoritmo en acción primero. ¡Después de haberlo


visto en acción una vez, tendrás muchas preguntas! Haré todo lo posible para
abordar todas las preguntas.
Machine Translated by Google

164 Capítulo 9 I Programación dinámica

Cada algoritmo de programación dinámica comienza con una cuadrícula. Aquí hay una
cuadrícula para el problema de la mochila.

Las filas de la cuadrícula son los artículos y las columnas son los pesos de las mochilas
de 1 lb a 4 lb. Necesitas todas esas columnas porque te ayudarán a calcular los valores de
las submochilas.

La cuadrícula comienza vacía. Vas a llenar cada celda de la cuadrícula.


Una vez que complete la cuadrícula, ¡tendrá su respuesta a este problema!
Por favor, sígueme. Haz tu propia cuadrícula y la completaremos juntos.

la fila de la guitarra

Te mostraré la fórmula exacta para calcular esta cuadrícula más adelante. Primero hagamos
un recorrido. Comience con la primera fila.

Esta es la fila de guitarras, lo que significa que estás tratando de meter la guitarra en la
mochila. En cada celda, hay una simple decisión: ¿robar la guitarra o no? Recuerde, está
tratando de encontrar el conjunto de artículos para robar que le darán el mayor valor.

La primera celda tiene una mochila de 1 libra de capacidad. La guitarra también es de 1


libra, lo que significa que cabe en la mochila. Así que el valor de esta celda es de $1500 y
contiene una guitarra.
Machine Translated by Google

El problema de la mochila 165

Comencemos a llenar la cuadrícula.

Así, cada celda de la cuadrícula contendrá una lista de todos los elementos que caben en
la mochila en ese punto.

Veamos la siguiente celda. Aquí tienes una mochila de 2 lb de capacidad.


Bueno, ¡la guitarra definitivamente encajará allí!

Lo mismo para el resto de las celdas de esta fila. Recuerde, esta es la primera fila, por lo
que solo tiene la guitarra para elegir. Estás fingiendo que los otros dos elementos no están
disponibles para robar en este momento.

En este punto, probablemente estés confundido. ¿Por qué haces esto para mochilas
con una capacidad de 1 lb, 2 lb, etc., cuando el problema habla de una mochila de 4 lb?
¿Recuerdas que te dije que la programación dinámica comienza con un pequeño
problema y se desarrolla hasta llegar al gran problema? Estás resolviendo subproblemas
aquí que te ayudarán a resolver el gran problema. Siga leyendo y las cosas se aclararán.
Machine Translated by Google

166 Capítulo 9 I Programación dinámica

En este punto, su cuadrícula debería verse así.

Recuerde, está tratando de maximizar el valor de la mochila.


Esta fila representa la mejor suposición actual para este máximo. Entonces,
ahora mismo, de acuerdo con esta fila, si tuviera una mochila de 4 libras de
capacidad, el valor máximo que podría poner ahí sería de $1,500.

Sabes que esa no es la solución final. A medida que avanzamos en el algoritmo,


refinará su estimación.

La fila estéreo
Hagamos la siguiente fila. Este es para el estéreo. Ahora que estás en la segunda
fila, puedes robar el estéreo o la guitarra. En cada fila, puede robar el elemento de
esa fila o los elementos de las filas superiores. Así que no puedes elegir robar la
computadora portátil en este momento, pero puedes robar el estéreo y/o la guitarra.
Comencemos con la primera celda, una mochila de 1 libra de capacidad. El valor
máximo actual que puede caber en una mochila de 1 libra es de $1500.
Machine Translated by Google

El problema de la mochila 167

¿Deberías robar el estéreo o no?

Tienes una mochila de 1 lb de capacidad. ¿Cabrá el estéreo ahí? ¡No, es demasiado


pesado! Debido a que no puede colocar el estéreo, $ 1,500 sigue siendo el máximo
supongo que para una mochila de 1 libra.

Lo mismo para las próximas dos celdas. Estas mochilas tienen una capacidad de 2 lb
y 3 lb. El valor máximo anterior para ambas era de $1,500.

El estéreo aún no encaja, por lo que sus conjeturas permanecen sin cambios.

¿Qué pasa si tienes una mochila de 4 lb de capacidad? Ajá: ¡el estéreo finalmente encaja!
El valor máximo anterior era de $1500, pero si colocas el estéreo allí, ¡el valor es de
$3000! Tomemos el estéreo.
Machine Translated by Google

168 Capítulo 9 I Programación dinámica

¡Acabas de actualizar tu estimación! Si tiene una mochila de 4 libras, puede colocar en


ella mercancías por valor de al menos $ 3,000. Puede ver en la cuadrícula que está
actualizando gradualmente su estimación.

La fila de portátiles

¡Hagamos lo mismo con la computadora portátil! La computadora portátil pesa 3 lb, por
lo que no cabe en una mochila de 1 lb o 2 lb. El presupuesto para las dos primeras celdas se
mantiene en $1,500.

A 3 lb, el cálculo anterior era de $1500. Pero puede elegir la computadora portátil en
su lugar, y eso vale $ 2,000. ¡Así que la nueva estimación máxima es de $2,000!

Con 4 lb, las cosas se ponen realmente interesantes. Esta es una parte importante.
La estimación actual es de $3,000. Puedes poner la computadora portátil en la mochila,
pero solo vale $2,000.
Machine Translated by Google

El problema de la mochila 169

Hmm, eso no es tan bueno como la estimación anterior. ¡Pero espera! La


computadora portátil pesa solo 3 lb, ¡así que tiene 1 lb gratis! Podrías poner
algo en este 1 lb.

¿Cuál es el valor máximo que puede caber en 1 libra de espacio? Bueno, lo has
estado calculando todo el tiempo.

De acuerdo con la última mejor estimación, puedes colocar la guitarra en ese


espacio de 1 libra, y eso vale $1500. Así que la comparación real es la siguiente.

Es posible que se haya preguntado por qué estaba calculando valores máximos
para mochilas más pequeñas. ¡Espero que ahora tenga sentido! Cuando te quede
espacio, puedes usar las respuestas a esos subproblemas para averiguar qué cabrá
en ese espacio. Es mejor llevar la laptop + la guitarra por $3,500.
Machine Translated by Google

170 Capítulo 9 I Programación dinámica

La cuadrícula final se ve así.

Ahí está la respuesta: el valor máximo que cabrá en la mochila es de


$3.500, ¡compuesta por una guitarra y una laptop!
Tal vez pienses que usé una fórmula diferente para calcular el valor de esa
última celda. Eso es porque omití cierta complejidad innecesaria al completar
los valores de las celdas anteriores. El valor de cada celda se calcula con la
misma fórmula. Aquí está.

Puede usar esta fórmula con cada celda de esta cuadrícula y debería
terminar con la misma cuadrícula que yo. ¿Recuerdas que hablé de resolver
subproblemas? Combinaste las soluciones de dos subproblemas para
resolver el problema mayor.
Machine Translated by Google

Preguntas frecuentes sobre el problema de la mochila


171

Preguntas frecuentes sobre el problema de la mochila

Tal vez esto todavía se siente como magia. Esta sección responde algunas
preguntas comunes.

¿Qué pasa si agregas un artículo?


¡Supongamos que se da cuenta de que hay un cuarto artículo que puede robar y
que no notó antes! También puedes robar un iPhone.

¿Tiene que volver a calcular todo para dar cuenta de este nuevo artículo?
No. Recuerde, la programación dinámica se basa progresivamente en su
estimación. Hasta ahora, estos son los valores máximos.

Eso significa que por una mochila de 4 libras, puedes robar $3,500 en bienes.
Pensaste que ese era el valor máximo final. Pero agreguemos una fila para el
iPhone.
Machine Translated by Google

172 Capítulo 9 I Programación dinámica

¡Resulta que tienes un nuevo valor máximo! Intente completar esta nueva fila antes de
seguir leyendo.

Comencemos con la primera celda. El iPhone cabe en la mochila de 1 libra.


El máximo anterior era de $1500, pero el iPhone vale $2000. Tomemos el iPhone en su
lugar.

En la siguiente celda, puede colocar el iPhone y la guitarra.

Para el celular 3, nada mejor que tomar el iPhone y la guitarra nuevamente, así que
déjalo como está.

Para la última celda, las cosas se ponen interesantes. El máximo actual es de $3,500.
En su lugar, puede robar el iPhone y le sobran 3 libras de espacio.
Machine Translated by Google

Preguntas frecuentes sobre el problema de la mochila 173

¡Esas 3 libras valen $2,000! $2000 del iPhone + $2000 del antiguo subproblema: eso es
$4000. ¡Un nuevo máximo!

Aquí está la nueva grilla final.

Pregunta: ¿Bajaría alguna vez el valor de una columna? es posible?

Piense en una respuesta antes de seguir leyendo.

Respuesta: No. En cada iteración, está almacenando la estimación máxima actual.


¡La estimación nunca puede empeorar de lo que era antes!

EJERCICIO
9.1 Suponga que puede robar otro artículo: un reproductor de MP3. Pesa
1 libra y vale $1,000. ¿Deberías robarlo?
Machine Translated by Google

174 Capítulo 9 I Programación dinámica

¿Qué pasa si cambias el orden de las filas?


¿Cambia la respuesta? Suponga que llena las filas en este orden: estéreo,
computadora portátil, guitarra. ¿Cómo se ve la cuadrícula? Complete la cuadrícula
usted mismo antes de continuar.

Así es como se ve la cuadrícula.

La respuesta no cambia. El orden de las filas no importa.

¿Puedes completar la cuadrícula por columnas


en lugar de por filas?
¡Pruébelo usted mismo! Para este problema, no hace la diferencia. Podría
hacer una diferencia para otros problemas.

¿Qué pasa si agregas un artículo más pequeño?


Supongamos que puedes robar un collar. Pesa 0.5 libras y vale $1,000.
Hasta ahora, su cuadrícula asume que todos los pesos son números enteros.
Ahora decides robar el collar. Te sobran 3.5 lb. ¿Cuál es el valor máximo que
puede caber en 3.5 lb? ¡No lo sabes! Solo calculó valores para mochilas de 1 lb, 2
lb, 3 lb y 4 lb. Necesitas saber el valor de una mochila de 3.5 libras.

Debido al collar, debe tener en cuenta una granularidad más fina, por lo que la
cuadrícula debe cambiar.
Machine Translated by Google

Preguntas frecuentes sobre el problema de la mochila 175

¿Puedes robar fracciones de un artículo?


Supongamos que eres un ladrón en una tienda de comestibles. Puedes robar
bolsas de lentejas y arroz. Si no cabe una bolsa entera, puede abrirla y tomar todo
lo que pueda llevar. Así que ahora no es todo o nada: puede tomar una fracción de
un artículo. ¿Cómo manejas esto usando programación dinámica?

Respuesta: No puedes. Con la solución de programación dinámica, tomas el


artículo o no. No hay forma de que se dé cuenta de que debe tomar la mitad de
un artículo.

¡Pero este caso también se resuelve fácilmente usando un algoritmo codicioso!


Primero, toma todo lo que puedas del artículo más valioso. Cuando se agote, tome
todo lo que pueda del siguiente artículo más valioso, y así sucesivamente.

Por ejemplo, suponga que tiene estos artículos para elegir.

La quinua es más cara por libra que cualquier otra cosa. ¡Entonces, toma toda
la quinua que puedas llevar! Si eso llena tu mochila, es lo mejor que puedes
hacer.

Si se acaba la quínoa y todavía tienes espacio en tu mochila, toma el


siguiente artículo más valioso, y así sucesivamente.

Optimización de su itinerario de viaje


Supongamos que vas a Londres para unas buenas vacaciones. Tienes dos días
allí y muchas cosas que quieres hacer. No puedes hacer todo, así que haces una
lista.
Machine Translated by Google

176 Capítulo 9 I Programación dinámica

Para cada cosa que desea ver, escribe cuánto tiempo llevará y califica cuánto desea
verla. ¿Puedes averiguar lo que deberías ver, según esta lista?

¡Otra vez el problema de la mochila! En lugar de una mochila, tienes una cantidad
limitada de tiempo. Y en lugar de estéreos y computadoras portátiles, tiene una lista de
lugares a los que desea ir. Dibuje la cuadrícula de programación dinámica para esta lista
antes de continuar.

Así es como se ve la cuadrícula.

¿Lo entendiste correctamente? Rellena en la cuadrícula. ¿Qué lugares deberías


terminar viendo? Aquí está la respuesta.
Machine Translated by Google

Preguntas frecuentes sobre el problema de la mochila


177

Manejo de elementos que dependen unos de otros


Supongamos que desea ir a París, por lo que agrega un par de elementos
a la lista.

Estos lugares toman mucho tiempo, porque primero hay que viajar de Londres
a París. Eso lleva medio día. Si desea hacer los tres elementos, tomará cuatro
días y medio.

Espera, eso no está bien. No tienes que viajar a París para cada artículo.
Una vez que estés en París, cada artículo solo debería tomar un día. Entonces
debería ser un día por artículo + medio día de viaje = 3,5 días, no 4,5 días.

Si pones la Torre Eiffel en tu mochila, entonces el Louvre se vuelve “más barato”:


solo te costará un día en lugar de 1,5 días. ¿Cómo modelas esto en programación
dinámica?

no puedes La programación dinámica es poderosa porque puede resolver


subproblemas y usar esas respuestas para resolver el gran problema. La
programación dinámica solo funciona cuando cada subproblema es discreto,
cuando no depende de otros subproblemas. Eso significa que no hay forma de dar
cuenta de París utilizando el algoritmo de programación dinámica.

¿Es posible que la solución requiera más de


dos submochilas?
Es posible que la mejor solución implique robar más de dos artículos.
Por la forma en que está configurado el algoritmo, estás combinando dos
mochilas como máximo; nunca tendrás más de dos mochilas secundarias. Pero es
posible que esas sub-mochilas tengan sus propias sub-mochilas.
Machine Translated by Google

178 Capítulo 9 I Programación dinámica

¿Es posible que la mejor solución no


llene la mochila por completo?
Si. Suponga que también podría robar un diamante.

Este es un gran diamante: pesa 3,5 libras. Vale un millón de dólares, mucho más
que cualquier otra cosa. ¡Definitivamente deberías robarlo!
Pero queda media libra de espacio, y nada cabrá en ese espacio.

EJERCICIO
9.2 Supongamos que vas a acampar. Tiene una mochila con capacidad para
6 lb y puede llevar los siguientes artículos. Cada uno tiene un valor, y
cuanto mayor sea el valor, más importante es el elemento:

• Agua, 3 libras, 10

• Libro, 1 libra, 3

• Alimentos, 2 libras, 9

• Chaqueta, 2 libras, 5

• Cámara, 1 libra, 6

¿Cuál es el conjunto óptimo de artículos para llevar en su viaje de campamento?

Subcadena común más larga


Has visto un problema de programación dinámica hasta ahora. ¿Cuáles son las
comidas para llevar?

• La programación dinámica es útil cuando intenta optimizar


algo dado una restricción. En el problema de la mochila, tenías que maximizar el valor
de los bienes que robabas, limitado por el tamaño de la mochila.

• Puede utilizar la programación dinámica cuando el problema se puede dividir en subproblemas


discretos y no dependen unos de otros.
Machine Translated by Google

Subcadena común más larga 179

Puede ser difícil encontrar una solución de programación dinámica. Eso es en lo que nos
centraremos en esta sección. A continuación se dan algunos consejos generales:

• Cada solución de programación dinámica involucra una grilla.

• Los valores de las celdas suelen ser los que intenta optimizar.
Para el problema de la mochila, los valores eran el valor de los bienes.

• Cada celda es un subproblema, así que piensa en cómo puedes dividir


su problema en subproblemas. Eso te ayudará a descubrir cuáles son los ejes.

Veamos otro ejemplo. Suponga que ejecuta dictionary.com.


Alguien escribe una palabra y le das la definición.

Pero si alguien escribe mal una palabra, querrá poder adivinar qué palabra
quiso decir. Alex está buscando pescado, pero accidentalmente metió hish.
Esa no es una palabra en su diccionario, pero tiene una lista de palabras que
son similares.

(Este es un ejemplo de juguete, por lo que limitará su lista a dos palabras. En realidad,
esta lista probablemente tendría miles de palabras).

Alex tecleó hish. ¿Qué palabra quiso escribir Alex: pez o vista?

haciendo la grilla
¿Cómo se ve la cuadrícula de este problema? Necesitas responder estas preguntas:

• ¿Cuáles son los valores de las celdas?

• ¿Cómo divides este problema en subproblemas?

• ¿Cuáles son los ejes de la cuadrícula?

En la programación dinámica, estás tratando de maximizar algo. En este caso, está tratando
de encontrar la subcadena más larga que tienen dos palabras en común. ¿Qué subcadena
tienen en común hish y fish? ¿Qué hay de hish y vista? Eso es lo que quieres calcular.
Machine Translated by Google

180 Capítulo 9 I Programación dinámica

Recuerde, los valores de las celdas suelen ser los que intenta optimizar. En
este caso, los valores probablemente serán un número: la longitud de la subcadena
más larga que las dos cadenas tienen en común.

¿Cómo se divide este problema en subproblemas? Podrías comparar subcadenas.


En lugar de comparar hish y fish, podrías comparar su
y fis primero. Cada celda contendrá la longitud de la subcadena más larga que
dos subcadenas tienen en común. Esto también te da una pista de que los ejes
probablemente serán las dos palabras. Así que la cuadrícula probablemente se vea así.

Si esto te parece magia negra, no te preocupes. Esto es algo difícil—


¡Es por eso que lo estoy enseñando tan tarde en el libro! Más adelante te daré
un ejercicio para que puedas practicar tú mismo la programación dinámica.

Llenando la grilla
Ahora tiene una buena idea de cómo debería verse la cuadrícula. ¿Cuál es la
fórmula para llenar cada celda de la cuadrícula? Puede hacer un poco de trampa,
porque ya sabe cuál debería ser la solución: hish y fish tienen una subcadena de
longitud 3 en común: ish.

Pero eso todavía no te dice la fórmula a usar. Los informáticos a veces bromean
sobre el uso del algoritmo de Feynman. El algoritmo de Feynman lleva el
nombre del famoso físico Richard Feynman y funciona así:

1. Escriba el problema.
2. Piensa mucho.

3. Escriba la solución.
Machine Translated by Google

Subcadena común más larga 181

¡Los informáticos son un grupo divertido!

La verdad es que no hay una manera fácil de calcular la fórmula aquí.


Tienes que experimentar y tratar de encontrar algo que funcione. A veces, los
algoritmos no son una receta exacta. Son un marco sobre el que construyes
tu idea.

Intente encontrar una solución a este problema usted mismo. Te daré una pista:
parte de la cuadrícula se ve así.

¿Cuáles son los otros valores? Recuerda que cada celda es el valor de un
subproblema. ¿Por qué la celda (3, 3) tiene un valor de 2? ¿Por qué la celda (3,
4) tiene un valor de 0?

Siga leyendo después de que haya tratado de encontrar una fórmula usted
mismo. Incluso si no lo haces bien, mi explicación tendrá mucho más sentido.
Machine Translated by Google

182 Capítulo 9 I Programación dinámica

La solución
Aquí está la grilla final.

Aquí está mi fórmula para llenar cada celda.

Así es como se ve la fórmula en pseudocódigo:

si palabra_a[i] == palabra_b[j]: Las letras coinciden.


celda[i][j] = celda[i-1][j-1] + 1
más: Las letras no coinciden.
celda[i][j] = 0
Machine Translated by Google

Subcadena común más larga 183

Aquí está la grilla de hish vs. vista.

Una cosa a tener en cuenta: para este problema, ¡la solución final puede no
estar en la última celda! Para el problema de la mochila, esta última celda
siempre tenía la solución final. Pero para la subcadena común más larga, la solución
es el número más grande de la cuadrícula, y puede que no sea la última celda.

Volvamos a la pregunta original: ¿qué cadena tiene más en común con


hish? hish y fish tienen una subcadena de tres letras en común. hish y vista
tienen una subcadena de dos letras en común.

Alex probablemente quiso escribir pez.

Subsecuencia común más larga


Supongamos que Alex accidentalmente buscó fosh. ¿Qué palabra quiso decir:
pescado o fuerte?

Comparémoslos usando la fórmula de la subcadena común más larga.


Machine Translated by Google

184 Capítulo 9 I Programación dinámica

Ambos son iguales: ¡dos letras! Pero el fosh está más cerca del pescado.

Estás comparando la subcadena común más larga, pero realmente necesitas


comparar la subsecuencia común más larga: el número de letras en una secuencia
que las dos palabras tienen en común. ¿Cómo se calcula la subsecuencia común más
larga?

Aquí está la grilla parcial para pescado y fosh.

¿Puedes encontrar la fórmula para esta cuadrícula? La subsecuencia común más


larga es muy similar a la subcadena común más larga, y las fórmulas también son
bastante similares. Trate de resolverlo usted mismo, le doy la
responde a continuación.

Subsecuencia común más larga: solución


Aquí está la grilla final.
Machine Translated by Google

Subcadena común más larga 185

Aquí está mi fórmula para llenar cada celda.

Y aquí está en pseudocódigo:

si palabra_a[i] == palabra_b[j]: Las letras coinciden.


celda[i][j] = celda[i-1][j-1] + 1
otra Las letras no coinciden.
cosa: celda[i][j] = max(celda[i-1][j], celda[i][j-1])

¡Uf, lo hiciste! Este es definitivamente uno de los capítulos más difíciles del libro.
Entonces, ¿realmente se usa alguna vez la programación dinámica? Sí:

• Los biólogos usan la subsecuencia común más larga para encontrar similitudes en
las cadenas de ADN. Pueden usar esto para decir qué tan similares son dos animales
o dos enfermedades. La subsecuencia común más larga se está utilizando para
encontrar una cura para la esclerosis múltiple.

• ¿Ha usado alguna vez diff (como git diff)? Diff le dice las diferencias entre dos archivos
y utiliza programación dinámica para hacerlo.

• Hablamos sobre la similitud de cadenas. La distancia de Levenshtein mide cuán


similares son dos cadenas y utiliza programación dinámica.
La distancia de Levenshtein se usa para todo, desde el corrector ortográfico
hasta averiguar si un usuario está cargando datos con derechos de autor.
Machine Translated by Google

186 Capítulo 9 I Programación dinámica

• ¿Ha usado alguna vez una aplicación que ajuste el ajuste de texto, como Microsoft Word?
¿Cómo determina dónde envolver para que la longitud de la línea se mantenga
constante? ¡Programación dinámica!

EJERCICIO
9.3 Dibuje y complete la cuadrícula para calcular la subcadena común más larga
entre azul y pistas.

Resumen

• La programación dinámica es útil cuando intenta optimizar algo dada una restricción.

• Puede usar la programación dinámica cuando el problema puede ser


dividido en subproblemas discretos.

• Cada solución de programación dinámica involucra una grilla.

• Los valores de las celdas suelen ser los que intenta optimizar.

• Cada celda es un subproblema, así que piensa en cómo puedes dividir tu problema
en subproblemas.

• No existe una fórmula única para calcular una programación dinámica


solución.
Machine Translated by Google

k- vecinos
más cercanos 10

En este capítulo
• Aprendes a construir un sistema de clasificación usando el
algoritmo de k-vecinos más cercanos.

• Aprenderá sobre la extracción de características.

• Aprende sobre regresión: predecir un número, como el valor de


una acción mañana, o cuánto disfrutará un usuario de una
película.

• Aprende sobre los casos de uso y las limitaciones


de k-vecinos más cercanos.

Clasificación de naranjas vs toronjas


Mira esta fruta. ¿Es una naranja o un pomelo?
Bueno, sé que los pomelos son generalmente más grandes
y más rojos.

187
Machine Translated by Google

188 Capítulo 10 I k-vecinos más cercanos

Mi proceso de pensamiento es algo así: tengo un gráfico en mi mente.

En términos generales, la fruta más grande y roja es la toronja. Esta fruta es


grande y roja, por lo que probablemente sea una toronja. Pero, ¿y si obtienes una
fruta como esta?

¿Cómo clasificarías esta fruta? Una forma es mirar a los vecinos de este lugar. Echa
un vistazo a los tres vecinos más cercanos de este lugar.
Machine Translated by Google

Construyendo un sistema de recomendaciones 189

Más vecinos son naranjas que pomelos. Así que esta fruta es probablemente
una naranja. Felicitaciones: ¡Acaba de usar el algoritmo de k-vecinos más
cercanos (KNN) para la clasificación! Todo el algoritmo es bastante simple.

¡El algoritmo KNN es simple pero útil! Si está tratando de clasificar algo,
es posible que desee probar KNN primero. Veamos un ejemplo más real.

Construyendo un sistema de recomendaciones


Suponga que es Netflix y desea crear un sistema de
recomendaciones de películas para sus usuarios. En un nivel alto,
¡esto es similar al problema de la toronja!
Machine Translated by Google

190 Capítulo 10 I k-vecinos más cercanos

Puede trazar cada usuario en un gráfico.

Estos usuarios se grafican por similitud, por lo que los usuarios con gustos similares se
grafican más juntos. Suponga que desea recomendar películas para Priyanka. Encuentra a
los cinco usuarios más cercanos a ella.

Justin, JC, Joey, Lance y Chris tienen gustos similares en películas. Entonces, ¡cualquiera
que sea la película que les guste, a Priyanka probablemente también le gustará!

Una vez que tenga este gráfico, construir un sistema de recomendaciones es fácil.
Si a Justin le gusta una película, recomiéndasela a Priyanka.
Machine Translated by Google

Construyendo un sistema de recomendaciones 191

Pero todavía falta una gran pieza. Graficaste los usuarios por similitud.
¿Cómo averiguas qué tan similares son dos usuarios?

Extracción de características

En el ejemplo de la toronja, comparaste frutas según su tamaño y su


color rojo. El tamaño y el color son las características.
estás comparando. Ahora suponga que tiene tres frutas. Puede extraer las
características.

Luego puedes graficar las tres frutas.

A partir de la gráfica, puedes decir visualmente que las frutas A y B son similares.
Medimos qué tan cerca están. Para encontrar la distancia entre dos puntos, usas
la fórmula de Pitágoras.
Machine Translated by Google

192 Capítulo 10 I k-vecinos más cercanos

Aquí está la distancia entre A y B, por ejemplo.

La distancia entre A y B es 1. También puedes encontrar el resto de las


distancias.

La fórmula de la distancia confirma lo que viste visualmente: las frutas A y B son


similares.

Suponga que está comparando usuarios de Netflix, en cambio. Necesita alguna


forma de graficar los usuarios. Por lo tanto, debe convertir cada usuario en un conjunto
de coordenadas, tal como lo hizo con la fruta.
Machine Translated by Google

Construyendo un sistema de recomendaciones 193

Una vez que pueda graficar a los usuarios, puede medir la distancia entre ellos.

Así es como puede convertir usuarios en un conjunto de números. Cuando los


usuarios se registren en Netflix, pídales que califiquen algunas categorías de películas
en función de cuánto les gustan esas categorías. ¡Para cada usuario, ahora tiene un
conjunto de calificaciones!

A Priyanka y Justin les gusta el romance y odian el terror. A Morpheus le gusta la


acción pero odia el romance (odia cuando una buena película de acción se arruina
por una escena romántica cursi). ¿Recuerdas cómo en naranjas versus toronjas, cada
fruta estaba representada por un conjunto de dos números? Aquí, cada usuario está
representado por un conjunto de cinco números.

Un matemático diría, en lugar de calcular la distancia en dos dimensiones, ahora estás


calculando la distancia en cinco dimensiones. Pero la fórmula de la distancia sigue siendo
la misma.
Machine Translated by Google

194 Capítulo 10 I K-vecinos más cercanos

Simplemente implica un conjunto de cinco números en lugar de un conjunto de dos números.

La fórmula de la distancia es flexible: podrías tener un conjunto de un millón


números y seguir usando la misma vieja fórmula de distancia para encontrar la

distancia. Tal vez te estés preguntando, "¿Qué significa la distancia cuando tienes cinco
números?" La distancia te dice qué tan similares son esos conjuntos de números.

Aquí está la distancia entre Priyanka y Justin.

Priyanka y Justin son bastante similares. ¿Cuál es la diferencia entre Priyanka y Morfeo?
Calcula la distancia antes de continuar.

¿Lo entendiste correctamente? Priyanka y Morpheus tienen 24 años de diferencia. La distancia


te dice que los gustos de Priyanka se parecen más a los de Justin que a los de Morpheus.

¡Estupendo! Ahora recomendar películas a Priyanka es fácil: si a Justin le gusta una película,
recomiéndasela a Priyanka y viceversa. ¡Acabas de crear un sistema de recomendaciones de
películas!

Si eres un usuario de Netflix, Netflix seguirá diciéndote: “Califica más películas. Cuantas más
películas califiques, mejores serán tus recomendaciones”. Ahora sabes por qué. Cuantas más
películas califiques, Netflix podrá ver con mayor precisión a qué otros usuarios te pareces.
Machine Translated by Google

Construyendo un sistema de recomendaciones 195

EJERCICIOS
10.1 En el ejemplo de Netflix, calculó la distancia entre dos usuarios diferentes
utilizando la fórmula de distancia. Pero no todos los usuarios califican las
películas de la misma manera. Suponga que tiene dos usuarios, Yogi y Pinky,
que tienen el mismo gusto por las películas. Pero Yogi califica cualquier película
que le gusta con un 5, mientras que Pinky es más selectiva y reserva los 5 solo
para las mejores. Están bien emparejados, pero según el algoritmo de distancia,
no son vecinos. ¿Cómo consideraría sus diferentes estrategias de calificación?

10.2 Supongamos que Netflix nomina a un grupo de “influencers”. Por ejemplo,


Quentin Tarantino y Wes Anderson son personas influyentes en Netflix, por lo
que sus calificaciones cuentan más que las de un usuario normal. ¿Cómo
cambiaría el sistema de recomendaciones para que esté sesgado hacia las
calificaciones de las personas influyentes?

Regresión
Supongamos que quiere hacer algo más que recomendar una película: quiere adivinar
cómo calificará Priyanka esta película. Toma a las cinco personas más cercanas a ella.

Por cierto, sigo hablando de las cinco personas más cercanas. No hay nada

especial sobre el número 5: podrías hacer el 2, o el 10, o el 10,000 más cercano.


¡Es por eso que el algoritmo se llama k-vecinos más cercanos y no cinco vecinos
más cercanos!

Suponga que está tratando de adivinar una calificación para Pitch Perfect. Bueno,
¿cómo lo calificaron Justin, JC, Joey, Lance y Chris?
Machine Translated by Google

196 Capítulo 10 I k-vecinos más cercanos

Podría tomar el promedio de sus calificaciones y obtener 4.2 estrellas.


Eso se llama regresión. Estas son las dos cosas básicas que hará con
KNN: clasificación y regresión:

• Clasificación = categorización en un grupo

• Regresión = predecir una respuesta (como un número)

La regresión es muy útil. Suponga que tiene una pequeña panadería en Berkeley y
hace pan fresco todos los días. Estás tratando de predecir cuántos panes hacer para
hoy. Tienes un conjunto de características:

• Clima en una escala de 1 a 5 (1 = malo, 5 = excelente).

• ¿Fin de semana o festivo? (1 si es fin de semana o festivo, 0 en caso contrario).

• ¿Hay un juego? (1 en caso afirmativo, 0 en caso negativo)

Y sabe cuántas hogazas de pan ha vendido en el pasado para


diferentes conjuntos de funciones.
Machine Translated by Google

Construyendo un sistema de recomendaciones 197

Hoy es un día de fin de semana con buen tiempo. Según los datos que acabas
de ver, ¿cuántos panes venderás? Usemos KNN, donde K = 4. Primero,
determine los cuatro vecinos más cercanos para este punto.

Aquí están las distancias. A, B, D y E son los más cercanos.

Tome un promedio de los panes vendidos en esos días y obtendrá 218.75.


¡Esa es la cantidad de panes que debes hacer para hoy!

Semejanza de coseno

Hasta ahora, ha estado usando la fórmula de la distancia para comparar la distancia


entre dos usuarios. ¿Es esta la mejor fórmula para usar? Uno común usado en la
práctica es la similitud del coseno. Supongamos que dos usuarios son similares,
pero uno de ellos es más conservador en sus calificaciones. Ambos amaban a Amar
Akbar Anthony de Manmohan Desai. Paul lo calificó con 5 estrellas, pero Rowan lo
calificó con 4 estrellas. Si sigue usando la fórmula de la distancia, es posible que
estos dos usuarios no sean vecinos entre sí, aunque tengan gustos similares.

La similitud del coseno no mide la distancia entre dos vectores.


En cambio, compara los ángulos de los dos vectores. Es mejor lidiar con casos
como este. La similitud del coseno está fuera del alcance de este libro, ¡pero
búsquelo si usa KNN!
Machine Translated by Google

198 Capítulo 10 I k-vecinos más cercanos

Elegir buenas características


Para averiguar recomendaciones, hizo que los usuarios calificaran
categorías de películas. ¿Qué pasaría si les hicieras calificar fotos de
gatos en su lugar? Entonces encontraría usuarios que calificaron esas
imágenes de manera similar. ¡Este probablemente sería un motor de recomendaciones
peor, porque las "características" no tienen mucho que ver con el gusto por las películas!

O suponga que le pide a los usuarios que califiquen películas para poder
darles recomendaciones, pero solo les pide que califiquen Toy Story, Toy Story
2 y Toy Story 3. ¡Esto no le dirá mucho sobre los gustos cinematográficos de los usuarios!

Cuando trabaja con KNN, es muy importante elegir las características correctas para
compararlas. Escoger las características correctas significa

• Funciones que se correlacionan directamente con las películas que intentas


recomendar

• Funciones que no tienen sesgo (por ejemplo, si les pide a los usuarios que solo
califiquen películas de comedia, eso no le indicará si les gustan las películas de
acción)

¿Crees que las calificaciones son una buena forma de recomendar películas? Tal vez
califiqué The Wire más alto que House Hunters, pero en realidad paso más tiempo
viendo House Hunters. ¿Cómo mejorarías este sistema de recomendaciones de Netflix?

Volviendo a la panadería: ¿puedes pensar en dos características buenas y dos


malas que podrías haber elegido para la panadería? Tal vez necesite hacer más panes
después de anunciarse en el periódico. O tal vez necesites hacer más panes los lunes.

No hay una respuesta correcta cuando se trata de elegir buenas funciones. Tienes que
pensar en todas las cosas diferentes que necesitas considerar.

EJERCICIO
10.3 Netflix tiene millones de usuarios. El ejemplo anterior analizó a los cinco vecinos más
cercanos para construir el sistema de recomendaciones. ¿Es esto demasiado bajo?
¿Demasiado alto?
Machine Translated by Google

Introducción al aprendizaje automático 199

Introducción al aprendizaje automático


¡KNN es un algoritmo realmente útil y es su introducción al mundo mágico del
aprendizaje automático! El aprendizaje automático se trata de hacer que su
computadora sea más inteligente. Ya vio un ejemplo de aprendizaje automático:
crear un sistema de recomendaciones. Veamos algunos otros ejemplos.

LOC
OCR significa reconocimiento óptico de caracteres. Significa que puede tomar una foto
de una página de texto y su computadora leerá automáticamente el texto por usted. Google
usa OCR para digitalizar libros. ¿Cómo funciona OCR?
Por ejemplo, considere este número.

¿Cómo averiguarías automáticamente qué número es este? Puedes usar KNN para esto:

1. Revise muchas imágenes de números y extraiga las características de esos


números.

2. Cuando obtenga una nueva imagen, extraiga las características de esa imagen y
¡Mira cuáles son sus vecinos más cercanos!

Es el mismo problema que las naranjas contra las toronjas. En términos generales, los
algoritmos de OCR miden líneas, puntos y curvas.

Luego, cuando obtienes un nuevo personaje, puedes extraer las mismas características de
él.
Machine Translated by Google

200 Capítulo 10 I k-vecinos más cercanos

La extracción de características es mucho más complicada en OCR que en el


ejemplo de la fruta. Pero es importante comprender que incluso las tecnologías
complejas se basan en ideas simples, como KNN. Podría usar las mismas ideas para el
reconocimiento de voz o el reconocimiento facial. Cuando subes una foto a Facebook, a
veces es lo suficientemente inteligente como para etiquetar a las personas en la foto
automáticamente. ¡Eso es aprendizaje automático en acción!

El primer paso de OCR, donde revisa imágenes de números y extrae características,


se llama entrenamiento. La mayoría de los algoritmos de aprendizaje automático tienen
un paso de entrenamiento: antes de que su computadora pueda realizar la tarea, debe
estar entrenada. El siguiente ejemplo involucra filtros de spam y tiene un paso de
entrenamiento.

Construyendo un filtro de spam


Los filtros de spam usan otro algoritmo simple llamado clasificador Naive Bayes.
Primero, entrena su clasificador Naive Bayes en algunos datos.

Suponga que recibe un correo electrónico con el asunto "¡Recoja su millón de dólares
ahora!" ¿Es correo no deseado? Puedes dividir esta oración en palabras. Luego, para
cada palabra, vea cuál es la probabilidad de que esa palabra aparezca en un correo
electrónico no deseado. Por ejemplo, en este modelo muy simple, la palabra millón
solo aparece en los correos electrónicos no deseados. Naive Bayes calcula la
probabilidad de que algo sea spam. Tiene aplicaciones similares a KNN.
Machine Translated by Google

Introducción al aprendizaje automático 201

Por ejemplo, podría usar Naive Bayes para categorizar la fruta: tiene una fruta
que es grande y roja. ¿Cuál es la probabilidad de que sea una toronja?
Es otro algoritmo simple que es bastante efectivo. ¡Nos encantan esos
algoritmos!

Predecir el mercado de valores


Aquí hay algo que es difícil de hacer con el aprendizaje automático:
realmente predecir si el mercado de valores subirá o bajará. ¿Cómo elegir
buenas características en un mercado de valores? Suponga que dice que si la
acción subió ayer, subirá hoy. ¿Es esa una buena característica? O suponga que
dice que la acción siempre bajará en mayo. ¿Eso funcionará? No existe una
forma garantizada de utilizar números anteriores para predecir el rendimiento
futuro. Predecir el futuro es difícil y es casi imposible cuando hay tantas variables
involucradas.

Resumen

¡Espero que esto le dé una idea de todas las cosas diferentes que puede hacer
con KNN y con el aprendizaje automático! El aprendizaje automático es un campo
interesante en el que puede profundizar bastante si decide:

• KNN se utiliza para la clasificación y la regresión e implica buscar


en los k-vecinos más cercanos.

• Clasificación = categorización en un grupo.

• Regresión = predecir una respuesta (como un número).


Machine Translated by Google

202 Capítulo 10 I k-vecinos más cercanos

• La extracción de características significa convertir un elemento (como una fruta o un


usuario) en una lista de números que se pueden comparar.

• Elegir buenas características es una parte importante de un KNN exitoso


algoritmo.
Machine Translated by Google

a donde
ir al siguiente 11

En este capítulo
• Obtiene una breve descripción de 10 algoritmos
que no se trataron en este libro y por qué son útiles.

• Obtiene indicaciones sobre qué leer a continuación,


según cuáles sean sus intereses.

Árboles

Volvamos al ejemplo de búsqueda binaria.


Cuando un usuario inicia sesión en Facebook,
Facebook tiene que mirar a través de una gran
variedad para ver si existe el nombre de usuario.
Dijimos que la forma más rápida de buscar a través de
esta matriz es ejecutar una búsqueda binaria. Pero hay un
problema: cada vez que un nuevo usuario se registra,
inserta su nombre de usuario en la matriz. Luego, debe
volver a ordenar la matriz, porque la búsqueda binaria solo
funciona con matrices ordenadas. ¿No sería bueno si pudieras insertar

203
Machine Translated by Google

204 Capítulo 11 I Adónde ir después

el nombre de usuario en la ranura correcta de la matriz de inmediato, para que no


tenga que ordenar la matriz después? Esa es la idea detrás de la estructura de datos del
árbol de búsqueda binaria.

Un árbol de búsqueda binario se ve así.

Para cada nodo, los nodos a su izquierda tienen un valor menor y los nodos a la derecha
tienen un valor mayor.

Supongamos que estás buscando a Maggie. Comienza en el nodo raíz.


Machine Translated by Google

Árboles 205

Maggie viene detrás de David, así que ve hacia la derecha.

Maggie llega antes que Manning, así que ve a la izquierda.

¡Encontraste a Maggie! ¡Es casi como ejecutar una búsqueda binaria! La búsqueda de un
elemento en un árbol de búsqueda binaria requiere un tiempo O(log n) en promedio y un
tiempo O(n) en el peor de los casos. La búsqueda de una matriz ordenada lleva tiempo O (log
n) en el peor de los casos, por lo que podría pensar que una matriz ordenada es mejor. Pero
un árbol de búsqueda binario es mucho más rápido para inserciones y eliminaciones en promedio.

Los árboles de búsqueda binarios también tienen algunas desventajas: por un lado,
no obtiene acceso aleatorio. No puedes decir: “Dame el quinto elemento de este árbol”.
Esos tiempos de rendimiento también son promedio y dependen de que el árbol esté
equilibrado. Suponga que tiene un árbol desequilibrado como el que se muestra a
continuación.
Machine Translated by Google

206 Capítulo 11 I Adónde ir después

¿Ves cómo se inclina hacia la derecha? Este árbol no tiene muy buen rendimiento, porque no
está equilibrado. Hay árboles de búsqueda binarios especiales que se equilibran. Un ejemplo es el
árbol rojo-negro.

Entonces, ¿cuándo se usan los árboles de búsqueda binarios? Los árboles B, un tipo especial de
árbol binario, se usan comúnmente para almacenar datos en bases de datos.

Si está interesado en bases de datos o estructuras de datos más avanzadas, consulte estos:

• árboles B

• Árboles rojo-negros

• Montones

• árboles de juego

Índices invertidos
Aquí hay una versión muy simplificada de cómo funciona un motor de búsqueda. Suponga que tiene
tres páginas web con este contenido simple.
Machine Translated by Google

La transformada de Fourier 207

Construyamos una tabla hash a partir de este contenido.

Las claves de la tabla hash son las palabras, y los valores le indican
en qué páginas aparece cada palabra. Ahora supongamos que un
usuario busca hola. Veamos en qué páginas aparece hi.

Aha: Aparece en las páginas A y B. Mostremos al usuario esas páginas como


resultado. O supongamos que el usuario busca allí. Bueno, sabes que aparece
en las páginas A y C. Bastante fácil, ¿eh? Esta es una estructura de datos útil:
un hash que asigna palabras a los lugares donde aparecen. Esta estructura de
datos se denomina índice invertido y se usa comúnmente para crear motores de
búsqueda. Si está interesado en la búsqueda, este es un buen lugar para
comenzar.

La transformada de Fourier
La transformada de Fourier es uno de esos raros algoritmos: brillante,
elegante y con un millón de casos de uso. La mejor analogía para la transformada
de Fourier proviene de Better Explained (un gran sitio web que explica las
matemáticas de manera simple): dado un batido, la transformada de Fourier le
dirá los ingredientes del batido.1 O, para decirlo de otra manera, dada una canción,
la transformada puede separarlo en frecuencias individuales.

Resulta que esta simple idea tiene muchos casos de uso. Por ejemplo, si puede
separar una canción en frecuencias, puede aumentar las que le interesan.
Podrías potenciar los graves y ocultar los agudos. La transformada de Fourier
es excelente para procesar señales. También puedes usarlo para comprimir
música. Primero, divide un archivo de audio en sus notas de ingredientes. La
transformada de Fourier te dice exactamente cuánto contribuye cada nota a la
canción en general. Así que puedes deshacerte de las notas que no son
importantes. ¡Así es como funciona el formato MP3!

La música no es el único tipo de señal digital. El formato JPG es otro formato


comprimido y funciona de la misma manera. La gente usa la transformada de
Fourier para tratar de predecir los próximos terremotos y analizar el ADN.

1
Kalid, "Una guía interactiva de la transformada de Fourier", mejor explicado, http://mng.bx/874X.
Machine Translated by Google

208 Capítulo 11 I Adónde ir después

Puede usarlo para crear una aplicación como Shazam, que adivina qué canción se está reproduciendo.
La transformada de Fourier tiene muchos usos. ¡Hay muchas posibilidades de que te encuentres con
él!

Algoritmos paralelos
Los siguientes tres temas tratan sobre la escalabilidad y el trabajo con una gran cantidad de datos.
En el pasado, las computadoras eran cada vez más rápidas. Si quisiera hacer que su algoritmo
fuera más rápido, podría esperar unos meses y las computadoras mismas se volverían más
rápidas. Pero estamos cerca del final de ese período. En cambio, las computadoras portátiles y las
computadoras se envían con múltiples núcleos. Para que sus algoritmos sean más rápidos, debe
cambiarlos para que se ejecuten en paralelo en todos los núcleos a la vez.

Aquí hay un ejemplo simple. Lo mejor que puede hacer con un algoritmo de clasificación es
aproximadamente O (n log n). Es bien sabido que no puede ordenar una matriz en tiempo O(n), ¡a menos
que use un algoritmo paralelo! Hay una versión paralela de ordenación rápida que ordenará una matriz
en tiempo O(n).

Los algoritmos paralelos son difíciles de diseñar. Y también es difícil asegurarse de que funcionen
correctamente y averiguar qué tipo de aumento de velocidad verá. Una cosa es segura: las ganancias
de tiempo no son lineales. Entonces, si tiene dos núcleos en su computadora portátil en lugar de
uno, eso casi nunca significa que su algoritmo se ejecutará mágicamente el doble de rápido. Hay un
par de razones para esto:

• Sobrecarga de administrar el paralelismo: suponga que tiene que ordenar


una matriz de 1.000 elementos. ¿Cómo se divide esta tarea entre los dos núcleos? ¿Le da a cada
núcleo 500 elementos para ordenar y luego fusiona las dos matrices ordenadas en una gran
matriz ordenada? Fusionar las dos matrices lleva tiempo.

• Equilibrio de carga: suponga que tiene 10 tareas para realizar, por lo que asigna 5 tareas principales
a cada una. Pero el núcleo A realiza todas las tareas fáciles, por lo que se realiza en 10 segundos,
mientras que el núcleo B realiza todas las tareas difíciles, por lo que demora un minuto.
¡Eso significa que el núcleo A estuvo inactivo durante 50 segundos mientras que el núcleo B
estaba haciendo todo el trabajo! ¿Cómo distribuye el trabajo de manera uniforme para que
ambos núcleos trabajen igual de duro?

Si está interesado en el lado teórico del rendimiento y la escalabilidad, ¡los algoritmos paralelos podrían
ser para usted!
Machine Translated by Google

Mapa reducido 209

Mapa reducido
Hay un tipo especial de algoritmo paralelo que se está volviendo cada vez más popular:
el algoritmo distribuido. Está bien ejecutar un algoritmo paralelo en su computadora
portátil si necesita de dos a cuatro núcleos, pero ¿qué sucede si necesita cientos de
núcleos? Luego puede escribir su algoritmo para que se ejecute en varias máquinas. El
algoritmo MapReduce es un algoritmo distribuido popular. Puede usarlo a través de la
popular herramienta de código abierto Apache Hadoop.

¿Por qué son útiles los algoritmos distribuidos?


Suponga que tiene una tabla con miles de millones o billones de filas y desea
ejecutar una consulta SQL complicada en ella. No puede ejecutarlo en MySQL, porque
tiene problemas después de unos miles de millones de filas. ¡Use MapReduce a través
de Hadoop!

O suponga que tiene que procesar una larga lista de trabajos. Cada trabajo tarda
10 segundos en procesarse y necesita procesar 1 millón de trabajos como este. Si
haces esto en una máquina, ¡te llevará meses! Si pudiera ejecutarlo en 100 máquinas,
podría terminar en unos pocos días.

Los algoritmos distribuidos son geniales cuando tienes mucho trabajo por hacer y
quieres acelerar el tiempo necesario para hacerlo. MapReduce en particular se
construye a partir de dos ideas simples: la función map y la función reduce .

La función de mapa
La función map es simple: toma una matriz y aplica la misma función a cada
elemento de la matriz. Por ejemplo, aquí estamos duplicando todos los elementos
de la matriz:

>>> arr1 = [1, 2, 3, 4, 5]


>>> arr2 = mapa(lambda x: 2 * [2, 4, 6, 8, 10] x, arr1)
Machine Translated by Google

210 Capítulo 11 I Adónde ir después

arr2 ahora contiene [2, 4, 6, 8, 10]: ¡cada elemento en arr1 se


duplicó! Duplicar un elemento es bastante rápido. Pero supongamos
que aplica una función que lleva más tiempo procesar. Mira este pseudocódigo:
>>> arr1 = # Una lista de URL
>>> arr2 = mapa(descargar_pagina, arr1)

Aquí tiene una lista de URL y desea descargar cada página y almacenar el
contenido en arr2. Esto podría tomar un par de segundos para cada URL. Si
tuviera 1,000 URL, ¡esto podría llevar un par de horas!

¿No sería genial si tuviera 100 máquinas y Map pudiera distribuir


automáticamente el trabajo entre todas ellas? ¡Entonces estaría descargando
100 páginas a la vez, y el trabajo sería mucho más rápido!
Esta es la idea detrás del “mapa” en MapReduce.

La función de reducción
La función de reducción confunde a la gente a veces. La idea es que “reduzcas”
una lista completa de elementos a un solo elemento. Con el mapa, pasa de una
matriz a otra.

Con reduce, transforma una matriz en un solo elemento.

Aquí hay un ejemplo:

>>> arr1 = [1, 2, 3, 4, 5]


>>> reducir(lambda x,y: x+y, arr1)
15
Machine Translated by Google

Filtros Bloom y HyperLogLog 211

En este caso, sumas todos los elementos de la matriz: 1 + 2 + 3 + 4 + 5 = 15. No


explicaré reduce con más detalle aquí, porque hay muchos tutoriales en línea.

MapReduce utiliza estos dos conceptos simples para ejecutar consultas sobre datos
en varias máquinas. Cuando tiene un gran conjunto de datos (miles de millones de
filas), MapReduce puede brindarle una respuesta en minutos, mientras que una base
de datos tradicional podría demorar horas.

Filtros Bloom y HyperLogLog


Supongamos que está ejecutando Reddit. Cuando alguien publica un enlace, desea ver
si se ha publicado antes. Las historias que no se han publicado antes se consideran
más valiosas. Por lo tanto, debe averiguar si este enlace se ha publicado antes.

O suponga que es Google y está rastreando páginas web. Solo desea rastrear una
página web si aún no la ha rastreado. Por lo tanto, debe averiguar si esta página se ha
rastreado antes.

O suponga que está ejecutando bit.ly, que es un acortador de URL. No desea redirigir
a los usuarios a sitios web maliciosos. Tiene un conjunto de direcciones URL que se
consideran maliciosas. Ahora debe averiguar si está redirigiendo al usuario a una URL en
ese conjunto.

Todos estos ejemplos tienen el mismo problema. Tienes un conjunto muy grande.
Machine Translated by Google

212 Capítulo 11 I Adónde ir después

Ahora tiene un nuevo elemento y desea ver si pertenece a ese conjunto. Podrías
hacer esto rápidamente con un hash. Por ejemplo, supongamos que Google tiene
un gran hash en el que las claves son todas las páginas que ha rastreado.

Quiere ver si ya ha rastreado adit.io. Búscalo en el hash.

adit.io es una clave en el hash, por lo que ya lo ha rastreado. El tiempo de


búsqueda promedio para las tablas hash es O(1). adit.io está en el hash,
por lo que ya lo rastreó. Lo descubriste en tiempo constante. ¡Bastante bueno!
Excepto que este hash debe ser enorme. Google indexa billones de páginas web.
Si este hash tiene todas las URL que Google ha indexado, ocupará mucho espacio.
Reddit y bit.ly tienen el mismo problema de espacio. ¡Cuando tienes tantos datos,
necesitas ser creativo!

Filtros de floración
Los filtros Bloom ofrecen una solución. Los filtros Bloom son estructuras de
datos probabilísticos. Te dan una respuesta que podría estar equivocada pero
probablemente sea correcta. En lugar de un hash, puede preguntarle a su filtro de
floración si ha rastreado esta URL antes. Una tabla hash le daría una respuesta
precisa. Un filtro de floración le dará una respuesta que probablemente sea correcta:

• Los falsos positivos son posibles. Google podría decir: "Ya ha rastreado este sitio",
aunque no lo haya hecho.

• Los falsos negativos no son posibles. Si el filtro de floración dice: "No ha rastreado
este sitio", entonces definitivamente no ha rastreado este sitio.

Los filtros Bloom son geniales porque ocupan muy poco espacio. Una tabla hash
tendría que almacenar cada URL rastreada por Google, pero un filtro de floración
no tiene que hacer eso. Son geniales cuando no necesitas una respuesta exacta,
como en todos estos ejemplos. Está bien que bit.ly diga: "Creemos que este sitio
puede ser malicioso, así que ten mucho cuidado".
Machine Translated by Google

Los algoritmos SHA 213

HyperLogLog
En la misma línea hay otro algoritmo llamado HyperLogLog.
Supongamos que Google quiere contar el número de búsquedas
únicas realizadas por sus usuarios. O supongamos que Amazon quiere contar
la cantidad de artículos únicos que los usuarios miraron hoy. ¡Responder a
estas preguntas requiere mucho espacio! Con Google, tendría que mantener
un registro de todas las búsquedas únicas. Cuando un usuario busca algo,
debe ver si ya está en el registro. De lo contrario, debe agregarlo al registro.
¡Incluso por un solo día, este registro sería enorme!
HyperLogLog aproxima el número de elementos únicos en un conjunto.
Al igual que los filtros de floración, no le dará una respuesta exacta, pero
se acerca mucho y usa solo una fracción de la memoria que tomaría una tarea
como esta.

Si tiene muchos datos y está satisfecho con las respuestas aproximadas,


¡consulte los algoritmos probabilísticos!

Los algoritmos SHA


¿Recuerdas el hashing del capítulo 5? Solo para recapitular, suponga que
tiene una clave y desea colocar el valor asociado en una matriz.

Utiliza una función hash para decirle en qué ranura colocar el valor.

Y pones el valor en esa ranura.


Machine Translated by Google

214 Capítulo 11 I Adónde ir después

Esto le permite realizar búsquedas en tiempo constante. Cuando desee saber el


valor de una clave, puede usar la función hash nuevamente y le dirá en tiempo O
(1) qué ranura verificar.

En este caso, desea que la función hash le proporcione una buena distribución.
Entonces, una función hash toma una cadena y le devuelve el número de ranura para
esa cadena.

Comparando archivos
Otra función hash es una función de algoritmo hash seguro (SHA).
Dada una cadena, SHA le da un hash para esa cadena.

La terminología puede ser un poco confusa aquí. SHA es una función hash.
Genera un hash, que es solo una cadena corta. La función hash para las tablas
hash pasó de una cadena a un índice de matriz, mientras que SHA va de una
cadena a otra.

SHA genera un hash diferente para cada cadena.

Nota
Los hash SHA son largos. Han sido truncados aquí.

Puede usar SHA para saber si dos archivos son iguales. Esto es útil cuando tiene
archivos muy grandes. Suponga que tiene un archivo de 4 GB. Desea verificar si su
amigo tiene el mismo archivo grande. No tiene que tratar de enviarles por correo
electrónico su archivo grande. En su lugar, puede calcular el hash SHA y compararlo.
Machine Translated by Google

Los algoritmos SHA 215

Comprobación de contraseñas
SHA también es útil cuando desea comparar cadenas sin revelar cuál era la cadena
original. Por ejemplo, suponga que Gmail es pirateado y el atacante roba todas las
contraseñas. ¿Está su contraseña a la vista? No, no lo es. ¡Google no almacena la
contraseña original, solo el hash SHA de la contraseña! Cuando ingresa su contraseña,
Google la codifica y la compara con el hash en su base de datos.

Entonces, solo está comparando hashes, ¡no tiene que almacenar su contraseña!
SHA se usa muy comúnmente para codificar contraseñas como esta. Es un hash
unidireccional. Puede obtener el hash de una cadena.
Machine Translated by Google

216 Capítulo 11 I Adónde ir después

Pero no puede obtener la cadena original del hash.

Eso significa que si un atacante obtiene los hash SHA de Gmail, ¡no podrá
volver a convertir esos hash en las contraseñas originales! Puede convertir
una contraseña en un hash, pero no al revés.
SHA es en realidad una familia de algoritmos: SHA-0, SHA-1, SHA-2 y
SHA-3. Al escribir estas líneas, SHA-0 y SHA-1 tienen algunas debilidades.
Si usa un algoritmo SHA para el hash de contraseña, use SHA-2 o SHA-3.
Actualmente, el estándar de oro para las funciones de hashing de contraseñas
es bcrypt (aunque nada es infalible).

Hashing sensible a la localidad


SHA tiene otra característica importante: es insensible a la localidad. Suponga
que tiene una cadena y genera un hash para ella.

Si cambia solo un carácter de la cadena y regenera el hash, ¡es totalmente


diferente!

Esto es bueno porque un atacante no puede comparar hashes para ver si


está cerca de descifrar una contraseña.
A veces, desea lo contrario: desea una función hash sensible a la localidad.
Ahí es donde entra Simhash. Si realiza un pequeño cambio en una cadena,
Simhash genera un hash que es solo un poco diferente. Esto le permite
comparar hashes y ver qué tan similares son dos cadenas, ¡lo cual es
bastante útil!

• Google usa Simhash para detectar duplicados mientras rastrea la web.

• Un maestro podría usar Simhash para ver si un estudiante estaba copiando


un ensayo de la web.
Machine Translated by Google

Intercambio de claves Diffie-Hellman 217

• Scribd permite a los usuarios cargar documentos o libros para compartir con
otros. ¡Pero Scribd no quiere que los usuarios carguen contenido con derechos de autor!
El sitio podría usar Simhash para verificar si una carga es similar a un libro de Harry Potter y,
de ser así, rechazarla automáticamente.

Simhash es útil cuando desea buscar elementos similares.

Intercambio de claves Diffie-Hellman


El algoritmo de Diffie-Hellman merece una mención aquí, porque resuelve un antiguo problema
de una manera elegante. ¿Cómo cifra un mensaje para que solo pueda leerlo la persona a la
que se lo envió?

La forma más sencilla es idear un cifrado, como a = 1, b = 2, etc.


Luego, si te envío el mensaje “4,15,7”, puedes traducirlo a “d,o,g”.
Pero para que esto funcione, ambos tenemos que estar de acuerdo con el cifrado. No podemos
ponernos de acuerdo sobre el correo electrónico, porque alguien podría piratear su correo
electrónico, descifrar el cifrado y decodificar nuestros mensajes. Diablos, incluso si nos
encontramos en persona, alguien podría adivinar el código, no es complicado. Así que
deberíamos cambiarlo todos los días. ¡Pero luego tenemos que reunirnos en persona para
cambiarlo todos los días!

Incluso si logramos cambiarlo todos los días, un cifrado simple como este es fácil de descifrar
con un ataque de fuerza bruta. Supongamos que veo el mensaje "9,6,13,13,16 24,16,19,13,5".
Supongo que esto usa a = 1, b = 2 y
pronto.

Eso es un galimatías. Intentemos a = 2, b = 3, y así sucesivamente.


Machine Translated by Google

218 Capítulo 11 I Adónde ir después

¡Eso funciono! Un cifrado simple como este es fácil de descifrar. Los alemanes usaron
un cifrado mucho más complicado en la Segunda Guerra Mundial, pero aún estaba descifrado.
Diffie-Hellman resuelve ambos problemas:

• Ambas partes no necesitan saber el cifrado. Así que no tenemos que encontrarnos
y de acuerdo con lo que debería ser el cifrado.

• Los mensajes encriptados son extremadamente difíciles de decodificar.

Diffie-Hellman tiene dos claves: una clave pública y una clave privada. La clave pública es
exactamente eso: pública. Puedes publicarlo en tu sitio web, enviarlo por correo electrónico
a tus amigos o hacer lo que quieras con él. No tienes que ocultarlo.
Cuando alguien quiere enviarte un mensaje, lo encripta usando la clave pública. Un
mensaje cifrado solo se puede descifrar con la clave privada. Mientras seas la única
persona con la clave privada, ¡solo tú podrás descifrar este mensaje!

El algoritmo Diffie-Hellman todavía se usa en la práctica, junto con su sucesor, RSA. Si


está interesado en la criptografía, Diffie-Hellman es un buen lugar para comenzar: es elegante
y no demasiado difícil de seguir.

Programación lineal
Guardé lo mejor para el final. La programación lineal es una de las mejores cosas
que conozco.

La programación lineal se usa para maximizar algo dadas algunas restricciones. Por
ejemplo, suponga que su empresa fabrica dos productos, camisas y bolsos. Las camisas
necesitan 1 metro de tela y 5 botones. Los totes necesitan 2 metros de tela y 2 botones.
Tienes 11 metros de tela y 20 botones. Usted gana $2 por camisa y $3 por bolso. ¿Cuántas

camisas y bolsos debe fabricar para maximizar sus ganancias?

Aquí está tratando de maximizar las ganancias y está limitado por la cantidad de
materiales que tiene.

Otro ejemplo: eres un político y quieres maximizar la cantidad de votos que obtienes. Su
investigación ha demostrado que se necesita un promedio de una hora de trabajo
(marketing, investigación, etc.) por cada voto de un residente de San Francisco o 1,5
horas/voto de un residente de Chicago. Necesita al menos 500 habitantes de San
Francisco y al menos 300 habitantes de Chicago. Tienes
Machine Translated by Google

Epílogo 219

50 días También te cuesta $2/San Francisco versus $1/Chicago. Su presupuesto


total es de $1,500. ¿Cuál es el número máximo de votos totales que puede obtener
(San Francisco + Chicago)?

Aquí está tratando de maximizar los votos y está limitado por el tiempo y el dinero.

Es posible que esté pensando: “Ha hablado sobre muchos temas de optimización en
este libro. ¿Cómo se relacionan con la programación lineal?” Todos los algoritmos de
gráficos se pueden hacer a través de la programación lineal en su lugar.
La programación lineal es un marco mucho más general, y los problemas de
gráficos son un subconjunto de eso. ¡Espero que tu mente esté alucinada!

La programación lineal utiliza el algoritmo Simplex. Es un algoritmo complejo,


razón por la cual no lo incluí en este libro. Si está interesado en la
optimización, ¡busque programación lineal!

Epílogo
Espero que este recorrido rápido por 10 algoritmos le haya mostrado cuánto queda
por descubrir. Creo que la mejor manera de aprender es encontrar algo que te interese
y sumergirte. Este libro te dio una base sólida para hacer precisamente eso.
Machine Translated by Google
Machine Translated by Google

respuestas
a ejercicios

CAPÍTULO 1
1.1 Suponga que tiene una lista ordenada de 128 nombres y la está
buscando utilizando una búsqueda binaria. ¿Cuál es el número
máximo de pasos que daría?
Respuesta: 7.

1.2 Suponga que duplica el tamaño de la lista. ¿Cuál es el número


máximo de pasos ahora?
Respuesta: 8.

1.3 Tiene un nombre y desea encontrar el número de teléfono de la


persona en la guía telefónica.
Respuesta: O(log n).

1.4 Tiene un número de teléfono y desea encontrar el nombre de la


persona en la guía telefónica. (Pista: ¡Tendrás que buscar en
todo el libro!)
Respuesta: O(n).

1.5 Quiere leer los números de cada persona en la guía telefónica.


Respuesta: O(n).

1.6 Quiere leer los números de solo el As.

Respuesta: O(n). Puede pensar: "Solo estoy haciendo esto para 1 de


26 caracteres, por lo que el tiempo de ejecución debe ser O (n/26)". Una
regla simple para recordar es ignorar los números que se suman, restan,
multiplican o dividen. Ninguno de estos son tiempos de ejecución correctos de Big O:

221
Machine Translated by Google

222 respuestas a ejercicios

O(n + 26), O(n - 26), O(n * 26), O(n / 26). ¡Son todos iguales a O(n)! ¿Por qué? Si
tiene curiosidad, vaya a "Revisión de la notación Big O", en el capítulo 4, y lea sobre
las constantes en la notación Big O (una constante es solo un número; 26 fue la
constante en esta pregunta).

CAPITULO 2
2.1 Suponga que está creando una aplicación para realizar un seguimiento de sus finanzas.

Todos los días, escribes todo en lo que gastaste dinero. Al final del mes, revisas
tus gastos y sumas cuánto gastaste. Entonces, tienes muchas inserciones y
algunas lecturas.
¿Deberías usar una matriz o una lista?

Respuesta: En este caso, está agregando gastos a la lista todos los días y leyendo
todos los gastos una vez al mes. Las matrices tienen lecturas rápidas e inserciones
lentas. Las listas enlazadas tienen lecturas lentas e inserciones rápidas.

Debido a que insertará más a menudo que leerá, tiene sentido usar una lista enlazada.
Además, las listas vinculadas tienen lecturas lentas solo si accede a elementos
aleatorios en la lista. porque estas leyendo
cada elemento de la lista, las listas vinculadas también funcionarán bien en las lecturas .
Entonces, una lista enlazada es una buena solución a este problema.

2.2 Suponga que está creando una aplicación para que los restaurantes lleven a los clientes
pedidos. Su aplicación necesita almacenar una lista de pedidos. Los meseros
continúan agregando pedidos a esta lista, y los chefs quitan pedidos de la lista y los preparan.
Es una cola de pedidos: los servidores agregan pedidos al final de la cola y el
chef toma el primer pedido de la cola y lo cocina.
Machine Translated by Google

respuestas a ejercicios 223

¿Usaría una matriz o una lista enlazada para implementar esta cola?
(Pista: las listas enlazadas son buenas para inserciones/eliminaciones, y las matrices
son buenas para el acceso aleatorio. ¿Cuál vas a hacer aquí?)

Respuesta: Una lista enlazada. Se están produciendo muchas inserciones


(servidores que agregan pedidos), en las que se destacan las listas vinculadas. No
necesita búsqueda o acceso aleatorio (en qué se destacan las matrices), porque los
chefs siempre toman el primer pedido de la cola.

2.3 Hagamos un experimento mental. Supongamos que Facebook mantiene una lista de nombres de
usuario. Cuando alguien intenta iniciar sesión en Facebook, se realiza una búsqueda de su
nombre de usuario. Si su nombre está en la lista de nombres de usuario, pueden iniciar sesión.

Las personas inician sesión en Facebook con bastante frecuencia, por lo que hay muchas
búsquedas a través de esta lista de nombres de usuario. Supongamos que Facebook usa la
búsqueda binaria para buscar en la lista. La búsqueda binaria necesita acceso aleatorio: debe
poder llegar al medio de la lista de nombres de usuario al instante. Sabiendo esto, ¿implementaría
la lista como una matriz o como una lista enlazada?

Respuesta: Una matriz ordenada. Las matrices le brindan acceso aleatorio: puede
obtener un elemento del medio de la matriz al instante. No puedes hacer eso con
listas enlazadas. Para llegar al elemento central en una lista enlazada, debe comenzar
en el primer elemento y seguir todos los enlaces hasta el elemento central.

2.4 Las personas también se registran en Facebook con bastante frecuencia. Supongamos que
decidió usar una matriz para almacenar la lista de usuarios. ¿Cuáles son las
desventajas de una matriz para inserciones? En particular, suponga que está
utilizando la búsqueda binaria para buscar inicios de sesión. ¿Qué sucede cuando
agrega nuevos usuarios a una matriz?

Respuesta: La inserción en arreglos es lenta. Además, si está utilizando la búsqueda


binaria para buscar nombres de usuario, la matriz debe ordenarse.
Supongamos que alguien llamado Adit B se registra en Facebook. Su nombre se
insertará al final de la matriz. ¡Entonces debe ordenar la matriz cada vez que se
inserta un nombre!
Machine Translated by Google

224 respuestas a ejercicios

2.5 En realidad, Facebook no utiliza ni una matriz ni una lista enlazada para almacenar la
información del usuario. Consideremos una estructura de datos híbrida: una matriz de
listas enlazadas. Tienes una matriz con 26 ranuras. Cada ranura apunta a una lista
enlazada. Por ejemplo, el primer espacio en la matriz apunta a una lista vinculada que
contiene todos los nombres de usuario que comienzan con a. El segundo espacio apunta
a una lista enlazada que contiene todos los nombres de usuario que comienzan con b, y
así sucesivamente.

Suponga que Adit B se registra en Facebook y desea agregarlos a la lista. Vaya a la ranura
1 en la matriz, vaya a la lista vinculada para la ranura 1 y agregue Adit B al final. Ahora,
suponga que desea buscar a Zakhir H. Vaya al espacio 26, que apunta a una lista vinculada
de todos los nombres de Z. Luego busca en esa lista para encontrar a Zakhir H.

Compare esta estructura de datos híbrida con arreglos y listas enlazadas. ¿Es más lento o
más rápido que cada uno para buscar e insertar? No tiene que dar tiempos de ejecución de
Big O, solo si la nueva estructura de datos sería más rápida o más lenta.

Respuesta: Búsqueda: más lenta que las matrices, más rápida que las listas enlazadas.
Inserción: más rápido que las matrices, la misma cantidad de tiempo que las listas vinculadas.
Por lo tanto, es más lento para buscar que una matriz, pero más rápido o igual que las
listas vinculadas para todo. Hablaremos de otra estructura de datos híbrida llamada tabla
hash más adelante en el libro. Esto debería darle una idea de cómo puede construir

estructuras de datos más complejas a partir de estructuras simples.

Entonces, ¿qué usa realmente Facebook? Probablemente use una docena de


bases de datos diferentes, con diferentes estructuras de datos detrás de ellas: tablas

hash, árboles B y otros. Las matrices y las listas vinculadas son los componentes básicos
de estas estructuras de datos más complejas.
Machine Translated by Google

respuestas a ejercicios 225

CAPÍTULO 3
3.1 Suponga que le muestro una pila de llamadas como esta.

¿Qué información me puede dar, solo en función de esta pila de llamadas?

Respuesta: Aquí hay algunas cosas que podrías decirme:

• La función de saludo se llama primero, con nombre = maggie.

• Entonces la función saludar llama a la función saludar2 , con nombre =


maggie.

• En este momento, la función de saludo se encuentra en un estado


suspendido incompleto.

• La llamada de función actual es la función Greet2 .

• Después de que se complete la llamada de esta función, la función de saludo


reanudar.

3.2 Suponga que accidentalmente escribe una función recursiva que se ejecuta para
siempre. Como vio, su computadora asigna memoria en la pila para cada llamada
de función. ¿Qué sucede con la pila cuando su función recursiva se ejecuta para
siempre?

Respuesta: La pila crece para siempre. Cada programa tiene una cantidad
limitada de espacio en la pila de llamadas. Cuando su programa se quede sin
espacio (que eventualmente lo hará), saldrá con un error de desbordamiento de
pila.
Machine Translated by Google

226 respuestas a ejercicios

CAPÍTULO 4
4.1 Escriba el código de la función de suma anterior .

Respuesta:

def suma(lista):
si lista == []:
volver 0
devolver lista[0] + suma(lista[1:])

4.2 Escriba una función recursiva para contar el número de elementos en una lista.

Respuesta:

def cuenta(lista):
si lista == []:
volver 0
devuelve 1 + cuenta (lista [1:])

4.3 Encuentra el número máximo en una lista.

Respuesta:

def max(lista):
si len(lista) == 2:
volver lista[0] si lista[0] > lista[1] sino lista[1]
sub_max = max(lista[1:])
volver lista[0] si lista[0] > sub_max else sub_max

4.4 ¿Recuerdas la búsqueda binaria del capítulo 1? Es una división y


algoritmo de conquista, también. ¿Puedes encontrar el caso base y el caso recursivo para
la búsqueda binaria?

Respuesta: El caso base para la búsqueda binaria es una matriz con un elemento.
Si el elemento que está buscando coincide con el elemento de la matriz, ¡lo ha encontrado!
De lo contrario, no está en la matriz.

En el caso recursivo de la búsqueda binaria, divide la matriz por la mitad, desecha una
mitad y llama a la búsqueda binaria en la otra mitad.

¿Cuánto tiempo tomaría cada una de estas operaciones en notación Big O?

4.5 Imprimiendo el valor de cada elemento en un arreglo.


Respuesta: O(n)

4.6 Duplicar el valor de cada elemento de un arreglo.


Respuesta: O(n)

4.7 Duplicar el valor del primer elemento de un arreglo.


Respuesta: O(1)
Machine Translated by Google

respuestas a ejercicios 227

4.8 Crear una tabla de multiplicar con todos los elementos del arreglo.
Entonces, si tu arreglo es [2, 3, 7, 8, 10], primero multiplicas cada elemento por 2, luego
multiplicas cada elemento por 3, luego por 7, y así sucesivamente.

Respuesta: O(n2 )

CAPÍTULO 5
¿Cuáles de estas funciones hash son consistentes?

5.1 f(x) = 1 Devuelve "1" para todas las entradas

Respuesta: consistente

5.2 f(x) = rand() Devuelve un número aleatorio cada vez

Respuesta: no consistente

5.3 f(x) = siguiente_ranura_vacia() Devuelve el índice del siguiente

ranura vacía en la tabla hash

Respuesta: no consistente

5.4 f(x) = largo(x) Utiliza la longitud de la cadena como índice.

Respuesta: consistente

Suponga que tiene estas cuatro funciones hash que funcionan con cadenas:

A. Devuelve "1" para todas las entradas.

B. Use la longitud de la cadena como índice.

C. Utilice el primer carácter de la cadena como índice. Entonces, todas las cadenas
que comienzan con a se codifican juntas, y así sucesivamente.

D. Asigne cada letra a un número primo: a = 2, b = 3, c = 5, d = 7, e = 11, y así


sucesivamente. Para una cadena, la función hash es la suma de todos los
caracteres módulo el tamaño del hash. Por ejemplo, si su tamaño de hash es 10 y
la cadena es "bolsa", el índice es 3 + 2 +
17 % 10 = 22 % 10 = 2.

Para cada uno de los siguientes ejemplos, ¿qué funciones hash proporcionarían una
buena distribución? Suponga un tamaño de tabla hash de 10 ranuras.

5.5 Una agenda donde las claves son nombres y los valores son números
de teléfono. Los nombres son los siguientes: Esther, Ben, Bob y Dan.

Respuesta: Las funciones hash C y D darían una buena distribución.


Machine Translated by Google

228 respuestas a ejercicios

5.6 Un mapeo del tamaño de la batería a la potencia. Los tamaños son A, AA, AAA y AAAA.

Respuesta: Las funciones hash B y D darían una buena distribución.

5.7 Un mapeo de títulos de libros a autores. Los títulos son Maus, Fun Home y Watchmen.

Respuesta: Las funciones hash B, C y D darían una buena distribución.

CAPÍTULO 6
Ejecute el algoritmo de búsqueda primero en amplitud en cada uno de estos gráficos
para encontrar la solución.

6.1 Encuentra la longitud del camino más corto de principio a fin.

Respuesta: El camino más corto tiene una longitud de 2.

6.2 Encuentre la longitud del camino más corto desde "taxi" hasta "murciélago".

Respuesta: El camino más corto tiene una longitud de 2.


Machine Translated by Google

respuestas a ejercicios 229

6.3 Aquí hay un pequeño gráfico de mi rutina matutina.

Para estas tres listas, marque si cada una es válida o no.

Respuestas: A: no válido; B—Válido; C: no válido.

6.4 Aquí hay un gráfico más grande. Haz una lista válida para este gráfico.

Respuesta: 1—Despierta; 2—Ejercicio; 3—Ducha; 4—Cepille los dientes;


5—Vístete; 6—Almuerzo para llevar; 7—Desayuna.
Machine Translated by Google

230 respuestas a ejercicios

6.5 ¿Cuáles de los siguientes gráficos también son árboles?

Respuestas: A—Árbol; B—No es un árbol; C-árbol. El último ejemplo es solo un


árbol de lado. Los árboles son un subconjunto de gráficos. Entonces, un árbol
siempre es un gráfico, pero un gráfico puede o no ser un árbol.

CAPÍTULO 7
7.1 En cada una de estas gráficas, ¿cuál es el peso del camino más corto
de principio a fin?

Respuestas: A: A—8; B-60; C—Pregunta capciosa. No es posible el camino más


corto (ciclo de peso negativo).
Machine Translated by Google

respuestas a ejercicios 231

CAPÍTULO 8

8.1 Trabajas para una empresa de muebles y tienes que enviar muebles a todo el
país. Necesitas empacar tu camión con cajas. Todas las cajas son de
diferentes tamaños y estás tratando de maximizar el espacio que usas en
cada camión. ¿Cómo escogerías las cajas para maximizar el espacio?
Piensa en una estrategia codiciosa. ¿Eso le dará la solución óptima?

Respuesta: Una estrategia codiciosa sería elegir la caja más grande que
quepa en el espacio restante y repetir hasta que no puedas empacar más cajas.
No, esto no le dará la solución óptima.

8.2 Te vas a Europa y tienes siete días para ver todo lo que puedas. Asignas un
valor en puntos a cada artículo (cuánto quieres verlo) y estimas cuánto
tiempo lleva. ¿Cómo puede maximizar el total de puntos (ver todas las cosas
que realmente quiere ver) durante su estadía? Piensa en una estrategia
codiciosa. ¿Eso le dará la solución óptima?

Respuesta: Siga eligiendo la actividad con el valor de puntos más alto que todavía
puede hacer en el tiempo que le queda. Detente cuando no puedas hacer nada
más. No, esto no le dará la solución óptima.

Para cada uno de estos algoritmos, diga si es un algoritmo codicioso o no.

8.3 Clasificación rápida

respuesta: no

8.4 Búsqueda en amplitud

Respuesta: Sí.

8.5 Algoritmo de Dijkstra


Respuesta: Sí.

8.6 Un cartero necesita hacer entregas en 20 hogares. Necesita encontrar la


ruta más corta que vaya a las 20 casas. ¿Es este un problema NP-
completo?
Respuesta: Sí.

8.7 Encontrar la camarilla más grande en un conjunto de personas (una camarilla


es un conjunto de personas que se conocen). ¿Es este un problema NP-
completo?
Respuesta: Sí.
Machine Translated by Google

232 respuestas a ejercicios

8.8 Estás haciendo un mapa de los EE. UU. y necesitas colorear los estados
adyacentes con diferentes colores. Tienes que encontrar el número mínimo
de colores que necesitas para que no haya dos estados adyacentes del
mismo color. ¿Es este un problema NP-completo?
Respuesta: Sí.

CAPÍTULO 9
9.1 Suponga que puede robar otro artículo: un reproductor de MP3. Pesa 1 libra y
vale $1,000. ¿Deberías robarlo?

Respuesta: Sí. Entonces podrías robar el reproductor de MP3, el iPhone y la guitarra, por un valor
total de $4,500.

9.2 Supongamos que vas a acampar. Tienes una mochila que aguanta
6 lb, y puede llevar los siguientes artículos. Cada uno tiene un valor, y cuanto mayor sea el valor,
más importante es el artículo:

• Agua, 3 libras, 10

• Libro, 1 libra, 3

• Alimentos, 2 libras, 9

• Chaqueta, 2 libras, 5

• Cámara, 1 libra, 6

¿Cuál es el conjunto óptimo de artículos para llevar en su viaje de campamento?

Respuesta: Debes llevar agua, comida y una cámara.

9.3 Dibuja y completa la cuadrícula para calcular la subcadena común más larga entre el azul
y las pistas.

Respuesta:
Machine Translated by Google

respuestas a ejercicios 233

CAPÍTULO 10
10.1 En el ejemplo de Netflix, calculó la distancia entre dos
diferentes usuarios utilizando la fórmula de la distancia. Pero no todos los
usuarios califican las películas de la misma manera. Suponga que tiene dos
usuarios, Yogi y Pinky, que tienen el mismo gusto por las películas. Pero Yogi
califica cualquier película que le gusta con un 5, mientras que Pinky es más
selectiva y reserva los 5 solo para las mejores. Están bien emparejados, pero
según el algoritmo de distancia, no son vecinos. ¿Cómo consideraría sus
diferentes estrategias de calificación?

Respuesta: Podrías usar algo llamado normalización. Observa la calificación


promedio de cada persona y la usa para escalar sus calificaciones. Por
ejemplo, puede notar que la calificación promedio de Pinky es 3, mientras que
la calificación promedio de Yogi es 3.5. Así que aumentas un poco las
calificaciones de Pinky, hasta que su calificación promedio también sea 3.5.
Luego puede comparar sus calificaciones en la misma escala.

10.2 Supongamos que Netflix nomina a un grupo de “influencers”. Por ejemplo,


Quentin Tarantino y Wes Anderson son personas influyentes en Netflix, por lo
que sus calificaciones cuentan más que las de un usuario normal. ¿Cómo
cambiaría el sistema de recomendaciones para que esté sesgado hacia las
calificaciones de las personas influyentes?

Respuesta: Podrías darle más peso a las calificaciones de los


influencers cuando usas KNN. Suponga que tiene tres vecinos: Joe, Dave y
Wes Anderson (un influencer). Calificaron a Caddyshack con un 3, un 4 y un
5, respectivamente. En lugar de simplemente tomar el promedio de sus
calificaciones (3 + 4 + 5 / 3 = 4 estrellas), podría darle más peso a la calificación
de Wes Anderson: 3 + 4 + 5 + 5 + 5 / 5 = 4.4
estrellas.

10.3 Netflix tiene millones de usuarios. El ejemplo anterior analizó el


cinco vecinos más cercanos para construir el sistema de recomendaciones.
¿Es esto demasiado bajo? ¿Demasiado alto?

Respuesta: Es demasiado bajo. Si observa menos vecinos, existe una mayor


posibilidad de que los resultados sean sesgados. Una buena regla general es
que, si tiene N usuarios, debe mirar a los vecinos sqrt(N).
Machine Translated by Google
Machine Translated by Google

Índice

bordes de peso negativo 128– comprobación de contraseñas 215–216


A 130 comparación de archivos 214 descripción
resumen 115–119 general 213
adit.io 212
terminología relacionada con algoritmos de aproximación 147–
algoritmos
120–122 150
algoritmos de aproximación 147–
150 cambiar por piano ejemplo 122-128 cálculo de respuesta 149
código para configuración 147–
cálculo de respuesta 149
distribuido, utilidad de 209 148 conjuntos 149–150
código para configuración 147–
Euclides 54
148 conjuntos 149–150
Feynman 180 eliminaciones de matrices y 30
Bellman-Ford 130
algoritmos codiciosos 141–159 ejercicios 30–31
Notación O grande y 10–19
problema de programación de inserciones y 28–29
tiempos de ejecución comunes 15–16
clases 142-144 resumen 28
dibujar cuadrados ejemplo 13-14
ejercicios 145–146 terminología utilizada con 27–28 usos
problema de la mochila 144-145 de 26–27
ejercicios 17
Problemas NP-completos 152–
crecimiento de los tiempos de ejecución a
diferentes tasas 11–13
158
B
resumen 141
resumen 10
problema de cobertura de caso base 40–41, 41, 53
problema del vendedor ambulante
conjuntos 146-151
17-19 Algoritmo Bellman-Ford 130 best_station
Algoritmo HyperLogLog 213 algoritmo 151
tiempo de ejecución en el peor de los casos 15

de k vecinos más cercanos sistema de Sitio web mejor explicado 207


búsqueda binaria 3–10
recomendaciones de construcción Notación Big O 10–19 tiempos
mejor manera de buscar 5–7
189–194 clasificación de naranjas de ejecución comunes 15–16
ejercicios 6–9
descripción general 3–4
frente a uvas dibujar cuadrados ejemplo 13–14 ejercicios
fruta 187–189 17
tiempo de ejecución
ejercicios 195–199 crecimiento de los tiempos de ejecución a
10 búsqueda en anchura 107–113
aprendizaje automático 199–201 diferentes tasas 11–13
ejercicio 111–113
Algoritmo MapReduce 209–211 función de resumen 10
tiempo de ejecución 111
mapa 209–210 función de reducción clasificación rápida y 66–71
Algoritmo de Dijkstra 115–139
210–211 caso promedio frente al peor caso
ejercicio 139
paralelo 208 68–71
implementación 131–139
Algoritmos SHA 213–216 ejercicios 72

235
Machine Translated by Google

236 índice

ordenación por fusión frente a ordenación rellenando la cuadrícula 180–


rápida 67–68 D 182 subsecuencia común más
visión general 66 larga 183–186
DAG (gráficos acíclicos dirigidos)
Problema del vendedor ambulante 17–19 122 cuadrícula de creación 179–180
descripción general 179–180
D&C (divide y vencerás) 52–60 def countdown(i)
tiempo de ejecución en el peor de los casos 15 solución 182–183
función 41 eliminaciones 30
búsqueda binaria 3–10
mejor manera de buscar 5–7 mi
función deque 107 función
ejercicios 6–9
dict 78
descripción general 3–4
bordes 99, 113
Intercambio de llaves Diffie-Hellman 217
tiempo de ejecución matriz vacía 57, 58
Algoritmo de Dijkstra 115–139 ejercicio
10 árboles de búsqueda binarios 204– 139 mensajes cifrados 218 operación
205 filtros de floración 211–212 en cola 104
implementación 131–139 bordes
búsqueda en amplitud 95–113 Algoritmo de Euclides 54
de peso negativo 128–130 descripción
gráficos y 99–104 ejercicios general 115–119
104
terminología relacionada con 120–122 F
encontrar el camino más
intercambio de piano ejemplo 122–128
corto 102–103
Facebook, ejemplo de registro e inicio de
resumen 107–110
sesión de usuario 31 función de hecho
gráfico dirigido 106 fórmula
colas 103–104 45, 47 función factorial 45
de distancia 194
implementando 105–106
algoritmos distribuidos 209
implementando algoritmo 107– tiempo factorial 19
Resolución DNS 81
113
falsos negativos 212
cola doble 107 entradas duplicadas,
ejercicio 111-113 falsos positivos 212
evitando 81–83
resumen 107–110
Algoritmo de Feynman 180
tiempo de ejecución 111 Datos FIFO (primero en entrar, primero en salir)
programación dinámica 161–185 ejercicios
resumen 95–98 estructura 104
173–178, 186 problema de la mochila
tabla hash incorporada 90 función find_lowest_cost_node 134, 139
161–171
adiós función 44 conexión de primer grado 103 para bucle
cambiar el orden de las filas 174
149 para nodo 136
Preguntas frecuentes
C 171–173 Rellenar la cuadrícula por
columnas 174
caché, usando tablas hash como 83–85 Transformada de Fourier 207–208
fila de guitarra 164–167 si
Pila de llamadas Caldwell, la solución no llena
Leigh 40
mochila completa 178 si la solución GRAMO

descripción general
requiere más de dos submochilas 177
42–45 con recursividad 45–
git diff 185
fila de portátiles 168–170 optimización
50 nodo más barato 117, 125 gráficos
del itinerario de viaje 175–177
clasificación 189 problema de búsqueda en amplitud y 99–104

programación del aula 142–144 ejercicios 104

resumen 161 encontrar el camino más


subcadena común 184 corto 102–104
solución simple 162–163 robar
constantes 35 resumen 99–101
fracciones de un artículo 175
tiempo constante 88–89 colas 103–104
conjunto cubierto 151 resumen 96–98
fila estéreo 166–168
Atajo Ctrl-C 41 ciclos,
gráfico[“inicio”] tabla hash 132 algoritmos
subcadena común más larga 178–
gráfico 121 185 codiciosos 141–159
Machine Translated by Google

índice 237

problema de programación de clases


142–144
k METRO

ejercicios 145–146
Khan Academy 7, 54 aprendizaje automático 199–201
problema de la mochila 144-145
problema de mochila Algoritmo MapReduce
Problemas NP-completos 152–158
cambiando el orden de las filas 174 función de mapa 209–210
Problema de cobertura de conjuntos 146– función de reducción 210–211
Preguntas
151 Algoritmos de aproximación 147–
frecuentes 171–173 llenar la cuadrícula memoria 22–23
150
en columnas 174 fila de guitarras 164– clasificación por fusión frente a clasificación
volver al código 151–152
167 si la solución no llena la mochila por rápida 67–68 formato MP3 207
ejercicio 152
completo 178 si la solución requiere
resumen 146
más de dos mochilas secundarias 177 norte
saludar2 función 44
fila de computadoras portátiles 168–
saludar función 43–45
170 optimización del itinerario de viaje Clasificador Naive Bayes 200
variable de nombre 43 vecinos 99
H 175–177
n! (n factorial) operaciones 19
descripción general 144–145, nodos 99, 105 n operaciones 12
tablas hash 73–88
161 solución simple 162–163 problemas NP-completos 152–158
colisiones 86–88
robar fracciones de un artículo 175 fila
funciones hash 76–78
estéreo 166–168
rendimiento 88–91 ejercicios
algoritmo de los k-vecinos más cercanos
93
construcción de un sistema de O
buena función hash 90–91 factor
recomendaciones 189–194
de carga 90–91
clasificación de naranjas frente a pomelos OCR (reconocimiento óptico
casos de uso 79–86
187–189
de caracteres) 199–201
prevención de entradas duplicadas ejercicios 195–198
81–83
aprendizaje automático 199–201 PAGS

uso de tablas hash como caché


83–85
L algoritmos paralelos 208
uso de tablas hash para búsquedas
79–81 partición 61 función
distancia levenshtein 185 person_is_seller 108, 111 elemento pivote 60
Haskell 59
Datos LIFO (último en entrar, último en salir) acción pop (eliminar y leer) 42
Algoritmo HyperLogLog 213 estructura 104
función de impresión
I programación lineal 218–219 tiempo
lineal 10, 15, 89 listas enlazadas 25–26 43 función print_items 67 clave
privada, Diffie-Hellman 218 estructura de
pruebas inductivas 65 supresiones y 30 datos probabilísticos 212 pseudocódigo
infinito, representando en Python
ejercicios 28, 30–31 38, 40, 182 clave pública, Diffie-Hellman
133
inserciones y 28–29
218 acción de empujar (insertar) 42
inserciones 28–29
resumen 25–26
índices invertidos 206–207
terminología utilizada con 27–28 fórmula pitagórica 191
Dirección IP, asignación de dirección web
equilibrio de carga 208 hashing sensible a
a 81
la localidad 216 tiempo logarítmico. Ver q
logaritmos de tiempo de registro 7 tiempo
j
de registro 7, 10, 15 búsquedas, usando colas 30–31
tablas hash para 79–81 clasificación rápida, notación Big O y
formato JPG 207
66–71
Machine Translated by Google

238 índice

caso promedio frente al peor de los tiempo de ejecución 10 pilas 42–49

casos 68–71 búsqueda en anchura pila de llamadas 43–45

ejercicios 72 gráficos y 99–104 pila de llamadas con recursividad 45–50

ordenación por fusión frente a ordenación rápida 67–68 implementando 105–106 ejercicio 45, 49–50 resumen

implementando algoritmo 107–113 42

R estados_cubiertos conjunto 149


clasificación de selección 32–33 estados_para_estación 151
sistema de 30 acceso secuencial 30 problema estados_necesitados 151

recomendaciones de acceso aleatorio, edificio 189– de cobertura de conjuntos 146–151 algoritmos mercado de valores, predicción 201
194 de aproximación calcular respuesta 149 cadenas, asignación a números 76 función de
recursividad 37–49 caso código para configuración 147–148 suma 57, 59
base y caso recursivo conjuntos 149–150
Pila de T
llamadas 40–41 con 45–50 ejercicio 152
resumen 37–39 resumen 146
conexión de tercer grado 103 clasificación
establecer diferencia 150
regresión 196 cambio topológica 112 entrenamiento 200 árboles
establecer intersección 150
de tamaño 91 tiempo de 203–206
ejecución juegos 148
tiempos de ejecución comunes 15–16 establecer unión 150

crecimiento de a diferentes tasas 11–13 algoritmos SHA 213–216 comprobación tu


de contraseñas 215–216 comparación de
resumen 10 archivos 214 descripción general 213 gráfico no dirigido 122 búsquedas

únicas 213 gráfico no ponderado

S SHA (algoritmo hash seguro) función 92, 214 120


ruta más corta 98, 128 señales,

búsquedas procesamiento 207 W


búsqueda binaria 3–10 como

mejor forma de buscar 5–7 ejercicios 6– Simhash 216, 217


gráfico ponderado 120
9 búsqueda simple 5, 11, 200
descripción general 3–4 Consulta SQL 209
Machine Translated by Google

También podría gustarte