Semana 7 — Tablas hash
Objetivos
- Explicar el tipo abstracto diccionario y por qué una tabla hash lo implementa rápido.
- Elegir y evaluar una función hash para cadenas.
- Implementar una tabla hash con encadenamiento que crece.
Teoría — Tablas hash: funciones hash y encadenamiento
Lectura: Apuntes, semana 7.
Apoyo: Open Data Structures, secs. 5.1 (ChainedHashTable) y 5.3 (Hash Codes). CLRS, capítulo Hash Tables, 4.ª edición.
Temas:
- El diccionario: guardar, buscar y borrar por clave. Dónde lo han usado ya: la tabla de variables del intérprete de Introducción a la Programación.
- Del arreglo indexado por clave a la función hash: de una clave grande a un índice pequeño.
- Colisiones: son inevitables.
- Qué pide una buena función hash: determinista, rápida, que reparta bien. Sumar los caracteres, por qué falla, y FNV-1a como alternativa.
- Encadenamiento: una lista por casilla. Factor de carga α = n/m, y costo esperado O(1 + α) si la función reparte bien. Con intuición: la probabilidad formal viene en otro curso.
- Crecer: al superar un factor de carga, duplicar y reinsertar todo. Otra vez costo amortizado.
- El peor caso sigue siendo Θ(n), y hay quien lo provoca a propósito con claves elegidas.
std::unordered_mapystd::hash. Por qué sus claves no salen ordenadas.- Error común: modificar una clave que ya está en la tabla.
Laboratorio — La tabla de variables, ahora como tabla hash
Esta semana una sesión es para la Práctica 2.
Sesión 1:
- Escribir
HashMap<V>, con clavesstd::string:set,get,containsyremove, con encadenamiento y crecimiento. - Probarla con operaciones al azar contra
std::unordered_map, con ASan. - Usarla como tabla de variables de un evaluador pequeño dado.
- Con un archivo de palabras, comparar el largo de las listas de cada casilla con la función de sumar caracteres y con FNV-1a.
Sesión 2: Práctica 2.
Entregas
- Práctica 2: Arreglos dinámicos, colas, listas enlazadas, deques y ordenamiento. Ver
practicas/README.md.
Preguntas de repaso
- ¿Por qué sumar los códigos de los caracteres es una mala función hash? Den dos palabras que colisionen.
- Una tabla con 100 casillas guarda 300 claves, con encadenamiento. ¿Cuál es el factor de carga? ¿Cuántas comparaciones esperan en una búsqueda?
- ¿Por qué al crecer hay que reinsertar todo, y no basta con copiar las casillas?
- ¿Cuándo preferirían un arreglo ordenado con búsqueda binaria en vez de una tabla hash?
- ¿Qué tiene que cumplir una función hash con dos claves iguales?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (16)Errores (4)Pruebas (4)Laboratorio (15)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// Encadenamiento en su forma más simple: número fijo de casillas, sin borrar y sin crecer.
// HashMap, la del laboratorio, agrega lo que aquí falta.
#ifndef CHAIN_TABLE_H
#define CHAIN_TABLE_H
#include <cassert>
#include <cstddef>
#include <string>
#include <vector>
#include "hash_functions.h"
// Invariantes:
// - Cada nodo de la cadena i cumple hash_(key) % capacity_ == i.
// - Una clave aparece en un solo nodo.
// - size_ es el total de nodos.
template <typename V>
class ChainTable {
public:
ChainTable(std::size_t capacity, HashFunction hash)
: slots_(new Node*[capacity]()), capacity_(capacity), hash_(hash) {
assert(capacity > 0);
}
~ChainTable() {
for (std::size_t i = 0; i < capacity_; i++) {
Node* node = slots_[i];
while (node != nullptr) {
Node* next = node->next;
delete node;
node = next;
}
}
delete[] slots_;
}
// La tabla es owner de sus nodos: para no repetir la regla de cinco, aquí no se copia.
ChainTable(const ChainTable&) = delete;
ChainTable& operator=(const ChainTable&) = delete;
void set(const std::string& key, const V& value) {
Node* node = find_node(key);
if (node != nullptr) {
node->value = value;
return;
}
std::size_t slot = slot_of(key);
slots_[slot] = new Node{key, value, slots_[slot]};
size_++;
}
const V* get(const std::string& key) const {
Node* node = find_node(key);
return node == nullptr ? nullptr : &node->value;
}
std::size_t size() const { return size_; }
std::size_t capacity() const { return capacity_; }
double load_factor() const { return static_cast<double>(size_) / capacity_; }
// Cuántas claves hay que comparar para buscar esta clave. No es parte del diccionario:
// está para medir.
std::size_t comparisons_for(const std::string& key) const {
std::size_t count = 0;
for (Node* node = slots_[slot_of(key)]; node != nullptr; node = node->next) {
count++;
if (node->key == key) {
break;
}
}
return count;
}
std::size_t chain_length(std::size_t slot) const {
std::size_t length = 0;
for (Node* node = slots_[slot]; node != nullptr; node = node->next) {
length++;
}
return length;
}
// Las claves de una cadena, en orden. Está para mostrar la tabla en los ejemplos.
std::vector<std::string> chain_keys(std::size_t slot) const {
std::vector<std::string> keys;
for (Node* node = slots_[slot]; node != nullptr; node = node->next) {
keys.push_back(node->key);
}
return keys;
}
bool check_invariants() const {
std::size_t total = 0;
for (std::size_t i = 0; i < capacity_; i++) {
for (Node* node = slots_[i]; node != nullptr; node = node->next) {
total++;
if (slot_of(node->key) != i) {
return false;
}
for (Node* other = node->next; other != nullptr; other = other->next) {
if (other->key == node->key) {
return false;
}
}
}
}
return total == size_;
}
private:
struct Node {
std::string key;
V value;
Node* next;
};
std::size_t slot_of(const std::string& key) const {
return hash_(key) % capacity_;
}
Node* find_node(const std::string& key) const {
for (Node* node = slots_[slot_of(key)]; node != nullptr; node = node->next) {
if (node->key == key) {
return node;
}
}
return nullptr;
}
Node** slots_;
std::size_t capacity_;
std::size_t size_ = 0;
HashFunction hash_;
};
#endif// Muestra las cadenas de una tabla, paso a paso. Se inserta al inicio de la cadena.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
#include "chain_table.h"
void print_table(const ChainTable<int>& table) {
for (std::size_t i = 0; i < table.capacity(); i++) {
std::cout << " " << i << ":";
for (const std::string& key : table.chain_keys(i)) {
std::cout << " " << key;
}
std::cout << "\n";
}
}
int main() {
std::cout << "Con la suma y 8 casillas\n";
ChainTable<int> small(8, sum_hash);
for (const std::string word : {"lima", "amor", "roma", "ad", "bc", "mora"}) {
small.set(word, 1);
std::cout << "set(" << word << "): suma " << sum_hash(word) << ", casilla " << sum_hash(word) % 8 << "\n";
}
print_table(small);
std::cout << "comparaciones para mora: " << small.comparisons_for("mora") << ", para amor: "
<< small.comparisons_for("amor") << ", para ramo: " << small.comparisons_for("ramo") << "\n";
const std::vector<std::string> cities = {"lima", "cusco", "puno", "tacna", "ica", "piura"};
for (std::size_t capacity : {4u, 8u}) {
std::cout << "Con FNV-1a y " << capacity << " casillas\n";
ChainTable<int> table(capacity, fnv1a);
for (const std::string& city : cities) {
table.set(city, 1);
}
print_table(table);
}
}// ¿Cuántas claves entran antes de la primera colisión, con m casillas?
// Uso: first_collision archivo_de_palabras
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include "hash_functions.h"
#include "word_file.h"
// Inserta palabras en orden hasta que dos caigan en la misma casilla.
std::size_t keys_until_collision(const std::vector<std::string>& words, std::size_t m) {
std::vector<bool> taken(m, false);
for (std::size_t i = 0; i < words.size(); i++) {
std::size_t slot = fnv1a(words[i]) % m;
if (taken[slot]) {
return i + 1;
}
taken[slot] = true;
}
return words.size();
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: first_collision archivo_de_palabras\n";
return 1;
}
std::vector<std::string> words = read_words(argv[1]);
std::mt19937 rng(7);
std::printf("%-11s %-8s %-8s %s\n", "m", "mediana", "raiz(m)", "repeticiones");
for (std::size_t m : {1000u, 100000u, 1000000u, 10000000u}) {
const int reps = 51;
std::vector<std::size_t> counts;
for (int r = 0; r < reps; r++) {
// Basta barajar el comienzo: nunca llegamos a usar toda la lista.
for (std::size_t i = 0; i < 20000 && i < words.size(); i++) {
std::uniform_int_distribution<std::size_t> pick(i, words.size() - 1);
std::swap(words[i], words[pick(rng)]);
}
counts.push_back(keys_until_collision(words, m));
}
std::sort(counts.begin(), counts.end());
std::printf("%-11zu %-8zu %-8.0f %d\n", m, counts[reps / 2], std::sqrt(static_cast<double>(m)), reps);
}
}// Claves elegidas a propósito para que caigan todas en la misma casilla.
// Las tablas no tienen defensa: la función hash es pública y no usa ningún secreto.
#include <cstddef>
#include <cstdio>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
#include "bench.h"
#include "chain_table.h"
// Las primeras claves "k0", "k1", ... cuyo hash es múltiplo de divisor.
template <typename Hash>
std::vector<std::string> crafted_keys(std::size_t count, std::size_t divisor, Hash hash) {
std::vector<std::string> keys;
for (std::size_t i = 0; keys.size() < count; i++) {
std::string key = "k" + std::to_string(i);
if (hash(key) % divisor == 0) {
keys.push_back(key);
}
}
return keys;
}
std::vector<std::string> plain_keys(std::size_t count) {
std::vector<std::string> keys;
for (std::size_t i = 0; i < count; i++) {
keys.push_back("k" + std::to_string(i));
}
return keys;
}
void own_table(const char* name, const std::vector<std::string>& keys, std::size_t m) {
ChainTable<int> table(m, fnv1a);
for (const std::string& key : keys) {
table.set(key, 1);
}
std::size_t longest = 0;
for (std::size_t i = 0; i < m; i++) {
longest = std::max(longest, table.chain_length(i));
}
double total = 0;
for (const std::string& key : keys) {
total += static_cast<double>(table.comparisons_for(key));
}
std::printf("%-10s m=%-5zu la mayor cadena %5zu, comparaciones por busqueda %8.1f\n", name, m, longest,
total / static_cast<double>(keys.size()));
}
void library_table(const char* name, const std::vector<std::string>& keys, std::size_t buckets) {
std::size_t largest = 0;
double seconds = median_seconds(5, [&] {
std::unordered_map<std::string, int> table;
table.reserve(buckets);
for (const std::string& key : keys) {
table[key] = 1;
}
largest = table.bucket_size(table.bucket(keys[0]));
keep(table.size());
});
std::printf("%-10s la casilla de keys[0] tiene %5zu, %9.2f ms\n", name, largest, seconds * 1000);
}
int main() {
const std::size_t n = 4000;
// Si el hash es múltiplo de 8192, también lo es de 4096, 2048, ... Crecer hasta 8192 no ayuda.
std::vector<std::string> crafted = crafted_keys(n, 8192, fnv1a);
std::printf("%zu claves; la ultima elegida es %s\n", n, crafted.back().c_str());
own_table("normales", plain_keys(n), 4096);
own_table("elegidas", crafted, 4096);
own_table("elegidas", crafted, 8192);
std::unordered_map<std::string, int> probe;
probe.reserve(n);
std::size_t buckets = probe.bucket_count();
std::printf("unordered_map con reserve(%zu): bucket_count %zu\n", n, buckets);
std::vector<std::string> against_library = crafted_keys(n, buckets, std::hash<std::string>{});
library_table("normales", plain_keys(n), n);
library_table("elegidas", against_library, n);
}// Sigue FNV-1a byte por byte: un cambio en un byte afecta a todos los que vienen después.
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>
void trace(const std::string& key) {
std::uint64_t hash = 14695981039346656037ULL;
std::cout << "clave \"" << key << "\"\n" << std::hex << std::setfill('0');
std::cout << " inicio " << std::setw(16) << hash << "\n";
for (char c : key) {
hash ^= static_cast<unsigned char>(c);
std::cout << " xor " << std::setw(2) << static_cast<int>(static_cast<unsigned char>(c))
<< " " << std::setw(16) << hash << "\n";
hash *= 1099511628211ULL;
std::cout << " por el primo " << std::setw(16) << hash << "\n";
}
std::cout << std::dec << std::setfill(' ');
}
int main() {
trace("ab");
trace("ba");
}// Cuánto se reinserta en total si se duplica al superar α = 1, empezando con 8 casillas.
#include <cstddef>
#include <cstdio>
#include <initializer_list>
int main() {
std::printf("%10s %10s %12s %12s\n", "n", "casillas", "reinsertadas", "por_insercion");
for (std::size_t n : {1000u, 10000u, 100000u, 1000000u, 1048576u, 1048577u}) {
std::size_t capacity = 8;
std::size_t moved = 0;
for (std::size_t size = 0; size < n; size++) {
// Antes de insertar la clave número size + 1.
if (size + 1 > capacity) {
moved += size;
capacity *= 2;
}
}
std::printf("%10zu %10zu %12zu %12.2f\n", n, capacity, moved, static_cast<double>(moved) / static_cast<double>(n));
}
}// Cuánto se usa cada casilla con cada función hash, sin construir ninguna tabla.
// Uso: hash_compare archivo_de_palabras
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include "hash_functions.h"
#include "word_file.h"
void report(const char* name, const std::vector<std::string>& words, std::size_t m, HashFunction hash) {
std::vector<std::size_t> lengths(m, 0);
for (const std::string& word : words) {
lengths[hash(word) % m]++;
}
std::size_t used = 0;
std::size_t longest = 0;
double comparisons = 0;
for (std::size_t length : lengths) {
used += length > 0 ? 1 : 0;
longest = std::max(longest, length);
// En una cadena de c claves, buscar cada una cuesta 1, 2, ..., c comparaciones.
comparisons += static_cast<double>(length) * static_cast<double>(length + 1) / 2;
}
std::printf("%-6s %9zu %6.2f %9zu %9zu %11.1f\n", name, m, static_cast<double>(words.size()) / static_cast<double>(m),
used, longest, comparisons / static_cast<double>(words.size()));
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: hash_compare archivo_de_palabras\n";
return 1;
}
std::vector<std::string> words = read_words(argv[1]);
std::set<std::uint64_t> sums;
for (const std::string& word : words) {
sums.insert(sum_hash(word));
}
std::printf("%zu palabras; la suma da %zu valores distintos, de %llu a %llu\n", words.size(), sums.size(),
static_cast<unsigned long long>(*sums.begin()), static_cast<unsigned long long>(*sums.rbegin()));
std::printf("%-6s %9s %6s %9s %9s %11s\n", "hash", "m", "alfa", "usadas", "la_mayor", "comp_acierto");
for (std::size_t m : {1024u, 32768u, 1048576u}) {
report("suma", words, m, sum_hash);
report("fnv1a", words, m, fnv1a);
}
}// Las dos funciones hash sobre unas pocas palabras, y la casilla que le toca a cada una.
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "hash_functions.h"
int main() {
const std::vector<std::string> words = {"amor", "roma", "mora", "ramo", "ad", "bc", "lima", "cusco"};
std::cout << "palabra suma suma%8 fnv1a fnv1a%8\n";
for (const std::string& word : words) {
std::cout << std::left << std::setw(9) << word << std::setw(6) << sum_hash(word)
<< std::setw(8) << sum_hash(word) % 8 << std::hex << std::setfill('0')
<< std::right << std::setw(16) << fnv1a(word) << std::dec << std::setfill(' ')
<< " " << fnv1a(word) % 8 << "\n";
}
}// Dos funciones hash para cadenas: la que falla y la que usaremos.
#ifndef HASH_FUNCTIONS_H
#define HASH_FUNCTIONS_H
#include <cstdint>
#include <string>
// Puntero a una función hash: las tablas de esta carpeta la reciben al construirse.
using HashFunction = std::uint64_t (*)(const std::string&);
// Suma los bytes de la clave. Es determinista y rápida, y reparte muy mal.
inline std::uint64_t sum_hash(const std::string& key) {
std::uint64_t hash = 0;
for (char c : key) {
hash += static_cast<unsigned char>(c);
}
return hash;
}
// FNV-1a de 64 bits, con las constantes oficiales.
inline std::uint64_t fnv1a(const std::string& key) {
std::uint64_t hash = 14695981039346656037ULL;
for (char c : key) {
hash ^= static_cast<unsigned char>(c);
hash *= 1099511628211ULL;
}
return hash;
}
#endif// std::unordered_map por dentro, hasta donde la interfaz deja mirar.
#include <cstddef>
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
void print_order(const std::unordered_map<std::string, int>& table) {
for (const auto& entry : table) {
std::cout << " " << entry.first;
}
std::cout << "\n";
}
int main() {
std::unordered_map<std::string, int> sizes;
std::cout << "al inicio: bucket_count " << sizes.bucket_count() << ", max_load_factor "
<< sizes.max_load_factor() << "\n";
std::cout << "crece en n =";
std::size_t buckets = sizes.bucket_count();
std::vector<std::size_t> history = {buckets};
for (int i = 0; i < 2000; i++) {
sizes["k" + std::to_string(i)] = i;
if (sizes.bucket_count() != buckets) {
buckets = sizes.bucket_count();
std::cout << " " << sizes.size();
history.push_back(buckets);
}
}
std::cout << "\nbucket_count:";
for (std::size_t value : history) {
std::cout << " " << value;
}
std::cout << "\ncon n = " << sizes.size() << ": load_factor " << sizes.load_factor() << "\n";
std::unordered_map<std::string, int> cities;
for (const std::string city : {"lima", "cusco", "puno", "tacna", "ica", "piura"}) {
cities[city] = static_cast<int>(city.size());
}
std::cout << "orden:";
print_order(cities);
cities.rehash(100);
std::cout << "tras rehash(100), bucket_count " << cities.bucket_count() << ":";
print_order(cities);
std::cout << "contains(\"ica\") " << cities.contains("ica") << ", contains(\"nazca\") "
<< cities.contains("nazca") << ", size " << cities.size() << "\n";
std::cout << "cities[\"nazca\"] " << cities["nazca"] << ", size " << cities.size() << "\n";
std::cout << "hash(\"lima\") = " << std::hash<std::string>{}("lima") << "\n";
}// Cuenta las comparaciones de una búsqueda con distintos factores de carga.
// Uso: load_experiment archivo_de_palabras
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include "chain_table.h"
#include "word_file.h"
void measure(const std::vector<std::string>& words, std::size_t m, std::size_t n) {
ChainTable<int> table(m, fnv1a);
for (std::size_t i = 0; i < n; i++) {
table.set(words[i], 1);
}
// Las n primeras están en la tabla. Las n siguientes, no.
double hits = 0;
double misses = 0;
for (std::size_t i = 0; i < n; i++) {
hits += static_cast<double>(table.comparisons_for(words[i]));
misses += static_cast<double>(table.comparisons_for(words[n + i]));
}
double alpha = table.load_factor();
double count = static_cast<double>(n);
std::printf("%6zu %7zu %6.2f %9.2f %9.2f %9.2f %9.2f\n", m, n, alpha, hits / count, 1 + alpha / 2, misses / count, alpha);
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: load_experiment archivo_de_palabras\n";
return 1;
}
std::vector<std::string> words = read_words(argv[1]);
std::mt19937 rng(11);
std::shuffle(words.begin(), words.end(), rng);
std::printf("%6s %7s %6s %9s %9s %9s %9s\n", "m", "n", "alfa", "acierto", "1+alfa/2", "fallo", "alfa");
measure(words, 100, 300);
for (std::size_t n : {512u, 1024u, 2048u, 3072u, 5120u, 10240u, 102400u}) {
measure(words, 1024, n);
}
}// Arreglo ordenado con búsqueda binaria contra std::unordered_map, sobre el mismo diccionario.
// Uso: lookup_bench archivo_de_palabras_ordenado
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <new>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
#include "bench.h"
#include "word_file.h"
namespace {
std::size_t live_bytes = 0;
// Cada bloque guarda su tamaño al inicio, para restarlo al liberarlo.
const std::size_t HEADER = 16;
}
// Cuenta los bytes reservados y todavía sin liberar, para comparar lo que ocupa cada estructura.
void* operator new(std::size_t size) {
void* block = std::malloc(size + HEADER);
if (block == nullptr) {
throw std::bad_alloc();
}
*static_cast<std::size_t*>(block) = size;
live_bytes += size;
return static_cast<char*>(block) + HEADER;
}
void operator delete(void* pointer) noexcept {
if (pointer != nullptr) {
void* block = static_cast<char*>(pointer) - HEADER;
live_bytes -= *static_cast<std::size_t*>(block);
std::free(block);
}
}
void operator delete(void* pointer, std::size_t) noexcept {
operator delete(pointer);
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: lookup_bench archivo_de_palabras_ordenado\n";
return 1;
}
std::size_t before = live_bytes;
const std::vector<std::string> words = read_words(argv[1]);
std::size_t vector_bytes = live_bytes - before;
if (!std::is_sorted(words.begin(), words.end())) {
std::cerr << "el archivo debe estar ordenado por bytes\n";
return 1;
}
before = live_bytes;
std::unordered_map<std::string, int> table;
double build_seconds = median_seconds(1, [&] {
for (std::size_t i = 0; i < words.size(); i++) {
table[words[i]] = static_cast<int>(i);
}
});
std::size_t table_bytes = live_bytes - before;
// Mismas consultas para las dos, en un orden que no favorece a ninguna.
std::mt19937 rng(5);
std::uniform_int_distribution<std::size_t> pick(0, words.size() - 1);
std::vector<std::string> queries;
for (int i = 0; i < 200000; i++) {
queries.push_back(words[pick(rng)]);
}
double sorted_seconds = median_seconds(5, [&] {
std::size_t hits = 0;
for (const std::string& query : queries) {
auto it = std::lower_bound(words.begin(), words.end(), query);
hits += (it != words.end() && *it == query) ? 1 : 0;
}
keep(hits);
});
double table_seconds = median_seconds(5, [&] {
std::size_t hits = 0;
for (const std::string& query : queries) {
hits += table.contains(query) ? 1 : 0;
}
keep(hits);
});
std::size_t comparisons = 0;
for (const std::string& query : queries) {
auto it = std::lower_bound(words.begin(), words.end(), query, [&](const std::string& a, const std::string& b) {
comparisons++;
return a < b;
});
keep(it == words.end());
}
double chain_cost = 0;
for (std::size_t b = 0; b < table.bucket_count(); b++) {
double length = static_cast<double>(table.bucket_size(b));
chain_cost += length * (length + 1) / 2;
}
double count = static_cast<double>(queries.size());
std::printf("%zu palabras, %zu consultas\n", words.size(), queries.size());
std::printf("%-22s %12s %16s %10s\n", "", "ns/consulta", "comparaciones", "MB");
std::printf("%-22s %12.0f %16.1f %10.1f\n", "arreglo ordenado", sorted_seconds * 1e9 / count,
static_cast<double>(comparisons) / count, static_cast<double>(vector_bytes) / 1e6);
std::printf("%-22s %12.0f %16.1f %10.1f\n", "std::unordered_map", table_seconds * 1e9 / count,
chain_cost / static_cast<double>(words.size()), static_cast<double>(table_bytes) / 1e6);
std::printf("construir la tabla: %.0f ms\n", build_seconds * 1000);
// Lo que la tabla no sabe hacer: un rango de claves.
auto first = std::lower_bound(words.begin(), words.end(), std::string("cas"));
auto last = std::lower_bound(words.begin(), words.end(), std::string("cat"));
std::printf("empiezan con cas: %td, de %s a %s\n", last - first, first->c_str(), (last - 1)->c_str());
std::size_t scanned = 0;
for (const auto& entry : table) {
scanned += entry.first.starts_with("cas") ? 1 : 0;
}
std::printf("la tabla debe revisar %zu claves para hallar las mismas %zu\n", table.size(), scanned);
// El orden es por bytes: las palabras con tilde inicial quedan después de la z.
auto a_accent = std::lower_bound(words.begin(), words.end(), std::string("á"));
auto b_start = std::lower_bound(words.begin(), words.end(), std::string("b"));
std::printf("antes de la primera b: %s; ultima antes de la primera con a acentuada: %s; primera: %s\n",
(b_start - 1)->c_str(), (a_accent - 1)->c_str(), a_accent->c_str());
}// Crecer copiando las casillas, sin reinsertar: las claves siguen ahí, y ya no se encuentran.
// Uso: naive_copy archivo_de_palabras
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
#include "hash_functions.h"
#include "word_file.h"
using Slots = std::vector<std::vector<std::string>>;
bool found(const Slots& slots, const std::string& word) {
const std::vector<std::string>& chain = slots[fnv1a(word) % slots.size()];
return std::find(chain.begin(), chain.end(), word) != chain.end();
}
std::size_t count_found(const Slots& slots, const std::vector<std::string>& words) {
std::size_t count = 0;
for (const std::string& word : words) {
count += found(slots, word) ? 1 : 0;
}
return count;
}
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: naive_copy archivo_de_palabras\n";
return 1;
}
std::vector<std::string> words = read_words(argv[1]);
for (const std::string word : {"lima", "cusco", "tacna", "puno", "arequipa"}) {
std::uint64_t hash = fnv1a(word);
std::cout << word << ": hash % 8 = " << hash % 8 << ", hash % 16 = " << hash % 16 << "\n";
}
Slots slots(1024);
for (const std::string& word : words) {
slots[fnv1a(word) % slots.size()].push_back(word);
}
std::cout << "con 1024 casillas: " << count_found(slots, words) << " de " << words.size() << " encontradas\n";
Slots copied = slots;
copied.resize(2048);
std::cout << "copiando a 2048: " << count_found(copied, words) << " de " << words.size() << " encontradas\n";
Slots reinserted(2048);
for (const std::string& word : words) {
reinserted[fnv1a(word) % reinserted.size()].push_back(word);
}
std::cout << "reinsertando: " << count_found(reinserted, words) << " de " << words.size() << " encontradas\n";
}// Tipo para las pruebas: cuenta cuántos objetos viven y cuántas copias y movimientos hubo.
#ifndef TRACKED_H
#define TRACKED_H
#include <utility>
class Tracked {
public:
struct Counts {
long live = 0;
long copies = 0;
long moves = 0;
};
static Counts& counts() {
static Counts values;
return values;
}
Tracked() { counts().live++; }
explicit Tracked(int value) : value_(value) { counts().live++; }
Tracked(const Tracked& other) : value_(other.value_) {
counts().live++;
counts().copies++;
}
Tracked(Tracked&& other) noexcept : value_(std::exchange(other.value_, 0)) {
counts().live++;
counts().moves++;
}
Tracked& operator=(const Tracked& other) {
value_ = other.value_;
counts().copies++;
return *this;
}
Tracked& operator=(Tracked&& other) noexcept {
value_ = std::exchange(other.value_, 0);
counts().moves++;
return *this;
}
~Tracked() { counts().live--; }
int value() const { return value_; }
bool operator==(const Tracked& other) const { return value_ == other.value_; }
private:
int value_ = 0;
};
#endif// Lee una lista de palabras. Si una línea trae un tabulador, como en terminos.tsv
// (frecuencia<TAB>término), la palabra es lo que viene después.
#ifndef WORD_FILE_H
#define WORD_FILE_H
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
inline std::vector<std::string> read_words(const std::string& path) {
std::ifstream file(path);
if (!file) {
std::cerr << "no se pudo abrir " << path << "\n";
std::exit(1);
}
std::vector<std::string> words;
std::string line;
while (std::getline(file, line)) {
std::size_t tab = line.find('\t');
words.push_back(tab == std::string::npos ? line : line.substr(tab + 1));
}
return words;
}
#endifErrores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// Error a propósito: no compila. En std::unordered_map la clave es const.
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> table;
table["lima"] = 10;
for (auto& entry : table) {
entry.first = "cusco";
}
}// Error a propósito: calcular la casilla en una tabla sin casillas, como la que queda después de mover.
#include <cstddef>
#include <iostream>
#include <string>
#include "../hash_functions.h"
int main() {
std::size_t capacity = 0;
std::cout << "casilla: " << fnv1a("lima") % capacity << "\n";
}// Error a propósito: se cambia una clave que ya está en la tabla.
// La tabla sigue teniendo el dato, pero en una casilla que ya no le corresponde.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
#include "../hash_functions.h"
class LeakyTable {
public:
void set(const std::string& key, int value) {
slots_[fnv1a(key) % slots_.size()].push_back(Entry{key, value});
}
const int* get(const std::string& key) const {
for (const Entry& entry : slots_[fnv1a(key) % slots_.size()]) {
if (entry.key == key) {
return &entry.value;
}
}
return nullptr;
}
// El error de diseño: entrega la clave sin const.
std::string* key_of(const std::string& key) {
for (Entry& entry : slots_[fnv1a(key) % slots_.size()]) {
if (entry.key == key) {
return &entry.key;
}
}
return nullptr;
}
std::size_t size() const {
std::size_t total = 0;
for (const std::vector<Entry>& slot : slots_) {
total += slot.size();
}
return total;
}
bool check_invariants() const {
for (std::size_t i = 0; i < slots_.size(); i++) {
for (const Entry& entry : slots_[i]) {
if (fnv1a(entry.key) % slots_.size() != i) {
return false;
}
}
}
return true;
}
private:
struct Entry {
std::string key;
int value;
};
std::vector<std::vector<Entry>> slots_ = std::vector<std::vector<Entry>>(8);
};
int main() {
LeakyTable table;
table.set("lima", 10);
std::cout << "invariantes: " << table.check_invariants() << "\n";
*table.key_of("lima") = "cusco";
std::cout << "size: " << table.size() << "\n";
std::cout << "get(\"lima\"): " << (table.get("lima") == nullptr ? "no está" : "está") << "\n";
std::cout << "get(\"cusco\"): " << (table.get("cusco") == nullptr ? "no está" : "está") << "\n";
std::cout << "invariantes: " << table.check_invariants() << "\n";
}// Error a propósito: sumar los char sin convertirlos a unsigned char.
// Compilen con -funsigned-char y con -fsigned-char: según la plataforma, char tiene signo o no.
#include <cstdint>
#include <iostream>
#include <string>
std::uint64_t bad_sum(const std::string& key) {
std::uint64_t hash = 0;
for (char c : key) {
hash += c;
}
return hash;
}
int main() {
std::cout << "bad_sum(\"lima\") = " << bad_sum("lima") << "\n";
std::cout << "bad_sum(\"ñandú\") = " << bad_sum("ñandú") << "\n";
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos absolutos."""
import os
import pathlib
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
DATA = os.environ.get("DATA", "../../data")
SAN = ["g++", "-Wall", "-Wextra", "-std=c++20", "-g", "-fsanitize=address,undefined"]
def run(*command):
return subprocess.run(
list(command), cwd=HERE, text=True, encoding="utf-8", stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
class Examples(unittest.TestCase):
def test_structures_pass(self):
done = run("build/test_structures")
self.assertEqual(done.returncode, 0, done.stdout)
def test_hashes_with_signed_char(self):
"""El mismo resultado aunque char tenga signo, como en x86-64."""
done = run(*SAN, "-fsigned-char", "test_structures.cpp", "-o", "build/test_structures_signed")
self.assertEqual(done.returncode, 0, done.stdout)
done = run("build/test_structures_signed")
self.assertEqual(done.returncode, 0, done.stdout)
def test_sum_hash_uses_few_values(self):
lines = run("build/hash_compare", f"{DATA}/terminos.tsv").stdout.splitlines()
rows = [line.split() for line in lines[2:]]
by_m = {}
for row in rows:
by_m.setdefault(row[1], {})[row[0]] = row
# Más casillas no ayudan a la suma: se queda con los mismos valores.
self.assertEqual(by_m["32768"]["suma"][3], by_m["1048576"]["suma"][3])
self.assertGreater(int(by_m["1048576"]["suma"][4]), 20 * int(by_m["1048576"]["fnv1a"][4]))
def test_load_experiment_follows_the_formula(self):
rows = [line.split() for line in run("build/load_experiment", f"{DATA}/palabras.txt").stdout.splitlines()[1:]]
self.assertEqual(rows[0][:3], ["100", "300", "3.00"])
for row in rows:
alpha, hit, hit_formula, miss = float(row[2]), float(row[3]), float(row[4]), float(row[5])
with self.subTest(alpha=alpha):
self.assertLess(abs(hit - hit_formula), 0.25 + 0.05 * alpha)
self.assertLess(abs(miss - alpha), 0.25 + 0.1 * alpha)
def test_naive_copy_loses_about_half(self):
lines = run("build/naive_copy", f"{DATA}/terminos.tsv").stdout.splitlines()
counts = [line.split(":")[1].split() for line in lines if "encontradas" in line]
total = int(counts[0][2])
self.assertEqual(int(counts[0][0]), total)
self.assertEqual(int(counts[2][0]), total)
self.assertLess(abs(int(counts[1][0]) / total - 0.5), 0.05)
def test_flood_puts_every_key_in_one_slot(self):
lines = run("build/flood").stdout.splitlines()
self.assertTrue(any("elegidas" in line and "m=4096" in line and "cadena 4000" in line for line in lines))
self.assertTrue(any("elegidas" in line and "m=8192" in line and "cadena 4000" in line for line in lines))
self.assertTrue(any("elegidas" in line and "tiene 4000" in line for line in lines))
def test_hash_is_stable_within_this_library(self):
lines = [run("build/library_demo").stdout.splitlines()[-1] for _ in range(2)]
self.assertEqual(lines[0], lines[1])
def test_programs_exit_cleanly(self):
for name in ["hash_demo", "fnv_trace", "chain_trace", "growth_count", "library_demo"]:
with self.subTest(program=name):
self.assertEqual(run(f"build/{name}").returncode, 0)
class DeliberateErrors(unittest.TestCase):
def test_mutated_key(self):
run(*SAN, "errors/mutated_key.cpp", "-o", "build/mutated_key")
done = run("build/mutated_key")
self.assertEqual(done.returncode, 0, done.stdout)
self.assertIn('get("lima"): no está', done.stdout)
self.assertIn("invariantes: 0", done.stdout)
def test_modulo_zero(self):
run(*SAN, "errors/modulo_zero.cpp", "-o", "build/modulo_zero")
done = run("build/modulo_zero")
self.assertIn("division by zero", done.stdout)
def test_const_key_does_not_compile(self):
done = run("g++", "-std=c++20", "-c", "errors/const_key.cpp", "-o", "build/const_key.o")
self.assertNotEqual(done.returncode, 0)
self.assertIn("no match for 'operator='", done.stdout.replace("‘", "'").replace("’", "'"))
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está su trabajo: lab o solution.
Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido
la hace fallar aunque el resultado parezca correcto.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = os.environ.get("DATA", "../../data")
OUT = HERE / "build" / LAB_DIR
MAP_TESTS = [
"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", "run_map_suite",
"capacity_doubles_past_one", "overwrite_does_not_grow", "remove_does_not_shrink", "long_chains",
"lower_load_factor_grows_now", "growth_allocates_only_slots", "growth_copies_no_values",
"pointers_survive_growth", "value_from_the_same_table", "value_without_default_constructor",
"no_objects_left_behind", "moves_are_noexcept",
]
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"(hash_map|var_table|chain_lengths|map_suite)\.h:\d+", line)]
return "\n".join(useful[:12])
def run(*command):
return subprocess.run(
[str(part) for part in command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
def build(*targets):
return run("make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", *[f"build/{LAB_DIR}/{t}" for t in targets])
@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 name in ["map_tests", "hash_tests", "calc", "chain_report"]:
(OUT / name).unlink(missing_ok=True)
done = build("map_tests", "hash_tests", "calc", "chain_report")
self.assertEqual(done.returncode, 0, done.stdout)
# Solo interesan las advertencias del compilador, no las de make por el reloj.
compiler = [line for line in done.stdout.splitlines() if not line.startswith("make")]
self.assertNotIn("warning", "\n".join(compiler))
class BuiltFirst(unittest.TestCase):
@classmethod
def setUpClass(cls):
done = build("map_tests", "hash_tests", "calc", "chain_report")
if done.returncode != 0:
raise AssertionError(done.stdout)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestMap(BuiltFirst):
"""Pasos 1 y 2."""
for test_name in MAP_TESTS:
def map_case(self, name=test_name):
done = run(OUT / "map_tests", name)
self.assertEqual(done.returncode, 0, short_report(done.stdout))
setattr(TestMap, f"test_{test_name}", map_case)
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestCalc(BuiltFirst):
"""Paso 3."""
def run_program(self, text):
with tempfile.NamedTemporaryFile("w", suffix=".calc", encoding="utf-8", delete=False) as file:
file.write(text)
try:
return run(OUT / "calc", file.name)
finally:
os.unlink(file.name)
def test_example_program(self):
done = run(OUT / "calc", "lab/tests/program.calc")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
self.assertEqual(done.stdout, (HERE / "lab/tests/program.expected").read_text(encoding="utf-8"))
def test_undefined_variable(self):
done = self.run_program("x = 1\nprint y\n")
self.assertEqual(done.returncode, 1, short_report(done.stdout))
self.assertIn("error en la línea 2: variable 'y' sin definir", done.stdout)
def test_delete_then_read(self):
done = self.run_program("x = 1\ndel x\nprint x\n")
self.assertEqual(done.returncode, 1, short_report(done.stdout))
self.assertIn("error en la línea 3: variable 'x' sin definir", done.stdout)
def test_delete_absent(self):
done = self.run_program("del x\n")
self.assertEqual(done.returncode, 1, short_report(done.stdout))
self.assertIn("no se puede borrar 'x'", done.stdout)
def test_many_variables_force_growth(self):
lines = [f"v{i} = {i}" for i in range(5000)]
lines += ["total = " + " + ".join(f"v{i}" for i in range(5000)), "print total", "vars"]
done = self.run_program("\n".join(lines) + "\n")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
self.assertEqual(done.stdout.split(), [str(sum(range(5000))), "5001"])
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestHash(BuiltFirst):
"""Las funciones hash y el paso 4. Los conteos salen de una lista pequeña, en lab/tests."""
def test_fnv1a(self):
done = run(OUT / "hash_tests", "fnv1a")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
def test_sum_hash(self):
done = run(OUT / "hash_tests", "sum_hash")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
def test_chain_lengths(self):
done = run(OUT / "hash_tests", "chain_lengths")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
def test_report_on_small_list(self):
done = run(OUT / "chain_report", "lab/tests/words_small.txt")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
lines = done.stdout.splitlines()
self.assertRegex(lines[0], r"palabras 697, distintas en la tabla 697, encontradas 697, casillas 1024")
rows = {line.split()[0]: line.split() for line in lines[2:]}
self.assertEqual(rows["suma"][1:3], ["550", "6"])
self.assertEqual(rows["fnv1a"][1:3], ["525", "4"])
def test_report_on_real_words(self):
"""Con 28 039 términos reales, la suma ya se nota: es un conteo, no un tiempo."""
done = build("chain_report_fast")
self.assertEqual(done.returncode, 0, done.stdout)
done = run(OUT / "chain_report_fast", f"{DATA}/terminos.tsv")
self.assertEqual(done.returncode, 0, short_report(done.stdout))
rows = {line.split()[0]: line.split() for line in done.stdout.splitlines()[2:]}
self.assertEqual(rows["suma"][2], "100")
self.assertEqual(rows["fnv1a"][2], "6")
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import os
import pathlib
import platform
import re
import subprocess
import unittest
try:
import pty
except ImportError:
pty = None
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-07.md"
SLIDES = ROOT / "slides" / "week-07.tex"
# Antes de un bloque cuya salida cambia con el sistema. Se corre, pero no se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe de ASan. Puede omitir líneas, pero no cambiarlas ni reordenarlas.
# Las direcciones y los números de proceso no se comparan.
REPORT = "<!-- informe recortado -->"
# Los informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
)
return normalize(done.stdout)
def run_in_terminal(command):
"""Corre el comando en una terminal de verdad: así se intercala la salida del programa
con el informe, igual que cuando ustedes lo corren."""
master, slave = pty.openpty()
process = subprocess.Popen(
["bash", "-c", command], cwd=HERE, stdin=subprocess.DEVNULL, stdout=slave, stderr=slave,
)
os.close(slave)
data = b""
while True:
try:
chunk = os.read(master, 4096)
except OSError:
break
if not chunk:
break
data += chunk
os.close(master)
process.wait(timeout=120)
text = data.decode("utf-8", errors="replace").replace("\r", "")
return normalize(re.sub(r"\x1b\[[0-9;]*[mK]", "", text))
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def stable(line):
"""Quita de una línea de informe lo que cambia en cada corrida."""
line = re.sub(r"0x[0-9a-f]+", "ADDR", line.strip())
return re.sub(r"==\d+==", "==N==", line)
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.cpp")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.cpp"))
sources += list((HERE / "lab").glob("*.h")) + list((HERE / "lab" / "given").glob("*"))
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 ReportCheck(unittest.TestCase):
def assert_in_order(self, command, expected, actual):
"""Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
real = [stable(line) for line in actual]
position = 0
for line in wanted:
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(real)) if real[i] == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
class NotesAreTrue(ReportCheck):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 5)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark != REPORT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_in_terminal(command))
self.assert_in_order(command, expected, actual)
checked += 1
self.assertGreater(checked, 0)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("cpp"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
class SlidesAreTrue(ReportCheck):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_program_output_in_slides(self):
for block in slide_blocks("terminal|salida"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_shell(command)))
# Algunos informes usan programas que compiló la prueba de los apuntes, que corre antes.
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
@unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
def test_reports_in_slides(self):
for block in slide_blocks("informe"):
for command, expected in split_commands(block):
self.assert_in_order(command, expected, clean_lines(run_in_terminal(command)))
@unittest.skipUnless(NOTES.exists() and SLIDES.exists(), "faltan los apuntes o las diapositivas")
def test_measurements_in_slides_come_from_notes(self):
"""Los tiempos dependen de la máquina: las diapositivas copian los de los apuntes."""
notes = NOTES.read_text(encoding="utf-8").splitlines()
blocks = slide_blocks("medicion")
self.assertGreater(len(blocks), 2)
for block in blocks:
for line in block:
with self.subTest(line=line):
self.assertIn(line, notes)
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()// Pruebas de las funciones hash y de ChainTable: casos dirigidos y operaciones al azar,
// con semilla fija, contra std::unordered_map.
#include <cassert>
#include <cstdint>
#include <iostream>
#include <random>
#include <string>
#include <unordered_map>
#include "chain_table.h"
#include "hash_functions.h"
#include "tracked.h"
namespace {
// Vectores de la página oficial de FNV (test_fnv.c, fnv1a_64_vector).
void test_fnv1a_official_vectors() {
assert(fnv1a("") == 0xcbf29ce484222325ULL);
assert(fnv1a("a") == 0xaf63dc4c8601ec8cULL);
assert(fnv1a("b") == 0xaf63df4c8601f1a5ULL);
assert(fnv1a("foobar") == 0x85944171f73967e8ULL);
// Un byte mayor que 127: falla si no se convierte a unsigned char.
assert(fnv1a(std::string("\xff\x00\x00\x01", 4)) == 0x6961196491cc682dULL);
assert(fnv1a(std::string("\x01\x00\x00\xff", 4)) == 0xad2bb1774799dfe9ULL);
}
void test_sum_hash() {
assert(sum_hash("") == 0);
assert(sum_hash("ab") == 97 + 98);
assert(sum_hash("amor") == sum_hash("roma"));
assert(sum_hash("ad") == sum_hash("bc"));
// "á" son dos bytes en UTF-8: 0xC3 y 0xA1.
assert(sum_hash("á") == 0xC3 + 0xA1);
}
void test_set_and_get() {
ChainTable<int> table(4, fnv1a);
assert(table.get("lima") == nullptr);
table.set("lima", 1);
table.set("cusco", 2);
assert(*table.get("lima") == 1);
table.set("lima", 3);
assert(*table.get("lima") == 3);
assert(table.size() == 2);
assert(table.check_invariants());
}
void test_collisions_keep_both() {
ChainTable<int> table(16, sum_hash);
table.set("amor", 1);
table.set("roma", 2);
assert(*table.get("amor") == 1);
assert(*table.get("roma") == 2);
assert(table.chain_length(sum_hash("amor") % 16) == 2);
// Se inserta adelante: "roma" está primero, y "amor" cuesta dos comparaciones.
assert(table.comparisons_for("roma") == 1);
assert(table.comparisons_for("amor") == 2);
}
void test_no_objects_left_behind() {
{
ChainTable<Tracked> table(4, fnv1a);
for (int i = 0; i < 100; i++) {
table.set("k" + std::to_string(i % 40), Tracked(i));
}
assert(table.size() == 40);
}
assert(Tracked::counts().live == 0);
}
void test_random_against_library() {
std::mt19937 rng(42);
ChainTable<int> table(64, fnv1a);
std::unordered_map<std::string, int> model;
for (int step = 0; step < 50000; step++) {
std::string key = "k" + std::to_string(rng() % 2000);
if (rng() % 2 == 0) {
int value = static_cast<int>(rng() % 1000);
table.set(key, value);
model[key] = value;
} else {
const int* found = table.get(key);
auto it = model.find(key);
assert((found == nullptr) == (it == model.end()));
if (found != nullptr) {
assert(*found == it->second);
}
}
assert(table.size() == model.size());
}
assert(table.check_invariants());
}
} // namespace
int main() {
test_fnv1a_official_vectors();
test_sum_hash();
test_set_and_get();
test_collisions_keep_both();
test_no_objects_left_behind();
test_random_against_library();
std::cout << "ok\n";
}Laboratorio
Archivos de partida del laboratorio de la semana.
// Lo que necesita chain_report: la función de sumar y el largo de cada cadena.
#ifndef CHAIN_LENGTHS_H
#define CHAIN_LENGTHS_H
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "hash_map.h"
using HashFunction = std::uint64_t (*)(const std::string&);
// Suma los bytes de la clave, como números de 0 a 255.
inline std::uint64_t sum_hash(const std::string&) {
not_written("sum_hash");
}
// Cuántas de las palabras caen en cada una de las m casillas. Sin construir ninguna tabla.
inline std::vector<std::size_t> chain_lengths(const std::vector<std::string>&, std::size_t, HashFunction) {
not_written("chain_lengths");
}
#endif// Tabla hash con encadenamiento que crece. Las claves son std::string.
// Completen cada función que llama a not_written. Lo demás ya está hecho, y conviene leerlo.
// Este archivo no depende de ningún otro de la carpeta: en la semana 9 lo copiarán tal cual.
#ifndef HASH_MAP_H
#define HASH_MAP_H
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <utility>
// Para lo que todavía no escriben: detiene el programa y dice qué falta.
[[noreturn]] inline void not_written(const char* name) {
std::fprintf(stderr, "sin implementar: %s\n", name);
std::abort();
}
// FNV-1a de 64 bits, con las constantes de los apuntes. Cuidado: char puede tener signo.
inline std::uint64_t fnv1a(const std::string&) {
not_written("fnv1a");
}
// 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.
// - Cada nodo de la cadena i cumple fnv1a(key) % capacity_ == i.
// - Una clave aparece en un solo nodo.
// - size_ es el total de nodos, y size_ <= max_load_ * capacity_.
template <typename V>
class HashMap {
public:
HashMap() : slots_(new Node*[INITIAL_CAPACITY]()), capacity_(INITIAL_CAPACITY) {}
~HashMap() { destroy(); }
HashMap(const HashMap& other)
: slots_(other.capacity_ == 0 ? nullptr : new Node*[other.capacity_]()),
capacity_(other.capacity_),
size_(other.size_),
max_load_(other.max_load_) {
for (std::size_t i = 0; i < capacity_; i++) {
for (Node* node = other.slots_[i]; node != nullptr; node = node->next) {
slots_[i] = new Node{node->key, node->value, slots_[i]};
}
}
}
HashMap& operator=(const HashMap& other) {
HashMap copy(other);
swap(copy);
return *this;
}
// Al mover, el origen queda vacío y se puede seguir usando, con capacity() == 0.
HashMap(HashMap&& other) noexcept
: slots_(std::exchange(other.slots_, nullptr)),
capacity_(std::exchange(other.capacity_, 0)),
size_(std::exchange(other.size_, 0)),
max_load_(other.max_load_) {}
HashMap& operator=(HashMap&& other) noexcept {
if (this != &other) {
destroy();
slots_ = std::exchange(other.slots_, nullptr);
capacity_ = std::exchange(other.capacity_, 0);
size_ = std::exchange(other.size_, 0);
max_load_ = other.max_load_;
}
return *this;
}
// Inserta o sobrescribe. Sobrescribir no hace crecer la tabla. Antes de insertar una clave
// nueva, crece mientras needs_growth(size_ + 1). Inserten el nodo al inicio de su cadena.
void set(const std::string&, const V&) {
not_written("set");
}
// Puntero al valor, o nullptr si la clave no está.
const V* get(const std::string&) const {
not_written("get");
}
bool contains(const std::string&) const {
not_written("contains");
}
// Devuelve true si la clave estaba. Cuidado con capacity_ == 0, y con el primer nodo de la cadena.
bool remove(const std::string&) {
not_written("remove");
}
std::size_t size() const { return size_; }
std::size_t capacity() const { return capacity_; }
// Si la tabla ya supera el nuevo máximo, crece de inmediato.
void set_max_load_factor(double alpha) {
assert(alpha > 0);
max_load_ = alpha;
while (capacity_ > 0 && needs_growth(size_)) {
grow();
}
}
bool check_invariants() const {
if ((capacity_ == 0) != (slots_ == nullptr)) {
return false;
}
if (capacity_ != 0 && (capacity_ < INITIAL_CAPACITY || capacity_ % INITIAL_CAPACITY != 0 ||
(capacity_ & (capacity_ - 1)) != 0)) {
return false;
}
std::size_t total = 0;
for (std::size_t i = 0; i < capacity_; i++) {
for (Node* node = slots_[i]; node != nullptr; node = node->next) {
total++;
if (fnv1a(node->key) % capacity_ != i) {
return false;
}
for (Node* other = node->next; other != nullptr; other = other->next) {
if (other->key == node->key) {
return false;
}
}
}
}
return total == size_ && static_cast<double>(size_) <= max_load_ * static_cast<double>(capacity_);
}
private:
static constexpr std::size_t INITIAL_CAPACITY = 8;
struct Node {
std::string key;
V value;
Node* next;
};
// Con capacity_ == 0 no se puede llamar: sería un resto de la división entre 0.
std::size_t slot_of(const std::string& key) const {
return fnv1a(key) % capacity_;
}
bool needs_growth(std::size_t new_size) const {
return capacity_ == 0 || static_cast<double>(new_size) > max_load_ * static_cast<double>(capacity_);
}
// El nodo de esta clave, o nullptr. Lo usarán set, get y contains. Debe funcionar con capacity_ == 0.
Node* find_node(const std::string&) const {
not_written("find_node");
}
// Duplica las casillas, o crea INITIAL_CAPACITY si no hay. Cada nodo vuelve a enlazarse donde le
// toca con la capacidad nueva, sin crear ni copiar nodos ni valores.
void grow() {
not_written("grow");
}
void destroy() {
for (std::size_t i = 0; i < capacity_; i++) {
Node* node = slots_[i];
while (node != nullptr) {
Node* next = node->next;
delete node;
node = next;
}
}
delete[] slots_;
}
void swap(HashMap& other) noexcept {
std::swap(slots_, other.slots_);
std::swap(capacity_, other.capacity_);
std::swap(size_, other.size_);
std::swap(max_load_, other.max_load_);
}
Node** slots_ = nullptr;
std::size_t capacity_ = 0;
std::size_t size_ = 0;
double max_load_ = 1.0;
};
#endif# Informe del paso 4
Antes de correr `make report`, escriban su predicción. Después peguen la tabla y respondan con sus
propios números.
## Predicción
Con unas 1 000 000 de palabras y la capacidad que elija su tabla, ¿cuál será la cadena más larga con
cada función, aproximadamente? ¿Por qué?
## Cómo se corrió
Máquina y comando. Los conteos de esta tabla no dependen de la máquina: ¿por qué?
## Tabla
## Preguntas
1. ¿Cuántas casillas quedan vacías con cada función? ¿Qué dice ese número sobre `suma`?
2. ¿Cuántas comparaciones cuesta en promedio buscar una palabra que está, con cada una?
3. Si duplican las casillas, ¿`suma` mejora? Respondan con lo que saben de los valores que puede dar.
4. ¿Qué tan cerca de 1 + α/2 quedó `fnv1a`?// La tabla de variables del evaluador, sobre su HashMap. Es lo único que el evaluador
// sabe de sus variables: lean en given/evaluator.h cómo la usa.
#ifndef VAR_TABLE_H
#define VAR_TABLE_H
#include <cstddef>
#include <string>
#include "hash_map.h"
class VarTable {
public:
void assign(const std::string&, long long) {
not_written("VarTable::assign");
}
// Si la variable existe, deja su valor en value y devuelve true.
bool lookup(const std::string&, long long&) const {
not_written("VarTable::lookup");
}
bool erase(const std::string&) {
not_written("VarTable::erase");
}
std::size_t count() const {
not_written("VarTable::count");
}
private:
HashMap<long long> table_;
};
#endif// Corre un programa del evaluador. Uso: calc archivo. Se detiene en el primer error.
#include <exception>
#include <fstream>
#include <iostream>
#include <string>
#include "evaluator.h"
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: calc archivo\n";
return 2;
}
std::ifstream file(argv[1]);
if (!file) {
std::cerr << "no se pudo abrir " << argv[1] << "\n";
return 2;
}
Evaluator evaluator(std::cout);
std::string line;
int number = 0;
while (std::getline(file, line)) {
number++;
try {
evaluator.run_line(line);
} catch (const std::exception& error) {
std::cout << "error en la línea " << number << ": " << error.what() << "\n";
return 1;
}
}
}// Compara el largo de las cadenas con cada función hash, y comprueba su HashMap con las
// mismas palabras. Uso: chain_report archivo_de_palabras
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "chain_lengths.h"
#include "hash_map.h"
namespace {
std::vector<std::string> read_words(const std::string& path) {
std::ifstream file(path);
if (!file) {
std::cerr << "no se pudo abrir " << path << "\n";
std::exit(1);
}
std::vector<std::string> words;
std::string line;
while (std::getline(file, line)) {
std::size_t tab = line.find('\t');
words.push_back(tab == std::string::npos ? line : line.substr(tab + 1));
}
return words;
}
void report(const char* name, const std::vector<std::string>& words, std::size_t m, HashFunction hash) {
std::vector<std::size_t> lengths = chain_lengths(words, m, hash);
std::size_t empty = 0;
std::size_t longest = 0;
double comparisons = 0;
std::size_t by_length[5] = {0, 0, 0, 0, 0};
std::size_t total = 0;
for (std::size_t length : lengths) {
total += length;
empty += length == 0 ? 1 : 0;
longest = std::max(longest, length);
comparisons += static_cast<double>(length) * static_cast<double>(length + 1) / 2;
by_length[std::min<std::size_t>(length, 4)]++;
}
std::printf("%-6s %10zu %10zu %10.1f %10zu %8zu %8zu %8zu\n", name, empty, longest,
total == 0 ? 0.0 : comparisons / static_cast<double>(total), by_length[1], by_length[2], by_length[3],
by_length[4]);
}
} // namespace
int main(int argc, char** argv) {
if (argc != 2) {
std::cerr << "uso: chain_report archivo_de_palabras\n";
return 1;
}
std::vector<std::string> words = read_words(argv[1]);
// Su tabla decide el número de casillas: las dos funciones se comparan con ese m.
HashMap<int> table;
for (std::size_t i = 0; i < words.size(); i++) {
table.set(words[i], static_cast<int>(i));
}
std::size_t found = 0;
for (const std::string& word : words) {
found += table.contains(word) ? 1 : 0;
}
std::size_t m = table.capacity();
std::printf("palabras %zu, distintas en la tabla %zu, encontradas %zu, casillas %zu, alfa %.2f\n", words.size(),
table.size(), found, m, static_cast<double>(table.size()) / static_cast<double>(m));
std::printf("%-6s %10s %10s %10s %10s %8s %8s %8s\n", "hash", "vacias", "la_mayor", "comp_prom", "largo_1", "largo_2",
"largo_3", "largo_4+");
report("suma", words, m, sum_hash);
report("fnv1a", words, m, fnv1a);
}// Evaluador de un lenguaje pequeño, dado. Ustedes solo escriben su tabla de variables.
//
// x = 2 + 3 * 4 guarda el valor
// print x - 1 imprime el valor de una expresión
// del x borra una variable
// vars imprime cuántas variables hay
// # comentario una línea vacía o un comentario no hace nada
//
// Los nombres admiten letras, dígitos, _ y bytes no ASCII: "año" es un nombre válido.
#ifndef EVALUATOR_H
#define EVALUATOR_H
#include <cctype>
#include <cstddef>
#include <ostream>
#include <stdexcept>
#include <string>
#include "var_table.h"
class Evaluator {
public:
explicit Evaluator(std::ostream& out) : out_(out) {}
// Ejecuta una línea. Si tiene un error, lanza std::runtime_error.
void run_line(const std::string& line) {
text_ = line;
position_ = 0;
skip_spaces();
if (at_end() || peek() == '#') {
return;
}
std::size_t start = position_;
std::string word = is_name_char(peek()) ? read_name() : "";
if (word == "print") {
out_ << expression() << "\n";
} else if (word == "del") {
std::string name = read_name();
if (!vars_.erase(name)) {
throw std::runtime_error("no se puede borrar '" + name + "': no existe");
}
} else if (word == "vars") {
out_ << vars_.count() << "\n";
} else {
skip_spaces();
if (!word.empty() && !at_end() && peek() == '=') {
position_++;
long long value = expression();
vars_.assign(word, value);
} else {
position_ = start;
throw std::runtime_error("no entiendo la instrucción");
}
}
skip_spaces();
if (!at_end()) {
throw std::runtime_error(std::string("sobra texto desde '") + peek() + "'");
}
}
private:
// expresión: término (('+' | '-') término)*
long long expression() {
long long value = term();
for (skip_spaces(); !at_end() && (peek() == '+' || peek() == '-'); skip_spaces()) {
char op = text_[position_++];
long long right = term();
value = op == '+' ? value + right : value - right;
}
return value;
}
// término: factor (('*' | '/' | '%') factor)*
long long term() {
long long value = factor();
for (skip_spaces(); !at_end() && (peek() == '*' || peek() == '/' || peek() == '%'); skip_spaces()) {
char op = text_[position_++];
long long right = factor();
if (op != '*' && right == 0) {
throw std::runtime_error("división entre cero");
}
value = op == '*' ? value * right : op == '/' ? value / right : value % right;
}
return value;
}
// factor: número | nombre | '(' expresión ')' | '-' factor
long long factor() {
skip_spaces();
if (at_end()) {
throw std::runtime_error("falta un valor al final");
}
if (peek() == '-') {
position_++;
return -factor();
}
if (peek() == '(') {
position_++;
long long value = expression();
skip_spaces();
if (at_end() || peek() != ')') {
throw std::runtime_error("falta ')'");
}
position_++;
return value;
}
if (std::isdigit(static_cast<unsigned char>(peek()))) {
long long value = 0;
while (!at_end() && std::isdigit(static_cast<unsigned char>(peek()))) {
value = value * 10 + (text_[position_++] - '0');
}
return value;
}
if (is_name_char(peek())) {
std::string name = read_name();
long long value = 0;
if (!vars_.lookup(name, value)) {
throw std::runtime_error("variable '" + name + "' sin definir");
}
return value;
}
throw std::runtime_error(std::string("no esperaba '") + peek() + "'");
}
std::string read_name() {
skip_spaces();
std::string name;
while (!at_end() && is_name_char(peek())) {
name += text_[position_++];
}
return name;
}
static bool is_name_char(char c) {
unsigned char byte = static_cast<unsigned char>(c);
return std::isalnum(byte) || c == '_' || byte >= 0x80;
}
bool at_end() const { return position_ >= text_.size(); }
char peek() const { return text_[position_]; }
void skip_spaces() {
while (!at_end() && std::isspace(static_cast<unsigned char>(peek()))) {
position_++;
}
}
std::ostream& out_;
VarTable vars_;
std::string text_;
std::size_t position_ = 0;
};
#endif// Cuenta las reservas de memoria de un programa, reemplazando el operator new global.
// Se incluye en un solo .cpp de cada programa.
#ifndef ALLOC_COUNTER_H
#define ALLOC_COUNTER_H
#include <cstddef>
#include <cstdlib>
#include <new>
// Número de llamadas a operator new desde que empezó el programa.
inline std::size_t& allocation_count() {
static std::size_t count = 0;
return count;
}
void* operator new(std::size_t size) {
allocation_count()++;
void* block = std::malloc(size == 0 ? 1 : size);
if (block == nullptr) {
throw std::bad_alloc();
}
return block;
}
// ASan reemplaza por separado la versión de arreglos: hay que contarla también.
void* operator new[](std::size_t size) {
return operator new(size);
}
void operator delete(void* block) noexcept {
std::free(block);
}
void operator delete(void* block, std::size_t) noexcept {
std::free(block);
}
void operator delete[](void* block) noexcept {
std::free(block);
}
void operator delete[](void* block, std::size_t) noexcept {
std::free(block);
}
#endif// Las funciones hash, con vectores oficiales. El Makefile compila esta prueba con -fsigned-char
// para que un char sin convertir falle en cualquier máquina.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
#include "chain_lengths.h"
#include "hash_map.h"
int main(int argc, char** argv) {
std::string which = argc > 1 ? argv[1] : "";
if (which == "" || which == "fnv1a") {
// Vectores de la página oficial de FNV (test_fnv.c, fnv1a_64_vector).
assert(fnv1a("") == 0xcbf29ce484222325ULL);
assert(fnv1a("a") == 0xaf63dc4c8601ec8cULL);
assert(fnv1a("foobar") == 0x85944171f73967e8ULL);
assert(fnv1a(std::string("\xff\x00\x00\x01", 4)) == 0x6961196491cc682dULL);
assert(fnv1a(std::string("\x01\x00\x00\xff", 4)) == 0xad2bb1774799dfe9ULL);
}
if (which == "" || which == "sum_hash") {
assert(sum_hash("") == 0);
assert(sum_hash("ab") == 97 + 98);
assert(sum_hash("amor") == sum_hash("roma"));
assert(sum_hash("á") == 0xC3 + 0xA1);
}
if (which == "" || which == "chain_lengths") {
std::vector<std::string> words = {"ab", "ba", "c"};
std::vector<std::size_t> lengths = chain_lengths(words, 5, sum_hash);
// 97 + 98 = 195, y 195 % 5 = 0. La 'c' es 99, y 99 % 5 = 4.
assert((lengths == std::vector<std::size_t>{2, 0, 0, 0, 1}));
assert(chain_lengths({}, 3, sum_hash) == std::vector<std::size_t>(3, 0));
}
std::cout << "ok\n";
}// 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 una prueba por nombre: map_tests set_and_get. Sin argumentos, todas.
// Las de map_suite.h valen para cualquier diccionario. Las de abajo son propias del encadenamiento.
#include <cassert>
#include <cstddef>
#include <cstring>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "alloc_counter.h"
#include "hash_map.h"
#include "map_suite.h"
#include "tracked.h"
namespace {
using Ints = HashMap<int>;
std::vector<std::string> make_keys(int count) {
std::vector<std::string> keys;
keys.reserve(count);
for (int i = 0; i < count; i++) {
keys.push_back("k" + std::to_string(i));
}
return keys;
}
void test_capacity_doubles_past_one() {
Ints map;
assert(map.capacity() == 8);
std::vector<std::string> keys = make_keys(17);
for (int i = 0; i < 8; i++) {
map.set(keys[i], i);
}
assert(map.capacity() == 8);
map.set(keys[8], 8);
assert(map.capacity() == 16);
for (int i = 9; i < 16; i++) {
map.set(keys[i], i);
}
assert(map.capacity() == 16);
map.set(keys[16], 16);
assert(map.capacity() == 32);
assert(map.check_invariants());
}
void test_overwrite_does_not_grow() {
Ints map;
std::vector<std::string> keys = make_keys(8);
for (int i = 0; i < 8; i++) {
map.set(keys[i], i);
}
for (int i = 0; i < 8; i++) {
map.set(keys[i], -i);
}
assert(map.capacity() == 8);
assert(map.size() == 8);
}
void test_remove_does_not_shrink() {
Ints map;
std::vector<std::string> keys = make_keys(100);
for (int i = 0; i < 100; i++) {
map.set(keys[i], i);
}
std::size_t capacity = map.capacity();
for (int i = 0; i < 100; i++) {
assert(map.remove(keys[i]));
}
assert(map.size() == 0);
assert(map.capacity() == capacity);
assert(map.check_invariants());
}
// Con α enorme nunca crece: 8 casillas y cadenas largas. Se borra al inicio, en el medio y al final.
void test_long_chains() {
Ints map;
map.set_max_load_factor(1000.0);
std::vector<std::string> keys = make_keys(400);
for (int i = 0; i < 400; i++) {
map.set(keys[i], i);
}
assert(map.capacity() == 8);
for (int i = 0; i < 400; i += 3) {
assert(map.remove(keys[i]));
assert(map.check_invariants());
}
for (int i = 0; i < 400; i++) {
const int* found = map.get(keys[i]);
assert((found == nullptr) == (i % 3 == 0));
}
for (int i = 0; i < 400; i++) {
assert(map.remove(keys[i]) == (i % 3 != 0));
}
assert(map.size() == 0);
assert(map.check_invariants());
}
void test_lower_load_factor_grows_now() {
Ints map;
std::vector<std::string> keys = make_keys(8);
for (int i = 0; i < 8; i++) {
map.set(keys[i], i);
}
map.set_max_load_factor(0.25);
assert(map.capacity() == 32);
assert(map.check_invariants());
for (int i = 0; i < 8; i++) {
assert(*map.get(keys[i]) == i);
}
}
// Crecer vuelve a enlazar los nodos: una reserva nueva para las casillas, y nada más.
void test_growth_allocates_only_slots() {
std::vector<std::string> keys = make_keys(1000);
std::size_t before = allocation_count();
{
Ints map;
for (int i = 0; i < 1000; i++) {
map.set(keys[i], i);
}
std::size_t growths = 0;
for (std::size_t capacity = map.capacity(); capacity > 8; capacity /= 2) {
growths++;
}
// Una reserva inicial, una por crecimiento y una por nodo.
assert(allocation_count() - before == 1 + growths + 1000);
}
}
void test_growth_copies_no_values() {
Tracked::counts() = Tracked::Counts{};
{
HashMap<Tracked> map;
std::vector<std::string> keys = make_keys(500);
Tracked value(7);
for (const std::string& key : keys) {
map.set(key, value);
}
// Una copia por nodo nuevo, y ninguna más al crecer.
assert(Tracked::counts().copies == 500);
assert(Tracked::counts().live == 501);
map.set(keys[0], value);
assert(Tracked::counts().copies == 501);
assert(Tracked::counts().live == 501);
assert(map.remove(keys[0]));
assert(Tracked::counts().live == 500);
}
assert(Tracked::counts().live == 0);
}
void test_pointers_survive_growth() {
Ints map;
map.set("lima", 42);
const int* before = map.get("lima");
std::vector<std::string> keys = make_keys(1000);
for (int i = 0; i < 1000; i++) {
map.set(keys[i], i);
}
assert(map.get("lima") == before);
assert(*before == 42);
}
void test_value_from_the_same_table() {
HashMap<std::string> map;
map.set("origen", std::string(100, 'v'));
map.set("origen", *map.get("origen"));
assert(*map.get("origen") == std::string(100, 'v'));
// El crecimiento ocurre en medio de la llamada: value sigue siendo válido.
for (int i = 0; i < 1000; i++) {
map.set("k" + std::to_string(i), *map.get("origen"));
}
assert(*map.get("k999") == std::string(100, 'v'));
}
struct NoDefault {
explicit NoDefault(int value) : value(value) {}
int value;
};
void test_value_without_default_constructor() {
HashMap<NoDefault> map;
map.set("a", NoDefault(1));
map.set("a", NoDefault(2));
assert(map.get("a")->value == 2);
}
void test_no_objects_left_behind() {
Tracked::counts() = Tracked::Counts{};
{
HashMap<Tracked> map;
for (int i = 0; i < 100; i++) {
map.set("k" + std::to_string(i), Tracked(i));
}
HashMap<Tracked> copy(map);
HashMap<Tracked> assigned;
assigned = copy;
HashMap<Tracked> moved(std::move(copy));
assigned = std::move(moved);
assert(map.remove("k5"));
assert(Tracked::counts().live == 199);
}
assert(Tracked::counts().live == 0);
}
void test_moves_are_noexcept() {
static_assert(std::is_nothrow_move_constructible_v<Ints>);
static_assert(std::is_nothrow_move_assignable_v<Ints>);
}
struct Test {
const char* name;
void (*run)();
};
const std::vector<Test> TESTS = {
{"new_is_empty", test_new_is_empty<Ints>},
{"set_and_get", test_set_and_get<Ints>},
{"overwrite", test_overwrite<Ints>},
{"contains", test_contains<Ints>},
{"remove_present", test_remove_present<Ints>},
{"remove_absent", test_remove_absent<Ints>},
{"unusual_keys", test_unusual_keys<Ints>},
{"growth", test_growth<Ints>},
{"copy_is_independent", test_copy_is_independent<Ints>},
{"self_assignment", test_self_assignment<Ints>},
{"move_leaves_empty_and_usable", test_move_leaves_empty_and_usable<Ints>},
{"random_small", test_random_small<Ints>},
{"random_large", test_random_large<Ints>},
{"run_map_suite", run_map_suite<Ints>},
{"capacity_doubles_past_one", test_capacity_doubles_past_one},
{"overwrite_does_not_grow", test_overwrite_does_not_grow},
{"remove_does_not_shrink", test_remove_does_not_shrink},
{"long_chains", test_long_chains},
{"lower_load_factor_grows_now", test_lower_load_factor_grows_now},
{"growth_allocates_only_slots", test_growth_allocates_only_slots},
{"growth_copies_no_values", test_growth_copies_no_values},
{"pointers_survive_growth", test_pointers_survive_growth},
{"value_from_the_same_table", test_value_from_the_same_table},
{"value_without_default_constructor", test_value_without_default_constructor},
{"no_objects_left_behind", test_no_objects_left_behind},
{"moves_are_noexcept", test_moves_are_noexcept},
};
} // namespace
int main(int argc, char** argv) {
for (const Test& test : TESTS) {
if (argc < 2 || std::strcmp(argv[1], test.name) == 0) {
test.run();
if (argc >= 2) {
return 0;
}
}
}
if (argc >= 2) {
std::cerr << "no existe la prueba " << argv[1] << "\n";
return 2;
}
std::cout << "ok\n";
}# Un programa pequeño: variables, sobrescribir y borrar.
base = 8
altura = 3 + 2
area = base * altura
print area
print area / 3
print area % 3
print -(base - 10) * 2
año = 2026
print año + 1
area = area + 1
print area
vars
del altura
vars
altura = 1
print altura40
13
1
4
2027
41
4
3
1// Tipo para las pruebas: cuenta cuántos objetos viven y cuántas copias y movimientos hubo.
#ifndef TRACKED_H
#define TRACKED_H
#include <utility>
class Tracked {
public:
struct Counts {
long live = 0;
long copies = 0;
long moves = 0;
};
static Counts& counts() {
static Counts values;
return values;
}
Tracked() { counts().live++; }
explicit Tracked(int value) : value_(value) { counts().live++; }
Tracked(const Tracked& other) : value_(other.value_) {
counts().live++;
counts().copies++;
}
Tracked(Tracked&& other) noexcept : value_(std::exchange(other.value_, 0)) {
counts().live++;
counts().moves++;
}
Tracked& operator=(const Tracked& other) {
value_ = other.value_;
counts().copies++;
return *this;
}
Tracked& operator=(Tracked&& other) noexcept {
value_ = std::exchange(other.value_, 0);
counts().moves++;
return *this;
}
~Tracked() { counts().live--; }
int value() const { return value_; }
bool operator==(const Tracked& other) const { return value_ == other.value_; }
private:
int value_ = 0;
};
#endifabaos
abatieses
ablandarnos
abominaras
abovedan
abriolaré
absorbérnoslas
aburrínoslos
acalabrotaré
acarearais
acañavereen
acentuárosla
achajuanaste
achuchadla
aclamándonoslas
acogieras
acompañado
acoramos
acostumbrarla
acriminaban
acuchillámela
acusádsela
adelantale
adietaren
admitídsela
adornarse
adulcigüemos
afanéis
afiebrarías
afligiría
afretaran
agarradles
agilizá
agracíeles
agremiaran
aguarapaba
aguándonos
ahorcáremos
aindiá
ajustaras
alandreareis
alborozaríamos
aleccionados
aletargar
alienabas
alistártelos
almacigábamos
alojándonos
aludieses
amacollara
amanerase
amartelara
amenazadles
aminorádselos
amonestará
amortiguaréis
amplísimo
anacrónico
aneemos
anhelan
anquilosarlo
antigramatical
aojaban
apaleárselo
apartido
apegualasen
aperrearás
apiojaren
aplazáis
aporismarías
apreciativos
apretarais
aprovisionándola
apuñalándomelo
arbitrajes
arengárnoslo
armonizárselo
arrancándonoslos
arredondeen
arrepintiéndoos
arriádselos
arropádnoslas
arruéstrame
asaetáremos
aselaron
aseverarla
asiéntalo
asordas
asumírnoslos
atalajemos
atemperándosela
aterrársela
atirantarían
atornillándomela
atrapadas
atrojarse
atínennoslo
auscultándole
autodestruyéndonoslas
autoproclamando
avanzaseis
avergonzales
avizorabais
azogados
añorale
bailádselos
baliangao
banquetearé
barnizándoosla
basculéis
batucada
beldabas
besucasen
birlársela
blanquearme
bodigos
bonificara
bosquecillo
brezaseis
brujulearíais
buscándote
cablearé
cachondeádmelas
calagurritano
caleños
calumniádselos
caminadlos
canceriformes
cantearé
capitanearais
caracterizabas
carearlos
carpio
cascamajen
catalogabais
catéeselas
cazumbrados
cegárnosla
censúranoslos
cercenádnoslas
certísimas
chamboneé
chapecándose
chaspad
chileros
chirrisquearán
chorizaras
churrará
cimblases
circunscribiéndooslo
clareáremos
clonámelas
cobrearen
codirigiéndose
coiméemela
colgándotelos
coludamos
comemierdas
compadecelos
compendiaron
complotas
compréndelo
concediéndooslo
concluiéndole
condensarlo
conectivas
confirmámelos
confutaren
conjeturarían
conmociónelos
consensuármelas
constará
consumádselas
contenedora
contrabalancearas
contraponeles
contravenciones
convenciéndoosla
coordinasteis
corasteis
corporeizó
corroyeras
cosecharé
cotizó
criaderas
criémonos
cuadriplicarais
cubicánoslas
cuestionártela
cumplimentádmelo
curtiere
cítenla
debelare
decapopularmente
decliné
dedicativos
defraudádnosla
delego
demandante
demórelos
denunciases
deposítenoslas
derrengarse
desabarrancaban
desaconsejarían
desafinarais
desahoguen
desalojaremos
desamurares
desapliqué
desarraigales
desasnábamos
desatrancaste
desbaratádnoslo
desborrará
descacharremos
descampes
descarriladura
descercando
descoagulara
descompasadas
descongeládnoslos
desconvocádmelas
describiéndonoslas
descuncharás
deselectrizados
desembozáis
desempercudidos
desencantareis
desencuadernaras
desengañánosla
desenojas
desenterrara
desequipadla
desestimulándonoslas
desfogonaréis
desglosaban
desguazáselas
deshidratánoslo
desilusionadas
desinstalároslas
deslechuguilladas
desmadejaríamos
desmayárnosla
desminadle
desmotaban
desobedécete
desortijaste
despanchurrando
despedácenlo
desperdiciaría
despicháramos
desplegueteamos
desposéamelo
desprovéanoslas
desrabotaréis
destarar
destituímela
destruiéndosela
desvalorarais
desviejes
deszumás
detonases
devuelven
dictamináis
difumínemela
dilatares
dinamíteles
disconvendrá
diseñadlo
disipámelas
dispusieres
distribuyéramos
divinádmela
doblégaselos
domicilias
dosifícasela
dudás
echando
edulcorámelos
ejercitadora
eliminarían
embaldosaré
embargánosla
embejucas
embobares
emborráchalas
embriagándonoslo
emburujará
empadrónanos
empapizarais
empastánosla
empellierais
empeñarás
emplearas
empotraré
empuñándosela
enamoraran
encabezárselo
encalamoques
encanares
encaprichádmelo
encarnizaste
encasquetárselo
enceguecerlos
enceránoslas
enchumbara
encofráremos
enconfite
encorujad
encueraren
enderezáselo
endulzará
enfardes
enfosca
enfurian
engarbullando
engañádselos
engorrone
engrumecé
enhacinábamos
enjabonó
enjugándote
enlenzá
enmaderaras
enmendándoselas
enorgullécemelos
enrejalad
enrolándoles
ensalzadoras
enserenó
ensortijad
entarimase
entibiado
entornillaran
entrechocalos
entremetédsela
entreténganlos
entronero
entusiasmárselas
envegado
envinagraban
enzuricé
eraban
erute
escalaría
escandalizadlas
escarche
escatimádmelos
escocar
escopeteaste
escuchanos
escénico
eslabonare
espaldean
especificáselos
espiarlos
esponjaron
esquiladores
estadísticos
estenografiadas
estipendiaríais
estranguládmelas
estropearíais
estároslas
evanecer
exabrupto
exceptuabais
excédenosla
existencialismo
expendédsela
explotación
expugnéis
externalizaron
extralimítate
eyaculadles
faenad
faltéis
fascínenoslos
felicítalos
festéjalas
fililís
firmabas
flatulentos
fluctuaréis
forcateará
forras
fotografíenme
franquéanoslos
friccionámelas
fugad
fundíamos
galga
ganzuareis
garrobas
generalízalos
gigateslas
gobernasen
gorgoriteará
granarais
gravedades
guachinea
guataqueando
guiñádselos
habla
hamaco
hechizaseis
herbecieran
hervirles
himplaré
hispanoamericana
hombreas
horadabas
hospitalícennos
humarada
husmaste
idolatrándomelos
ilustrabais
impartiéndomelos
implicarles
impregnándoles
imputáselos
incentiváis
incomunicádselo
incrustemos
independencias
indizaban
infectarían
influyeres
ingerirnos
iniciámelas
inmovilizándooslos
insalivéis
inspeccionártelos
instrumentándoos
intemperada
intercálela
interpelame
intimidante
intuyéndooslos
investigarles
ionizaréis
iteran
jalearme
jaspeen
jolines
jumarían
kappas
ladraré
lampeéis
lascarían
lañare
lerdeando
libertades
lidérala
linchasteis
litó
llevádsela
logree
ludiaban
macerarais
maduradlo
majadeabais
malentiendo
malparías
mamádselos
mangoneadme
manoteasteis
maquillándonoslas
marginal
martilleándonosla
mastícanos
maxiligerísimo
meciéndoselo
megaohmísimo
menciónelas
menudamente
mestizarán
miccional
mimaría
minorarse
mitiguémonos
modulabais
molimos
monologaron
mordacearé
mosconeando
muca
multárselas
musítemelos
naguanagua
navajearías
nepalís
noceda
notarles
numérennos
obnubilara
obteniéndoselos
ocupándoos
ofrezcan
olivarías
opináis
ordéñamelos
orlaron
oteádmelo
pacificalo
palanganeé
paloteas
papeleaseis
paramente
parlamentarías
paránoslo
patasca
paularas
pectarais
peleón
penetrártelo
percíbeselas
perfórenlas
permutases
personalizándomelos
pesquisad
pichuleábamos
pillárselos
pintársela
pisoteárnosla
planas
plastezcas
pléguennoslos
politizártelos
pordioseras
posibilidad
postularnos
precaveríamos
predefiniré
preguntadoras
prensares
presentadles
presumídmela
previeres
privilegiánoslo
prodigara
profundícenme
promisión
propasártelo
prorroguéis
protocolice
pto
pulsare
purgársela
pítemelos
quermés
quitaríais
racionarais
ralentizaréis
rarezas
rastrojearse
reabriría
realquile
rebalsaremos
rebobinaren
recabándoles
recatearemos
rechazárnosla
reclutárnoslas
recompensaríamos
reconquistándonoslo
recorré
rectificárnoslos
recójase
redobleguemos
reemplazale
reestructurádmelos
reflotalos
refrenándomela
regabinaréis
registránoslas
regularizare
rehuimos
reinicíennoslos
rejuntás
relegarían
relumbró
remediándooslos
remolcando
remudaron
renovales
repapilarse
repentizarán
replantároslos
repreguntadas
repudiarse
requiriese
rescindiendo
resguardárnoslo
respaldes
resquilamos
restándolos
retalleciese
retocártelas
retransmitíamos
retráigamelos
revalidala
revesados
revolcarle
rezándose
ripiado
rociámelo
romanee
rosáremos
rucarás
rustiré
saboteárnoslas
saeteabais
salificásemos
salteándotelas
sancionatoria
sanáremos
sazonada
secundarían
segundáis
sensibilizale
serenándomelo
sextavemos
sijismo
simultanéamelas
sintetizarlo
soasaréis
sobrecoge
sobrellevárnosla
sobreseyeren
socaváremos
soguease
solidarizándooslos
somatizale
sonora
soplarás
sosañaréis
subalternase
subliman
substanciada
subínoslo
sugiriéndose
supedítenlos
suplicárnosla
suscitasen
sustraámonos
tablearé
tallarle
tanguéenos
tarabuco
tasemos
telefoneándolo
templárnoslos
tepexco
tertuliano
tijereteádselo
tiquillaca
titulándola
tonasteis
tornamiento
totalizase
tragoneáramos
transandinos
transgrediéndome
transubstanciasteis
trascribirán
trasladasteis
traspapelados
trastejar
tratártela
trepándoles
triplíquenos
trocéenlos
trufándonosla
tupiríamos
téntelo
umpaleñas
untáis
utilizaría
valelos
vandalizalo
vaticinándole
vendimiados
veranada
vertebren
victoreamos
violalas
visteas
viviéndooslos
volviere
vírgulas
zaceáramos
zampándoselos
zarandeádselos
zumbándoles
írritosCompilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 7: tablas hash. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-map prueba solo HashMap (pasos 1 y 2 del laboratorio)
# make test-calc prueba el evaluador (paso 3)
# make test-hash prueba las funciones hash y chain_lengths (paso 4)
# make test-lab prueba todo su laboratorio; falla hasta que lo terminen
# make report tabla del paso 4 con el diccionario real, con -O2 y sin sanitizers
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
DATA ?= ../../data
LAB_DIR ?= lab
BUILD = build
PROGRAMS = hash_demo fnv_trace chain_trace growth_count library_demo test_structures
BENCHES = first_collision hash_compare load_experiment naive_copy flood lookup_bench
COMMON_HEADERS = hash_functions.h chain_table.h word_file.h tracked.h bench.h
LAB_HEADERS = $(LAB_DIR)/hash_map.h $(LAB_DIR)/chain_lengths.h $(LAB_DIR)/var_table.h
OUT = $(BUILD)/$(LAB_DIR)
TEST_HEADERS = lab/tests/map_suite.h lab/tests/alloc_counter.h lab/tests/tracked.h
.PHONY: all lab test test-map test-calc test-hash test-lab report 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 y los programas dados toman los archivos de esa carpeta.
$(OUT)/map_tests: lab/tests/map_tests.cpp $(TEST_HEADERS) $(LAB_HEADERS) | $(OUT)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) -Ilab/tests $< -o $@
$(OUT)/hash_tests: lab/tests/hash_tests.cpp $(LAB_HEADERS) | $(OUT)
$(CXX) $(CXXFLAGS) -fsigned-char -I$(LAB_DIR) $< -o $@
$(OUT)/calc: lab/given/calc.cpp lab/given/evaluator.h $(LAB_HEADERS) | $(OUT)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) $< -o $@
$(OUT)/chain_report: lab/given/chain_report.cpp $(LAB_HEADERS) | $(OUT)
$(CXX) $(CXXFLAGS) -I$(LAB_DIR) $< -o $@
$(OUT)/chain_report_fast: lab/given/chain_report.cpp $(LAB_HEADERS) | $(OUT)
$(CXX) $(BENCHFLAGS) -I$(LAB_DIR) $< -o $@
$(BUILD) $(OUT):
mkdir -p $@
lab: $(OUT)/map_tests $(OUT)/hash_tests $(OUT)/calc $(OUT)/chain_report $(OUT)/chain_report_fast
# 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-map: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestMap
test-calc: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestCalc
test-hash: lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestHash
test-lab: all lab
DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab
report: $(OUT)/chain_report_fast
$< $(DATA)/palabras.txt
clean:
rm -rf $(BUILD) __pycache__