Semana 13 — Heaps y colas de prioridad
Objetivos
- Implementar un heap binario sobre un arreglo.
- Explicar por qué construir un heap cuesta Θ(n), y no Θ(n log n).
- Usar una cola de prioridad para resolver problemas: los k mayores y la compresión de Huffman.
Teoría — Heaps y colas de prioridad · Heapsort
Lectura: Apuntes, semana 13.
Apoyo: Open Data Structures, secs. 10.1 (BinaryHeap) y 11.1.3 (Heap-sort). CLRS, capítulo Heapsort.
Temas:
- El tipo abstracto cola de prioridad: insertar y sacar el mínimo. Tres implementaciones ingenuas y su costo.
- La invariante del heap: cada padre es menor o igual que sus hijos. Es más débil que el orden de un árbol de búsqueda.
- Un árbol completo sin punteros: en un arreglo, el padre de i está en (i − 1)/2, y los hijos en 2i + 1 y 2i + 2.
- Insertar y subir; sacar la raíz y bajar. Costo O(log n).
- Construir de abajo hacia arriba: la mayoría de los nodos baja poco, y el total es Θ(n). Con una suma por niveles.
- Heapsort: en el lugar y O(n log n), pero no es estable y salta por toda la memoria.
- Los k mayores de una secuencia con un heap de tamaño k.
std::priority_queue. - Huffman: códigos sin prefijos comunes, y unir repetidamente los dos árboles de menor frecuencia. Por qué es óptimo queda para Algoritmos.
- Error común: buscar en un heap como si fuera un árbol de búsqueda.
Laboratorio — Compresión de Huffman
Sesión 1:
- Escribir
MinHeap<T>con comparador, conpush,pop,topy construcción desde un arreglo, concheck_invariantsy pruebas al azar contrastd::priority_queue. - Medir la construcción de abajo hacia arriba frente a n inserciones.
- Escribir un programa que imprime las k palabras más frecuentes de un texto, y medirlo contra ordenar todo.
Sesión 2:
4. Con las frecuencias de bytes de un archivo, que se dan contadas, construir el árbol de Huffman con su heap.
5. Escribir la compresión y la descompresión: cabecera con lo necesario para volver a armar el árbol, y codificar y decodificar el contenido. Se dan hechas la tabla de códigos y la escritura y lectura de bits.
6. Comprobar con cmp que comprimir y descomprimir devuelve el archivo original, incluso con archivos binarios, vacíos o de un solo símbolo. Medir cuánto comprime cada tipo de archivo.
Preguntas de repaso
- Inserten 5, 3, 8, 1 y 4 en un min-heap vacío. Dibujen el arreglo después de cada inserción.
- Saquen el mínimo de ese heap. ¿Cómo queda?
- ¿Por qué el mínimo está en la raíz y el máximo no tiene un lugar fijo? ¿Dónde puede estar el máximo?
- Con frecuencias a:5, b:2, c:1 y d:1, construyan el árbol de Huffman y den los códigos. ¿Cuántos bits usa el texto completo?
- ¿Por qué ningún código de Huffman es prefijo de otro? ¿Qué permite eso al descomprimir?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (13)Errores (6)Pruebas (4)Laboratorio (21)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// Construir un min-heap de dos formas, contando comparaciones. Los conteos no dependen de la máquina.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>
#include "heapsort.h"
#include "int_heap.h"
namespace {
std::vector<int> make_input(const std::string& order, int n) {
std::vector<int> items(n);
std::iota(items.begin(), items.end(), 0);
if (order == "desc") {
std::reverse(items.begin(), items.end());
} else if (order == "azar") {
std::mt19937 generator(13);
std::shuffle(items.begin(), items.end(), generator);
}
return items;
}
long count_pushes(const std::vector<int>& items) {
IntHeap heap;
for (int item : items) {
heap.push(item);
}
heap.check_invariants();
return heap.comparisons();
}
// build_max_heap con el orden al revés da un min-heap: la raíz es la menor.
long count_bottom_up(std::vector<int> items, std::size_t* levels = nullptr) {
long comparisons = 0;
std::size_t total = build_max_heap(items, [&](int a, int b) {
comparisons++;
return a > b;
});
if (levels != nullptr) {
*levels = total;
}
return comparisons;
}
// Cuántos nodos de un árbol perfecto tienen altura h, y cuánto pueden bajar en total.
void print_levels(int k) {
int n = (1 << (k + 1)) - 1;
std::cout << "\nArbol perfecto con n = " << n << ", entrada descendente\n";
std::cout << " altura nodos bajan como maximo\n";
long total = 0;
for (int h = 0; h <= k; h++) {
long nodes = 1L << (k - h);
total += nodes * h;
std::cout << std::setw(7) << h << std::setw(7) << nodes << std::setw(19) << nodes * h << "\n";
}
std::size_t real = 0;
count_bottom_up(make_input("desc", n), &real);
std::cout << "suma: " << total << ", niveles que bajaron de verdad: " << real << "\n";
}
} // namespace
int main() {
std::cout << " n orden abajo-arriba /n n pushes /n log2n\n";
for (int n : {1023, 16383, 262143, 1048575}) {
for (const std::string order : {"asc", "azar", "desc"}) {
std::vector<int> items = make_input(order, n);
long bottom_up = count_bottom_up(items);
long pushes = count_pushes(items);
std::cout << std::setw(9) << n << std::setw(7) << order << std::setw(14) << bottom_up
<< std::fixed << std::setprecision(2) << std::setw(6) << double(bottom_up) / n
<< std::setw(10) << pushes << std::setw(6) << double(pushes) / n
<< std::setprecision(1) << std::setw(5) << std::log2(n) << "\n";
}
}
print_levels(9);
return 0;
}// Sigue un min-heap paso a paso: el arreglo después de cada operación.
#include <iostream>
#include "int_heap.h"
namespace {
void print(const char* label, int value, const IntHeap& heap) {
std::cout << label << " " << value << ": [";
const char* separator = "";
for (int item : heap.items()) {
std::cout << separator << item;
separator = " ";
}
std::cout << "]\n";
}
} // namespace
int main() {
IntHeap heap;
for (int value : {5, 3, 8, 1, 4}) {
heap.push(value);
heap.check_invariants();
print("push", value, heap);
}
int least = heap.top();
heap.pop();
heap.check_invariants();
print("pop ", least, heap);
return 0;
}// ¿Cuánto salta cada algoritmo? Anota cada lectura y escritura del arreglo, y simula con ellas una
// cache pequeña: 32 KB, líneas de 64 bytes, cada línea en un único lugar posible (correspondencia directa).
// Es un conteo sobre un modelo, no una medición de la cache de esta máquina.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
namespace {
class CacheModel {
public:
CacheModel() : tags_(lines_in_cache, -1) {}
// position es el número del int dentro de la memoria simulada.
void touch(std::size_t position) {
std::int64_t line = static_cast<std::int64_t>(position / ints_per_line);
std::int64_t& tag = tags_[static_cast<std::size_t>(line) % lines_in_cache];
accesses_++;
if (tag != line) {
misses_++;
tag = line;
}
}
long accesses() const { return accesses_; }
long misses() const { return misses_; }
private:
static constexpr std::size_t ints_per_line = 64 / sizeof(int);
static constexpr std::size_t lines_in_cache = 32 * 1024 / 64;
std::vector<std::int64_t> tags_;
long accesses_ = 0;
long misses_ = 0;
};
void sift_down(std::vector<int>& items, std::size_t i, std::size_t n, CacheModel& cache) {
while (true) {
std::size_t left = 2 * i + 1;
std::size_t right = 2 * i + 2;
std::size_t largest = i;
cache.touch(i);
if (left < n) {
cache.touch(left);
if (items[largest] < items[left]) {
largest = left;
}
}
if (right < n) {
cache.touch(right);
if (items[largest] < items[right]) {
largest = right;
}
}
if (largest == i) {
return;
}
std::swap(items[i], items[largest]);
i = largest;
}
}
void modeled_heapsort(std::vector<int>& items, CacheModel& cache) {
for (std::size_t i = items.size() / 2; i > 0; i--) {
sift_down(items, i - 1, items.size(), cache);
}
for (std::size_t end = items.size(); end > 1; end--) {
cache.touch(0);
cache.touch(end - 1);
std::swap(items[0], items[end - 1]);
sift_down(items, 0, end - 1, cache);
}
}
// Mergesort de abajo hacia arriba. El arreglo auxiliar va después del original en la memoria simulada.
void modeled_mergesort(std::vector<int>& items, CacheModel& cache) {
const std::size_t n = items.size();
std::vector<int> buffer(n);
std::size_t source_at = 0;
std::size_t target_at = n;
for (std::size_t width = 1; width < n; width *= 2) {
for (std::size_t low = 0; low < n; low += 2 * width) {
std::size_t mid = std::min(low + width, n);
std::size_t high = std::min(low + 2 * width, n);
std::size_t a = low;
std::size_t b = mid;
std::size_t out = low;
while (a < mid || b < high) {
bool take_b;
if (a < mid && b < high) {
cache.touch(source_at + a);
cache.touch(source_at + b);
take_b = items[b] < items[a];
} else {
take_b = a == mid;
cache.touch(source_at + (take_b ? b : a));
}
cache.touch(target_at + out);
buffer[out++] = take_b ? items[b++] : items[a++];
}
}
items.swap(buffer);
std::swap(source_at, target_at);
}
}
} // namespace
int main() {
std::cout << " n algoritmo accesos fallos fallos/n\n";
for (int n : {1000, 10000, 100000, 1000000, 4000000}) {
std::vector<int> input(n);
std::iota(input.begin(), input.end(), 0);
std::mt19937 generator(13);
std::shuffle(input.begin(), input.end(), generator);
std::vector<int> items = input;
CacheModel heap_cache;
modeled_heapsort(items, heap_cache);
if (!std::is_sorted(items.begin(), items.end())) {
return 1;
}
items = input;
CacheModel merge_cache;
modeled_mergesort(items, merge_cache);
if (!std::is_sorted(items.begin(), items.end())) {
return 1;
}
std::cout << std::fixed << std::setprecision(2);
std::cout << std::setw(9) << n << " heapsort" << std::setw(12) << heap_cache.accesses() << std::setw(13)
<< heap_cache.misses() << std::setw(10) << double(heap_cache.misses()) / n << "\n";
std::cout << std::setw(9) << n << " mergesort" << std::setw(12) << merge_cache.accesses() << std::setw(13)
<< merge_cache.misses() << std::setw(10) << double(merge_cache.misses()) / n << "\n";
}
return 0;
}// Comparaciones de heapsort según la entrada. Los conteos no dependen de la máquina.
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>
#include "heapsort.h"
namespace {
std::vector<int> make_input(const std::string& kind, int n) {
std::vector<int> items(n, 7);
if (kind == "iguales") {
return items;
}
std::iota(items.begin(), items.end(), 0);
if (kind == "desc") {
std::reverse(items.begin(), items.end());
} else if (kind == "azar") {
std::mt19937 generator(13);
std::shuffle(items.begin(), items.end(), generator);
}
return items;
}
} // namespace
int main() {
std::cout << " n entrada comparaciones /n /(n log2 n)\n";
for (int n : {1000, 10000, 100000, 1000000}) {
for (const std::string kind : {"azar", "asc", "desc", "iguales"}) {
std::vector<int> items = make_input(kind, n);
long comparisons = 0;
heapsort(items, [&](int a, int b) {
comparisons++;
return a < b;
});
if (!std::is_sorted(items.begin(), items.end())) {
std::cout << "no quedo ordenado\n";
return 1;
}
std::cout << std::setw(9) << n << std::setw(9) << kind << std::setw(15) << comparisons
<< std::fixed << std::setprecision(2) << std::setw(7) << double(comparisons) / n
<< std::setw(13) << double(comparisons) / (n * std::log2(n)) << "\n";
}
}
return 0;
}// Heapsort paso a paso, y un caso que muestra que no es estable.
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
#include "heapsort.h"
namespace {
using Item = std::pair<int, char>;
void print_range(const std::vector<int>& items, std::size_t end) {
std::cout << "[";
for (std::size_t i = 0; i < items.size(); i++) {
if (i == end) {
std::cout << " | ";
} else if (i > 0) {
std::cout << " ";
}
std::cout << items[i];
}
if (end == items.size()) {
std::cout << " |";
}
std::cout << "]\n";
}
std::string text(const std::vector<Item>& items) {
std::string out;
for (const Item& item : items) {
out += std::to_string(item.first) + std::string(1, item.second) + " ";
}
return out;
}
} // namespace
int main() {
auto ascending = [](int a, int b) { return a < b; };
std::vector<int> items = {4, 1, 7, 3, 9, 2};
std::cout << "entrada: ";
print_range(items, items.size());
build_max_heap(items, ascending);
std::cout << "heap: ";
print_range(items, items.size());
for (std::size_t end = items.size(); end > 1; end--) {
std::swap(items[0], items[end - 1]);
sift_down_max(items, 0, end - 1, ascending);
std::cout << "saca " << items[end - 1] << ": ";
print_range(items, end - 1);
}
// Tres claves: los dos 2 salen en el orden contrario al de entrada.
std::vector<Item> pairs = {{2, 'a'}, {2, 'b'}, {1, 'c'}};
std::cout << "\nentrada: " << text(pairs) << "\n";
heapsort(pairs, [](const Item& a, const Item& b) { return a.first < b.first; });
std::cout << "salida: " << text(pairs) << "\n";
return 0;
}// Mide heapsort frente a las ordenaciones de la biblioteca. Compilar con -O2 y sin sanitizers.
// Los tiempos dependen de la máquina.
#include <algorithm>
#include <functional>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include "bench.h"
#include "heapsort.h"
namespace {
template <typename Sorter>
double time_sort(const std::vector<int>& input, Sorter sorter) {
return median_seconds(5, [&] {
std::vector<int> items = input;
sorter(items);
keep(items[items.size() / 2]);
});
}
} // namespace
int main() {
std::cout << " n heapsort std::sort stable_sort heap/sort\n";
for (int n : {100000, 200000, 400000, 800000, 1600000, 3200000}) {
std::vector<int> input(n);
std::iota(input.begin(), input.end(), 0);
std::mt19937 generator(13);
std::shuffle(input.begin(), input.end(), generator);
double heap = time_sort(input, [](std::vector<int>& items) { heapsort(items, std::less<int>()); });
double quick = time_sort(input, [](std::vector<int>& items) { std::sort(items.begin(), items.end()); });
double stable =
time_sort(input, [](std::vector<int>& items) { std::stable_sort(items.begin(), items.end()); });
std::cout << std::fixed << std::setprecision(4) << std::setw(9) << n << std::setw(11) << heap
<< std::setw(11) << quick << std::setw(13) << stable << std::setprecision(2)
<< std::setw(11) << heap / quick << "\n";
}
return 0;
}// Heapsort sobre un std::vector, en el lugar. Usa un heap con el mayor en la raíz.
#ifndef HEAPSORT_H
#define HEAPSORT_H
#include <cstddef>
#include <utility>
#include <vector>
// Baja el elemento de i dentro de items[0, n). less(a, b): a va antes que b en el orden final.
// Devuelve cuántos niveles bajó.
template <typename T, typename Less>
std::size_t sift_down_max(std::vector<T>& items, std::size_t i, std::size_t n, Less less) {
std::size_t levels = 0;
while (true) {
std::size_t left = 2 * i + 1;
std::size_t right = 2 * i + 2;
std::size_t largest = i;
if (left < n && less(items[largest], items[left])) {
largest = left;
}
if (right < n && less(items[largest], items[right])) {
largest = right;
}
if (largest == i) {
return levels;
}
std::swap(items[i], items[largest]);
i = largest;
levels++;
}
}
// Construye de abajo hacia arriba: las hojas ya son heaps, y se empieza por el último padre.
// El índice baja hasta 0 sin pasar por debajo: size_t no tiene negativos.
// Devuelve el total de niveles que bajaron los elementos.
template <typename T, typename Less>
std::size_t build_max_heap(std::vector<T>& items, Less less) {
std::size_t total = 0;
for (std::size_t i = items.size() / 2; i > 0; i--) {
total += sift_down_max(items, i - 1, items.size(), less);
}
return total;
}
template <typename T, typename Less>
void heapsort(std::vector<T>& items, Less less) {
build_max_heap(items, less);
for (std::size_t end = items.size(); end > 1; end--) {
// El mayor de items[0, end) pasa a su lugar definitivo.
std::swap(items[0], items[end - 1]);
sift_down_max(items, 0, end - 1, less);
}
}
#endif// Huffman sobre una tabla pequeña. Cada árbol se guarda como su lista de códigos: al unir dos árboles,
// los códigos del primero ganan un 0 adelante y los del segundo un 1.
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <queue>
#include <string>
#include <utility>
#include <vector>
namespace {
struct Symbol {
char letter;
long frequency;
};
struct Partial {
long weight;
int order; // desempata: primero el creado antes
std::vector<std::pair<char, std::string>> codes;
std::string name;
};
struct Heavier {
bool operator()(const Partial& a, const Partial& b) const {
if (a.weight != b.weight) {
return a.weight > b.weight;
}
return a.order > b.order;
}
};
void run(const std::vector<Symbol>& symbols) {
std::priority_queue<Partial, std::vector<Partial>, Heavier> heap;
int order = 0;
long total_symbols = 0;
for (const Symbol& symbol : symbols) {
heap.push({symbol.frequency, order++, {{symbol.letter, ""}}, std::string(1, symbol.letter)});
total_symbols += symbol.frequency;
}
long merged_total = 0;
while (heap.size() > 1) {
Partial first = heap.top();
heap.pop();
Partial second = heap.top();
heap.pop();
std::cout << "une " << first.name << ":" << first.weight << " con " << second.name << ":" << second.weight
<< " -> " << first.weight + second.weight << "\n";
Partial joined{first.weight + second.weight, order++, {}, "(" + first.name + second.name + ")"};
for (auto& [letter, code] : first.codes) {
joined.codes.push_back({letter, "0" + code});
}
for (auto& [letter, code] : second.codes) {
joined.codes.push_back({letter, "1" + code});
}
merged_total += joined.weight;
heap.push(joined);
}
long bits = 0;
const Partial& root = heap.top();
for (const Symbol& symbol : symbols) {
for (const auto& [letter, code] : root.codes) {
if (letter == symbol.letter) {
std::cout << symbol.letter << " (" << symbol.frequency << "): " << code << "\n";
bits += symbol.frequency * static_cast<long>(code.size());
}
}
}
std::size_t fixed_width = 0;
while ((std::size_t{1} << fixed_width) < symbols.size()) {
fixed_width++;
}
std::cout << "bits: " << bits << ", suma de los pesos unidos: " << merged_total
<< ", con codigo fijo de " << fixed_width << " bits: " << total_symbols * static_cast<long>(fixed_width)
<< "\n";
}
} // namespace
int main() {
run({{'a', 5}, {'b', 2}, {'c', 1}, {'d', 1}});
std::cout << "\n";
run({{'a', 45}, {'b', 13}, {'c', 12}, {'d', 16}, {'e', 9}, {'f', 5}});
std::cout << "\n";
run({{'a', 1}, {'b', 1}, {'c', 1}, {'d', 1}});
return 0;
}// Min-heap de enteros sobre un arreglo. Es el heap de la clase: cuenta sus comparaciones.
#ifndef INT_HEAP_H
#define INT_HEAP_H
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>
// Invariante: para todo i > 0, items_[(i - 1) / 2] <= items_[i].
// El árbol es completo porque el arreglo no tiene huecos: el nivel de abajo se llena de izquierda
// a derecha, y no hay punteros que guardar.
class IntHeap {
public:
void push(int value) {
items_.push_back(value);
sift_up(items_.size() - 1);
}
// Requiere que no esté vacío.
void pop() {
assert(!items_.empty());
items_[0] = items_.back();
items_.pop_back();
if (!items_.empty()) {
sift_down(0);
}
}
// Requiere que no esté vacío.
int top() const {
assert(!items_.empty());
return items_[0];
}
bool empty() const { return items_.empty(); }
std::size_t size() const { return items_.size(); }
const std::vector<int>& items() const { return items_; }
long comparisons() const { return comparisons_; }
void check_invariants() const {
for (std::size_t i = 1; i < items_.size(); i++) {
assert(items_[(i - 1) / 2] <= items_[i]);
}
}
private:
// Sube el elemento de i mientras sea menor que su padre.
void sift_up(std::size_t i) {
while (i > 0) {
std::size_t parent = (i - 1) / 2;
comparisons_++;
if (items_[parent] <= items_[i]) {
return;
}
std::swap(items_[parent], items_[i]);
i = parent;
}
}
// Baja el elemento de i mientras algún hijo sea menor. Se cambia con el menor de los dos.
void sift_down(std::size_t i) {
while (true) {
std::size_t left = 2 * i + 1;
std::size_t right = 2 * i + 2;
std::size_t smallest = i;
if (left < items_.size()) {
comparisons_++;
if (items_[left] < items_[smallest]) {
smallest = left;
}
}
if (right < items_.size()) {
comparisons_++;
if (items_[right] < items_[smallest]) {
smallest = right;
}
}
if (smallest == i) {
return;
}
std::swap(items_[i], items_[smallest]);
i = smallest;
}
}
std::vector<int> items_;
long comparisons_ = 0;
};
#endif// std::priority_queue y los algoritmos de heap: el convenio de la biblioteca, y sus cotas contadas.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <vector>
namespace {
template <typename Queue>
void drain(const char* label, Queue queue) {
std::cout << label;
while (!queue.empty()) {
std::cout << " " << queue.top();
queue.pop();
}
std::cout << "\n";
}
} // namespace
int main() {
std::vector<int> values = {5, 3, 8, 1, 4};
// Por defecto usa std::less, y deja en top() al mayor.
std::priority_queue<int> largest_first(std::less<int>(), values);
drain("std::less: ", largest_first);
// Con std::greater, deja en top() al menor.
std::priority_queue<int, std::vector<int>, std::greater<int>> smallest_first(std::greater<int>(), values);
drain("std::greater:", smallest_first);
// Se cuentan las comparaciones de los algoritmos sobre un arreglo.
const int n = 100000;
std::vector<int> items(n);
std::mt19937 generator(13);
for (int& item : items) {
item = static_cast<int>(generator() >> 8);
}
long count = 0;
auto counted = [&count](int a, int b) {
count++;
return a < b;
};
std::make_heap(items.begin(), items.end(), counted);
std::cout << "make_heap: " << count << " comparaciones, cota 3n = " << 3 * n << "\n";
int log_n = static_cast<int>(std::log2(n));
int twice_log_n = static_cast<int>(2 * std::log2(n));
long worst_pop = 0;
long worst_push = 0;
std::vector<int> heap = items;
for (int i = 0; i < 1000; i++) {
count = 0;
std::pop_heap(heap.begin(), heap.end(), counted);
worst_pop = std::max(worst_pop, count);
// Un valor mayor que todos los demás sube hasta la raíz: el peor caso de push_heap.
heap.back() = (1 << 24) + i;
count = 0;
std::push_heap(heap.begin(), heap.end(), counted);
worst_push = std::max(worst_push, count);
}
std::cout << "pop_heap, peor de 1000: " << worst_pop << ", cota 2 log2 n = " << twice_log_n << "\n";
std::cout << "push_heap, peor de 1000: " << worst_push << ", cota log2 n = " << log_n << "\n";
return 0;
}// Los k mayores de n números: heap de tamaño k frente a ordenar todo.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <algorithm>
#include <cstddef>
#include <functional>
#include <iomanip>
#include <iostream>
#include <queue>
#include <random>
#include <vector>
#include "bench.h"
namespace {
// Un min-heap de tamaño k: en la raíz está el peor de los k mejores hasta ahora.
template <typename Greater>
std::vector<int> k_largest(const std::vector<int>& items, std::size_t k, Greater greater) {
std::priority_queue<int, std::vector<int>, Greater> heap(greater);
for (int item : items) {
if (heap.size() < k) {
heap.push(item);
} else if (k > 0 && greater(item, heap.top())) {
heap.pop();
heap.push(item);
}
}
std::vector<int> result;
while (!heap.empty()) {
result.push_back(heap.top());
heap.pop();
}
std::reverse(result.begin(), result.end());
return result;
}
template <typename Greater>
std::vector<int> by_sorting(std::vector<int> items, std::size_t k, Greater greater) {
std::sort(items.begin(), items.end(), greater);
items.resize(std::min(k, items.size()));
return items;
}
} // namespace
int main() {
const std::size_t n = 2000000;
std::vector<int> items(n);
std::mt19937 generator(13);
for (int& item : items) {
item = static_cast<int>(generator() >> 1);
}
std::cout << "n = " << n << "\n";
std::cout << " k comp. heap comp. sort t. heap t. sort\n";
for (std::size_t k : {std::size_t{10}, std::size_t{1000}, std::size_t{100000}, n}) {
long heap_count = 0;
long sort_count = 0;
auto count_heap = [&](int a, int b) {
heap_count++;
return a > b;
};
auto count_sort = [&](int a, int b) {
sort_count++;
return a > b;
};
std::vector<int> from_heap = k_largest(items, k, count_heap);
std::vector<int> from_sort = by_sorting(items, k, count_sort);
if (from_heap != from_sort) {
std::cout << "no coinciden\n";
return 1;
}
double heap_time = median_seconds(3, [&] { keep(k_largest(items, k, std::greater<int>()).size()); });
double sort_time = median_seconds(3, [&] { keep(by_sorting(items, k, std::greater<int>()).size()); });
std::cout << std::setw(8) << k << std::setw(12) << heap_count << std::setw(12) << sort_count
<< std::fixed << std::setprecision(4) << std::setw(9) << heap_time << std::setw(9)
<< sort_time << "\n";
}
return 0;
}// ¿Dónde puede estar el máximo de un min-heap? Revisa todas las permutaciones de 7 claves.
#include <algorithm>
#include <array>
#include <cstddef>
#include <iostream>
#include <numeric>
#include <vector>
namespace {
bool is_min_heap(const std::vector<int>& items) {
for (std::size_t i = 1; i < items.size(); i++) {
if (items[(i - 1) / 2] > items[i]) {
return false;
}
}
return true;
}
} // namespace
int main() {
std::vector<int> items(7);
std::iota(items.begin(), items.end(), 1);
std::array<int, 7> least_at{};
std::array<int, 7> greatest_at{};
int total = 0;
int heaps = 0;
do {
total++;
if (is_min_heap(items)) {
heaps++;
least_at[std::min_element(items.begin(), items.end()) - items.begin()]++;
greatest_at[std::max_element(items.begin(), items.end()) - items.begin()]++;
}
} while (std::next_permutation(items.begin(), items.end()));
std::cout << "permutaciones: " << total << ", son heaps: " << heaps << "\n";
std::cout << "indice minimo maximo\n";
for (std::size_t i = 0; i < items.size(); i++) {
std::cout << " " << i << " " << least_at[i] << " " << greatest_at[i] << "\n";
}
// Con claves repetidas, un nodo interno puede valer lo mismo que el máximo.
std::vector<int> repeated = {1, 2, 2, 2};
std::cout << "[1 2 2 2] es heap: " << (is_min_heap(repeated) ? "si" : "no")
<< ", y el indice 1 vale el maximo y tiene un hijo\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: buscar en un heap como si fuera un árbol de búsqueda.
#include <cstddef>
#include <iostream>
#include <vector>
namespace {
// Baja a la izquierda si la clave es menor, y a la derecha si es mayor. En un heap, eso no sirve.
bool looks_like_bst_search(const std::vector<int>& heap, int key) {
std::size_t i = 0;
while (i < heap.size()) {
if (heap[i] == key) {
return true;
}
i = key < heap[i] ? 2 * i + 1 : 2 * i + 2;
}
return false;
}
} // namespace
int main() {
std::vector<int> heap = {1, 3, 8, 5, 4};
for (int key : heap) {
std::cout << "busca " << key << ": " << (looks_like_bst_search(heap, key) ? "esta" : "NO ESTA") << "\n";
}
return 0;
}// Error a propósito: al bajar, cambiar siempre con el hijo izquierdo, sin mirar cuál es el menor.
#include <cstddef>
#include <iostream>
#include <utility>
#include <vector>
namespace {
void sift_down_wrong(std::vector<int>& heap, std::size_t i) {
while (2 * i + 1 < heap.size() && heap[2 * i + 1] < heap[i]) {
std::swap(heap[i], heap[2 * i + 1]);
i = 2 * i + 1;
}
}
bool is_min_heap(const std::vector<int>& heap) {
for (std::size_t i = 1; i < heap.size(); i++) {
if (heap[(i - 1) / 2] > heap[i]) {
return false;
}
}
return true;
}
} // namespace
int main() {
std::vector<int> heap = {1, 4, 2, 5, 6, 3};
std::cout << "antes de sacar: es heap: " << (is_min_heap(heap) ? "si" : "no") << "\n";
heap[0] = heap.back();
heap.pop_back();
sift_down_wrong(heap, 0);
std::cout << "arreglo:";
for (int item : heap) {
std::cout << " " << item;
}
std::cout << "\nraiz: " << heap[0] << ", es heap: " << (is_min_heap(heap) ? "si" : "no") << "\n";
return 0;
}// Error a propósito: un decodificador que lee un bit antes de mirar si ya está en una hoja.
// Con un árbol de una sola hoja, que es el de un archivo con un solo símbolo, baja a un hijo que no existe.
#include <iostream>
#include <vector>
namespace {
struct Node {
int symbol;
int left;
int right;
};
char decode_one(const std::vector<Node>& nodes, int root, bool bit) {
int node = root;
do {
node = bit ? nodes[node].right : nodes[node].left;
} while (nodes[node].left >= 0);
return static_cast<char>(nodes[node].symbol);
}
} // namespace
int main() {
std::vector<Node> nodes = {{'a', -1, -1}};
std::cout << decode_one(nodes, 0, false) << "\n";
return 0;
}// Error a propósito: descomprimir hasta que se acaben los bits, sin mirar el tamaño original.
#include <iostream>
#include <string>
namespace {
// a = 1, b = 0: un árbol con dos hojas. Decodifica cada bit de una cadena.
std::string decode(const std::string& bits) {
std::string out;
for (char bit : bits) {
out += bit == '1' ? 'a' : 'b';
}
return out;
}
} // namespace
int main() {
// El texto "aab" da los bits 110. El último byte se rellena con ceros hasta 8 bits.
std::string bits = "110";
std::string byte = bits + std::string(8 - bits.size(), '0');
std::cout << "original: aab\n";
std::cout << "bits en el archivo: " << byte << "\n";
std::cout << "sin mirar el tamano: " << decode(byte) << "\n";
std::cout << "con tamano 3: " << decode(byte.substr(0, 3)) << "\n";
return 0;
}// Error a propósito: usar un char como índice de la tabla de frecuencias.
// Se compila con -fsigned-char para que pase lo mismo en toda arquitectura.
#include <array>
#include <cstdio>
#include <string>
int main() {
std::string text = "ñ"; // en UTF-8 son dos bytes: 0xC3 y 0xB1
std::array<long, 256> counts{};
for (char c : text) {
counts.data()[c]++;
}
std::printf("contó %ld\n", counts[0xC3]);
return 0;
}// Error a propósito: dos formas de leer un archivo binario que pierden bytes.
// Se compila con -fsigned-char: con char sin signo, el primer ciclo no termina nunca.
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
int main() {
const std::string path = "build/stop_at_ff.bin";
{
std::ofstream out(path, std::ios::binary);
out << 'a' << ' ' << 'b' << '\xFF' << 'c' << '\n' << 'd';
}
{
std::ifstream in(path, std::ios::binary);
int count = 0;
char c;
// get() devuelve int, para que EOF no se confunda con un byte. Guardarlo en char los confunde.
while ((c = static_cast<char>(in.get())) != EOF) {
count++;
}
std::cout << "con char y EOF: " << count << " bytes de 7\n";
}
{
std::ifstream in(path, std::ios::binary);
int count = 0;
char c;
// >> se salta los espacios en blanco, también en modo binario.
while (in >> c) {
count++;
}
std::cout << "con >>: " << count << " bytes de 7\n";
}
{
std::ifstream in(path, std::ios::binary);
int count = 0;
int c;
while ((c = in.get()) != EOF) {
count++;
}
std::cout << "con int y EOF: " << count << " bytes de 7\n";
}
std::remove(path.c_str());
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos."""
import filecmp
import math
import pathlib
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
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=300,
)
class Examples(unittest.TestCase):
def test_structure_tests_pass(self):
done = run("build/test_structures")
self.assertEqual(done.returncode, 0, done.stdout)
def test_programs_exit_cleanly(self):
for name in ["heap_demo", "where_is_max", "heapsort_demo", "pq_demo", "huffman_demo"]:
with self.subTest(program=name):
done = run(f"build/{name}")
self.assertEqual(done.returncode, 0, done.stdout)
def test_questions_one_and_two(self):
lines = run("build/heap_demo").stdout.splitlines()
self.assertEqual(lines[4], "push 4: [1 3 8 5 4]")
self.assertEqual(lines[5], "pop 1: [3 4 8 5]")
def test_question_four(self):
lines = run("build/huffman_demo").stdout.splitlines()
self.assertEqual(lines[3:8], [
"a (5): 1", "b (2): 00", "c (1): 010", "d (1): 011",
"bits: 15, suma de los pesos unidos: 15, con codigo fijo de 2 bits: 18",
])
def test_greatest_is_always_a_leaf_with_distinct_keys(self):
lines = run("build/where_is_max").stdout.splitlines()
self.assertEqual(lines[0], "permutaciones: 5040, son heaps: 80")
rows = [line.split() for line in lines[2:9]]
self.assertEqual([int(row[1]) for row in rows], [80, 0, 0, 0, 0, 0, 0])
self.assertEqual([int(row[2]) for row in rows[:3]], [0, 0, 0])
self.assertEqual(sum(int(row[2]) for row in rows), 80)
def test_build_counts(self):
lines = run("build/build_counts").stdout.splitlines()
for row in lines[1:13]:
fields = row.split()
n, order, bottom_up, pushes = int(fields[0]), fields[1], int(fields[2]), int(fields[4])
self.assertLessEqual(bottom_up, 2 * n)
if order == "asc":
self.assertEqual(pushes, n - 1)
if order == "azar":
self.assertLess(pushes, 3 * n)
if order == "desc" and n > 1000:
self.assertGreater(pushes, 0.7 * n * math.log2(n))
self.assertEqual(lines[-1], "suma: 1013, niveles que bajaron de verdad: 1013")
def test_heapsort_counts(self):
lines = run("build/heapsort_counts").stdout.splitlines()
for row in lines[1:]:
fields = row.split()
n, kind, comparisons = int(fields[0]), fields[1], int(fields[2])
self.assertLessEqual(comparisons, 2 * n * math.log2(n))
if kind == "iguales":
self.assertLessEqual(comparisons, 3 * n)
def test_heapsort_is_not_stable(self):
lines = run("build/heapsort_demo").stdout.splitlines()
self.assertEqual(lines[-1].strip(), "salida: 1c 2b 2a".strip())
def test_cache_model(self):
rows = [line.split() for line in run("build/heapsort_cache").stdout.splitlines()[1:]]
by_size = {(int(row[0]), row[1]): int(row[3]) for row in rows}
self.assertGreater(by_size[(4000000, "heapsort")], 2 * by_size[(4000000, "mergesort")])
def test_library_bounds(self):
lines = run("build/pq_demo").stdout.splitlines()
self.assertEqual(lines[0].split()[-5:], ["8", "5", "4", "3", "1"])
self.assertEqual(lines[1].split()[-5:], ["1", "3", "4", "5", "8"])
self.assertLessEqual(int(lines[2].split()[1]), 300000)
self.assertLessEqual(int(lines[3].split()[4].rstrip(",")), 32)
self.assertLessEqual(int(lines[4].split()[4].rstrip(",")), 16)
def test_top_k_counts(self):
rows = [line.split() for line in run("build/top_k_bench").stdout.splitlines()[2:]]
self.assertEqual(len(rows), 4)
n = 2000000
for row in rows:
k, heap_comparisons = int(row[0]), int(row[1])
self.assertLessEqual(heap_comparisons, n * (1 + 3 * math.log2(k)))
self.assertLess(int(rows[0][1]), int(rows[0][2]) / 10)
@unittest.skipUnless((HERE / "solution").is_dir(), "la solución no se reparte")
class Solution(unittest.TestCase):
def test_given_programs_match(self):
for name in ["compress.cpp", "decompress.cpp"]:
with self.subTest(file=name):
self.assertTrue(filecmp.cmp(HERE / "lab" / name, HERE / "solution" / name, shallow=False))
def test_interface_of_week_15(self):
"""El comentario de semana 15 y las firmas no se pueden mover sin avisar."""
for folder in ["lab", "solution"]:
text = (HERE / folder / "min_heap.h").read_text(encoding="utf-8")
for signature in [
"template <typename T, typename Less = std::less<T>>",
"MinHeap() = default;",
"explicit MinHeap(Less less) : less_(std::move(less)) {}",
"explicit MinHeap(std::vector<T> items, Less less = Less())",
"void push(T value) {",
"void pop() {",
"const T& top() const {",
"bool empty() const { return items_.empty(); }",
"std::size_t size() const { return items_.size(); }",
"void check_invariants() const",
]:
with self.subTest(folder=folder, signature=signature):
self.assertIn(signature, text)
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.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = os.environ.get("DATA", "../../data")
BUILD = HERE / "build" / LAB_DIR
FIXTURE = HERE / "lab" / "tests" / "fixture.txt"
SAMPLES = HERE / "build" / "samples"
# Un tamaño mal leído de la cabecera hace crecer la salida sin fin: se corta antes de agotar la memoria.
LIMIT_SECONDS = 30
SAMPLE_NAMES = ["empty", "one_byte", "single_symbol", "all_bytes", "random", "special"]
HEAP = [
"new_is_empty", "guide_example", "pop_example", "pop_chooses_smaller_child", "pops_in_order",
"one_element", "other_comparator", "comparator_with_capture", "only_uses_comparator",
"pairs_for_dijkstra", "no_copies_inside", "copy_is_independent", "build_example",
"build_small_sizes", "build_comparisons", "push_comparisons", "pop_comparisons",
"random_against_priority_queue", "random_builds_and_strings",
]
BROKEN = ["broken_root", "broken_right_child", "broken_last_leaf"]
TOP = ["top_k_cases", "top_k_random", "top_k_comparisons", "top_k_keeps_few_objects"]
TREE = [
"count_frequencies", "tree_guide_example", "tree_ties_follow_creation_order",
"tree_weights_above_32_bits", "tree_empty", "tree_one_symbol", "tree_random_is_optimal",
"codes_longer_than_a_word", "tree_write_guide_example", "tree_write_and_read_all_symbols",
"tree_read_one_leaf",
]
COMPRESS = [
"exact_bytes", "exact_size_in_header", "empty_input", "one_byte_input", "one_symbol_input",
"two_symbols", "padding_is_not_data", "all_byte_values", "special_bytes", "random_round_trips",
"reference_file", "rejects_bad_header", "rejects_truncated_file", "rejects_endless_tree",
"rejects_too_many_leaves",
]
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 and "Assertion" 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 "Assertion" in line
or "sin implementar" in line
or re.search(r"(min_heap\.h|top_k\.h|huffman\.h|_tests\.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(binary, name):
return subprocess.run(
[str(BUILD / binary), name], cwd=HERE, text=True, errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=LIMIT_SECONDS,
)
def add_cases(cls, binary, names):
for name in names:
def case(self, name=name):
done = run_case(binary, name)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
setattr(cls, f"test_{name}", case)
return cls
def add_broken(cls, names):
for name in names:
def case(self, name=name):
done = run_case("heap_tests", name)
self.assertIn("sano", done.stdout, "check_invariants falló en un heap sano\n" + short_report(done.stdout))
self.assertNotEqual(done.returncode, 0, "check_invariants no notó el daño")
self.assertNotIn("sin implementar", done.stdout)
setattr(cls, f"test_{name}", case)
class LabCase(unittest.TestCase):
TARGETS = []
@classmethod
def setUpClass(cls):
for target in cls.TARGETS:
code, output = build(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):
for target in ["heap_tests", "huffman_tests", "top_words", "compress", "decompress"]:
(BUILD / target).unlink(missing_ok=True)
code, output = build(target)
self.assertEqual(code, 0, output)
# Solo las advertencias del compilador: make también avisa de otras cosas, como un reloj desfasado.
self.assertNotRegex(output, r"\.(cpp|h):\d+:\d+: warning")
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestHeap(LabCase):
"""Sesión 1, paso 1: MinHeap con comparador, contra std::priority_queue."""
TARGETS = ["heap_tests"]
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTop(LabCase):
"""Sesión 1, paso 3: top_k y el programa top_words, sobre un texto pequeño."""
TARGETS = ["heap_tests", "top_words"]
def run_program(self, *args):
return subprocess.run(
[str(BUILD / "top_words"), *args], cwd=HERE, text=True, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60,
)
def lines(self, *args):
done = self.run_program(*args)
self.assertEqual(done.returncode, 0, short_report(done.stderr))
return done.stdout.splitlines()
def test_top_three_with_ties_in_byte_order(self):
self.assertEqual(self.lines(str(FIXTURE), "3"), ["4 árbol", "3 el", "3 y"])
def test_all_words(self):
self.assertEqual(self.lines(str(FIXTURE), "100"), [
"4 árbol", "3 el", "3 y", "2 ana", "2 llegó", "2 zorra", "2 ñandú",
"1 de", "1 después", "1 la", "1 no", "1 quién", "1 una", "1 zorras",
])
def test_k_zero_prints_nothing(self):
self.assertEqual(self.lines(str(FIXTURE), "0"), [])
def test_bad_usage(self):
self.assertNotEqual(self.run_program().returncode, 0)
self.assertNotEqual(self.run_program("/no/existe.txt", "3").returncode, 0)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTree(LabCase):
"""Sesión 2, pasos 4 y 5: frecuencias, árbol, códigos, escribir y leer el árbol."""
TARGETS = ["huffman_tests"]
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestCompress(LabCase):
"""Sesión 2, pasos 5 y 6: comprimir, descomprimir y comparar archivos con cmp."""
TARGETS = ["huffman_tests", "compress", "decompress"]
def setUp(self):
self.folder = tempfile.TemporaryDirectory()
self.addCleanup(self.folder.cleanup)
self.work = pathlib.Path(self.folder.name)
def run_tool(self, tool, source, target):
return subprocess.run(
[str(BUILD / tool), str(source), str(target)], cwd=HERE, text=True, errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=LIMIT_SECONDS,
)
def assert_round_trip(self, source):
packed = self.work / "packed"
back = self.work / "back"
done = self.run_tool("compress", source, packed)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
done = self.run_tool("decompress", packed, back)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
compared = subprocess.run(["cmp", str(source), str(back)], text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
self.assertEqual(compared.returncode, 0, compared.stdout)
return packed
def test_round_trip_fixture(self):
self.assert_round_trip(FIXTURE)
def test_round_trip_real_text(self):
source = HERE / DATA / "tradiciones.txt"
packed = self.assert_round_trip(source)
self.assertLess(packed.stat().st_size, source.stat().st_size * 0.7)
def test_empty_file_size(self):
self.assertEqual(self.assert_round_trip(SAMPLES / "empty.bin").stat().st_size, 12)
def test_single_symbol_size(self):
self.assertEqual(self.assert_round_trip(SAMPLES / "single_symbol.bin").stat().st_size, 14)
def test_garbage_fails_without_output(self):
target = self.work / "out"
done = self.run_tool("decompress", FIXTURE, target)
self.assertEqual(done.returncode, 1, short_report(done.stdout))
self.assertNotIn("ERROR:", done.stdout)
self.assertFalse(target.exists())
def test_truncated_file_fails_without_output(self):
packed = self.assert_round_trip(FIXTURE)
cut = self.work / "cut"
cut.write_bytes(packed.read_bytes()[:-1])
target = self.work / "out"
done = self.run_tool("decompress", cut, target)
self.assertEqual(done.returncode, 1, short_report(done.stdout))
self.assertNotIn("ERROR:", done.stdout)
self.assertFalse(target.exists())
def test_missing_file(self):
done = self.run_tool("compress", self.work / "no_existe", self.work / "out")
self.assertEqual(done.returncode, 1)
for sample in SAMPLE_NAMES:
def sample_case(self, sample=sample):
self.assert_round_trip(SAMPLES / f"{sample}.bin")
setattr(TestCompress, f"test_round_trip_{sample}", sample_case)
add_cases(TestHeap, "heap_tests", HEAP)
add_broken(TestHeap, BROKEN)
add_cases(TestTop, "heap_tests", TOP)
add_cases(TestTree, "huffman_tests", TREE)
add_cases(TestCompress, "huffman_tests", COMPRESS)
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-13.md"
SLIDES = ROOT / "slides" / "week-13.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 informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"
# La solución del laboratorio no se reparte: sin esa carpeta, estos comandos no se pueden correr.
HAS_SOLUTION = (HERE / "solution").is_dir()
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], 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], 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).
Sin la carpeta solution/, omite el bloque entero si alguno de sus comandos la usa."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
if not HAS_SOLUTION and any("solution/" in command for command, _ in pairs):
return []
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").glob("*.cpp"))
sources += list((HERE / "lab" / "tests").glob("*.cpp"))
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.assertGreater(checked, 0)
@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()// Pruebas de IntHeap y de heapsort: dirigidas y al azar, con semilla fija, contra la biblioteca.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <utility>
#include <vector>
#include "heapsort.h"
#include "int_heap.h"
namespace {
void test_guide_example() {
IntHeap heap;
for (int value : {5, 3, 8, 1, 4}) {
heap.push(value);
heap.check_invariants();
}
assert((heap.items() == std::vector<int>{1, 3, 8, 5, 4}));
heap.pop();
assert((heap.items() == std::vector<int>{3, 4, 8, 5}));
}
void test_random_against_priority_queue() {
std::mt19937 generator(13);
IntHeap heap;
std::priority_queue<int, std::vector<int>, std::greater<int>> reference;
for (int step = 0; step < 100000; step++) {
if (reference.empty() || generator() % 3 != 0) {
int value = static_cast<int>(generator() % 1000);
heap.push(value);
reference.push(value);
} else {
assert(heap.top() == reference.top());
heap.pop();
reference.pop();
}
assert(heap.size() == reference.size());
if (step % 1000 == 0) {
heap.check_invariants();
}
}
}
void test_operation_bounds() {
IntHeap heap;
for (int value = 100000; value > 0; value--) {
long before = heap.comparisons();
heap.push(value);
assert(heap.comparisons() - before <= static_cast<long>(std::log2(double(heap.size()))) + 1);
}
while (!heap.empty()) {
long before = heap.comparisons();
std::size_t size = heap.size();
heap.pop();
assert(heap.comparisons() - before <= 2 * (static_cast<long>(std::log2(double(size))) + 1));
}
}
void test_build_bound() {
for (int n = 0; n < 2000; n += 37) {
std::vector<int> items(n);
for (int i = 0; i < n; i++) {
items[i] = n - i;
}
long comparisons = 0;
build_max_heap(items, [&](int a, int b) {
comparisons++;
return a > b;
});
assert(comparisons <= 2L * n);
for (int i = 1; i < n; i++) {
assert(items[(i - 1) / 2] <= items[i]);
}
}
}
void test_heapsort_against_sort() {
std::mt19937 generator(13);
for (int round = 0; round < 300; round++) {
std::vector<int> items(generator() % 300);
for (int& item : items) {
item = static_cast<int>(generator() % 50);
}
std::vector<int> expected = items;
std::sort(expected.begin(), expected.end());
heapsort(items, std::less<int>());
assert(items == expected);
}
}
void test_heapsort_small_sizes() {
for (int n = 0; n < 8; n++) {
std::vector<int> items(n);
for (int i = 0; i < n; i++) {
items[i] = n - i;
}
heapsort(items, std::less<int>());
assert(std::is_sorted(items.begin(), items.end()));
}
}
} // namespace
int main() {
test_guide_example();
test_random_against_priority_queue();
test_operation_bounds();
test_build_bound();
test_heapsort_against_sort();
test_heapsort_small_sizes();
std::cout << "todas las pruebas pasaron\n";
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
// Escritura y lectura de bits sobre un arreglo de bytes. Viene hecho, con su orden fijo:
// dentro de cada byte se llena primero el bit más significativo.
#ifndef BIT_IO_H
#define BIT_IO_H
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <vector>
using Bytes = std::vector<std::uint8_t>;
// Agrega bits al final de `out`. Llamen a finish() una vez, al terminar.
class BitWriter {
public:
explicit BitWriter(Bytes& out) : out_(out) {}
void put_bit(bool bit) {
current_ = static_cast<std::uint8_t>((current_ << 1) | (bit ? 1 : 0));
used_++;
if (used_ == 8) {
out_.push_back(current_);
current_ = 0;
used_ = 0;
}
}
void put_byte(std::uint8_t byte) {
for (int i = 7; i >= 0; i--) {
put_bit(((byte >> i) & 1) != 0);
}
}
// Completa el último byte con ceros, si quedó a medias.
void finish() {
while (used_ != 0) {
put_bit(false);
}
}
private:
Bytes& out_;
std::uint8_t current_ = 0;
int used_ = 0;
};
// Lee bits de `data`, desde el byte `start`. Si se acaban, lanza std::runtime_error.
class BitReader {
public:
BitReader(const Bytes& data, std::size_t start) : data_(data), position_(start) {}
bool get_bit() {
if (position_ >= data_.size()) {
throw std::runtime_error("archivo truncado");
}
bool bit = ((data_[position_] >> (7 - used_)) & 1) != 0;
used_++;
if (used_ == 8) {
used_ = 0;
position_++;
}
return bit;
}
std::uint8_t get_byte() {
int byte = 0;
for (int i = 0; i < 8; i++) {
byte = (byte << 1) | (get_bit() ? 1 : 0);
}
return static_cast<std::uint8_t>(byte);
}
private:
const Bytes& data_;
std::size_t position_;
int used_ = 0;
};
#endif// Leer y escribir un archivo completo como bytes. Viene hecho: es fácil hacerlo mal.
#ifndef BYTE_FILE_H
#define BYTE_FILE_H
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include "bit_io.h"
// istreambuf_iterator entrega cada byte tal cual: no salta espacios, y no se detiene en 0x00 ni en 0xFF.
// El modo binario evita que el sistema traduzca fines de línea; en Linux no cambia nada, en Windows sí.
inline Bytes read_file(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("no se pudo abrir " + path);
}
// Cada char pasa a uint8_t: 0xC3 vale 195 aquí, tenga o no signo el char.
return Bytes(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
inline void write_file(const std::string& path, const Bytes& bytes) {
std::ofstream out(path, std::ios::binary);
if (!out) {
throw std::runtime_error("no se pudo crear " + path);
}
out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
out.close();
if (!out) {
throw std::runtime_error("no se pudo escribir " + path);
}
}
#endif// Comprime un archivo: compress entrada salida. Viene hecho.
#include <exception>
#include <iostream>
#include "byte_file.h"
#include "huffman.h"
int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "uso: compress entrada salida\n";
return 2;
}
try {
write_file(argv[2], compress_bytes(read_file(argv[1])));
} catch (const std::exception& error) {
std::cerr << "compress: " << error.what() << "\n";
return 1;
}
return 0;
}// Descomprime un archivo: decompress entrada salida. Viene hecho.
// Solo crea la salida si todo salió bien.
#include <exception>
#include <iostream>
#include "byte_file.h"
#include "huffman.h"
int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "uso: decompress entrada salida\n";
return 2;
}
try {
write_file(argv[2], decompress_bytes(read_file(argv[1])));
} catch (const std::exception& error) {
std::cerr << "decompress: " << error.what() << "\n";
return 1;
}
return 0;
}// Compresión de Huffman de un arreglo de bytes. Completen cada función marcada con not_implemented.
// Los tipos y el formato ya están decididos: las pruebas comparan archivos byte por byte.
#ifndef HUFFMAN_H
#define HUFFMAN_H
#include <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>
#include "bit_io.h"
#include "min_heap.h"
#include "not_implemented.h"
// El formato del archivo comprimido:
// bytes 0-3 "HUF1"
// bytes 4-11 tamaño original en bytes, de 64 bits, primero el byte menos significativo
// después, solo si el tamaño no es 0, una corriente de bits (ver bit_io.h):
// el árbol en preorden: un nodo interno es un bit 0 seguido de sus hijos, izquierdo y derecho;
// una hoja es un bit 1 seguido de los 8 bits de su símbolo
// los códigos de los bytes del archivo, en orden
// ceros de relleno hasta completar el último byte
// Un archivo con un solo símbolo tiene un árbol de una hoja, cuyo código tiene 0 bits.
inline constexpr std::size_t header_size = 12;
inline constexpr int max_tree_depth = 255;
using Frequencies = std::array<std::uint64_t, 256>;
using Codes = std::array<std::string, 256>; // el código de cada byte, con caracteres '0' y '1'
// Los nodos viven en un arreglo y se señalan por índice. Así nadie tiene que hacer new ni delete.
struct HuffmanNode {
std::uint64_t weight = 0;
int symbol = -1; // el byte, en una hoja
int left = -1; // hijo izquierdo: el bit 0
int right = -1; // hijo derecho: el bit 1
bool is_leaf() const { return left < 0; }
};
struct HuffmanTree {
std::vector<HuffmanNode> nodes;
int root = -1; // -1 si el árbol está vacío
};
// Lo que guarda el heap: un árbol parcial, con su peso.
struct HeapEntry {
std::uint64_t weight;
int node;
};
// El desempate es parte del formato: a igual peso, primero el nodo creado antes.
struct LighterEntry {
bool operator()(const HeapEntry& a, const HeapEntry& b) const {
if (a.weight != b.weight) {
return a.weight < b.weight;
}
return a.node < b.node;
}
};
// Viene hecho: cuántas veces aparece cada byte.
inline Frequencies count_frequencies(const Bytes& data) {
Frequencies frequencies{};
for (std::uint8_t byte : data) {
frequencies[byte]++;
}
return frequencies;
}
// Paso 4: una hoja por cada byte con frecuencia mayor que 0, de menor a mayor símbolo. Luego, con
// MinHeap<HeapEntry, LighterEntry>, unan los dos más livianos hasta que quede uno: el primero que
// sale es el hijo izquierdo, y el nodo nuevo va al final de nodes. Sin símbolos, root queda en -1.
inline HuffmanTree build_tree(const Frequencies& frequencies) {
(void)frequencies;
not_implemented("build_tree");
}
// Viene hecho: el camino desde la raíz hasta cada hoja, 0 a la izquierda y 1 a la derecha. La recursión
// llega a 255 niveles como máximo. Una sola hoja tiene un código vacío. write_tree se escribe parecido.
inline void collect_codes(const HuffmanTree& tree, int node, std::string& path, Codes& codes) {
const HuffmanNode& current = tree.nodes[node];
if (current.is_leaf()) {
codes[current.symbol] = path;
return;
}
path.push_back('0');
collect_codes(tree, current.left, path, codes);
path.back() = '1';
collect_codes(tree, current.right, path, codes);
path.pop_back();
}
inline Codes make_codes(const HuffmanTree& tree) {
Codes codes;
if (tree.root >= 0) {
std::string path;
collect_codes(tree, tree.root, path, codes);
}
return codes;
}
// Paso 5: el árbol en preorden, como dice el formato. Requiere un árbol no vacío.
inline void write_tree(const HuffmanTree& tree, BitWriter& writer) {
(void)tree;
(void)writer;
not_implemented("write_tree");
}
// Paso 5: lo contrario de write_tree. Los pesos quedan en 0. Un archivo dañado no debe agotar la
// pila ni colgar el programa: lancen std::runtime_error si la profundidad pasa de max_tree_depth
// o si hay más de 256 hojas. Si se acaban los bits, BitReader ya lanza su error.
// Cuidado: un push_back en nodes puede invalidar las referencias a sus elementos.
inline HuffmanTree read_tree(BitReader& reader) {
(void)reader;
not_implemented("read_tree");
}
// Paso 5: la cabecera, y si hay datos, el árbol y los códigos, con un BitWriter sobre el mismo
// arreglo. No olviden finish().
inline Bytes compress_bytes(const Bytes& data) {
(void)data;
not_implemented("compress_bytes");
}
// Paso 5: lanza std::runtime_error si no empieza con la cabecera correcta. Se detiene por el tamaño de
// la cabecera, y no porque se acaben los bits: el relleno no son símbolos. Antes de leer un bit,
// miren si ya están en una hoja.
inline Bytes decompress_bytes(const Bytes& compressed) {
(void)compressed;
not_implemented("decompress_bytes");
}
#endif// Cola de prioridad sobre un heap binario guardado en un arreglo.
// Completen cada método marcado con not_implemented. La semana 15 usará esta clase en Dijkstra:
// no cambien estas firmas.
#ifndef MIN_HEAP_H
#define MIN_HEAP_H
#include <cassert>
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>
#include "not_implemented.h"
// less(a, b) dice si a tiene que estar más cerca de la raíz que b. Debe ser un orden estricto.
// Invariante: para todo i > 0, el hijo items_[i] no es menor que su padre items_[(i - 1) / 2].
// El árbol es completo porque el arreglo no tiene huecos.
// Ownership: el arreglo es un std::vector, así que copiar, mover y destruir ya funcionan.
// La referencia de top() vale hasta el próximo push o pop.
template <typename T, typename Less = std::less<T>>
class MinHeap {
friend struct MinHeapTestAccess;
public:
MinHeap() = default;
explicit MinHeap(Less less) : less_(std::move(less)) {}
// Paso 1: construye de abajo hacia arriba, desde el último padre hasta la raíz. Cuidado con los
// tamaños 0 y 1: size_t no tiene negativos. Una prueba cuenta las comparaciones.
explicit MinHeap(std::vector<T> items, Less less = Less()) : items_(std::move(items)), less_(std::move(less)) {
not_implemented("MinHeap(items)");
}
// Paso 1: agrega al final y sube. Muevan el valor: una prueba cuenta las copias.
void push(T value) {
(void)value;
not_implemented("push");
}
// Paso 1: la última hoja pasa a la raíz, y baja. Requiere que no esté vacío.
void pop() {
assert(!items_.empty());
not_implemented("pop");
}
// Requiere que no esté vacío.
const T& top() const {
assert(!items_.empty());
return items_.front();
}
bool empty() const { return items_.empty(); }
std::size_t size() const { return items_.size(); }
// Paso 1: detiene el programa, con assert, si algún hijo es menor que su padre.
// Usen solo less_: T no tiene por qué tener operator<.
void check_invariants() const { not_implemented("check_invariants"); }
private:
// Sugerimos dos funciones privadas, sift_up y sift_down. Usen std::swap o std::move, sin copiar T.
std::vector<T> items_;
Less 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
Peguen aquí sus tablas y respondan con sus propios números. Digan qué midieron y qué suponen.
## Cómo midieron
Máquina, cómo compilaron, cuántas repeticiones y cuántas corridas.
## Paso 2: construir frente a insertar
Peguen la tabla de `make bench`.
¿Cuántas comparaciones por elemento hace cada forma, con claves al azar y con claves descendentes? ¿Cuál de las dos
columnas crece con n? ¿Se cumple la cota de 2n? ¿Qué tanto se parece la diferencia de tiempos a la de comparaciones?
## Paso 3: los k mayores
Peguen la tabla de `make bench-top`.
¿Para qué k conviene el heap, y desde qué k conviene ordenar todo? ¿Se explica con las comparaciones, o hay algo más?
## Paso 6: cuánto comprime
Peguen la tabla de `make bench-compress`, y anoten el tamaño de cada archivo con `gzip -9n`.
¿Cuál es el archivo que más comprime y cuál el que menos? ¿Por qué `random.bin` y `all_bytes.bin` crecen? ¿De dónde
sale exactamente lo que crecen? ¿Por qué `gzip` le gana a Huffman en los textos?// Los k mejores de una secuencia, con un heap de tamaño k.
#ifndef TOP_K_H
#define TOP_K_H
#include <algorithm>
#include <cstddef>
#include <vector>
#include "min_heap.h"
#include "not_implemented.h"
// Paso 3. better(a, b): a va antes que b. Devuelve los k mejores, del mejor al peor.
// Si hay menos de k, los devuelve todos. Debe costar O(n log k) comparaciones y guardar a lo
// sumo unos k elementos a la vez. Las pruebas cuentan las dos cosas.
// Para armar el heap, un comparador con captura sirve:
// MinHeap<T, decltype(lambda)> heap(lambda);
template <typename T, typename Better>
std::vector<T> top_k(const std::vector<T>& items, std::size_t k, Better better) {
(void)items;
(void)k;
(void)better;
not_implemented("top_k");
}
#endif// Imprime las k palabras más frecuentes de un texto: top_words archivo k
// Cada línea es "veces palabra", en el orden de MoreFrequent. Ver word_count.h.
#include <cstddef>
#include <exception>
#include <iostream>
#include <string>
#include <vector>
#include "top_k.h"
#include "word_count.h"
int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "uso: top_words archivo k\n";
return 2;
}
try {
std::size_t k = std::stoul(argv[2]);
std::string text = read_text(argv[1]);
// Paso 3: cuenten con count_words, elijan con top_k e impriman.
(void)k;
(void)text;
not_implemented("top_words");
} catch (const std::exception& error) {
std::cerr << "top_words: " << error.what() << "\n";
return 1;
}
return 0;
}// Contar las palabras de un texto. Viene hecho: el tema del paso 3 es elegir las k mayores.
#ifndef WORD_COUNT_H
#define WORD_COUNT_H
#include <cstddef>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
#include "words.h"
struct WordCount {
std::string word;
long count = 0;
};
// El orden de la salida: más veces primero y, si empatan, el orden de bytes de la palabra.
struct MoreFrequent {
bool operator()(const WordCount& a, const WordCount& b) const {
if (a.count != b.count) {
return a.count > b.count;
}
return a.word < b.word;
}
};
inline std::string read_text(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) {
throw std::runtime_error("no se pudo abrir " + path);
}
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}
// Una entrada por cada palabra distinta del texto, en cualquier orden.
inline std::vector<WordCount> count_words(const std::string& text) {
std::unordered_map<std::string, long> counts;
for (const std::string& word : split_words(text)) {
counts[word]++;
}
std::vector<WordCount> result;
result.reserve(counts.size());
for (const auto& [word, count] : counts) {
result.push_back({word, count});
}
return result;
}
#endif// Parte un texto en UTF-8 en palabras. Viene hecho: no es el tema del laboratorio.
#ifndef WORDS_H
#define WORDS_H
#include <cstddef>
#include <string>
#include <vector>
// Una palabra es una secuencia de letras: ASCII, o las de dos bytes que empiezan con 0xC3 (á, é, ñ,
// ü...), salvo × y ÷. Lo demás separa, incluidos ¿, ¡, « y la raya. Pasa a minúsculas también
// Á, É, Ñ: tolower de C trabaja por byte y no las cambiaría.
inline std::vector<std::string> split_words(const std::string& text) {
std::vector<std::string> words;
std::string current;
std::size_t i = 0;
while (i < text.size()) {
unsigned char first = static_cast<unsigned char>(text[i]);
if ((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) {
current += static_cast<char>(first >= 'a' ? first : first + ('a' - 'A'));
i++;
continue;
}
if (first == 0xC3 && i + 1 < text.size()) {
unsigned char second = static_cast<unsigned char>(text[i + 1]);
if (second >= 0x80 && second != 0x97 && second != 0xB7) {
// En este bloque, cada mayúscula está 0x20 antes que su minúscula.
current += static_cast<char>(first);
current += static_cast<char>(second < 0x9F ? second + 0x20 : second);
i += 2;
continue;
}
}
if (!current.empty()) {
words.push_back(current);
current.clear();
}
// Saltar el carácter completo: los bytes 10xxxxxx continúan al anterior.
i++;
while (i < text.size() && (static_cast<unsigned char>(text[i]) & 0xC0) == 0x80) {
i++;
}
}
if (!current.empty()) {
words.push_back(current);
}
return words;
}
#endif// Paso 6: cuánto comprime cada archivo y cuánto tarda. Compilar con -O2 y sin sanitizers.
// Con --sizes solo imprime los tamaños, que no dependen de la máquina. Los tiempos sí dependen.
#include <cstring>
#include <exception>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "bench.h"
#include "byte_file.h"
#include "huffman.h"
int main(int argc, char** argv) {
bool sizes_only = argc > 1 && std::strcmp(argv[1], "--sizes") == 0;
int first = sizes_only ? 2 : 1;
if (argc <= first) {
std::cerr << "uso: bench_compress [--sizes] archivo...\n";
return 2;
}
std::cout << "archivo original comprimido %";
std::cout << (sizes_only ? "\n" : " t.comp t.desc\n");
try {
for (int i = first; i < argc; i++) {
Bytes data = read_file(argv[i]);
Bytes compressed = compress_bytes(data);
if (decompress_bytes(compressed) != data) {
std::cout << "no volvio igual: " << argv[i] << "\n";
return 1;
}
std::string name = std::filesystem::path(argv[i]).filename().string();
std::cout << std::left << std::setw(20) << name << std::right << std::setw(11) << data.size()
<< std::setw(12) << compressed.size() << std::fixed << std::setprecision(1) << std::setw(8);
if (data.empty()) {
std::cout << "-"; // no se puede dividir entre 0
} else {
std::cout << 100.0 * double(compressed.size()) / double(data.size());
}
if (!sizes_only) {
double compress_time = median_seconds(3, [&] { keep(compress_bytes(data).size()); });
double decompress_time = median_seconds(3, [&] { keep(decompress_bytes(compressed).size()); });
std::cout << std::setprecision(4) << std::setw(9) << compress_time << std::setw(9) << decompress_time;
}
std::cout << "\n";
}
} catch (const std::exception& error) {
std::cerr << "bench_compress: " << error.what() << "\n";
return 1;
}
return 0;
}// Paso 2: construir de abajo hacia arriba frente a n inserciones.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>
#include "bench.h"
#include "min_heap.h"
namespace {
struct CountingLess {
long* count;
bool operator()(int a, int b) const {
(*count)++;
return a < b;
}
};
std::vector<int> make_input(const std::string& order, int n) {
std::vector<int> items(n);
std::iota(items.begin(), items.end(), 0);
if (order == "desc") {
std::reverse(items.begin(), items.end());
} else if (order == "azar") {
std::mt19937 generator(13);
std::shuffle(items.begin(), items.end(), generator);
}
return items;
}
} // namespace
int main() {
std::cout << " n orden comp.build /n comp.push /n t.build t.push\n";
for (int n : {1 << 14, 1 << 16, 1 << 18, 1 << 20, 1 << 22}) {
for (const std::string order : {"azar", "desc"}) {
std::vector<int> items = make_input(order, n);
long build_count = 0;
{
MinHeap<int, CountingLess> heap(items, CountingLess{&build_count});
keep(heap.top());
}
long push_count = 0;
{
MinHeap<int, CountingLess> heap{CountingLess{&push_count}};
for (int item : items) {
heap.push(item);
}
keep(heap.top());
}
double build_time = median_seconds(3, [&] {
MinHeap<int> heap(items);
keep(heap.top());
});
double push_time = median_seconds(3, [&] {
MinHeap<int> heap;
for (int item : items) {
heap.push(item);
}
keep(heap.top());
});
std::cout << std::setw(8) << n << std::setw(6) << order << std::setw(12) << build_count << std::fixed
<< std::setprecision(2) << std::setw(5) << double(build_count) / n << std::setw(11)
<< push_count << std::setw(6) << double(push_count) / n << std::setprecision(4) << std::setw(8)
<< build_time << std::setw(8) << push_time << "\n";
}
}
return 0;
}// Paso 3: las k palabras más frecuentes con un heap, frente a ordenar todas.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
// Se mide solo la selección: contar las palabras cuesta lo mismo en los dos casos.
#include <algorithm>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "bench.h"
#include "top_k.h"
#include "word_count.h"
namespace {
std::vector<WordCount> by_sorting(std::vector<WordCount> counts, std::size_t k) {
std::sort(counts.begin(), counts.end(), MoreFrequent());
counts.resize(std::min(k, counts.size()));
return counts;
}
bool same_words(const std::vector<WordCount>& a, const std::vector<WordCount>& b) {
if (a.size() != b.size()) {
return false;
}
for (std::size_t i = 0; i < a.size(); i++) {
if (a[i].word != b[i].word || a[i].count != b[i].count) {
return false;
}
}
return true;
}
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: bench_top archivo\n";
return 2;
}
std::vector<WordCount> counts = count_words(read_text(argv[1]));
std::cout << "palabras distintas: " << counts.size() << "\n";
std::cout << " k comp.heap comp.sort t.heap t.sort\n";
for (std::size_t k : {std::size_t{10}, std::size_t{100}, std::size_t{1000}, counts.size()}) {
long heap_count = 0;
long sort_count = 0;
std::vector<WordCount> from_heap = top_k(counts, k, [&heap_count](const WordCount& a, const WordCount& b) {
heap_count++;
return MoreFrequent()(a, b);
});
std::vector<WordCount> copy = counts;
std::sort(copy.begin(), copy.end(), [&sort_count](const WordCount& a, const WordCount& b) {
sort_count++;
return MoreFrequent()(a, b);
});
copy.resize(std::min(k, copy.size()));
if (!same_words(from_heap, copy)) {
std::cout << "no coinciden, con k = " << k << "\n";
return 1;
}
double heap_time = median_seconds(5, [&] { keep(top_k(counts, k, MoreFrequent()).size()); });
double sort_time = median_seconds(5, [&] { keep(by_sorting(counts, k).size()); });
std::cout << std::setw(8) << k << std::setw(11) << heap_count << std::setw(11) << sort_count << std::fixed
<< std::setprecision(5) << std::setw(10) << heap_time << std::setw(10) << sort_time << "\n";
}
return 0;
}—¿Quién llegó? ¡El Ñandú y el ñandú!
Árbol, árbol y ÁRBOL: el árbol de la zorra.
Una zorra, 12 zorras y Ana. «Ana» llegó después, ¿no?// Pruebas públicas de los pasos 1 y 3. Se corre una por proceso: heap_tests nombre.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <functional>
#include <iostream>
#include <memory>
#include <queue>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include "min_heap.h"
#include "top_k.h"
// Permite a las pruebas mirar y dañar el arreglo.
struct MinHeapTestAccess {
template <typename Heap>
static auto& items(Heap& heap) {
return heap.items_;
}
};
namespace {
using IntHeap = MinHeap<int>;
std::vector<int> items_of(IntHeap& heap) { return MinHeapTestAccess::items(heap); }
// Comparador que cuenta. El contador vive afuera porque el heap guarda su propia copia.
struct CountingLess {
long* count;
bool operator()(int a, int b) const {
(*count)++;
return a < b;
}
};
// Valor sin operator<: si check_invariants lo necesitara, la prueba no compilaría.
struct NoOrder {
int value = 0;
};
struct NoOrderLess {
bool operator()(const NoOrder& a, const NoOrder& b) const { return a.value < b.value; }
};
// Cuenta sus copias: bajar y subir no deben copiar nada.
struct CountedCopies {
int value = 0;
inline static long copies = 0;
explicit CountedCopies(int v) : value(v) {}
CountedCopies(const CountedCopies& other) : value(other.value) { copies++; }
CountedCopies(CountedCopies&& other) noexcept : value(other.value) {}
CountedCopies& operator=(const CountedCopies& other) {
value = other.value;
copies++;
return *this;
}
CountedCopies& operator=(CountedCopies&& other) noexcept {
value = other.value;
return *this;
}
bool operator<(const CountedCopies& other) const { return value < other.value; }
};
// Cuenta cuántos objetos viven a la vez.
struct Live {
int value = 0;
inline static long count = 0;
inline static long peak = 0;
explicit Live(int v) : value(v) { born(); }
Live(const Live& other) : value(other.value) { born(); }
Live& operator=(const Live& other) = default;
~Live() { count--; }
static void born() {
count++;
peak = std::max(peak, count);
}
};
void test_new_is_empty() {
IntHeap heap;
assert(heap.empty());
assert(heap.size() == 0);
heap.check_invariants();
}
void test_guide_example() {
IntHeap heap;
const std::vector<std::vector<int>> expected = {{5}, {3, 5}, {3, 5, 8}, {1, 3, 8, 5}, {1, 3, 8, 5, 4}};
const std::vector<int> values = {5, 3, 8, 1, 4};
for (std::size_t i = 0; i < values.size(); i++) {
heap.push(values[i]);
heap.check_invariants();
assert(items_of(heap) == expected[i]);
assert(heap.size() == i + 1);
}
assert(heap.top() == 1);
}
void test_pop_example() {
IntHeap heap;
for (int value : {5, 3, 8, 1, 4}) {
heap.push(value);
}
heap.pop();
assert((items_of(heap) == std::vector<int>{3, 4, 8, 5}));
heap.check_invariants();
}
void test_pop_chooses_smaller_child() {
// Si baja siempre por la izquierda, la raíz queda mal.
IntHeap heap(std::vector<int>{1, 4, 2, 5, 6, 3});
assert((items_of(heap) == std::vector<int>{1, 4, 2, 5, 6, 3}));
heap.pop();
assert(heap.top() == 2);
heap.check_invariants();
}
void test_pops_in_order() {
IntHeap heap;
for (int value : {9, 2, 7, 2, 8, 1, 5, 9, 0}) {
heap.push(value);
}
std::vector<int> out;
while (!heap.empty()) {
out.push_back(heap.top());
heap.pop();
heap.check_invariants();
}
assert((out == std::vector<int>{0, 1, 2, 2, 5, 7, 8, 9, 9}));
}
void test_one_element() {
MinHeap<std::string> heap;
heap.push("solo");
assert(heap.top() == "solo");
heap.pop();
assert(heap.empty());
heap.push("otra vez");
assert(heap.top() == "otra vez");
}
void test_other_comparator() {
MinHeap<int, std::greater<int>> heap;
for (int value : {5, 3, 8, 1, 4}) {
heap.push(value);
}
heap.check_invariants();
assert(heap.top() == 8);
heap.pop();
assert(heap.top() == 5);
}
void test_comparator_with_capture() {
std::vector<int> distance = {30, 10, 20};
auto closer = [&distance](int a, int b) { return distance[a] < distance[b]; };
MinHeap<int, decltype(closer)> heap(closer);
heap.push(0);
heap.push(1);
heap.push(2);
assert(heap.top() == 1);
MinHeap<int, decltype(closer)> built(std::vector<int>{0, 1, 2}, closer);
assert(built.top() == 1);
}
void test_only_uses_comparator() {
MinHeap<NoOrder, NoOrderLess> heap;
for (int value : {4, 1, 3}) {
heap.push(NoOrder{value});
}
heap.check_invariants();
assert(heap.top().value == 1);
MinHeap<NoOrder, NoOrderLess> built(std::vector<NoOrder>{{3}, {2}, {1}});
built.check_invariants();
assert(built.top().value == 1);
}
void test_pairs_for_dijkstra() {
// La semana 15 guarda (distancia, nodo). std::pair ya compara así.
MinHeap<std::pair<long, int>> heap;
heap.push({30, 1});
heap.push({10, 2});
heap.push({10, 1});
assert((heap.top() == std::pair<long, int>{10, 1}));
heap.pop();
assert((heap.top() == std::pair<long, int>{10, 2}));
}
void test_no_copies_inside() {
std::vector<CountedCopies> start;
start.reserve(2000);
for (int i = 2000; i > 0; i--) {
start.emplace_back(i);
}
CountedCopies::copies = 0;
MinHeap<CountedCopies> heap(std::move(start));
for (int i = 0; i < 2000; i++) {
heap.push(CountedCopies(i));
}
while (!heap.empty()) {
heap.pop();
}
assert(CountedCopies::copies == 0);
}
void test_copy_is_independent() {
IntHeap heap(std::vector<int>{3, 1, 2});
IntHeap copy = heap;
copy.pop();
assert(heap.size() == 3);
assert(heap.top() == 1);
assert(copy.top() == 2);
}
void test_build_example() {
IntHeap heap(std::vector<int>{5, 3, 8, 1, 4});
assert((items_of(heap) == std::vector<int>{1, 3, 8, 5, 4}));
IntHeap larger(std::vector<int>{9, 8, 7, 6, 5, 4, 3, 2, 1});
assert((items_of(larger) == std::vector<int>{1, 2, 3, 6, 5, 4, 7, 8, 9}));
}
void test_build_small_sizes() {
for (int n = 0; n <= 70; n++) {
std::vector<int> values(n);
for (int i = 0; i < n; i++) {
values[i] = n - i;
}
IntHeap heap(values);
assert(heap.size() == static_cast<std::size_t>(n));
heap.check_invariants();
for (int i = 1; i <= n; i++) {
assert(heap.top() == i);
heap.pop();
}
assert(heap.empty());
}
}
void test_build_comparisons() {
// De abajo hacia arriba son unas 2n. La biblioteca promete 3n. n push sobre esto, unas 15n.
const int n = 1 << 16;
std::vector<int> descending(n);
for (int i = 0; i < n; i++) {
descending[i] = n - i;
}
long count = 0;
MinHeap<int, CountingLess> heap(descending, CountingLess{&count});
assert(count <= 3L * n);
heap.check_invariants();
}
void test_push_comparisons() {
const int n = 1 << 14;
const long log_n = 14;
long count = 0;
MinHeap<int, CountingLess> descending_heap{CountingLess{&count}};
for (int value = n; value > 0; value--) {
long before = count;
descending_heap.push(value);
assert(count - before <= log_n);
}
// Un valor mayor que el de su padre no sube: una sola comparación.
count = 0;
MinHeap<int, CountingLess> ascending_heap{CountingLess{&count}};
for (int value = 0; value < n; value++) {
ascending_heap.push(value);
}
assert(count <= n);
}
void test_pop_comparisons() {
const int n = 1 << 14;
const long log_n = 14;
long count = 0;
std::vector<int> values(n);
std::mt19937 generator(13);
for (int& value : values) {
value = static_cast<int>(generator() % 1000000);
}
MinHeap<int, CountingLess> heap(values, CountingLess{&count});
while (!heap.empty()) {
long before = count;
heap.pop();
assert(count - before <= 2 * log_n);
}
}
void test_random_against_priority_queue() {
std::mt19937 generator(13);
IntHeap heap;
std::priority_queue<int, std::vector<int>, std::greater<int>> reference;
for (int step = 0; step < 200000; step++) {
if (reference.empty() || generator() % 5 < 3) {
int value = static_cast<int>(generator() % 1000);
heap.push(value);
reference.push(value);
} else {
assert(heap.top() == reference.top());
heap.pop();
reference.pop();
}
assert(heap.size() == reference.size());
assert(heap.empty() == reference.empty());
if (step % 5000 == 0) {
heap.check_invariants();
}
}
heap.check_invariants();
}
void test_random_builds_and_strings() {
std::mt19937 generator(13);
for (int round = 0; round < 200; round++) {
std::vector<std::string> values(generator() % 200);
for (std::string& value : values) {
value = std::to_string(generator() % 500);
}
MinHeap<std::string> heap(values);
heap.check_invariants();
std::sort(values.begin(), values.end());
for (const std::string& value : values) {
assert(heap.top() == value);
heap.pop();
}
assert(heap.empty());
}
}
// Cada una daña un heap sano. El programa debe imprimir "sano" y luego detenerse.
template <typename Damage>
void broken(Damage damage) {
IntHeap heap(std::vector<int>{10, 20, 30, 40, 50, 60, 70, 80, 90, 100});
heap.check_invariants();
std::cout << "sano" << std::endl;
damage(MinHeapTestAccess::items(heap));
heap.check_invariants();
}
void test_broken_root() {
broken([](std::vector<int>& items) { items[0] = 25; });
}
void test_broken_right_child() {
broken([](std::vector<int>& items) { items[2] = 5; });
}
void test_broken_last_leaf() {
broken([](std::vector<int>& items) { items.back() = 45; });
}
// ---- Paso 3 ----
void test_top_k_cases() {
std::vector<int> values = {5, 1, 9, 3, 7, 9};
auto greater = std::greater<int>();
assert(top_k(values, 0, greater).empty());
assert((top_k(values, 1, greater) == std::vector<int>{9}));
assert((top_k(values, 3, greater) == std::vector<int>{9, 9, 7}));
assert((top_k(values, 6, greater) == std::vector<int>{9, 9, 7, 5, 3, 1}));
assert((top_k(values, 100, greater) == std::vector<int>{9, 9, 7, 5, 3, 1}));
assert(top_k(std::vector<int>{}, 3, greater).empty());
assert((top_k(values, 2, std::less<int>()) == std::vector<int>{1, 3}));
}
void test_top_k_random() {
std::mt19937 generator(13);
for (int round = 0; round < 300; round++) {
std::vector<int> values(generator() % 400);
for (int& value : values) {
value = static_cast<int>(generator() % 300);
}
std::size_t k = generator() % 450;
std::vector<int> expected = values;
std::sort(expected.begin(), expected.end(), std::greater<int>());
expected.resize(std::min(k, expected.size()));
assert(top_k(values, k, std::greater<int>()) == expected);
}
}
void test_top_k_comparisons() {
// Con valores crecientes, cada uno entra al heap. Ordenar todo usaría unas 17n.
const int n = 100000;
const std::size_t k = 8;
std::vector<int> values(n);
for (int i = 0; i < n; i++) {
values[i] = i;
}
long count = 0;
std::vector<int> result = top_k(values, k, [&count](int a, int b) {
count++;
return a > b;
});
assert(result.front() == n - 1);
assert(result.size() == k);
assert(count <= 10L * n);
}
void test_top_k_keeps_few_objects() {
const int n = 20000;
const std::size_t k = 10;
std::vector<Live> values;
values.reserve(n);
for (int i = 0; i < n; i++) {
values.emplace_back((i * 7919) % n);
}
Live::peak = Live::count;
std::vector<Live> result = top_k(values, k, [](const Live& a, const Live& b) { return a.value > b.value; });
assert(result.size() == k);
assert(result[0].value == n - 1);
// El heap, el resultado y algún temporal: de ninguna forma n copias.
assert(Live::peak - n <= 3 * static_cast<long>(k) + 16);
}
struct Test {
const char* name;
void (*run)();
};
const Test tests[] = {
{"new_is_empty", test_new_is_empty},
{"guide_example", test_guide_example},
{"pop_example", test_pop_example},
{"pop_chooses_smaller_child", test_pop_chooses_smaller_child},
{"pops_in_order", test_pops_in_order},
{"one_element", test_one_element},
{"other_comparator", test_other_comparator},
{"comparator_with_capture", test_comparator_with_capture},
{"only_uses_comparator", test_only_uses_comparator},
{"pairs_for_dijkstra", test_pairs_for_dijkstra},
{"no_copies_inside", test_no_copies_inside},
{"copy_is_independent", test_copy_is_independent},
{"build_example", test_build_example},
{"build_small_sizes", test_build_small_sizes},
{"build_comparisons", test_build_comparisons},
{"push_comparisons", test_push_comparisons},
{"pop_comparisons", test_pop_comparisons},
{"random_against_priority_queue", test_random_against_priority_queue},
{"random_builds_and_strings", test_random_builds_and_strings},
{"broken_root", test_broken_root},
{"broken_right_child", test_broken_right_child},
{"broken_last_leaf", test_broken_last_leaf},
{"top_k_cases", test_top_k_cases},
{"top_k_random", test_top_k_random},
{"top_k_comparisons", test_top_k_comparisons},
{"top_k_keeps_few_objects", test_top_k_keeps_few_objects},
};
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: heap_tests nombre\n";
return 2;
}
for (const Test& test : tests) {
if (std::strcmp(test.name, argv[1]) == 0) {
test.run();
return 0;
}
}
std::cerr << "no existe la prueba " << argv[1] << "\n";
return 2;
}// Pruebas públicas de los pasos 4 y 5. Se corre una por proceso: huffman_tests nombre.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
#include "byte_file.h"
#include "huffman.h"
namespace {
Bytes bytes_of(const std::string& text) { return Bytes(text.begin(), text.end()); }
Frequencies frequencies_of(const std::vector<std::pair<int, std::uint64_t>>& pairs) {
Frequencies frequencies{};
for (const auto& [symbol, count] : pairs) {
frequencies[symbol] = count;
}
return frequencies;
}
std::uint64_t total_bits(const Frequencies& frequencies, const Codes& codes) {
std::uint64_t bits = 0;
for (int symbol = 0; symbol < 256; symbol++) {
bits += frequencies[symbol] * codes[symbol].size();
}
return bits;
}
// El costo óptimo: la suma de los pesos de cada unión, con la cola de la biblioteca.
std::uint64_t optimal_bits(const Frequencies& frequencies) {
std::priority_queue<std::uint64_t, std::vector<std::uint64_t>, std::greater<std::uint64_t>> queue;
for (std::uint64_t count : frequencies) {
if (count > 0) {
queue.push(count);
}
}
std::uint64_t total = 0;
while (queue.size() > 1) {
std::uint64_t first = queue.top();
queue.pop();
std::uint64_t second = queue.top();
queue.pop();
total += first + second;
queue.push(first + second);
}
return total;
}
void assert_prefix_free(const Frequencies& frequencies, const Codes& codes) {
for (int a = 0; a < 256; a++) {
for (int b = 0; b < 256; b++) {
if (a != b && frequencies[a] > 0 && frequencies[b] > 0) {
assert(codes[b].rfind(codes[a], 0) != 0);
}
}
}
}
void assert_round_trip(const Bytes& data) {
Bytes compressed = compress_bytes(data);
assert(decompress_bytes(compressed) == data);
}
// Comprueba que descomprimir lance std::runtime_error, y no otra cosa.
void assert_rejected(const Bytes& compressed) {
bool rejected = false;
try {
decompress_bytes(compressed);
} catch (const std::runtime_error&) {
rejected = true;
}
assert(rejected);
}
void test_count_frequencies() {
Bytes data = {'a', 'b', 'a', 0x00, 0xFF, 0xFF, 0xC3, 0xFF};
Frequencies frequencies = count_frequencies(data);
assert(frequencies['a'] == 2);
assert(frequencies['b'] == 1);
assert(frequencies[0x00] == 1);
assert(frequencies[0xFF] == 3);
assert(frequencies[0xC3] == 1);
assert(frequencies['c'] == 0);
assert(count_frequencies(Bytes{})[0] == 0);
}
void test_tree_guide_example() {
Frequencies frequencies = frequencies_of({{'a', 5}, {'b', 2}, {'c', 1}, {'d', 1}});
HuffmanTree tree = build_tree(frequencies);
assert(tree.nodes.size() == 7);
assert(tree.root == 6);
assert(tree.nodes[tree.root].weight == 9);
Codes codes = make_codes(tree);
assert(codes['a'] == "1");
assert(codes['b'] == "00");
assert(codes['c'] == "010");
assert(codes['d'] == "011");
assert(total_bits(frequencies, codes) == 15);
}
void test_tree_ties_follow_creation_order() {
Frequencies frequencies = frequencies_of({{'a', 1}, {'b', 1}, {'c', 1}, {'d', 1}});
Codes codes = make_codes(build_tree(frequencies));
assert(codes['a'] == "00");
assert(codes['b'] == "01");
assert(codes['c'] == "10");
assert(codes['d'] == "11");
}
void test_tree_weights_above_32_bits() {
Frequencies frequencies = frequencies_of({{'a', 5000000000ULL}, {'b', 1}, {'c', 1}, {'d', 3000000000ULL}});
Codes codes = make_codes(build_tree(frequencies));
assert(codes['a'].size() == 1);
assert(codes['d'].size() == 2);
assert(codes['b'].size() == 3);
}
void test_tree_empty() {
HuffmanTree tree = build_tree(Frequencies{});
assert(tree.root == -1);
assert(tree.nodes.empty());
assert(make_codes(tree)[0].empty());
}
void test_tree_one_symbol() {
Frequencies frequencies = frequencies_of({{0xFF, 42}});
HuffmanTree tree = build_tree(frequencies);
assert(tree.nodes.size() == 1);
assert(tree.root == 0);
assert(tree.nodes[0].is_leaf());
assert(tree.nodes[0].symbol == 0xFF);
assert(make_codes(tree)[0xFF].empty());
}
void test_tree_random_is_optimal() {
std::mt19937 generator(13);
for (int round = 0; round < 300; round++) {
Frequencies frequencies{};
int symbols = 2 + static_cast<int>(generator() % 255);
for (int i = 0; i < symbols; i++) {
frequencies[generator() % 256] = 1 + generator() % 1000;
}
HuffmanTree tree = build_tree(frequencies);
Codes codes = make_codes(tree);
assert_prefix_free(frequencies, codes);
assert(total_bits(frequencies, codes) == optimal_bits(frequencies));
}
}
void test_codes_longer_than_a_word() {
// Frecuencias de Fibonacci: cada unión absorbe al árbol anterior, y los códigos se alargan.
Frequencies frequencies{};
std::uint64_t a = 1;
std::uint64_t b = 1;
for (int symbol = 0; symbol < 80; symbol++) {
frequencies[symbol] = a;
std::uint64_t next = a + b;
a = b;
b = next;
}
HuffmanTree tree = build_tree(frequencies);
Codes codes = make_codes(tree);
std::size_t longest = 0;
for (const std::string& code : codes) {
longest = std::max(longest, code.size());
}
assert(longest == 79);
assert_prefix_free(frequencies, codes);
// El árbol también viaja por el archivo.
Bytes out;
BitWriter writer(out);
write_tree(tree, writer);
writer.finish();
BitReader reader(out, 0);
assert(make_codes(read_tree(reader)) == codes);
}
void test_tree_write_guide_example() {
Frequencies frequencies = frequencies_of({{'a', 2}, {'b', 1}});
HuffmanTree tree = build_tree(frequencies);
Bytes out;
BitWriter writer(out);
write_tree(tree, writer);
writer.finish();
// 0 | 1 01100010 | 1 01100001, con relleno
assert((out == Bytes{0x58, 0xAC, 0x20}));
}
void test_tree_write_and_read_all_symbols() {
Frequencies frequencies{};
for (int symbol = 0; symbol < 256; symbol++) {
frequencies[symbol] = 1 + symbol % 7;
}
HuffmanTree tree = build_tree(frequencies);
Bytes out;
BitWriter writer(out);
write_tree(tree, writer);
writer.finish();
assert(out.size() == 320); // 255 + 256 * 9 = 2559 bits
BitReader reader(out, 0);
HuffmanTree copy = read_tree(reader);
assert(make_codes(copy) == make_codes(tree));
}
void test_tree_read_one_leaf() {
Bytes bits = {0xFF, 0x80}; // 1, y el símbolo 11111111
BitReader reader(bits, 0);
HuffmanTree tree = read_tree(reader);
assert(tree.nodes.size() == 1);
assert(tree.root == 0);
assert(tree.nodes[0].symbol == 0xFF);
}
void test_exact_bytes() {
Bytes expected = {'H', 'U', 'F', '1', 3, 0, 0, 0, 0, 0, 0, 0, 0x58, 0xAC, 0x38};
assert(compress_bytes(bytes_of("aab")) == expected);
}
void test_exact_size_in_header() {
Bytes data(70000, 'x');
Bytes compressed = compress_bytes(data);
// 70000 = 0x11170: el byte menos significativo va primero.
assert(compressed[4] == 0x70);
assert(compressed[5] == 0x11);
assert(compressed[6] == 0x01);
assert(compressed[7] == 0x00);
}
void test_empty_input() {
Bytes compressed = compress_bytes(Bytes{});
assert((compressed == Bytes{'H', 'U', 'F', '1', 0, 0, 0, 0, 0, 0, 0, 0}));
assert(decompress_bytes(compressed).empty());
}
void test_one_byte_input() {
Bytes data = {0xFF};
Bytes compressed = compress_bytes(data);
assert(compressed.size() == header_size + 2);
assert(decompress_bytes(compressed) == data);
}
void test_one_symbol_input() {
Bytes data(100000, 'a');
Bytes compressed = compress_bytes(data);
// Una hoja son 9 bits, y sus códigos no ocupan nada.
assert(compressed.size() == header_size + 2);
assert(decompress_bytes(compressed) == data);
}
void test_two_symbols() {
assert_round_trip(bytes_of("ab"));
assert_round_trip(bytes_of("aaaaaaab"));
Bytes data = compress_bytes(Bytes(16, 'a'));
assert(decompress_bytes(data).size() == 16);
}
void test_padding_is_not_data() {
// Cualquier relleno, de 0 a 7 bits, debe ignorarse: el tamaño manda.
for (std::size_t n = 1; n <= 40; n++) {
Bytes data(n, 'b');
data[0] = 'a';
assert_round_trip(data);
}
}
void test_all_byte_values() {
Bytes data;
for (int round = 0; round < 5; round++) {
for (int value = 0; value < 256; value++) {
data.push_back(static_cast<std::uint8_t>(value));
}
}
assert_round_trip(data);
}
void test_special_bytes() {
assert_round_trip(Bytes{0x00, 0x00, 0xFF, 0x0A, 0x0D, 0x0A, 0x1A, 0x20, 0x00});
}
void test_random_round_trips() {
std::mt19937 generator(13);
for (int round = 0; round < 200; round++) {
Bytes data(generator() % 2000);
int alphabet = 1 + static_cast<int>(generator() % 256);
for (std::uint8_t& byte : data) {
byte = static_cast<std::uint8_t>(generator() % alphabet);
}
assert_round_trip(data);
}
}
void test_reference_file() {
// Lo comprimió la solución del curso. Si lee esto, su formato coincide.
Bytes compressed = read_file("lab/tests/reference.huf");
Bytes original = read_file("lab/tests/fixture.txt");
assert(decompress_bytes(compressed) == original);
assert(compress_bytes(original) == compressed);
}
void test_rejects_bad_header() {
assert_rejected(Bytes{});
assert_rejected(bytes_of("HUF"));
assert_rejected(bytes_of("HUF1abc"));
assert_rejected(bytes_of("esto no es un archivo comprimido"));
// Un archivo correcto, con un solo byte cambiado en la marca.
Bytes good = compress_bytes(bytes_of("aab"));
for (std::size_t i = 0; i < 4; i++) {
Bytes bad = good;
bad[i] = 'x';
assert_rejected(bad);
}
}
void test_rejects_truncated_file() {
Bytes compressed = compress_bytes(bytes_of("el ñandú llegó después, y la zorra no llegó"));
for (std::size_t size = 0; size < compressed.size(); size++) {
Bytes cut(compressed.begin(), compressed.begin() + static_cast<std::ptrdiff_t>(size));
assert_rejected(cut);
}
}
void test_rejects_endless_tree() {
// Todo ceros: un nodo interno tras otro. Sin un límite, la recursión agota la pila.
Bytes compressed = {'H', 'U', 'F', '1', 1, 0, 0, 0, 0, 0, 0, 0};
compressed.resize(compressed.size() + 1000000, 0);
assert_rejected(compressed);
}
// Escribe en preorden un árbol perfecto con 2^depth hojas.
void write_perfect_tree(BitWriter& writer, int depth) {
if (depth == 0) {
writer.put_bit(true);
writer.put_byte('x');
return;
}
writer.put_bit(false);
write_perfect_tree(writer, depth - 1);
write_perfect_tree(writer, depth - 1);
}
void test_rejects_too_many_leaves() {
// 512 hojas, sin pasar del límite de profundidad. Le siguen bits de sobra para decodificar.
Bytes out = {'H', 'U', 'F', '1', 1, 0, 0, 0, 0, 0, 0, 0};
BitWriter writer(out);
write_perfect_tree(writer, 9);
for (int i = 0; i < 16; i++) {
writer.put_bit(false);
}
writer.finish();
assert_rejected(out);
}
struct Test {
const char* name;
void (*run)();
};
const Test tree_tests[] = {
{"count_frequencies", test_count_frequencies},
{"tree_guide_example", test_tree_guide_example},
{"tree_ties_follow_creation_order", test_tree_ties_follow_creation_order},
{"tree_weights_above_32_bits", test_tree_weights_above_32_bits},
{"tree_empty", test_tree_empty},
{"tree_one_symbol", test_tree_one_symbol},
{"tree_random_is_optimal", test_tree_random_is_optimal},
{"codes_longer_than_a_word", test_codes_longer_than_a_word},
{"tree_write_guide_example", test_tree_write_guide_example},
{"tree_write_and_read_all_symbols", test_tree_write_and_read_all_symbols},
{"tree_read_one_leaf", test_tree_read_one_leaf},
{"exact_bytes", test_exact_bytes},
{"exact_size_in_header", test_exact_size_in_header},
{"empty_input", test_empty_input},
{"one_byte_input", test_one_byte_input},
{"one_symbol_input", test_one_symbol_input},
{"two_symbols", test_two_symbols},
{"padding_is_not_data", test_padding_is_not_data},
{"all_byte_values", test_all_byte_values},
{"special_bytes", test_special_bytes},
{"random_round_trips", test_random_round_trips},
{"reference_file", test_reference_file},
{"rejects_bad_header", test_rejects_bad_header},
{"rejects_truncated_file", test_rejects_truncated_file},
{"rejects_endless_tree", test_rejects_endless_tree},
{"rejects_too_many_leaves", test_rejects_too_many_leaves},
};
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: huffman_tests nombre\n";
return 2;
}
for (const Test& test : tree_tests) {
if (std::strcmp(test.name, argv[1]) == 0) {
test.run();
return 0;
}
}
std::cerr << "no existe la prueba " << argv[1] << "\n";
return 2;
}// Crea los archivos de prueba del paso 6 en la carpeta que se le indique. Con semilla fija.
#include <cstdint>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>
namespace {
void save(const std::string& path, const std::vector<std::uint8_t>& bytes) {
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
}
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: make_samples carpeta\n";
return 2;
}
const std::string folder = std::string(argv[1]) + "/";
save(folder + "empty.bin", {});
save(folder + "one_byte.bin", {0xFF});
save(folder + "single_symbol.bin", std::vector<std::uint8_t>(100000, 'a'));
std::vector<std::uint8_t> all_bytes;
for (int round = 0; round < 1000; round++) {
for (int value = 0; value < 256; value++) {
all_bytes.push_back(static_cast<std::uint8_t>(value));
}
}
save(folder + "all_bytes.bin", all_bytes);
std::mt19937 generator(13);
std::vector<std::uint8_t> random_bytes(1 << 20);
for (std::uint8_t& byte : random_bytes) {
byte = static_cast<std::uint8_t>(generator() >> 24);
}
save(folder + "random.bin", random_bytes);
// Dos bytes difíciles y fines de línea, con pocos símbolos distintos.
const std::vector<std::uint8_t> tricky = {0x00, 0xFF, 0x0A, 0x0D, 0x1A};
std::vector<std::uint8_t> special(100000);
for (std::uint8_t& byte : special) {
byte = tricky[generator() % tricky.size()];
}
save(folder + "special.bin", special);
return 0;
}HUF1� {Cd���g.��f��!��S�B����RUm!p�29CW�n�Vr�H
���b�s/7�sv,����߳9/O&�Jf�'�����j����6: �裠���V�V�=��tG�/}�#��{�D?5*k�D?w�W܋��}����>�=�3N~�#!/usr/bin/env bash
# Paso 6: comprime y descomprime cada archivo, y comprueba con cmp que vuelve igual.
# Uso: roundtrip.sh carpeta_de_binarios archivo...
set -u
bin="$1"
shift
status=0
work="$(mktemp -d)"
for file in "$@"; do
if "$bin/compress" "$file" "$work/packed" && "$bin/decompress" "$work/packed" "$work/back" \
&& cmp "$file" "$work/back"; then
echo "ok $file"
else
echo "FALLA $file"
status=1
fi
done
rm -rf "$work"
exit "$status"Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 13: heaps y colas de prioridad, heapsort, Huffman. 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-heap prueba MinHeap (paso 1)
# make test-top prueba top_k y top_words (paso 3)
# make test-tree prueba frecuencias, árbol y códigos (pasos 4 y 5)
# make test-compress prueba compress y decompress, con cmp (pasos 5 y 6)
# make test-lab prueba todo su laboratorio; falla hasta que lo terminen
# make bench construir frente a insertar, con -O2 y sin sanitizers (paso 2)
# make bench-top heap de tamaño k frente a ordenar (paso 3)
# make samples crea los archivos de prueba en build/samples (paso 6)
# make roundtrip comprime, descomprime y compara con cmp cada archivo (paso 6)
# make bench-compress cuánto comprime cada archivo (paso 6)
# 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
DATA ?= ../../data
LAB_DIR ?= lab
BUILD = build
LAB_BUILD = $(BUILD)/$(LAB_DIR)
PROGRAMS = heap_demo where_is_max heapsort_demo pq_demo huffman_demo test_structures
BENCHES = build_counts heapsort_counts heapsort_cache heapsort_times top_k_bench
COMMON_HEADERS = int_heap.h heapsort.h bench.h
GIVEN = lab/bit_io.h lab/byte_file.h lab/word_count.h lab/words.h lab/not_implemented.h
LAB_HEADERS = $(LAB_DIR)/min_heap.h $(LAB_DIR)/top_k.h $(LAB_DIR)/huffman.h $(GIVEN)
LAB_INCLUDES = -I$(LAB_DIR) -Ilab
LAB_PROGRAMS = heap_tests huffman_tests top_words compress decompress
LAB_BENCHES = bench_heap bench_top bench_compress
SAMPLES = empty one_byte single_symbol all_bytes random special
SAMPLE_FILES = $(SAMPLES:%=$(BUILD)/samples/%.bin)
.PHONY: all lab samples test test-heap test-top test-tree test-compress test-lab bench bench-top roundtrip bench-compress clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%)
$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -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_BUILD)/heap_tests: lab/tests/heap_tests.cpp $(LAB_HEADERS) | $(LAB_BUILD)
$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@
$(LAB_BUILD)/huffman_tests: lab/tests/huffman_tests.cpp $(LAB_HEADERS) | $(LAB_BUILD)
$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@
$(LAB_BUILD)/top_words $(LAB_BUILD)/compress $(LAB_BUILD)/decompress: $(LAB_BUILD)/%: $(LAB_DIR)/%.cpp $(LAB_HEADERS) | $(LAB_BUILD)
$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@
$(LAB_BENCHES:%=$(LAB_BUILD)/%): $(LAB_BUILD)/%: lab/tests/%.cpp $(LAB_HEADERS) bench.h | $(LAB_BUILD)
$(CXX) $(BENCHFLAGS) $(LAB_INCLUDES) -I. $< -o $@
$(BUILD)/make_samples: lab/tests/make_samples.cpp | $(BUILD)
$(CXX) $(BENCHFLAGS) $< -o $@
$(SAMPLE_FILES) &: $(BUILD)/make_samples
mkdir -p $(BUILD)/samples
$(BUILD)/make_samples $(BUILD)/samples
samples: $(SAMPLE_FILES)
$(BUILD) $(LAB_BUILD):
mkdir -p $@
lab: $(LAB_PROGRAMS:%=$(LAB_BUILD)/%) $(LAB_BENCHES:%=$(LAB_BUILD)/%) samples
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all samples
@if [ -d solution ]; then $(MAKE) --no-print-directory LAB_DIR=solution lab; fi
DATA=$(DATA) python3 -m unittest discover -p 'test_*.py'
test-heap: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestHeap
test-top: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestTop
test-tree: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestTree
test-compress: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestCompress
test-lab: all lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
bench: $(LAB_BUILD)/bench_heap
$<
bench-top: $(LAB_BUILD)/bench_top
$< $(DATA)/tradiciones.txt
roundtrip: $(LAB_BUILD)/compress $(LAB_BUILD)/decompress samples
lab/tests/roundtrip.sh $(LAB_BUILD) $(SAMPLE_FILES) lab/tests/fixture.txt $(DATA)/tradiciones.txt
bench-compress: $(LAB_BUILD)/bench_compress samples
$< $(SAMPLE_FILES) $(DATA)/tradiciones.txt $(DATA)/terminos.tsv $(DATA)/palabras.txt
clean:
rm -rf $(BUILD) __pycache__