
Axel EspinosaBienvenido a esta serie de artículos donde cubrimos temas fundamentales de computación. Hoy...
Bienvenido a esta serie de artículos donde cubrimos temas fundamentales de computación. Hoy platicaremos sobre arrays, una de las estructuras de datos más utilizadas en cualquier lenguaje de programación.
Pero antes de entrar de lleno, necesitamos hablar sobre la memoria. Específicamente, la memoria RAM.
En estos ejemplos usaremos JavaScript como lenguaje principal y algunos ejemplos básicos de C. No necesitas ser experto en ninguno de los dos.
Todas las computadoras utilizan memoria para funcionar. En particular, hablamos de la memoria RAM, la cual almacena información temporal sobre los programas que estás ejecutando. Cuando digo temporal es porque, cuando la computadora se apaga, toda esa información se pierde y, al encender, la memoria vuelve a estar limpia. Es como si la RAM tuviera mala memoria.
Al hablar de memoria RAM, es inevitable hablar de direcciones y sé lo que estás pensando. ¿Cómo que direcciones?
Déjame darte un ejemplo que me ayudó a entender este concepto:
Imagina un elevador en un edificio. Cada piso tiene un número: 0, 1, 2, 3... y en cada piso hay exactamente una oficina con información. Cuando presionas el botón del piso 42, el elevador no necesita detenerse en cada piso antes. No importa si es el piso 1 o el 99, el tiempo de llegada es prácticamente el mismo. Así funciona la RAM: cada posición de memoria tiene una dirección numérica y tu computadora accede a cualquiera de forma instantánea, sin recorrer las anteriores.
Ahora imagina que una empresa renta 5 pisos consecutivos para sus 5 departamentos, todos del mismo tamaño. Eso es un array: una secuencia de datos del mismo tipo, almacenados en posiciones contiguas de memoria. Si sabes en qué piso empieza la empresa y cuánto ocupa cada departamento, puedes llegar a cualquiera de ellos con el elevador sin recorrer los demás.
La contigüidad importa porque, como vimos en la analogía, podemos acceder de manera directa a cualquier elemento sin necesidad de recorrer los anteriores. Tu computadora calcula la dirección exacta del elemento que necesitas y va directo.
En un momento vamos a ver cómo lucen los arrays en código. Por ahora, quédate conmigo para hablar de los tipos de arrays que existen.
Los arrays en su forma original son un bloque de memoria contigua con un tamaño fijo. No pueden crecer ni reducirse después de creados. Este tipo de arrays lo vemos en lenguajes como C, donde tú tienes que decirle explícitamente al lenguaje cuántos espacios debe reservarte en la memoria.
Volviendo a la analogía: la empresa rentó exactamente 5 pisos. Si después necesita un sexto, no hay forma de crecer en el mismo lugar.
Ventajas: predecible en memoria, sin costo adicional de redimensionamiento.
Limitaciones: Si necesitas más espacio, no puedes agregarlo.
Este es un ejemplo de cómo se ve un array estático en C:
int my_array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Aquí le estamos diciendo a C: "Reserva espacio para exactamente 10 números enteros". Ni más ni menos.
Los arrays dinámicos resuelven la limitación principal de los estáticos: no necesitas saber de antemano cuántos elementos vas a almacenar. Crecen conforme vas insertando elementos.
¿Cómo funciona esto por debajo? Cuando el array se llena, el lenguaje crea uno nuevo más grande en otra parte de la memoria y copia todos los elementos. Siguiendo la analogía: cuando la empresa necesita más pisos, se muda a un edificio más grande y lleva todo consigo.
Ventajas: flexibilidad, no necesitas definir un tamaño desde el inicio.
Desventajas: la cantidad de memoria no es predecible y la "mudanza" tiene un costo.
En lenguajes como JavaScript y Python, los arrays dinámicos vienen integrados por defecto:
const array = [1, 2, 3, 4, 5];
array = [1, 2, 3, 4, 5]
En JavaScript no necesitas preocuparte por declarar el tamaño. El lenguaje se encarga de todo por debajo.
Ahora que conoces ambos tipos, aquí va una comparación directa:
| Característica | Array estático | Array dinámico |
|---|---|---|
| Tamaño | Fijo, definido al crear | Crece automáticamente |
| Flexibilidad | Baja | Alta |
| Uso de memoria | Predecible | Variable |
| Costo de inserción | No aplica (tamaño fijo) | O(1) amortizado, O(n) al crecer |
| Lenguajes comunes | C, C++ | JavaScript, Python, Java (ArrayList) |
En la práctica, la mayoría de lenguajes modernos usan arrays dinámicos por defecto. Pero entender los estáticos te da una mejor perspectiva de lo que sucede por debajo.
Vamos a platicar sobre lo que puedes hacer con los arrays. Todos los ejemplos están en JavaScript.
El acceso por índice nos permite seleccionar un elemento a partir de su posición. Como en el elevador: tú sabes directamente a qué piso vas.
Los arrays tienen una forma particular de numerar sus elementos. Se empieza a contar a partir del 0.
const array = ["🥸", "☺️", "👻", "🐆", "👾", "🐶"];
array[0]; // nos devuelve "🥸"
array[1]; // nos devuelve "☺️"
array[5]; // nos devuelve "🐶"
Podemos insertar elementos al final del array utilizando el método push().
const array = ["🥸", "☺️", "👻", "🐆", "👾", "🐶"];
array.push("👏🏻");
// resultado: ["🥸", "☺️", "👻", "🐆", "👾", "🐶", "👏🏻"]
Podemos eliminar el último elemento del array con pop(). Este método además te devuelve el elemento que eliminó.
const array = ["🥸", "☺️", "👻", "🐆", "👾", "🐶"];
const eliminado = array.pop();
console.log(eliminado); // "🐶"
// resultado: ["🥸", "☺️", "👻", "🐆", "👾"]
Podemos obtener la longitud de nuestro array con la propiedad length. Esto nos dice cuántos elementos contiene.
const array = ["🥸", "☺️", "👻", "🐆", "👾", "🐶"];
console.log(array.length); // 6
Algo muy utilizado en los programas es recorrer un array, ya sea para mostrar qué elementos hay dentro o para buscar algún elemento. En JavaScript puedes hacer este recorrido con for, forEach o map.
const array = ["🥸", "☺️", "👻", "🐆", "👾", "🐶"];
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}
Si te das cuenta, aquí acabamos de utilizar lo que aprendimos en los conceptos anteriores. Usamos el acceso por índice con array[i]. El for nos ayuda a repetir algo muchas veces y en nuestro ejemplo, vamos aumentando el valor del índice para ir de izquierda a derecha. Y también usamos .length para saber cuántos elementos tiene el array.
Podemos hacer modificaciones al inicio del array o en el medio. El detalle es que estas operaciones requieren pasos adicionales. Déjame explicarte.
Cuando tienes un array, hablamos de posiciones contiguas en memoria.
Si insertamos un elemento al inicio, necesitamos desplazar todos los elementos una posición a la derecha para hacer espacio. Primero se mueven los elementos y luego se inserta el nuevo.
Y al contrario, si eliminamos un elemento del inicio, tenemos que mover todos los elementos que le seguían una posición hacia la izquierda para llenar el hueco.
Esto hace que sea más costoso que insertar o eliminar al final. En JavaScript podemos hacer estas operaciones con unshift() para insertar al inicio y shift() para eliminar del inicio.
const array = ["☺️", "👻", "🐆"];
// Insertar al inicio
array.unshift("🥸");
// resultado: ["🥸", "☺️", "👻", "🐆"]
// Eliminar del inicio
array.shift();
// resultado: ["☺️", "👻", "🐆"]
Pero no todo es miel sobre hojuelas. Como vimos, es muy fácil trabajar con los arrays pero las operaciones tienen un costo. El costo de las operaciones nos lo indica Big O, una notación matemática que nos ayuda a conocer cuánto tarda una operación a medida que el array crece. Nos ayuda a identificar la mejor estructura de datos para lo que necesitamos.
En esta tabla te dejo el costo de las operaciones que vimos:
| Operación | Complejidad |
|---|---|
| Acceso por índice | O(1) |
| Buscar un elemento | O(n) |
| Insertar al final | O(1)* |
| Insertar al inicio | O(n) |
| Eliminar al final | O(1) |
| Eliminar al inicio | O(n) |
* Amortizado. Ocasionalmente O(n) cuando el array dinámico necesita crecer.
Las operaciones O(1) indican que toman tiempo constante. No importa cuántos elementos haya en el array, el tiempo va a ser el mismo. Por eso acceder por índice es tan rápido: tu computadora calcula la dirección exacta y va directo, como el elevador.
Las operaciones O(n) quieren decir que toman un tiempo lineal: entre más elementos haya, más tiempo tarda. Por eso insertar al inicio es costoso: hay que mover todos los elementos una posición.
Si quieres saber más sobre Big O y complejidad algorítmica, dime en los comentarios y preparo un artículo dedicado.
Vamos a construir tu propia implementación de un array. Para este ejercicio debemos asumir que no tenemos los métodos especiales que vimos anteriormente. Vamos a utilizar el acceso por índices para todo.
Implementaremos los métodos: get(index), push(item), pop() y delete(index).
El objetivo es que entiendas qué pasa por debajo cuando usas un array y los métodos que los lenguajes de alto nivel nos proporcionan.
class MyArray {
constructor() {
this.length = 0;
this.data = {};
}
get(index) {
if (index < 0 || index >= this.length) {
throw new Error("Índice inválido");
}
return this.data[index];
}
push(item) {
this.data[this.length] = item;
this.length++;
return this.length;
}
pop() {
if (this.length === 0) {
throw new Error("El array está vacío");
}
const lastItem = this.data[this.length - 1];
delete this.data[this.length - 1];
this.length--;
return lastItem;
}
delete(index) {
if (index < 0 || index >= this.length) {
throw new Error("Índice inválido");
}
const item = this.data[index];
this._shiftItems(index);
return item;
}
_shiftItems(index) {
for (let i = index; i < this.length - 1; i++) {
this.data[i] = this.data[i + 1];
}
delete this.data[this.length - 1];
this.length--;
}
}
Probemos nuestra implementación:
const miArray = new MyArray();
miArray.push("🥸");
miArray.push("☺️");
miArray.push("👻");
miArray.push("🐆");
console.log(miArray.get(0)); // "🥸"
console.log(miArray.get(2)); // "👻"
console.log(miArray.pop()); // "🐆"
miArray.delete(1);
console.log(miArray.get(1)); // "👻"
Este código puedes probarlo en RunJS o de una forma visual en PythonTutor que tiene soporte para JavaScript y te muestra el paso a paso.
Fíjate en el método _shiftItems: ahí puedes ver exactamente por qué eliminar un elemento del inicio o del medio es O(n). Tenemos que recorrer todos los elementos que están después del índice eliminado y moverlos una posición.
Te reto a que implementes el método
unshift(item)para agregar elementos al inicio. Comparte tu solución en los comentarios. 💪
Entender los arrays al principio puede ser intimidante. Recuerdo que yo no entendía cómo recorrer un array con for, pero con la práctica puedes dominarlo.
Vimos cómo funciona la memoria RAM con la analogía del elevador, los arrays estáticos y los arrays dinámicos. Recuerda que los arrays dinámicos vienen en los lenguajes de alto nivel como Python o JavaScript, así que no debes preocuparte por declarar los elementos que necesitas de antemano. Pero aun así es bueno tener el entendimiento de lo que sucede por debajo. Eso nos da un mayor panorama.
En el siguiente artículo vamos a cubrir strings como estructura de datos. Y después de eso, vamos a ver patrones de entrevista con arrays y strings.
Espero que el artículo te sirva. Coméntame si te gustaría que aborde algún tema con mayor profundidad. Gracias por leer. 🙌🏻