Read in English · Lire en français · Auf Deutsch lesen
Sección 0.2 de 0.8 · Principiante · 16 min de lectura
Datos, estructuras de datos y algoritmos, y por qué la velocidad depende de ellos
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Imagina que buscas una carta en una oficina de correos. Si las cartas están amontonadas, las revisas una por una, y un montón más grande tarda más. Si están clasificadas en casilleros por código postal, vas directo al casillero correcto, haya las cartas que haya. De esa diferencia trata esta lección.
Primero, ¿de qué están hechos los datos? Las computadoras lo almacenan todo como bits: dígitos binarios que valen cero o uno. Ocho bits forman un byte. Por sí solos, los bits no significan nada. El mismo patrón podría ser el número sesenta y cinco o la letra A. Un tipo de datos es el acuerdo sobre cómo leerlos. Los enteros son números sin decimales, para contar. Los booleanos son verdadero o falso. Las cadenas son texto. Los números de coma flotante son decimales, pero solo aproximados, así que el dinero debe almacenarse como peniques o céntimos enteros, o con un tipo decimal.
Una estructura de datos es una forma de organizar muchos valores para que las operaciones que necesitas sean rápidas. Aquí tienes seis de las más comunes. Una lista mantiene los elementos en orden, como una lista de la compra numerada. Obtener el elemento de una posición es instantáneo, pero encontrar un nombre obliga a revisar los elementos uno por uno. Un diccionario guarda valores bajo claves únicas, como un guardarropa. Entregas el resguardo C tres siete siete y recibes el abrigo correcto de inmediato. En una cola, el primero que entra es el primero que sale, como los pedidos que esperan a la cocina. En una pila, el último que entra es el primero que sale, como un montón de platos o el botón Atrás de tu navegador. Un árbol tiene una raíz con ramas por debajo, como las carpetas de tu computadora. Un grafo enlaza elementos con cualquier patrón, como un mapa de carreteras.
Un algoritmo es un método preciso, paso a paso, para resolver un problema y que siempre termina. Distintos algoritmos para el mismo problema pueden hacer cantidades de trabajo muy diferentes. Esta es una búsqueda binaria en una lista ordenada de dieciséis ID de cliente. Buscamos el cero tres siete siete. Lo comparamos con el elemento central. Es mayor, así que se descarta toda la primera mitad. Lo comparamos con el centro de lo que queda. Ahora es menor, así que también se descarta la parte superior. Cada consulta reduce a la mitad lo que queda. Dos consultas más, y lo encontramos. Con un millón de nombres ordenados, la búsqueda binaria necesita unas veinte consultas en lugar de hasta un millón.
Los ingenieros lo describen con la notación O grande. Se pregunta: si los datos crecen mucho, ¿cuánto trabajo más hay? Tiempo constante significa que el trabajo no crece, como una búsqueda en un diccionario, de media. Logarítmico significa un paso más cada vez que los datos se duplican, como en la búsqueda binaria. Lineal significa el doble de datos, el doble de trabajo, como al recorrer una lista. Cuadrático significa el doble de datos, cuatro veces el trabajo, lo que ocurre cuando cada elemento se compara con todos los demás. Un código que iba bien con mil clientes puede quedar paralizado con un millón.
Veamos ahora un ejemplo práctico. Una aplicación de reparto de comida almacena un millón de clientes, y cada vez que llega un pedido tiene que encontrar al cliente por su ID. El diseño A recorre una lista. De media, eso supone alrededor de medio millón de comprobaciones, o unos cincuenta milisegundos por búsqueda. Con doscientos pedidos por segundo, son diez segundos de trabajo del procesador cada segundo. El servidor no da abasto, así que las solicitudes se acumulan en cola y agotan su tiempo de espera. El diseño B usa un diccionario con el ID como clave. Va directamente a la entrada: más o menos una comprobación, haya mil clientes o un millón. El diseño B no es gratis. Usa memoria adicional y hay que mantenerlo al día cuando cambian los clientes. Pero convierte un fracaso en un margen holgado.
Esta es la diferencia. Arriba, el recorrido compara el ID buscado con cada cliente por turno. Un millón de clientes puede exigir un millón de comparaciones. Abajo, el diccionario pasa la clave por una función hash, que indica que C tres siete siete está en la ranura cuatro, y va directamente allí. Un paso, sea cual sea el número de clientes.
El diccionario es más rápido porque usa memoria adicional para organizar los datos de antemano. Es uno de los compromisos más habituales de la informática: comprar velocidad con espacio. Un índice, como el que hay al final de un libro, acelera las lecturas, pero ocupa almacenamiento y debe actualizarse en cada escritura. Una caché guarda una copia de una respuesta costosa en algún lugar rápido. Pero cuando cambian los datos, ¿hasta qué punto puede estar obsoleta la copia? Un precio obsoleto durante unos segundos puede no ser problema. Un saldo bancario obsoleto quizá sí lo sea. Así que los arquitectos se preguntan qué preguntas deben responderse rápidamente y cuánto crecerán los datos. Ninguna cantidad de hardware adicional salvará por mucho tiempo a un algoritmo cuadrático.
La sección completa incluye más sobre el texto, una tabla que compara las estructuras de datos y las cifras que hay detrás de Big-O. A continuación, en la sección cero punto tres, pasamos de los datos al código y a cómo se organiza. Nos vemos allí.
Lo que necesitas saber
- Las computadoras lo almacenan todo como bits; un byte son ocho bits, y los tipos de datos (enteros, números de coma flotante, booleanos, cadenas) indican cómo leerlos.
- Los números de coma flotante son aproximados, así que el dinero debe almacenarse como enteros de la unidad más pequeña o con un tipo decimal.
- El texto se almacena como caracteres Unicode codificados como bytes, normalmente en UTF-8, donde un carácter puede ocupar de uno a cuatro bytes.
- Cada estructura de datos, como las listas, los diccionarios, los conjuntos, las colas, las pilas, los árboles y los grafos, hace rápidas operaciones distintas.
- La notación O grande (Big-O) describe cómo crece el trabajo de un algoritmo con los datos: O(1), O(log n), O(n), O(n log n), O(n2).
- Los índices y las cachés cambian espacio adicional (y el trabajo de mantenerlos al día) por respuestas más rápidas.
Todo son bits
Las computadoras lo almacenan todo como bits: dígitos binarios que valen 0 o 1. Físicamente, un bit puede ser una diminuta carga eléctrica, una zona magnética de un disco o un pulso de luz en una fibra, pero para el software es simplemente uno de dos valores. Ocho bits forman un byte, que puede contener 28 = 256 patrones distintos, de 00000000 a 11111111.
A partir de ahí, los tamaños van creciendo. Un kilobyte son unos mil bytes, un megabyte alrededor de un millón, un gigabyte alrededor de mil millones y un terabyte alrededor de un billón. (En sentido estricto, los prefijos estándar kilo, mega y giga significan potencias de 1000; la memoria suele medirse en potencias de 1024, y para ellas existen las unidades kibibyte, mebibyte y gibibyte. La diferencia rara vez importa para el diseño, pero explica por qué un disco de "1 TB" muestra menos espacio en algunos gestores de archivos.) Una página de texto plano ocupa unos pocos kilobytes, una foto de un teléfono unos pocos megabytes y una hora de video en alta definición unos pocos gigabytes.
Los bits no significan nada por sí solos. El patrón 01000001 podría ser el número 65, la letra A, parte de un color de una foto o parte de una instrucción para la CPU. Lo que da significado a los bits es el acuerdo sobre cómo leerlos, que es la idea de un tipo de datos.
Tipos de datos: números, texto y verdadero o falso
Un tipo de datos le dice a un programa qué clase de valor es un dato y, por tanto, qué operaciones tienen sentido. Todos los lenguajes de programación tienen unos cuantos tipos básicos, y los de Python son representativos.
Los enteros son números sin decimales, como 3, 0 o -42, que se usan para contar cosas: artículos en un carrito, asientos en un vuelo. Los números de coma flotante (floats), como 12.5 o 0.2, representan los decimales de forma aproximada, en binario. Como muchos decimales no tienen una forma binaria exacta, 0.1 + 0.2 en Python da 0.30000000000000004. Eso no es problema en física o en gráficos, pero es incorrecto para el dinero, y por eso los sistemas financieros almacenan los importes como números enteros de peniques o céntimos, o usan un tipo decimal (Python tiene decimal.Decimal). Los booleanos tienen solo dos valores, True y False, y registran hechos de sí o no, como "¿se ha pagado este pedido?". Las cadenas son texto, como el nombre de un cliente. Los sistemas reales añaden más tipos por encima: fechas y horas, dinero y datos binarios como las imágenes.
Los tipos importan más allá de un solo programa. Cuando dos sistemas intercambian datos, deben ponerse de acuerdo tanto en los tipos como en los valores: ¿un código postal es un número o texto (texto: algunos empiezan por letras y algunos códigos numéricos empiezan por cero)? ¿Una fecha va en orden día-mes o mes-día, y en qué zona horaria? Muchos bugs de integración costosos son desacuerdos sobre tipos.
El texto y UTF-8
El texto se almacena asignando un número a cada carácter y guardando después esos números como bytes. El estándar internacional Unicode asigna un número, llamado punto de código, a bastante más de cien mil caracteres de los sistemas de escritura del mundo, además de símbolos y emojis. Después, una codificación decide cómo esos números se convierten en bytes. Hoy, la codificación dominante, en la web y en la mayoría de los sistemas nuevos, es UTF-8.
UTF-8 usa un número variable de bytes por carácter: un byte para las letras básicas del inglés, los dígitos y la puntuación (así que el texto que solo usa estos caracteres es idéntico, byte a byte, en UTF-8 y en la codificación ASCII, mucho más antigua), dos bytes para caracteres como é, tres para caracteres como € y la mayoría de los caracteres chinos, japoneses y coreanos, y cuatro para los emojis y algunas escrituras menos comunes. Esto tiene una consecuencia práctica: el número de caracteres de una cadena y el número de bytes que ocupa pueden ser distintos. Una columna de base de datos limitada a 50 bytes puede no ser capaz de contener un nombre de 50 caracteres escrito en griego.
El bug de texto clásico son los caracteres ilegibles (por ejemplo Café en lugar de Café), que aparecen cuando unos bytes escritos en una codificación se leen como si fueran de otra. La solución es fácil de enunciar: usa UTF-8 en todas partes, y dilo explícitamente siempre que el texto cruce un límite, como un archivo, una base de datos o un mensaje de red.
Estructuras de datos: formas de organizar los datos
Los programas rara vez manejan un solo valor a la vez. Una estructura de datos es una forma de organizar muchos valores en memoria para que las operaciones que necesitas sean rápidas. La elección correcta puede marcar la diferencia entre una página que carga al instante y otra que no llega a cargar nunca. Cada una tiene una analogía cotidiana.
Una lista (llamada array en muchos lenguajes) mantiene los elementos en orden, como una lista de la compra numerada. Obtener el quinto elemento es instantáneo, porque la computadora puede calcular dónde está. Encontrar un elemento por su valor obliga a revisar los elementos uno por uno. Un diccionario (también llamado mapa o tabla hash) guarda valores bajo claves únicas, como un guardarropa: entregas el resguardo C377 y recibes el abrigo correcto de inmediato, sin mirar todos los abrigos. Un conjunto es una colección sin duplicados, como una lista de invitados, y responde rápidamente a "¿está esta persona en ella?".
Una cola funciona como la fila ante el mostrador de una oficina de correos: el primero que entra es el primero que sale. Los sistemas usan colas para el trabajo pendiente, como los pedidos que esperan a la cocina. Una pila funciona como un montón de platos: el último que entra es el primero que sale. El botón Atrás de tu navegador usa una: la página que visitaste más recientemente es la primera a la que vuelves. Un árbol tiene una única raíz con ramas por debajo, como un árbol genealógico o las carpetas de tu computadora; los árboles también son la forma en que las bases de datos mantienen los datos ordenados para buscar rápido. Un grafo son elementos (nodos) unidos por enlaces (aristas) con cualquier patrón, como un mapa de carreteras o una red social, y responde a preguntas como "¿cuál es la ruta más corta?" o "¿quiénes son amigos de mis amigos?".
Seis estructuras de datos comunes
Para qué sirve cada estructura
| Estructura | Rápida en | Lenta en | Uso típico |
|---|---|---|---|
| Lista / array | Obtener el elemento de una posición; añadir al final | Encontrar un elemento por su valor (hay que recorrerla); insertar cerca del principio | Un carrito ordenado de artículos; una página de resultados de búsqueda |
| Diccionario / mapa | Encontrar, añadir o eliminar por clave | Todo lo que necesite los elementos en orden | Clientes por ID; ajustes por nombre; una caché |
| Conjunto | Comprobar si un elemento pertenece; eliminar duplicados | Mantener un orden (la mayoría de los conjuntos no tienen ninguno) | Qué usuarios ya han votado; etiquetas únicas |
| Cola | Añadir al final y sacar por el principio | Acceder a los elementos del medio | Pedidos que esperan a la cocina; tareas que esperan a un worker |
| Pila | Añadir y sacar por arriba | Acceder a los elementos de más abajo | Historial de deshacer; el botón Atrás; seguimiento de las llamadas a funciones |
| Árbol (ordenado) | Encontrar por valor y listar un rango en orden | Más complejo de mantener equilibrado | Índices de bases de datos; carpetas; menús |
| Grafo | Seguir relaciones | Los grafos muy grandes necesitan un almacenamiento especializado | Rutas y mapas; redes sociales; recomendaciones |
basket = ["pizza", "cola", "pizza"] # list: ordered, duplicates allowed
print(basket[0]) # by position: "pizza"
customers = {"C102": "Ana", "C377": "Chen"} # dictionary: key to value
print(customers["C377"]) # by key: "Chen"
voted = {"ana@example.com", "ben@example.com"} # set: no duplicates
print("ana@example.com" in voted) # True
from collections import deque
kitchen = deque(["order 1", "order 2"]) # queue
kitchen.append("order 3") # joins at the back
print(kitchen.popleft()) # served first: "order 1"Algoritmos
Un algoritmo es un método preciso, paso a paso, para resolver un problema y que siempre termina: como una receta, pero sin margen de interpretación. Ordenar una lista de nombres, encontrar la ruta más corta entre dos direcciones, comprobar si una contraseña coincide y decidir qué anuncio mostrar son tareas que realizan los algoritmos.
Normalmente hay más de un algoritmo para el mismo problema, y pueden diferir enormemente en la cantidad de trabajo que hacen. Piensa en buscar un nombre en una guía telefónica impresa. Podrías empezar en la página uno y leer cada entrada hasta encontrarlo: correcto, pero lento. O podrías abrir la guía por la mitad, ver si el nombre va antes o después de esa página, descartar la mitad que no puede contenerlo y repetir. Como la guía está ordenada, cada consulta reduce a la mitad las páginas restantes. Este segundo método se llama búsqueda binaria, y en una guía de un millón de nombres necesita unas 20 consultas en lugar de hasta un millón.
Búsqueda binaria: dividir el problema a la mitad
Big-O: cómo crece el trabajo con los datos
Los ingenieros describen cómo crece el costo de un algoritmo usando la notación O grande (Big-O). Ignora el tiempo exacto en una máquina concreta y plantea una sola pregunta: si la cantidad de datos, llamada n, se hace diez o mil veces mayor, ¿cuánto trabajo más hay? Describe el peor caso salvo que se indique otra cosa, e ignora los factores constantes, lo cual es a la vez su fortaleza (capta la forma del crecimiento) y su debilidad (dos algoritmos O(n) pueden diferir en la práctica en un factor de diez).
O(1), tiempo constante: el trabajo no depende de n. Buscar una clave en un diccionario es O(1) de media. O(log n), logarítmico: el trabajo crece un paso cada vez que n se duplica, como en la búsqueda binaria. O(n), lineal: el doble de datos, el doble de trabajo, como al recorrer una lista. O(n log n): un poco peor que lineal; los buenos algoritmos de ordenación de uso general trabajan a este ritmo. O(n2), cuadrático: el doble de datos, cuatro veces el trabajo. Suele aparecer cuando, para cada elemento, un programa recorre todos los demás elementos, por ejemplo al comparar cada cliente con todos los demás para encontrar duplicados.
Cuántos pasos básicos, aproximadamente, a medida que crecen los datos
| n (elementos) | O(1) | O(log n) | O(n) | O(n log n) | O(n2) |
|---|---|---|---|---|---|
| 10 | 1 | unos 3 | 10 | unos 33 | 100 |
| 1,000 | 1 | unos 10 | 1,000 | unos 10 000 | 1,000,000 |
| 1,000,000 | 1 | unos 20 | 1,000,000 | unos 20 000 000 | 1,000,000,000,000 |
Lee con atención la última columna. A mil millones de pasos sencillos por segundo, un algoritmo de un millón al cuadrado necesita un billón de pasos: alrededor de un cuarto de hora de cálculo puro y, en la práctica, mucho más. El mismo trabajo hecho con un método O(n log n) lleva una pequeña fracción de segundo. Un código que parecía ir bien en las pruebas con 1000 clientes puede quedar paralizado con un millón, y ninguna cantidad de hardware adicional salvará por mucho tiempo a un algoritmo cuadrático.
Ejemplo práctico
Ejemplo práctico: encontrar a un cliente entre un millón
Una aplicación de reparto de comida almacena 1 000 000 de clientes. Cada vez que llega un pedido, tiene que encontrar al cliente por su ID. Compara dos diseños, suponiendo que cada comprobación tarda 0.1 microsegundos.
| Diseño A: una lista | for c in customers: if c.id == wanted: ... recorre la lista hasta encontrar una coincidencia. |
|---|---|
| Comprobaciones por búsqueda (A) | Hasta 1 000 000; de media, unas 500 000. |
| Tiempo por búsqueda (A) | Unas 500 000 x 0.1 microsegundos = 50 milisegundos. |
| Con 200 pedidos por segundo (A) | 200 x 50 ms = 10 segundos de trabajo de CPU cada segundo: el servidor no da abasto, así que las solicitudes se acumulan en cola y agotan su tiempo de espera. |
| Diseño B: un diccionario con el ID como clave | customers_by_id[wanted] calcula dónde está la entrada y va directamente a ella. |
| Comprobaciones por búsqueda (B) | Más o menos 1, haya mil clientes o un millón. |
| Tiempo por búsqueda (B) | Muy por debajo de un microsegundo. |
| Costo del diseño B | Memoria adicional para el diccionario, y el trabajo de construirlo y mantenerlo al día cada vez que se añade o modifica un cliente. |
Los dos diseños dan respuestas idénticas; solo cambia la estructura de datos, y eso convierte el resultado de un fracaso en un margen holgado.
Las bases de datos ofrecen la misma elección. Sin un índice, encontrar una fila por una columna obliga a recorrer la tabla; con un índice, bastan unos pocos pasos a través de un árbol ordenado. La sección sobre el almacenamiento de datos vuelve sobre esto.
Recorrido frente a búsqueda directa
Espacio frente a tiempo
El diccionario del ejemplo práctico es más rápido porque usa memoria adicional para organizar los datos de antemano. Este es uno de los compromisos más habituales de la informática: a menudo puedes comprar velocidad con espacio, o ahorrar espacio gastando tiempo.
Un índice son datos adicionales, guardados junto a los datos principales, que responden rápidamente a un tipo concreto de pregunta, como el índice al final de un libro. Acelera las lecturas, pero ocupa almacenamiento y debe actualizarse en cada escritura, así que insertar en una tabla con diez índices es lento. Una caché guarda una copia de la respuesta a una pregunta costosa en algún lugar rápido, como la memoria, para que la siguiente persona que pregunte la obtenga de inmediato. Las cachés son la razón de que la página de un producto popular cargue rápido aunque la base de datos que hay detrás esté ocupada, pero plantean una pregunta difícil: cuando cambian los datos subyacentes, ¿con qué rapidez debe cambiar también la copia? Mostrar un precio obsoleto durante unos segundos puede ser aceptable; mostrar un saldo bancario obsoleto quizá no.
La compresión es el intercambio contrario: gastar tiempo de CPU para comprimir los datos en menos bytes, lo que ahorra almacenamiento y tiempo de transferencia por la red.
Por qué esto importa para la arquitectura
Los arquitectos rara vez escriben algoritmos de ordenación, pero toman constantemente decisiones cuyas consecuencias son exactamente estas. ¿Qué preguntas tendrá que responder rápidamente el sistema, y tienen los datos los índices necesarios para responderlas? ¿Cuánto crecerán los datos en tres años, y qué partes del diseño son lineales o peores ante ese crecimiento? ¿Dónde compensa la caché su complejidad, y hasta qué punto pueden estar obsoletos los datos en caché? Cuando el volumen de datos se multiplica por diez, ¿qué componente falla primero?
Son preguntas sobre rendimiento y escalabilidad, que el Capítulo 2 precisa, y sobre el uso de cachés y la elección de base de datos, que el Capítulo 7 trata a fondo. La sección siguiente pasa de los datos al código, y a cómo se organiza el código para que las personas puedan entenderlo y modificarlo.
Comprueba lo que has entendido
¿Por qué 0.1 + 0.2 no da exactamente 0.3 en la mayoría de los lenguajes?
Los números de coma flotante se almacenan en binario, y muchas fracciones decimales, como 0.1 y 0.2, no tienen una forma binaria exacta, así que aparecen pequeños errores de redondeo.
¿Qué estructura de datos encaja con un botón de "deshacer" en un editor de texto, y por qué?
Una pila: cada acción se coloca encima, y deshacer saca primero la más reciente (el último que entra es el primero que sale).
Si un proceso O(n) tarda un segundo con 100 000 elementos, ¿cuánto tardará aproximadamente con 1 000 000?
Unos diez segundos, porque el trabajo lineal crece en proporción a los datos.
¿Qué pagas a cambio de la velocidad que te da un índice de base de datos?
Almacenamiento adicional, y trabajo adicional en cada inserción, actualización y eliminación para mantener el índice al día.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “¿Cómo sé si una página lenta de nuestra aplicación se debe a que falta un índice en la base de datos o a otra cosa?”