Semana 13 — Búsqueda y eficiencia

Objetivos

  • Implementar búsqueda lineal y binaria, y explicar cuándo se puede usar cada una.
  • Estimar cómo crece el trabajo de un algoritmo con el tamaño de la entrada.
  • Comparar ordenamientos con mediciones reales.

Teoría — Búsqueda, ordenamiento y eficiencia · ✂️ Recursión

Lectura: Apuntes, semana 13.

Temas:

  • Búsqueda lineal y binaria, en pseudocódigo y en C. Requisito de la binaria: datos ordenados.
  • Contar pasos en lugar de medir segundos. Cómo crecen N, log N y N². Sin definición formal de la notación O.
  • Ordenamiento por selección. qsort como caja negra.
  • Cuándo vale la pena ordenar para después buscar.
  • ✂️ Recursión: caso base, caso recursivo y factorial. Se ve a fondo en Introducción a la Programación.

Laboratorio — Medir búsquedas y ordenamientos

  1. Implementar búsqueda lineal y binaria con pruebas assert.
  2. Contar comparaciones de cada una en 1 000, 10 000 y 100 000 elementos.
  3. Implementar ordenamiento por selección y medir su tiempo con clock. Comparar con qsort. Los tiempos dependen de la máquina; la tendencia, no.

Preguntas de repaso

  1. ¿Cuántas comparaciones hace como máximo la búsqueda binaria en 1 000 000 de elementos?
  2. ¿Por qué la búsqueda binaria no funciona en una lista desordenada?
  3. Un algoritmo tarda 2 s con N = 1000. ¿Cuánto esperan con N = 2000 si crece como N? ¿Y como N²?
  4. Hay que buscar una sola vez en una lista desordenada. ¿Conviene ordenar primero? ¿Y si son 100 000 búsquedas?
  5. ¿Qué pasa si una función recursiva no tiene caso base?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (12)Errores (6)Pruebas (3)Laboratorio (2)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

binary_search.cDescargar binary_search.c
// Búsqueda binaria: en datos ordenados, descarta la mitad en cada paso.
#include <assert.h>
#include <stdio.h>

#define N 10

// a debe estar ordenado de menor a mayor.
int binary_search(const int a[], int n, int value) {
    int low = 0;
    int high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (a[mid] == value) {
            return mid;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

int main(void) {
    int codes[N] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};

    assert(binary_search(codes, N, 2) == 0);     // el primero
    assert(binary_search(codes, N, 91) == 9);    // el último
    assert(binary_search(codes, N, 23) == 5);    // uno del medio
    assert(binary_search(codes, N, 1) == -1);    // menor que todos
    assert(binary_search(codes, N, 100) == -1);  // mayor que todos
    assert(binary_search(codes, N, 40) == -1);   // entre dos valores
    assert(binary_search(codes, 0, 2) == -1);    // arreglo vacío
    assert(binary_search(codes, 1, 2) == 0);     // un solo elemento

    printf("Todas las pruebas pasaron\n");
    return 0;
}
binary_trace.cDescargar binary_trace.c
// Muestra los índices de cada paso de la búsqueda binaria.
#include <stdio.h>

#define N 10

int binary_search_trace(const int a[], int n, int value) {
    int low = 0;
    int high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        printf("low = %d, high = %d, mid = %d, a[mid] = %d\n", low, high, mid, a[mid]);
        if (a[mid] == value) {
            return mid;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    printf("low = %d, high = %d: ya no queda nada\n", low, high);
    return -1;
}

int main(void) {
    int codes[N] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};

    int value;
    if (scanf("%d", &value) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Resultado: %d\n", binary_search_trace(codes, N, value));
    return 0;
}
check_searching.cDescargar check_searching.c
// Parte de las pruebas del laboratorio: test_lab.py corre esto con sus funciones.
// No hace falta leerlo. Usa argumentos de la línea de comandos, que no vemos en el curso.
#include <stdio.h>
#include <string.h>

#define CAP 300

struct search_result {
    int index;
    int comparisons;
};

int linear_search(const int a[], int n, int value);
int binary_search(const int a[], int n, int value);
struct search_result linear_count(const int a[], int n, int value);
struct search_result binary_count(const int a[], int n, int value);

static int max_halvings(int n) {
    int count = 0;
    while (n > 0) {
        n = n / 2;
        count++;
    }
    return count;
}

// Lleno con pares; los que siguen a n también son pares, para detectar si se pasan de n.
static void fill(int a[], int size) {
    for (int i = 0; i < size; i++) {
        a[i] = 2 * i;
    }
}

static int check_linear(void) {
    int a[CAP];
    fill(a, CAP);
    for (int n = 0; n < CAP; n++) {
        for (int i = 0; i < n; i++) {
            if (linear_search(a, n, a[i]) != i) {
                printf("linear_search: n=%d, no encuentra el índice %d\n", n, i);
                return 1;
            }
        }
        // a[n] existe pero no es parte de los n elementos.
        if (linear_search(a, n, a[n]) != -1 || linear_search(a, n, -1) != -1) {
            printf("linear_search: n=%d, debía devolver -1\n", n);
            return 1;
        }
    }
    int repeated[5] = {7, 3, 7, 3, 7};
    if (linear_search(repeated, 5, 3) != 1) {
        printf("linear_search: con repetidos debe devolver el primero\n");
        return 1;
    }
    int unsorted[5] = {5, 1, 9, 3, 7};
    if (linear_search(unsorted, 5, 1) != 1) {
        printf("linear_search: debe funcionar sin orden\n");
        return 1;
    }
    return 0;
}

static int check_binary(void) {
    int a[CAP];
    fill(a, CAP);
    for (int n = 0; n < CAP; n++) {
        for (int i = 0; i < n; i++) {
            if (binary_search(a, n, a[i]) != i) {
                printf("binary_search: n=%d, no encuentra el índice %d\n", n, i);
                return 1;
            }
            if (binary_search(a, n, a[i] + 1) != -1) {
                printf("binary_search: n=%d, encontró un valor que no está\n", n);
                return 1;
            }
        }
        if (binary_search(a, n, a[n]) != -1 || binary_search(a, n, -1) != -1) {
            printf("binary_search: n=%d, debía devolver -1\n", n);
            return 1;
        }
    }
    int repeated[5] = {1, 3, 3, 3, 9};
    int found = binary_search(repeated, 5, 3);
    if (found < 1 || found > 3) {
        printf("binary_search: con repetidos debe dar uno de los índices\n");
        return 1;
    }
    // Sin orden, una búsqueda binaria no encuentra este 1. Una lineal disfrazada sí.
    int unsorted[5] = {5, 1, 9, 3, 7};
    if (binary_search(unsorted, 5, 1) != -1) {
        printf("binary_search: encontró un valor mirando más de lo que parte a la mitad\n");
        return 1;
    }
    return 0;
}

static int check_linear_count(void) {
    int a[CAP];
    fill(a, CAP);
    for (int n = 0; n < CAP; n++) {
        for (int i = 0; i < n; i++) {
            struct search_result r = linear_count(a, n, a[i]);
            if (r.index != i || r.comparisons != i + 1) {
                printf("linear_count: n=%d, valor en %d: da índice %d y %d comparaciones\n",
                       n, i, r.index, r.comparisons);
                return 1;
            }
        }
        struct search_result r = linear_count(a, n, a[n]);
        if (r.index != -1 || r.comparisons != n) {
            printf("linear_count: n=%d, valor ausente: da índice %d y %d comparaciones\n",
                   n, r.index, r.comparisons);
            return 1;
        }
    }
    return 0;
}

static int check_binary_count(void) {
    int a[CAP];
    fill(a, CAP);
    for (int n = 1; n < CAP; n++) {
        int worst = 0;
        for (int value = -1; value <= 2 * n; value++) {
            struct search_result r = binary_count(a, n, value);
            int expected = (value >= 0 && value < 2 * n && value % 2 == 0) ? value / 2 : -1;
            if (r.index != expected) {
                printf("binary_count: n=%d, valor %d: da índice %d\n", n, value, r.index);
                return 1;
            }
            if (r.comparisons < 1) {
                printf("binary_count: n=%d, valor %d: no contó\n", n, value);
                return 1;
            }
            if (r.comparisons > worst) {
                worst = r.comparisons;
            }
        }
        if (worst != max_halvings(n)) {
            printf("binary_count: n=%d, el peor caso da %d y debe ser %d\n", n, worst,
                   max_halvings(n));
            return 1;
        }
    }
    // Pasos exactos en 0, 2, ..., 18: para el 2 se miran a[4] y a[1], y ahí está.
    struct search_result r = binary_count(a, 10, 2);
    if (r.index != 1 || r.comparisons != 2) {
        printf("binary_count: n=10, valor 2: da índice %d y %d comparaciones; deben ser 1 y 2\n",
               r.index, r.comparisons);
        return 1;
    }
    r = binary_count(a, 0, 2);
    if (r.index != -1 || r.comparisons != 0) {
        printf("binary_count: n=0 debe dar -1 y 0 comparaciones\n");
        return 1;
    }
    // Dos búsquedas seguidas no deben sumar sus cuentas.
    struct search_result again = binary_count(a, 10, 2);
    if (again.comparisons != 2) {
        printf("binary_count: la segunda llamada da %d; cada una debe contar desde 0\n",
               again.comparisons);
        return 1;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        return 2;
    }
    if (strcmp(argv[1], "linear") == 0) {
        return check_linear();
    }
    if (strcmp(argv[1], "binary") == 0) {
        return check_binary();
    }
    if (strcmp(argv[1], "linear_count") == 0) {
        return check_linear_count();
    }
    if (strcmp(argv[1], "binary_count") == 0) {
        return check_binary_count();
    }
    return 2;
}
check_sorting.cDescargar check_sorting.c
// Parte de las pruebas del laboratorio: test_lab.py corre esto con sus funciones.
// No hace falta leerlo. Reemplaza a clock por un reloj de prueba, con un valor conocido,
// para comprobar la medición sin depender de la máquina, y a qsort por una versión que
// se deja contar. Por eso usa variables globales, que en el resto del curso no se usan.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

long long selection_sort(int a[], int n);
int is_sorted(const int a[], int n);
double seconds_selection(int a[], int n);
double seconds_qsort(int a[], int n);

#define N 50
#define BIG 70000

static int *watched;
static int clock_calls;
static int sorted_at_call[8];
static int qsort_calls;

static int really_sorted(const int a[], int n) {
    for (int i = 1; i < n; i++) {
        if (a[i - 1] > a[i]) {
            return 0;
        }
    }
    return 1;
}

// Reloj de prueba: cada llamada avanza medio segundo y anota si el arreglo ya estaba ordenado.
clock_t clock(void) {
    if (clock_calls < 8 && watched != NULL) {
        sorted_at_call[clock_calls] = really_sorted(watched, N);
    }
    clock_calls++;
    return (clock_t) (clock_calls * (CLOCKS_PER_SEC / 2));
}

// Reemplazo de qsort: ordena por inserción con la función que recibe, y cuenta la llamada.
void qsort(void *base, size_t count, size_t size, int (*compare)(const void *, const void *)) {
    qsort_calls++;
    char *bytes = base;
    char saved[64];
    if (size > sizeof(saved)) {
        abort();
    }
    for (size_t i = 1; i < count; i++) {
        memcpy(saved, bytes + i * size, size);
        size_t j = i;
        while (j > 0 && compare(bytes + (j - 1) * size, saved) > 0) {
            memcpy(bytes + j * size, bytes + (j - 1) * size, size);
            j--;
        }
        memcpy(bytes + j * size, saved, size);
    }
}

static void fill_shuffled(int a[], int n) {
    // Una permutación fija de 0..n-1, sin rand.
    for (int i = 0; i < n; i++) {
        a[i] = (i * 37 + 11) % n;
    }
}

static int check_sort(void) {
    int cases[][6] = {{3, 1, 2, 0, 0, 0}, {1, 1, 1, 1, 1, 1}, {6, 5, 4, 3, 2, 1},
                      {1, 2, 3, 4, 5, 6}, {-5, 2000000000, -2000000000, 0, 7, -5}};
    int sizes[] = {3, 6, 6, 6, 6};
    for (int c = 0; c < 5; c++) {
        int n = sizes[c];
        int a[8];
        int reference[8];
        for (int i = 0; i < n; i++) {
            a[i] = cases[c][i];
            reference[i] = cases[c][i];
        }
        // Dos casillas de más que no se deben tocar.
        a[n] = -999;
        a[n + 1] = 999;
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                if (reference[j] < reference[i]) {
                    int s = reference[i];
                    reference[i] = reference[j];
                    reference[j] = s;
                }
            }
        }
        long long count = selection_sort(a, n);
        for (int i = 0; i < n; i++) {
            if (a[i] != reference[i]) {
                printf("selection_sort: caso %d, posición %d: da %d y debe ser %d\n", c, i, a[i],
                       reference[i]);
                return 1;
            }
        }
        if (a[n] != -999 || a[n + 1] != 999) {
            printf("selection_sort: caso %d, escribió después de n\n", c);
            return 1;
        }
        if (count != (long long) n * (n - 1) / 2) {
            printf("selection_sort: caso %d, %lld comparaciones; deben ser %d\n", c, count,
                   n * (n - 1) / 2);
            return 1;
        }
    }
    int one[1] = {4};
    if (selection_sort(one, 1) != 0 || one[0] != 4) {
        printf("selection_sort: con un elemento debe dar 0 comparaciones\n");
        return 1;
    }
    if (selection_sort(one, 0) != 0) {
        printf("selection_sort: con n = 0 debe dar 0 comparaciones\n");
        return 1;
    }
    return 0;
}

static int check_is_sorted(void) {
    int up[4] = {1, 2, 2, 3};
    int last_bad[4] = {1, 2, 3, 0};
    int first_bad[4] = {5, 1, 2, 3};
    if (is_sorted(up, 4) != 1 || is_sorted(up, 1) != 1 || is_sorted(up, 0) != 1) {
        printf("is_sorted: dice que no a un arreglo ordenado\n");
        return 1;
    }
    if (is_sorted(last_bad, 4) != 0 || is_sorted(first_bad, 4) != 0) {
        printf("is_sorted: dice que sí a un arreglo desordenado\n");
        return 1;
    }
    if (is_sorted(last_bad, 3) != 1) {
        printf("is_sorted: miró más allá de n\n");
        return 1;
    }
    return 0;
}

static int check_seconds(double (*measure)(int[], int), const char *name, int expected_qsort) {
    int a[N];
    fill_shuffled(a, N);
    if (really_sorted(a, N)) {
        return 2;
    }
    watched = a;
    clock_calls = 0;
    qsort_calls = 0;
    double seconds = measure(a, N);
    watched = NULL;
    if (qsort_calls != expected_qsort) {
        printf("%s: llamó a qsort %d veces; debe ser %d\n", name, qsort_calls, expected_qsort);
        return 1;
    }
    if (clock_calls != 2) {
        printf("%s: llamó a clock %d veces; deben ser 2\n", name, clock_calls);
        return 1;
    }
    if (sorted_at_call[0]) {
        printf("%s: ordenó antes de empezar a medir\n", name);
        return 1;
    }
    if (!sorted_at_call[1]) {
        printf("%s: la segunda medición llegó antes de terminar de ordenar\n", name);
        return 1;
    }
    if (seconds < 0.499 || seconds > 0.501) {
        printf("%s: con un reloj que avanza 0.5 s da %f\n", name, seconds);
        return 1;
    }
    return 0;
}

static int check_big_count(void) {
    // No cabe en un int. Se compila con -O2 y sin sanitizers para que tarde poco.
    static int a[BIG];
    fill_shuffled(a, BIG);
    long long count = selection_sort(a, BIG);
    long long expected = (long long) BIG * (BIG - 1) / 2;
    if (count != expected) {
        printf("selection_sort: con %d elementos da %lld y debe ser %lld\n", BIG, count, expected);
        return 1;
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        return 2;
    }
    if (strcmp(argv[1], "sort") == 0) {
        return check_sort();
    }
    if (strcmp(argv[1], "is_sorted") == 0) {
        return check_is_sorted();
    }
    if (strcmp(argv[1], "seconds_selection") == 0) {
        return check_seconds(seconds_selection, "seconds_selection", 0);
    }
    if (strcmp(argv[1], "seconds_qsort") == 0) {
        return check_seconds(seconds_qsort, "seconds_qsort", 1);
    }
    if (strcmp(argv[1], "big_count") == 0) {
        return check_big_count();
    }
    return 2;
}
count_search.cDescargar count_search.c
// Cuenta las comparaciones de cada búsqueda, en tres tamaños.
// Una comparación es mirar un elemento del arreglo y compararlo con el valor buscado.
#include <assert.h>
#include <stdio.h>

#define MAX_N 100000
#define SIZES 3

struct search_result {
    int index;         // dónde está, o -1
    int comparisons;   // cuántos elementos se miraron
};

struct search_result linear_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    for (int i = 0; i < n; i++) {
        result.comparisons++;
        if (a[i] == value) {
            result.index = i;
            return result;
        }
    }
    return result;
}

struct search_result binary_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    int low = 0;
    int high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        result.comparisons++;
        if (a[mid] == value) {
            result.index = mid;
            return result;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return result;
}

// Prueba todos los valores: los pares están en el arreglo y los impares no.
int binary_worst_case(const int a[], int n) {
    int worst = 0;
    for (int value = -1; value <= 2 * n; value++) {
        struct search_result result = binary_count(a, n, value);
        if (result.comparisons > worst) {
            worst = result.comparisons;
        }
    }
    return worst;
}

// Promedio de comparaciones si el valor sí está, probando cada posición una vez.
double linear_average(const int a[], int n) {
    long long total = 0;
    for (int i = 0; i < n; i++) {
        total = total + linear_count(a, n, a[i]).comparisons;
    }
    return (double) total / n;
}

double binary_average(const int a[], int n) {
    long long total = 0;
    for (int i = 0; i < n; i++) {
        total = total + binary_count(a, n, a[i]).comparisons;
    }
    return (double) total / n;
}

int main(void) {
    int sizes[SIZES] = {1000, 10000, 100000};
    int data[MAX_N];
    for (int i = 0; i < MAX_N; i++) {
        data[i] = 2 * i;   // ordenado, y solo con pares
    }

    assert(linear_count(data, 5, 4).index == 2);
    assert(linear_count(data, 5, 4).comparisons == 3);
    assert(binary_count(data, 5, 4).index == 2);
    assert(binary_count(data, 5, 4).comparisons == 1);

    printf("%8s %8s %8s %8s\n", "N", "lineal", "binaria", "peor");
    for (int s = 0; s < SIZES; s++) {
        int n = sizes[s];
        int missing = 2 * n;   // mayor que todos: no está
        printf("%8d %8d %8d %8d\n", n, linear_count(data, n, missing).comparisons,
               binary_count(data, n, missing).comparisons, binary_worst_case(data, n));
    }
    printf("Promedio con N = 1000, si el valor está: lineal %.2f, binaria %.2f\n",
           linear_average(data, 1000), binary_average(data, 1000));
    return 0;
}
factorial.cDescargar factorial.c
// Factorial recursivo, con un rastro de cada llamada.
#include <assert.h>
#include <stdio.h>

// n debe ser 0 o más, y a lo sumo 20: 21! ya no cabe en 64 bits.
long long factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

long long factorial_trace(int n) {
    printf("entra factorial(%d)\n", n);
    if (n <= 1) {
        printf("caso base: factorial(%d) devuelve 1\n", n);
        return 1;
    }
    long long result = n * factorial_trace(n - 1);
    printf("factorial(%d) devuelve %lld\n", n, result);
    return result;
}

int main(void) {
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(5) == 120);
    assert(factorial(20) == 2432902008176640000);

    factorial_trace(4);
    printf("factorial(20) = %lld\n", factorial(20));
    return 0;
}
growth.cDescargar growth.c
// Cómo crecen N, log N y N²: tablas de números exactos.
#include <assert.h>
#include <stdio.h>

// Cuántas veces se puede partir n a la mitad, sin decimales, hasta llegar a 0.
// Es el máximo de comparaciones de la búsqueda binaria en n elementos.
int halvings(int n) {
    int count = 0;
    while (n > 0) {
        n = n / 2;
        count++;
    }
    return count;
}

void print_row(int n) {
    long long square = (long long) n * n;
    printf("%9d %5d %9d %14lld\n", n, halvings(n), n, square);
}

int main(void) {
    assert(halvings(0) == 0);
    assert(halvings(1) == 1);
    assert(halvings(2) == 2);
    assert(halvings(3) == 2);

    printf("%9s %5s %9s %14s\n", "N", "log", "N", "N*N");
    for (int n = 1000; n <= 8000; n = n * 2) {
        print_row(n);
    }
    printf("\n");
    for (int n = 1000; n <= 1000000; n = n * 10) {
        print_row(n);
    }
    return 0;
}
linear_search.cDescargar linear_search.c
// Búsqueda lineal: mira los elementos uno por uno, en cualquier orden.
#include <assert.h>
#include <stdio.h>

#define N 6

int linear_search(const int a[], int n, int value) {
    for (int i = 0; i < n; i++) {
        if (a[i] == value) {
            return i;
        }
    }
    return -1;
}

int main(void) {
    int grades[N] = {14, 9, 17, 11, 20, 9};

    assert(linear_search(grades, N, 14) == 0);   // el primero
    assert(linear_search(grades, N, 9) == 1);    // repetido: da el primero que encuentra
    assert(linear_search(grades, N, 13) == -1);  // no está
    assert(linear_search(grades, 0, 14) == -1);  // arreglo vacío

    printf("Buscar 17 devuelve %d\n", linear_search(grades, N, 17));
    printf("Buscar 13 devuelve %d\n", linear_search(grades, N, 13));
    return 0;
}
qsort_demo.cDescargar qsort_demo.c
// qsort ordena un arreglo: le decimos qué tamaño tiene y cómo comparar dos elementos.
#include <stdio.h>
#include <stdlib.h>

#define N 6

// La forma de esta función la pide qsort. Devuelve un número negativo si el primero
// va antes, positivo si va después, y 0 si da igual.
int compare_ints(const void *first, const void *second) {
    int x = *(const int *) first;
    int y = *(const int *) second;
    if (x < y) {
        return -1;
    } else if (x > y) {
        return 1;
    }
    return 0;
}

int main(void) {
    int grades[N] = {14, 9, 17, 11, 20, 9};

    qsort(grades, N, sizeof(grades[0]), compare_ints);

    for (int i = 0; i < N; i++) {
        printf(" %d", grades[i]);
    }
    printf("\n");
    return 0;
}
selection_sort.cDescargar selection_sort.c
// Ordenamiento por selección: muestra cada pasada y cuenta las comparaciones.
#include <assert.h>
#include <stdio.h>

#define N 5

void print_array(const int a[], int n) {
    for (int i = 0; i < n; i++) {
        printf(" %d", a[i]);
    }
    printf("\n");
}

void swap_at(int a[], int i, int j) {
    int saved = a[i];
    a[i] = a[j];
    a[j] = saved;
}

// Ordena de menor a mayor y devuelve cuántas comparaciones hizo.
long long selection_sort(int a[], int n) {
    long long comparisons = 0;
    for (int i = 0; i < n - 1; i++) {
        int smallest = i;
        for (int j = i + 1; j < n; j++) {
            comparisons++;
            if (a[j] < a[smallest]) {
                smallest = j;
            }
        }
        swap_at(a, i, smallest);
        printf("i = %d, menor en %d:", i, smallest);
        print_array(a, n);
    }
    return comparisons;
}

int main(void) {
    int unsorted[N] = {29, 10, 14, 37, 13};
    int sorted[N] = {10, 13, 14, 29, 37};

    printf("Inicio:");
    print_array(unsorted, N);
    printf("Comparaciones: %lld\n", selection_sort(unsorted, N));

    printf("Inicio:");
    print_array(sorted, N);
    printf("Comparaciones: %lld\n", selection_sort(sorted, N));

    assert(selection_sort(sorted, 0) == 0);
    return 0;
}
sort_then_search.cDescargar sort_then_search.c
// ¿Conviene ordenar antes de buscar? Hace k búsquedas de valores que no están, de dos
// formas. Lee N y después los valores de k. Se compila con -O2 y sin sanitizers.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_N 100000

struct search_result {
    int index;
    int comparisons;
};

int compare_ints(const void *first, const void *second) {
    int x = *(const int *) first;
    int y = *(const int *) second;
    if (x < y) {
        return -1;
    } else if (x > y) {
        return 1;
    }
    return 0;
}

struct search_result linear_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    for (int i = 0; i < n; i++) {
        result.comparisons++;
        if (a[i] == value) {
            result.index = i;
            return result;
        }
    }
    return result;
}

struct search_result binary_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    int low = 0;
    int high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        result.comparisons++;
        if (a[mid] == value) {
            result.index = mid;
            return result;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return result;
}

int main(void) {
    int original[MAX_N];
    int work[MAX_N];

    int n;
    if (scanf("%d", &n) != 1 || n < 1 || n > MAX_N) {
        printf("Entrada inválida\n");
        return 1;
    }

    // Los pares de 0 a 2n - 2, desordenados. Los impares no están en el arreglo.
    srand(2026);
    for (int i = 0; i < n; i++) {
        original[i] = 2 * i;
    }
    for (int i = n - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int saved = original[i];
        original[i] = original[j];
        original[j] = saved;
    }

    printf("%7s %15s %12s %10s %10s\n", "k", "lineal", "binaria", "t lineal", "t ordenar");
    int k;
    while (scanf("%d", &k) == 1) {
        long long linear_total = 0;
        clock_t start = clock();
        for (int t = 0; t < k; t++) {
            struct search_result r = linear_count(original, n, 2 * (t % n) + 1);
            assert(r.index == -1);
            linear_total = linear_total + r.comparisons;
        }
        clock_t end = clock();
        double linear_seconds = (double) (end - start) / CLOCKS_PER_SEC;

        for (int i = 0; i < n; i++) {
            work[i] = original[i];
        }
        long long binary_total = 0;
        start = clock();
        qsort(work, n, sizeof(work[0]), compare_ints);
        for (int t = 0; t < k; t++) {
            struct search_result r = binary_count(work, n, 2 * (t % n) + 1);
            assert(r.index == -1);
            binary_total = binary_total + r.comparisons;
        }
        end = clock();
        double sorted_seconds = (double) (end - start) / CLOCKS_PER_SEC;

        printf("%7d %15lld %12lld %9.4fs %9.4fs\n", k, linear_total, binary_total,
               linear_seconds, sorted_seconds);
    }
    return 0;
}
sort_times.cDescargar sort_times.c
// Mide selección y qsort con clock. Lee los tamaños de la entrada, hasta que se acabe.
// Para medir se compila con -O2 y sin sanitizers: make build/fast/sort_times
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_N 100000

int compare_ints(const void *first, const void *second) {
    int x = *(const int *) first;
    int y = *(const int *) second;
    if (x < y) {
        return -1;
    } else if (x > y) {
        return 1;
    }
    return 0;
}

long long selection_sort(int a[], int n) {
    long long comparisons = 0;
    for (int i = 0; i < n - 1; i++) {
        int smallest = i;
        for (int j = i + 1; j < n; j++) {
            comparisons++;
            if (a[j] < a[smallest]) {
                smallest = j;
            }
        }
        int saved = a[i];
        a[i] = a[smallest];
        a[smallest] = saved;
    }
    return comparisons;
}

int is_sorted(const int a[], int n) {
    for (int i = 1; i < n; i++) {
        if (a[i - 1] > a[i]) {
            return 0;
        }
    }
    return 1;
}

int main(void) {
    int original[MAX_N];
    int work[MAX_N];
    srand(2026);
    for (int i = 0; i < MAX_N; i++) {
        original[i] = rand();
    }

    int n;
    printf("%7s %12s %10s %10s  %s\n", "N", "comparac.", "selección", "qsort", "ordenado");
    while (scanf("%d", &n) == 1) {
        if (n < 1 || n > MAX_N) {
            printf("Tamaño inválido: %d\n", n);
            return 1;
        }

        for (int i = 0; i < n; i++) {
            work[i] = original[i];
        }
        clock_t start = clock();
        long long comparisons = selection_sort(work, n);
        clock_t end = clock();
        double selection_seconds = (double) (end - start) / CLOCKS_PER_SEC;
        int ok = is_sorted(work, n);

        for (int i = 0; i < n; i++) {
            work[i] = original[i];
        }
        start = clock();
        qsort(work, n, sizeof(work[0]), compare_ints);
        end = clock();
        double qsort_seconds = (double) (end - start) / CLOCKS_PER_SEC;
        ok = ok && is_sorted(work, n);

        printf("%7d %12lld %9.4fs %9.4fs  ", n, comparisons, selection_seconds, qsort_seconds);
        if (ok) {
            printf("sí\n");
        } else {
            printf("NO\n");
        }
    }
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/binary_unsorted.cDescargar errors/binary_unsorted.c
// Error a propósito: búsqueda binaria en un arreglo que no está ordenado.
#include <stdio.h>

#define N 5

int binary_search(const int a[], int n, int value) {
    int low = 0;
    int high = n - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (a[mid] == value) {
            return mid;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

int main(void) {
    int a[N] = {5, 1, 9, 3, 7};
    for (int i = 0; i < N; i++) {
        printf("El %d está en el índice %d; binaria responde %d\n", a[i], i,
               binary_search(a, N, a[i]));
    }
    return 0;
}
errors/compare_subtract.cDescargar errors/compare_subtract.c
// Error a propósito: restar para comparar parece cómodo, pero la resta puede no caber.
#include <stdio.h>
#include <stdlib.h>

#define N 3

int compare_subtract(const void *first, const void *second) {
    int x = *(const int *) first;
    int y = *(const int *) second;
    return x - y;
}

int main(void) {
    int a[N] = {2000000000, -2000000000, 5};

    qsort(a, N, sizeof(a[0]), compare_subtract);

    for (int i = 0; i < N; i++) {
        printf(" %d", a[i]);
    }
    printf("\n");
    return 0;
}
errors/deep_recursion.cDescargar errors/deep_recursion.c
// Tiene caso base, pero con un n muy grande la pila no alcanza.
#include <stdio.h>

long long sum_down(int n) {
    if (n <= 0) {
        return 0;
    }
    return n + sum_down(n - 1);
}

int main(void) {
    int n;
    if (scanf("%d", &n) != 1) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("%lld\n", sum_down(n));
    return 0;
}
errors/low_less_high.cDescargar errors/low_less_high.c
// Error a propósito: el ciclo se detiene cuando aún queda un elemento por mirar.
#include <stdio.h>

#define N 3

int binary_search_wrong(const int a[], int n, int value) {
    int low = 0;
    int high = n - 1;
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (a[mid] == value) {
            return mid;
        } else if (a[mid] < value) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

int main(void) {
    int a[N] = {10, 20, 30};
    for (int i = 0; i < N; i++) {
        printf("El %d está en el índice %d; responde %d\n", a[i], i,
               binary_search_wrong(a, N, a[i]));
    }
    return 0;
}
errors/mid_overflow.cDescargar errors/mid_overflow.c
// Error a propósito: low + high puede no caber en un int, aunque el resultado sí cabría.
#include <stdio.h>

int main(void) {
    int low;
    int high;
    if (scanf("%d %d", &low, &high) != 2) {
        printf("Entrada inválida\n");
        return 1;
    }
    printf("Fórmula segura: %d\n", low + (high - low) / 2);
    printf("Fórmula con la suma: %d\n", (low + high) / 2);
    return 0;
}
errors/no_base_case.cDescargar errors/no_base_case.c
// Error a propósito: una función recursiva sin caso base.
#include <stdio.h>

long long sum_down(int n) {
    return n + sum_down(n - 1);
}

int main(void) {
    printf("%lld\n", sum_down(3));
    return 0;
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.pyDescargar test_examples.py
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 13.
Los tiempos dependen de la máquina: aquí se comparan cuentas, nunca segundos."""
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]


def execute(command, stdin=""):
    done = subprocess.run(
        command, input=stdin, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout, done.returncode


def run(program, stdin=""):
    """Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
    return execute([str(BUILD / program)], stdin)


def build_source(source, *extra):
    """Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
    out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
    done = subprocess.run(
        ["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE, encoding="utf-8",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.stdout, done.returncode, out


def binary_probes(n, value):
    """La misma búsqueda binaria en Python, sobre los pares 0, 2, ..., 2n - 2."""
    low, high, probes = 0, n - 1, 0
    while low <= high:
        mid = low + (high - low) // 2
        probes += 1
        if 2 * mid == value:
            return probes
        if 2 * mid < value:
            low = mid + 1
        else:
            high = mid - 1
    return probes


class CleanCompile(unittest.TestCase):
    def test_examples_have_no_warnings(self):
        for source in sorted(HERE.glob("*.c")):
            with self.subTest(source=source.name):
                done = subprocess.run(
                    ["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_no_malloc_in_the_course(self):
        for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
            with self.subTest(source=source.name):
                self.assertNotIn("malloc", source.read_text(encoding="utf-8"))

    def test_timing_programs_have_no_sanitizers(self):
        for program in ["sort_times", "sort_then_search"]:
            with self.subTest(program=program):
                self.assertNotIn(b"__asan", (BUILD / "fast" / program).read_bytes())
        self.assertIn(b"__asan", (BUILD / "selection_sort").read_bytes())


class Searches(unittest.TestCase):
    def test_linear_search(self):
        self.assertEqual(run("linear_search"), (
            "Buscar 17 devuelve 2\nBuscar 13 devuelve -1\n", 0))

    def test_binary_search_asserts(self):
        self.assertEqual(run("binary_search"), ("Todas las pruebas pasaron\n", 0))

    def test_trace_of_a_value_that_is_there(self):
        self.assertEqual(run("binary_trace", "23\n"), (
            "low = 0, high = 9, mid = 4, a[mid] = 16\n"
            "low = 5, high = 9, mid = 7, a[mid] = 56\n"
            "low = 5, high = 6, mid = 5, a[mid] = 23\n"
            "Resultado: 5\n", 0))

    def test_trace_of_a_value_that_is_not_there(self):
        output, code = run("binary_trace", "40\n")
        self.assertEqual(code, 0)
        self.assertEqual(output.splitlines()[-2:], [
            "low = 7, high = 6: ya no queda nada", "Resultado: -1"])

    def test_trace_rejects_bad_input(self):
        self.assertEqual(run("binary_trace", "abc\n"), ("Entrada inválida\n", 1))

    def test_unsorted_data_breaks_binary_search(self):
        _, status, binary = build_source("errors/binary_unsorted.c")
        self.assertEqual(status, 0)
        self.assertEqual(execute([str(binary)]), (
            "El 5 está en el índice 0; binaria responde 0\n"
            "El 1 está en el índice 1; binaria responde -1\n"
            "El 9 está en el índice 2; binaria responde 2\n"
            "El 3 está en el índice 3; binaria responde -1\n"
            "El 7 está en el índice 4; binaria responde -1\n", 0))

    def test_low_less_than_high_misses_values(self):
        _, status, binary = build_source("errors/low_less_high.c")
        self.assertEqual(status, 0)
        self.assertEqual(execute([str(binary)]), (
            "El 10 está en el índice 0; responde -1\n"
            "El 20 está en el índice 1; responde 1\n"
            "El 30 está en el índice 2; responde -1\n", 0))

    def test_sum_of_low_and_high_overflows(self):
        _, status, binary = build_source("errors/mid_overflow.c", *SANITIZE)
        self.assertEqual(status, 0)
        output, _ = execute([str(binary)], "2000000000 2100000000\n")
        self.assertIn("signed integer overflow", output)
        self.assertIn("Fórmula segura: 2050000000", output)

    def test_safe_formula_is_silent_when_nothing_overflows(self):
        _, status, binary = build_source("errors/mid_overflow.c", *SANITIZE)
        self.assertEqual(status, 0)
        output, _ = execute([str(binary)], "4 9\n")
        self.assertNotIn("runtime error", output)
        self.assertEqual(output, "Fórmula segura: 6\nFórmula con la suma: 6\n")


class Counting(unittest.TestCase):
    def test_table_matches_an_independent_simulation(self):
        output, code = run("count_search")
        self.assertEqual(code, 0, output)
        rows = [line.split() for line in output.splitlines()[1:4]]
        for row in rows:
            n, linear, binary, worst = map(int, row)
            with self.subTest(n=n):
                self.assertEqual(linear, n)
                self.assertEqual(binary, binary_probes(n, 2 * n))
                self.assertEqual(worst, max(binary_probes(n, v) for v in range(-1, 2 * n + 1)))
        self.assertEqual([row[0] for row in rows], ["1000", "10000", "100000"])

    def test_averages_with_1000_elements(self):
        output, _ = run("count_search")
        average = sum(binary_probes(1000, 2 * i) for i in range(1000)) / 1000
        self.assertIn(f"lineal {(1000 + 1) / 2:.2f}, binaria {average:.2f}", output)

    def test_one_million_needs_twenty(self):
        output, _ = run("growth")
        self.assertRegex(output, r"1000000\s+20\s+1000000\s+1000000000000\n")
        self.assertEqual(binary_probes(1000000, 2 * 1000000), 20)

    def test_growth_doubling_rows(self):
        output, _ = run("growth")
        first = [list(map(int, line.split())) for line in output.splitlines()[1:5]]
        self.assertEqual([row[0] for row in first], [1000, 2000, 4000, 8000])
        for before, after in zip(first, first[1:]):
            self.assertEqual(after[1], before[1] + 1)      # log suma 1
            self.assertEqual(after[2], before[2] * 2)      # N se duplica
            self.assertEqual(after[3], before[3] * 4)      # N*N se multiplica por 4


class Sorting(unittest.TestCase):
    def test_selection_sort_trace(self):
        self.assertEqual(run("selection_sort"), (
            "Inicio: 29 10 14 37 13\n"
            "i = 0, menor en 1: 10 29 14 37 13\n"
            "i = 1, menor en 4: 10 13 14 37 29\n"
            "i = 2, menor en 2: 10 13 14 37 29\n"
            "i = 3, menor en 4: 10 13 14 29 37\n"
            "Comparaciones: 10\n"
            "Inicio: 10 13 14 29 37\n"
            "i = 0, menor en 0: 10 13 14 29 37\n"
            "i = 1, menor en 1: 10 13 14 29 37\n"
            "i = 2, menor en 2: 10 13 14 29 37\n"
            "i = 3, menor en 3: 10 13 14 29 37\n"
            "Comparaciones: 10\n", 0))

    def test_exact_comparisons_and_real_sorting(self):
        output, code = run("fast/sort_times", "1 2 1000 5000\n")
        self.assertEqual(code, 0, output)
        rows = [line.split() for line in output.splitlines()[1:]]
        self.assertEqual(len(rows), 4)
        for row in rows:
            n = int(row[0])
            with self.subTest(n=n):
                self.assertEqual(int(row[1]), n * (n - 1) // 2)
                self.assertRegex(row[2], r"^\d+\.\d{4}s$")
                self.assertRegex(row[3], r"^\d+\.\d{4}s$")
                self.assertEqual(row[4], "sí")

    def test_sizes_are_checked(self):
        output, code = run("fast/sort_times", "100001\n")
        self.assertEqual(code, 1)
        self.assertIn("Tamaño inválido", output)

    def test_qsort_demo(self):
        self.assertEqual(run("qsort_demo"), (" 9 9 11 14 17 20\n", 0))

    def test_subtracting_to_compare_overflows(self):
        _, status, binary = build_source("errors/compare_subtract.c", *SANITIZE)
        self.assertEqual(status, 0)
        output, _ = execute([str(binary)])
        self.assertIn("signed integer overflow", output)

    def test_sort_then_search_counts(self):
        output, code = run("fast/sort_then_search", "1000 1 10 1000\n")
        self.assertEqual(code, 0, output)
        rows = [line.split() for line in output.splitlines()[1:]]
        for row in rows:
            k = int(row[0])
            with self.subTest(k=k):
                self.assertEqual(int(row[1]), k * 1000)
                expected = sum(binary_probes(1000, 2 * (t % 1000) + 1) for t in range(k))
                self.assertEqual(int(row[2]), expected)


class Recursion(unittest.TestCase):
    def test_factorial_trace(self):
        self.assertEqual(run("factorial"), (
            "entra factorial(4)\nentra factorial(3)\nentra factorial(2)\nentra factorial(1)\n"
            "caso base: factorial(1) devuelve 1\n"
            "factorial(2) devuelve 2\nfactorial(3) devuelve 6\nfactorial(4) devuelve 24\n"
            "factorial(20) = 2432902008176640000\n", 0))

    def test_no_base_case_warns_and_dies(self):
        messages, status, binary = build_source("errors/no_base_case.c")
        self.assertEqual(status, 0)
        self.assertIn("-Winfinite-recursion", messages)
        _, code = execute(["bash", "-c", f"ulimit -s 8192; {binary}"])
        self.assertNotEqual(code, 0)

    def test_no_base_case_with_the_sanitizer(self):
        _, status, binary = build_source("errors/no_base_case.c", *SANITIZE)
        self.assertEqual(status, 0)
        # Siempre muere, pero en x86-64 emulado a veces no alcanza a escribir el informe.
        outputs = []
        for _ in range(12):
            output, code = execute(["bash", "-c", f"ulimit -s 8192; {binary}"])
            self.assertNotEqual(code, 0)
            outputs.append(output)
            if "AddressSanitizer: stack-overflow" in output:
                break
        self.assertRegex(outputs[-1], "AddressSanitizer: stack-overflow")

    def test_deep_recursion(self):
        _, status, binary = build_source("errors/deep_recursion.c")
        self.assertEqual(status, 0)
        self.assertEqual(execute([str(binary)], "1000\n"), ("500500\n", 0))
        _, code = execute(["bash", "-c", f"ulimit -s 8192; echo 10000000 | {binary}"])
        self.assertNotEqual(code, 0)


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution."""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")


def run(program, *args, stdin=""):
    done = subprocess.run(
        [str(HERE / "build" / LAB_DIR / program), *args], input=stdin, encoding="utf-8",
        errors="replace", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return done.stdout, done.returncode


def without_comments(path):
    return re.sub(r"//[^\n]*", "", path.read_text(encoding="utf-8"))


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
    def check(self, program, part):
        output, code = run(program, part)
        self.assertEqual(code, 0, output)

    def test_no_warnings(self):
        for name in ["searching", "sorting"]:
            with self.subTest(name=name):
                done = subprocess.run(
                    ["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
                     str(HERE / LAB_DIR / f"{name}.c")],
                    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                )
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_searching_prints_the_table(self):
        output, code = run("searching")
        self.assertEqual(code, 0, output)
        self.assertEqual(output, "1000 1000 10\n10000 10000 14\n100000 100000 17\n"
                                 "Todas las pruebas pasaron\n")

    def test_linear_search(self):
        self.check("check_searching", "linear")

    def test_binary_search(self):
        self.check("check_searching", "binary")

    def test_linear_count(self):
        self.check("check_searching", "linear_count")

    def test_binary_count(self):
        self.check("check_searching", "binary_count")

    def test_mid_does_not_add_low_and_high(self):
        code = without_comments(HERE / LAB_DIR / "searching.c")
        self.assertNotRegex(code, r"\blow\s*\+\s*high\b")
        self.assertNotRegex(code, r"\bhigh\s*\+\s*low\b")

    def test_selection_sort(self):
        self.check("check_sorting", "sort")

    def test_is_sorted(self):
        self.check("check_sorting", "is_sorted")

    def test_seconds_selection_with_a_fake_clock(self):
        self.check("check_sorting", "seconds_selection")

    def test_seconds_qsort_with_a_fake_clock(self):
        self.check("check_sorting", "seconds_qsort")

    def test_count_does_not_fit_in_int(self):
        self.check("check_sorting_fast", "big_count")

    def test_sorting_program(self):
        # Solo la forma de la salida: los tiempos dependen de la máquina.
        output, code = run("sorting", stdin="10 100\n")
        self.assertEqual(code, 0, output)
        self.assertRegex(output, r"\A10 \d+\.\d{4} \d+\.\d{4} ordenado\n"
                                 r"100 \d+\.\d{4} \d+\.\d{4} ordenado\n\Z")


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-13.md"
SLIDES = ROOT / "slides" / "week-13.tex"
# Antes de un bloque con direcciones o con comportamiento indefinido. De él solo se
# exigen estas cadenas estables, si el bloque las muestra.
MACHINE_DEPENDENT = "<!-- depende de la corrida -->"
STABLE = ["runtime error: signed integer overflow"]
# Antes de un bloque con tiempos. No se corre aquí, porque tarda y cambia en cada corrida:
# de él se revisan solo las cuentas, que son exactas.
MEASURED = "<!-- medición -->"
# En esta salida, solo las direcciones cambian.
ADDRESSES = re.compile(r"0x[0-9a-f]+")


def normalize(text):
    # Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
    return ADDRESSES.sub("0x", text.replace("‘", "'").replace("’", "'"))


def run_shell(command):
    done = subprocess.run(
        ["bash", "-c", command], cwd=HERE, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
    )
    return normalize(done.stdout)


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida)."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    return pairs


def note_blocks(language, marker=MACHINE_DEPENDENT):
    """Devuelve (líneas, tiene_la_marca) de cada bloque de los apuntes."""
    blocks, current, flagged, previous = [], None, False, ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, flagged = [], previous.strip() == marker
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, flagged))
            current = None
        else:
            current.append(line)
    return blocks


def slide_blocks(environments):
    text = SLIDES.read_text(encoding="utf-8")
    pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
    return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]


def known_code_lines():
    lines = set()
    for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class NotesAreTrue(unittest.TestCase):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        measured = [lines for lines, marked in note_blocks("text", MEASURED) if marked]
        for lines, flagged in note_blocks("text"):
            if not lines or not lines[0].startswith("$ ") or lines in measured:
                continue
            for command, expected in split_commands(lines):
                actual = run_shell(command).rstrip("\n")
                checked += 1
                with self.subTest(command=command):
                    if flagged:
                        for token in STABLE:
                            if any(token in line for line in expected):
                                self.assertIn(token, actual)
                    else:
                        lines = [line.rstrip() for line in actual.split("\n")]
                        self.assertEqual("\n".join(lines), "\n".join(expected))
        self.assertGreater(checked, 20)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("c"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_measured_blocks_have_exact_counts(self):
        rows = {"sort_times": 0, "sort_then_search": 0}
        measured = [lines for lines, marked in note_blocks("text", MEASURED) if marked]
        self.assertEqual(len(measured), 3)
        for block in measured:
            for command, output in split_commands(block):
                for line in output:
                    fields = line.split()
                    if "sort_times" in command and len(fields) == 5 and fields[0].isdigit():
                        n = int(fields[0])
                        self.assertEqual(int(fields[1]), n * (n - 1) // 2, line)
                        self.assertEqual(fields[4], "sí", line)
                        rows["sort_times"] += 1
                    if "sort_then_search" in command and len(fields) == 5 and fields[0].isdigit():
                        self.assertEqual(int(fields[1]), int(fields[0]) * 100000, line)
                        rows["sort_then_search"] += 1
        self.assertEqual(rows, {"sort_times": 7, "sort_then_search": 6})

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_notes_flag_what_depends_on_the_machine(self):
        text = " ".join(NOTES.read_text(encoding="utf-8").split())
        self.assertIn("en esta corrida", text)
        self.assertIn("esta máquina", text)
        self.assertEqual(text.count(MEASURED), 3)
        self.assertNotIn("TODO", text)


class SlidesAreTrue(unittest.TestCase):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_terminal_blocks_in_slides(self):
        for block in slide_blocks("terminal|terminalmini|salidalarga"):
            for command, expected in split_commands(block):
                actual = run_shell(command).splitlines()
                position = 0
                # Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
                for line in expected:
                    line = line.strip()
                    if not line:
                        continue
                    with self.subTest(command=command, line=line):
                        found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
                        self.assertTrue(found, f"no aparece en la salida real: {line}")
                        if found:
                            position = found[0] + 1

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_measurements_in_slides_come_from_the_notes(self):
        notes = NOTES.read_text(encoding="utf-8")
        blocks = slide_blocks("informe")
        self.assertGreater(len(blocks), 0)
        for block in blocks:
            for line in block:
                if line.strip():
                    with self.subTest(line=line):
                        self.assertIn(line.rstrip(), notes)

    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_code_blocks_in_slides_exist(self):
        known = known_code_lines()
        for block in slide_blocks("codigo|codigochico|codigomini"):
            for line in block:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/searching.cDescargar lab/searching.c
// Laboratorio, partes 1 y 2: búsqueda lineal y binaria, y contar comparaciones.
//
// Escriban cada función según su comentario. Borren las líneas (void) de cada una:
// solo están para que el archivo compile sin advertencias mientras tanto.
//
// En main hay una prueba de cada función. Agreguen más con assert: el primero, el
// último, un valor que no está, n igual a 0.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>

#define MAX_N 100000
#define SIZES 3

struct search_result {
    int index;         // dónde está, o -1
    int comparisons;   // cuántos elementos se miraron
};

// Devuelve el primer índice donde está value, o -1 si no está.
int linear_search(const int a[], int n, int value) {
    (void) a;
    (void) n;
    (void) value;
    return -1;
}

// a está ordenado de menor a mayor. Devuelve un índice donde está value, o -1.
// Usen low, high y mid, y calculen mid con low + (high - low) / 2.
int binary_search(const int a[], int n, int value) {
    (void) a;
    (void) n;
    (void) value;
    return -1;
}

// Como linear_search, y además cuenta los elementos que miró: uno por cada
// elemento del arreglo que compara con value.
struct search_result linear_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    (void) a;
    (void) n;
    (void) value;
    return result;
}

// Como binary_search, y además cuenta los elementos que miró: uno por cada a[mid].
struct search_result binary_count(const int a[], int n, int value) {
    struct search_result result = {-1, 0};
    (void) a;
    (void) n;
    (void) value;
    return result;
}

int main(void) {
    int small[5] = {10, 20, 30, 40, 50};

    assert(linear_search(small, 5, 50) == 4);
    assert(binary_search(small, 5, 50) == 4);
    assert(linear_count(small, 5, 30).comparisons == 3);
    assert(binary_count(small, 5, 30).comparisons == 1);

    // Con estos datos los pares están y los impares no. 2 * n es mayor que todos.
    int data[MAX_N];
    for (int i = 0; i < MAX_N; i++) {
        data[i] = 2 * i;
    }
    int sizes[SIZES] = {1000, 10000, 100000};
    for (int s = 0; s < SIZES; s++) {
        int n = sizes[s];
        // Escriban aquí una línea con printf("%d %d %d\n", ...): n, las comparaciones
        // de linear_count y las de binary_count, buscando 2 * n en los n primeros.
        (void) n;
        (void) data;
    }

    printf("Todas las pruebas pasaron\n");
    return 0;
}
lab/sorting.cDescargar lab/sorting.c
// Laboratorio, parte 3: ordenamiento por selección, y medir con clock.
//
// Escriban cada función según su comentario. Borren las líneas (void) de cada una.
// Pruébenlo con make test-lab. Para medir de verdad, usen make measure: compila con
// -O2 y sin sanitizers. Los tiempos dependen de la máquina; la tendencia, no.
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_N 100000

// La función de comparar que pide qsort. Viene lista.
int compare_ints(const void *first, const void *second) {
    int x = *(const int *) first;
    int y = *(const int *) second;
    if (x < y) {
        return -1;
    } else if (x > y) {
        return 1;
    }
    return 0;
}

// Ordena de menor a mayor por selección. Devuelve cuántas comparaciones entre elementos
// hizo. Usen long long para contar: con 100 000 elementos, la cuenta no cabe en un int.
long long selection_sort(int a[], int n) {
    (void) a;
    (void) n;
    return 0;
}

// Devuelve 1 si a está ordenado de menor a mayor, y 0 si no.
int is_sorted(const int a[], int n) {
    (void) a;
    (void) n;
    return 0;
}

// Ordena a con selection_sort y devuelve los segundos que tardó, medidos con clock.
double seconds_selection(int a[], int n) {
    (void) a;
    (void) n;
    return 0.0;
}

// Ordena a con qsort y compare_ints, y devuelve los segundos que tardó, medidos con clock.
double seconds_qsort(int a[], int n) {
    (void) a;
    (void) n;
    return 0.0;
}

int main(void) {
    int small[5] = {29, 10, 14, 37, 13};
    assert(selection_sort(small, 5) == 10);
    assert(is_sorted(small, 5) == 1);

    int original[MAX_N];
    int work[MAX_N];
    srand(2026);
    for (int i = 0; i < MAX_N; i++) {
        original[i] = rand();
    }

    // Lee los tamaños de la entrada, hasta que se acabe. Este main viene listo.
    int n;
    while (scanf("%d", &n) == 1) {
        if (n < 1 || n > MAX_N) {
            printf("Tamaño inválido: %d\n", n);
            return 1;
        }
        for (int i = 0; i < n; i++) {
            work[i] = original[i];
        }
        double selection = seconds_selection(work, n);
        int ok = is_sorted(work, n);

        for (int i = 0; i < n; i++) {
            work[i] = original[i];
        }
        double library = seconds_qsort(work, n);
        ok = ok && is_sorted(work, n);

        printf("%d %.4f %.4f ", n, selection, library);
        if (ok) {
            printf("ordenado\n");
        } else {
            printf("NO ORDENADO\n");
        }
    }
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 13: búsqueda, ordenamiento y eficiencia. Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos y de los apuntes
#   make test-lab  prueba su laboratorio (lab/*.c); falla hasta que lo terminen
#   make measure   mide su lab/sorting.c, con -O2 y sin sanitizers
#   make clean     borra lo generado
#
# Para medir tiempos se compila aparte, en build/fast/, con -O2 y sin sanitizers: los
# sanitizers hacen más lento el programa, y sin optimizar se mediría otra cosa.
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
#
# check_searching.c y check_sorting.c son parte de las pruebas: llaman a las funciones
# de su laboratorio. Para enlazarlos, el main del laboratorio se compila con otro nombre.

CC = gcc
BASEFLAGS = -Wall -Wextra -std=c17
CFLAGS = $(BASEFLAGS) -g -fsanitize=address,undefined
FASTFLAGS = $(BASEFLAGS) -O2
BUILD = build
PROGRAMS = linear_search binary_search binary_trace count_search growth selection_sort \
           qsort_demo factorial
FAST_PROGRAMS = sort_times sort_then_search

.PHONY: all lab test test-lab measure clean

all: $(PROGRAMS:%=$(BUILD)/%) $(FAST_PROGRAMS:%=$(BUILD)/fast/%)

$(BUILD)/%: %.c | $(BUILD)
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/fast/%: %.c | $(BUILD)/fast
	$(CC) $(FASTFLAGS) $< -o $@

$(BUILD) $(BUILD)/fast $(BUILD)/lab $(BUILD)/solution:
	mkdir -p $@

$(BUILD)/lab/%: lab/%.c | $(BUILD)/lab
	$(CC) $(CFLAGS) $< -o $@

$(BUILD)/solution/%: solution/%.c | $(BUILD)/solution
	$(CC) $(CFLAGS) $< -o $@

# Las mismas reglas para las dos carpetas: $(1) es lab o solution.
define CHECKS
$(BUILD)/$(1)/check_searching: check_searching.c $(1)/searching.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) -Dmain=student_main -c $(1)/searching.c -o $(BUILD)/$(1)/searching.o
	$(CC) $(CFLAGS) check_searching.c $(BUILD)/$(1)/searching.o -o $$@

$(BUILD)/$(1)/check_sorting: check_sorting.c $(1)/sorting.c | $(BUILD)/$(1)
	$(CC) $(CFLAGS) -Dmain=student_main -c $(1)/sorting.c -o $(BUILD)/$(1)/sorting.o
	$(CC) $(CFLAGS) check_sorting.c $(BUILD)/$(1)/sorting.o -o $$@

$(BUILD)/$(1)/check_sorting_fast: check_sorting.c $(1)/sorting.c | $(BUILD)/$(1)
	$(CC) $(FASTFLAGS) -Dmain=student_main -c $(1)/sorting.c -o $(BUILD)/$(1)/sorting_fast.o
	$(CC) $(FASTFLAGS) check_sorting.c $(BUILD)/$(1)/sorting_fast.o -o $$@

$(BUILD)/$(1)/sorting_fast: $(1)/sorting.c | $(BUILD)/$(1)
	$(CC) $(FASTFLAGS) $$< -o $$@
endef
$(eval $(call CHECKS,lab))
$(eval $(call CHECKS,solution))

LAB_FILES = searching sorting check_searching check_sorting check_sorting_fast

lab: $(LAB_FILES:%=$(BUILD)/lab/%)

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
	@if [ -d solution ]; then $(MAKE) --no-print-directory \
	  $(LAB_FILES:%=$(BUILD)/solution/%); fi
	python3 -m unittest discover -p 'test_*.py'

test-lab: all lab
	LAB_DIR=lab python3 -m unittest -v test_lab

measure: $(BUILD)/lab/sorting_fast
	@echo "N, segundos de selección, segundos de qsort:"
	@echo 10000 20000 40000 80000 | $(BUILD)/lab/sorting_fast

clean:
	rm -rf $(BUILD) __pycache__