Semana 10 — Árboles binarios de búsqueda
Objetivos
- Implementar un árbol binario de búsqueda con inserción, búsqueda y borrado.
- Usar los recorridos para resolver consultas que una tabla hash no resuelve.
- Mostrar con una medición por qué la altura lo decide todo.
Teoría — Árboles binarios de búsqueda: recorridos y mapas ordenados
Lectura: Apuntes, semana 10.
Apoyo: Open Data Structures, secs. 6.1 (BinaryTree) y 6.2 (BinarySearchTree). CLRS, capítulo Binary Search Trees.
Temas:
- Lo que no da una tabla hash: el mínimo, el siguiente de una clave, todas las claves de un rango.
- Invariante: en cada nodo, las claves de la izquierda son menores y las de la derecha mayores.
- Buscar e insertar: bajar desde la raíz. Costo Θ(altura).
- Borrar: sin hijos, con un hijo y con dos hijos, usando el sucesor.
- Recorridos: en orden da las claves ordenadas; preorden y postorden, y para qué sirve cada uno. Destruir un árbol en postorden.
- Consultas ordenadas: mínimo, máximo, piso, techo y rango.
- La altura: entre log n y n. Insertar claves ordenadas da una lista. Claves al azar dan una altura del orden de log n, unas 2 a 2,5 veces log₂ n, lo cual se mide y no se demuestra.
std::mapfrente astd::unordered_map: qué se gana y qué se paga.- Error común: en el borrado con dos hijos, copiar la clave del sucesor y olvidar borrar el nodo del sucesor.
Laboratorio — Un mapa ordenado, probado contra std::map
Sesión 1:
- Escribir
BstMap<K, V>:insert,findy un recorrido en orden, con ownership claro ycheck_invariants. - Escribir
erase, con los tres casos. Probar con operaciones al azar contrastd::map, comprobando las invariantes después de cada operación.
Sesión 2:
3. Agregar floor, ceiling y range, y un iterador en orden.
4. Escribir un programa real: frecuencia de palabras en un texto, en orden alfabético y con consultas por rango.
5. Medir la altura y el tiempo insertando claves al azar y claves ordenadas. Ver hasta qué n aguanta el caso ordenado dentro de un límite de tiempo.
Preguntas de repaso
- Inserten 5, 3, 8, 1, 4 y 7 en un árbol vacío. Dibújenlo y escriban sus recorridos en orden, preorden y postorden.
- Borren el 5 de ese árbol usando el sucesor. Dibujen el resultado.
- ¿Qué orden de inserción da la mayor altura para esas mismas claves? ¿Cuánto mide?
- ¿Por qué un destructor recursivo puede fallar en un árbol degenerado de un millón de nodos?
- Den dos consultas que
std::mapresponde bien ystd::unordered_mapno.
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (15)Errores (4)Pruebas (4)Laboratorio (8)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Cuenta las reservas de memoria de un programa, reemplazando el operator new global.
// Se incluye en un solo .cpp de cada programa.
#ifndef ALLOC_COUNTER_H
#define ALLOC_COUNTER_H
#include <cstddef>
#include <cstdlib>
#include <new>
// Número de llamadas a operator new desde que empezó el programa.
inline std::size_t& allocation_count() {
static std::size_t count = 0;
return count;
}
void* operator new(std::size_t size) {
allocation_count()++;
void* block = std::malloc(size == 0 ? 1 : size);
if (block == nullptr) {
throw std::bad_alloc();
}
return block;
}
void operator delete(void* block) noexcept {
std::free(block);
}
void operator delete(void* block, std::size_t) noexcept {
std::free(block);
}
#endif// 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// Árbol binario de búsqueda sin valores: el conjunto de claves de los apuntes.
// El laboratorio construye un mapa con valores y con puntero al padre.
#ifndef BST_SET_H
#define BST_SET_H
#include <cassert>
#include <cstddef>
#include <sstream>
#include <string>
#include <vector>
// Invariantes:
// - En cada nodo, todas las claves del subárbol izquierdo son menores que la suya,
// y todas las del derecho son mayores. Por eso no hay claves repetidas.
// - El árbol tiene exactamente size_ nodos, y root_ es nulo si y solo si size_ == 0.
//
// K solo necesita operator<: a y b son la misma clave si ni a < b ni b < a.
template <typename K>
class BstSet {
struct Node {
K key;
Node* left = nullptr;
Node* right = nullptr;
};
public:
BstSet() = default;
// Sin recursión: un árbol degenerado no agota la pila.
~BstSet() {
std::vector<Node*> pending;
if (root_ != nullptr) {
pending.push_back(root_);
}
while (!pending.empty()) {
Node* node = pending.back();
pending.pop_back();
if (node->left != nullptr) {
pending.push_back(node->left);
}
if (node->right != nullptr) {
pending.push_back(node->right);
}
delete node;
}
}
// Este conjunto es para estudiar las operaciones: no se copia ni se mueve.
BstSet(const BstSet&) = delete;
BstSet& operator=(const BstSet&) = delete;
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
bool contains(const K& key) const {
const Node* node = root_;
while (node != nullptr) {
if (key < node->key) {
node = node->left;
} else if (node->key < key) {
node = node->right;
} else {
return true;
}
}
return false;
}
// Devuelve false si la clave ya estaba.
bool insert(const K& key) {
Node** link = &root_;
while (*link != nullptr) {
if (key < (*link)->key) {
link = &(*link)->left;
} else if ((*link)->key < key) {
link = &(*link)->right;
} else {
return false;
}
}
*link = new Node{key};
size_++;
return true;
}
// Devuelve false si la clave no estaba.
bool erase(const K& key) {
Node** link = &root_;
while (*link != nullptr) {
if (key < (*link)->key) {
link = &(*link)->left;
} else if ((*link)->key < key) {
link = &(*link)->right;
} else {
break;
}
}
if (*link == nullptr) {
return false;
}
Node* node = *link;
if (node->left != nullptr && node->right != nullptr) {
// Dos hijos: el sucesor es el mínimo del subárbol derecho.
Node** next_link = &node->right;
while ((*next_link)->left != nullptr) {
next_link = &(*next_link)->left;
}
node->key = (*next_link)->key;
// Ahora el que se quita es el nodo del sucesor, que no tiene hijo izquierdo.
link = next_link;
node = *link;
}
*link = node->left != nullptr ? node->left : node->right;
delete node;
size_--;
return true;
}
// Puntero a la clave menor; nulo si está vacío.
const K* min() const {
const Node* node = root_;
if (node == nullptr) {
return nullptr;
}
while (node->left != nullptr) {
node = node->left;
}
return &node->key;
}
// Puntero a la clave mayor; nulo si está vacío.
const K* max() const {
const Node* node = root_;
if (node == nullptr) {
return nullptr;
}
while (node->right != nullptr) {
node = node->right;
}
return &node->key;
}
// Puntero a la mayor clave <= key; nulo si no hay.
const K* floor(const K& key) const {
const Node* node = root_;
const K* candidate = nullptr;
while (node != nullptr) {
if (key < node->key) {
node = node->left;
} else if (node->key < key) {
candidate = &node->key;
node = node->right;
} else {
return &node->key;
}
}
return candidate;
}
// Las claves de [low, high), en orden.
std::vector<K> range(const K& low, const K& high) const {
std::vector<K> keys;
collect_range(root_, low, high, keys);
return keys;
}
std::vector<K> in_order() const {
std::vector<K> keys;
walk_in(root_, keys);
return keys;
}
std::vector<K> pre_order() const {
std::vector<K> keys;
walk_pre(root_, keys);
return keys;
}
std::vector<K> post_order() const {
std::vector<K> keys;
walk_post(root_, keys);
return keys;
}
// Altura: aristas del camino más largo desde la raíz. Vacío: -1. Un solo nodo: 0.
// Va nivel por nivel, sin recursión.
int height() const {
int levels = 0;
std::vector<const Node*> level;
if (root_ != nullptr) {
level.push_back(root_);
}
while (!level.empty()) {
std::vector<const Node*> next;
for (const Node* node : level) {
if (node->left != nullptr) {
next.push_back(node->left);
}
if (node->right != nullptr) {
next.push_back(node->right);
}
}
level = std::move(next);
levels++;
}
return levels - 1;
}
// Suma de las profundidades de todos los nodos: la raíz tiene profundidad 0.
long total_depth() const {
long total = 0;
long depth = 0;
std::vector<const Node*> level;
if (root_ != nullptr) {
level.push_back(root_);
}
while (!level.empty()) {
std::vector<const Node*> next;
for (const Node* node : level) {
total += depth;
if (node->left != nullptr) {
next.push_back(node->left);
}
if (node->right != nullptr) {
next.push_back(node->right);
}
}
level = std::move(next);
depth++;
}
return total;
}
// La forma del árbol en una línea: clave(izquierdo,derecho), con _ si falta un hijo.
std::string shape() const {
std::ostringstream out;
write_shape(root_, out);
return out.str();
}
// Comprueba con límites: mirar solo a los hijos directos no alcanza.
// Es recursiva: su profundidad es la altura del árbol.
void check_invariants() const {
assert(count_checked(root_, nullptr, nullptr) == size_);
assert((root_ == nullptr) == (size_ == 0));
}
private:
static void walk_in(const Node* node, std::vector<K>& keys) {
if (node == nullptr) {
return;
}
walk_in(node->left, keys);
keys.push_back(node->key);
walk_in(node->right, keys);
}
static void walk_pre(const Node* node, std::vector<K>& keys) {
if (node == nullptr) {
return;
}
keys.push_back(node->key);
walk_pre(node->left, keys);
walk_pre(node->right, keys);
}
static void walk_post(const Node* node, std::vector<K>& keys) {
if (node == nullptr) {
return;
}
walk_post(node->left, keys);
walk_post(node->right, keys);
keys.push_back(node->key);
}
static void collect_range(const Node* node, const K& low, const K& high, std::vector<K>& keys) {
if (node == nullptr) {
return;
}
if (low < node->key) {
collect_range(node->left, low, high, keys);
}
if (!(node->key < low) && node->key < high) {
keys.push_back(node->key);
}
if (node->key < high) {
collect_range(node->right, low, high, keys);
}
}
static void write_shape(const Node* node, std::ostringstream& out) {
if (node == nullptr) {
out << "_";
return;
}
out << node->key;
if (node->left != nullptr || node->right != nullptr) {
out << "(";
write_shape(node->left, out);
out << ",";
write_shape(node->right, out);
out << ")";
}
}
// Cuenta los nodos, y exige que cada clave quede entre low y high (nulo: sin límite).
static std::size_t count_checked(const Node* node, const K* low, const K* high) {
if (node == nullptr) {
return 0;
}
assert(low == nullptr || *low < node->key);
assert(high == nullptr || node->key < *high);
return 1 + count_checked(node->left, low, &node->key)
+ count_checked(node->right, &node->key, high);
}
Node* root_ = nullptr;
std::size_t size_ = 0;
};
#endif// El orden de std::string es el de los bytes. Consultas por rango sobre palabras.txt.
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: byte_order palabras.txt\n";
return 2;
}
std::ifstream file(argv[1]);
if (!file) {
std::cerr << "no se pudo abrir " << argv[1] << "\n";
return 2;
}
std::map<std::string, int> words;
std::string word;
while (std::getline(file, word)) {
words[word] = 1;
}
auto count_range = [&](const std::string& low, const std::string& high) {
auto first = words.lower_bound(low);
auto last = words.lower_bound(high);
long count = std::distance(first, last);
std::cout << "[" << low << ", " << high << "): " << count;
if (count > 0) {
std::cout << ", de " << first->first << " a " << std::prev(last)->first;
}
std::cout << "\n";
};
std::cout << "ultima: " << words.rbegin()->first << "\n";
count_range("a", "b");
count_range("á", "â");
count_range("n", "o");
count_range("ñ", "ò");
count_range("pre", "prf");
count_range("z", "{");
auto next = words.upper_bound("zuzos");
std::cout << "despues de zuzos: " << next->first << "\n";
auto before = std::prev(words.lower_bound("rodillas"));
std::cout << "piso de rodillas: " << before->first << ", siguiente: " << std::next(before)->first << "\n";
return 0;
}// Clave entera que cuenta cuántas veces se compara. Solo tiene operator<.
#ifndef COUNTED_KEY_H
#define COUNTED_KEY_H
#include <ostream>
struct CountedKey {
int value = 0;
inline static long comparisons = 0;
bool operator<(const CountedKey& other) const {
comparisons++;
return value < other.value;
}
};
inline std::ostream& operator<<(std::ostream& out, const CountedKey& key) {
return out << key.value;
}
#endif// palabras.txt viene ordenado. ¿Qué árbol sale si se insertan sus palabras tal como vienen?
#include <algorithm>
#include <fstream>
#include <iostream>
#include <map>
#include <random>
#include <string>
#include <vector>
#include "bench.h"
#include "bst_set.h"
namespace {
template <typename It>
void report(const std::string& name, It first, It last) {
int height = 0;
double seconds = median_seconds(3, [&] {
BstSet<std::string> tree;
for (It it = first; it != last; ++it) {
tree.insert(*it);
}
height = tree.height();
});
std::cout << name << ": " << last - first << " palabras, altura " << height << ", " << seconds << " s\n";
}
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: dictionary_order palabras.txt\n";
return 2;
}
std::ifstream file(argv[1]);
if (!file) {
std::cerr << "no se pudo abrir " << argv[1] << "\n";
return 2;
}
std::vector<std::string> words;
std::string word;
while (std::getline(file, word)) {
words.push_back(word);
}
std::cout << "orden de bytes: " << (std::is_sorted(words.begin(), words.end()) ? "si" : "no") << "\n";
report("tal como vienen", words.begin(), words.begin() + 20000);
std::vector<std::string> shuffled = words;
std::mt19937 random(10);
std::shuffle(shuffled.begin(), shuffled.end(), random);
report("barajadas", shuffled.begin(), shuffled.begin() + 20000);
report("barajadas", shuffled.begin(), shuffled.end());
// std::map se mantiene balanceado: el mismo orden que degenera a BstSet no le afecta.
double seconds = median_seconds(3, [&] {
std::map<std::string, int> map;
for (const std::string& key : words) {
map[key] = 1;
}
keep(map.size());
});
std::cout << "std::map, tal como vienen: " << words.size() << " palabras, " << seconds << " s\n";
return 0;
}// Los tres casos del borrado, cada uno sobre el mismo árbol.
#include <iostream>
#include "bst_set.h"
namespace {
void show_erase(int key) {
BstSet<int> tree;
for (int value : {50, 30, 70, 20, 40, 60, 80, 35, 65, 90}) {
tree.insert(value);
}
std::cout << "borrar " << key << ": " << tree.shape() << "\n";
tree.erase(key);
tree.check_invariants();
std::cout << "queda: " << tree.shape() << "\n";
}
} // namespace
int main() {
show_erase(20);
show_erase(80);
show_erase(60);
show_erase(30);
show_erase(50);
show_erase(70);
return 0;
}// Altura y comparaciones al insertar n claves, al azar y ordenadas. Todo son conteos, no tiempos.
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include "bst_set.h"
#include "counted_key.h"
int main() {
std::cout << "Claves al azar, tres semillas\n";
std::cout << std::setw(9) << "n" << std::setw(7) << "log2n" << std::setw(6) << "h1" << std::setw(6) << "h2"
<< std::setw(6) << "h3" << std::setw(9) << "h/log2n" << std::setw(11) << "prof.media"
<< std::setw(10) << "prof/log" << "\n";
for (int n = 1000; n <= 1024000; n *= 4) {
std::vector<int> keys(n);
std::iota(keys.begin(), keys.end(), 0);
double log_n = std::log2(n);
int heights[3];
double mean_depth = 0;
for (int seed = 0; seed < 3; seed++) {
std::mt19937 random(seed + 1);
std::shuffle(keys.begin(), keys.end(), random);
BstSet<int> tree;
for (int key : keys) {
tree.insert(key);
}
heights[seed] = tree.height();
mean_depth += static_cast<double>(tree.total_depth()) / n / 3;
}
double mean_height = (heights[0] + heights[1] + heights[2]) / 3.0;
std::cout << std::setw(9) << n << std::fixed << std::setprecision(1) << std::setw(7) << log_n
<< std::setw(6) << heights[0] << std::setw(6) << heights[1] << std::setw(6) << heights[2]
<< std::setprecision(2) << std::setw(9) << mean_height / log_n << std::setw(11) << mean_depth
<< std::setw(10) << mean_depth / log_n << "\n";
}
std::cout << "Claves ordenadas\n";
std::cout << std::setw(9) << "n" << std::setw(9) << "altura" << std::setw(15) << "comparaciones"
<< std::setw(15) << "n(n-1)" << "\n";
for (int n = 1000; n <= 16000; n *= 2) {
BstSet<CountedKey> tree;
CountedKey::comparisons = 0;
for (int key = 0; key < n; key++) {
tree.insert(CountedKey{key});
}
std::cout << std::setw(9) << n << std::setw(9) << tree.height() << std::setw(15) << CountedKey::comparisons
<< std::setw(15) << static_cast<long>(n) * (n - 1) << "\n";
}
return 0;
}// Tiempo de insertar y de buscar n claves, al azar y ordenadas. Compilar con -O2 y sin sanitizers.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include "bench.h"
#include "bst_set.h"
namespace {
double insert_seconds(const std::vector<int>& keys) {
return median_seconds(5, [&] {
BstSet<int> tree;
for (int key : keys) {
tree.insert(key);
}
keep(tree.size());
});
}
} // namespace
int main() {
std::cout << std::setw(9) << "n" << std::setw(11) << "azar(s)" << std::setw(10) << "x" << std::setw(12) << "orden(s)"
<< std::setw(10) << "x" << "\n";
std::mt19937 random(10);
double last_random = 0;
double last_sorted = 0;
for (int n = 1000; n <= 32000; n *= 2) {
std::vector<int> keys(n);
std::iota(keys.begin(), keys.end(), 0);
double sorted_time = insert_seconds(keys);
std::shuffle(keys.begin(), keys.end(), random);
double random_time = insert_seconds(keys);
std::cout << std::setw(9) << n << std::fixed << std::setprecision(5) << std::setw(11) << random_time
<< std::setprecision(1) << std::setw(10) << (last_random > 0 ? random_time / last_random : 0)
<< std::setprecision(5) << std::setw(12) << sorted_time
<< std::setprecision(1) << std::setw(10) << (last_sorted > 0 ? sorted_time / last_sorted : 0) << "\n";
last_random = random_time;
last_sorted = sorted_time;
}
return 0;
}// Inserta las mismas 6 claves en todos los órdenes posibles y cuenta qué altura sale.
#include <algorithm>
#include <iostream>
#include <map>
#include <vector>
#include "bst_set.h"
int main() {
std::vector<int> keys = {1, 3, 4, 5, 7, 8};
std::map<int, int> orders_with_height;
int total = 0;
do {
BstSet<int> tree;
for (int key : keys) {
tree.insert(key);
}
orders_with_height[tree.height()]++;
total++;
} while (std::next_permutation(keys.begin(), keys.end()));
std::cout << "ordenes: " << total << "\n";
for (const auto& [height, count] : orders_with_height) {
std::cout << "altura " << height << ": " << count << " ordenes\n";
}
return 0;
}// Qué promete std::map sobre sus elementos, y cuánta memoria pide esta biblioteca por nodo.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <map>
#include <memory>
#include <unordered_map>
#include "alloc_counter.h"
namespace {
// Un asignador que anota cuántos bytes pidió el contenedor en cada reserva.
inline std::size_t last_block = 0;
template <typename T>
struct SpyAllocator {
using value_type = T;
SpyAllocator() = default;
template <typename U>
SpyAllocator(const SpyAllocator<U>&) {}
T* allocate(std::size_t n) {
last_block = n * sizeof(T);
return std::allocator<T>{}.allocate(n);
}
void deallocate(T* block, std::size_t n) { std::allocator<T>{}.deallocate(block, n); }
bool operator==(const SpyAllocator&) const { return true; }
};
} // namespace
int main() {
std::map<int, int> map;
map[5] = 50;
map[3] = 30;
map[8] = 80;
map[7] = 70;
int* to_seven = &map[7];
for (int i = 100; i < 1100; i++) {
map[i] = i;
}
// Borrar el 5, que tiene dos hijos: el 7 sigue en su sitio, aunque es su sucesor.
map.erase(5);
std::cout << "puntero al 7 tras mil inserciones y un borrado: " << *to_seven << "\n";
assert(to_seven == &map[7]);
std::size_t before = allocation_count();
std::map<int, int> counted;
for (int i = 0; i < 1000; i++) {
counted[i] = i;
}
std::cout << "reservas de std::map al insertar 1000 claves: " << allocation_count() - before << "\n";
std::map<int, int, std::less<int>, SpyAllocator<std::pair<const int, int>>> spied;
spied[1] = 1;
std::cout << "bytes por nodo de std::map<int, int>: " << last_block << "\n";
std::unordered_map<int, int, std::hash<int>, std::equal_to<int>, SpyAllocator<std::pair<const int, int>>> spied_hash;
spied_hash.reserve(16);
spied_hash[1] = 1;
std::cout << "bytes por nodo de std::unordered_map<int, int>: " << last_block << "\n";
return 0;
}// La misma consulta en std::map y en std::unordered_map: la menor clave >= x, y una por rango.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <map>
#include <random>
#include <unordered_map>
#include <vector>
#include "counted_key.h"
namespace {
struct CountedHash {
std::size_t operator()(const CountedKey& key) const { return std::hash<int>{}(key.value); }
};
struct SameKey {
bool operator()(const CountedKey& a, const CountedKey& b) const { return a.value == b.value; }
};
} // namespace
int main() {
const int n = 100000;
std::mt19937 random(10);
std::vector<int> values(n);
for (int i = 0; i < n; i++) {
values[i] = i * 3;
}
std::shuffle(values.begin(), values.end(), random);
std::map<CountedKey, int> ordered;
std::unordered_map<CountedKey, int, CountedHash, SameKey> hashed;
for (int value : values) {
ordered[CountedKey{value}] = value;
hashed[CountedKey{value}] = value;
}
CountedKey target{100000};
CountedKey::comparisons = 0;
auto at_or_after = ordered.lower_bound(target);
std::cout << "std::map, techo de 100000: " << at_or_after->first.value << ", comparaciones: "
<< CountedKey::comparisons << "\n";
// Sin orden no queda más que mirar todas las claves.
long visited = 0;
int best = -1;
for (const auto& [key, value] : hashed) {
visited++;
if (key.value >= target.value && (best == -1 || key.value < best)) {
best = key.value;
}
}
std::cout << "std::unordered_map, techo de 100000: " << best << ", claves visitadas: " << visited << "\n";
CountedKey::comparisons = 0;
auto after = ordered.upper_bound(CountedKey{100000});
std::cout << "std::map, piso de 100000: " << std::prev(after)->first.value << ", comparaciones: "
<< CountedKey::comparisons << "\n";
long in_range = 0;
CountedKey::comparisons = 0;
auto stop = ordered.lower_bound(CountedKey{100030});
for (auto it = ordered.lower_bound(CountedKey{100000}); it != stop; ++it) {
in_range++;
}
std::cout << "std::map, claves en [100000, 100030): " << in_range << ", comparaciones: "
<< CountedKey::comparisons << "\n";
std::cout << "std::map, primeras:";
auto it_map = ordered.begin();
for (int i = 0; i < 5; i++, ++it_map) {
std::cout << " " << it_map->first.value;
}
std::cout << "\n";
std::cout << "std::unordered_map, primeras:";
auto it_hash = hashed.begin();
for (int i = 0; i < 5; i++, ++it_hash) {
std::cout << " " << it_hash->first.value;
}
std::cout << "\n";
return 0;
}// std::map frente a std::unordered_map con palabras reales. Compilar con -O2 y sin sanitizers.
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
#include "bench.h"
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: map_vs_unordered palabras.txt\n";
return 2;
}
std::ifstream file(argv[1]);
if (!file) {
std::cerr << "no se pudo abrir " << argv[1] << "\n";
return 2;
}
std::vector<std::string> words;
std::string word;
while (std::getline(file, word)) {
words.push_back(word);
}
std::mt19937 random(10);
std::shuffle(words.begin(), words.end(), random);
std::cout << " (tiempo por palabra, en ns; mediana de 3)\n";
std::cout << std::setw(9) << "n" << std::setw(10) << "ins.map" << std::setw(10) << "ins.hash" << std::setw(7) << "x"
<< std::setw(10) << "bus.map" << std::setw(10) << "bus.hash" << std::setw(7) << "x"
<< std::setw(10) << "ord.map" << std::setw(10) << "ord.hash" << std::setw(7) << "x" << "\n";
for (std::size_t n : {std::size_t{10000}, std::size_t{100000}, words.size()}) {
std::vector<std::string> keys(words.begin(), words.begin() + n);
std::vector<std::string> queries = keys;
std::shuffle(queries.begin(), queries.end(), random);
double per_key = 1e9 / n;
double insert_map = per_key * median_seconds(3, [&] {
std::map<std::string, int> map;
for (const std::string& key : keys) {
map[key] = 1;
}
keep(map.size());
});
double insert_hash = per_key * median_seconds(3, [&] {
std::unordered_map<std::string, int> map;
for (const std::string& key : keys) {
map[key] = 1;
}
keep(map.size());
});
std::map<std::string, int> ordered;
std::unordered_map<std::string, int> hashed;
for (const std::string& key : keys) {
ordered[key] = 1;
hashed[key] = 1;
}
double find_map = per_key * median_seconds(3, [&] {
long found = 0;
for (const std::string& key : queries) {
found += ordered.count(key);
}
keep(found);
});
double find_hash = per_key * median_seconds(3, [&] {
long found = 0;
for (const std::string& key : queries) {
found += hashed.count(key);
}
keep(found);
});
// Sacar todas las claves en orden: el mapa solo las recorre, y la tabla tiene que ordenarlas.
double sorted_map = per_key * median_seconds(3, [&] {
std::size_t bytes = 0;
for (const auto& [key, value] : ordered) {
bytes += key.size() + value;
}
keep(bytes);
});
double sorted_hash = per_key * median_seconds(3, [&] {
std::vector<const std::string*> pointers;
pointers.reserve(hashed.size());
for (const auto& [key, value] : hashed) {
pointers.push_back(&key);
}
std::sort(pointers.begin(), pointers.end(),
[](const std::string* a, const std::string* b) { return *a < *b; });
std::size_t bytes = 0;
for (const std::string* key : pointers) {
bytes += key->size();
}
keep(bytes);
});
std::cout << std::setw(9) << n << std::fixed << std::setprecision(0) << std::setw(10) << insert_map
<< std::setw(10) << insert_hash << std::setprecision(1) << std::setw(7) << insert_map / insert_hash
<< std::setprecision(0) << std::setw(10) << find_map << std::setw(10) << find_hash
<< std::setprecision(1) << std::setw(7) << find_map / find_hash
<< std::setprecision(0) << std::setw(10) << sorted_map << std::setw(10) << sorted_hash
<< std::setprecision(1) << std::setw(7) << sorted_hash / sorted_map << "\n";
}
return 0;
}// Consultas ordenadas sobre un árbol perfecto de 1023 claves, contando comparaciones.
#include <iostream>
#include <vector>
#include "bst_set.h"
#include "counted_key.h"
namespace {
// La raíz primero: así el árbol sale perfecto.
void add_balanced(BstSet<CountedKey>& tree, int low, int high) {
if (low > high) {
return;
}
int middle = low + (high - low) / 2;
tree.insert(CountedKey{middle * 10});
add_balanced(tree, low, middle - 1);
add_balanced(tree, middle + 1, high);
}
} // namespace
int main() {
BstSet<CountedKey> tree;
add_balanced(tree, 0, 1022);
std::cout << "claves: " << tree.size() << ", altura: " << tree.height() << "\n";
CountedKey::comparisons = 0;
std::cout << "contains(5000): " << tree.contains(CountedKey{5000});
std::cout << ", comparaciones: " << CountedKey::comparisons << "\n";
CountedKey::comparisons = 0;
std::cout << "contains(5001): " << tree.contains(CountedKey{5001});
std::cout << ", comparaciones: " << CountedKey::comparisons << "\n";
CountedKey::comparisons = 0;
std::cout << "floor(5001): " << *tree.floor(CountedKey{5001});
std::cout << ", comparaciones: " << CountedKey::comparisons << "\n";
for (int count : {1, 10, 100, 1000}) {
CountedKey::comparisons = 0;
std::vector<CountedKey> keys = tree.range(CountedKey{100}, CountedKey{100 + count * 10});
std::cout << "range con " << keys.size() << " claves, comparaciones: " << CountedKey::comparisons << "\n";
}
return 0;
}// El árbol de los apuntes: sus tres recorridos, su altura, y qué pasa al borrar la raíz.
#include <iostream>
#include <string>
#include <vector>
#include "bst_set.h"
namespace {
void print_line(const std::string& name, const std::vector<int>& keys) {
std::cout << name << ":";
for (int key : keys) {
std::cout << " " << key;
}
std::cout << "\n";
}
} // namespace
int main() {
BstSet<int> tree;
for (int key : {5, 3, 8, 1, 4, 7}) {
tree.insert(key);
}
std::cout << "forma: " << tree.shape() << "\n";
print_line("en orden", tree.in_order());
print_line("preorden", tree.pre_order());
print_line("postorden", tree.post_order());
std::cout << "altura: " << tree.height() << "\n";
tree.erase(5);
std::cout << "sin el 5: " << tree.shape() << "\n";
print_line("en orden", tree.in_order());
tree.check_invariants();
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: en el borrado con dos hijos, se copia la clave del sucesor
// y se olvida borrar el nodo del sucesor. check_invariants lo descubre.
#include <cassert>
#include <cstddef>
#include <iostream>
class BrokenSet {
struct Node {
int key;
Node* left = nullptr;
Node* right = nullptr;
};
public:
BrokenSet() = default;
BrokenSet(const BrokenSet&) = delete;
BrokenSet& operator=(const BrokenSet&) = delete;
~BrokenSet() { destroy(root_); }
void insert(int key) {
Node** link = &root_;
while (*link != nullptr) {
link = key < (*link)->key ? &(*link)->left : &(*link)->right;
}
*link = new Node{key};
size_++;
}
void erase_two_children(int key) {
Node* node = root_;
while (node->key != key) {
node = key < node->key ? node->left : node->right;
}
Node* next = node->right;
while (next->left != nullptr) {
next = next->left;
}
node->key = next->key;
size_--;
// Falta quitar next del árbol: su clave quedó dos veces.
}
void print_in_order() const {
print(root_);
std::cout << std::endl;
}
void check_invariants() const {
assert(count_checked(root_, nullptr, nullptr) == size_);
}
private:
static void destroy(Node* node) {
if (node != nullptr) {
destroy(node->left);
destroy(node->right);
delete node;
}
}
static void print(const Node* node) {
if (node != nullptr) {
print(node->left);
std::cout << node->key << " ";
print(node->right);
}
}
static std::size_t count_checked(const Node* node, const int* low, const int* high) {
if (node == nullptr) {
return 0;
}
assert(low == nullptr || *low < node->key);
assert(high == nullptr || node->key < *high);
return 1 + count_checked(node->left, low, &node->key)
+ count_checked(node->right, &node->key, high);
}
Node* root_ = nullptr;
std::size_t size_ = 0;
};
int main() {
BrokenSet tree;
for (int key : {5, 3, 8, 1, 4, 7}) {
tree.insert(key);
}
tree.erase_two_children(5);
tree.print_in_order();
tree.check_invariants();
return 0;
}// Error a propósito: un check_invariants que solo mira a los hijos directos.
// Aprueba un árbol roto, en el que una clave que está no se puede encontrar.
#include <iostream>
struct Node {
int key;
Node* left;
Node* right;
};
bool looks_fine(const Node* node) {
if (node == nullptr) {
return true;
}
if (node->left != nullptr && !(node->left->key < node->key)) {
return false;
}
if (node->right != nullptr && !(node->key < node->right->key)) {
return false;
}
return looks_fine(node->left) && looks_fine(node->right);
}
bool contains(const Node* node, int key) {
while (node != nullptr) {
if (key == node->key) {
return true;
}
node = key < node->key ? node->left : node->right;
}
return false;
}
int main() {
// El 7 está a la derecha del 3, bien, pero dentro del subárbol izquierdo del 5.
Node seven{7, nullptr, nullptr};
Node three{3, nullptr, &seven};
Node root{5, &three, nullptr};
std::cout << "revision local: " << (looks_fine(&root) ? "todo bien" : "roto") << "\n";
std::cout << "contains(7): " << (contains(&root, 7) ? "si" : "no") << "\n";
return 0;
}// Error a propósito: guardar un puntero a una clave y borrar otra con dos hijos.
// El nodo del sucesor desaparece, aunque la clave siga en el conjunto.
#include <iostream>
#include "../bst_set.h"
int main() {
BstSet<int> tree;
for (int key : {5, 3, 8, 1, 4, 7}) {
tree.insert(key);
}
const int* to_seven = tree.floor(7);
tree.erase(5);
std::cout << "sigue el 7: " << tree.contains(7) << std::endl;
std::cout << *to_seven << "\n";
return 0;
}// Error a propósito: un destructor recursivo sobre una cadena larga se queda sin pila.
// Uso: recursive_destroy n
#include <cstdlib>
#include <iostream>
struct Node {
int key;
Node* left;
Node* right;
};
void destroy(Node* node) {
if (node == nullptr) {
return;
}
destroy(node->left);
destroy(node->right);
delete node;
}
int main(int argc, char** argv) {
long n = argc > 1 ? std::atol(argv[1]) : 1000000;
// Se arma la cadena directo: con insert, un millón de claves ordenadas tardaría muchos minutos.
Node* root = nullptr;
Node* last = nullptr;
for (long i = 0; i < n; i++) {
Node* node = new Node{static_cast<int>(i), nullptr, nullptr};
if (last == nullptr) {
root = node;
} else {
last->right = node;
}
last = node;
}
destroy(root);
std::cout << "destruido: " << n << " nodos" << std::endl;
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 os
import pathlib
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
WORDS = f"{DATA}/palabras.txt"
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
def run(*command):
return subprocess.run(
list(command), cwd=HERE, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300,
)
class Examples(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Se corre una sola vez: tarda unos segundos, y varias pruebas leen su salida.
cls.height_lines = run("build/height_table").stdout.splitlines()
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 ["tree_demo", "erase_demo", "insertion_orders", "query_demo", "map_queries", "map_guarantees"]:
with self.subTest(program=name):
done = run(f"build/{name}")
self.assertEqual(done.returncode, 0, done.stdout)
def test_longest_chain_has_32_orders(self):
self.assertIn("altura 5: 32 ordenes", run("build/insertion_orders").stdout)
def test_sorted_comparisons_are_exact(self):
"""Las dos últimas columnas son el conteo real y la fórmula."""
lines = self.height_lines
rows = lines[lines.index("Claves ordenadas") + 2:]
self.assertEqual(len(rows), 5)
for row in rows:
n, height, comparisons, formula = row.split()
self.assertEqual(int(height), int(n) - 1)
self.assertEqual(comparisons, formula)
def test_random_height_is_far_from_n(self):
lines = self.height_lines
for row in lines[2:lines.index("Claves ordenadas")]:
fields = row.split()
n, log_n = int(fields[0]), float(fields[1])
for height in fields[2:5]:
self.assertLess(int(height), 4 * log_n)
self.assertLess(int(height), n)
def test_dictionary_shape_is_counted(self):
lines = run("build/dictionary_order", WORDS).stdout.splitlines()
self.assertEqual(lines[0], "orden de bytes: si")
self.assertIn("20000 palabras, altura 19999,", lines[1])
self.assertIn("20000 palabras, altura 31,", lines[2])
self.assertIn("1045757 palabras, altura 49,", lines[3])
def test_map_keeps_pointer_and_counts_nodes(self):
lines = run("build/map_guarantees").stdout.splitlines()
self.assertEqual(lines[1], "reservas de std::map al insertar 1000 claves: 1000")
class DeliberateErrors(unittest.TestCase):
def build(self, name):
binary = f"build/{name}"
done = run(*SAN, f"errors/{name}.cpp", "-o", binary)
self.assertEqual(done.returncode, 0, done.stdout)
return binary
def test_recursive_destroy_overflows(self):
binary = self.build("recursive_destroy")
self.assertEqual(run(binary, "1000").returncode, 0)
done = run(binary, "1000000")
self.assertNotEqual(done.returncode, 0)
self.assertIn("stack-overflow", done.stdout)
def test_forgot_successor_is_found_by_invariants(self):
done = run(self.build("forgot_successor"))
self.assertNotEqual(done.returncode, 0)
self.assertIn("1 3 4 7 7 8", done.stdout)
self.assertIn("count_checked", done.stdout)
self.assertNotIn("AddressSanitizer", done.stdout)
def test_pointer_after_erase(self):
done = run(self.build("pointer_after_erase"))
self.assertNotEqual(done.returncode, 0)
self.assertIn("heap-use-after-free", done.stdout)
self.assertIn("sigue el 7: 1", done.stdout)
def test_local_check_misses_the_break(self):
done = run(self.build("local_check"))
self.assertEqual(done.returncode, 0, done.stdout)
self.assertIn("todo bien", done.stdout)
self.assertIn("contains(7): no", done.stdout)
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el mapa: 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 resource
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
BINARY = HERE / "build" / LAB_DIR / "map_tests"
WORD_FREQ = HERE / "build" / LAB_DIR / "word_freq"
FIXTURE = HERE / "lab" / "tests" / "fixture.txt"
BASICS = [
"new_is_empty", "insert_and_find", "insert_does_not_overwrite", "find_lets_edit_value",
"subscript_operator", "in_order_is_sorted", "shape_follows_insertion_order",
"sorted_keys_make_a_chain", "comparisons_follow_height", "strings_as_keys",
"no_objects_left_behind", "copy_is_independent", "self_assignment",
"move_leaves_empty_and_usable",
]
BROKEN = ["broken_deep_order", "broken_repeated_key", "broken_parent", "broken_size"]
ERASE = [
"erase_absent", "erase_leaf", "erase_one_child", "erase_root_with_one_child",
"erase_two_children_uses_successor", "erase_successor_is_right_child",
"erase_successor_with_right_child", "erase_releases_objects", "only_needs_less_than",
"random_against_map", "random_strings", "copy_after_random_operations",
]
QUERIES = [
"min_max", "floor_ceiling_cases", "floor_ceiling_random", "range_is_half_open", "range_random",
"range_does_not_walk_everything", "iterator_in_order", "iterator_edits_value", "const_iterator",
"iterator_of_empty_map", "iterator_survives_insert", "iterator_random", "iterate_chain_comparisons",
]
# Pila de 128 KB: alcanza para el programa, y no para una recursión de 6000 niveles.
SMALL_STACK = 128 * 1024
def short_report(text):
"""Deja lo útil de un informe: el tipo de error y las líneas de sus archivos."""
if "ERROR:" not in text and "runtime error" not in text:
return text.strip()
useful = [line.strip() for line in text.splitlines()
if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line
or re.search(r"(bst_map\.h|map_tests\.cpp|word_freq\.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(name, setup=None):
return subprocess.run(
[str(BINARY), name], cwd=HERE, text=True, preexec_fn=setup,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
def small_stack():
resource.setrlimit(resource.RLIMIT_STACK, (SMALL_STACK, SMALL_STACK))
def add_cases(cls, names):
for name in names:
def case(self, name=name):
done = run_case(name)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
setattr(cls, f"test_{name}", case)
return cls
class LabCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
code, output = build("map_tests")
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 ["map_tests", "word_freq"]:
(HERE / "build" / LAB_DIR / target).unlink(missing_ok=True)
code, output = build(target)
self.assertEqual(code, 0, output)
self.assertNotIn("warning", output)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestBasics(LabCase):
"""Sesión 1, paso 1: insert, find, recorrido en orden, destructor, check_invariants."""
def test_destructor_without_recursion(self):
done = run_case("destructor_without_recursion", small_stack)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
def add_broken(cls, names):
for name in names:
def case(self, name=name):
done = run_case(name)
self.assertIn("sano", done.stdout, "check_invariants falló en un árbol 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)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestErase(LabCase):
"""Sesión 1, paso 2: erase con sus tres casos, contra std::map."""
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestQueries(LabCase):
"""Sesión 2, paso 3: floor, ceiling, range e iterador."""
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestWordFreq(unittest.TestCase):
"""Sesión 2, paso 4: el programa de frecuencias, sobre un texto pequeño."""
@classmethod
def setUpClass(cls):
code, output = build("word_freq")
if code != 0:
raise AssertionError(output)
def run_program(self, *args):
return subprocess.run(
[str(WORD_FREQ), *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_all_words_in_byte_order(self):
lines = self.lines(str(FIXTURE))
self.assertEqual(lines[:-1], [
"ana 2", "de 1", "después 1", "el 3", "la 1", "llegó 2", "no 1", "quién 1", "una 1", "y 3",
"zorra 2", "zorras 1", "árbol 4", "ñandú 2",
])
def test_range_is_half_open(self):
lines = self.lines(str(FIXTURE), "el", "no")
self.assertEqual(lines[:-1], ["el 3", "la 1", "llegó 2"])
def test_range_after_z(self):
lines = self.lines(str(FIXTURE), "z", "ñ")
self.assertEqual(lines[:-1], ["zorra 2", "zorras 1", "árbol 4"])
def test_summary_line(self):
lines = self.lines(str(FIXTURE))
self.assertTrue(lines[-1].startswith("palabras: 25, distintas: 14, altura: "), lines[-1])
def test_bad_usage(self):
self.assertNotEqual(self.run_program().returncode, 0)
self.assertNotEqual(self.run_program("/no/existe.txt").returncode, 0)
add_cases(TestBasics, BASICS)
add_broken(TestBasics, BROKEN)
add_cases(TestErase, ERASE)
add_cases(TestQueries, QUERIES)
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-10.md"
SLIDES = ROOT / "slides" / "week-10.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"
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)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.cpp")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.cpp"))
sources += list((HERE / "lab").glob("*.h")) + list((HERE / "lab" / "tests").glob("*"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class ReportCheck(unittest.TestCase):
def assert_in_order(self, command, expected, actual):
"""Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
real = [stable(line) for line in actual]
position = 0
for line in wanted:
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(real)) if real[i] == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(ReportCheck):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 5)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark != REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_in_terminal(command))
self.assert_in_order(command, expected, actual)
checked += 1
self.assertGreater(checked, 1)
@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 BstSet: casos dirigidos, y operaciones al azar con semilla fija contra std::set.
#include <cassert>
#include <iostream>
#include <random>
#include <set>
#include <vector>
#include "bst_set.h"
#include "counted_key.h"
namespace {
void test_review_questions() {
BstSet<int> tree;
for (int key : {5, 3, 8, 1, 4, 7}) {
assert(tree.insert(key));
}
assert(tree.shape() == "5(3(1,4),8(7,_))");
assert((tree.in_order() == std::vector<int>{1, 3, 4, 5, 7, 8}));
assert((tree.pre_order() == std::vector<int>{5, 3, 1, 4, 8, 7}));
assert((tree.post_order() == std::vector<int>{1, 4, 3, 7, 8, 5}));
assert(tree.height() == 2);
assert(tree.erase(5));
assert(tree.shape() == "7(3(1,4),8)");
tree.check_invariants();
}
// Insertar en preorden repite la forma: cada nodo llega antes que sus descendientes.
void test_pre_order_rebuilds_shape() {
BstSet<int> tree;
for (int key : {50, 30, 70, 20, 40, 60, 80, 35, 65, 90}) {
tree.insert(key);
}
BstSet<int> copy;
for (int key : tree.pre_order()) {
copy.insert(key);
}
assert(copy.shape() == tree.shape());
}
void test_empty_tree() {
BstSet<int> tree;
tree.check_invariants();
assert(tree.height() == -1);
assert(tree.min() == nullptr);
assert(tree.max() == nullptr);
assert(tree.floor(1) == nullptr);
assert(tree.range(0, 10).empty());
assert(!tree.erase(1));
assert(tree.shape() == "_");
}
void test_repeated_insert() {
BstSet<int> tree;
assert(tree.insert(1));
assert(!tree.insert(1));
assert(tree.size() == 1);
}
void test_floor() {
BstSet<int> tree;
for (int key : {20, 10, 30, 5, 15}) {
tree.insert(key);
}
assert(tree.floor(4) == nullptr);
assert(*tree.floor(5) == 5);
assert(*tree.floor(14) == 10);
assert(*tree.floor(29) == 20);
assert(*tree.floor(100) == 30);
}
void test_range_is_half_open() {
BstSet<int> tree;
for (int key : {20, 10, 30, 5, 15}) {
tree.insert(key);
}
assert((tree.range(10, 30) == std::vector<int>{10, 15, 20}));
assert(tree.range(30, 10).empty());
assert(tree.range(11, 12).empty());
}
void test_sorted_insert_is_a_chain() {
BstSet<CountedKey> tree;
const int n = 1000;
for (int key = 0; key < n; key++) {
tree.insert(CountedKey{key});
}
assert(tree.height() == n - 1);
// Bajar hasta el final de la cadena hace dos comparaciones por nodo.
assert(CountedKey::comparisons == static_cast<long>(n) * (n - 1));
assert(tree.total_depth() == static_cast<long>(n) * (n - 1) / 2);
}
void test_random_against_set() {
std::mt19937 random(20261110);
std::uniform_int_distribution<int> key_of(0, 299);
std::uniform_int_distribution<int> action_of(0, 9);
BstSet<int> mine;
std::set<int> model;
for (int step = 0; step < 20000; step++) {
int key = key_of(random);
int action = action_of(random);
if (action < 5) {
assert(mine.insert(key) == model.insert(key).second);
} else if (action < 9) {
assert(mine.erase(key) == (model.erase(key) == 1));
} else {
assert(mine.contains(key) == (model.count(key) == 1));
auto after = model.upper_bound(key);
const int* floor = mine.floor(key);
assert((floor == nullptr) == (after == model.begin()));
assert(floor == nullptr || *floor == *std::prev(after));
}
mine.check_invariants();
assert(mine.size() == model.size());
if (step % 200 == 0) {
std::vector<int> expected(model.begin(), model.end());
assert(mine.in_order() == expected);
auto low = model.lower_bound(100);
auto high = model.lower_bound(200);
assert(mine.range(100, 200) == std::vector<int>(low, high));
assert(model.empty() || (*mine.min() == *model.begin() && *mine.max() == *model.rbegin()));
}
}
}
} // namespace
int main() {
test_review_questions();
test_pre_order_rebuilds_shape();
test_empty_tree();
test_repeated_insert();
test_floor();
test_range_is_half_open();
test_sorted_insert_is_a_chain();
test_random_against_set();
std::cout << "ok\n";
return 0;
}Laboratorio
Archivos de partida del laboratorio de la semana.
// Mapa ordenado sobre un árbol binario de búsqueda. Completen cada método marcado con not_implemented.
// La copia, el movimiento, height y pre_order_keys ya vienen hechos.
#ifndef BST_MAP_H
#define BST_MAP_H
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>
#include "not_implemented.h"
// Invariantes:
// - En cada nodo, todas las claves del subárbol izquierdo son menores que la suya, y todas las
// del derecho son mayores. No hay claves repetidas.
// - Si x tiene hijo h, entonces h->parent == x. La raíz tiene padre nulo.
// - El árbol tiene exactamente size_ nodos, y root_ es nulo si y solo si size_ == 0.
//
// K solo necesita operator<, y K y V se copian y se asignan. operator[] además pide V{}.
//
// Ownership: el mapa es owner de todos sus nodos. Los punteros de find, floor, ceiling, min_key y
// max_key, y los iteradores, siguen válidos al insertar. Al borrar una clave con dos hijos, el nodo
// del sucesor desaparece y su contenido pasa a otro nodo: dejan de valer los que señalaban al
// sucesor y al nodo borrado. Tras un borrado, no los usen.
template <typename K, typename V>
class BstMap {
struct Node {
K key;
V value;
Node* parent = nullptr;
Node* left = nullptr;
Node* right = nullptr;
};
friend struct BstMapTestAccess;
public:
// Lo que entrega un iterador. W es V, o const V si el mapa es const.
template <typename W>
struct Entry {
const K& key;
W& value;
};
template <typename W>
class Iter {
public:
Entry<W> operator*() const { return {node_->key, node_->value}; }
// Paso 3: avanza al sucesor.
Iter& operator++() { not_implemented("Iter::operator++"); }
bool operator==(const Iter& other) const { return node_ == other.node_; }
bool operator!=(const Iter& other) const { return node_ != other.node_; }
private:
friend class BstMap;
explicit Iter(Node* node) : node_(node) {}
Node* node_;
};
using iterator = Iter<V>;
using const_iterator = Iter<const V>;
BstMap() = default;
// Paso 1: libera todos los nodos en postorden, sin recursión: baje hasta una hoja, bórrela
// y vuelva por parent, cortando antes el enlace del padre. La prueba corre con poca pila.
~BstMap() { not_implemented("~BstMap"); }
// Delega en el constructor por defecto: si una copia lanza una excepción, el objeto ya
// existe, así que corre el destructor y libera lo copiado hasta ahí.
BstMap(const BstMap& other) : BstMap() {
copy_nodes_from(other);
}
BstMap& operator=(const BstMap& other) {
BstMap copy(other);
swap(copy);
return *this;
}
BstMap(BstMap&& other) noexcept
: root_(std::exchange(other.root_, nullptr)), size_(std::exchange(other.size_, 0)) {}
BstMap& operator=(BstMap&& other) noexcept {
BstMap moved(std::move(other));
swap(moved);
return *this;
}
void swap(BstMap& other) noexcept {
std::swap(root_, other.root_);
std::swap(size_, other.size_);
}
std::size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
// Paso 1. Como std::map::insert: si la clave ya estaba, no cambia su valor y devuelve false.
// Con un ciclo, sin recursión: la prueba del destructor arma una cadena con poca pila.
bool insert(const K&, const V&) { not_implemented("insert"); }
// Paso 1. Como en std::map: si la clave no está, la inserta con V{}.
V& operator[](const K&) { not_implemented("operator[]"); }
// Paso 1. Puntero al valor; nulo si la clave no está.
V* find(const K&) { not_implemented("find"); }
const V* find(const K&) const { not_implemented("find const"); }
// Paso 2. Devuelve false si la clave no estaba.
bool erase(const K&) { not_implemented("erase"); }
// Paso 3. Punteros a la clave menor y a la mayor; nulos si está vacío.
const K* min_key() const { not_implemented("min_key"); }
const K* max_key() const { not_implemented("max_key"); }
// Paso 3. Puntero a la mayor clave <= key; nulo si no hay.
const K* floor(const K&) const { not_implemented("floor"); }
// Paso 3. Puntero a la menor clave >= key; nulo si no hay.
const K* ceiling(const K&) const { not_implemented("ceiling"); }
// Paso 3. Los pares con clave en [low, high), en orden. Debe costar O(altura + cantidad de pares).
std::vector<std::pair<K, V>> range(const K&, const K&) const { not_implemented("range"); }
// Paso 3. end es el iterador con nodo nulo.
iterator begin() { not_implemented("begin"); }
iterator end() { not_implemented("end"); }
const_iterator begin() const { not_implemented("begin const"); }
const_iterator end() const { not_implemented("end const"); }
// Paso 1. Las claves en orden.
std::vector<K> in_order_keys() const { not_implemented("in_order_keys"); }
// Las claves en preorden, que fijan la forma del árbol. Con pila propia, sin recursión.
std::vector<K> pre_order_keys() const {
std::vector<K> keys;
keys.reserve(size_);
std::vector<const Node*> pending;
if (root_ != nullptr) {
pending.push_back(root_);
}
while (!pending.empty()) {
const Node* node = pending.back();
pending.pop_back();
keys.push_back(node->key);
if (node->right != nullptr) {
pending.push_back(node->right);
}
if (node->left != nullptr) {
pending.push_back(node->left);
}
}
return keys;
}
// Altura: aristas del camino más largo desde la raíz. Vacío: -1. Un solo nodo: 0.
int height() const {
int levels = 0;
std::vector<const Node*> level;
if (root_ != nullptr) {
level.push_back(root_);
}
while (!level.empty()) {
std::vector<const Node*> next;
for (const Node* node : level) {
if (node->left != nullptr) {
next.push_back(node->left);
}
if (node->right != nullptr) {
next.push_back(node->right);
}
}
level = std::move(next);
levels++;
}
return levels - 1;
}
// Paso 1. Con assert, comprueba todas las invariantes de arriba. Puede ser recursiva: las
// pruebas la usan en árboles pequeños. Mirar solo a los hijos directos no alcanza.
void check_invariants() const { not_implemented("check_invariants"); }
private:
// Sugeridas: estas funciones privadas evitan repetir código, y las semanas 11 y 12 las usarán.
// Pueden agregar otras.
// El nodo con la menor clave de este subárbol. node no es nulo.
static Node* min_node(Node*) { not_implemented("min_node"); }
// El nodo que sigue en el orden de las claves; nulo si es el último.
static Node* successor(Node*) { not_implemented("successor"); }
// El nodo con esa clave; nulo si no está.
Node* find_node(const K&) const { not_implemented("find_node"); }
// El nodo con esa clave, y si se acaba de crear. Lo usan insert y operator[]. Con un ciclo.
std::pair<Node*, bool> insert_node(const K&, const V&) { not_implemented("insert_node"); }
// En el lugar de old_child, que es hijo de parent (o la raíz), pone a child, que puede ser
// nulo. Actualiza el enlace de parent, o root_, y el padre de child.
void replace_child(Node*, Node*, Node*) { not_implemented("replace_child"); }
// Quita node, y devuelve el padre del nodo que salió de verdad del árbol.
Node* remove_node(Node*) { not_implemented("remove_node"); }
// Copia la forma de other en preorden, sin recursión: baja por los hijos que faltan y
// vuelve con el puntero al padre. El árbol a medias queda siempre colgado de root_.
// Copia el nodo con todos sus campos, sin sus enlaces.
static Node* clone_node(const Node* from, Node* parent) {
Node* copy = new Node(*from);
copy->parent = parent;
copy->left = nullptr;
copy->right = nullptr;
return copy;
}
void copy_nodes_from(const BstMap& other) {
if (other.root_ == nullptr) {
return;
}
const Node* from = other.root_;
root_ = clone_node(from, nullptr);
Node* to = root_;
while (true) {
if (from->left != nullptr && to->left == nullptr) {
from = from->left;
to->left = clone_node(from, to);
to = to->left;
} else if (from->right != nullptr && to->right == nullptr) {
from = from->right;
to->right = clone_node(from, to);
to = to->right;
} else if (to->parent != nullptr) {
from = from->parent;
to = to->parent;
} else {
break;
}
}
size_ = other.size_;
}
Node* root_ = nullptr;
std::size_t size_ = 0;
};
#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í la tabla de `make bench` y respondan con sus propios números.
## Cómo midieron
Máquina, cómo compilaron, cuántas repeticiones y cuántas corridas.
## Tabla
## Altura
¿Cuánto vale `h/log2n` con claves al azar, y qué tan parecido se mantiene al crecer n? ¿Por qué con claves
ordenadas la altura es exactamente n - 1?
## Tiempo
Cada vez que n se duplica, ¿por cuánto se multiplica el tiempo en cada columna? ¿Qué predecía el análisis?
## Hasta qué n aguanta
¿Hasta qué n aguantó el caso ordenado en su máquina con ese límite de tiempo? Estimen, con la misma proporción,
cuánto tardaría en insertar las palabras de `palabras.txt`, que vienen ordenadas. ¿Qué cambiaría
si el destructor fuera recursivo?// Paso 4. Frecuencia de palabras de un texto, en el orden de las claves:
// word_freq archivo imprime "palabra cantidad" de todas las palabras
// word_freq archivo low high solo las de [low, high)
// En ambos casos termina con la línea
// palabras: T, distintas: D, altura: H
// Con pocos argumentos, o si no puede abrir el archivo, escribe un mensaje en std::cerr y devuelve 2.
#include <string>
#include "bst_map.h"
#include "not_implemented.h"
#include "words.h"
int main() {
// Usen split_words(texto), de words.h, y un BstMap<std::string, int>.
not_implemented("word_freq");
}// 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// Mide su BstMap: altura y tiempo de insertar n claves, al azar y ordenadas.
// El caso ordenado se detiene cuando una corrida pasa de limit_seconds: hasta ahí aguantó.
// Los tiempos dependen de la máquina. Compilar con -O2 y sin sanitizers.
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>
#include "bench.h"
#include "bst_map.h"
namespace {
const double limit_seconds = 1.0;
std::vector<int> ascending(int n) {
std::vector<int> keys(n);
std::iota(keys.begin(), keys.end(), 0);
return keys;
}
// Inserta las claves, deja la altura en height y devuelve la mediana de 3 corridas.
double insert_all(const std::vector<int>& keys, int& height) {
return median_seconds(3, [&] {
BstMap<int, int> map;
for (int key : keys) {
map.insert(key, key);
}
height = map.height();
keep(map.size());
});
}
} // namespace
int main() {
std::cout << std::setw(9) << "n" << std::setw(7) << "log2n"
<< std::setw(7) << "h.azar" << std::setw(8) << "h/log2n" << std::setw(9) << "t.azar"
<< std::setw(9) << "h.orden" << std::setw(9) << "t.orden" << "\n";
std::mt19937 random(10);
bool sorted_alive = true;
for (int n = 1000; n <= 1024000; n *= 2) {
std::vector<int> keys = ascending(n);
std::vector<int> shuffled = keys;
std::shuffle(shuffled.begin(), shuffled.end(), random);
// Primero el caso ordenado: los bloques que libera una corrida al azar quedan en otro
// orden, y si se reusan, los nodos de la cadena ya no estarían seguidos en la memoria.
int sorted_height = 0;
double sorted_time = 0;
if (sorted_alive) {
sorted_time = insert_all(keys, sorted_height);
}
int random_height = 0;
double random_time = insert_all(shuffled, random_height);
double log_n = std::log2(n);
std::cout << std::setw(9) << n << std::fixed << std::setprecision(1) << std::setw(7) << log_n
<< std::setw(7) << random_height << std::setprecision(2) << std::setw(8) << random_height / log_n
<< std::setprecision(4) << std::setw(9) << random_time;
if (sorted_alive) {
std::cout << std::setw(9) << sorted_height << std::setw(9) << sorted_time;
sorted_alive = sorted_time < limit_seconds;
} else {
std::cout << std::setw(9) << "-" << std::setw(9) << "-";
}
std::cout << std::endl;
}
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 del laboratorio. Se corre una por proceso: map_tests nombre.
#include <algorithm>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <iterator>
#include <map>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include "bst_map.h"
// Permite a las pruebas dañar un árbol, para ver si check_invariants lo nota.
struct BstMapTestAccess {
template <typename Map>
static auto* root(Map& map) { return map.root_; }
template <typename Map>
static void add_to_size(Map& map) { map.size_++; }
};
namespace {
using IntMap = BstMap<int, int>;
// Clave que solo se puede comparar con <, y que cuenta las comparaciones.
struct LessOnly {
int value = 0;
inline static long comparisons = 0;
bool operator<(const LessOnly& other) const {
comparisons++;
return value < other.value;
}
};
// Valor que cuenta cuántos objetos viven: una copia olvidada se nota aquí.
struct Live {
int value = 0;
inline static long count = 0;
Live() { count++; }
explicit Live(int v) : value(v) { count++; }
Live(const Live& other) : value(other.value) { count++; }
Live& operator=(const Live& other) = default;
~Live() { count--; }
};
IntMap make_map(const std::vector<int>& keys) {
IntMap map;
for (int key : keys) {
map.insert(key, key * 10);
}
return map;
}
// Claves en un orden que deja un árbol perfecto si se insertan en orden: la raíz primero.
void add_balanced(std::vector<int>& keys, int low, int high) {
if (low > high) {
return;
}
int middle = low + (high - low) / 2;
keys.push_back(middle);
add_balanced(keys, low, middle - 1);
add_balanced(keys, middle + 1, high);
}
template <typename K, typename V>
void assert_same(const BstMap<K, V>& mine, const std::map<K, V>& model) {
mine.check_invariants();
assert(mine.size() == model.size());
std::vector<K> keys;
for (const auto& [key, value] : model) {
keys.push_back(key);
const V* found = mine.find(key);
assert(found != nullptr);
assert(*found == value);
}
assert(mine.in_order_keys() == keys);
}
// ---- Sesión 1: insertar, buscar y recorrer ----
void test_new_is_empty() {
IntMap map;
map.check_invariants();
assert(map.empty());
assert(map.size() == 0);
assert(map.height() == -1);
assert(map.find(1) == nullptr);
assert(map.in_order_keys().empty());
}
void test_insert_and_find() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
map.check_invariants();
assert(map.size() == 6);
for (int key : {1, 3, 4, 5, 7, 8}) {
assert(map.find(key) != nullptr);
assert(*map.find(key) == key * 10);
}
for (int key : {0, 2, 6, 9}) {
assert(map.find(key) == nullptr);
}
}
void test_insert_does_not_overwrite() {
IntMap map;
assert(map.insert(1, 100));
assert(!map.insert(1, 200));
assert(*map.find(1) == 100);
assert(map.size() == 1);
map.check_invariants();
}
void test_find_lets_edit_value() {
IntMap map = make_map({2, 1, 3});
*map.find(1) = 77;
const IntMap& reader = map;
assert(*reader.find(1) == 77);
assert(reader.find(9) == nullptr);
}
void test_subscript_operator() {
IntMap map;
map[4] = 40;
map[2]++;
map[2]++;
assert(map.size() == 2);
assert(map[4] == 40);
assert(map[2] == 2);
assert(map.size() == 2);
map.check_invariants();
}
void test_in_order_is_sorted() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
assert((map.in_order_keys() == std::vector<int>{1, 3, 4, 5, 7, 8}));
}
void test_shape_follows_insertion_order() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
assert((map.pre_order_keys() == std::vector<int>{5, 3, 1, 4, 8, 7}));
assert(map.height() == 2);
}
void test_sorted_keys_make_a_chain() {
IntMap map;
for (int i = 0; i < 500; i++) {
map.insert(i, i);
assert(map.height() == i);
}
map.check_invariants();
}
void test_comparisons_follow_height() {
using Map = BstMap<LessOnly, int>;
std::vector<int> keys;
add_balanced(keys, 0, 1022);
Map map;
for (int key : keys) {
map.insert(LessOnly{key}, key);
}
assert(map.height() == 9);
// Bajar cuesta como máximo dos comparaciones por nivel, en find y en insert.
for (int key : {0, 511, 1022, 300}) {
LessOnly::comparisons = 0;
assert(map.find(LessOnly{key}) != nullptr);
assert(LessOnly::comparisons <= 20);
LessOnly::comparisons = 0;
assert(!map.insert(LessOnly{key}, 0));
assert(LessOnly::comparisons <= 20);
}
LessOnly::comparisons = 0;
assert(map.find(LessOnly{-1}) == nullptr);
assert(LessOnly::comparisons <= 20);
}
void test_only_needs_less_than() {
BstMap<LessOnly, std::string> map;
map.insert(LessOnly{2}, "dos");
map.insert(LessOnly{1}, "uno");
assert(!map.insert(LessOnly{2}, "otro"));
assert(*map.find(LessOnly{2}) == "dos");
assert(map.erase(LessOnly{2}));
assert(map.find(LessOnly{2}) == nullptr);
map.check_invariants();
}
void test_strings_as_keys() {
BstMap<std::string, int> map;
map.insert("pera", 1);
map.insert("ananá", 2);
map.insert("zapote", 3);
map["ñame"] = 4;
// Orden de bytes: la ñ, que empieza con 0xC3, va después de la z.
assert((map.in_order_keys() == std::vector<std::string>{"ananá", "pera", "zapote", "ñame"}));
map.check_invariants();
}
void test_no_objects_left_behind() {
{
BstMap<int, Live> map;
for (int i = 0; i < 100; i++) {
map.insert((i * 37) % 101, Live(i));
}
assert(Live::count == 100);
}
assert(Live::count == 0);
}
void test_copy_is_independent() {
IntMap original = make_map({5, 3, 8, 1, 4, 7});
IntMap copy(original);
copy.check_invariants();
assert(copy.pre_order_keys() == original.pre_order_keys());
copy.insert(9, 90);
*copy.find(5) = 0;
assert(original.find(9) == nullptr);
assert(*original.find(5) == 50);
IntMap assigned;
assigned.insert(100, 100);
assigned = original;
assigned.check_invariants();
assert(assigned.find(100) == nullptr);
assert(assigned.pre_order_keys() == original.pre_order_keys());
}
void test_self_assignment() {
IntMap map = make_map({2, 1, 3});
IntMap& alias = map;
map = alias;
map.check_invariants();
assert(map.size() == 3);
map = std::move(alias);
map.check_invariants();
assert(map.size() == 3);
}
void test_move_leaves_empty_and_usable() {
IntMap source = make_map({2, 1, 3});
IntMap target(std::move(source));
assert(target.size() == 3);
target.check_invariants();
source.check_invariants();
assert(source.empty());
source.insert(1, 1);
assert(source.size() == 1);
IntMap other;
other = std::move(target);
assert(other.size() == 3);
other.check_invariants();
}
// Con un tope bajo de pila, un destructor recursivo se queda sin ella. test_lab.py pone el tope.
void test_destructor_without_recursion() {
IntMap map;
for (int i = 0; i < 6000; i++) {
map.insert(i, i);
}
assert(map.height() == 5999);
}
// Cada una de estas daña un árbol sano. check_invariants debe detener el programa.
// test_lab.py exige ver "sano" y luego un fallo, así que no pasan sin check_invariants.
// Si no se detiene, se sale sin destruir: el fallo no debe venir del destructor sobre un árbol roto.
void test_broken_deep_order() {
IntMap map = make_map({5, 3, 4});
map.check_invariants();
std::cout << "sano" << std::endl;
// 7 es mayor que su padre 3, como debe ser, pero está a la izquierda del 5.
BstMapTestAccess::root(map)->left->right->key = 7;
map.check_invariants();
std::_Exit(0);
}
void test_broken_repeated_key() {
IntMap map = make_map({5, 8});
map.check_invariants();
std::cout << "sano" << std::endl;
BstMapTestAccess::root(map)->right->key = 5;
map.check_invariants();
std::_Exit(0);
}
void test_broken_parent() {
IntMap map = make_map({5, 3, 8, 4});
map.check_invariants();
std::cout << "sano" << std::endl;
auto* root = BstMapTestAccess::root(map);
root->left->right->parent = root;
map.check_invariants();
std::_Exit(0);
}
void test_broken_size() {
IntMap map = make_map({5, 3, 8});
map.check_invariants();
std::cout << "sano" << std::endl;
BstMapTestAccess::add_to_size(map);
map.check_invariants();
std::_Exit(0);
}
// ---- Sesión 1: borrar ----
void test_erase_absent() {
IntMap map = make_map({2, 1, 3});
assert(!map.erase(9));
assert(map.size() == 3);
IntMap empty;
assert(!empty.erase(1));
map.check_invariants();
}
void test_erase_leaf() {
IntMap map = make_map({5, 3, 8});
assert(map.erase(3));
map.check_invariants();
assert((map.pre_order_keys() == std::vector<int>{5, 8}));
assert(map.find(3) == nullptr);
assert(map.size() == 2);
}
void test_erase_one_child() {
IntMap left = make_map({5, 3, 1, 8});
assert(left.erase(3));
left.check_invariants();
assert((left.pre_order_keys() == std::vector<int>{5, 1, 8}));
IntMap right = make_map({5, 3, 4, 8});
assert(right.erase(3));
right.check_invariants();
assert((right.pre_order_keys() == std::vector<int>{5, 4, 8}));
}
void test_erase_root_with_one_child() {
IntMap map = make_map({5, 8, 9});
assert(map.erase(5));
map.check_invariants();
assert((map.pre_order_keys() == std::vector<int>{8, 9}));
assert(map.erase(8));
assert(map.erase(9));
map.check_invariants();
assert(map.empty());
}
void test_erase_two_children_uses_successor() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
assert(map.erase(5));
map.check_invariants();
assert((map.pre_order_keys() == std::vector<int>{7, 3, 1, 4, 8}));
assert(*map.find(7) == 70);
assert(map.find(5) == nullptr);
}
void test_erase_successor_is_right_child() {
IntMap map = make_map({5, 3, 8, 9});
assert(map.erase(5));
map.check_invariants();
assert((map.pre_order_keys() == std::vector<int>{8, 3, 9}));
assert(*map.find(8) == 80);
}
void test_erase_successor_with_right_child() {
IntMap map = make_map({2, 1, 6, 4, 7, 5});
assert(map.erase(2));
map.check_invariants();
assert((map.pre_order_keys() == std::vector<int>{4, 1, 6, 5, 7}));
assert(*map.find(4) == 40);
assert(*map.find(5) == 50);
}
void test_erase_releases_objects() {
{
BstMap<int, Live> map;
for (int i = 0; i < 100; i++) {
map.insert((i * 37) % 101, Live(i));
}
for (int i = 0; i < 100; i += 2) {
assert(map.erase((i * 37) % 101));
map.check_invariants();
}
assert(Live::count == 50);
}
assert(Live::count == 0);
}
void test_random_against_map() {
std::mt19937 random(20261110);
std::uniform_int_distribution<int> key_of(0, 199);
std::uniform_int_distribution<int> action_of(0, 9);
IntMap mine;
std::map<int, int> model;
for (int step = 0; step < 6000; step++) {
int key = key_of(random);
int action = action_of(random);
if (action < 5) {
auto [position, added] = model.insert({key, step});
(void)position;
assert(mine.insert(key, step) == added);
} else if (action < 9) {
assert(mine.erase(key) == (model.erase(key) == 1));
} else {
const int* found = mine.find(key);
auto position = model.find(key);
assert((found != nullptr) == (position != model.end()));
assert(found == nullptr || *found == position->second);
}
mine.check_invariants();
assert(mine.size() == model.size());
if (step % 50 == 0) {
assert_same(mine, model);
}
}
assert_same(mine, model);
}
void test_random_strings() {
std::mt19937 random(11);
std::uniform_int_distribution<int> letter(0, 5);
auto word = [&] {
std::string text;
for (int i = 0; i < 3; i++) {
text += static_cast<char>('a' + letter(random));
}
return text;
};
BstMap<std::string, std::string> mine;
std::map<std::string, std::string> model;
for (int step = 0; step < 3000; step++) {
std::string key = word();
if (step % 3 != 2) {
assert(mine.insert(key, word() + "valor largo para el heap") == model.insert({key, ""}).second);
model[key] = *mine.find(key);
} else {
assert(mine.erase(key) == (model.erase(key) == 1));
}
mine.check_invariants();
}
assert_same(mine, model);
}
void test_copy_after_random_operations() {
std::mt19937 random(5);
std::uniform_int_distribution<int> key_of(0, 99);
IntMap mine;
for (int step = 0; step < 1000; step++) {
int key = key_of(random);
if (step % 3 == 0) {
mine.erase(key);
} else {
mine.insert(key, key);
}
}
IntMap copy(mine);
copy.check_invariants();
assert(copy.pre_order_keys() == mine.pre_order_keys());
}
// ---- Sesión 2: consultas ordenadas ----
void test_min_max() {
IntMap map;
assert(map.min_key() == nullptr);
assert(map.max_key() == nullptr);
map = make_map({5, 3, 8, 1, 4, 7});
assert(*map.min_key() == 1);
assert(*map.max_key() == 8);
}
void test_floor_ceiling_cases() {
IntMap map = make_map({20, 10, 30, 5, 15, 25, 35});
assert(map.floor(4) == nullptr);
assert(*map.floor(5) == 5);
assert(*map.floor(14) == 10);
assert(*map.floor(26) == 25);
assert(*map.floor(99) == 35);
assert(map.ceiling(36) == nullptr);
assert(*map.ceiling(35) == 35);
assert(*map.ceiling(16) == 20);
assert(*map.ceiling(11) == 15);
assert(*map.ceiling(-1) == 5);
IntMap empty;
assert(empty.floor(1) == nullptr);
assert(empty.ceiling(1) == nullptr);
}
// Con std::map, techo es lower_bound, y piso es el anterior a upper_bound.
void test_floor_ceiling_random() {
std::mt19937 random(77);
std::uniform_int_distribution<int> key_of(0, 999);
IntMap mine;
std::map<int, int> model;
for (int i = 0; i < 300; i++) {
int key = key_of(random);
mine.insert(key, key);
model.insert({key, key});
}
for (int key = -1; key <= 1000; key++) {
auto at_or_after = model.lower_bound(key);
const int* ceiling = mine.ceiling(key);
assert((ceiling == nullptr) == (at_or_after == model.end()));
assert(ceiling == nullptr || *ceiling == at_or_after->first);
auto after = model.upper_bound(key);
const int* floor = mine.floor(key);
assert((floor == nullptr) == (after == model.begin()));
assert(floor == nullptr || *floor == std::prev(after)->first);
}
}
void test_range_is_half_open() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
auto pairs = map.range(3, 7);
assert((pairs == std::vector<std::pair<int, int>>{{3, 30}, {4, 40}, {5, 50}}));
assert(map.range(6, 7).empty());
assert(map.range(7, 3).empty());
assert(map.range(4, 4).empty());
assert(map.range(0, 100).size() == 6);
IntMap empty;
assert(empty.range(0, 100).empty());
}
void test_range_random() {
std::mt19937 random(123);
std::uniform_int_distribution<int> key_of(0, 500);
IntMap mine;
std::map<int, int> model;
for (int i = 0; i < 200; i++) {
int key = key_of(random);
mine.insert(key, i);
model.insert({key, i});
}
for (int i = 0; i < 500; i++) {
int low = key_of(random);
int high = key_of(random);
std::vector<std::pair<int, int>> expected;
if (low <= high) {
for (auto it = model.lower_bound(low); it != model.lower_bound(high); ++it) {
expected.push_back(*it);
}
}
assert(mine.range(low, high) == expected);
}
}
void test_range_does_not_walk_everything() {
using Map = BstMap<LessOnly, int>;
std::vector<int> keys;
add_balanced(keys, 0, 4094);
Map map;
for (int key : keys) {
map.insert(LessOnly{key}, key);
}
LessOnly::comparisons = 0;
auto pairs = map.range(LessOnly{1000}, LessOnly{1005});
assert(pairs.size() == 5);
// Altura 11: un recorrido completo haría al menos 4095 comparaciones.
assert(LessOnly::comparisons <= 100);
}
void test_iterator_in_order() {
IntMap map = make_map({5, 3, 8, 1, 4, 7});
std::vector<int> keys;
std::vector<int> values;
for (auto [key, value] : map) {
keys.push_back(key);
values.push_back(value);
}
assert((keys == std::vector<int>{1, 3, 4, 5, 7, 8}));
assert((values == std::vector<int>{10, 30, 40, 50, 70, 80}));
}
void test_iterator_edits_value() {
IntMap map = make_map({2, 1, 3});
for (auto [key, value] : map) {
value = key + 1;
}
assert(*map.find(1) == 2);
assert(*map.find(3) == 4);
}
void test_const_iterator() {
const IntMap map = make_map({2, 1, 3});
int sum = 0;
for (const auto& entry : map) {
sum += entry.key + entry.value;
}
assert(sum == 66);
auto entry = *map.begin();
static_assert(std::is_const_v<std::remove_reference_t<decltype(entry.value)>>);
}
void test_iterator_of_empty_map() {
IntMap map;
assert(map.begin() == map.end());
assert(!(map.begin() != map.end()));
}
// El iterador solo guarda su nodo, así que una inserción no lo invalida, igual que en std::map.
void test_iterator_survives_insert() {
IntMap map = make_map({10, 20, 30});
auto it = map.begin();
assert((*it).key == 10);
map.insert(15, 150);
map.insert(5, 50);
++it;
assert((*it).key == 15);
}
void test_iterator_random() {
std::mt19937 random(9);
std::uniform_int_distribution<int> key_of(0, 2000);
IntMap mine;
std::map<int, int> model;
for (int i = 0; i < 1000; i++) {
int key = key_of(random);
if (i % 4 == 3) {
mine.erase(key);
model.erase(key);
} else {
mine.insert(key, i);
model.insert({key, i});
}
}
auto expected = model.begin();
for (auto [key, value] : mine) {
assert(expected != model.end());
assert(key == expected->first);
assert(value == expected->second);
++expected;
}
assert(expected == model.end());
}
// Recorrer una cadena no debe hacer recursión ni volver a bajar desde la raíz cada vez.
void test_iterate_chain_comparisons() {
BstMap<LessOnly, int> map;
for (int i = 0; i < 2000; i++) {
map.insert(LessOnly{i}, i);
}
LessOnly::comparisons = 0;
long sum = 0;
for (auto [key, value] : map) {
(void)key;
sum += value;
}
assert(sum == 1999L * 2000 / 2);
assert(LessOnly::comparisons == 0);
}
struct Test {
const char* name;
void (*run)();
};
const Test tests[] = {
{"new_is_empty", test_new_is_empty},
{"insert_and_find", test_insert_and_find},
{"insert_does_not_overwrite", test_insert_does_not_overwrite},
{"find_lets_edit_value", test_find_lets_edit_value},
{"subscript_operator", test_subscript_operator},
{"in_order_is_sorted", test_in_order_is_sorted},
{"shape_follows_insertion_order", test_shape_follows_insertion_order},
{"sorted_keys_make_a_chain", test_sorted_keys_make_a_chain},
{"comparisons_follow_height", test_comparisons_follow_height},
{"only_needs_less_than", test_only_needs_less_than},
{"strings_as_keys", test_strings_as_keys},
{"no_objects_left_behind", test_no_objects_left_behind},
{"copy_is_independent", test_copy_is_independent},
{"self_assignment", test_self_assignment},
{"move_leaves_empty_and_usable", test_move_leaves_empty_and_usable},
{"destructor_without_recursion", test_destructor_without_recursion},
{"broken_deep_order", test_broken_deep_order},
{"broken_repeated_key", test_broken_repeated_key},
{"broken_parent", test_broken_parent},
{"broken_size", test_broken_size},
{"erase_absent", test_erase_absent},
{"erase_leaf", test_erase_leaf},
{"erase_one_child", test_erase_one_child},
{"erase_root_with_one_child", test_erase_root_with_one_child},
{"erase_two_children_uses_successor", test_erase_two_children_uses_successor},
{"erase_successor_is_right_child", test_erase_successor_is_right_child},
{"erase_successor_with_right_child", test_erase_successor_with_right_child},
{"erase_releases_objects", test_erase_releases_objects},
{"random_against_map", test_random_against_map},
{"random_strings", test_random_strings},
{"copy_after_random_operations", test_copy_after_random_operations},
{"min_max", test_min_max},
{"floor_ceiling_cases", test_floor_ceiling_cases},
{"floor_ceiling_random", test_floor_ceiling_random},
{"range_is_half_open", test_range_is_half_open},
{"range_random", test_range_random},
{"range_does_not_walk_everything", test_range_does_not_walk_everything},
{"iterator_in_order", test_iterator_in_order},
{"iterator_edits_value", test_iterator_edits_value},
{"const_iterator", test_const_iterator},
{"iterator_of_empty_map", test_iterator_of_empty_map},
{"iterator_survives_insert", test_iterator_survives_insert},
{"iterator_random", test_iterator_random},
{"iterate_chain_comparisons", test_iterate_chain_comparisons},
};
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: map_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;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 10: árboles binarios de búsqueda. 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-basics prueba insert, find, recorrido y check_invariants (sesión 1)
# make test-erase prueba erase, contra std::map (sesión 1)
# make test-queries prueba floor, ceiling, range e iterador (sesión 2)
# make test-words prueba el programa de frecuencias (sesión 2)
# make test-lab prueba todo su laboratorio; falla hasta que lo terminen
# make bench mide su mapa, con -O2 y sin sanitizers
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
DATA ?= ../../data
LAB_DIR ?= lab
BUILD = build
PROGRAMS = tree_demo erase_demo insertion_orders query_demo map_queries map_guarantees test_structures
BENCHES = height_table insert_times dictionary_order byte_order map_vs_unordered
COMMON_HEADERS = bst_set.h counted_key.h alloc_counter.h bench.h
LAB_HEADERS = $(LAB_DIR)/bst_map.h lab/words.h lab/not_implemented.h
.PHONY: all lab test test-basics test-erase test-queries test-words test-lab bench 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 mapa de esa carpeta.
$(BUILD)/$(LAB_DIR)/map_tests: lab/tests/map_tests.cpp $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab $< -o $@
$(BUILD)/$(LAB_DIR)/word_freq: $(LAB_DIR)/word_freq.cpp $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab $< -o $@
$(BUILD)/$(LAB_DIR)/bench_bst: lab/tests/bench_bst.cpp $(LAB_HEADERS) bench.h | $(BUILD)/$(LAB_DIR)
$(CXX) $(BENCHFLAGS) -I$(LAB_DIR) -Ilab -I. $< -o $@
$(BUILD) $(BUILD)/$(LAB_DIR):
mkdir -p $@
lab: $(BUILD)/$(LAB_DIR)/map_tests $(BUILD)/$(LAB_DIR)/word_freq $(BUILD)/$(LAB_DIR)/bench_bst
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory LAB_DIR=solution lab; fi
DATA=$(DATA) python3 -m unittest discover -p 'test_*.py'
test-basics: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestBasics
test-erase: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestErase
test-queries: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestQueries
test-words: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestWordFreq
test-lab: all lab
LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
bench: $(BUILD)/$(LAB_DIR)/bench_bst
$<
clean:
rm -rf $(BUILD) __pycache__