Hub / EDA / Parcial 2

Notas de estudio — Parcial 2

Unidades: 1, 2, 3, 4 • 31 temas

1. Tabla Maestra de Complejidades

EstructuraAccesoBusquedaInsercionEliminacion
ArregloO(1)O(1)O(n)O(n) / O(logn)O(\log n) ordenadoO(n)O(n)O(n)O(n)
Lista enlazadaO(n)O(n)O(n)O(n)O(1)O(1)*O(1)O(1)*
Pila / ColaO(n)O(n)O(n)O(n)O(1)O(1)O(1)O(1)
Hash tableO(1)O(1) avg / O(n)O(n) peorO(1)O(1) avgO(1)O(1) avg
HeapO(n)O(n)O(logn)O(\log n)O(logn)O(\log n)
BST balanceadoO(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)
BST degeneradoO(n)O(n)O(n)O(n)O(n)O(n)
AVLO(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)

*Lista: O(1)O(1) si ya tienes el puntero. Encontrar la posicion es O(n)O(n).

Jerarquia Big O: O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(n^3) < O(2^n) < O(n!)

Reglas: ignorar constantes, quedarse con termino dominante, loops anidados se multiplican, secuenciales se suman.

2. Punteros en C++

Un puntero es una variable que almacena una direccion de memoria. Es el mecanismo fundamental para construir estructuras dinamicas (listas, arboles, grafos). Si no entiendes punteros, no puedes entender nada de lo que sigue.

2.1 Declaracion y operadores basicos

int x = 42;          // x vive en la direccion 0x100, su valor es 42
int* ptr = &x;       // ptr vive en 0x200, su valor es 0x100 (la direccion de x)
cout << x;            // imprime 42 — el valor de x directamente
cout << ptr;          // imprime 0x100 — la direccion almacenada en ptr
cout << *ptr;         // imprime 42 — dereferencia: el valor EN la direccion que ptr guarda
cout << &x;           // imprime 0x100 — la direccion de x
cout << &ptr;         // imprime 0x200 — la direccion del propio puntero
*ptr = 99;            // cambia el valor en la direccion 0x100, o sea cambia x a 99
cout << x;            // imprime 99 — x fue modificado a traves del puntero

Los dos significados de *:

  • En declaracion: int* ptr — “ptr es un puntero a int”
  • En uso: *ptr — “dame el valor en la direccion que ptr contiene” (dereferencia)

Los dos significados de &:

  • En expresion: &x — “dame la direccion de x” (operador address-of)
  • En declaracion de parametro: int& ref — “ref es una referencia a int”

nullptr (o NULL en C) es un puntero que no apunta a nada. Siempre inicializa punteros a nullptr si no tienes un valor aun.

2.2 Stack vs Heap

AspectoStackHeap
AsignacionAutomatica (variables locales)Manual (new / delete)
VelocidadMuy rapida (mover stack pointer)Mas lenta (buscar bloque libre)
TamanoLimitado (~1-8 MB tipico)Grande (toda la RAM disponible)
Ciclo de vidaSe destruye al salir del scopePersiste hasta que hagas delete
OrdenLIFO estrictoCualquier orden
void ejemplo() {
    int a = 10;               // stack: se destruye al salir de ejemplo()
    int* p = new int(20);     // heap: 20 persiste hasta que hagas delete
    delete p;                 // liberar memoria del heap — obligatorio
}                             // a se destruye aqui automaticamente

Regla: usa stack por defecto. Usa heap cuando necesitas que el dato sobreviva al scope actual, o cuando el tamano es muy grande o desconocido en compilacion.

2.3 Memoria dinamica: new y delete

// Asignar un solo entero en el heap
int* p = new int(42);         // p apunta a un int con valor 42 en el heap
cout << *p;                   // imprime 42
delete p;                     // libera la memoria — obligatorio
p = nullptr;                  // buena practica: evitar dangling pointer

// Asignar un arreglo en el heap
int* arr = new int[5];        // arreglo de 5 enteros en el heap
arr[0] = 10;                  // acceso por indice, igual que arreglo normal
arr[1] = 20;                  // segundo elemento
*(arr + 2) = 30;              // equivalente a arr[2] = 30 (aritmetica de punteros)
delete[] arr;                 // delete con [] para arreglos — NO usar delete sin []
arr = nullptr;                // limpiar el puntero

Reglas de oro:

  • Todo new necesita exactamente un delete
  • Todo new[] necesita exactamente un delete[]
  • Nunca uses delete para lo que asignaste con new[] (comportamiento indefinido)
  • Nunca hagas delete dos veces al mismo puntero (double free = crash)
  • Nunca uses un puntero despues de hacerle delete (dangling pointer)

2.4 Aritmetica de punteros

int arr[4] = {10, 20, 30, 40};   // arreglo de 4 enteros
int* ptr = arr;                    // nombre del arreglo = puntero al primer elemento
cout << *ptr;                      // imprime 10 — primer elemento
cout << *(ptr + 1);                // imprime 20 — avanza sizeof(int) = 4 bytes
cout << *(ptr + 2);                // imprime 30 — avanza 8 bytes desde el inicio
cout << ptr[3];                    // imprime 40 — ptr[i] es equivalente a *(ptr + i)
ptr++;                             // ptr ahora apunta al segundo elemento (20)
cout << *ptr;                      // imprime 20

Clave: ptr + 1 no avanza 1 byte, avanza sizeof(tipo) bytes. Para int*, avanza 4 bytes. El compilador hace la conversion automaticamente.

Equivalencia arreglo-puntero: arr[i] es exactamente lo mismo que *(arr + i). El nombre de un arreglo decae (decay) a puntero al primer elemento cuando se pasa a una funcion.

2.5 Punteros en estructuras de datos

// Nodo de lista enlazada simple
struct Node {
    int data;                      // el dato almacenado
    Node* next;                    // puntero al siguiente nodo (autoreferencia)
};

// Crear nodos y enlazarlos
Node* a = new Node();              // crear nodo a en el heap
a->data = 10;                      // asignar dato (-> es shortcut de (*a).data)
a->next = nullptr;                 // por ahora no hay siguiente

Node* b = new Node();              // crear nodo b en el heap
b->data = 20;                      // asignar dato
b->next = nullptr;                 // ultimo nodo

a->next = b;                       // enlazar: a apunta a b
cout << a->next->data;             // imprime 20 — seguir el puntero al siguiente nodo

El operador -> : ptr->campo es equivalente a (*ptr).campo. Desreferencia el puntero y accede al campo en un solo paso.

Doble puntero (para modificar head):

// Funcion que inserta al inicio de la lista
void insertFront(Node** head, int val) {   // head es puntero a puntero
    Node* nuevo = new Node();               // crear nuevo nodo en el heap
    nuevo->data = val;                      // asignar el valor
    nuevo->next = *head;                    // nuevo apunta al antiguo primero
    *head = nuevo;                          // head ahora apunta al nuevo nodo
}

// Uso:
Node* lista = nullptr;                      // lista vacia
insertFront(&lista, 5);                     // pasar la DIRECCION de lista
insertFront(&lista, 10);                    // ahora lista: 10 -> 5 -> nullptr

Se necesita Node** porque queremos modificar el puntero head en si, no solo el nodo al que apunta. Si usaramos Node*, la modificacion seria local (copia).

2.6 Paso por valor vs por referencia vs por puntero

void porValor(int x) {            // copia de x — el original NO cambia
    x = 99;                        // modifica la copia local
}

void porReferencia(int& x) {      // referencia al original — SI cambia
    x = 99;                        // modifica directamente el original
}

void porPuntero(int* x) {         // puntero al original — SI cambia (via dereferencia)
    *x = 99;                       // modifica el valor en la direccion que x contiene
}

int main() {
    int a = 1, b = 1, c = 1;      // tres variables con valor 1
    porValor(a);                   // a sigue siendo 1 (se paso una copia)
    porReferencia(b);              // b ahora es 99 (se paso una referencia)
    porPuntero(&c);                // c ahora es 99 (se paso su direccion)
    return 0;                      // a=1, b=99, c=99
}
MetodoSintaxis llamadaPuede ser nullPuede reasignarseModifica original
Valorf(x)N/AN/ANo
Referenciaf(x)NoNoSi
Punterof(&x)SiSiSi (con *)

Cuando usar cada uno:

  • Valor: tipos pequenos (int, char) que no necesitas modificar
  • Referencia: cuando quieres modificar el original, o para evitar copias costosas (const string&)
  • Puntero: cuando necesitas nullable, reasignable, o trabajas con memoria dinamica

2.7 Trampas de punteros (errores fatales en examen)

1. Dangling pointer — usar puntero despues de delete:

int* p = new int(42);          // p apunta a memoria valida
delete p;                      // liberar memoria
cout << *p;                    // ERROR: comportamiento indefinido — memoria ya liberada

2. Memory leak — olvidar hacer delete:

void leak() {
    int* p = new int(42);      // asignar memoria en heap
}                              // p se destruye (era local) pero la memoria sigue ocupada — leak

3. Double free — hacer delete dos veces:

int* p = new int(42);          // asignar memoria
delete p;                      // liberar — ok
delete p;                      // ERROR: double free — crash o corrupcion

4. Wild pointer — puntero sin inicializar:

int* p;                        // p contiene basura — apunta a memoria aleatoria
*p = 42;                       // ERROR: escribir en direccion desconocida — crash

5. Null dereference — desreferenciar nullptr:

int* p = nullptr;              // p no apunta a nada
*p = 42;                       // ERROR: segmentation fault — crash garantizado

6. Shallow vs deep copy — copiar puntero copia la direccion, no los datos:

int* a = new int(42);          // a apunta a 42
int* b = a;                    // b apunta a la MISMA direccion que a — no es una copia del dato
*b = 99;                       // cambia el valor — ahora *a tambien es 99
delete a;                      // libera la memoria
// b ahora es dangling — apunta a memoria liberada

7. Retornar puntero a variable local — dangling instantaneo:

int* mala() {
    int x = 42;                // x vive en el stack de esta funcion
    return &x;                 // retornar direccion de variable local
}                              // x se destruye aqui — el puntero retornado es dangling

8. delete vs delete[] — usar el incorrecto:

int* arr = new int[10];        // arreglo de 10 enteros en el heap
delete arr;                    // ERROR: deberia ser delete[] arr — comportamiento indefinido

9. Array decay — pasar arreglo a funcion pierde el tamano:

void f(int* arr) {             // arr es solo un puntero — no sabe el tamano
    cout << sizeof(arr);       // imprime 8 (tamano del puntero, no del arreglo)
}
int x[100];                    // arreglo de 100 enteros
f(x);                          // se pierde la informacion de tamano al pasar

10. Off-by-one con punteros:

int arr[5] = {1,2,3,4,5};     // indices validos: 0 a 4
int* p = arr + 5;              // apunta UNA posicion despues del ultimo — no desreferenciar
cout << *p;                    // ERROR: fuera de limites — comportamiento indefinido

2.8 Smart pointers (mencion breve)

En C++ moderno se prefiere usar smart pointers en lugar de new/delete manuales:

#include <memory>                              // necesario para smart pointers

unique_ptr<int> p1 = make_unique<int>(42);     // un solo dueno — se libera automaticamente
// No se puede copiar, solo mover

shared_ptr<int> p2 = make_shared<int>(42);     // multiples duenos — cuenta referencias
shared_ptr<int> p3 = p2;                       // p2 y p3 apuntan al mismo dato — ref count = 2
// Se libera cuando el ultimo shared_ptr se destruye (ref count llega a 0)

unique_ptr es el reemplazo directo de new/delete. shared_ptr usa conteo de referencias y se libera automaticamente cuando el ultimo dueno se destruye. Para el curso usamos punteros crudos, pero saber que existen es importante.

3. Listas / Pilas / Colas

  • Arreglo: contiguo en memoria, acceso O(1)O(1) por aritmetica de punteros. vector redimensiona x2 (amortizado O(1)O(1)).
  • Lista enlazada: nodos dispersos, insertar/eliminar en posicion conocida O(1)O(1), sin acceso por indice.
  • Doubly linked (con tail): O(1)O(1) insertar/eliminar en ambos extremos. Singly sin tail: eliminar al final O(n)O(n).
  • Pila (LIFO): push/pop/top O(1)O(1). Usos: parentesis balanceados, call stack, DFS, undo.
  • Cola (FIFO): enqueue/dequeue/front O(1)O(1). Usos: BFS, scheduling. Cola circular: indices con % capacity.

4. Ordenamiento

AlgoritmoMejorPromedioPeorMemoriaEstable
BubbleO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)Si
InsertionO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)Si
SelectionO(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)No
MergeO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)Si
QuickO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n2)O(n^2)O(logn)O(\log n)No
HeapO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(1)O(1)No
  • Estable = mantiene orden relativo de elementos con misma clave
  • Bubble: comparar pares adyacentes, burbujear el mayor al final. Early termination si no hubo swaps.
  • Insertion: mantener parte ordenada, insertar siguiente en su lugar. Bueno para casi-ordenados.
  • Selection: encontrar minimo, ponerlo al inicio. Siempre O(n2)O(n^2), minimo de swaps.
  • Merge: dividir, ordenar mitades, merge. Siempre O(nlogn)O(n \log n) pero usa O(n)O(n) extra.
  • Quick: pivote, particionar menores/mayores, recursar. Peor caso con mal pivote (ya ordenado).
  • Heap: buildHeap + extractMax repetido. In-place, siempre O(nlogn)O(n \log n).

Estabilidad — regla rapida:

  • Estables: Bubble, Insertion, Merge (los que nunca hacen swaps de largo alcance)
  • Inestables: Selection (swap con posicion lejana), Quick (partition cruza iguales), Heap (percolate salta niveles)

4.1 Traza: Merge Sort

Arreglo: [38, 27, 43, 3, 9, 82, 10]

Division (top-down):

38, 27, 43, 3, 9, 82, 1038, 27, 433, 9, 82, 1038, 27433, 982, 103827398210

Merge (bottom-up):

[38]+[27] → [27, 38] | [3]+[9] → [3, 9] | [82]+[10] → [10, 82]

[27, 38]+[43] → [27, 38, 43] | [3, 9]+[10, 82] → [3, 9, 10, 82]

[27, 38, 43] + [3, 9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]

El merge de dos listas ordenadas usa dos punteros: comparar frentes, copiar el menor, avanzar ese puntero. O(n)O(n) por nivel, logn\log n niveles = O(nlogn)O(n \log n) total.

4.2 Traza: Quick Sort (un paso de particion)

Arreglo: [35, 10, 65, 20, 45, 15, 50] — Pivote = 35 (primer elemento)

Paso a paso (Lomuto, pivote = arr[0] = 35): i = posicion del ultimo menor encontrado

[35, 10, 65, 20, 45, 15, 50] j=1: 10<35 → swap arr[1],arr[1] → i=1 [35, 10, 65, 20, 45, 15, 50] j=2: 65≥35 → nada [35, 10, 20, 65, 45, 15, 50] j=3: 20<35 → swap arr[2],arr[3] → i=2 [35, 10, 20, 65, 45, 15, 50] j=4: 45≥35 → nada [35, 10, 20, 15, 45, 65, 50] j=5: 15<35 → swap arr[3],arr[5] → i=3

Poner pivote en su lugar: swap arr[0] con arr[i=3] [15, 10, 20, 35, 45, 65, 50] ↑ pivote en posicion final

Resultado: [15, 10, 20] | 35 | [45, 65, 50] todos < 35 todos ≥ 35

Despues se recursa en cada mitad. El pivote ya esta en su posicion definitiva.

4.3 Merge Sort — Codigo C++

// Merge: combinar dos mitades ordenadas — O(n) donde n = r - l + 1
void merge(vector<int>& arr, int l, int m, int r) {
    vector<int> L(arr.begin()+l, arr.begin()+m+1);   // copiar mitad izquierda [l..m]
    vector<int> R(arr.begin()+m+1, arr.begin()+r+1);  // copiar mitad derecha [m+1..r]
    int i = 0, j = 0, k = l;                          // i=indice L, j=indice R, k=posicion en arr original
    while (i < L.size() && j < R.size())               // mientras ambas mitades tengan elementos
        arr[k++] = (L[i] <= R[j]) ? L[i++] : R[j++]; // elegir el menor de ambos frentes (<=: estable)
    while (i < L.size()) arr[k++] = L[i++];           // copiar sobrantes de L (R ya se agoto)
    while (j < R.size()) arr[k++] = R[j++];           // copiar sobrantes de R (L ya se agoto)
}
// Merge Sort — O(n log n) siempre, usa O(n) memoria extra
void mergeSort(vector<int>& arr, int l, int r) {
    if (l >= r) return;                // caso base: subarreglo de 0 o 1 elemento ya esta ordenado
    int m = l + (r - l) / 2;          // punto medio (evita overflow vs (l+r)/2)
    mergeSort(arr, l, m);             // ordenar mitad izquierda recursivamente
    mergeSort(arr, m + 1, r);         // ordenar mitad derecha recursivamente
    merge(arr, l, m, r);             // combinar ambas mitades ordenadas en una sola
}

5. Recursion y Teorema Maestro

  • Caso base (sin esto = stack overflow) + caso recursivo (entrada mas pequena)
  • Fibonacci naive: O(2n)O(2^n) por trabajo repetido. Con memoizacion: O(n)O(n).

Teorema Maestro: T(n)=aT(n/b)+O(nd)T(n) = aT(n/b) + O(n^d)

  • d>logbaO(nd)d > \log_b a \Rightarrow O(n^d)
  • d=logbaO(ndlogn)d = \log_b a \Rightarrow O(n^d \log n)
  • d<logbaO(nlogba)d < \log_b a \Rightarrow O(n^{\log_b a})

Ejemplo: Merge sort T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)a=2,b=2,d=1a=2, b=2, d=1log22=1=d\log_2 2 = 1 = dO(nlogn)O(n \log n)

Ejemplo: Busqueda binaria T(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)a=1,b=2,d=0a=1, b=2, d=0log21=0=d\log_2 1 = 0 = dO(logn)O(\log n)

6. Matrices

  • Multiplicacion: cij=kaikbkjc_{ij} = \sum_{k} a_{ik} \cdot b_{kj} — Complejidad O(n3)O(n^3) para matrices n×nn \times n
  • Regla de dimensiones: Am×n×Bn×p=Cm×pA_{m \times n} \times B_{n \times p} = C_{m \times p} — columnas de A deben = filas de B
  • Si no coinciden, la multiplicacion no esta definida
  • No conmutativa: ABBAAB \neq BA
  • Transposicion: AT[j][i]=A[i][j]A^T[j][i] = A[i][j] — filas se vuelven columnas
  • (AB)T=BTAT(AB)^T = B^T A^T — el orden se invierte
  • Row-major (C++): offset =i×n+j= i \times n + j — recorrer por filas = cache-friendly
  • Diagonal principal: i=ji = j — Anti-diagonal: i+j=n1i + j = n - 1

Regla rapida para dimensiones: al multiplicar varias matrices, las dimensiones internas se “cancelan”:

(2×3)(3×5)(5×1)=(2×1)(2 \times 3)(3 \times 5)(5 \times 1) = (2 \times 1)

7. Tablas Hash

  • Funcion hash: h(k)=kmodmh(k) = k \mod m — transforma clave en indice [0,m1][0, m-1]
  • Factor de carga: λ=n/m\lambda = n/m — rehash cuando λ>0.7\lambda > 0.7 (duplicar mm, reinsertar todo)

Resolucion de colisiones:

MetodoFormula de sondeoProblema
ChainingLista en cada slotMemoria extra, cache unfriendly
Linear probing(h(k)+i)modm(h(k) + i) \mod mPrimary clustering
Quadratic probing(h(k)+i2)modm(h(k) + i^2) \mod mPuede no encontrar slot; garantizado si mm primo y λ<0.5\lambda < 0.5

Complejidad promedio: Chaining O(1+λ)O(1 + \lambda) — Linear probing O(1/(1λ))O(1/(1 - \lambda))

7.1 Traza: Hash con Linear Probing

Tabla de tamano m=7m = 7, h(k)=kmod7h(k) = k \mod 7. Insertar: 22, 9, 30, 16, 37

Insert 22: h(22)=22%7=1 → slot 1 libre → tabla[1]=22 Insert 9: h(9) = 9%7=2 → slot 2 libre → tabla[2]=9 Insert 30: h(30)=30%7=2 → slot 2 ocupado (9) slot 3 libre → tabla[3]=30 Insert 16: h(16)=16%7=2 → slot 2 ocupado (9) slot 3 ocupado (30) slot 4 libre → tabla[4]=16 Insert 37: h(37)=37%7=2 → slots 2,3,4 ocupados slot 5 libre → tabla[5]=37

Tabla final: [0] [1] [2] [3] [4] [5] [6]

  • 22 9 30 16 37 -

Buscar 16: h(16)=2 → check 2(9≠16), 3(30≠16), 4(16=16) ✓ Buscar 11: h(11)=4 → check 4(16≠11), 5(37≠11), 6(vacio) → no existe

Notar el clustering: 22 esta solo, pero 9, 30, 16, 37 forman un bloque contiguo. Esto degrada busquedas.

8. Heaps y Colas de Prioridad

  • Max-heap: padre \geq hijos (maximo en raiz). Min-heap: padre \leq hijos.
  • Arbol binario completo almacenado en arreglo:
FormulaValor (indice desde 0)
Padre de ii(i1)/2\lfloor (i-1)/2 \rfloor
Hijo izquierdo2i+12i + 1
Hijo derecho2i+22i + 2
OperacionComplejidad
Insert (percolate up)O(logn)O(\log n)
ExtractMax/Min (percolate down)O(logn)O(\log n)
PeekO(1)O(1)
buildHeap (sift-down desde mitad)O(n)O(n)

buildHeap es O(n)O(n), no O(nlogn)O(n \log n). Intuicion: la mayoria de nodos estan abajo y hacen poco trabajo. n/2n/2 hojas hacen 0 swaps, n/4n/4 hacen 1, n/8n/8 hacen 2… La suma k=0lognn2k+1k\sum_{k=0}^{\log n} \frac{n}{2^{k+1}} \cdot k converge a O(n)O(n) por serie geometrica.

8.1 Traza: Heap Insert + ExtractMax

Insert en max-heap — insertar 50, 30, 40, 10, 20, 45:

Insert 50: [50]

50

Insert 30: [50,30] — 30<50 izq

5030

Insert 40: [50,30,40] — 40<50 izq… 40>30 der

503040

Insert 10: [50,30,40,10] — 10<50 izq, 10<30 izq

50304010

Insert 20: [50,30,40,10,20] — 20<50 izq, 20<30 izq… 20>10 der

5030401020

Insert 45: h[5]=45, padre=h[2]=40. 45>40 → swap. h[2]=45, padre=h[0]=50. 45<50 → stop.

Resultado: [50,30,45,10,20,40]

503045102040

ExtractMax: sacar raiz (50), mover ultimo (40) a raiz, percolate down:

Antes: [50,30,45,10,20,40] → mover ultimo (40) a raiz → [40,30,45,10,20]

4030451020swap 454530401020

arr = [45,30,40,10,20] — Maximo extraido: 50

8.2 Heap — Codigo C++

// Percolate down (max-heap) — O(log n)
// Hunde un nodo hasta que cumpla propiedad de heap (padre >= hijos)
void percolateDown(vector<int>& heap, int i, int n) {
    while (true) {
        int largest = i, l = 2*i+1, r = 2*i+2;       // hijos izq y der en arreglo
        if (l < n && heap[l] > heap[largest]) largest = l;  // hijo izq es mayor que padre?
        if (r < n && heap[r] > heap[largest]) largest = r;  // hijo der es aun mayor?
        if (largest == i) break;                       // padre ya es el mayor: propiedad cumplida
        swap(heap[i], heap[largest]);                  // intercambiar padre con el hijo mayor
        i = largest;                                   // continuar hundiendo desde la nueva posicion
    }
}

// Percolate up (max-heap) — O(log n)
// Sube un nodo hasta que su padre sea mayor (usado al insertar)
void percolateUp(vector<int>& heap, int i) {
    while (i > 0) {                                    // mientras no sea la raiz
        int parent = (i - 1) / 2;                     // indice del padre en arreglo
        if (heap[i] <= heap[parent]) break;            // padre ya es mayor: propiedad cumplida
        swap(heap[i], heap[parent]);                   // subir el nodo intercambiandolo con su padre
        i = parent;                                    // continuar subiendo desde la posicion del padre
    }
}

// Insertar en max-heap — O(log n)
void insert(vector<int>& heap, int val) {
    heap.push_back(val);                               // agregar al final (siguiente posicion libre)
    percolateUp(heap, heap.size() - 1);                // subir hasta restaurar propiedad de heap
}

// Extraer el maximo (raiz) — O(log n)
int extractMax(vector<int>& heap) {
    int maxVal = heap[0];                              // guardar el maximo (siempre en la raiz)
    heap[0] = heap.back();                             // mover ultimo elemento a la raiz
    heap.pop_back();                                   // eliminar la posicion duplicada del final
    percolateDown(heap, 0, heap.size());               // hundir la nueva raiz hasta su lugar correcto
    return maxVal;                                     // retornar el maximo que extrajimos
}

// buildHeap — O(n), NO O(n log n)
// Convierte arreglo arbitrario en heap desde abajo hacia arriba
void buildHeap(vector<int>& heap) {
    for (int i = heap.size()/2 - 1; i >= 0; i--)      // desde el ultimo padre hasta la raiz (hojas ya cumplen)
        percolateDown(heap, i, heap.size());           // corregir cada subarbol de abajo hacia arriba
}

9. Arboles Binarios y Recorridos

  • Raiz: nodo sin padre. Hoja: nodo sin hijos. Altura: distancia maxima raiz-hoja.
  • Preorden (NLR): nodo, izq, der — Inorden (LNR): izq, nodo, der — Postorden (LRN): izq, der, nodo
  • Todos los recorridos: O(n)O(n)
  • Reconstruir arbol: preorden+inorden = unico. Postorden+inorden = unico. Preorden+postorden = NO (ambiguo).

9.1 Recorridos — Codigo C++

// Estructura base de un nodo de arbol binario
struct TreeNode {
    int val;                        // dato almacenado en el nodo
    TreeNode *left, *right;         // punteros a hijos izquierdo y derecho (nullptr si no tiene)
};

// Todos los recorridos son O(n) — visitan cada nodo exactamente una vez

// Preorden (NLR): procesar nodo ANTES de sus hijos — util para copiar/serializar arbol
void preorder(TreeNode* n)  { if (!n) return; cout << n->val << " "; preorder(n->left); preorder(n->right); }
// Inorden (LNR): procesar nodo ENTRE hijos — en BST produce datos en orden ascendente
void inorder(TreeNode* n)   { if (!n) return; inorder(n->left); cout << n->val << " "; inorder(n->right); }
// Postorden (LRN): procesar nodo DESPUES de hijos — util para liberar memoria / evaluar expresiones
void postorder(TreeNode* n) { if (!n) return; postorder(n->left); postorder(n->right); cout << n->val << " "; }

Ejemplo rapido:

83101614
  • Preorden (NLR): 8, 3, 1, 6, 10, 14
  • Inorden (LNR): 1, 3, 6, 8, 10, 14 — ordenado! (es BST)
  • Postorden (LRN): 1, 6, 3, 14, 10, 8

Truco para examen: inorden de BST = datos en orden ascendente. Si tu recorrido inorden no sale ordenado, el arbol esta mal construido.

10. BST (Arbol Binario de Busqueda)

  • Propiedad: izquierdo < nodo < derecho (para todo nodo)
  • Inorden de un BST = datos ordenados ascendente
  • Complejidad: O(h)O(h) donde hh = altura. Balanceado: h=lognh = \log n. Degenerado: h=nh = n.

Eliminar nodo — 3 casos:

  1. Hoja: simplemente borrar
  2. Un hijo: reemplazar nodo con su unico hijo (bypass)
  3. Dos hijos: reemplazar con sucesor inorden (menor del subarbol derecho), luego eliminar el sucesor

10.1 Traza: Construir BST

Insertar en orden: 40, 20, 60, 10, 50

Insert 40:

40

Insert 20: 20<40 → izquierda

4020

Insert 60: 60>40 → derecha

402060

Insert 10: 10<40 → izq, 10<20 → izq

40206010

Insert 50: 50>40 → der, 50<60 → izq

4020601050

Inorden: 10, 20, 40, 50, 60 (ordenado)

Ojo: el orden de insercion determina la forma. Insertar [10,20,40,50,60] da un arbol degenerado (lista enlazada), con busqueda O(n)O(n).

10.2 BST — Codigo C++

// Insertar en BST — O(log n) promedio, O(n) peor caso (arbol degenerado)
BSTNode* insert(BSTNode* root, int key) {
    if (!root) return new BSTNode(key);                    // arbol vacio: crear nodo hoja aqui
    if (key < root->key) root->left = insert(root->left, key);   // menor: buscar lugar en subarbol izquierdo
    else if (key > root->key) root->right = insert(root->right, key); // mayor: buscar en subarbol derecho
    return root;                                           // duplicados se ignoran (no se insertan)
}

// Eliminar de BST — O(h), maneja los 3 casos clasicos
BSTNode* remove(BSTNode* root, int key) {
    if (!root) return nullptr;                             // clave no encontrada: nada que eliminar
    if (key < root->key) root->left = remove(root->left, key);   // buscar en izquierda
    else if (key > root->key) root->right = remove(root->right, key); // buscar en derecha
    else {                                                 // encontrado: eliminar este nodo
        if (!root->left) { auto t = root->right; delete root; return t; }  // caso 1/2: sin hijo izq, reemplazar con derecho
        if (!root->right) { auto t = root->left; delete root; return t; }  // caso 2: sin hijo der, reemplazar con izquierdo
        BSTNode* succ = root->right;          // caso 3 (dos hijos): buscar sucesor inorden
        while (succ->left) succ = succ->left; // sucesor = menor del subarbol derecho (ir a la izq hasta el fondo)
        root->key = succ->key;                // copiar valor del sucesor al nodo actual
        root->right = remove(root->right, succ->key); // eliminar el sucesor de su posicion original
    }
    return root;                                           // retornar raiz (posiblemente modificada)
}

// Busqueda en BST — O(h): h=log n balanceado, h=n degenerado
BSTNode* search(BSTNode* root, int key) {
    if (!root || root->key == key) return root;            // no existe o encontrado
    if (key < root->key) return search(root->left, key);  // menor: solo puede estar a la izquierda
    return search(root->right, key);                       // mayor: solo puede estar a la derecha
}

11. Arboles AVL

  • BST auto-balanceado: BF(nodo)=h(izq)h(der)BF(\text{nodo}) = h(\text{izq}) - h(\text{der}) debe ser 1,0,+1-1, 0, +1 para todo nodo
  • Garantiza altura O(logn)O(\log n) — nunca degenera

Tabla de rotaciones:

BF nodoBF hijoCasoRotacion
+2+20\geq 0LLSimple derecha
+2+2<0< 0LRIzq en hijo, luego der en nodo
2-20\leq 0RRSimple izquierda
2-2>0> 0RLDer en hijo, luego izq en nodo

Truco: misma direccion (LL/RR) = simple. Direcciones opuestas (LR/RL) = doble.

11.1 Traza: AVL con rotacion

Insertar 30, 20, 10 (causa rotacion LL):

Insert 30: BF=0

30

Insert 20: BF(30)=+1

3020

Insert 10: BF(30)=+2 — DESBALANCEADO. Caso LL: BF(30)=+2, BF(20)=+1. Rotacion simple derecha en 30.

30BF=+22010rot. der20BF=01030

Ejemplo LR — Insertar 30, 10, 20:

Caso LR: BF(30)=+2, BF(10)=-1. Paso 1: rot. izq en 10. Paso 2: rot. der en 30.

30+210-120rot. izq 10302010rot. der 30201030

11.2 AVL — Codigo C++

// Helpers — O(1): obtener altura y factor de balance de forma segura (nullptr = 0)
int height(AVLNode* n) { return n ? n->height : 0; }          // altura del nodo (0 si nulo)
int bf(AVLNode* n) { return n ? height(n->left) - height(n->right) : 0; } // BF = h(izq) - h(der)

// Rotacion simple derecha (caso LL) — O(1)
// z esta desbalanceado a la izquierda, su hijo izq (y) sube a reemplazarlo
AVLNode* rotateRight(AVLNode* z) {
    AVLNode* y = z->left;                 // y = hijo izquierdo que va a subir
    z->left = y->right; y->right = z;    // el hijo derecho de y pasa a ser hijo izquierdo de z
    z->height = 1 + max(height(z->left), height(z->right));  // recalcular z primero (ahora es hijo)
    y->height = 1 + max(height(y->left), height(y->right));  // luego y (ahora es padre)
    return y;                             // y es la nueva raiz del subarbol
}
// Rotacion simple izquierda (caso RR) — O(1), espejo de rotateRight
AVLNode* rotateLeft(AVLNode* z) {
    AVLNode* y = z->right;               // y = hijo derecho que va a subir
    z->right = y->left; y->left = z;     // el hijo izquierdo de y pasa a ser hijo derecho de z
    z->height = 1 + max(height(z->left), height(z->right));  // recalcular alturas desde abajo
    y->height = 1 + max(height(y->left), height(y->right));
    return y;                             // y es la nueva raiz del subarbol
}
// Balancear nodo AVL — O(1): detecta tipo de desbalance y aplica rotaciones
AVLNode* balance(AVLNode* node) {
    node->height = 1 + max(height(node->left), height(node->right)); // actualizar altura
    int b = bf(node);                     // calcular factor de balance
    if (b > 1) {                          // desbalanceado a la izquierda (BF = +2)
        if (bf(node->left) < 0) node->left = rotateLeft(node->left); // LR: primero rotar hijo a la izq
        return rotateRight(node);         // LL (o segunda parte de LR): rotar nodo a la derecha
    }
    if (b < -1) {                         // desbalanceado a la derecha (BF = -2)
        if (bf(node->right) > 0) node->right = rotateRight(node->right); // RL: primero rotar hijo a la der
        return rotateLeft(node);          // RR (o segunda parte de RL): rotar nodo a la izquierda
    }
    return node;                          // BF en {-1, 0, +1}: nada que corregir
}
// Insertar en AVL — O(log n) garantizado (el balance evita degeneracion)
AVLNode* insert(AVLNode* node, int key) {
    if (!node) return new AVLNode(key);                       // caso base: crear hoja nueva
    if (key < node->key) node->left = insert(node->left, key);    // menor: insertar a la izquierda
    else if (key > node->key) node->right = insert(node->right, key); // mayor: insertar a la derecha
    return balance(node);                 // re-balancear en el camino de regreso (post-recursion)
}

Rotaciones visuales (referencia rapida):

LL — Rotacion simple derecha:

zyxT4T3T1T2yxzT1T2T3T4

RR — Rotacion simple izquierda:

zyxT1T2T3T4yzxT1T2T3T4

LR — Doble rotacion (izq en y, luego der en z):

zyxT1T4T2T3izq en yzxyT1T2T3T4der en zxyzT1T2T3T4

RL — Doble rotacion (der en y, luego izq en z):

zyxT1T4T2T3der en yzxyT1T2T3T4izq en zxzyT1T2T3T4

12. Arboles Red-Black

Las 5 propiedades (esto es todo lo que piden):

  1. Cada nodo es rojo o negro
  2. La raiz es negra
  3. Todas las hojas NIL son negras
  4. Nodo rojo implica ambos hijos negros (no hay dos rojos seguidos)
  5. Todo camino raiz-hoja tiene el mismo numero de nodos negros (black-height) | | AVL | Red-Black | |---|---|---| | Altura max | 1.44logn\sim 1.44 \log n | 2logn\sim 2 \log n | | Busqueda | Mas rapida | Un poco mas lenta | | Insert/Delete | Mas rotaciones | Menos rotaciones | | Uso ideal | Muchas lecturas | Muchas escrituras | | Ejemplo | Indices de BD | std::map, std::set en C++ |

13. Backtracking / MinMax / Alpha-Beta

  • Backtracking: hacer eleccion, explorar, si falla deshacer y probar otra opcion.
  • MinMax: juegos de 2 jugadores. MAX maximiza, MIN minimiza. Evaluar hojas, propagar hacia arriba.
  • Alpha-Beta: poda de MinMax. α\alpha = mejor para MAX, β\beta = mejor para MIN.
    • Nodo MAX: si valor β\geq \beta — podar (MIN no lo permitiria)
    • Nodo MIN: si valor α\leq \alpha — podar (MAX no lo elegiria)
  • Complejidad: MinMax O(bd)O(b^d) — Alpha-Beta optimo O(bd/2)O(b^{d/2}) (duplica profundidad alcanzable)

Patron de backtracking (N-Queens, Sudoku, etc.):

// Patron general de backtracking — O(b^d) peor caso (b=opciones, d=profundidad)
// Explora todas las combinaciones posibles podando ramas invalidas
void backtrack(State& state, int step) {
    if (isSolution(state)) { saveSolution(state); return; }  // caso base: encontramos una solucion valida
    for (auto& choice : getChoices(state, step)) {           // probar cada opcion disponible en este paso
        if (!isValid(state, choice)) continue;               // poda: descartar opciones que violan restricciones
        applyChoice(state, choice);     // HACER: aplicar la eleccion al estado actual
        backtrack(state, step + 1);     // EXPLORAR: recursar al siguiente paso con la eleccion hecha
        undoChoice(state, choice);      // DESHACER: revertir para probar la siguiente opcion (backtrack)
    }
}

MinMax — pseudocodigo clave:

  • minimax(nodo, profundidad, esMax): si hoja o prof=0 retornar evaluacion; si esMax retornar max de hijos; si esMin retornar min de hijos.

Alpha-Beta — la poda ocurre cuando:

  • Nodo MAX: α=max(α,valor)\alpha = \max(\alpha, valor). Si αβ\alpha \geq \beta → cortar (beta cutoff)
  • Nodo MIN: β=min(β,valor)\beta = \min(\beta, valor). Si αβ\alpha \geq \beta → cortar (alpha cutoff)

14. Heap Sort — Detalle

Algoritmo en 2 fases:

  1. buildHeap sobre el arreglo — O(n)O(n)
  2. Repetir n1n-1 veces: swap raiz (max) con ultimo, reducir tamano, percolateDown — cada una O(logn)O(\log n)

Total: O(n)+O(nlogn)=O(nlogn)O(n) + O(n \log n) = O(n \log n). In-place (solo O(1)O(1) memoria extra).

// Heap Sort — O(n log n) siempre, in-place (O(1) memoria extra)
// Paso 1: construir max-heap, Paso 2: extraer maximos uno a uno
void heapSort(vector<int>& arr) {
    int n = arr.size();                        // tamano del arreglo
    // Fase 1: buildHeap — O(n), convierte arreglo en max-heap
    for (int i = n/2 - 1; i >= 0; i--)        // desde ultimo nodo interno hasta raiz (hojas ya son heaps)
        percolateDown(arr, i, n);              // corregir cada subarbol de abajo hacia arriba
    // Fase 2: extraer maximos — O(n log n), n extracciones de O(log n)
    for (int i = n - 1; i > 0; i--) {         // desde el final hacia el inicio
        swap(arr[0], arr[i]);                  // mover maximo (raiz) a su posicion final ordenada
        percolateDown(arr, 0, i);              // restaurar propiedad de heap en el subarreglo reducido
    }
}

15. Grafos

Definiciones fundamentales

Grafo G=(V,E)G = (V, E): conjunto de vertices VV y aristas EE. Cada arista conecta dos vertices.

  • Dirigido (digrafo): aristas tienen direccion — (u,v)(v,u)(u, v) \neq (v, u)
  • No dirigido: aristas sin direccion — {u,v}={v,u}\{u, v\} = \{v, u\}
  • Ponderado (weighted): cada arista tiene un peso/costo asociado
  • No ponderado: todas las aristas tienen el mismo “costo” (1)

Grado de un vertice:

  • No dirigido: deg(v)\deg(v) = numero de aristas incidentes
  • Dirigido: deg+(v)\deg^+(v) = out-degree (aristas que salen), deg(v)\deg^-(v) = in-degree (aristas que llegan)
  • Suma de grados = 2E2|E| (cada arista contribuye 2 al total)

Terminologia clave:

  • Camino (path): secuencia de vertices conectados por aristas, sin repetir vertices
  • Ciclo: camino que empieza y termina en el mismo vertice
  • Conexo (connected): existe camino entre todo par de vertices (no dirigido)
  • Fuertemente conexo (strongly connected): existe camino dirigido entre todo par (dirigido)
  • Grafo completo KnK_n: arista entre todo par de vertices — E=n(n1)2|E| = \frac{n(n-1)}{2}
  • DAG: Directed Acyclic Graph — digrafo sin ciclos. Permite orden topologico.

Grafo no dirigido — ejemplo con 4 vertices:

ABCD

Aristas: A-B, A-C, A-D, B-D, C-D. Grados: A=3, B=2, C=2, D=3. Suma = 10 = 2×5 aristas.

Grafo dirigido — ejemplo con 4 vertices:

ABCD

Aristas: A→B, A→C, A→D, B→D, C→D. Out-degrees: A=3, B=1, C=1, D=0. In-degrees: A=0, B=1, C=1, D=3. Es un DAG (no tiene ciclos).

Representacion en memoria

Dos formas principales:

RepresentacionEspacioVerificar aristaVecinos de vMejor para
Matriz de adyacenciaO(V2)O(V^2)O(1)O(1)O(V)O(V)Grafos densos (EV2E \approx V^2)
Lista de adyacenciaO(V+E)O(V + E)O(deg(v))O(\deg(v))O(deg(v))O(\deg(v))Grafos dispersos (EV2E \ll V^2)

Regla practica: en la mayoria de problemas reales los grafos son dispersos → usar lista de adyacencia.

Ejemplo — mismo grafo con vertices A=0, B=1, C=2, D=3:

0123

Matriz de adyacencia (simetrica porque es no dirigido):

0123
00111
11001
21001
31110

Lista de adyacencia:

  • 0 → [1, 2, 3]
  • 1 → [0, 3]
  • 2 → [0, 3]
  • 3 → [0, 1, 2]
// Representacion con lista de adyacencia — O(V + E) espacio
// Cada vertice tiene un vector con sus vecinos
#include <vector>                                   // para vector
using namespace std;                                // evitar std:: en todo

// Grafo no dirigido con lista de adyacencia
struct Graph {
    int V;                                          // numero de vertices
    vector<vector<int>> adj;                        // adj[u] = lista de vecinos de u

    Graph(int V) : V(V), adj(V) {}                  // constructor: crear V listas vacias

    void addEdge(int u, int v) {                    // agregar arista no dirigida u-v
        adj[u].push_back(v);                        // v es vecino de u
        adj[v].push_back(u);                        // u es vecino de v (bidireccional)
    }
};

// Uso:
// Graph g(4);                                      // grafo con 4 vertices (0,1,2,3)
// g.addEdge(0, 1);                                 // arista 0-1
// g.addEdge(0, 2);                                 // arista 0-2
// g.addEdge(0, 3);                                 // arista 0-3
// g.addEdge(1, 3);                                 // arista 1-3
// g.addEdge(2, 3);                                 // arista 2-3

Para grafo dirigido: solo adj[u].push_back(v) (sin la linea inversa).

Para grafo ponderado: usar vector<vector<pair<int,int>>> donde cada par es (vecino, peso).

BFS (Breadth-First Search) — Busqueda en anchura

Idea: explorar por niveles. Primero todos los vecinos directos, luego los vecinos de los vecinos, etc. Usa una cola (queue).

Aplicaciones:

  • Camino mas corto en grafo no ponderado (cada arista cuesta 1)
  • Componentes conexas
  • Verificar si un grafo es bipartito

Complejidad: O(V+E)O(V + E) — visita cada vertice una vez y cada arista una vez.

Algoritmo:

  1. Encolar vertice inicial, marcarlo como visitado
  2. Mientras la cola no este vacia:
    • Desencolar vertice uu
    • Para cada vecino vv de uu no visitado: marcarlo, encolarlo
  3. Resultado: vertices en orden de distancia al origen
// BFS — Busqueda en anchura — O(V + E)
// Encuentra distancias minimas desde 'start' en grafo no ponderado
#include <vector>                                   // para vector
#include <queue>                                    // para queue
using namespace std;                                // evitar std::

// Retorna vector de distancias desde 'start' (-1 si no es alcanzable)
vector<int> bfs(const vector<vector<int>>& adj, int start) {
    int V = adj.size();                             // numero total de vertices
    vector<int> dist(V, -1);                        // distancias: -1 = no visitado
    queue<int> q;                                   // cola para explorar por niveles

    dist[start] = 0;                                // distancia al inicio es 0
    q.push(start);                                  // encolar vertice inicial

    while (!q.empty()) {                            // mientras haya vertices por explorar
        int u = q.front();                          // tomar el primero de la cola
        q.pop();                                    // sacarlo de la cola
        for (int v : adj[u]) {                      // para cada vecino v de u
            if (dist[v] == -1) {                    // si v no ha sido visitado
                dist[v] = dist[u] + 1;              // distancia = distancia del padre + 1
                q.push(v);                          // encolar v para explorar sus vecinos despues
            }
        }
    }
    return dist;                                    // retornar todas las distancias
}

Traza BFS desde vertice 0:

d=00d=11d=12d=13Cola: [0]Visitar 0 → encolar 1,2,3Cola: [1,2,3]Visitar 1 → 3 ya visitadoVisitar 2 → 3 ya visitadoVisitar 3 → todos visitadosOrden BFS: 0, 1, 2, 3

BFS con grafo mas interesante (5 vertices):

Grafo: 0-1, 0-2, 1-3, 2-3, 3-4. Desde vertice 0:

  • Nivel 0: {0} — distancia 0
  • Nivel 1: {1, 2} — distancia 1
  • Nivel 2: {3} — distancia 2 (vecino de 1 y 2, pero solo se visita una vez)
  • Nivel 3: {4} — distancia 3

Orden BFS: 0 → 1 → 2 → 3 → 4

DFS (Depth-First Search) — Busqueda en profundidad

Idea: ir lo mas profundo posible antes de retroceder. Usa recursion (pila implicita) o una pila explicita.

Aplicaciones:

  • Deteccion de ciclos
  • Orden topologico (en DAGs)
  • Componentes conexas / fuertemente conexas
  • Verificar si un grafo es aciclico

Complejidad: O(V+E)O(V + E) — misma que BFS.

Algoritmo:

  1. Marcar vertice actual como visitado
  2. Para cada vecino no visitado: recursar
  3. Al terminar, el vertice esta “completamente explorado”

DFS vs BFS:

  • BFS = nivel por nivel (usa cola) → camino mas corto en no ponderado
  • DFS = ir profundo primero (usa pila/recursion) → detectar ciclos, topo sort
// DFS — Busqueda en profundidad — O(V + E)
// Explora lo mas profundo posible antes de retroceder
#include <vector>                                   // para vector
using namespace std;                                // evitar std::

// DFS recursivo desde vertice u
void dfs(const vector<vector<int>>& adj,            // lista de adyacencia del grafo
         int u,                                     // vertice actual
         vector<bool>& visited,                     // registro de vertices visitados
         vector<int>& order) {                      // orden de visita (resultado)
    visited[u] = true;                              // marcar u como visitado
    order.push_back(u);                             // registrar u en el orden de visita
    for (int v : adj[u]) {                          // para cada vecino v de u
        if (!visited[v]) {                          // si v no ha sido visitado
            dfs(adj, v, visited, order);             // recursar: explorar v antes de continuar con otros vecinos
        }
    }
}

// Funcion auxiliar para iniciar DFS
vector<int> dfsTraversal(const vector<vector<int>>& adj, int start) {
    int V = adj.size();                             // numero total de vertices
    vector<bool> visited(V, false);                 // ningun vertice visitado al inicio
    vector<int> order;                              // almacenar orden de visita
    dfs(adj, start, visited, order);                // iniciar DFS desde 'start'
    return order;                                   // retornar orden de visita
}

Deteccion de ciclos con DFS (grafo dirigido):

Usar 3 colores: BLANCO (no visitado), GRIS (en proceso), NEGRO (terminado). Si encontramos una arista hacia un nodo GRIS → ciclo detectado (back edge).

// Deteccion de ciclos en grafo dirigido — O(V + E)
// Usa coloreo: 0=blanco(no visitado), 1=gris(en proceso), 2=negro(terminado)
#include <vector>                                   // para vector
using namespace std;                                // evitar std::

bool hasCycleDFS(const vector<vector<int>>& adj,    // lista de adyacencia
                 int u,                             // vertice actual
                 vector<int>& color) {              // color de cada vertice
    color[u] = 1;                                   // marcar u como GRIS (en proceso)
    for (int v : adj[u]) {                          // para cada vecino v de u
        if (color[v] == 1) return true;             // v es GRIS → back edge → CICLO
        if (color[v] == 0 &&                        // v es BLANCO (no visitado) y
            hasCycleDFS(adj, v, color)) return true; // recursar en v encuentra ciclo
    }
    color[u] = 2;                                   // marcar u como NEGRO (terminado)
    return false;                                   // no se encontro ciclo desde u
}

bool hasCycle(const vector<vector<int>>& adj) {
    int V = adj.size();                             // numero total de vertices
    vector<int> color(V, 0);                        // todos empiezan BLANCOS
    for (int i = 0; i < V; i++) {                   // intentar DFS desde cada vertice
        if (color[i] == 0 &&                        // si no ha sido visitado y
            hasCycleDFS(adj, i, color)) return true; // encontramos ciclo → retornar true
    }
    return false;                                   // ningun ciclo en todo el grafo
}

Traza DFS desde vertice 0 (mismo grafo que BFS):

#10#21#42#33dfs(0): visitar 0 dfs(1): visitar 1 dfs(3): visitar 3 dfs(2): visitar 2 retornar a 3, 1, 0Orden DFS: 0, 1, 3, 2(vs BFS: 0, 1, 2, 3)

Orden topologico (Topological Sort):

Solo para DAGs. Ordena vertices de modo que para toda arista uvu → v, uu aparece antes que vv. Usar DFS: al terminar un vertice (marcar negro), ponerlo en la pila. Al final, la pila invertida es el orden topologico.

Dijkstra — Camino mas corto en grafos ponderados

Idea: algoritmo greedy. Mantener distancias tentativas y siempre expandir el vertice con menor distancia conocida. Usa cola de prioridad (min-heap).

Limitacion: NO funciona con pesos negativos. Para pesos negativos usar Bellman-Ford.

Complejidad: O((V+E)logV)O((V + E) \log V) con binary heap (priority_queue).

Algoritmo:

  1. Inicializar: dist[start]=0dist[start] = 0, dist[v]=dist[v] = \infty para todos los demas
  2. Insertar (0,start)(0, start) en la cola de prioridad
  3. Mientras la cola no este vacia:
    • Extraer vertice uu con menor distancia
    • Para cada vecino vv de uu con peso ww:
      • Si dist[u]+w<dist[v]dist[u] + w < dist[v]: actualizar dist[v]dist[v] e insertar (dist[v],v)(dist[v], v) en la cola
  4. Al final, dist[v]dist[v] = camino mas corto desde startstart a vv

Relajacion: si encontramos un camino mas corto a vv pasando por uu, actualizamos.

// Dijkstra — Camino mas corto desde un origen — O((V+E) log V)
// No funciona con pesos negativos. Usa min-heap (priority_queue invertida)
#include <vector>                                   // para vector
#include <queue>                                    // para priority_queue
#include <climits>                                  // para INT_MAX
using namespace std;                                // evitar std::

// Retorna distancias minimas desde 'start' a todos los vertices
vector<int> dijkstra(const vector<vector<pair<int,int>>>& adj, // adj[u] = [(vecino, peso), ...]
                     int start) {
    int V = adj.size();                             // numero total de vertices
    vector<int> dist(V, INT_MAX);                   // distancias: infinito al inicio
    // min-heap: (distancia, vertice) — ordena por distancia menor primero
    priority_queue<pair<int,int>,                   // tipo de elemento: (dist, vertice)
                   vector<pair<int,int>>,            // contenedor subyacente
                   greater<pair<int,int>>> pq;       // comparador: menor primero (min-heap)

    dist[start] = 0;                                // distancia al origen es 0
    pq.push({0, start});                            // encolar (distancia=0, vertice=start)

    while (!pq.empty()) {                           // mientras haya vertices por procesar
        auto [d, u] = pq.top();                     // extraer vertice con menor distancia
        pq.pop();                                   // removerlo de la cola
        if (d > dist[u]) continue;                  // si ya encontramos algo mejor, ignorar (lazy deletion)
        for (auto [v, w] : adj[u]) {                // para cada vecino v con peso w
            if (dist[u] + w < dist[v]) {            // si encontramos camino mas corto a v
                dist[v] = dist[u] + w;              // actualizar distancia de v (relajacion)
                pq.push({dist[v], v});              // encolar nueva distancia para explorar desde v
            }
        }
    }
    return dist;                                    // retornar todas las distancias minimas
}

Traza Dijkstra — grafo ponderado:

42713d=0Ad=4Bd=2Cd=5DInicio: dist = [0, inf, inf, inf]Extraer A(d=0): B: 0+4=4, C: 0+2=2, D: 0+7=7 dist = [0, 4, 2, 7]Extraer C(d=2): D: 2+3=5 < 7 → actualizar dist = [0, 4, 2, 5]Extraer B(d=4): D: 4+1=5 = 5 → no mejoraFinal: A=0, B=4, C=2, D=5

Caminos optimos desde A: A→B (costo 4), A→C (costo 2), A→C→D (costo 5, mejor que A→D directo=7 o A→B→D=5).

Dato clave de examen: Dijkstra es greedy — una vez que un vertice sale de la cola con su distancia final, nunca se actualiza de nuevo. Por eso no funciona con pesos negativos (podria encontrar un atajo despues).

Tabla de complejidades — Algoritmos de grafos

AlgoritmoTiempoEspacioUsaPara que
BFSO(V+E)O(V + E)O(V)O(V)ColaCamino mas corto (no ponderado), componentes conexas
DFSO(V+E)O(V + E)O(V)O(V)Pila/recursionCiclos, topo sort, componentes, explorar todo
Dijkstra (binary heap)O((V+E)logV)O((V+E) \log V)O(V)O(V)Min-heapCamino mas corto (pesos 0\geq 0)
Bellman-FordO(VE)O(V \cdot E)O(V)O(V)Relajacion iterativaCamino mas corto (permite pesos negativos)
Floyd-WarshallO(V3)O(V^3)O(V2)O(V^2)DP sobre matrizTodos los pares de caminos mas cortos
Kruskal (MST)O(ElogE)O(E \log E)O(V)O(V)Union-FindArbol de expansion minima
Prim (MST)O((V+E)logV)O((V+E) \log V)O(V)O(V)Min-heapArbol de expansion minima
Topo Sort (DFS)O(V+E)O(V + E)O(V)O(V)DFS + pilaOrdenar DAG

Cuando usar cual:

  • Grafo no ponderado, camino mas corto → BFS
  • Grafo ponderado (pesos 0\geq 0), camino mas corto → Dijkstra
  • Detectar ciclos en dirigido → DFS con colores
  • Ordenar dependencias (DAG) → Topological Sort
  • ¿El grafo es conexo? → BFS o DFS desde cualquier nodo, ver si visita todo

16. Tabla Maestra de Ordenamiento — Estabilidad

AlgoritmoEstable?Por que
Bubble sortSiSolo swaps adyacentes, nunca cruza iguales
Insertion sortSiInserta en posicion correcta sin saltar iguales
Selection sortNoSwap de largo alcance puede desordenar iguales. Ej: [2a, 2b, 1] → swap 2a con 1 → [1, 2b, 2a]
Merge sortSiEn merge, si iguales, tomar primero del array izquierdo
Quick sortNoPartition mueve elementos sobre el pivote, puede cruzar iguales
Heap sortNoPercolateDown salta niveles del arbol, desordenando iguales

Regla general: los algoritmos que solo hacen comparaciones/swaps entre vecinos tienden a ser estables.

17. Resumen de Formulas Clave

ConceptoFormula / Dato
Altura arbol completoh=log2nh = \lfloor \log_2 n \rfloor
Nodos en arbol completo nivel kk2k2^k
Nodos maximos hasta nivel hh2h+112^{h+1} - 1
Hojas en heap de nn elementosn/2\lceil n/2 \rceil
Complejidad de buildHeapO(n)O(n), no O(nlogn)O(n \log n)
Factor de carga hashλ=n/m\lambda = n/m, rehash si >0.7> 0.7
AVL: condicion de balance$
Red-Black: altura maxima2log2(n+1)\leq 2 \log_2(n+1)
Teorema MaestroT(n)=aT(n/b)+O(nd)T(n) = aT(n/b) + O(n^d)
Matrices: (m×n)×(n×p)(m \times n) \times (n \times p)=(m×p)= (m \times p), costo O(mnp)O(mnp)

18. Errores Fatales de Examen

  1. Olvidar caso base en recursion — stack overflow
  2. BST delete con 2 hijos: sucesor = menor del subarbol derecho (ir der, luego todo izq)
  3. AVL: recalcular alturas DESPUES de rotar — si no, BF queda mal
  4. Hash linear probing: recordar wrap-around (volver a indice 0 despues del ultimo)
  5. Quadratic probing: puede no encontrar slot aunque haya vacios; mm primo + λ<0.5\lambda < 0.5 lo garantiza
  6. Selection sort NO es estable (swap puede desordenar iguales)
  7. Quick sort peor caso O(n2)O(n^2) con datos ya ordenados y mal pivote
  8. Confundir BFS (cola) con DFS (pila/recursion)
  9. Multiplicacion de matrices: columnas de A = filas de B, resultado es m×pm \times p, NO es conmutativa
  10. Inorden de BST = ordenado. Si no sale ordenado, el BST esta mal construido.
  11. buildHeap es O(n)O(n), NO O(nlogn)O(n \log n) — pregunta clasica de parcial
  12. AVL rotacion LR/RL: son DOS rotaciones, no una. Primero rotar hijo, luego nodo.
  13. Merge sort necesita O(n)O(n) espacio extra — no es in-place
  14. Red-Black: todo camino tiene mismo numero de nodos NEGROS (no total de nodos)
  15. Heap: padre de indice ii es (i1)/2\lfloor(i-1)/2\rfloor, no i/2\lfloor i/2 \rfloor (eso es para indice desde 1)