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::map frente a std::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:

  1. Escribir BstMap<K, V>: insert, find y un recorrido en orden, con ownership claro y check_invariants.
  2. Escribir erase, con los tres casos. Probar con operaciones al azar contra std::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

  1. Inserten 5, 3, 8, 1, 4 y 7 en un árbol vacío. Dibújenlo y escriban sus recorridos en orden, preorden y postorden.
  2. Borren el 5 de ese árbol usando el sucesor. Dibujen el resultado.
  3. ¿Qué orden de inserción da la mayor altura para esas mismas claves? ¿Cuánto mide?
  4. ¿Por qué un destructor recursivo puede fallar en un árbol degenerado de un millón de nodos?
  5. Den dos consultas que std::map responde bien y std::unordered_map no.

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.

alloc_counter.hDescargar alloc_counter.h
// 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
bench.hDescargar bench.h
// Medidor de tiempos del curso. Compilar con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H

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

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

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

#endif
bst_set.hDescargar bst_set.h
// Á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
byte_order.cppDescargar byte_order.cpp
// 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;
}
counted_key.hDescargar counted_key.h
// 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
dictionary_order.cppDescargar dictionary_order.cpp
// 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;
}
erase_demo.cppDescargar erase_demo.cpp
// 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;
}
height_table.cppDescargar height_table.cpp
// 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;
}
insert_times.cppDescargar insert_times.cpp
// 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;
}
insertion_orders.cppDescargar insertion_orders.cpp
// 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;
}
map_guarantees.cppDescargar map_guarantees.cpp
// 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;
}
map_queries.cppDescargar map_queries.cpp
// 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;
}
map_vs_unordered.cppDescargar map_vs_unordered.cpp
// 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;
}
query_demo.cppDescargar query_demo.cpp
// 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;
}
tree_demo.cppDescargar tree_demo.cpp
// 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.

errors/forgot_successor.cppDescargar errors/forgot_successor.cpp
// 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;
}
errors/local_check.cppDescargar errors/local_check.cpp
// 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;
}
errors/pointer_after_erase.cppDescargar errors/pointer_after_erase.cpp
// 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;
}
errors/recursive_destroy.cppDescargar errors/recursive_destroy.cpp
// 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.

test_examples.pyDescargar test_examples.py
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos."""
import os
import pathlib
import 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()
test_lab.pyDescargar test_lab.py
"""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()
test_notes.pyDescargar test_notes.py
"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import os
import pathlib
import platform
import re
import subprocess
import unittest

try:
    import pty
except ImportError:
    pty = None

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-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()
test_structures.cppDescargar test_structures.cpp
// 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.

lab/bst_map.hDescargar lab/bst_map.h
// 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
lab/not_implemented.hDescargar lab/not_implemented.h
// Para los métodos que todavía no escriben: detiene el programa y dice cuál falta.
#ifndef NOT_IMPLEMENTED_H
#define NOT_IMPLEMENTED_H

#include <cstdio>
#include <cstdlib>

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

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

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?
lab/word_freq.cppDescargar lab/word_freq.cpp
// 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");
}
lab/words.hDescargar lab/words.h
// 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
lab/tests/bench_bst.cppDescargar lab/tests/bench_bst.cpp
// 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;
}
lab/tests/fixture.txtDescargar lab/tests/fixture.txt
—¿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?
lab/tests/map_tests.cppDescargar lab/tests/map_tests.cpp
// 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.

MakefileDescargar Makefile
# 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__