Semana 9 — Direccionamiento abierto
Objetivos
- Implementar una tabla hash con linear probing, con borrado correcto.
- Explicar por qué una tabla con peor agrupamiento puede ser más rápida en una máquina real.
- Leer cómo dos implementaciones actuales resuelven el mismo problema.
Teoría — Direccionamiento abierto: linear probing y tombstones · Swiss Tables y el dict de CPython
Lectura: Apuntes, semana 9.
Apoyo: Open Data Structures, sec. 5.2 (LinearHashTable). CLRS, capítulo Hash Tables, 4.ª edición, que resalta el linear probing por el efecto de la cache. Faster Go maps with Swiss Tables, Go Blog, 2025.
Temas:
- Todo dentro del arreglo: si la casilla está ocupada, se prueba la siguiente.
- Linear probing: buscar hasta encontrar la clave o una casilla vacía. Agrupamiento primario, y por qué α debe mantenerse lejos de 1.
- Por qué no se puede borrar vaciando la casilla. Tombstones, y limpiarlos al reconstruir la tabla.
- Encadenamiento frente a linear probing: el análisis favorece al primero con α alto. Las casillas contiguas pueden favorecer al segundo por la cache, pero depende del caso: se mide, con búsquedas exitosas y fallidas.
- Caso de estudio, Swiss Tables: casillas en grupos, con un byte de metadatos por casilla que guarda 7 bits del hash. Se comparan todos los bytes de un grupo a la vez y solo se revisan las claves que coinciden. Go las adoptó en su versión 1.24.
- Caso de estudio, el
dictde CPython: un arreglo de índices y otro de entradas, lo que conserva el orden de inserción y ahorra memoria. Su secuencia de probing no es lineal. - Qué cambió y qué no: las ideas son las de esta clase, y lo nuevo es la atención a la memoria.
- Error común: buscar hasta el primer tombstone en vez de hasta la primera casilla vacía.
Laboratorio — Corrector ortográfico: comparar diseños
Sesión 1:
- Escribir
ProbingHashMap<V>con linear probing, tombstones y crecimiento, con las mismas pruebas de la semana 7. - Agregar una prueba que inserte y borre muchas veces, y comprobar que las búsquedas no se vuelven lentas.
Sesión 2:
3. Escribir un corrector: carga un diccionario de palabras, revisa un texto y, para cada palabra desconocida, propone las que están a un cambio de distancia.
4. Hacer que el corrector funcione con la tabla de la semana 7, con la de esta semana, con std::unordered_map y con un arreglo ordenado con búsqueda binaria.
5. Medir los cuatro. Medir sus tablas con varios factores de carga, mirando el factor real: con capacidades potencia de 2, dos máximos distintos pueden dar la misma tabla. Explicar los resultados.
Preguntas de repaso
- Inserten en una tabla de 7 casillas, con h(k) = k mod 7 y linear probing, las claves 10, 17, 24 y 3. Dibujen la tabla. ¿Cuántas casillas revisa buscar 31?
- Borren el 17 vaciando la casilla. ¿Qué búsquedas fallan ahora, y por qué?
- ¿Por qué tener muchos tombstones vuelve lenta una tabla, aunque tenga pocos elementos?
- ¿Para qué guarda Swiss Tables 7 bits del hash en sus metadatos?
- ¿Qué dos arreglos usa el
dictde CPython? ¿Qué propiedad de los diccionarios de Python sale de ese diseño?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (12)Errores (2)Pruebas (3)Laboratorio (17)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Medidor de tiempos del curso. Compilar con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H
#include <algorithm>
#include <chrono>
#include <vector>
// Corre fn `reps` veces y devuelve la mediana en segundos.
template <typename Fn>
double median_seconds(int reps, Fn fn) {
std::vector<double> times;
times.reserve(reps);
for (int i = 0; i < reps; i++) {
auto start = std::chrono::steady_clock::now();
fn();
auto stop = std::chrono::steady_clock::now();
times.push_back(std::chrono::duration<double>(stop - start).count());
}
std::sort(times.begin(), times.end());
return times[times.size() / 2];
}
// Evita que el compilador borre un cálculo cuyo resultado no se usa.
template <typename T>
void keep(const T& value) {
volatile T sink = value;
(void)sink;
}
#endif// El diseño del dict de CPython, reducido: un arreglo de índices y otro de entradas. Muestra los
// dos arreglos después de cada operación. Usa la misma recurrencia de probing y las mismas reglas
// de tamaño que Objects/dictobject.c de Python 3.14, pero FNV-1a en vez del hash de Python.
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
#include "demo_tables.h"
namespace {
constexpr int kEmpty = -1;
constexpr int kDummy = -2;
class CompactDict {
public:
CompactDict() : indices_(8, kEmpty) {}
void set(const std::string& key, int value) {
const std::size_t slot = find_slot(key);
if (indices_[slot] >= 0) {
entries_[static_cast<std::size_t>(indices_[slot])].value = value;
return;
}
if (entries_.size() == usable()) {
resize();
}
indices_[first_free_slot(key)] = static_cast<int>(entries_.size());
entries_.push_back({key, value, true});
used_++;
}
void erase(const std::string& key) {
const std::size_t slot = find_slot(key);
if (indices_[slot] >= 0) {
entries_[static_cast<std::size_t>(indices_[slot])].alive = false;
indices_[slot] = kDummy;
used_--;
}
}
void print() const {
std::cout << " índices (" << indices_.size() << "):";
for (int index : indices_) {
if (index == kEmpty) {
std::cout << " _";
} else if (index == kDummy) {
std::cout << " X";
} else {
std::cout << " " << index;
}
}
std::cout << "\n entradas:";
for (std::size_t i = 0; i < entries_.size(); i++) {
std::cout << " " << i << ":";
if (entries_[i].alive) {
std::cout << entries_[i].key << "=" << entries_[i].value;
} else {
std::cout << "(borrada)";
}
}
std::cout << "\n";
}
void print_in_order() const {
std::cout << " recorrer:";
for (const Entry& entry : entries_) {
if (entry.alive) {
std::cout << " " << entry.key;
}
}
std::cout << "\n";
}
private:
struct Entry {
std::string key;
int value;
bool alive;
};
std::size_t usable() const { return indices_.size() * 2 / 3; }
// La casilla con la clave o, si no está, la casilla vacía donde termina el probing.
std::size_t find_slot(const std::string& key) const {
const std::uint64_t hash = demo_hash(key);
const std::uint64_t mask = indices_.size() - 1;
std::uint64_t perturb = hash;
std::uint64_t i = hash & mask;
while (indices_[i] != kEmpty) {
if (indices_[i] >= 0 && entries_[static_cast<std::size_t>(indices_[i])].key == key) {
return i;
}
perturb >>= 5;
i = (i * 5 + perturb + 1) & mask;
}
return i;
}
// Donde se inserta una clave nueva: reúsa el primer tombstone del camino, como una vacía.
std::size_t first_free_slot(const std::string& key) const {
const std::uint64_t hash = demo_hash(key);
const std::uint64_t mask = indices_.size() - 1;
std::uint64_t perturb = hash;
std::uint64_t i = hash & mask;
while (indices_[i] >= 0) {
perturb >>= 5;
i = (i * 5 + perturb + 1) & mask;
}
return i;
}
// Tamaño nuevo: la menor potencia de 2 que llegue a used * 3. Sin tombstones ni entradas borradas.
void resize() {
std::size_t size = 8;
while (size < used_ * 3) {
size *= 2;
}
std::cout << " (reconstruye: " << used_ << " en uso, " << indices_.size() << " -> " << size << " índices)\n";
std::vector<Entry> old = std::move(entries_);
entries_.clear();
indices_.assign(size, kEmpty);
for (Entry& entry : old) {
if (entry.alive) {
indices_[first_free_slot(entry.key)] = static_cast<int>(entries_.size());
entries_.push_back(std::move(entry));
}
}
}
std::vector<int> indices_;
std::vector<Entry> entries_;
std::size_t used_ = 0;
};
} // namespace
int main() {
CompactDict dict;
const char* names[] = {"lima", "cusco", "puno", "tacna", "ica"};
for (int i = 0; i < 5; i++) {
std::cout << "d[" << names[i] << "] = " << i << "\n";
dict.set(names[i], i);
}
dict.print();
dict.print_in_order();
std::cout << "del d[cusco]\n";
dict.erase("cusco");
dict.print();
dict.print_in_order();
std::cout << "d[puno] = 9 (reemplaza: no cambia de lugar)\n";
dict.set("puno", 9);
dict.print();
std::cout << "d[cusco] = 7 (vuelve, y va al final)\n";
dict.set("cusco", 7);
dict.print();
dict.print_in_order();
return 0;
}// Tablas de la clase, reducidas a lo que hace falta para medir y contar. No son el laboratorio.
#ifndef DEMO_TABLES_H
#define DEMO_TABLES_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
// FNV-1a de 64 bits, con las constantes oficiales.
inline std::uint64_t demo_hash(const std::string& key) {
std::uint64_t hash = 14695981039346656037ULL;
for (char c : key) {
hash ^= static_cast<unsigned char>(c);
hash *= 1099511628211ULL;
}
return hash;
}
// Enteros con linear probing: entradas contiguas en un arreglo. 0 marca una casilla vacía.
class ProbingInts {
public:
explicit ProbingInts(std::size_t slots) : entries_(slots), mask_(slots - 1) {}
void insert(std::uint64_t key, std::uint64_t value) {
std::uint64_t i = key & mask_;
while (entries_[i].key != 0) {
i = (i + 1) & mask_;
}
entries_[i] = {key, value};
}
const std::uint64_t* find(std::uint64_t key) const {
std::uint64_t i = key & mask_;
while (entries_[i].key != 0) {
if (entries_[i].key == key) {
return &entries_[i].value;
}
i = (i + 1) & mask_;
}
return nullptr;
}
// Bytes de la tabla.
std::size_t bytes() const { return entries_.size() * sizeof(Entry); }
private:
struct Entry {
std::uint64_t key = 0;
std::uint64_t value = 0;
};
std::vector<Entry> entries_;
std::uint64_t mask_;
};
// Enteros con encadenamiento: cada entrada es un nodo reservado por separado.
class ChainedInts {
public:
explicit ChainedInts(std::size_t slots) : heads_(slots, nullptr), mask_(slots - 1) {}
ChainedInts(const ChainedInts&) = delete;
ChainedInts& operator=(const ChainedInts&) = delete;
~ChainedInts() {
for (Node* node : heads_) {
while (node != nullptr) {
Node* next = node->next;
delete node;
node = next;
}
}
}
void insert(std::uint64_t key, std::uint64_t value) {
Node*& head = heads_[key & mask_];
head = new Node{key, value, head};
count_++;
}
const std::uint64_t* find(std::uint64_t key) const {
for (const Node* node = heads_[key & mask_]; node != nullptr; node = node->next) {
if (node->key == key) {
return &node->value;
}
}
return nullptr;
}
// Bytes pedidos con new, sin contar lo que agrega el reservador de memoria.
std::size_t bytes() const { return heads_.size() * sizeof(Node*) + count_ * sizeof(Node); }
private:
struct Node {
std::uint64_t key;
std::uint64_t value;
Node* next;
};
std::vector<Node*> heads_;
std::uint64_t mask_;
std::size_t count_ = 0;
};
// Conjunto de cadenas con linear probing, que cuenta comparaciones de claves.
class CountingLinear {
public:
explicit CountingLinear(std::size_t slots) : keys_(slots), used_(slots, 0), mask_(slots - 1) {}
void insert(const std::string& key) {
std::uint64_t i = demo_hash(key) & mask_;
while (used_[i]) {
i = (i + 1) & mask_;
}
keys_[i] = key;
used_[i] = 1;
}
bool contains(const std::string& key) {
std::uint64_t i = demo_hash(key) & mask_;
while (used_[i]) {
comparisons_++;
if (keys_[i] == key) {
return true;
}
i = (i + 1) & mask_;
}
return false;
}
std::uint64_t comparisons() const { return comparisons_; }
void reset() { comparisons_ = 0; }
private:
std::vector<std::string> keys_;
std::vector<char> used_;
std::uint64_t mask_;
std::uint64_t comparisons_ = 0;
};
#endif"""Comportamiento real del dict de CPython. Se corre con python3 dict_demo.py."""
import sys
def show_order():
d = {"lima": 0, "cusco": 1, "puno": 2}
print("creado: ", list(d))
d["puno"] = 9
print("reemplazar puno:", list(d))
del d["cusco"]
d["tacna"] = 3
print("borrar cusco y agregar tacna:", list(d))
d["cusco"] = 7
print("volver a agregar cusco: ", list(d))
print("popitem saca el último:", d.popitem())
def expected_size(log2_size):
"""Lo que predice Objects/dictobject.c para claves de texto: 64 del objeto dict con el
encabezado del recolector, 32 del encabezado de las claves, los índices y las entradas de
16 bytes, 2/3 de las casillas."""
size = 1 << log2_size
index_bytes = 1 if size <= 128 else 2 if size <= 2**15 else 4
return 64 + 32 + size * index_bytes + (size * 2 // 3) * 16
def show_sizes():
"""La predicción no mira a sys.getsizeof: sale de las reglas de dictobject.c. Se empieza con 8
casillas y, cuando se acaban las entradas usables (2/3), el tamaño nuevo es la menor potencia
de 2 que llegue a used * 3. Sin borrados, used es la cantidad de claves que ya hay."""
print("claves sys.getsizeof casillas usables predicción")
log2_size = 3
d = {}
last = sys.getsizeof(d)
for i in range(400):
used = len(d)
if used == (1 << log2_size) * 2 // 3:
log2_size = 3
while (1 << log2_size) < used * 3:
log2_size += 1
d[f"clave{i}"] = i
size = sys.getsizeof(d)
if size != last:
slots = 1 << log2_size
print(f"{i + 1:6} {size:14} {slots:9} {slots * 2 // 3:8} {expected_size(log2_size):11}")
last = size
def show_cleaning():
"""Con pocas claves vivas, la reconstrucción no crece: la tabla se achica."""
d = {f"k{i}": i for i in range(1000)}
before = sys.getsizeof(d)
for i in range(990):
del d[f"k{i}"]
print(f"1000 claves: {before} bytes; al borrar 990: {sys.getsizeof(d)} bytes")
i = 0
while sys.getsizeof(d) == before:
d[f"z{i}"] = i
del d[f"z{i}"]
i += 1
print(f"tras {i} altas y bajas, con {len(d)} claves: {sys.getsizeof(d)} bytes")
def main():
print("Python", sys.version.split()[0], sys.implementation.name)
show_order()
print()
show_cleaning()
print()
show_sizes()
if __name__ == "__main__":
main()// Compara los 8 bytes de una palabra de control a la vez, con operaciones de bits.
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include "mini_swiss.h"
namespace {
void show(std::uint64_t ctrl, unsigned char h2) {
std::printf("buscar h2 = %3u: casillas candidatas:", h2);
std::uint64_t marks = match_h2(ctrl, h2);
if (marks == 0) {
std::printf(" ninguna");
}
while (marks != 0) {
std::printf(" %d", first_marked(marks));
marks &= marks - 1;
}
std::printf("\n");
}
} // namespace
int main() {
// El byte de la casilla 0 es el de menor peso.
const unsigned char bytes[8] = {23, 89, 50, kCtrlEmpty, 89, kCtrlDeleted, 7, kCtrlEmpty};
std::uint64_t ctrl = 0;
for (int slot = 7; slot >= 0; slot--) {
ctrl = (ctrl << 8) | bytes[slot];
}
std::printf("palabra de control: 0x%016" PRIx64 "\n", ctrl);
std::printf("bytes por casilla:");
for (unsigned char byte : bytes) {
std::printf(" %u", byte);
}
std::printf("\n");
show(ctrl, 89);
show(ctrl, 50);
show(ctrl, 100);
std::printf("casillas vacías:");
std::uint64_t marks = match_empty(ctrl);
while (marks != 0) {
std::printf(" %d", first_marked(marks));
marks &= marks - 1;
}
std::printf("\n");
// Un falso positivo: 0x03 justo después de una coincidencia real con 0x02.
std::printf("otra palabra, bytes por casilla: 2 3 128 128 128 128 128 128\n");
show(0x0302ULL | (kLowBits * kCtrlEmpty << 16), 2);
return 0;
}// Linear probing contra encadenamiento, con claves y valores enteros: las mismas claves, las mismas
// casillas y el mismo factor de carga. Mide búsquedas en orden al azar. -O2 y sin sanitizers.
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <random>
#include <vector>
#include "bench.h"
#include "demo_tables.h"
namespace {
constexpr std::size_t kLookups = 1000000;
constexpr int kReps = 5;
std::uint64_t draw(std::mt19937_64& rng) {
std::uint64_t key = rng();
while (key == 0) {
key = rng();
}
return key;
}
template <typename Table>
double lookup_ns(const Table& table, const std::vector<std::uint64_t>& queries) {
const double seconds = median_seconds(kReps, [&] {
std::uint64_t sum = 0;
for (std::uint64_t query : queries) {
const std::uint64_t* value = table.find(query);
sum += value == nullptr ? 1 : *value;
}
keep(sum);
});
return seconds * 1e9 / static_cast<double>(queries.size());
}
void compare(int log_slots, double alpha) {
const std::size_t slots = std::size_t{1} << log_slots;
const std::size_t n = static_cast<std::size_t>(alpha * static_cast<double>(slots));
std::mt19937_64 rng(2026);
std::vector<std::uint64_t> keys(n);
for (std::uint64_t& key : keys) {
key = draw(rng);
}
ProbingInts probing(slots);
ChainedInts chained(slots);
for (std::uint64_t key : keys) {
probing.insert(key, key);
chained.insert(key, key);
}
std::vector<std::uint64_t> hits(kLookups);
std::vector<std::uint64_t> misses(kLookups);
for (std::size_t i = 0; i < kLookups; i++) {
hits[i] = keys[rng() % n];
misses[i] = draw(rng);
}
const double probing_hit = lookup_ns(probing, hits);
const double chained_hit = lookup_ns(chained, hits);
const double probing_miss = lookup_ns(probing, misses);
const double chained_miss = lookup_ns(chained, misses);
std::printf("2^%-3d %5.2f %9.1f %9.1f %9.1f %9.1f %9.1f %9.1f\n", log_slots, alpha, probing_hit,
chained_hit, probing_miss, chained_miss,
static_cast<double>(probing.bytes()) / static_cast<double>(n),
static_cast<double>(chained.bytes()) / static_cast<double>(n));
}
} // namespace
int main() {
std::printf("ns por búsqueda, mediana de %d rondas de %zu. Bytes por clave.\n", kReps, kLookups);
std::printf("%-5s %5s %9s %9s %9s %9s %9s %9s\n", "cas.", "alfa", "lin.hit", "cad.hit", "lin.miss",
"cad.miss", "lin.B", "cad.B");
for (int log_slots : {12, 16, 20, 22}) {
for (double alpha : {0.50, 0.75, 0.90}) {
compare(log_slots, alpha);
}
}
return 0;
}// Una Swiss Table de juguete para la clase, escrita para contar y no para ser rápida. Sigue el
// diseño de los mapas de Go 1.24: grupos de 8 casillas, una palabra de control de 64 bits por
// grupo, y los 7 bits bajos del hash en el byte de cada casilla llena.
#ifndef MINI_SWISS_H
#define MINI_SWISS_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "demo_tables.h"
constexpr std::uint64_t kLowBits = 0x0101010101010101ULL;
constexpr std::uint64_t kHighBits = 0x8080808080808080ULL;
constexpr unsigned char kCtrlEmpty = 0x80; // 1000 0000
constexpr unsigned char kCtrlDeleted = 0xFE; // 1111 1110
// Marca con el bit alto los bytes de la palabra que valen h2, sin revisar uno por uno.
// Puede dar falsos positivos raros, justo después de una coincidencia real, y nunca se pierde
// una: por eso siempre se compara la clave completa después.
inline std::uint64_t match_h2(std::uint64_t ctrl, unsigned char h2) {
const std::uint64_t v = ctrl ^ (kLowBits * h2);
return (v - kLowBits) & ~v & kHighBits;
}
// Marca los bytes vacíos: tienen el bit alto en 1 y el bit 1 en 0.
inline std::uint64_t match_empty(std::uint64_t ctrl) {
return ctrl & ~(ctrl << 6) & kHighBits;
}
// Número de casilla (0 a 7) del primer byte marcado.
inline int first_marked(std::uint64_t marks) {
return __builtin_ctzll(marks) / 8;
}
class MiniSwiss {
public:
static constexpr std::size_t kGroupSlots = 8;
// El total de casillas es group_count * 8, con group_count potencia de 2.
explicit MiniSwiss(std::size_t group_count)
: ctrl_(group_count, kLowBits * kCtrlEmpty), keys_(group_count * kGroupSlots), mask_(group_count - 1) {}
void insert(const std::string& key) {
const std::uint64_t hash = demo_hash(key);
const unsigned char h2 = hash & 0x7F;
std::uint64_t group = (hash >> 7) & mask_;
std::uint64_t step = 0;
while (match_empty(ctrl_[group]) == 0) {
step++;
group = (group + step) & mask_; // quadratic probing entre grupos
}
const int slot = first_marked(match_empty(ctrl_[group]));
set_byte(group, slot, h2);
keys_[group * kGroupSlots + static_cast<std::size_t>(slot)] = key;
}
bool contains(const std::string& key) {
const std::uint64_t hash = demo_hash(key);
const unsigned char h2 = hash & 0x7F;
std::uint64_t group = (hash >> 7) & mask_;
std::uint64_t step = 0;
while (true) {
groups_read_++;
std::uint64_t marks = match_h2(ctrl_[group], h2);
while (marks != 0) {
const int slot = first_marked(marks);
comparisons_++;
if (keys_[group * kGroupSlots + static_cast<std::size_t>(slot)] == key) {
return true;
}
marks &= marks - 1;
}
if (match_empty(ctrl_[group]) != 0) {
return false; // un grupo con una vacía termina la búsqueda
}
step++;
group = (group + step) & mask_;
}
}
std::uint64_t comparisons() const { return comparisons_; }
std::uint64_t groups_read() const { return groups_read_; }
void reset() {
comparisons_ = 0;
groups_read_ = 0;
}
private:
void set_byte(std::uint64_t group, int slot, unsigned char value) {
const int shift = slot * 8;
ctrl_[group] = (ctrl_[group] & ~(std::uint64_t{0xFF} << shift)) | (std::uint64_t{value} << shift);
}
std::vector<std::uint64_t> ctrl_;
std::vector<std::string> keys_;
std::uint64_t mask_;
std::uint64_t comparisons_ = 0;
std::uint64_t groups_read_ = 0;
};
#endif// La secuencia de probing del dict de CPython: probe_order <casillas> <hash> [cuántas]
// Usa la recurrencia de Objects/dictobject.c: perturb >>= 5; i = (i*5 + perturb + 1) & mask.
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iostream>
int main(int argc, char** argv) {
if (argc < 3) {
std::cerr << "uso: probe_order <casillas> <hash> [cuántas]\n";
return 2;
}
const std::uint64_t slots = std::strtoull(argv[1], nullptr, 10);
const std::uint64_t hash = std::strtoull(argv[2], nullptr, 10);
const std::size_t count = argc > 3 ? std::strtoul(argv[3], nullptr, 10) : slots;
const std::uint64_t mask = slots - 1;
std::uint64_t perturb = hash;
std::uint64_t i = hash & mask;
std::cout << "casillas " << slots << ", hash " << hash << ":";
for (std::size_t step = 0; step < count; step++) {
std::cout << " " << i;
perturb >>= 5;
i = (i * 5 + perturb + 1) & mask;
}
std::cout << "\n";
return 0;
}// Una tabla de enteros con h(k) = k mod m y linear probing, que muestra cada paso.
// probe_trace <m> <órdenes>. Órdenes: i10 inserta, f31 busca, e17 borra vaciando la casilla
// (está mal, a propósito), t17 borra con tombstone.
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
namespace {
constexpr int kEmpty = -1;
constexpr int kTombstone = -2;
class TraceTable {
public:
explicit TraceTable(std::size_t slots) : slots_(slots, kEmpty) {}
void insert(int key) {
std::size_t i = home(key);
std::cout << "insertar " << key << " (inicio " << i << "):";
// Basta para esta demostración: no revisa si la clave ya está.
while (slots_[i] >= 0) {
std::cout << " " << i;
i = (i + 1) % slots_.size();
}
std::cout << " " << i << " <- aquí\n";
slots_[i] = key;
}
// Devuelve la casilla donde está la clave, o -1.
int find(int key, bool show = true) const {
std::size_t i = home(key);
std::size_t probes = 0;
std::string path;
int where = -1;
while (probes < slots_.size()) {
probes++;
path += " " + std::to_string(i);
if (slots_[i] == key) {
where = static_cast<int>(i);
break;
}
if (slots_[i] == kEmpty) {
break;
}
i = (i + 1) % slots_.size();
}
if (show) {
std::cout << "buscar " << key << " (inicio " << home(key) << "):" << path << " -> "
<< (where >= 0 ? "está" : "no está") << ", " << probes << " casillas\n";
}
return where;
}
void erase_wrong(int key) {
const int i = find(key, false);
std::cout << "borrar " << key << " vaciando la casilla " << i << "\n";
if (i >= 0) {
slots_[static_cast<std::size_t>(i)] = kEmpty;
}
}
void erase_tombstone(int key) {
const int i = find(key, false);
std::cout << "borrar " << key << " con tombstone en la casilla " << i << "\n";
if (i >= 0) {
slots_[static_cast<std::size_t>(i)] = kTombstone;
}
}
void print() const {
std::cout << "tabla:";
for (std::size_t i = 0; i < slots_.size(); i++) {
std::cout << " [" << i << ":";
if (slots_[i] == kEmpty) {
std::cout << "_";
} else if (slots_[i] == kTombstone) {
std::cout << "X";
} else {
std::cout << slots_[i];
}
std::cout << "]";
}
std::cout << "\n";
}
private:
std::size_t home(int key) const { return static_cast<std::size_t>(key) % slots_.size(); }
std::vector<int> slots_;
};
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "uso: probe_trace <m> <órdenes: i10 f31 e17 t17>\n";
return 2;
}
TraceTable table(std::strtoul(argv[1], nullptr, 10));
for (int a = 2; a < argc; a++) {
const char order = argv[a][0];
const int key = std::atoi(argv[a] + 1);
if (order == 'i') {
table.insert(key);
} else if (order == 'f') {
table.find(key);
} else if (order == 'e') {
table.erase_wrong(key);
} else if (order == 't') {
table.erase_tombstone(key);
} else if (order == 'p') {
table.print();
}
}
table.print();
return 0;
}// Cuenta casillas revisadas por búsqueda, sin medir tiempo: linear probing, double hashing (sin
// agrupamiento primario) y encadenamiento, contra las fórmulas clásicas.
// Las claves son números al azar de 64 bits, con semilla fija, así que k mod m reparte bien.
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <random>
#include <vector>
namespace {
constexpr std::size_t kSlots = 1u << 20;
constexpr std::size_t kMask = kSlots - 1;
constexpr std::uint64_t kEmpty = 0;
struct Counts {
double hit;
double miss;
};
// Un número al azar distinto de kEmpty. No se fuerza ningún bit: eso dejaría casillas sin uso.
std::uint64_t draw(std::mt19937_64& rng) {
std::uint64_t key = rng();
while (key == kEmpty) {
key = rng();
}
return key;
}
// El paso es impar, para que el probing visite todas las casillas de una tabla de 2^k.
std::uint64_t step_for(std::uint64_t key, bool linear) {
return linear ? 1 : ((key >> 32) | 1);
}
Counts open_addressing(const std::vector<std::uint64_t>& keys, std::size_t n,
const std::vector<std::uint64_t>& absent, bool linear) {
std::vector<std::uint64_t> table(kSlots, kEmpty);
for (std::size_t k = 0; k < n; k++) {
const std::uint64_t step = step_for(keys[k], linear);
std::uint64_t i = keys[k] & kMask;
while (table[i] != kEmpty) {
i = (i + step) & kMask;
}
table[i] = keys[k];
}
std::uint64_t probes = 0;
for (std::size_t k = 0; k < n; k++) {
const std::uint64_t step = step_for(keys[k], linear);
std::uint64_t i = keys[k] & kMask;
probes++;
while (table[i] != keys[k]) {
i = (i + step) & kMask;
probes++;
}
}
const double hit = static_cast<double>(probes) / static_cast<double>(n);
probes = 0;
for (std::uint64_t key : absent) {
const std::uint64_t step = step_for(key, linear);
std::uint64_t i = key & kMask;
probes++;
while (table[i] != kEmpty) {
i = (i + step) & kMask;
probes++;
}
}
return {hit, static_cast<double>(probes) / static_cast<double>(absent.size())};
}
// Con encadenamiento se cuentan nodos visitados: buscar sin éxito recorre toda la lista.
Counts chaining(const std::vector<std::uint64_t>& keys, std::size_t n,
const std::vector<std::uint64_t>& absent) {
std::vector<std::uint32_t> length(kSlots, 0);
std::uint64_t nodes = 0;
for (std::size_t k = 0; k < n; k++) {
length[keys[k] & kMask]++;
// Al insertar al final, la clave k-ésima se encontrará recorriendo esta cantidad de nodos.
nodes += length[keys[k] & kMask];
}
const double hit = static_cast<double>(nodes) / static_cast<double>(n);
nodes = 0;
for (std::uint64_t key : absent) {
nodes += length[key & kMask];
}
return {hit, static_cast<double>(nodes) / static_cast<double>(absent.size())};
}
} // namespace
int main() {
std::mt19937_64 rng(2026);
std::vector<std::uint64_t> keys(kSlots);
for (std::uint64_t& key : keys) {
key = draw(rng);
}
std::vector<std::uint64_t> absent(200000);
for (std::uint64_t& key : absent) {
key = draw(rng);
}
std::printf("Con éxito: casillas o nodos revisados por búsqueda, promedio\n");
std::printf("%5s %8s %8s %8s %8s\n", "alfa", "linear", "fórmula", "double", "cadenas");
const double alphas[] = {0.25, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99};
std::vector<Counts> line;
std::vector<Counts> twice;
std::vector<Counts> chain;
for (double alpha : alphas) {
const std::size_t n = static_cast<std::size_t>(alpha * static_cast<double>(kSlots));
line.push_back(open_addressing(keys, n, absent, true));
twice.push_back(open_addressing(keys, n, absent, false));
chain.push_back(chaining(keys, n, absent));
}
for (std::size_t a = 0; a < line.size(); a++) {
const double alpha = alphas[a];
const double formula = 0.5 * (1.0 + 1.0 / (1.0 - alpha));
std::printf("%5.2f %8.2f %8.2f %8.2f %8.2f\n", alpha, line[a].hit, formula, twice[a].hit, chain[a].hit);
}
std::printf("\nSin éxito: casillas o nodos revisados por búsqueda, promedio\n");
std::printf("%5s %8s %8s %8s %8s\n", "alfa", "linear", "fórmula", "double", "cadenas");
for (std::size_t a = 0; a < line.size(); a++) {
const double alpha = alphas[a];
const double formula = 0.5 * (1.0 + 1.0 / ((1.0 - alpha) * (1.0 - alpha)));
std::printf("%5.2f %8.2f %8.2f %8.2f %8.2f\n", alpha, line[a].miss, formula, twice[a].miss, chain[a].miss);
}
return 0;
}// Cuenta comparaciones de claves completas: linear probing contra una Swiss Table de juguete, en la
// misma cantidad de casillas y con las mismas palabras. swiss_counts <palabras.txt>
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include "mini_swiss.h"
namespace {
constexpr std::size_t kSlots = std::size_t{1} << 20;
void compare(const std::vector<std::string>& words, double alpha) {
const std::size_t n = static_cast<std::size_t>(alpha * static_cast<double>(kSlots));
CountingLinear linear(kSlots);
MiniSwiss swiss(kSlots / MiniSwiss::kGroupSlots);
for (std::size_t i = 0; i < n; i++) {
linear.insert(words[i]);
swiss.insert(words[i]);
}
const double total = static_cast<double>(n);
for (std::size_t i = 0; i < n; i++) {
linear.contains(words[i]);
swiss.contains(words[i]);
}
const double linear_hit = static_cast<double>(linear.comparisons()) / total;
const double swiss_hit = static_cast<double>(swiss.comparisons()) / total;
const double groups_hit = static_cast<double>(swiss.groups_read()) / total;
linear.reset();
swiss.reset();
for (std::size_t i = 0; i < n; i++) {
linear.contains(words[i] + "#"); // ninguna palabra lleva #
swiss.contains(words[i] + "#");
}
std::printf("%5.2f %12.2f %12.2f %8.2f %12.2f %12.4f %8.2f\n", alpha, linear_hit, swiss_hit, groups_hit,
static_cast<double>(linear.comparisons()) / total, static_cast<double>(swiss.comparisons()) / total,
static_cast<double>(swiss.groups_read()) / total);
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "uso: swiss_counts <palabras.txt>\n";
return 2;
}
std::ifstream in(argv[1]);
if (!in) {
std::cerr << "no se pudo abrir " << argv[1] << "\n";
return 2;
}
std::vector<std::string> words;
std::string word;
while (std::getline(in, word)) {
words.push_back(word);
}
std::mt19937 rng(2026);
std::shuffle(words.begin(), words.end(), rng);
std::printf("%zu casillas. Promedios por búsqueda.\n", kSlots);
std::printf("%5s %25s %8s %25s %8s\n", "", "--- con éxito: compara ---", "", "--- sin éxito: compara ---", "");
std::printf("%5s %12s %12s %8s %12s %12s %8s\n", "alfa", "linear", "swiss", "grupos", "linear", "swiss", "grupos");
for (double alpha : {0.25, 0.50, 0.70, 0.80, 0.875}) {
compare(words, alpha);
}
return 0;
}// Una tabla con muy pocas claves vivas y muchas inserciones y borrados: cuenta cuántas casillas
// revisa una búsqueda sin éxito, con tres políticas. No mide tiempo: todo es conteo.
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <random>
#include <vector>
namespace {
constexpr std::size_t kSlots = 1024;
constexpr std::size_t kMask = kSlots - 1;
constexpr std::size_t kLive = 100;
constexpr std::uint64_t kEmpty = 0;
constexpr std::uint64_t kTombstone = 1;
// Un número al azar que no se confunde con las dos marcas.
std::uint64_t draw(std::mt19937_64& rng) {
std::uint64_t key = rng();
while (key <= kTombstone) {
key = rng();
}
return key;
}
enum class Policy { never_clean, reuse, reuse_and_rebuild };
class Table {
public:
explicit Table(Policy policy) : slots_(kSlots, kEmpty), policy_(policy) {}
void insert(std::uint64_t key) {
std::uint64_t i = key & kMask;
std::size_t first_tombstone = kSlots;
std::size_t walked = 0;
while (slots_[i] != kEmpty && walked < kSlots) {
if (slots_[i] == kTombstone && first_tombstone == kSlots) {
first_tombstone = i;
}
i = (i + 1) & kMask;
walked++;
}
if (policy_ != Policy::never_clean && first_tombstone != kSlots) {
slots_[first_tombstone] = key;
tombstones_--;
} else {
slots_[i] = key;
}
size_++;
const double used = static_cast<double>(size_ + tombstones_);
if (policy_ == Policy::reuse_and_rebuild && used > 0.7 * kSlots) {
rebuild();
}
}
void erase(std::uint64_t key) {
std::uint64_t i = key & kMask;
while (slots_[i] != key) {
i = (i + 1) & kMask;
}
slots_[i] = kTombstone;
size_--;
tombstones_++;
}
// Casillas que revisa buscar una clave que no está.
std::size_t miss_probes(std::uint64_t key) const {
std::uint64_t i = key & kMask;
std::size_t probes = 1;
while (slots_[i] != kEmpty && probes < kSlots) {
i = (i + 1) & kMask;
probes++;
}
return probes;
}
std::size_t tombstones() const { return tombstones_; }
std::size_t rebuilds() const { return rebuilds_; }
bool full() const { return size_ + tombstones_ >= kSlots - 1; }
private:
void rebuild() {
std::vector<std::uint64_t> fresh(kSlots, kEmpty);
for (std::uint64_t key : slots_) {
if (key > kTombstone) {
std::uint64_t i = key & kMask;
while (fresh[i] != kEmpty) {
i = (i + 1) & kMask;
}
fresh[i] = key;
}
}
slots_.swap(fresh);
tombstones_ = 0;
rebuilds_++;
}
std::vector<std::uint64_t> slots_;
Policy policy_;
std::size_t size_ = 0;
std::size_t tombstones_ = 0;
std::size_t rebuilds_ = 0;
};
void run(const char* name, Policy policy) {
std::mt19937_64 rng(2026);
Table table(policy);
std::vector<std::uint64_t> live;
for (std::size_t k = 0; k < kLive; k++) {
live.push_back(draw(rng));
table.insert(live.back());
}
std::printf("%s\n%10s %10s %12s %12s\n", name, "cambios", "tombstones", "casillas", "rebuilds");
std::size_t changes = 0;
for (std::size_t target : {std::size_t{0}, std::size_t{100}, std::size_t{500}, std::size_t{1000},
std::size_t{5000}, std::size_t{50000}}) {
while (changes < target) {
if (policy == Policy::never_clean && table.full()) {
break;
}
const std::size_t victim = rng() % live.size();
table.erase(live[victim]);
live[victim] = draw(rng);
table.insert(live[victim]);
changes++;
}
std::uint64_t total = 0;
std::mt19937_64 misses(7);
for (int i = 0; i < 2000; i++) {
total += table.miss_probes(draw(misses));
}
std::printf("%10zu %10zu %12.1f %12zu\n", changes, table.tombstones(),
static_cast<double>(total) / 2000.0, table.rebuilds());
if (changes < target) {
std::printf("(solo queda una casilla vacía: sin reusar tombstones ya no se puede insertar)\n");
break;
}
}
std::printf("\n");
}
} // namespace
int main() {
std::printf("Tabla de %zu casillas con %zu claves vivas (alfa real = %.2f).\n", kSlots, kLive,
static_cast<double>(kLive) / kSlots);
std::printf("Un cambio borra una clave e inserta otra. \"casillas\" es el promedio que revisa\n");
std::printf("una búsqueda sin éxito, con 2000 búsquedas.\n\n");
run("Nunca se limpia y nunca se reúsa:", Policy::never_clean);
run("Se reúsan tombstones al insertar:", Policy::reuse);
run("Se reúsan y se reconstruye al pasar 0.7:", Policy::reuse_and_rebuild);
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// ERROR A PROPÓSITO: la inserción usa el primer tombstone sin terminar de buscar la clave.
// Compilar: g++ -Wall -Wextra -std=c++20 errors/duplicate_at_tombstone.cpp -o build/duplicate_at_tombstone
#include <array>
#include <cstddef>
#include <iostream>
namespace {
constexpr int kEmpty = -1;
constexpr int kTombstone = -2;
std::size_t home(int key) { return static_cast<std::size_t>(key) % 7; }
void insert(std::array<int, 7>& table, int key) {
std::size_t i = home(key);
// Aquí está el error: la clave podría estar más adelante, pasando el tombstone.
while (table[i] >= 0 && table[i] != key) {
i = (i + 1) % table.size();
}
table[i] = key;
}
void erase(std::array<int, 7>& table, int key) {
std::size_t i = home(key);
while (table[i] != kEmpty) {
if (table[i] == key) {
table[i] = kTombstone;
return;
}
i = (i + 1) % table.size();
}
}
} // namespace
int main() {
std::array<int, 7> table;
table.fill(kEmpty);
insert(table, 8);
insert(table, 15);
erase(table, 8);
insert(table, 15); // ya estaba: no debería cambiar nada
int copies = 0;
for (int value : table) {
copies += value == 15 ? 1 : 0;
}
std::cout << "copias de 15 en la tabla: " << copies << "\n";
return 0;
}// ERROR A PROPÓSITO: la búsqueda termina en el primer tombstone, en vez de en la primera vacía.
// Compilar: g++ -Wall -Wextra -std=c++20 errors/stop_at_tombstone.cpp -o build/stop_at_tombstone
#include <array>
#include <cstddef>
#include <iostream>
namespace {
constexpr int kEmpty = -1;
constexpr int kTombstone = -2;
std::size_t home(int key) { return static_cast<std::size_t>(key) % 7; }
void insert(std::array<int, 7>& table, int key) {
std::size_t i = home(key);
while (table[i] >= 0) {
i = (i + 1) % table.size();
}
table[i] = key;
}
bool contains(const std::array<int, 7>& table, int key) {
std::size_t i = home(key);
// Aquí está el error: un tombstone no dice nada sobre lo que viene después.
while (table[i] != kEmpty && table[i] != kTombstone) {
if (table[i] == key) {
return true;
}
i = (i + 1) % table.size();
}
return false;
}
void erase(std::array<int, 7>& table, int key) {
std::size_t i = home(key);
while (table[i] != kEmpty) {
if (table[i] == key) {
table[i] = kTombstone;
return;
}
i = (i + 1) % table.size();
}
}
} // namespace
int main() {
std::array<int, 7> table;
table.fill(kEmpty);
insert(table, 8);
insert(table, 15);
insert(table, 22);
erase(table, 15);
std::cout << "contains(8) = " << contains(table, 8) << "\n";
std::cout << "contains(22) = " << contains(table, 22) << " <- la clave sigue en la tabla\n";
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Pruebas de lo que muestra la clase: la comparación por grupos y la tabla de juguete.
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <random>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "mini_swiss.h"
namespace {
// La versión lenta, para comparar: byte por byte.
bool byte_matches(std::uint64_t ctrl, int slot, unsigned char h2) {
return static_cast<unsigned char>(ctrl >> (slot * 8)) == h2;
}
void test_fnv_vectors() {
assert(demo_hash("") == 0xcbf29ce484222325ULL);
assert(demo_hash("a") == 0xaf63dc4c8601ec8cULL);
assert(demo_hash("foobar") == 0x85944171f73967e8ULL);
}
// match_h2 nunca pierde una coincidencia, y solo marca de más justo después de una real.
void test_match_h2_against_bytes() {
std::mt19937_64 rng(2026);
for (int round = 0; round < 200000; round++) {
const std::uint64_t ctrl = rng() & 0x7F7F7F7F7F7F7F7FULL; // bytes de casillas llenas
const unsigned char h2 = rng() & 0x7F;
const std::uint64_t marks = match_h2(ctrl, h2);
bool earlier_real = false;
for (int slot = 0; slot < 8; slot++) {
const bool marked = (marks >> (slot * 8 + 7)) & 1;
const bool real = byte_matches(ctrl, slot, h2);
if (real) {
assert(marked);
earlier_real = true;
} else if (marked) {
assert(earlier_real);
}
}
}
}
void test_match_h2_ignores_empty_and_deleted() {
const std::uint64_t ctrl = kLowBits * kCtrlEmpty;
const std::uint64_t mixed = (ctrl & ~std::uint64_t{0xFF}) | kCtrlDeleted;
for (int h2 = 0; h2 < 128; h2++) {
assert(match_h2(ctrl, static_cast<unsigned char>(h2)) == 0);
assert(match_h2(mixed, static_cast<unsigned char>(h2)) == 0);
}
}
void test_match_empty() {
assert(match_empty(kLowBits * kCtrlEmpty) == kHighBits);
assert(match_empty(kLowBits * kCtrlDeleted) == 0);
assert(match_empty(kLowBits * 0x7F) == 0);
assert(match_empty(kLowBits * 0x00) == 0);
}
// La norma pide que insertar no invalide punteros a los elementos, aunque cambien las casillas.
void test_unordered_map_keeps_pointers() {
std::unordered_map<std::string, int> map;
map["fija"] = 7;
const int* address = &map["fija"];
const std::size_t buckets = map.bucket_count();
for (int i = 0; i < 10000; i++) {
map[std::to_string(i)] = i;
}
assert(map.bucket_count() > buckets);
assert(address == &map["fija"]);
assert(*address == 7);
}
void test_mini_swiss_against_std() {
std::mt19937 rng(2026);
MiniSwiss swiss(256);
std::unordered_set<std::string> model;
for (int i = 0; i < 1500; i++) { // 1500 de 2048 casillas
const std::string key = "k" + std::to_string(rng());
if (model.insert(key).second) {
swiss.insert(key);
}
}
for (const std::string& key : model) {
assert(swiss.contains(key));
}
for (int i = 0; i < 5000; i++) {
const std::string key = "z" + std::to_string(i);
assert(!swiss.contains(key));
}
}
} // namespace
int main() {
test_fnv_vectors();
test_match_h2_against_bytes();
test_match_h2_ignores_empty_and_deleted();
test_match_empty();
test_unordered_map_keeps_pointers();
test_mini_swiss_against_std();
std::cout << "ok\n";
return 0;
}"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido la hace
fallar aunque el resultado parezca correcto. Ninguna prueba mide tiempo: todas cuentan.
"""
import os
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = pathlib.Path(os.environ.get("DATA", "../../data"))
if not DATA.is_absolute():
DATA = (HERE / DATA).resolve()
FIXTURES = HERE / "lab" / "tests" / "fixtures"
BUILD = HERE / "build" / LAB_DIR
TABLE = [
"hash_vectors", "new_is_empty", "set_and_get", "overwrite", "contains", "remove_present",
"remove_absent", "unusual_keys", "growth", "copy_is_independent", "self_assignment",
"move_leaves_empty_and_usable", "random_small", "random_large", "new_table_has_8_slots",
"search_passes_tombstones", "set_does_not_duplicate_past_tombstone", "set_reuses_tombstone",
"lowering_load_factor_rebuilds", "high_load_factor_still_ends",
"probes_count_the_last_slot", "churn_keeps_probes_low", "rebuild_clears_tombstones",
"remove_releases_value", "moved_from_is_usable", "string_values", "my_churn_test",
]
WEEK7 = ["week7_table_passes_suite", "fnv1a_of_week7"]
SPELL = [
"helper_splits_letters", "helper_extracts_words", "candidates_are_the_three_edits",
"candidates_are_valid_utf8", "suggest_with_sorted", "suggest_with_std", "suggest_with_probing",
"suggest_with_week7", "suggestions_are_sorted_and_unique", "report_with_probing",
"report_with_week7", "report_with_std", "report_with_sorted", "suggests_each_word_once",
]
def compiler_lines(output):
"""Quita los avisos de make, como el de relojes desfasados en una carpeta compartida."""
return "\n".join(line for line in output.splitlines() if not line.startswith("make:"))
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"(probing_hash_map|hash_map|spell_checker|dictionaries)\.h:\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_map_case(testcase, name):
done = subprocess.run(
[str(BUILD / "map_tests"), name], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
testcase.assertEqual(done.returncode, 0, short_report(done.stdout))
def run_spell_case(testcase, name):
done = subprocess.run(
[str(BUILD / "spell_tests"), name, str(FIXTURES)], cwd=HERE, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
testcase.assertEqual(done.returncode, 0, short_report(done.stdout))
@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", "spell_tests", "spell", "bench_spell"):
(BUILD / target).unlink(missing_ok=True)
code, output = build(target)
self.assertEqual(code, 0, output)
self.assertNotIn("warning:", compiler_lines(output))
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTable(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 TestSpell(unittest.TestCase):
@classmethod
def setUpClass(cls):
for target in ("map_tests", "spell_tests", "spell"):
code, output = build(target)
if code != 0:
raise AssertionError(output)
def test_real_files(self):
"""El corrector completo, con los archivos reales: solo cuenta palabras."""
done = subprocess.run(
[str(BUILD / "spell"), str(DATA / "palabras.txt"), str(DATA / "tradiciones.txt"), "0"],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
self.assertIn("diccionario: 1045757 palabras", done.stdout)
self.assertIn("texto: 48004 palabras", done.stdout)
self.assertIn("desconocidas: 1378 apariciones de 708 palabras distintas", done.stdout)
for name in TABLE:
def table_case(self, name=name):
run_map_case(self, name)
setattr(TestTable, f"test_{name}", table_case)
for name in WEEK7:
def week7_case(self, name=name):
run_map_case(self, name)
setattr(TestSpell, f"test_{name}", week7_case)
for name in SPELL:
def spell_case(self, name=name):
run_spell_case(self, name)
setattr(TestSpell, f"test_{name}", spell_case)
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas, comprueba que el
código citado existe de verdad en esta carpeta y revisa las afirmaciones de la semana."""
import os
import pathlib
import re
import subprocess
import sys
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-09.md"
SLIDES = ROOT / "slides" / "week-09.tex"
DATA = os.environ.get("DATA", "../../data")
# Antes de un bloque cuya salida cambia con el sistema. Se corre, pero no se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de una medición larga, copiada de una corrida. No se corre aquí: solo se comprueba que el
# programa exista. Sus conclusiones se revisaron con tres corridas, a mano.
MEASUREMENT = "<!-- medición -->"
def normalize(text):
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
# Los apuntes muestran la ruta por defecto; aquí manda la variable DATA.
command = command.replace("../../data", DATA)
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180,
)
return normalize(done.stdout)
def compiler_lines(output):
"""Quita los avisos de make, como el de relojes desfasados en una carpeta compartida."""
return "\n".join(line for line in output.splitlines() if not line.startswith("make:"))
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def split_commands(lines):
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.glob("*.py"))
sources += list((HERE / "errors").glob("*.cpp")) + list((HERE / "lab").glob("*.h"))
sources += 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 NotesAreTrue(unittest.TestCase):
def test_terminal_blocks_in_notes(self):
checked = 0
measured = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ "):
continue
for command, expected in split_commands(lines):
# Estas salidas vienen de la solución, que no se reparte.
if "build/solution/" in command and not (HERE / "solution").is_dir():
continue
if mark == MEASUREMENT:
measured += 1
program = command.split()[0]
if program.startswith("./"):
self.assertTrue((HERE / program).exists(), program)
else:
target = command.split()[1]
makefile = (HERE / "Makefile").read_text(encoding="utf-8")
self.assertIn(f"\n{target}:", makefile)
continue
actual = clean_lines(run_shell(command))
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 8)
self.assertGreater(measured, 1)
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for language in ("cpp", "python"):
for lines, _ in note_blocks(language):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
def test_length_of_notes(self):
count = len(NOTES.read_text(encoding="utf-8").splitlines())
self.assertTrue(1100 <= count <= 1400, count)
# Los programas de errors/ que usan las diapositivas los compila la prueba de los apuntes, que corre antes.
class SlidesAreTrue(unittest.TestCase):
def test_program_output_in_slides(self):
for block in slide_blocks("terminal|salida"):
for command, expected in split_commands(block):
actual = clean_lines(run_shell(command))
position = 0
for line in expected:
if line.strip() and line.strip() != "...":
with self.subTest(command=command, line=line):
self.assertIn(line, actual[position:])
position = actual.index(line, position) + 1
def test_measurements_in_slides_come_from_notes(self):
notes = NOTES.read_text(encoding="utf-8").splitlines()
blocks = slide_blocks("medicion")
self.assertGreater(len(blocks), 1)
for block in blocks:
for line in block:
with self.subTest(line=line):
self.assertIn(line, notes)
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)
class WeekClaims(unittest.TestCase):
def test_review_question_1_and_2(self):
"""Las preguntas de repaso 1 y 2 de la guía, resueltas por el programa."""
out = run_shell("./build/probe_trace 7 i10 i17 i24 i3 f31")
self.assertIn("tabla: [0:_] [1:_] [2:_] [3:10] [4:17] [5:24] [6:3]", out)
self.assertIn("buscar 31 (inicio 3): 3 4 5 6 0 -> no está, 5 casillas", out)
out = run_shell("./build/probe_trace 7 i10 i17 i24 i3 e17 f24 f3")
self.assertIn("buscar 24 (inicio 3): 3 4 -> no está", out)
self.assertIn("buscar 3 (inicio 3): 3 4 -> no está", out)
out = run_shell("./build/probe_trace 7 i10 i17 i24 i3 t17 f24 f3")
self.assertIn("buscar 24 (inicio 3): 3 4 5 -> está", out)
self.assertIn("buscar 3 (inicio 3): 3 4 5 6 -> está", out)
def test_demos(self):
self.assertEqual(run_shell("./build/test_demos").strip(), "ok")
def test_cpython_probe_order_from_its_comment(self):
# dictobject.c: 0 -> 1 -> 6 -> 7 -> 4 -> 5 -> 2 -> 3 -> 0
self.assertIn(": 0 1 6 7 4 5 2 3 0", run_shell("./build/probe_order 8 0 9"))
def test_python_dict(self):
out = subprocess.run(
[sys.executable, "dict_demo.py"], cwd=HERE, text=True, stdout=subprocess.PIPE, check=True,
).stdout
self.assertIn("reemplazar puno: ['lima', 'cusco', 'puno']", out)
self.assertIn("volver a agregar cusco: ['lima', 'puno', 'tacna', 'cusco']", out)
rows = [line.split() for line in out.splitlines() if re.match(r"\s+\d+\s+\d+\s+\d+", line)]
self.assertGreater(len(rows), 5)
for row in rows:
self.assertEqual(row[1], row[4], row)
def test_error_programs(self):
run_shell("mkdir -p build")
out = run_shell("g++ -Wall -Wextra -std=c++20 errors/stop_at_tombstone.cpp -o build/stop_at_tombstone"
" && ./build/stop_at_tombstone")
self.assertIn("contains(22) = 0", out)
out = run_shell("g++ -Wall -Wextra -std=c++20 errors/duplicate_at_tombstone.cpp"
" -o build/duplicate_at_tombstone && ./build/duplicate_at_tombstone")
self.assertIn("copias de 15 en la tabla: 2", out)
def test_starter_compiles_and_fails(self):
"""El punto de partida compila sin advertencias y sus pruebas fallan."""
done = subprocess.run(
["make", "--no-print-directory", "LAB_DIR=lab", "build/lab/map_tests", "build/lab/spell_tests"],
cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
self.assertNotIn("warning:", compiler_lines(done.stdout))
done = subprocess.run([str(HERE / "build/lab/map_tests"), "set_and_get"], cwd=HERE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.assertNotEqual(done.returncode, 0)
done = subprocess.run([str(HERE / "build/lab/spell_tests"), "candidates_are_the_three_edits",
str(HERE / "lab/tests/fixtures")], cwd=HERE,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.assertNotEqual(done.returncode, 0)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// Paso 2 del laboratorio: su propia prueba de insertar y borrar muchas veces.
#ifndef CHURN_TEST_H
#define CHURN_TEST_H
#include <cassert>
#include <string>
#include "not_implemented.h"
#include "probing_hash_map.h"
// Escriban una prueba que:
// - mantenga pocas claves vivas (por ejemplo 100) e inserte y borre decenas de miles de veces;
// - cada cierto tiempo compruebe check_invariants();
// - al final compruebe con probes_for que una búsqueda sin éxito revisa pocas casillas.
// No midan tiempo: cuenten casillas. El tiempo cambia con la máquina y la cuenta no.
inline void my_churn_test() {
not_implemented("my_churn_test");
}
#endif// Las cuatro formas de guardar el diccionario. Todas ofrecen lo mismo:
// construirse con una lista de palabras y responder contains(word).
// SortedDict se les da hecho. Completen TableDict y StdDict.
#ifndef DICTIONARIES_H
#define DICTIONARIES_H
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
#include "hash_map.h"
#include "not_implemented.h"
#include "probing_hash_map.h"
// Arreglo ordenado con búsqueda binaria.
class SortedDict {
public:
explicit SortedDict(const std::vector<std::string>& words) : words_(words) {
std::sort(words_.begin(), words_.end());
}
bool contains(const std::string& word) const {
return std::binary_search(words_.begin(), words_.end(), word);
}
private:
std::vector<std::string> words_;
};
// Sirve para las dos tablas del curso: TableDict<HashMap> y TableDict<ProbingHashMap>. Solo
// importan las claves, así que usen Table<char> y guarden cualquier valor.
template <template <typename> class Table>
class TableDict {
public:
// Fijen el factor de carga máximo ANTES de insertar.
TableDict(const std::vector<std::string>& words, double max_load) {
(void)words;
(void)max_load;
not_implemented("TableDict");
}
bool contains(const std::string& word) const {
(void)word;
not_implemented("TableDict::contains");
}
// Factor de carga real: size() / capacity(). Se les da hecho.
double load_factor() const {
return static_cast<double>(table_.size()) / static_cast<double>(table_.capacity());
}
private:
Table<char> table_;
};
// Con std::unordered_map, de la biblioteca (o std::unordered_set, si prefieren).
class StdDict {
public:
StdDict(const std::vector<std::string>& words, float max_load) {
(void)words;
(void)max_load;
not_implemented("StdDict");
}
bool contains(const std::string& word) const {
(void)word;
not_implemented("StdDict::contains");
}
private:
std::unordered_map<std::string, char> table_;
};
#endif// Lugar para su tabla de la semana 7. REEMPLACEN este archivo por su hash_map.h:
//
// cp ../week-07/lab/hash_map.h lab/hash_map.h
//
// Mientras no lo hagan, compila, pero el programa se detiene al usar la tabla.
#ifndef HASH_MAP_H
#define HASH_MAP_H
#include <cstddef>
#include <cstdint>
#include <string>
#include "not_implemented.h"
inline std::uint64_t fnv1a(const std::string&) { not_implemented("fnv1a: copien su hash_map.h"); }
template <typename V>
class HashMap {
public:
HashMap() { not_implemented("HashMap: copien su hash_map.h"); }
void set(const std::string&, const V&) {}
const V* get(const std::string&) const { return nullptr; }
bool contains(const std::string&) const { return false; }
bool remove(const std::string&) { return false; }
std::size_t size() const { return 0; }
std::size_t capacity() const { return 0; }
bool check_invariants() const { return false; }
void set_max_load_factor(double) {}
};
#endif// Lee archivos del curso. Se les da hecho.
#ifndef LOAD_WORDS_H
#define LOAD_WORDS_H
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
// Una palabra por línea.
inline std::vector<std::string> load_lines(const std::string& path) {
std::ifstream in(path);
if (!in) {
std::cerr << "no se pudo abrir " << path << "\n";
std::exit(2);
}
std::vector<std::string> lines;
std::string line;
while (std::getline(in, line)) {
if (!line.empty()) {
lines.push_back(line);
}
}
return lines;
}
// Todo el archivo en una cadena.
inline std::string load_text(const std::string& path) {
std::ifstream in(path);
if (!in) {
std::cerr << "no se pudo abrir " << path << "\n";
std::exit(2);
}
std::stringstream buffer;
buffer << in.rdbuf();
return buffer.str();
}
#endif// Para los métodos que todavía no escriben: detiene el programa y dice cuál falta.
#ifndef NOT_IMPLEMENTED_H
#define NOT_IMPLEMENTED_H
#include <cstdio>
#include <cstdlib>
[[noreturn]] inline void not_implemented(const char* name) {
std::fprintf(stderr, "sin implementar: %s\n", name);
std::abort();
}
#endif// Tabla hash con linear probing y tombstones. Completen cada método marcado con not_implemented.
#ifndef PROBING_HASH_MAP_H
#define PROBING_HASH_MAP_H
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_set>
#include <utility>
#include "not_implemented.h"
// Invariantes:
// - capacity_ == 0 si y solo si slots_ es nulo. Solo ocurre en un objeto del que se movió.
// - Si capacity_ > 0, capacity_ es 8 multiplicado por una potencia de 2.
// - size_ cuenta las casillas llenas y tombstones_ los tombstones.
// - size_ + tombstones_ <= max_load_ * capacity_, con max_load_ < 1: siempre queda al menos una
// casilla vacía, y por eso toda búsqueda termina.
// - No hay claves repetidas. Entre la casilla inicial de una clave y la casilla donde está, sin
// contar esta última, no hay ninguna casilla vacía.
//
// El factor de carga máximo es 0.7 si no se cambia: el 1.0 de la semana 7 no sirve aquí, porque
// una tabla abierta necesita casillas vacías.
//
// V debe tener constructor por defecto, porque el arreglo se reserva con new Slot[]: las
// casillas sin elemento también guardan un V.
template <typename V>
class ProbingHashMap {
public:
ProbingHashMap() : slots_(new Slot[INITIAL_CAPACITY]), capacity_(INITIAL_CAPACITY) {}
~ProbingHashMap() { delete[] slots_; }
ProbingHashMap(const ProbingHashMap& other)
: slots_(other.capacity_ == 0 ? nullptr : new Slot[other.capacity_]),
capacity_(other.capacity_), size_(other.size_),
tombstones_(other.tombstones_), max_load_(other.max_load_) {
for (std::size_t i = 0; i < capacity_; i++) {
slots_[i] = other.slots_[i];
}
}
ProbingHashMap(ProbingHashMap&& other) noexcept
: slots_(other.slots_), capacity_(other.capacity_), size_(other.size_),
tombstones_(other.tombstones_), max_load_(other.max_load_) {
// Como en la semana 7, el origen queda vacío y usable, con capacity() == 0, sin reservar
// memoria: por eso puede ser noexcept.
other.slots_ = nullptr;
other.capacity_ = 0;
other.size_ = 0;
other.tombstones_ = 0;
}
// Se recibe por valor: sirve para copiar y para mover, y cubre la autoasignación.
ProbingHashMap& operator=(ProbingHashMap other) noexcept {
std::swap(slots_, other.slots_);
std::swap(capacity_, other.capacity_);
std::swap(size_, other.size_);
std::swap(tombstones_, other.tombstones_);
std::swap(max_load_, other.max_load_);
return *this;
}
// Inserta la clave, o reemplaza su valor si ya está.
// 1. Recorran desde home(key) hasta encontrar la clave (se reemplaza y se termina) o una
// casilla vacía. Recuerden el primer tombstone que vean, sin detenerse en él.
// 2. Si la clave no estaba y vieron un tombstone, reúsenlo: no sube size_ + tombstones_.
// 3. Si no, y size_ + tombstones_ + 1 pasa del límite, llamen a rebuild_for(size_ + 1) y
// vuelvan a buscar una casilla vacía, porque la tabla cambió.
// 4. Usen la casilla vacía.
// Una tabla movida tiene capacity_ == 0: antes de todo, si es el caso, llamen a rebuild_for.
void set(const std::string& key, const V& value) {
(void)key;
(void)value;
not_implemented("ProbingHashMap::set");
}
// nullptr si la clave no está. Recuerden: se busca hasta la primera casilla VACÍA.
const V* get(const std::string& key) const {
(void)key;
not_implemented("ProbingHashMap::get");
}
bool contains(const std::string& key) const {
(void)key;
not_implemented("ProbingHashMap::contains");
}
// Deja un tombstone, y limpien la clave y el valor para soltar lo que tengan reservado:
// slot.key = std::string{}; slot.value = V{};
bool remove(const std::string& key) {
(void)key;
not_implemented("ProbingHashMap::remove");
}
// Solo de esta tabla: cuántas casillas revisa buscar esta clave, contando la última, que es
// aquella donde está la clave o la vacía donde termina la búsqueda. En una tabla movida, 0.
std::size_t probes_for(const std::string& key) const {
(void)key;
not_implemented("ProbingHashMap::probes_for");
}
std::size_t size() const { return size_; }
std::size_t capacity() const { return capacity_; }
// Solo de esta tabla: cuántos tombstones hay ahora.
std::size_t tombstones() const { return tombstones_; }
bool check_invariants() const {
if ((capacity_ == 0) != (slots_ == nullptr)) {
return false;
}
if (capacity_ == 0) {
return size_ == 0 && tombstones_ == 0;
}
if (capacity_ < INITIAL_CAPACITY || (capacity_ & (capacity_ - 1)) != 0 || !(max_load_ < 1.0)) {
return false;
}
if (static_cast<double>(size_ + tombstones_) > max_load_ * static_cast<double>(capacity_)) {
return false;
}
std::unordered_set<std::string> seen;
std::size_t full = 0;
std::size_t dead = 0;
for (std::size_t i = 0; i < capacity_; i++) {
if (slots_[i].state == State::tombstone) {
dead++;
} else if (slots_[i].state == State::full) {
full++;
if (!seen.insert(slots_[i].key).second) {
return false;
}
// Desde su casilla inicial hasta aquí no puede haber una vacía.
for (std::size_t j = home(slots_[i].key); j != i; j = (j + 1) & (capacity_ - 1)) {
if (slots_[j].state == State::empty) {
return false;
}
}
}
}
return full == size_ && dead == tombstones_ && size_ + tombstones_ < capacity_;
}
// alpha debe estar entre 0 y 1, sin incluirlos: con alpha = 1 la tabla podría llenarse y una
// búsqueda sin éxito no terminaría. Si la tabla ya no cumple el límite, crece de inmediato.
void set_max_load_factor(double alpha) {
assert(alpha > 0.0 && alpha < 1.0);
max_load_ = alpha;
if (capacity_ > 0 && used_limit_exceeded(size_ + tombstones_)) {
rebuild_for(size_);
}
}
// FNV-1a de 64 bits. Está dentro de la clase porque el corrector incluye esta tabla y la de la
// semana 7, y esa ya define una función libre llamada fnv1a.
static std::uint64_t hash_of(const std::string& key) {
std::uint64_t hash = 14695981039346656037ULL;
for (char c : key) {
hash ^= static_cast<unsigned char>(c);
hash *= 1099511628211ULL;
}
return hash;
}
private:
enum class State : unsigned char { empty, full, tombstone };
struct Slot {
State state = State::empty;
std::string key;
V value{};
};
static constexpr std::size_t INITIAL_CAPACITY = 8;
// Casilla donde empieza el probing de una clave. La capacidad es potencia de 2.
std::size_t home(const std::string& key) const {
return hash_of(key) & (capacity_ - 1);
}
bool used_limit_exceeded(std::size_t used) const {
return static_cast<double>(used) > max_load_ * static_cast<double>(capacity_);
}
// Pasa las claves a un arreglo nuevo, sin tombstones, donde caben `needed` claves.
// Empiecen de la capacidad actual, o de INITIAL_CAPACITY si es menor. Si needed es como mucho
// la mitad del límite (max_load_ * capacidad / 2), la capacidad no cambia: casi todo lo que
// ocupaba la tabla eran tombstones, y reconstruir los limpia. Si no, dupliquen, y sigan
// duplicando mientras needed no quepa en el límite. Reserven con new Slot[nueva], reinserten
// solo las casillas llenas, liberen el arreglo viejo y dejen tombstones_ en 0.
void rebuild_for(std::size_t needed) {
(void)needed;
not_implemented("ProbingHashMap::rebuild_for");
}
Slot* slots_;
std::size_t capacity_;
std::size_t size_ = 0;
std::size_t tombstones_ = 0;
double max_load_ = 0.7;
};
#endif# Informe del corrector
Peguen aquí la salida de `make bench` y respondan con sus propios números.
## Cómo midieron
Máquina, cómo compilaron, cuántas corridas de `make bench` hicieron y si otros programas estaban
ocupando la máquina.
## Tabla
## Los cuatro diseños
¿Cuál carga más rápido? ¿Cuál responde más rápido? ¿Cuál reserva menos memoria? Digan por cuánto, y
si la respuesta se repite en sus corridas.
## El factor de carga
1. Comparen la columna `máx` con la columna `real`. ¿Por qué no siempre coinciden?
2. Con las tablas de 2^20 casillas: ¿cómo crece el tiempo de cada diseño cuando sube el factor de
carga real? ¿Se parece a lo que predicen las fórmulas de los apuntes?
## Explicación
¿Qué parte de su explicación midieron (tiempos, memoria, conteos de casillas) y qué parte suponen?// Corrector ortográfico: revisa palabras y propone las que están a un cambio de distancia.
// Trabajen con letras (split_letters, en utf8.h), nunca con bytes: "á" ocupa dos.
#ifndef SPELL_CHECKER_H
#define SPELL_CHECKER_H
#include <algorithm>
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
#include "not_implemented.h"
#include "utf8.h"
struct Misspelling {
std::string word;
std::size_t count; // cuántas veces aparece en el texto
std::vector<std::string> suggestions; // en orden de bytes, sin repetir
};
// Todas las palabras a un cambio de distancia de `word`, con repeticiones permitidas. Un cambio es
// una de estas tres cosas, y no otra (intercambiar dos letras vecinas son dos cambios):
// - borrar una letra;
// - reemplazar una letra por otra de alphabet();
// - insertar una letra de alphabet() en cualquier posición, incluidos el inicio y el final.
inline std::vector<std::string> one_edit_candidates(const std::string& word) {
(void)word;
not_implemented("one_edit_candidates");
}
// Los candidatos que están en el diccionario, ordenados y sin repetir, sin la cadena vacía.
// Dict es cualquier tipo con contains(word).
template <typename Dict>
std::vector<std::string> suggest(const Dict& dict, const std::string& word) {
(void)dict;
(void)word;
not_implemented("suggest");
}
// Un Misspelling por cada palabra distinta que no está en el diccionario, en el orden en que
// aparecen por primera vez. Calculen las sugerencias de cada palabra una sola vez: una palabra
// desconocida puede repetirse miles de veces.
template <typename Dict>
std::vector<Misspelling> check_words(const Dict& dict, const std::vector<std::string>& words) {
(void)dict;
(void)words;
not_implemented("check_words");
}
#endif// Letras de una palabra en UTF-8. Este archivo se les da hecho: lo central del laboratorio es la
// tabla y el corrector, no la codificación.
//
// Un std::string guarda bytes, y una letra como "á" ocupa dos. Si un corrector cambiara un char a
// la vez, partiría esa letra y probaría cadenas que ni siquiera son UTF-8 válido. Por eso todo el
// corrector trabaja con letras: cada letra es un std::string con los bytes de un carácter.
#ifndef UTF8_H
#define UTF8_H
#include <array>
#include <cstddef>
#include <string>
#include <vector>
// Cuántos bytes tiene el carácter que empieza con este byte. 0 si el byte no puede empezar uno.
inline std::size_t utf8_length(char first) {
unsigned char c = static_cast<unsigned char>(first);
if (c < 0x80) return 1;
if (c >= 0xC2 && c <= 0xDF) return 2;
if (c >= 0xE0 && c <= 0xEF) return 3;
if (c >= 0xF0 && c <= 0xF4) return 4;
return 0;
}
// Parte una palabra en letras: "año" da {"a", "ñ", "o"}. Un byte inválido queda como una letra.
inline std::vector<std::string> split_letters(const std::string& word) {
std::vector<std::string> letters;
std::size_t i = 0;
while (i < word.size()) {
std::size_t length = utf8_length(word[i]);
if (length == 0 || i + length > word.size()) {
length = 1;
}
letters.push_back(word.substr(i, length));
i += length;
}
return letters;
}
// Une letras otra vez en una palabra.
inline std::string join_letters(const std::vector<std::string>& letters) {
std::string word;
for (const std::string& letter : letters) {
word += letter;
}
return word;
}
// Las 33 letras que aparecen en palabras.txt.
inline const std::array<std::string, 33>& alphabet() {
static const std::array<std::string, 33> letters = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q",
"r", "s", "t", "u", "v", "w", "x", "y", "z", "á", "é", "í", "ñ", "ó", "ú", "ü"};
return letters;
}
// Si los bytes en word[i] son una letra (a-z, A-Z o una letra de Latin-1, como á o Ñ), la agrega
// en minúscula a `out` y devuelve cuántos bytes ocupaba. Si no, devuelve 0.
inline std::size_t take_lowercase_letter(const std::string& text, std::size_t i, std::string& out) {
unsigned char c = static_cast<unsigned char>(text[i]);
if (c >= 'a' && c <= 'z') {
out += static_cast<char>(c);
return 1;
}
if (c >= 'A' && c <= 'Z') {
out += static_cast<char>(c + ('a' - 'A'));
return 1;
}
// En UTF-8, U+00C0 a U+00FF son 0xC3 seguido de 0x80 a 0xBF. Las mayúsculas llegan hasta
// 0x9E y su minúscula está 0x20 más arriba. 0x97 y 0xB7 son los signos de multiplicar y dividir.
if (c == 0xC3 && i + 1 < text.size()) {
unsigned char d = static_cast<unsigned char>(text[i + 1]);
if (d >= 0x80 && d <= 0xBF && d != 0x97 && d != 0xB7) {
out += static_cast<char>(c);
out += static_cast<char>(d <= 0x9E ? d + 0x20 : d);
return 2;
}
}
return 0;
}
// Saca las palabras de un texto, en minúsculas: lo que no es letra las separa.
inline std::vector<std::string> extract_words(const std::string& text) {
std::vector<std::string> words;
std::string current;
std::size_t i = 0;
while (i < text.size()) {
std::size_t taken = take_lowercase_letter(text, i, current);
if (taken > 0) {
i += taken;
} else {
if (!current.empty()) {
words.push_back(current);
current.clear();
}
i++;
}
}
if (!current.empty()) {
words.push_back(current);
}
return words;
}
#endif// Mide las cuatro formas de guardar el diccionario, y las dos tablas con varios factores de carga.
// bench_spell <diccionario> <texto>. Se compila con -O2 y sin sanitizers.
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <random>
#include <utility>
#include <string>
#include <vector>
#include "bench.h"
#include "bytes_counter.h"
#include "dictionaries.h"
#include "load_words.h"
#include "spell_checker.h"
namespace {
constexpr int kLoadReps = 3;
constexpr int kAskReps = 5;
// Toda pregunta que revisar el texto le hace al diccionario, para medir solo las búsquedas.
std::vector<std::string> build_queries(const std::vector<std::string>& entries,
const std::vector<std::string>& words) {
const SortedDict dict(entries);
std::vector<std::string> queries = words;
std::vector<std::string> unknown;
for (const std::string& word : words) {
if (!dict.contains(word)) {
unknown.push_back(word);
}
}
std::sort(unknown.begin(), unknown.end());
unknown.erase(std::unique(unknown.begin(), unknown.end()), unknown.end());
for (const std::string& word : unknown) {
for (std::string& candidate : one_edit_candidates(word)) {
if (!candidate.empty()) {
queries.push_back(std::move(candidate));
}
}
}
return queries;
}
template <typename Dict>
double ask_ns(const Dict& dict, const std::vector<std::string>& queries) {
const double seconds = median_seconds(kAskReps, [&] {
std::size_t found = 0;
for (const std::string& query : queries) {
found += dict.contains(query) ? 1 : 0;
}
keep(found);
});
return seconds * 1e9 / static_cast<double>(queries.size());
}
template <typename Build>
void measure(const char* name, const char* max_load, const std::vector<std::string>& entries,
const std::vector<std::string>& words, const std::vector<std::string>& queries,
Build build) {
const double load = median_seconds(kLoadReps, [&] {
auto dict = build();
keep(dict.contains(entries.front()));
});
const std::size_t before = live_bytes();
const auto dict = build();
const std::size_t bytes = live_bytes() - before;
const double check = median_seconds(kAskReps, [&] {
keep(check_words(dict, words).size());
});
char real[16] = "-";
if constexpr (requires { dict.load_factor(); }) {
std::snprintf(real, sizeof real, "%.3f", dict.load_factor());
}
std::printf("%-16s %5s %6s %7.0f %8.1f %8.0f %7.1f\n", name, max_load, real, load * 1000,
check * 1000, ask_ns(dict, queries), static_cast<double>(bytes) / (1024.0 * 1024.0));
}
// Mismas casillas y mismas preguntas, cambiando cuántas palabras se guardan: así cambia el
// factor de carga real y casi nada más.
template <template <typename> class Table>
void same_slots(const char* name, const std::vector<std::string>& shuffled,
const std::vector<std::string>& queries, double max_load) {
const std::size_t slots = std::size_t{1} << 20;
for (double fraction : {0.55, 0.70, 0.80, 0.90, 0.95}) {
const std::size_t n = static_cast<std::size_t>(fraction * static_cast<double>(slots));
const std::vector<std::string> subset(shuffled.begin(), shuffled.begin() + static_cast<std::ptrdiff_t>(n));
const TableDict<Table> dict(subset, max_load);
std::printf("%-16s %6.3f %8.0f\n", name, dict.load_factor(), ask_ns(dict, queries));
}
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
std::fprintf(stderr, "uso: bench_spell <diccionario> <texto>\n");
return 2;
}
const std::vector<std::string> entries = load_lines(argv[1]);
const std::vector<std::string> words = extract_words(load_text(argv[2]));
const std::vector<std::string> queries = build_queries(entries, words);
std::printf("diccionario: %zu palabras. texto: %zu palabras. preguntas al revisar: %zu.\n",
entries.size(), words.size(), queries.size());
std::printf("cargar y revisar en ms; preguntar en ns por pregunta; MB reservados por el diccionario.\n");
std::printf("Mediana de %d cargas y de %d revisiones o rondas de preguntas.\n\n", kLoadReps, kAskReps);
std::printf("%-16s %5s %6s %7s %8s %8s %7s\n", "diseño", "máx", "real", "cargar", "revisar", "pregunta", "MB");
measure("arreglo ordenado", "-", entries, words, queries, [&] { return SortedDict(entries); });
measure("unordered_map", "1.00", entries, words, queries, [&] { return StdDict(entries, 1.0f); });
for (const auto& [text, alpha] : {std::pair{"0.50", 0.5}, {"1.00", 1.0}, {"2.00", 2.0}, {"4.00", 4.0}}) {
measure("encadenamiento", text, entries, words, queries,
[&, alpha = alpha] { return TableDict<HashMap>(entries, alpha); });
}
for (const auto& [text, alpha] : {std::pair{"0.25", 0.25}, {"0.50", 0.5}, {"0.70", 0.7}}) {
measure("linear probing", text, entries, words, queries,
[&, alpha = alpha] { return TableDict<ProbingHashMap>(entries, alpha); });
}
// La tabla duplica su capacidad, así que el factor real no siempre es el máximo pedido.
// Aquí se fija: 2^20 casillas y distintas cantidades de palabras, elegidas al azar con semilla fija.
std::vector<std::string> shuffled = entries;
std::mt19937 rng(2026);
std::shuffle(shuffled.begin(), shuffled.end(), rng);
std::printf("\nMismas 2^20 casillas, mismas preguntas. ns por pregunta.\n");
std::printf("%-16s %6s %8s\n", "diseño", "real", "pregunta");
same_slots<HashMap>("encadenamiento", shuffled, queries, 1.0);
same_slots<ProbingHashMap>("linear probing", shuffled, queries, 0.9995);
return 0;
}// Cuenta los bytes reservados con new y todavía sin liberar. Se incluye en un solo .cpp.
#ifndef BYTES_COUNTER_H
#define BYTES_COUNTER_H
#include <cstddef>
#include <cstdlib>
#include <new>
inline std::size_t& live_bytes() {
static std::size_t bytes = 0;
return bytes;
}
// Cada bloque guarda su tamaño en un encabezado, para descontarlo al liberar.
namespace bytes_counter {
constexpr std::size_t kHeader = 16;
}
[[gnu::noinline]] void* operator new(std::size_t size) {
void* raw = std::malloc(size + bytes_counter::kHeader);
if (raw == nullptr) {
throw std::bad_alloc();
}
*static_cast<std::size_t*>(raw) = size;
live_bytes() += size;
return static_cast<char*>(raw) + bytes_counter::kHeader;
}
[[gnu::noinline]] void operator delete(void* block) noexcept {
if (block != nullptr) {
char* raw = static_cast<char*>(block) - bytes_counter::kHeader;
live_bytes() -= *reinterpret_cast<std::size_t*>(raw);
std::free(raw);
}
}
[[gnu::noinline]] void operator delete(void* block, std::size_t) noexcept {
operator delete(block);
}
#endif// Pruebas comunes de un diccionario con la interfaz de la semana 7. Map es un mapa de
// std::string a int: HashMap<int>. Solo usan esa interfaz, sin mirar cómo está hecho por dentro.
#ifndef MAP_SUITE_H
#define MAP_SUITE_H
#include <cassert>
#include <cstddef>
#include <random>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
template <typename Map>
void test_new_is_empty() {
Map map;
assert(map.size() == 0);
assert(map.get("lima") == nullptr);
assert(!map.contains("lima"));
assert(!map.remove("lima"));
assert(map.check_invariants());
}
template <typename Map>
void test_set_and_get() {
Map map;
map.set("lima", 1);
map.set("cusco", 2);
assert(map.size() == 2);
assert(*map.get("lima") == 1);
assert(*map.get("cusco") == 2);
assert(map.get("puno") == nullptr);
assert(map.check_invariants());
}
template <typename Map>
void test_overwrite() {
Map map;
map.set("lima", 1);
map.set("lima", 2);
assert(map.size() == 1);
assert(*map.get("lima") == 2);
assert(map.check_invariants());
}
template <typename Map>
void test_contains() {
Map map;
map.set("lima", 0);
assert(map.contains("lima"));
assert(!map.contains("Lima"));
assert(!map.contains("lim"));
assert(!map.contains("limaa"));
}
template <typename Map>
void test_remove_present() {
Map map;
map.set("lima", 1);
map.set("cusco", 2);
assert(map.remove("lima"));
assert(map.size() == 1);
assert(!map.contains("lima"));
assert(*map.get("cusco") == 2);
assert(map.check_invariants());
map.set("lima", 3);
assert(*map.get("lima") == 3);
assert(map.size() == 2);
}
template <typename Map>
void test_remove_absent() {
Map map;
map.set("lima", 1);
assert(!map.remove("cusco"));
assert(map.size() == 1);
assert(map.remove("lima"));
assert(!map.remove("lima"));
assert(map.size() == 0);
assert(map.check_invariants());
}
template <typename Map>
void test_unusual_keys() {
Map map;
map.set("", 1);
map.set(std::string("a\0b", 3), 2);
map.set("a", 3);
map.set("ñandú", 4);
map.set(std::string(5000, 'x'), 5);
assert(map.size() == 5);
assert(*map.get("") == 1);
assert(*map.get(std::string("a\0b", 3)) == 2);
assert(*map.get("a") == 3);
assert(*map.get("ñandú") == 4);
assert(*map.get(std::string(5000, 'x')) == 5);
assert(map.check_invariants());
}
template <typename Map>
void test_growth() {
Map map;
map.set_max_load_factor(0.5);
std::size_t initial = map.capacity();
const int total = 20000;
for (int i = 0; i < total; i++) {
map.set("k" + std::to_string(i), i);
assert(static_cast<double>(map.size()) <= 0.5 * static_cast<double>(map.capacity()));
}
assert(map.size() == static_cast<std::size_t>(total));
assert(map.capacity() > initial);
for (int i = 0; i < total; i++) {
assert(*map.get("k" + std::to_string(i)) == i);
}
assert(map.check_invariants());
}
template <typename Map>
void test_copy_is_independent() {
Map original;
for (int i = 0; i < 100; i++) {
original.set("k" + std::to_string(i), i);
}
Map copy(original);
copy.set("k0", -1);
assert(copy.remove("k1"));
copy.set("nueva", 7);
assert(*original.get("k0") == 0);
assert(original.contains("k1"));
assert(!original.contains("nueva"));
assert(original.size() == 100);
assert(copy.size() == 100);
assert(*copy.get("k50") == 50);
assert(original.check_invariants());
assert(copy.check_invariants());
Map assigned;
assigned.set("vieja", 1);
assigned = original;
assert(!assigned.contains("vieja"));
assert(assigned.size() == 100);
assigned.set("k2", -2);
assert(*original.get("k2") == 2);
}
template <typename Map>
void test_self_assignment() {
Map map;
map.set("lima", 1);
Map& same = map;
map = same;
assert(*map.get("lima") == 1);
assert(map.size() == 1);
assert(map.check_invariants());
}
template <typename Map>
void test_move_leaves_empty_and_usable() {
Map source;
for (int i = 0; i < 50; i++) {
source.set("k" + std::to_string(i), i);
}
Map target(std::move(source));
assert(target.size() == 50);
assert(*target.get("k49") == 49);
assert(source.size() == 0);
assert(source.get("k1") == nullptr);
assert(!source.contains("k1"));
assert(!source.remove("k1"));
assert(source.check_invariants());
source.set("otra", 1);
assert(*source.get("otra") == 1);
assert(source.check_invariants());
Map assigned;
assigned.set("vieja", 1);
assigned = std::move(target);
assert(!assigned.contains("vieja"));
assert(assigned.size() == 50);
assert(target.size() == 0);
target.set("de_nuevo", 2);
assert(*target.get("de_nuevo") == 2);
assert(assigned.check_invariants());
assert(target.check_invariants());
}
// Operaciones al azar, con semilla fija, comparando con std::unordered_map.
template <typename Map>
void random_against_unordered_map(unsigned seed, int steps, unsigned key_range) {
std::mt19937 rng(seed);
Map map;
std::unordered_map<std::string, int> model;
for (int step = 0; step < steps; step++) {
std::string key = "k" + std::to_string(rng() % key_range);
switch (rng() % 4) {
case 0:
case 1: {
int value = static_cast<int>(rng() % 1000);
map.set(key, value);
model[key] = value;
break;
}
case 2: {
const int* found = map.get(key);
auto it = model.find(key);
assert((found == nullptr) == (it == model.end()));
if (found != nullptr) {
assert(*found == it->second);
}
assert(map.contains(key) == (it != model.end()));
break;
}
default:
assert(map.remove(key) == (model.erase(key) == 1));
break;
}
assert(map.size() == model.size());
if (step % 1000 == 0) {
assert(map.check_invariants());
}
}
assert(map.check_invariants());
for (const auto& entry : model) {
assert(*map.get(entry.first) == entry.second);
}
}
template <typename Map>
void test_random_small() {
// Pocas claves: se borra y se reinserta mucho.
random_against_unordered_map<Map>(1u, 30000, 50);
}
template <typename Map>
void test_random_large() {
// Muchas claves: la tabla tiene que crecer varias veces.
random_against_unordered_map<Map>(2u, 40000, 20000);
}
template <typename Map>
void run_map_suite() {
test_new_is_empty<Map>();
test_set_and_get<Map>();
test_overwrite<Map>();
test_contains<Map>();
test_remove_present<Map>();
test_remove_absent<Map>();
test_unusual_keys<Map>();
test_growth<Map>();
test_copy_is_independent<Map>();
test_self_assignment<Map>();
test_move_leaves_empty_and_usable<Map>();
test_random_small<Map>();
test_random_large<Map>();
}
#endif// Corre las pruebas de la sesión 1: map_tests <nombre>, o map_tests para ver los nombres.
#include <cassert>
#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <type_traits>
#include <vector>
#include "churn_test.h"
#include "hash_map.h"
#include "map_suite.h"
#include "probing_hash_map.h"
namespace {
using Probing = ProbingHashMap<int>;
// Las pruebas de abajo necesitan claves que empiecen en la misma casilla.
std::vector<std::string> colliding_keys(std::size_t capacity, std::size_t count) {
std::vector<std::string> keys;
const std::uint64_t target = Probing::hash_of("base") & (capacity - 1);
for (int i = 0; keys.size() < count; i++) {
const std::string key = "c" + std::to_string(i);
if ((Probing::hash_of(key) & (capacity - 1)) == target) {
keys.push_back(key);
}
}
return keys;
}
void hash_vectors() {
// Vectores de prueba publicados de FNV-1a de 64 bits.
assert(Probing::hash_of("") == 0xcbf29ce484222325ULL);
assert(Probing::hash_of("a") == 0xaf63dc4c8601ec8cULL);
assert(Probing::hash_of("foobar") == 0x85944171f73967e8ULL);
}
void search_passes_tombstones() {
// Es el error común de la semana: detenerse en el primer tombstone.
Probing map;
map.set_max_load_factor(0.5);
while (map.capacity() < 64) {
map.set("x" + std::to_string(map.size()), 0);
}
assert(map.capacity() == 64);
const std::vector<std::string> same = colliding_keys(map.capacity(), 3);
for (const std::string& key : same) {
map.set(key, 7);
}
assert(map.capacity() == 64);
assert(map.remove(same[0]));
assert(map.tombstones() == 1);
assert(!map.contains(same[0]));
assert(map.contains(same[1]));
assert(map.contains(same[2]));
assert(*map.get(same[2]) == 7);
assert(map.check_invariants());
}
void set_does_not_duplicate_past_tombstone() {
// El tombstone va antes que la clave: insertar sin seguir buscando la duplicaría.
Probing map;
map.set_max_load_factor(0.5);
while (map.capacity() < 64) {
map.set("x" + std::to_string(map.size()), 0);
}
const std::vector<std::string> same = colliding_keys(map.capacity(), 3);
map.set(same[0], 1);
map.set(same[1], 2);
map.remove(same[0]);
const std::size_t before = map.size();
map.set(same[1], 3);
assert(map.size() == before);
assert(*map.get(same[1]) == 3);
assert(map.remove(same[1]));
assert(!map.contains(same[1]));
assert(map.check_invariants());
}
void set_reuses_tombstone() {
Probing map;
map.set_max_load_factor(0.5);
while (map.capacity() < 64) {
map.set("x" + std::to_string(map.size()), 0);
}
const std::vector<std::string> same = colliding_keys(map.capacity(), 2);
map.set(same[0], 1);
map.remove(same[0]);
assert(map.tombstones() == 1);
map.set(same[1], 2); // mismo inicio: su búsqueda de lugar pasa por el tombstone
assert(map.tombstones() == 0);
assert(*map.get(same[1]) == 2);
assert(map.check_invariants());
}
void new_table_has_8_slots() {
assert(Probing().capacity() == 8);
}
void lowering_load_factor_rebuilds() {
Probing map;
for (int i = 0; i < 500; i++) {
map.set(std::to_string(i), i);
}
map.set_max_load_factor(0.25);
assert(static_cast<double>(map.size()) <= 0.25 * static_cast<double>(map.capacity()));
assert(map.check_invariants());
for (int i = 0; i < 500; i++) {
assert(*map.get(std::to_string(i)) == i);
}
}
void high_load_factor_still_ends() {
// Con alpha = 0.99 tiene que quedar una casilla vacía, o una búsqueda sin éxito no termina.
Probing map;
map.set_max_load_factor(0.99);
for (int i = 0; i < 3000; i++) {
map.set(std::to_string(i), i);
}
assert(!map.contains("no está"));
assert(map.check_invariants());
}
void probes_count_the_last_slot() {
Probing map;
// En una tabla vacía se revisa una casilla: la vacía donde termina.
assert(map.probes_for("x") == 1);
map.set("x", 1);
assert(map.probes_for("x") == 1);
}
void churn_keeps_probes_low() {
Probing map;
for (int i = 0; i < 100; i++) {
map.set("k" + std::to_string(i), i);
}
const std::size_t capacity_before = map.capacity();
for (int i = 100; i < 50100; i++) {
map.remove("k" + std::to_string(i - 100));
map.set("k" + std::to_string(i), i);
}
assert(map.size() == 100);
assert(map.check_invariants());
// Solo hay 100 claves vivas: la tabla no debe crecer sin límite.
assert(map.capacity() <= 4 * capacity_before);
std::size_t total = 0;
for (int i = 0; i < 1000; i++) {
total += map.probes_for("ausente" + std::to_string(i));
}
assert(total / 1000 < 20);
}
void rebuild_clears_tombstones() {
Probing map;
for (int i = 0; i < 100; i++) {
map.set("k" + std::to_string(i), i);
}
std::size_t max_tombstones = 0;
for (int i = 100; i < 20100; i++) {
map.remove("k" + std::to_string(i - 100));
map.set("k" + std::to_string(i), i);
if (map.tombstones() > max_tombstones) {
max_tombstones = map.tombstones();
}
}
assert(max_tombstones > 0);
assert(static_cast<double>(map.size() + max_tombstones) <= 0.7 * static_cast<double>(map.capacity()));
}
void remove_releases_value() {
ProbingHashMap<std::shared_ptr<int>> map;
auto value = std::make_shared<int>(5);
map.set("x", value);
assert(value.use_count() == 2);
map.remove("x");
assert(value.use_count() == 1);
}
void moved_from_is_usable() {
Probing first;
first.set("a", 1);
Probing second(std::move(first));
assert(first.size() == 0);
assert(first.capacity() == 0);
Probing copy_of_empty(first);
assert(copy_of_empty.capacity() == 0 && copy_of_empty.check_invariants());
assert(!first.contains("a"));
assert(!first.remove("a"));
first.set("b", 2);
assert(*first.get("b") == 2);
assert(first.check_invariants());
static_assert(std::is_nothrow_move_constructible_v<Probing>);
}
void string_values() {
ProbingHashMap<std::string> map;
map.set("llave", "valor con un texto bastante largo para reservar memoria");
map.set("otra", "x");
assert(*map.get("llave") == "valor con un texto bastante largo para reservar memoria");
map.remove("llave");
assert(map.get("llave") == nullptr);
}
struct Test {
const char* name;
void (*run)();
};
const Test kTests[] = {
{"hash_vectors", hash_vectors},
{"new_is_empty", test_new_is_empty<Probing>},
{"set_and_get", test_set_and_get<Probing>},
{"overwrite", test_overwrite<Probing>},
{"contains", test_contains<Probing>},
{"remove_present", test_remove_present<Probing>},
{"remove_absent", test_remove_absent<Probing>},
{"unusual_keys", test_unusual_keys<Probing>},
{"growth", test_growth<Probing>},
{"copy_is_independent", test_copy_is_independent<Probing>},
{"self_assignment", test_self_assignment<Probing>},
{"move_leaves_empty_and_usable", test_move_leaves_empty_and_usable<Probing>},
{"random_small", test_random_small<Probing>},
{"random_large", test_random_large<Probing>},
{"new_table_has_8_slots", new_table_has_8_slots},
{"search_passes_tombstones", search_passes_tombstones},
{"set_does_not_duplicate_past_tombstone", set_does_not_duplicate_past_tombstone},
{"set_reuses_tombstone", set_reuses_tombstone},
{"lowering_load_factor_rebuilds", lowering_load_factor_rebuilds},
{"high_load_factor_still_ends", high_load_factor_still_ends},
{"probes_count_the_last_slot", probes_count_the_last_slot},
{"churn_keeps_probes_low", churn_keeps_probes_low},
{"rebuild_clears_tombstones", rebuild_clears_tombstones},
{"remove_releases_value", remove_releases_value},
{"moved_from_is_usable", moved_from_is_usable},
{"string_values", string_values},
{"my_churn_test", my_churn_test},
{"week7_table_passes_suite", [] { run_map_suite<HashMap<int>>(); }},
{"fnv1a_of_week7", [] {
assert(fnv1a("") == 0xcbf29ce484222325ULL);
assert(fnv1a("foobar") == 0x85944171f73967e8ULL);
}},
};
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
for (const Test& test : kTests) {
std::cout << test.name << "\n";
}
return 0;
}
for (const Test& test : kTests) {
if (std::strcmp(test.name, argv[1]) == 0) {
test.run();
return 0;
}
}
std::cerr << "no existe la prueba " << argv[1] << "\n";
return 2;
}// Corre las pruebas de la sesión 2: spell_tests <nombre> <carpeta de fixtures>.
#include <algorithm>
#include <cassert>
#include <cstring>
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include "dictionaries.h"
#include "load_words.h"
#include "spell_checker.h"
namespace {
std::string fixtures;
// Comprueba que una cadena sea UTF-8 válido, carácter por carácter.
bool valid_utf8(const std::string& word) {
std::size_t i = 0;
while (i < word.size()) {
const std::size_t length = utf8_length(word[i]);
if (length == 0 || i + length > word.size()) {
return false;
}
for (std::size_t j = 1; j < length; j++) {
if ((static_cast<unsigned char>(word[i + j]) & 0xC0) != 0x80) {
return false;
}
}
i += length;
}
return true;
}
bool has(const std::vector<std::string>& list, const std::string& word) {
return std::find(list.begin(), list.end(), word) != list.end();
}
std::vector<std::string> dictionary() { return load_lines(fixtures + "/mini_dict.txt"); }
void helper_splits_letters() {
const std::vector<std::string> expected = {"a", "ñ", "o"};
assert(split_letters("año") == expected);
assert(split_letters("año").size() == 3);
assert(std::string("año").size() == 4);
assert(join_letters(split_letters("pingüino")) == "pingüino");
}
void helper_extracts_words() {
const std::vector<std::string> expected = {"la", "casa", "del", "niño", "y", "el", "águila"};
assert(extract_words("La CASA del NIÑO, ¿y el Águila?") == expected);
}
void candidates_are_the_three_edits() {
const std::vector<std::string> edits = one_edit_candidates("sol");
assert(has(edits, "ol")); // borrar
assert(has(edits, "so"));
assert(has(edits, "sal")); // reemplazar
assert(has(edits, "sól"));
assert(has(edits, "asol")); // insertar al inicio
assert(has(edits, "soñl")); // en medio
assert(has(edits, "solé")); // al final
assert(!has(edits, "sol"));
assert(!has(edits, "slo")); // intercambiar son dos cambios
// 3 borrados, 3 * 32 reemplazos y 4 * 33 inserciones.
assert(edits.size() == 3 + 3 * 32 + 4 * 33);
}
void candidates_are_valid_utf8() {
for (const char* word : {"año", "café", "pingüino", "ñ"}) {
for (const std::string& candidate : one_edit_candidates(word)) {
assert(valid_utf8(candidate));
}
}
// Borrar y reemplazar cuentan letras, no bytes.
assert(has(one_edit_candidates("año"), "ao"));
assert(has(one_edit_candidates("año"), "ano"));
assert(has(one_edit_candidates("cafe"), "café"));
assert(one_edit_candidates("ñ").size() == 1 + 32 + 2 * 33);
}
template <typename Dict>
void check_dict(const Dict& dict) {
assert(dict.contains("casa"));
assert(dict.contains("niño"));
assert(!dict.contains("niño "));
assert(!dict.contains("cas"));
const std::vector<std::string> near_cafe = {"café"};
assert(suggest(dict, "cafe") == near_cafe);
const std::vector<std::string> near_cosa = {"casa", "caso"};
assert(suggest(dict, "cosq") == std::vector<std::string>{"cosa"});
assert(suggest(dict, "cas") == near_cosa);
assert(suggest(dict, "xxxxxxx").empty());
// Nunca se sugiere la cadena vacía, aunque borrar dé "".
assert(suggest(dict, "z").empty());
}
void suggest_with_sorted() { check_dict(SortedDict(dictionary())); }
void suggest_with_std() { check_dict(StdDict(dictionary(), 1.0f)); }
void suggest_with_probing() { check_dict(TableDict<ProbingHashMap>(dictionary(), 0.7)); }
void suggest_with_week7() { check_dict(TableDict<HashMap>(dictionary(), 1.0)); }
// Las sugerencias salen en orden de bytes: ñ y las vocales con tilde van después de la z.
void suggestions_are_sorted_and_unique() {
const TableDict<ProbingHashMap> dict(dictionary(), 0.7);
const std::vector<std::string> near = suggest(dict, "ano");
assert(near == std::vector<std::string>{"año"});
const std::vector<std::string> mesa = suggest(dict, "mes");
assert(mesa == std::vector<std::string>{"mesa"});
const std::vector<std::string> sal = suggest(dict, "sl");
const std::vector<std::string> both = {"sal", "sol"};
assert(sal == both);
}
template <typename Dict>
void check_report(const Dict& dict) {
const std::vector<std::string> words = extract_words(load_text(fixtures + "/mini_text.txt"));
const std::vector<Misspelling> report = check_words(dict, words);
std::set<std::string> distinct;
for (const Misspelling& item : report) {
assert(distinct.insert(item.word).second);
assert(!dict.contains(item.word));
}
// "cafe" aparece dos veces y "casq" una. "la", "una", "tiene"... tampoco están en el fixture.
bool found_cafe = false;
bool found_casq = false;
for (const Misspelling& item : report) {
if (item.word == "cafe") {
found_cafe = true;
assert(item.count == 2);
assert(item.suggestions == std::vector<std::string>{"café"});
}
if (item.word == "casq") {
found_casq = true;
assert(item.count == 1);
const std::vector<std::string> expected = {"casa", "caso"};
assert(item.suggestions == expected);
}
assert(item.word != "casa" && item.word != "niño" && item.word != "perro");
}
assert(found_cafe && found_casq);
assert(report.front().word == "la");
}
void report_with_probing() { check_report(TableDict<ProbingHashMap>(dictionary(), 0.7)); }
void report_with_week7() { check_report(TableDict<HashMap>(dictionary(), 1.0)); }
void report_with_std() { check_report(StdDict(dictionary(), 1.0f)); }
void report_with_sorted() { check_report(SortedDict(dictionary())); }
// Un diccionario que cuenta cuántas veces se le pregunta.
struct CountingDict {
TableDict<ProbingHashMap> dict;
mutable std::size_t queries = 0;
bool contains(const std::string& word) const {
queries++;
return dict.contains(word);
}
};
void suggests_each_word_once() {
CountingDict dict{TableDict<ProbingHashMap>(dictionary(), 0.7)};
const std::vector<std::string> one = {"zzz"};
check_words(dict, one);
const std::size_t once = dict.queries;
assert(once > 1);
dict.queries = 0;
const std::vector<std::string> many(100, "zzz");
const std::vector<Misspelling> report = check_words(dict, many);
assert(report.size() == 1 && report[0].count == 100);
// Se revisan las 100 apariciones, pero se calculan las sugerencias una sola vez.
assert(dict.queries == once + 99);
}
struct Test {
const char* name;
void (*run)();
};
const Test kTests[] = {
{"helper_splits_letters", helper_splits_letters},
{"helper_extracts_words", helper_extracts_words},
{"candidates_are_the_three_edits", candidates_are_the_three_edits},
{"candidates_are_valid_utf8", candidates_are_valid_utf8},
{"suggest_with_sorted", suggest_with_sorted},
{"suggest_with_std", suggest_with_std},
{"suggest_with_probing", suggest_with_probing},
{"suggest_with_week7", suggest_with_week7},
{"suggestions_are_sorted_and_unique", suggestions_are_sorted_and_unique},
{"report_with_probing", report_with_probing},
{"report_with_week7", report_with_week7},
{"report_with_std", report_with_std},
{"report_with_sorted", report_with_sorted},
{"suggests_each_word_once", suggests_each_word_once},
};
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
for (const Test& test : kTests) {
std::cout << test.name << "\n";
}
return 0;
}
fixtures = argv[2];
for (const Test& test : kTests) {
if (std::strcmp(test.name, argv[1]) == 0) {
test.run();
return 0;
}
}
std::cerr << "no existe la prueba " << argv[1] << "\n";
return 2;
}// El corrector como programa: spell <diccionario> <texto> [cuántas palabras mostrar]
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "dictionaries.h"
#include "load_words.h"
#include "spell_checker.h"
int main(int argc, char** argv) {
if (argc < 3) {
std::cerr << "uso: spell <diccionario> <texto> [cuántas palabras mostrar]\n";
return 2;
}
const std::vector<std::string> entries = load_lines(argv[1]);
const std::vector<std::string> words = extract_words(load_text(argv[2]));
const std::size_t shown = argc > 3 ? std::strtoul(argv[3], nullptr, 10) : 10;
const TableDict<ProbingHashMap> dict(entries, 0.7);
const std::vector<Misspelling> report = check_words(dict, words);
std::size_t occurrences = 0;
for (const Misspelling& item : report) {
occurrences += item.count;
}
std::cout << "diccionario: " << entries.size() << " palabras\n"
<< "texto: " << words.size() << " palabras\n"
<< "desconocidas: " << occurrences << " apariciones de " << report.size()
<< " palabras distintas\n";
for (std::size_t i = 0; i < report.size() && i < shown; i++) {
std::cout << report[i].word << " (" << report[i].count << "):";
for (std::size_t j = 0; j < report[i].suggestions.size() && j < 6; j++) {
std::cout << " " << report[i].suggestions[j];
}
if (report[i].suggestions.size() > 6) {
std::cout << " ...";
}
std::cout << "\n";
}
return 0;
}agua
año
aña
café
cafés
casa
caso
cosa
masa
mesa
niño
niña
perro
pero
pera
sol
sal
tasaLa Casa del NIÑO tiene una mesa.
El perro bebe agua, ¿y el cafe? El perro no bebe cafe.
Una pesa y un Niño: la casq.Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 9: direccionamiento abierto, Swiss Tables y dict de CPython. Se usa dentro del entorno.
#
# 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-table prueba solo la tabla (sesión 1 del laboratorio)
# make test-spell prueba solo el corrector (sesión 2 del laboratorio)
# make test-lab prueba todo su laboratorio; falla hasta que lo terminen
# make bench mide el corrector con cuatro diseños, con -O2 y sin sanitizers
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make: los compilan las
# pruebas, y ustedes, con los comandos de los apuntes.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
LAB_DIR ?= lab
BUILD = build
DATA ?= ../../data
PROGRAMS = probe_trace probe_order compact_dict group_match test_demos
BENCHES = probing_counts tombstone_counts swiss_counts layout_bench
COMMON_HEADERS = bench.h mini_swiss.h demo_tables.h
LAB_HEADERS = $(LAB_DIR)/probing_hash_map.h $(LAB_DIR)/hash_map.h $(LAB_DIR)/churn_test.h \
$(LAB_DIR)/dictionaries.h $(LAB_DIR)/spell_checker.h lab/utf8.h lab/load_words.h lab/not_implemented.h
.PHONY: all lab test test-table test-spell 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 código de esa carpeta.
$(BUILD)/$(LAB_DIR)/map_tests: lab/tests/map_tests.cpp lab/tests/map_suite.h $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab -Ilab/tests -I. $< -o $@
$(BUILD)/$(LAB_DIR)/spell_tests: lab/tests/spell_tests.cpp $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab -Ilab/tests -I. $< -o $@
$(BUILD)/$(LAB_DIR)/spell: lab/tests/spell.cpp $(LAB_HEADERS) | $(BUILD)/$(LAB_DIR)
$(CXX) $(BENCHFLAGS) -I$(LAB_DIR) -Ilab -I. $< -o $@
$(BUILD)/$(LAB_DIR)/bench_spell: lab/tests/bench_spell.cpp lab/tests/bytes_counter.h $(LAB_HEADERS) bench.h | $(BUILD)/$(LAB_DIR)
$(CXX) $(BENCHFLAGS) -I$(LAB_DIR) -Ilab -Ilab/tests -I. $< -o $@
$(BUILD) $(BUILD)/$(LAB_DIR):
mkdir -p $@
lab: $(BUILD)/$(LAB_DIR)/map_tests $(BUILD)/$(LAB_DIR)/spell_tests $(BUILD)/$(LAB_DIR)/spell $(BUILD)/$(LAB_DIR)/bench_spell
# 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-table: all lab
LAB_DIR=$(LAB_DIR) DATA=$(DATA) python3 -m unittest -v test_lab.TestTable
test-spell: all lab
LAB_DIR=$(LAB_DIR) DATA=$(DATA) python3 -m unittest -v test_lab.TestSpell
test-lab: all lab
LAB_DIR=$(LAB_DIR) DATA=$(DATA) python3 -m unittest -v test_lab
bench: $(BUILD)/$(LAB_DIR)/bench_spell
$< $(DATA)/palabras.txt $(DATA)/tradiciones.txt
clean:
rm -rf $(BUILD) __pycache__