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::sortystd::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) / 2con overflow, y ciclos que no terminan porque el rango no se reduce.
Laboratorio — Autocompletado
Sesión 1:
- Escribir
merge_sortyquick_sortcomo templates con comparador, con pruebas al azar contrastd::sort. - 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
- Dibujen el árbol de llamadas de mergesort para n = 8. ¿Cuánto trabajo hay por nivel?
- ¿Qué entrada hace que un quicksort que siempre elige el primer elemento como pivote cueste Θ(n²)?
- ¿Qué significa que un ordenamiento sea estable? Den un ejemplo donde importe.
- ¿A qué algoritmos se aplica la cota Ω(n log n)? ¿Qué supone sobre lo que pueden hacer con los datos?
- 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.
// 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// 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;
}// 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;
}// 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;
}// 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// 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// 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;
}// 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;
}// 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;
}// 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;
}// 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;
}// 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.
// 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;
}// 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;
}// 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;
}// 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.
// 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;
}"""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()"""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()"""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.
// 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// 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// 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# 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?// 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// 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// 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;
}// 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;
}// 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;
}// 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;
}// 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_;
};
#endif50 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ébilCompilación
Cómo se compilan y ejecutan los ejemplos.
# 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__