06
Secuencias y ordenamiento

Ordenamiento y búsqueda binaria

Secuencias y ordenamiento

Semana 6 — Ordenamiento y búsqueda binaria

Objetivos

  • Implementar mergesort y quicksort y explicar su costo con el árbol de llamadas.
  • Reconocer el peor caso de quicksort y cómo evitarlo en la práctica.
  • Escribir una búsqueda binaria correcta a partir de su invariante.

Teoría — Ordenamiento: mergesort y quicksort · Búsqueda binaria

Lectura: Apuntes, semana 6.

Apoyo: Open Data Structures, secs. 11.1.1 (Merge-Sort), 11.1.2 (Quicksort) y 11.1.4 (A Lower-Bound for Comparison-Based Sorting). CLRS, capítulo Quicksort.

Temas:

  • Mergesort: dividir, ordenar y mezclar. Árbol de llamadas: log n niveles con n de trabajo cada uno. Necesita memoria extra.
  • Quicksort: partición, pivote e invariante de la partición. Promedio Θ(n log n), peor caso Θ(n²) con un arreglo ya ordenado y un pivote ingenuo.
  • Pivote al azar. Muchos elementos repetidos y la partición en tres.
  • Estabilidad: qué es y cuándo importa.
  • La cota Ω(n log n) para ordenar comparando, con la idea del árbol de decisiones. Sin demostración completa.
  • Qué usan las bibliotecas: std::sort y std::stable_sort. Pasarles un comparador.
  • Búsqueda binaria con invariante: todo a la izquierda es menor, todo a la derecha es mayor o igual. std::lower_bound.
  • Error común: (lo + hi) / 2 con overflow, y ciclos que no terminan porque el rango no se reduce.

Laboratorio — Autocompletado

Sesión 1:

  1. Escribir merge_sort y quick_sort como templates con comparador, con pruebas al azar contra std::sort.
  2. Medirlos con datos al azar, ordenados y con muchos repetidos. Encontrar la entrada que hace lento a un quicksort con pivote fijo.

Sesión 2: 3. Cargar un archivo de términos con su frecuencia y ordenarlo con su merge_sort. 4. Con dos búsquedas binarias, hallar el rango de términos que empiezan con un prefijo. 5. Devolver los k términos más frecuentes de ese rango, y medir contra revisar todo el archivo en cada consulta.

Preguntas de repaso

  1. Dibujen el árbol de llamadas de mergesort para n = 8. ¿Cuánto trabajo hay por nivel?
  2. ¿Qué entrada hace que un quicksort que siempre elige el primer elemento como pivote cueste Θ(n²)?
  3. ¿Qué significa que un ordenamiento sea estable? Den un ejemplo donde importe.
  4. ¿A qué algoritmos se aplica la cota Ω(n log n)? ¿Qué supone sobre lo que pueden hacer con los datos?
  5. Escriban la invariante de una búsqueda del primer elemento mayor o igual a x, y digan cuándo termina el ciclo.

Código de la semana

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

Ejemplos (12)Errores (4)Pruebas (4)Laboratorio (12)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

bench.hDescargar bench.h
// Medidor de tiempos del curso. Compilar con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H

#include <algorithm>
#include <chrono>
#include <vector>

// Corre fn `reps` veces y devuelve la mediana en segundos.
template <typename Fn>
double median_seconds(int reps, Fn fn) {
    std::vector<double> times;
    times.reserve(reps);
    for (int i = 0; i < reps; i++) {
        auto start = std::chrono::steady_clock::now();
        fn();
        auto stop = std::chrono::steady_clock::now();
        times.push_back(std::chrono::duration<double>(stop - start).count());
    }
    std::sort(times.begin(), times.end());
    return times[times.size() / 2];
}

// Evita que el compilador borre un cálculo cuyo resultado no se usa.
template <typename T>
void keep(const T& value) {
    volatile T sink = value;
    (void)sink;
}

#endif
byte_order.cppDescargar byte_order.cpp
// Qué orden sale al comparar bytes, y por qué un std::string no se confunde con el signo de char.
// Uso: byte_order archivo_de_terminos
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <limits>
#include <string>
#include <vector>

#include "lab/terms_io.h"

namespace {

// Compara char por char. Depende de si char tiene signo en esta máquina.
bool less_by_char(const std::string& a, const std::string& b) {
    std::size_t common = std::min(a.size(), b.size());
    for (std::size_t i = 0; i < common; i++) {
        if (a[i] != b[i]) {
            return a[i] < b[i];
        }
    }
    return a.size() < b.size();
}

void print_words(const char* label, const std::vector<std::string>& words) {
    std::cout << label << ":";
    for (const std::string& word : words) {
        std::cout << " " << word;
    }
    std::cout << "\n";
}

void print_bytes(const std::string& word) {
    std::cout << word << " =";
    for (char c : word) {
        std::printf(" %02x", static_cast<unsigned>(static_cast<unsigned char>(c)));
    }
    std::cout << ", size() = " << word.size() << "\n";
}

// Busca revisando todo el archivo: es lento, y por eso es confiable para este ejemplo.
void scan_prefix(const std::vector<Term>& sorted_terms, const std::string& prefix) {
    std::size_t first = sorted_terms.size();
    std::size_t last = 0;
    std::size_t count = 0;
    for (std::size_t i = 0; i < sorted_terms.size(); i++) {
        if (sorted_terms[i].text.starts_with(prefix)) {
            first = std::min(first, i);
            last = i + 1;
            count++;
        }
    }
    std::printf("%-4s %5zu términos en [%zu, %zu): de %s a %s\n", prefix.c_str(), count, first, last,
                sorted_terms[first].text.c_str(), sorted_terms[last - 1].text.c_str());
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: byte_order archivo_de_terminos\n";
        return 2;
    }
    print_bytes("a");
    print_bytes("á");
    print_bytes("ñ");
    print_bytes("z");

    std::vector<std::string> words = {"zorro", "cámara", "caña", "ñandú", "cazar", "árbol", "cama"};
    std::cout << "char con signo: " << (std::numeric_limits<char>::is_signed ? "sí" : "no") << "\n";
    std::sort(words.begin(), words.end());
    print_words("con std::string", words);
    std::sort(words.begin(), words.end(), less_by_char);
    print_words("char por char ", words);

    std::vector<Term> terms;
    if (!load_terms(argv[1], terms)) {
        std::cerr << "no se pudo leer " << argv[1] << "\n";
        return 1;
    }
    std::sort(terms.begin(), terms.end(), [](const Term& a, const Term& b) { return a.text < b.text; });
    for (const char* prefix : {"a", "á", "ca", "cá", "cañ", "z", "ñ"}) {
        scan_prefix(terms, prefix);
    }
    std::string wanted_low = "ca";
    std::string wanted_high = "cazzzz";
    std::size_t naive = 0;
    for (const Term& term : terms) {
        if (term.text >= wanted_low && term.text <= wanted_high) {
            naive++;
        }
    }
    std::printf("entre ca y cazzzz: %zu términos\n", naive);
    return 0;
}
comparators.cppDescargar comparators.cpp
// Pasarle un comparador a std::sort, de tres maneras.
#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

#include "lab/terms_io.h"

namespace {

bool by_text(const Term& a, const Term& b) {
    return a.text < b.text;
}

void print_terms(const char* label, const std::vector<Term>& terms) {
    std::cout << label << ":";
    for (const Term& term : terms) {
        std::cout << " " << term.text << "(" << term.weight << ")";
    }
    std::cout << "\n";
}

}  // namespace

int main() {
    std::vector<Term> terms = {{"lima", 40}, {"cusco", 90}, {"puno", 40}, {"tacna", 95}, {"arequipa", 90}};

    std::sort(terms.begin(), terms.end(), by_text);
    print_terms("por texto", terms);

    std::sort(terms.begin(), terms.end(), [](const Term& a, const Term& b) {
        if (a.weight != b.weight) {
            return a.weight > b.weight;
        }
        return a.text < b.text;
    });
    print_terms("por frecuencia, y texto en los empates", terms);

    std::vector<int> values = {3, 1, 2};
    std::sort(values.begin(), values.end(), std::greater<int>());
    std::cout << "de mayor a menor: " << values[0] << " " << values[1] << " " << values[2] << "\n";

    // En std::lower_bound, el comparador recibe un elemento y el valor buscado.
    std::sort(terms.begin(), terms.end(), by_text);
    std::string wanted = "lima";
    auto found = std::lower_bound(terms.begin(), terms.end(), wanted,
                                  [](const Term& term, const std::string& x) { return term.text < x; });
    std::cout << "lower_bound de lima: " << found->text << " en la posición " << (found - terms.begin()) << "\n";
    return 0;
}
count_sorts.cppDescargar count_sorts.cpp
// Cuenta comparaciones: no depende de la máquina, y sí permite comparar algoritmos.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <vector>

#include "inputs.h"
#include "int_sorts.h"

namespace {

const unsigned SEED = 2026;

long long count_merge(std::vector<int> items) {
    long long comparisons = 0;
    merge_sort_ints(items, comparisons);
    return comparisons;
}

long long count_first(std::vector<int> items) {
    long long comparisons = 0;
    quick_sort_first_ints(items, comparisons);
    return comparisons;
}

long long count_random(std::vector<int> items) {
    long long comparisons = 0;
    quick_sort_random_ints(items, SEED, comparisons);
    return comparisons;
}

long long count_three(std::vector<int> items) {
    long long comparisons = 0;
    quick_sort_three_ints(items, SEED, comparisons);
    return comparisons;
}

long long count_library(std::vector<int> items) {
    long long comparisons = 0;
    std::sort(items.begin(), items.end(), [&comparisons](int a, int b) {
        comparisons++;
        return a < b;
    });
    return comparisons;
}

long long count_stable(std::vector<int> items) {
    long long comparisons = 0;
    std::stable_sort(items.begin(), items.end(), [&comparisons](int a, int b) {
        comparisons++;
        return a < b;
    });
    return comparisons;
}

void row(const char* name, const std::vector<int>& items) {
    std::printf("%-10s %9lld %9lld %9lld %9lld %9lld %9lld\n", name, count_merge(items),
                count_first(items), count_random(items), count_three(items), count_library(items),
                count_stable(items));
}

}  // namespace

int main() {
    const std::size_t n = 4096;
    std::printf("comparaciones con n = %zu\n", n);
    std::printf("%-10s %9s %9s %9s %9s %9s %9s\n", "entrada", "merge", "q.primero", "q.azar", "q.tres",
                "std::sort", "stable");
    row("al azar", random_input(n, SEED));
    row("ordenada", sorted_input(n));
    row("inversa", reversed_input(n));
    row("10 valores", repeated_input(n, 10, SEED));
    row("iguales", std::vector<int>(n, 7));

    std::printf("\nquicksort con el primero como pivote, entrada ordenada\n");
    std::printf("%6s %10s %12s\n", "n", "comparac.", "n(n-1)/2");
    for (std::size_t size : {1000, 2000, 4000, 8000}) {
        long long formula = static_cast<long long>(size) * (static_cast<long long>(size) - 1) / 2;
        std::printf("%6zu %10lld %12lld\n", size, count_first(sorted_input(size)), formula);
    }

    std::printf("\nquicksort con pivote al azar, entrada al azar: promedio de 20 semillas\n");
    std::printf("%8s %11s %11s %7s\n", "n", "comparac.", "2 n ln n", "razón");
    for (std::size_t size : {1000, 10000, 100000, 1000000}) {
        double total = 0;
        for (unsigned seed = 1; seed <= 20; seed++) {
            std::vector<int> items = random_input(size, seed);
            long long comparisons = 0;
            quick_sort_random_ints(items, seed + 100, comparisons);
            total += static_cast<double>(comparisons);
        }
        double average = total / 20;
        double estimate = 2 * static_cast<double>(size) * std::log(static_cast<double>(size));
        std::printf("%8zu %11.0f %11.0f %7.3f\n", size, average, estimate, average / estimate);
    }
    return 0;
}
inputs.hDescargar inputs.h
// Las entradas con las que se comparan los ordenamientos. Semilla fija: se repiten igual.
#ifndef INPUTS_H
#define INPUTS_H

#include <cstddef>
#include <numeric>
#include <random>
#include <utility>
#include <vector>

inline std::vector<int> sorted_input(std::size_t n) {
    std::vector<int> items(n);
    std::iota(items.begin(), items.end(), 0);
    return items;
}

inline std::vector<int> reversed_input(std::size_t n) {
    std::vector<int> items = sorted_input(n);
    for (std::size_t i = 0; i < n / 2; i++) {
        std::swap(items[i], items[n - 1 - i]);
    }
    return items;
}

// Fisher-Yates: el mismo orden con la misma semilla, en cualquier biblioteca.
inline std::vector<int> random_input(std::size_t n, unsigned seed) {
    std::vector<int> items = sorted_input(n);
    std::mt19937 rng(seed);
    for (std::size_t i = n; i > 1; i--) {
        std::swap(items[i - 1], items[rng() % i]);
    }
    return items;
}

// Solo `distinct` valores distintos, repetidos muchas veces.
inline std::vector<int> repeated_input(std::size_t n, int distinct, unsigned seed) {
    std::vector<int> items(n);
    std::mt19937 rng(seed);
    for (int& value : items) {
        value = static_cast<int>(rng() % static_cast<unsigned>(distinct));
    }
    return items;
}

#endif
int_sorts.hDescargar int_sorts.h
// Los ordenamientos de la clase, solo para enteros. Cuentan comparaciones para poder
// comparar algoritmos con un número que no depende de la máquina.
#ifndef INT_SORTS_H
#define INT_SORTS_H

#include <cstddef>
#include <random>
#include <utility>
#include <vector>

// Mezcla items[lo, mid) con items[mid, hi), que ya están ordenados.
inline void merge_runs(std::vector<int>& items, std::vector<int>& buffer, std::size_t lo,
                       std::size_t mid, std::size_t hi, long long& comparisons) {
    std::size_t left = lo;
    std::size_t right = mid;
    std::size_t out = lo;
    while (left < mid && right < hi) {
        comparisons++;
        if (items[right] < items[left]) {
            buffer[out++] = items[right++];
        } else {
            buffer[out++] = items[left++];
        }
    }
    while (left < mid) {
        buffer[out++] = items[left++];
    }
    while (right < hi) {
        buffer[out++] = items[right++];
    }
    for (std::size_t i = lo; i < hi; i++) {
        items[i] = buffer[i];
    }
}

inline void merge_sort_range(std::vector<int>& items, std::vector<int>& buffer, std::size_t lo,
                             std::size_t hi, long long& comparisons) {
    if (hi - lo < 2) {
        return;
    }
    std::size_t mid = lo + (hi - lo) / 2;
    merge_sort_range(items, buffer, lo, mid, comparisons);
    merge_sort_range(items, buffer, mid, hi, comparisons);
    merge_runs(items, buffer, lo, mid, hi, comparisons);
}

inline void merge_sort_ints(std::vector<int>& items, long long& comparisons) {
    std::vector<int> buffer(items.size());
    merge_sort_range(items, buffer, 0, items.size(), comparisons);
}

// Partición con el primer elemento como pivote. Al terminar, el pivote queda en su lugar
// final, y se devuelve esa posición.
inline std::size_t partition_first(std::vector<int>& items, std::size_t lo, std::size_t hi,
                                   long long& comparisons) {
    int pivot = items[lo];
    std::size_t boundary = lo;
    // Invariante: items(lo, boundary] < pivot, e items(boundary, scan) >= pivot.
    for (std::size_t scan = lo + 1; scan < hi; scan++) {
        comparisons++;
        if (items[scan] < pivot) {
            boundary++;
            std::swap(items[boundary], items[scan]);
        }
    }
    std::swap(items[lo], items[boundary]);
    return boundary;
}

inline void quick_first_range(std::vector<int>& items, std::size_t lo, std::size_t hi,
                              long long& comparisons) {
    if (hi - lo < 2) {
        return;
    }
    std::size_t p = partition_first(items, lo, hi, comparisons);
    quick_first_range(items, lo, p, comparisons);
    quick_first_range(items, p + 1, hi, comparisons);
}

inline void quick_sort_first_ints(std::vector<int>& items, long long& comparisons) {
    quick_first_range(items, 0, items.size(), comparisons);
}

// Lo mismo, pero antes de partir se cambia el primer elemento por uno al azar.
inline void quick_random_range(std::vector<int>& items, std::size_t lo, std::size_t hi,
                               std::mt19937& rng, long long& comparisons) {
    if (hi - lo < 2) {
        return;
    }
    std::uniform_int_distribution<std::size_t> pick(lo, hi - 1);
    std::swap(items[lo], items[pick(rng)]);
    std::size_t p = partition_first(items, lo, hi, comparisons);
    quick_random_range(items, lo, p, rng, comparisons);
    quick_random_range(items, p + 1, hi, rng, comparisons);
}

inline void quick_sort_random_ints(std::vector<int>& items, unsigned seed, long long& comparisons) {
    std::mt19937 rng(seed);
    quick_random_range(items, 0, items.size(), rng, comparisons);
}

// Partición en tres, con pivote al azar. Invariante: items[lo, less_end) < pivot,
// items[less_end, scan) == pivot, items[scan, greater_start) sin revisar, items[greater_start, hi) > pivot.
inline void quick_three_range(std::vector<int>& items, std::size_t lo, std::size_t hi,
                              std::mt19937& rng, long long& comparisons) {
    if (hi - lo < 2) {
        return;
    }
    std::uniform_int_distribution<std::size_t> pick(lo, hi - 1);
    int pivot = items[pick(rng)];
    std::size_t less_end = lo;
    std::size_t scan = lo;
    std::size_t greater_start = hi;
    while (scan < greater_start) {
        comparisons++;
        if (items[scan] < pivot) {
            std::swap(items[less_end], items[scan]);
            less_end++;
            scan++;
            continue;
        }
        comparisons++;
        if (pivot < items[scan]) {
            greater_start--;
            std::swap(items[scan], items[greater_start]);
        } else {
            scan++;
        }
    }
    quick_three_range(items, lo, less_end, rng, comparisons);
    quick_three_range(items, greater_start, hi, rng, comparisons);
}

inline void quick_sort_three_ints(std::vector<int>& items, unsigned seed, long long& comparisons) {
    std::mt19937 rng(seed);
    quick_three_range(items, 0, items.size(), rng, comparisons);
}

// Búsqueda binaria. Devuelve la posición del primer elemento >= x, o size() si no hay.
inline std::size_t lower_bound_index(const std::vector<int>& items, int x, long long& probes) {
    std::size_t lo = 0;
    std::size_t hi = items.size();
    // Invariante: todo en [0, lo) es < x, y todo en [hi, n) es >= x.
    while (lo < hi) {
        std::size_t mid = lo + (hi - lo) / 2;
        probes++;
        if (items[mid] < x) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    return lo;
}

#endif
stability.cppDescargar stability.cpp
// La estabilidad con un ejemplo chico y con el archivo de términos.
// Uso: stability archivo_de_terminos
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include "lab/terms_io.h"

namespace {

struct Exam {
    int score;
    std::string name;
};

// La partición de la clase, sobre registros que se comparan solo por nota.
std::size_t partition_by_score(std::vector<Exam>& items, std::size_t lo, std::size_t hi) {
    int pivot = items[lo].score;
    std::size_t boundary = lo;
    for (std::size_t scan = lo + 1; scan < hi; scan++) {
        if (items[scan].score < pivot) {
            boundary++;
            std::swap(items[boundary], items[scan]);
        }
    }
    std::swap(items[lo], items[boundary]);
    return boundary;
}

void print_exams(const char* label, const std::vector<Exam>& items) {
    std::cout << label;
    for (const Exam& exam : items) {
        std::cout << " " << exam.score << exam.name;
    }
    std::cout << "\n";
}

// Ordena n registros con std::sort por una clave de 0 a 2, y cuenta vecinos empatados invertidos.
int inverted_ties(int n) {
    std::mt19937 rng(1);
    std::vector<Exam> items;
    for (int i = 0; i < n; i++) {
        items.push_back(Exam{static_cast<int>(rng() % 3), std::to_string(100 + i)});
    }
    std::sort(items.begin(), items.end(), [](const Exam& a, const Exam& b) { return a.score < b.score; });
    int inverted = 0;
    for (std::size_t i = 1; i < items.size(); i++) {
        if (items[i - 1].score == items[i].score && items[i - 1].name > items[i].name) {
            inverted++;
        }
    }
    return inverted;
}

std::size_t same_as(const std::vector<Term>& a, const std::vector<Term>& b) {
    std::size_t same = 0;
    for (std::size_t i = 0; i < a.size(); i++) {
        if (a[i].text == b[i].text) {
            same++;
        }
    }
    return same;
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: stability archivo_de_terminos\n";
        return 2;
    }
    std::vector<Exam> exams = {{2, "a"}, {3, "b"}, {3, "c"}, {1, "d"}};
    print_exams("antes:   ", exams);
    partition_by_score(exams, 0, exams.size());
    print_exams("partido: ", exams);
    std::printf("std::sort con 16 elementos, empates invertidos: %d\n", inverted_ties(16));
    std::printf("std::sort con 17 elementos, empates invertidos: %d\n", inverted_ties(17));

    std::vector<Term> file_order;
    if (!load_terms(argv[1], file_order)) {
        std::cerr << "no se pudo leer " << argv[1] << "\n";
        return 1;
    }
    auto by_text = [](const Term& a, const Term& b) { return a.text < b.text; };
    auto by_weight = [](const Term& a, const Term& b) { return a.weight > b.weight; };
    std::vector<Term> sorted_terms = file_order;
    std::sort(sorted_terms.begin(), sorted_terms.end(), by_text);

    std::vector<Term> with_stable = sorted_terms;
    std::stable_sort(with_stable.begin(), with_stable.end(), by_weight);
    std::vector<Term> with_sort = sorted_terms;
    std::sort(with_sort.begin(), with_sort.end(), by_weight);

    std::printf("líneas: %zu\n", file_order.size());
    std::printf("std::stable_sort, iguales al archivo: %zu\n", same_as(with_stable, file_order));
    std::printf("std::sort,        iguales al archivo: %zu\n", same_as(with_sort, file_order));
    return 0;
}
time_sorts.cppDescargar time_sorts.cpp
// Mide los ordenamientos de la clase. Se compila con -O2 y sin sanitizers, con mediana de 5.
// Los tiempos dependen de la máquina.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <vector>

#include "bench.h"
#include "inputs.h"
#include "int_sorts.h"

namespace {

const int REPS = 5;
const unsigned SEED = 2026;

template <typename Sort>
double millis(const std::vector<int>& input, Sort sort) {
    std::vector<std::vector<int>> copies(REPS, input);
    int rep = 0;
    double seconds = median_seconds(REPS, [&] {
        sort(copies[rep]);
        keep(copies[rep][0]);
        rep++;
    });
    return seconds * 1000;
}

void row(const char* name, const std::vector<int>& input) {
    long long ignored = 0;
    std::printf("%-10s %8.2f", name, millis(input, [&](std::vector<int>& v) { merge_sort_ints(v, ignored); }));
    std::printf(" %8.2f", millis(input, [&](std::vector<int>& v) { quick_sort_first_ints(v, ignored); }));
    std::printf(" %8.2f", millis(input, [&](std::vector<int>& v) { quick_sort_random_ints(v, SEED, ignored); }));
    std::printf(" %8.2f", millis(input, [&](std::vector<int>& v) { quick_sort_three_ints(v, SEED, ignored); }));
    std::printf(" %8.2f", millis(input, [](std::vector<int>& v) { std::sort(v.begin(), v.end()); }));
    std::printf(" %8.2f\n", millis(input, [](std::vector<int>& v) { std::stable_sort(v.begin(), v.end()); }));
    keep(ignored);
}

}  // namespace

int main() {
    const std::size_t n = 16000;
    std::printf("milisegundos con n = %zu\n", n);
    std::printf("%-10s %8s %8s %8s %8s %8s %8s\n", "entrada", "merge", "q.primero", "q.azar", "q.tres",
                "std::sort", "stable");
    row("al azar", random_input(n, SEED));
    row("ordenada", sorted_input(n));
    row("10 valores", repeated_input(n, 10, SEED));
    row("iguales", std::vector<int>(n, 7));

    std::printf("\nentrada al azar, milisegundos al duplicar n\n");
    std::printf("%8s %8s %8s %8s %9s %8s\n", "n", "merge", "q.azar", "q.tres", "std::sort", "stable");
    for (std::size_t size : {250000, 500000, 1000000}) {
        std::vector<int> input = random_input(size, SEED);
        long long ignored = 0;
        std::printf("%8zu", size);
        std::printf(" %8.1f", millis(input, [&](std::vector<int>& v) { merge_sort_ints(v, ignored); }));
        std::printf(" %8.1f", millis(input, [&](std::vector<int>& v) { quick_sort_random_ints(v, SEED, ignored); }));
        std::printf(" %8.1f", millis(input, [&](std::vector<int>& v) { quick_sort_three_ints(v, SEED, ignored); }));
        std::printf(" %9.1f", millis(input, [](std::vector<int>& v) { std::sort(v.begin(), v.end()); }));
        std::printf(" %8.1f\n", millis(input, [](std::vector<int>& v) { std::stable_sort(v.begin(), v.end()); }));
        keep(ignored);
    }
    return 0;
}
trace_merge.cppDescargar trace_merge.cpp
// El árbol de llamadas de mergesort para n = 8: primero las divisiones, después las mezclas.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

namespace {

struct Level {
    std::string runs;
    long long comparisons = 0;
    long long copied = 0;
};

std::string show(const std::vector<int>& items, std::size_t lo, std::size_t hi) {
    std::string text = "[";
    for (std::size_t i = lo; i < hi; i++) {
        text += (i == lo ? "" : " ") + std::to_string(items[i]);
    }
    return text + "] ";
}

void sort_range(std::vector<int>& items, std::size_t lo, std::size_t hi, std::size_t depth,
                std::vector<Level>& down, std::vector<Level>& up) {
    if (down.size() <= depth) {
        down.resize(depth + 1);
        up.resize(depth + 1);
    }
    down[depth].runs += show(items, lo, hi);
    if (hi - lo < 2) {
        return;
    }
    std::size_t mid = lo + (hi - lo) / 2;
    sort_range(items, lo, mid, depth + 1, down, up);
    sort_range(items, mid, hi, depth + 1, down, up);

    std::vector<int> merged;
    std::size_t left = lo;
    std::size_t right = mid;
    while (left < mid && right < hi) {
        up[depth].comparisons++;
        merged.push_back(items[right] < items[left] ? items[right++] : items[left++]);
    }
    while (left < mid) {
        merged.push_back(items[left++]);
    }
    while (right < hi) {
        merged.push_back(items[right++]);
    }
    for (std::size_t i = lo; i < hi; i++) {
        items[i] = merged[i - lo];
        up[depth].copied++;
    }
    up[depth].runs += show(items, lo, hi);
}

}  // namespace

int main() {
    std::vector<int> items = {5, 2, 8, 1, 9, 3, 7, 4};
    std::vector<Level> down;
    std::vector<Level> up;
    sort_range(items, 0, items.size(), 0, down, up);

    std::cout << "dividir\n";
    for (std::size_t depth = 0; depth < down.size(); depth++) {
        std::cout << "  nivel " << depth << ": " << down[depth].runs << "\n";
    }
    std::cout << "mezclar\n";
    for (std::size_t depth = up.size() - 1; depth-- > 0;) {
        std::cout << "  nivel " << depth << ": " << up[depth].runs << "\n";
        std::cout << "           " << up[depth].comparisons << " comparaciones, "
                  << up[depth].copied << " elementos copiados\n";
    }
    return 0;
}
trace_partition.cppDescargar trace_partition.cpp
// Sigue paso a paso las dos particiones de la clase.
#include <cstddef>
#include <iostream>
#include <utility>
#include <vector>

namespace {

void print_items(const std::vector<int>& items) {
    for (std::size_t i = 0; i < items.size(); i++) {
        std::cout << (i == 0 ? "" : " ") << items[i];
    }
}

void trace_first(std::vector<int> items) {
    std::cout << "pivote = primer elemento = " << items[0] << "\n";
    int pivot = items[0];
    std::size_t boundary = 0;
    for (std::size_t scan = 1; scan < items.size(); scan++) {
        if (items[scan] < pivot) {
            boundary++;
            std::swap(items[boundary], items[scan]);
        }
        std::cout << "  scan=" << scan << " boundary=" << boundary << "  ";
        print_items(items);
        std::cout << "\n";
    }
    std::swap(items[0], items[boundary]);
    std::cout << "  pivote a su lugar ";
    print_items(items);
    std::cout << "  (posición " << boundary << ")\n";
}

void trace_three(std::vector<int> items, int pivot) {
    std::cout << "pivote = " << pivot << "\n";
    std::size_t less_end = 0;
    std::size_t scan = 0;
    std::size_t greater_start = items.size();
    while (scan < greater_start) {
        if (items[scan] < pivot) {
            std::swap(items[less_end], items[scan]);
            less_end++;
            scan++;
        } else if (pivot < items[scan]) {
            greater_start--;
            std::swap(items[scan], items[greater_start]);
        } else {
            scan++;
        }
        std::cout << "  menores_fin=" << less_end << " scan=" << scan << " mayores_inicio="
                  << greater_start << "  ";
        print_items(items);
        std::cout << "\n";
    }
}

}  // namespace

int main() {
    trace_first({5, 2, 8, 1, 9, 3, 7, 4});
    trace_three({3, 5, 3, 1, 3, 2, 5, 3}, 3);
    return 0;
}
trace_search.cppDescargar trace_search.cpp
// Sigue la búsqueda binaria del primer elemento mayor o igual a x.
#include <cstddef>
#include <iostream>
#include <vector>

namespace {

void trace(const std::vector<int>& items, int x) {
    std::cout << "x = " << x << "\n";
    std::size_t lo = 0;
    std::size_t hi = items.size();
    while (lo < hi) {
        std::size_t mid = lo + (hi - lo) / 2;
        std::cout << "  lo=" << lo << " hi=" << hi << " mid=" << mid << " items[mid]=" << items[mid]
                  << (items[mid] < x ? " < x" : " >= x") << "\n";
        if (items[mid] < x) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    std::cout << "  termina con lo=hi=" << lo << "\n";
}

}  // namespace

int main() {
    std::vector<int> items = {2, 3, 3, 5, 8, 8, 9};
    trace(items, 8);
    trace(items, 4);
    trace(items, 10);
    return 0;
}
word_search.cppDescargar word_search.cpp
// Busca palabras en el diccionario con std::lower_bound y cuenta las comparaciones.
// Uso: word_search archivo_de_palabras palabra...
#include <algorithm>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

int main(int argc, char** argv) {
    if (argc < 3) {
        std::cerr << "uso: word_search archivo_de_palabras palabra...\n";
        return 2;
    }
    std::ifstream file(argv[1]);
    std::vector<std::string> words;
    std::string line;
    while (std::getline(file, line)) {
        words.push_back(line);
    }
    if (words.empty()) {
        std::cerr << "no se pudo leer " << argv[1] << "\n";
        return 1;
    }
    std::printf("%zu palabras; ordenado por bytes: %s\n", words.size(),
                std::is_sorted(words.begin(), words.end()) ? "sí" : "no");
    for (int i = 2; i < argc; i++) {
        std::string wanted = argv[i];
        long long comparisons = 0;
        auto found = std::lower_bound(words.begin(), words.end(), wanted,
                                      [&comparisons](const std::string& word, const std::string& x) {
                                          comparisons++;
                                          return word < x;
                                      });
        bool present = found != words.end() && *found == wanted;
        std::printf("%-10s %2lld comparaciones, %s", wanted.c_str(), comparisons, present ? "está" : "no está");
        if (!present && found != words.end()) {
            std::printf("; la siguiente es %s", found->c_str());
        }
        std::printf("\n");
    }
    return 0;
}

Errores

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

errors/bad_comparator.cppDescargar errors/bad_comparator.cpp
// Error a propósito: <= no es un orden estricto, y std::sort exige uno.
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items(100, 7);
    std::sort(items.begin(), items.end(), [](int a, int b) { return a <= b; });
    std::cout << "terminó\n";
    return 0;
}
errors/deep_recursion.cppDescargar errors/deep_recursion.cpp
// Error a propósito: con el primero como pivote, una entrada ordenada recurre n niveles.
// Uso: deep_recursion n
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <utility>
#include <vector>

namespace {

void quick_first(std::vector<int>& items, std::size_t lo, std::size_t hi) {
    if (hi - lo < 2) {
        return;
    }
    int pivot = items[lo];
    std::size_t boundary = lo;
    for (std::size_t scan = lo + 1; scan < hi; scan++) {
        if (items[scan] < pivot) {
            boundary++;
            std::swap(items[boundary], items[scan]);
        }
    }
    std::swap(items[lo], items[boundary]);
    quick_first(items, lo, boundary);
    quick_first(items, boundary + 1, hi);
}

}  // namespace

int main(int argc, char** argv) {
    std::size_t n = argc > 1 ? static_cast<std::size_t>(std::atol(argv[1])) : 1000;
    std::vector<int> items(n);
    for (std::size_t i = 0; i < n; i++) {
        items[i] = static_cast<int>(i);
    }
    quick_first(items, 0, n);
    std::cout << "ordenó " << n << " elementos\n";
    return 0;
}
errors/midpoint_overflow.cppDescargar errors/midpoint_overflow.cpp
// Error a propósito: (lo + hi) / 2 con índices int hace overflow cuando el rango es grande.
#include <iostream>

namespace {

// Un arreglo enorme que no existe en memoria: el elemento i vale i.
bool is_at_least(int i, int x) {
    return i >= x;
}

}  // namespace

int main() {
    int lo = 0;
    int hi = 2000000000;
    int x = 1500000000;
    while (lo < hi) {
        int mid = (lo + hi) / 2;
        if (is_at_least(mid, x)) {
            hi = mid;
        } else {
            lo = mid + 1;
        }
    }
    std::cout << "primero >= x: " << lo << "\n";
    return 0;
}
errors/stuck_search.cppDescargar errors/stuck_search.cpp
// Error a propósito: con lo = mid el rango deja de reducirse y el ciclo no termina.
#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> items = {2, 3, 5, 8};
    int x = 9;
    std::size_t lo = 0;
    std::size_t hi = items.size();
    long long steps = 0;
    while (lo < hi) {
        std::size_t mid = lo + (hi - lo) / 2;
        if (steps < 4) {
            std::cout << "lo=" << lo << " hi=" << hi << " mid=" << mid << std::endl;
        }
        steps++;
        if (items[mid] < x) {
            lo = mid;
        } else {
            hi = mid;
        }
    }
    std::cout << "termina en " << lo << "\n";
    return 0;
}

Pruebas

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

test_examples.cppDescargar test_examples.cpp
// Pruebas de los ordenamientos de la clase: casos dirigidos y al azar, contra la biblioteca.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <iostream>
#include <random>
#include <vector>

#include "inputs.h"
#include "int_sorts.h"

namespace {

void check_all_sorts(const std::vector<int>& input) {
    std::vector<int> expected = input;
    std::sort(expected.begin(), expected.end());
    long long comparisons = 0;

    std::vector<int> items = input;
    merge_sort_ints(items, comparisons);
    assert(items == expected);

    items = input;
    quick_sort_first_ints(items, comparisons);
    assert(items == expected);

    items = input;
    quick_sort_random_ints(items, 1, comparisons);
    assert(items == expected);

    items = input;
    quick_sort_three_ints(items, 1, comparisons);
    assert(items == expected);
}

void test_small_cases() {
    check_all_sorts({});
    check_all_sorts({1});
    check_all_sorts({2, 1});
    check_all_sorts({1, 1, 1});
    check_all_sorts({3, 1, 2, 3, 1, 2});
}

void test_random_inputs() {
    std::mt19937 rng(2026);
    for (int round = 0; round < 300; round++) {
        std::size_t n = rng() % 200;
        check_all_sorts(repeated_input(n, 1 + static_cast<int>(rng() % 50), rng()));
    }
}

void test_exact_counts() {
    // Con el primero como pivote, cada partición saca un solo elemento.
    for (std::size_t n : {10, 100, 1000}) {
        long long expected = static_cast<long long>(n * (n - 1) / 2);
        long long comparisons = 0;
        std::vector<int> items = sorted_input(n);
        quick_sort_first_ints(items, comparisons);
        assert(comparisons == expected);
        comparisons = 0;
        items = std::vector<int>(n, 7);
        quick_sort_first_ints(items, comparisons);
        assert(comparisons == expected);
    }
    // Mergesort con n = 2^k: como mucho n log2 n comparaciones.
    for (int k = 1; k <= 14; k++) {
        std::size_t n = std::size_t{1} << k;
        long long comparisons = 0;
        std::vector<int> items = random_input(n, 7);
        merge_sort_ints(items, comparisons);
        assert(comparisons <= static_cast<long long>(n) * k);
    }
    // La partición en tres con todos iguales: una pasada, dos comparaciones por elemento.
    long long comparisons = 0;
    std::vector<int> items(1000, 7);
    quick_sort_three_ints(items, 1, comparisons);
    assert(comparisons == 2000);
}

void test_lower_bound() {
    std::mt19937 rng(2026);
    for (int round = 0; round < 2000; round++) {
        std::size_t n = rng() % 64;
        std::vector<int> items = repeated_input(n, 1 + static_cast<int>(n), rng());
        std::sort(items.begin(), items.end());
        for (int x = -1; x <= static_cast<int>(n) + 1; x++) {
            long long probes = 0;
            std::size_t found = lower_bound_index(items, x, probes);
            std::size_t expected = static_cast<std::size_t>(std::lower_bound(items.begin(), items.end(), x) - items.begin());
            assert(found == expected);
            // Se revisan a lo sumo piso(log2 n) + 1 elementos.
            long long limit = 0;
            for (std::size_t size = n; size > 0; size /= 2) {
                limit++;
            }
            assert(probes <= limit);
        }
    }
}

}  // namespace

int main() {
    test_small_cases();
    test_random_inputs();
    test_exact_counts();
    test_lower_bound();
    std::cout << "ok\n";
    return 0;
}
test_examples.pyDescargar test_examples.py
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos."""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
TERMS = f"{DATA}/terminos.tsv"
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]


def run(*command):
    return subprocess.run(
        list(command), cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )


def shell(command):
    return run("bash", "-c", command)


class Examples(unittest.TestCase):
    def test_sorts_and_search_against_library(self):
        done = run("build/test_examples")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_programs_exit_cleanly(self):
        for name in ["trace_merge", "trace_partition", "trace_search", "comparators"]:
            with self.subTest(program=name):
                self.assertEqual(run(f"build/{name}").returncode, 0)

    def test_merge_tree_for_eight(self):
        """Cada nivel de mezclas copia exactamente 8 elementos."""
        output = run("build/trace_merge").stdout
        self.assertEqual(re.findall(r"(\d+) elementos copiados", output), ["8", "8", "8"])
        self.assertEqual(re.findall(r"(\d+) comparaciones", output), ["4", "6", "7"])

    def test_comparison_counts(self):
        lines = run("build/count_sorts").stdout.splitlines()
        rows = {line.split(maxsplit=1)[0]: line.split() for line in lines[2:7]}
        self.assertEqual(rows["ordenada"][2], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][2], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][3], str(4096 * 4095 // 2))
        self.assertEqual(rows["iguales"][4], str(2 * 4096))
        # En todas las entradas, mergesort hace a lo sumo n log2 n comparaciones.
        for name in ["ordenada", "inversa", "iguales"]:
            self.assertLessEqual(int(rows[name][1]), 4096 * 12)

    def test_stability_rebuilds_the_file(self):
        lines = run("build/stability", TERMS).stdout.splitlines()
        self.assertIn("partido:  1d 2a 3c 3b", lines)
        self.assertIn("líneas: 28039", lines)
        self.assertIn("std::sort con 16 elementos, empates invertidos: 0", lines)
        self.assertNotIn("std::sort con 17 elementos, empates invertidos: 0", lines)
        self.assertIn("std::stable_sort, iguales al archivo: 28039", lines)
        kept = next(line for line in lines if line.startswith("std::sort,"))
        self.assertLess(int(kept.split()[-1]), 28039)

    def test_byte_order_does_not_depend_on_the_sign_of_char(self):
        signed = run("build/byte_order_signed", TERMS).stdout.splitlines()
        unsigned = run("build/byte_order_unsigned", TERMS).stdout.splitlines()
        by_string = "con std::string: cama cazar caña cámara zorro árbol ñandú"
        self.assertIn(by_string, signed)
        self.assertIn(by_string, unsigned)
        self.assertIn("char con signo: sí", signed)
        self.assertIn("char con signo: no", unsigned)
        self.assertIn("char por char : árbol ñandú cámara caña cama cazar zorro", signed)
        self.assertIn("char por char : cama cazar caña cámara zorro árbol ñandú", unsigned)

    def test_binary_search_on_the_dictionary(self):
        lines = run("build/word_search", f"{DATA}/palabras.txt", "casa", "rodillas").stdout.splitlines()
        self.assertEqual(lines[0], "1045757 palabras; ordenado por bytes: sí")
        for line in lines[1:]:
            self.assertLessEqual(int(line.split()[1]), 21)


class DeliberateErrors(unittest.TestCase):
    def build(self, name):
        binary = f"build/{name}"
        done = run(*SAN, f"errors/{name}.cpp", "-o", binary)
        self.assertEqual(done.returncode, 0, done.stdout)
        return binary

    def test_midpoint_overflow(self):
        binary = self.build("midpoint_overflow")
        done = run("timeout", "2", binary)
        self.assertIn("signed integer overflow", done.stdout)

    def test_stuck_search(self):
        binary = self.build("stuck_search")
        self.assertEqual(run("timeout", "2", binary).returncode, 124)

    def test_bad_comparator(self):
        done = run(self.build("bad_comparator"))
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("heap-buffer-overflow", done.stdout)

    def test_deep_recursion(self):
        binary = self.build("deep_recursion")
        done = shell(f"ulimit -s 1024; {binary} 20000")
        self.assertNotEqual(done.returncode, 0)
        self.assertIn("stack-overflow", done.stdout)
        self.assertEqual(run(binary, "2000").returncode, 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.

Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido
la hace fallar aunque el resultado parezca correcto. Las pruebas cuentan comparaciones y
lecturas, nunca tiempos.
"""
import os
import pathlib
import re
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = os.environ.get("DATA", "../../data")
BINARY = HERE / "build" / LAB_DIR / "lab_tests"
SIGNED = HERE / "build" / LAB_DIR / "lab_tests_signed"
FIXTURE = "lab/tests/fixtures/terminos_chicos.tsv"
TERMS = f"{DATA}/terminos.tsv"

SORTS = [
    "merge_small", "merge_random", "merge_strings", "merge_uses_comparator", "merge_is_stable",
    "merge_does_not_copy", "merge_counts", "quick_small", "quick_random", "quick_strings",
    "quick_uses_comparator", "quick_sorted_input_is_fast", "quick_all_equal_is_one_pass",
    "quick_few_values_is_linear", "quick_copies_only_pivots", "slow_input_is_slow",
]
AUTOCOMPLETE = [
    "sort_is_byte_order", "sort_real_file", "prefix_ascii", "prefix_whole_term_is_included",
    "prefix_accents_are_other_bytes", "prefix_empty_is_everything", "prefix_absent",
    "prefix_extreme_bytes", "prefix_real_counts", "prefix_matches_oracle", "prefix_reads_few_terms",
    "top_k_order_and_ties", "top_k_edges", "top_k_keeps_weights", "top_k_real_file_order",
    "top_k_matches_scan", "top_k_reads_only_the_range", "scan_reads_the_whole_file",
]


def short_report(text):
    """Deja lo útil de un informe: el tipo de error y las líneas de sus archivos."""
    if "ERROR:" not in text and "runtime error" not in text:
        return text.strip()
    useful = [line.strip() for line in text.splitlines()
              if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
              or re.search(r"(sorts|autocomplete|lab_tests)\.(h|cpp):\d+", line)]
    return "\n".join(useful[:12])


def build(target):
    done = subprocess.run(
        ["make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", f"build/{LAB_DIR}/{target}"],
        cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.returncode, done.stdout


def run_case(testcase, binary, name):
    done = subprocess.run(
        [str(binary), name, FIXTURE, TERMS], cwd=HERE, text=True, encoding="utf-8", errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30,
    )
    testcase.assertEqual(done.returncode, 0, short_report(done.stdout))


def add_cases(cls, binary, names):
    for name in names:
        def case(self, name=name):
            run_case(self, binary, name)
        setattr(cls, f"test_{name}", case)
    return cls


class LabCase(unittest.TestCase):
    target = "lab_tests"

    @classmethod
    def setUpClass(cls):
        code, output = build(cls.target)
        if code != 0:
            raise AssertionError(output)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class Builds(unittest.TestCase):
    def test_builds_without_warnings(self):
        BINARY.unlink(missing_ok=True)
        code, output = build("lab_tests")
        self.assertEqual(code, 0, output)
        # make avisa a veces de relojes desfasados en carpetas compartidas: eso no cuenta.
        warnings = [line for line in output.splitlines() if "warning" in line and not line.startswith("make")]
        self.assertEqual(warnings, [])


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestSorts(LabCase):
    pass


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestAutocomplete(LabCase):
    pass


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestSignedChar(LabCase):
    """El orden por bytes no debe depender de si char tiene signo en la máquina."""
    target = "lab_tests_signed"


add_cases(TestSorts, BINARY, SORTS)
add_cases(TestAutocomplete, BINARY, AUTOCOMPLETE)
add_cases(TestSignedChar, SIGNED, AUTOCOMPLETE)


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 os
import pathlib
import platform
import re
import subprocess
import unittest

try:
    import pty
except ImportError:
    pty = None

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-06.md"
SLIDES = ROOT / "slides" / "week-06.tex"
# Antes de un bloque cuya salida cambia con el sistema. Se corre, pero no se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe de ASan. Puede omitir líneas, pero no cambiarlas ni reordenarlas.
# Las direcciones y los números de proceso no se comparan.
REPORT = "<!-- informe recortado -->"
# Los comandos de los apuntes usan la ruta por defecto; aquí se respeta DATA si la cambiaron.
DATA = os.environ.get("DATA", "../../data")
# Los informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"


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


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


def run_in_terminal(command):
    """Corre el comando en una terminal de verdad: así se intercala la salida del programa
    con el informe, igual que cuando ustedes lo corren."""
    master, slave = pty.openpty()
    process = subprocess.Popen(
        ["bash", "-c", command.replace("../../data", DATA)], cwd=HERE, stdin=subprocess.DEVNULL, stdout=slave, stderr=slave,
    )
    os.close(slave)
    data = b""
    while True:
        try:
            chunk = os.read(master, 4096)
        except OSError:
            break
        if not chunk:
            break
        data += chunk
    os.close(master)
    process.wait(timeout=120)
    text = data.decode("utf-8", errors="replace").replace("\r", "")
    return normalize(re.sub(r"\x1b\[[0-9;]*[mK]", "", text))


def clean_lines(text):
    return [line.rstrip() for line in text.splitlines()]


def stable(line):
    """Quita de una línea de informe lo que cambia en cada corrida."""
    line = re.sub(r"0x[0-9a-f]+", "ADDR", line.strip())
    return re.sub(r"==\d+==", "==N==", line)


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):
    """Devuelve (líneas, marca) de cada bloque de los apuntes."""
    blocks, current, mark, previous = [], None, "", ""
    for raw in NOTES.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^> ?", "", raw)
        if current is None:
            if line.strip() == "```" + language:
                current, mark = [], previous.strip()
            elif line.strip():
                previous = line
        elif line.strip() == "```":
            blocks.append((current, mark))
            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()
    sources = list(HERE.glob("*.cpp")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.cpp"))
    sources += list((HERE / "lab").glob("*.h")) + list((HERE / "lab" / "tests").glob("*.*"))
    for source in sources:
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class ReportCheck(unittest.TestCase):
    def assert_in_order(self, command, expected, actual):
        """Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
        wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
        real = [stable(line) for line in actual]
        position = 0
        for line in wanted:
            with self.subTest(command=command, line=line):
                found = [i for i in range(position, len(real)) if real[i] == line]
                self.assertTrue(found, f"no aparece en la salida real: {line}")
                if found:
                    position = found[0] + 1


class NotesAreTrue(ReportCheck):
    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_terminal_blocks_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if not lines or not lines[0].startswith("$ ") or mark == REPORT:
                continue
            for command, expected in split_commands(lines):
                actual = clean_lines(run_shell(command))
                checked += 1
                if mark != SYSTEM_DEPENDENT:
                    with self.subTest(command=command):
                        self.assertEqual(actual, expected)
        self.assertGreater(checked, 5)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    @unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
    def test_reports_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark != REPORT:
                continue
            for command, expected in split_commands(lines):
                actual = clean_lines(run_in_terminal(command))
                self.assert_in_order(command, expected, actual)
                checked += 1
        self.assertGreaterEqual(checked, 2)

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


class SlidesAreTrue(ReportCheck):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_program_output_in_slides(self):
        for block in slide_blocks("terminal|salida"):
            for command, expected in split_commands(block):
                self.assert_in_order(command, expected, clean_lines(run_shell(command)))

    # Algunos informes usan programas que compiló la prueba de los apuntes, que corre antes.
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    @unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
    def test_reports_in_slides(self):
        for block in slide_blocks("informe"):
            for command, expected in split_commands(block):
                self.assert_in_order(command, expected, clean_lines(run_in_terminal(command)))

    @unittest.skipUnless(NOTES.exists() and SLIDES.exists(), "faltan los apuntes o las diapositivas")
    def test_measurements_in_slides_come_from_notes(self):
        """Los tiempos dependen de la máquina: las diapositivas copian los de los apuntes."""
        notes = NOTES.read_text(encoding="utf-8").splitlines()
        blocks = slide_blocks("medicion")
        self.assertGreaterEqual(len(blocks), 2)
        for block in blocks:
            for line in block:
                with self.subTest(line=line):
                    self.assertIn(line, 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/autocomplete.hDescargar lab/autocomplete.h
// Sesión 2: autocompletado sobre el archivo de términos.
// Los templates sobre Terms aceptan cualquier contenedor con size() y operator[]: las
// pruebas cuentan cuántos términos leen. Por eso recorran con índices, no con iteradores.
#ifndef AUTOCOMPLETE_H
#define AUTOCOMPLETE_H

#include <cstddef>
#include <string>
#include <vector>

#include "not_implemented.h"
#include "sorts.h"
#include "terms_io.h"

// Los términos cuyos bytes empiezan con el prefijo son terms[first, last).
struct Range {
    std::size_t first;
    std::size_t last;
};

// Ordena por el texto, comparando bytes como lo hace std::string, con su merge_sort.
inline void sort_by_text(std::vector<Term>& terms) {
    (void)terms;
    not_implemented("sort_by_text");
}

// terms está ordenado por texto. Devuelve el rango de los términos que empiezan con prefix
// usando dos búsquedas binarias. El prefijo vacío da todo el arreglo. Si ninguno empieza
// así, first == last, y los dos señalan dónde estaría el prefijo.
//
// "Empezar con" se refiere a bytes: "ca" no incluye a "cámara", cuyo segundo byte no es 'a'.
template <typename Terms>
Range prefix_range(const Terms& terms, const std::string& prefix) {
    (void)terms;
    (void)prefix;
    not_implemented("prefix_range");
}

// Los k términos de terms[range.first, range.last) con mayor peso, de mayor a menor.
// Los empates van por texto, de menor a mayor. Si hay menos de k, devuelve los que haya.
// Solo puede leer los términos del rango. Piensen en qué ordenamiento les conviene.
template <typename Terms>
std::vector<Term> top_k(const Terms& terms, Range range, std::size_t k) {
    (void)terms;
    (void)range;
    (void)k;
    not_implemented("top_k");
}

// Lo mismo sin búsqueda binaria: revisa todos los términos en cada consulta, sin cortar
// antes de tiempo, y usa std::string::starts_with. Es la línea de base contra la que se
// mide, y las pruebas la usan como oráculo.
template <typename Terms>
std::vector<Term> top_k_scan(const Terms& terms, const std::string& prefix, std::size_t k) {
    (void)terms;
    (void)prefix;
    (void)k;
    not_implemented("top_k_scan");
}

#endif
lab/fixed_pivot.hDescargar lab/fixed_pivot.h
// Quicksort con el primer elemento como pivote. Ya viene hecho: es el que van a hacer
// sufrir en la sesión 1. No lo usen para otra cosa.
#ifndef FIXED_PIVOT_H
#define FIXED_PIVOT_H

#include <cstddef>
#include <utility>
#include <vector>

template <typename T, typename Less>
void quick_first_range(std::vector<T>& items, std::size_t lo, std::size_t hi, Less& less) {
    if (hi - lo < 2) {
        return;
    }
    std::size_t boundary = lo;
    for (std::size_t scan = lo + 1; scan < hi; scan++) {
        if (less(items[scan], items[lo])) {
            boundary++;
            std::swap(items[boundary], items[scan]);
        }
    }
    std::swap(items[lo], items[boundary]);
    quick_first_range(items, lo, boundary, less);
    quick_first_range(items, boundary + 1, hi, less);
}

template <typename T, typename Less>
void quick_sort_first(std::vector<T>& items, Less less) {
    quick_first_range(items, 0, items.size(), less);
}

#endif
lab/not_implemented.hDescargar lab/not_implemented.h
// Para los métodos que todavía no escriben: detiene el programa y dice cuál falta.
#ifndef NOT_IMPLEMENTED_H
#define NOT_IMPLEMENTED_H

#include <cstdio>
#include <cstdlib>

[[noreturn]] inline void not_implemented(const char* name) {
    std::fprintf(stderr, "sin implementar: %s\n", name);
    std::abort();
}

#endif
lab/report.mdDescargar lab/report.md
# Informe del laboratorio

Respondan con sus propios números. Digan qué midieron y qué supusieron.

## Cómo midieron

Máquina, cómo compilaron, cuántas repeticiones y cuántas corridas.

## Sesión 1: ordenamientos

Peguen la tabla de `make bench-sorts`.

- ¿Qué entrada eligieron en `slow_input` y cómo se ve su efecto en comparaciones y en tiempo?
- Al duplicar n, ¿cuánto crece cada ordenamiento con cada entrada? ¿Coincide con lo que predice la teoría?
- Con muchos repetidos, ¿qué le pasa a `quick_sort` frente a `merge_sort`? Usen los conteos.

## Sesión 2: consultas

Peguen la tabla de `make bench-query`.

- ¿Cuántos términos lee cada método en cada consulta?
- ¿Con qué prefijos gana más la búsqueda binaria, y con cuáles gana poco? ¿Qué parte de la consulta explica la diferencia?
- Corran `build/lab/autocomplete ../../data/terminos.tsv ca 5`, y luego con `cá`, `a` y `á`. ¿En cuál aparece `cámara`? ¿En cuál `árbol`? ¿Qué le falta a este autocompletado para un usuario real?
lab/sorts.hDescargar lab/sorts.h
// Sesión 1: sus ordenamientos. Las pruebas están en lab/tests/lab_tests.cpp.
#ifndef SORTS_H
#define SORTS_H

#include <cstddef>
#include <vector>

#include "not_implemented.h"

// Ordena items según less, de modo que nunca quede less(items[i + 1], items[i]).
// less(a, b) dice si a va antes que b, y es un orden estricto, como el < de los enteros.
//
// Debe ser estable: los elementos equivalentes conservan su orden original.
// Debe mover y no copiar: T puede no tener constructor por defecto ni ser barato de copiar.
// Hagan el buffer con reserve y push_back, y usen std::move.
// Pueden escribir las funciones auxiliares que quieran.
template <typename T, typename Less>
void merge_sort(std::vector<T>& items, Less less) {
    (void)items;
    (void)less;
    not_implemented("merge_sort");
}

// Quicksort con pivote al azar y partición en tres: menores, equivalentes y mayores que el
// pivote. Con todos los elementos iguales debe hacer una sola pasada.
//
// Usen std::mt19937 con esta semilla. Se permite copiar el pivote, y ningún otro elemento.
template <typename T, typename Less>
void quick_sort(std::vector<T>& items, Less less, unsigned seed = 2026) {
    (void)items;
    (void)less;
    (void)seed;
    not_implemented("quick_sort");
}

// Una entrada de n enteros que haga que quick_sort_first (fixed_pivot.h) haga al menos
// n * n / 4 comparaciones. make bench-sorts la mide.
inline std::vector<int> slow_input(std::size_t n) {
    (void)n;
    not_implemented("slow_input");
}

#endif
lab/terms_io.hDescargar lab/terms_io.h
// Carga el archivo de términos: una línea por término, frecuencia<TAB>término.
// Es la parte de entrada del laboratorio y ya viene hecha.
#ifndef TERMS_IO_H
#define TERMS_IO_H

#include <cstddef>
#include <fstream>
#include <string>
#include <vector>

struct Term {
    std::string text;
    long long weight;
};

// Agrega a terms las líneas del archivo. Devuelve false si no se puede abrir o si una
// línea no tiene la forma esperada.
inline bool load_terms(const std::string& path, std::vector<Term>& terms) {
    std::ifstream file(path);
    if (!file) {
        return false;
    }
    std::string line;
    while (std::getline(file, line)) {
        std::size_t tab = line.find('\t');
        if (tab == std::string::npos || tab == 0 || tab + 1 == line.size()) {
            return false;
        }
        long long weight = 0;
        for (std::size_t i = 0; i < tab; i++) {
            if (line[i] < '0' || line[i] > '9') {
                return false;
            }
            weight = weight * 10 + (line[i] - '0');
        }
        terms.push_back(Term{line.substr(tab + 1), weight});
    }
    return true;
}

#endif
lab/tests/autocomplete_main.cppDescargar lab/tests/autocomplete_main.cpp
// Prueba de mano: autocomplete archivo_de_terminos prefijo k
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <vector>

#include "autocomplete.h"

int main(int argc, char** argv) {
    if (argc != 4) {
        std::cerr << "uso: autocomplete archivo_de_terminos prefijo k\n";
        return 2;
    }
    std::vector<Term> terms;
    if (!load_terms(argv[1], terms)) {
        std::cerr << "no se pudo leer " << argv[1] << "\n";
        return 1;
    }
    sort_by_text(terms);
    Range range = prefix_range(terms, argv[2]);
    std::printf("%zu términos empiezan con \"%s\"\n", range.last - range.first, argv[2]);
    for (const Term& term : top_k(terms, range, static_cast<std::size_t>(std::atol(argv[3])))) {
        std::printf("%7lld  %s\n", term.weight, term.text.c_str());
    }
    return 0;
}
lab/tests/bench_query.cppDescargar lab/tests/bench_query.cpp
// Sesión 2: compara las dos maneras de responder una consulta, en términos leídos y en
// tiempo. Con -O2, sin sanitizers y con mediana de 5. Los tiempos dependen de la máquina.
// Uso: bench_query archivo_de_terminos
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <random>
#include <string>
#include <vector>

#include "autocomplete.h"
#include "bench.h"

namespace {

const int REPS = 5;
const int QUERIES_PER_REP = 200;
const std::size_t K = 5;

// Lee como un vector, pero cuenta las lecturas.
class CountingTerms {
public:
    explicit CountingTerms(const std::vector<Term>& terms) : terms_(terms) {}
    std::size_t size() const { return terms_.size(); }
    const Term& operator[](std::size_t i) const {
        reads_++;
        return terms_[i];
    }
    long long reads() const { return reads_; }

private:
    const std::vector<Term>& terms_;
    mutable long long reads_ = 0;
};

// Microsegundos por consulta.
template <typename Query>
double micros(Query query) {
    double seconds = median_seconds(REPS, [&] {
        for (int i = 0; i < QUERIES_PER_REP; i++) {
            query();
        }
    });
    return seconds * 1e6 / QUERIES_PER_REP;
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: bench_query archivo_de_terminos\n";
        return 2;
    }
    std::vector<Term> terms;
    if (!load_terms(argv[1], terms)) {
        std::cerr << "no se pudo leer " << argv[1] << "\n";
        return 1;
    }
    sort_by_text(terms);
    std::printf("%zu términos, k = %zu\n", terms.size(), K);
    std::printf("%-8s %6s %10s %10s %9s %9s %9s %8s\n", "prefijo", "rango", "lee rango", "lee todo", "rango us",
                "top us", "todo us", "todo/bin");
    for (const char* prefix : {"", "a", "de", "ca", "cas", "casa", "cañ", "zzz"}) {
        Range range = prefix_range(terms, prefix);
        CountingTerms counting(terms);
        Range counted = prefix_range(counting, prefix);
        top_k(counting, counted, K);
        long long reads_fast = counting.reads();
        CountingTerms counting_all(terms);
        top_k_scan(counting_all, prefix, K);

        double range_us = micros([&] { keep(prefix_range(terms, prefix).first); });
        double top_us = micros([&] { keep(top_k(terms, range, K).size()); });
        double scan_us = micros([&] { keep(top_k_scan(terms, prefix, K).size()); });
        std::printf("%-8s %6zu %10lld %10lld %9.2f %9.2f %9.2f %8.1f\n", prefix[0] == '\0' ? "(vacío)" : prefix,
                    range.last - range.first, reads_fast, counting_all.reads(), range_us, top_us, scan_us,
                    scan_us / (range_us + top_us));
    }
    return 0;
}
lab/tests/bench_sorts.cppDescargar lab/tests/bench_sorts.cpp
// Sesión 1: mide sus ordenamientos, en comparaciones y en tiempo. Con -O2, sin sanitizers y
// con mediana de 5. Los tiempos dependen de la máquina; las comparaciones, casi no.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <random>
#include <vector>

#include "bench.h"
#include "fixed_pivot.h"
#include "sorts.h"

namespace {

const int REPS = 5;

std::vector<int> random_values(std::size_t n, unsigned range) {
    std::mt19937 rng(2026);
    std::vector<int> items(n);
    for (int& value : items) {
        value = static_cast<int>(rng() % range);
    }
    return items;
}

std::vector<int> sorted_values(std::size_t n) {
    std::vector<int> items(n);
    for (std::size_t i = 0; i < n; i++) {
        items[i] = static_cast<int>(i);
    }
    return items;
}

template <typename Sort>
void cell(const std::vector<int>& input, Sort sort) {
    long long comparisons = 0;
    auto counting = [&comparisons](int a, int b) {
        comparisons++;
        return a < b;
    };
    std::vector<int> once = input;
    sort(once, counting);
    std::vector<std::vector<int>> copies(REPS, input);
    int rep = 0;
    double seconds = median_seconds(REPS, [&] {
        sort(copies[static_cast<std::size_t>(rep)], [](int a, int b) { return a < b; });
        keep(copies[static_cast<std::size_t>(rep)][0]);
        rep++;
    });
    std::printf(" %11lld %8.2f", comparisons, seconds * 1000);
}

void row(const char* name, std::size_t n, const std::vector<int>& input, bool with_first) {
    std::printf("%-10s %7zu", name, n);
    cell(input, [](std::vector<int>& v, auto less) { merge_sort(v, less); });
    cell(input, [](std::vector<int>& v, auto less) { quick_sort(v, less); });
    if (with_first) {
        cell(input, [](std::vector<int>& v, auto less) { quick_sort_first(v, less); });
    }
    std::printf("\n");
}

}  // namespace

int main() {
    std::printf("%-10s %7s %11s %8s %11s %8s %11s %8s\n", "entrada", "n", "merge cmp", "ms", "quick cmp", "ms",
                "primero cmp", "ms");
    // Con el pivote fijo, la entrada lenta se queda en n chicos: la recursión tiene n niveles.
    for (std::size_t n : {4000, 8000, 16000}) {
        row("al azar", n, random_values(n, 1000000000), true);
        row("ordenada", n, sorted_values(n), true);
        row("3 valores", n, random_values(n, 3), true);
        row("su entrada", n, slow_input(n), true);
    }
    for (std::size_t n : {250000, 500000, 1000000}) {
        row("al azar", n, random_values(n, 1000000000), false);
        row("ordenada", n, sorted_values(n), false);
        row("3 valores", n, random_values(n, 3), false);
    }
    return 0;
}
lab/tests/lab_tests.cppDescargar lab/tests/lab_tests.cpp
// Pruebas del laboratorio. Uso: lab_tests nombre_de_prueba terminos_chicos.tsv terminos.tsv
// Cada prueba corre sola, en su propio proceso. Nunca miden tiempos: cuentan.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstring>
#include <functional>
#include <iostream>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include "autocomplete.h"
#include "fixed_pivot.h"
#include "sorts.h"
#include "tracked.h"

namespace {

std::string fixture_path;
std::string data_path;

using IntSort = void (*)(std::vector<int>&);

void by_merge(std::vector<int>& items) {
    merge_sort(items, [](int a, int b) { return a < b; });
}

void by_quick(std::vector<int>& items) {
    quick_sort(items, [](int a, int b) { return a < b; });
}

void check_sorts(IntSort sort, const std::vector<int>& input) {
    std::vector<int> expected = input;
    std::sort(expected.begin(), expected.end());
    std::vector<int> items = input;
    sort(items);
    assert(items == expected);
}

void small_cases(IntSort sort) {
    check_sorts(sort, {});
    check_sorts(sort, {1});
    check_sorts(sort, {2, 1});
    check_sorts(sort, {1, 2});
    check_sorts(sort, {3, 3, 3});
    check_sorts(sort, {5, 2, 8, 1, 9, 3, 7, 4});
    check_sorts(sort, {3, 5, 3, 1, 3, 2, 5, 3});
}

std::vector<int> random_ints(std::mt19937& rng, std::size_t n, unsigned range) {
    std::vector<int> items(n);
    for (int& value : items) {
        value = static_cast<int>(rng() % range) - static_cast<int>(range / 2);
    }
    return items;
}

void random_cases(IntSort sort) {
    std::mt19937 rng(2026);
    for (int round = 0; round < 400; round++) {
        std::size_t n = rng() % 300;
        unsigned range = round % 2 == 0 ? 1000000 : 1 + rng() % 8;
        check_sorts(sort, random_ints(rng, n, range));
    }
}

std::vector<std::string> random_strings(std::mt19937& rng, std::size_t n) {
    const std::vector<std::string> letters = {"a", "b", "z", "á", "ñ", "ú", "ca", "cá"};
    std::vector<std::string> words(n);
    for (std::string& word : words) {
        std::size_t length = rng() % 5;
        for (std::size_t i = 0; i < length; i++) {
            word += letters[rng() % letters.size()];
        }
    }
    return words;
}

template <typename Sort>
void strings_like_the_library(Sort sort) {
    std::mt19937 rng(2026);
    for (int round = 0; round < 100; round++) {
        std::vector<std::string> words = random_strings(rng, rng() % 300);
        std::vector<std::string> expected = words;
        std::sort(expected.begin(), expected.end());
        sort(words);
        assert(words == expected);
    }
}

// ---------- sesión 1 ----------

void test_merge_small() {
    small_cases(by_merge);
}

void test_merge_random() {
    random_cases(by_merge);
}

void test_merge_strings() {
    strings_like_the_library([](std::vector<std::string>& words) {
        merge_sort(words, [](const std::string& a, const std::string& b) { return a < b; });
    });
}

void test_merge_uses_comparator() {
    std::vector<int> items = {1, 4, 2, 5, 3};
    merge_sort(items, std::greater<int>());
    assert((items == std::vector<int>{5, 4, 3, 2, 1}));
}

void test_merge_is_stable() {
    std::mt19937 rng(2026);
    std::vector<std::pair<int, int>> items;
    for (int i = 0; i < 5000; i++) {
        items.push_back({static_cast<int>(rng() % 20), i});
    }
    merge_sort(items, [](const auto& a, const auto& b) { return a.first < b.first; });
    for (std::size_t i = 1; i < items.size(); i++) {
        assert(items[i - 1].first <= items[i].first);
        if (items[i - 1].first == items[i].first) {
            assert(items[i - 1].second < items[i].second);
        }
    }
}

void test_merge_does_not_copy() {
    std::vector<Tracked> items;
    for (int i = 0; i < 1000; i++) {
        items.emplace_back((i * 7919) % 1000);
    }
    Tracked::copies() = 0;
    merge_sort(items, [](const Tracked& a, const Tracked& b) { return a.value() < b.value(); });
    assert(Tracked::copies() == 0);
    for (int i = 0; i < 1000; i++) {
        assert(items[static_cast<std::size_t>(i)].value() == i);
    }
}

void test_merge_counts() {
    for (int k = 1; k <= 14; k++) {
        std::size_t n = std::size_t{1} << k;
        std::mt19937 rng(static_cast<unsigned>(k));
        std::vector<int> items = random_ints(rng, n, 1000000);
        long long comparisons = 0;
        merge_sort(items, [&comparisons](int a, int b) {
            comparisons++;
            return a < b;
        });
        assert(comparisons <= static_cast<long long>(n) * k);
    }
}

void test_quick_small() {
    small_cases(by_quick);
}

void test_quick_random() {
    random_cases(by_quick);
}

void test_quick_strings() {
    strings_like_the_library([](std::vector<std::string>& words) {
        quick_sort(words, [](const std::string& a, const std::string& b) { return a < b; });
    });
}

void test_quick_uses_comparator() {
    std::vector<int> items = {1, 4, 2, 5, 3};
    quick_sort(items, std::greater<int>());
    assert((items == std::vector<int>{5, 4, 3, 2, 1}));
}

long long quick_count(std::vector<int> items) {
    long long comparisons = 0;
    quick_sort(items, [&comparisons](int a, int b) {
        comparisons++;
        return a < b;
    });
    assert(std::is_sorted(items.begin(), items.end()));
    return comparisons;
}

void test_quick_sorted_input_is_fast() {
    const long long n = 1 << 14;
    std::vector<int> items(static_cast<std::size_t>(n));
    for (long long i = 0; i < n; i++) {
        items[static_cast<std::size_t>(i)] = static_cast<int>(i);
    }
    // Un pivote fijo haría n(n-1)/2, unas 134 millones. Esto es mucho menos de eso.
    assert(quick_count(items) < 6 * n * 14);
    std::reverse(items.begin(), items.end());
    assert(quick_count(items) < 6 * n * 14);
}

void test_quick_all_equal_is_one_pass() {
    const std::size_t n = 5000;
    assert(quick_count(std::vector<int>(n, 7)) <= 2 * static_cast<long long>(n));
}

void test_quick_few_values_is_linear() {
    std::mt19937 rng(2026);
    const std::size_t n = 5000;
    // Con 3 valores: pocas pasadas, sin importar n.
    assert(quick_count(random_ints(rng, n, 3)) <= 8 * static_cast<long long>(n));
}

void test_quick_copies_only_pivots() {
    std::vector<Tracked> items;
    for (int i = 0; i < 1000; i++) {
        items.emplace_back((i * 7919) % 1000);
    }
    Tracked::copies() = 0;
    quick_sort(items, [](const Tracked& a, const Tracked& b) { return a.value() < b.value(); });
    assert(Tracked::copies() <= 1000);
    for (int i = 0; i < 1000; i++) {
        assert(items[static_cast<std::size_t>(i)].value() == i);
    }
}

void test_slow_input_is_slow() {
    const std::size_t n = 2000;
    std::vector<int> items = slow_input(n);
    assert(items.size() == n);
    long long comparisons = 0;
    quick_sort_first(items, [&comparisons](int a, int b) {
        comparisons++;
        return a < b;
    });
    assert(std::is_sorted(items.begin(), items.end()));
    assert(comparisons >= static_cast<long long>(n * n / 4));
}

// ---------- sesión 2 ----------

// Un arreglo de términos que cuenta las lecturas.
class CountingTerms {
public:
    explicit CountingTerms(const std::vector<Term>& terms) : terms_(terms) {}
    std::size_t size() const { return terms_.size(); }
    const Term& operator[](std::size_t i) const {
        reads_++;
        return terms_[i];
    }
    long long reads() const { return reads_; }

private:
    const std::vector<Term>& terms_;
    mutable long long reads_ = 0;
};

std::vector<Term> load_sorted(const std::string& path) {
    std::vector<Term> terms;
    bool ok = load_terms(path, terms);
    assert(ok);
    sort_by_text(terms);
    return terms;
}

std::vector<std::string> texts(const std::vector<Term>& terms) {
    std::vector<std::string> result;
    for (const Term& term : terms) {
        result.push_back(term.text);
    }
    return result;
}

std::vector<std::string> range_texts(const std::vector<Term>& terms, Range range) {
    assert(range.first <= range.last && range.last <= terms.size());
    std::vector<std::string> result;
    for (std::size_t i = range.first; i < range.last; i++) {
        result.push_back(terms[i].text);
    }
    return result;
}

using Words = std::vector<std::string>;

// El oráculo: revisar todo, con starts_with.
Range oracle_range(const std::vector<Term>& terms, const std::string& prefix) {
    std::size_t first = terms.size();
    std::size_t count = 0;
    for (std::size_t i = 0; i < terms.size(); i++) {
        if (terms[i].text.starts_with(prefix)) {
            first = std::min(first, i);
            count++;
        }
    }
    if (count == 0) {
        first = 0;
        while (first < terms.size() && terms[first].text < prefix) {
            first++;
        }
    }
    return Range{first, first + count};
}

void test_sort_is_byte_order() {
    std::vector<Term> terms = load_sorted(fixture_path);
    assert((texts(terms) == Words{"a", "az", "año", "aún", "ca", "cama", "casa", "cazar", "caña", "cañón",
                                  "cuna", "cámara", "cé", "de", "debe", "débil", "zapato", "zorro", "árbol",
                                  "ñandú"}));
}

void test_sort_real_file() {
    std::vector<Term> terms = load_sorted(data_path);
    assert(terms.size() == 28039);
    std::vector<std::string> expected = texts(terms);
    std::sort(expected.begin(), expected.end());
    assert(texts(terms) == expected);
    assert(terms[0].text == "a");
    assert(terms.back().text == "útilmente");
}

void test_prefix_ascii() {
    std::vector<Term> terms = load_sorted(fixture_path);
    assert((range_texts(terms, prefix_range(terms, "ca")) == Words{"ca", "cama", "casa", "cazar", "caña", "cañón"}));
    assert((range_texts(terms, prefix_range(terms, "cu")) == Words{"cuna"}));
    assert((range_texts(terms, prefix_range(terms, "z")) == Words{"zapato", "zorro"}));
}

void test_prefix_whole_term_is_included() {
    std::vector<Term> terms = load_sorted(fixture_path);
    assert((range_texts(terms, prefix_range(terms, "de")) == Words{"de", "debe"}));
    assert((range_texts(terms, prefix_range(terms, "zorro")) == Words{"zorro"}));
    assert((range_texts(terms, prefix_range(terms, "a")) == Words{"a", "az", "año", "aún"}));
}

void test_prefix_accents_are_other_bytes() {
    std::vector<Term> terms = load_sorted(fixture_path);
    // cámara no empieza con ca, y sí con cá. Lo mismo árbol con a.
    assert((range_texts(terms, prefix_range(terms, "cá")) == Words{"cámara"}));
    assert((range_texts(terms, prefix_range(terms, "á")) == Words{"árbol"}));
    assert((range_texts(terms, prefix_range(terms, "ñ")) == Words{"ñandú"}));
    // ñ va después de todas las letras sin tilde.
    assert((range_texts(terms, prefix_range(terms, "cañ")) == Words{"caña", "cañón"}));
    assert((range_texts(terms, prefix_range(terms, "añ")) == Words{"año"}));
}

void test_prefix_empty_is_everything() {
    std::vector<Term> terms = load_sorted(fixture_path);
    Range range = prefix_range(terms, "");
    assert(range.first == 0 && range.last == terms.size());
    std::vector<Term> none;
    Range nothing = prefix_range(none, "");
    assert(nothing.first == 0 && nothing.last == 0);
    Range also_nothing = prefix_range(none, "a");
    assert(also_nothing.first == 0 && also_nothing.last == 0);
}

void test_prefix_absent() {
    std::vector<Term> terms = load_sorted(fixture_path);
    struct Case {
        std::string prefix;
        std::size_t where;
    };
    for (const Case& absent : {Case{"0", 0}, Case{"b", 4}, Case{"caz0", 7}, Case{"zz", 18}, Case{"úa", 20}}) {
        Range range = prefix_range(terms, absent.prefix);
        assert(range.first == range.last);
        assert(range.first == absent.where);
    }
}

void test_prefix_extreme_bytes() {
    std::vector<Term> terms = load_sorted(fixture_path);
    // Un prefijo cortado en medio de una letra de dos bytes se trata como bytes.
    assert((range_texts(terms, prefix_range(terms, std::string("c\xC3"))) == Words{"cámara", "cé"}));
    assert((range_texts(terms, prefix_range(terms, std::string("\xC3"))) == Words{"árbol", "ñandú"}));
    // 0xFF no aparece en UTF-8: el rango queda vacío y al final. Sumarle 1 a ese byte falla.
    Range range = prefix_range(terms, std::string("\xFF"));
    assert(range.first == terms.size() && range.last == terms.size());
    std::vector<Term> with_high = {{"a", 1}, {std::string("a\xFF"), 1}, {std::string("a\xFF\xFF"), 1}, {"b", 1}};
    Range high = prefix_range(with_high, std::string("a\xFF"));
    assert(high.first == 1 && high.last == 3);
}

void test_prefix_real_counts() {
    std::vector<Term> terms = load_sorted(data_path);
    struct Case {
        std::string prefix;
        std::size_t count;
    };
    for (const Case& known : {Case{"", 28039}, Case{"a", 3471}, Case{"á", 41}, Case{"c", 3290}, Case{"ca", 819},
                              Case{"cá", 21}, Case{"cañ", 12}, Case{"de", 1474}, Case{"qu", 335}, Case{"ñ", 3},
                              Case{"z", 100}, Case{"zzz", 0}}) {
        Range range = prefix_range(terms, known.prefix);
        assert(range.last - range.first == known.count);
        for (std::size_t i = range.first; i < range.last; i++) {
            assert(terms[i].text.starts_with(known.prefix));
        }
    }
}

void test_prefix_matches_oracle() {
    std::vector<Term> terms = load_sorted(data_path);
    std::mt19937 rng(2026);
    for (int round = 0; round < 80; round++) {
        const std::string& text = terms[rng() % terms.size()].text;
        std::size_t length = rng() % (text.size() + 1);
        // Corta por bytes, a veces en medio de una letra.
        std::string prefix = text.substr(0, length);
        Range range = prefix_range(terms, prefix);
        Range expected = oracle_range(terms, prefix);
        assert(range.first == expected.first && range.last == expected.last);
    }
}

void test_prefix_reads_few_terms() {
    std::vector<Term> terms = load_sorted(data_path);
    CountingTerms counting(terms);
    for (const char* prefix : {"", "a", "ca", "cañ", "de", "zzz", "á", "ñ", "útil"}) {
        long long before = counting.reads();
        prefix_range(counting, prefix);
        // log2(28039) < 15. Dos búsquedas leen a lo sumo 2 * 16 términos, y esto da margen.
        assert(counting.reads() - before <= 40);
    }
}

void test_top_k_order_and_ties() {
    std::vector<Term> terms = load_sorted(fixture_path);
    Range ca = prefix_range(terms, "ca");
    assert((texts(top_k(terms, ca, 3)) == Words{"cama", "casa", "caña"}));
    assert((texts(top_k(terms, ca, 6)) == Words{"cama", "casa", "caña", "cañón", "cazar", "ca"}));
}

void test_top_k_edges() {
    std::vector<Term> terms = load_sorted(fixture_path);
    Range ca = prefix_range(terms, "ca");
    assert(top_k(terms, ca, 0).empty());
    assert(top_k(terms, ca, 100).size() == 6);
    assert(top_k(terms, ca, 5).size() == 5);
    assert(top_k(terms, ca, 6).size() == 6);
    assert(top_k_scan(terms, "ca", 5).size() == 5);
    assert(top_k(terms, Range{3, 3}, 5).empty());
    assert(top_k_scan(terms, "ca", 0).empty());
    assert(top_k_scan(terms, "zzz", 5).empty());
    assert(top_k_scan(terms, "ca", 100).size() == 6);
    std::vector<Term> all_of_them = top_k(terms, prefix_range(terms, ""), 100);
    assert(all_of_them.size() == terms.size());
    assert(terms[0].text == "a");  // top_k no cambia el arreglo
}

void test_top_k_keeps_weights() {
    std::vector<Term> terms = load_sorted(fixture_path);
    std::vector<Term> best = top_k(terms, prefix_range(terms, "cá"), 1);
    assert(best.size() == 1 && best[0].text == "cámara" && best[0].weight == 40);
}

void test_top_k_real_file_order() {
    // terminos.tsv está por frecuencia descendente, con los empates por bytes. Una vez ordenado
    // por texto, pedir todos los términos debe reconstruir el archivo línea por línea.
    std::vector<Term> file_order;
    bool ok = load_terms(data_path, file_order);
    assert(ok);
    std::vector<Term> terms = file_order;
    sort_by_text(terms);
    std::vector<Term> best = top_k(terms, prefix_range(terms, ""), terms.size());
    assert(texts(best) == texts(file_order));
}

void test_top_k_matches_scan() {
    std::vector<Term> terms = load_sorted(data_path);
    std::mt19937 rng(2026);
    for (int round = 0; round < 60; round++) {
        const std::string& text = terms[rng() % terms.size()].text;
        std::string prefix = text.substr(0, rng() % (text.size() + 1));
        std::size_t k = rng() % 12;
        std::vector<Term> fast = top_k(terms, prefix_range(terms, prefix), k);
        std::vector<Term> slow = top_k_scan(terms, prefix, k);
        assert(texts(fast) == texts(slow));
    }
}

void test_top_k_reads_only_the_range() {
    std::vector<Term> terms = load_sorted(data_path);
    Range range = prefix_range(terms, "ca");
    CountingTerms counting(terms);
    top_k(counting, range, 5);
    long long size = static_cast<long long>(range.last - range.first);
    assert(counting.reads() >= size);
    assert(counting.reads() <= 2 * size);
}

void test_scan_reads_the_whole_file() {
    std::vector<Term> terms = load_sorted(data_path);
    for (const char* prefix : {"", "a", "zzz"}) {
        CountingTerms counting(terms);
        top_k_scan(counting, prefix, 1);
        assert(counting.reads() >= static_cast<long long>(terms.size()));
    }
}

struct Test {
    const char* name;
    void (*run)();
};

const std::vector<Test> ALL_TESTS = {
    {"merge_small", test_merge_small},
    {"merge_random", test_merge_random},
    {"merge_strings", test_merge_strings},
    {"merge_uses_comparator", test_merge_uses_comparator},
    {"merge_is_stable", test_merge_is_stable},
    {"merge_does_not_copy", test_merge_does_not_copy},
    {"merge_counts", test_merge_counts},
    {"quick_small", test_quick_small},
    {"quick_random", test_quick_random},
    {"quick_strings", test_quick_strings},
    {"quick_uses_comparator", test_quick_uses_comparator},
    {"quick_sorted_input_is_fast", test_quick_sorted_input_is_fast},
    {"quick_all_equal_is_one_pass", test_quick_all_equal_is_one_pass},
    {"quick_few_values_is_linear", test_quick_few_values_is_linear},
    {"quick_copies_only_pivots", test_quick_copies_only_pivots},
    {"slow_input_is_slow", test_slow_input_is_slow},
    {"sort_is_byte_order", test_sort_is_byte_order},
    {"sort_real_file", test_sort_real_file},
    {"prefix_ascii", test_prefix_ascii},
    {"prefix_whole_term_is_included", test_prefix_whole_term_is_included},
    {"prefix_accents_are_other_bytes", test_prefix_accents_are_other_bytes},
    {"prefix_empty_is_everything", test_prefix_empty_is_everything},
    {"prefix_absent", test_prefix_absent},
    {"prefix_extreme_bytes", test_prefix_extreme_bytes},
    {"prefix_real_counts", test_prefix_real_counts},
    {"prefix_matches_oracle", test_prefix_matches_oracle},
    {"prefix_reads_few_terms", test_prefix_reads_few_terms},
    {"top_k_order_and_ties", test_top_k_order_and_ties},
    {"top_k_edges", test_top_k_edges},
    {"top_k_keeps_weights", test_top_k_keeps_weights},
    {"top_k_real_file_order", test_top_k_real_file_order},
    {"top_k_matches_scan", test_top_k_matches_scan},
    {"top_k_reads_only_the_range", test_top_k_reads_only_the_range},
    {"scan_reads_the_whole_file", test_scan_reads_the_whole_file},
};

}  // namespace

int main(int argc, char** argv) {
    if (argc != 4) {
        std::cerr << "uso: lab_tests nombre_de_prueba terminos_chicos.tsv terminos.tsv\n";
        return 2;
    }
    fixture_path = argv[2];
    data_path = argv[3];
    for (const Test& test : ALL_TESTS) {
        if (std::strcmp(test.name, argv[1]) == 0) {
            test.run();
            return 0;
        }
    }
    std::cerr << "no existe la prueba " << argv[1] << "\n";
    return 2;
}
lab/tests/tracked.hDescargar lab/tests/tracked.h
// Tipo para las pruebas: cuenta copias y no tiene constructor por defecto.
#ifndef TRACKED_H
#define TRACKED_H

#include <utility>

class Tracked {
public:
    static long& copies() {
        static long count = 0;
        return count;
    }

    explicit Tracked(int value) : value_(value) {}
    Tracked(const Tracked& other) : value_(other.value_) { copies()++; }
    Tracked(Tracked&& other) noexcept : value_(std::exchange(other.value_, -1)) {}
    Tracked& operator=(const Tracked& other) {
        value_ = other.value_;
        copies()++;
        return *this;
    }
    Tracked& operator=(Tracked&& other) noexcept {
        value_ = std::exchange(other.value_, -1);
        return *this;
    }

    int value() const { return value_; }

private:
    int value_;
};

#endif
lab/tests/fixtures/terminos_chicos.tsvDescargar lab/tests/fixtures/terminos_chicos.tsv
50	casa
50	cama
40	cámara
30	caña
30	cañón
20	cazar
10	cuna
9	cé
8	ñandú
7	zorro
7	zapato
6	árbol
5	ca
4	a
3	año
2	aún
1	az
1	de
1	debe
1	débil

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 6: ordenamiento y búsqueda binaria. Se usa dentro del entorno del curso.
#
#   make                  compila los programas de la clase en build/
#   make lab              compila el laboratorio en build/lab/
#   make test             corre las pruebas de los ejemplos y de los apuntes
#   make test-sorts       prueba solo la sesión 1 del laboratorio
#   make test-autocomplete  prueba solo la sesión 2 del laboratorio
#   make test-lab         prueba todo su laboratorio; falla hasta que lo terminen
#   make bench-sorts      mide sus ordenamientos, con -O2 y sin sanitizers
#   make bench-query      mide sus consultas, con -O2 y sin sanitizers
#   make clean            borra lo generado
#
# 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.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
LAB_DIR ?= lab
DATA ?= ../../data
BUILD = build
PROGRAMS = trace_merge trace_partition trace_search comparators test_examples
DATA_PROGRAMS = stability byte_order
BENCHES = count_sorts time_sorts word_search
COMMON_HEADERS = int_sorts.h inputs.h bench.h lab/terms_io.h
LAB_HEADERS = $(LAB_DIR)/sorts.h $(LAB_DIR)/autocomplete.h lab/fixed_pivot.h lab/terms_io.h lab/not_implemented.h

.PHONY: all lab test test-sorts test-autocomplete test-lab bench-sorts bench-query clean

all: $(PROGRAMS:%=$(BUILD)/%) $(DATA_PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%) $(BUILD)/byte_order_signed $(BUILD)/byte_order_unsigned

$(PROGRAMS:%=$(BUILD)/%) $(DATA_PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
	$(CXX) $(CXXFLAGS) $< -o $@

# El mismo programa, con char con signo y sin signo, sea cual sea la máquina.
$(BUILD)/byte_order_signed: byte_order.cpp lab/terms_io.h | $(BUILD)
	$(CXX) $(CXXFLAGS) -fsigned-char $< -o $@

$(BUILD)/byte_order_unsigned: byte_order.cpp lab/terms_io.h | $(BUILD)
	$(CXX) $(CXXFLAGS) -funsigned-char $< -o $@

# Las mediciones van con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
	$(CXX) $(BENCHFLAGS) $< -o $@

# LAB_DIR va primero: así las pruebas toman el código de esa carpeta.
LAB_INCLUDES = -I$(LAB_DIR) -Ilab -I.

$(BUILD)/$(LAB_DIR)/lab_tests: lab/tests/lab_tests.cpp lab/tests/tracked.h $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
	$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@

# Las mismas pruebas con char con signo, como en x86-64: una comparación char por char
# que funciona en ARM falla aquí, y al revés.
$(BUILD)/$(LAB_DIR)/lab_tests_signed: lab/tests/lab_tests.cpp lab/tests/tracked.h $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
	$(CXX) $(CXXFLAGS) -fsigned-char $(LAB_INCLUDES) $< -o $@

$(BUILD)/$(LAB_DIR)/autocomplete: lab/tests/autocomplete_main.cpp $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
	$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@

$(BUILD)/$(LAB_DIR)/bench_sorts: lab/tests/bench_sorts.cpp $(LAB_HEADERS) bench.h | $(BUILD)/$(LAB_DIR)
	$(CXX) $(BENCHFLAGS) $(LAB_INCLUDES) $< -o $@

$(BUILD)/$(LAB_DIR)/bench_query: lab/tests/bench_query.cpp $(LAB_HEADERS) bench.h | $(BUILD)/$(LAB_DIR)
	$(CXX) $(BENCHFLAGS) $(LAB_INCLUDES) $< -o $@

$(BUILD) $(BUILD)/$(LAB_DIR):
	mkdir -p $@

LAB_BINARIES = lab_tests lab_tests_signed autocomplete bench_sorts bench_query

lab: $(LAB_BINARIES:%=$(BUILD)/$(LAB_DIR)/%)

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

test-sorts: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestSorts

test-autocomplete: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestAutocomplete test_lab.TestSignedChar

test-lab: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab

bench-sorts: $(BUILD)/$(LAB_DIR)/bench_sorts
	$<

bench-query: $(BUILD)/$(LAB_DIR)/bench_query
	$< $(DATA)/terminos.tsv

clean:
	rm -rf $(BUILD) __pycache__