Contar palabras en C++ es el ejemplo perfecto para aprender a usar mapas: leer un texto y separarlo en palabras, contarlas con std::map o std::unordered_map, sacar las más frecuentes con partial_sort, evitar la trampa del operador [] y medir cuál de los dos mapas es más rápido.
También te puede interesar
Contar con std::map
La idea es sencilla: se lee el texto línea a línea, se parte en palabras, se pasan a minúsculas y se suma 1 en un mapa cuya clave es la palabra. std::map guarda las claves ordenadas, así que al recorrerlo salen en orden alfabético. Probamos con el comienzo de El Quijote, que el programa lee de la entrada estándar:
En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lentejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda.
#include <algorithm>
#include <cctype>
#include <iostream>
#include <map>
#include <print>
#include <string>
// Una palabra: letras ASCII o bytes de un carácter UTF-8 (á, ñ, ü...). Lo demás separa.
bool es_letra(unsigned char c) { return std::isalpha(c) || c >= 0x80; }
int main()
{
std::map<std::string, int> cuenta;
std::string linea, palabra;
int total = 0;
while (std::getline(std::cin, linea)) {
for (std::size_t i = 0; i <= linea.size(); ++i) {
unsigned char c = i < linea.size() ? linea[i] : ' ';
if (es_letra(c)) {
palabra += static_cast<char>(std::tolower(c));
} else if (!palabra.empty()) {
++cuenta[palabra]; // si no estaba, se crea con 0 y pasa a 1
++total;
palabra.clear();
}
}
}
std::println("{} palabras, {} distintas", total, cuenta.size());
for (const auto& [p, n] : cuenta)
if (n > 1) std::println("{:>3} {}", n, p);
}salida66 palabras, 50 distintas 7 de 2 en 2 las 4 los 2 más 2 no 2 que 2 un 2 y
El truco está en ++cuenta[palabra]: el operador [] de std::map busca la clave y, si no existe, la crea con el valor por omisión, que para un int es 0. Así no hace falta comprobar antes si la palabra ya estaba. Para ejecutarlo con un archivo propio: ./contar < texto.txt.
std::tolower trabaja byte a byte, y en UTF-8 la «Á» ocupa dos bytes, así que no la pasa a «á». Este programa trata bien las letras con tilde dentro de las palabras, pero «Árbol» y «árbol» contarían como palabras distintas. Para pasar a minúsculas texto en cualquier idioma hace falta una biblioteca de Unicode, como ICU.Las palabras más frecuentes: unordered_map y partial_sort
Cuando no importa el orden alfabético, std::unordered_map es la opción habitual: es una tabla hash, así que buscar e insertar cuesta, de media, lo mismo tenga 10 o un millón de claves. Para sacar las N más frecuentes se copian los pares a un vector y se ordenan solo los N primeros con std::partial_sort:
#include <algorithm>
#include <cctype>
#include <iostream>
#include <print>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
bool es_letra(unsigned char c) { return std::isalpha(c) || c >= 0x80; }
int main()
{
std::unordered_map<std::string, int> cuenta;
std::string palabra;
char ch;
while (std::cin.get(ch)) {
unsigned char c = ch;
if (es_letra(c)) {
palabra += static_cast<char>(std::tolower(c));
} else if (!palabra.empty()) {
++cuenta[palabra];
palabra.clear();
}
}
if (!palabra.empty()) ++cuenta[palabra];
std::vector<std::pair<std::string, int>> pares(cuenta.begin(), cuenta.end());
const std::size_t n = std::min<std::size_t>(5, pares.size());
// Más repeticiones primero; a igualdad, orden alfabético (para que el resultado no dependa del hash)
std::partial_sort(pares.begin(), pares.begin() + n, pares.end(), [](const auto& a, const auto& b) {
return a.second != b.second ? a.second > b.second : a.first < b.first;
});
std::println("Las {} palabras más frecuentes:", n);
for (std::size_t i = 0; i < n; ++i)
std::println("{}. {:<6} {} veces", i + 1, pares[i].first, pares[i].second);
}salidaLas 5 palabras más frecuentes: 1. de 7 veces 2. los 4 veces 3. en 2 veces 4. las 2 veces 5. más 2 veces
El desempate por orden alfabético no es un capricho: un std::unordered_map no garantiza ningún orden al recorrerlo, y ese orden puede cambiar entre compiladores, versiones o incluso al añadir claves. Si la comparación solo mirase el número de repeticiones, las palabras empatadas podrían salir en un orden distinto en cada máquina.
El error típico: el operador [] crea claves al consultar
Ese mismo [] que tan cómodo resulta al contar es una trampa al consultar: preguntar por una palabra que no existe la añade al mapa con valor 0.
#include <print>
#include <string>
#include <unordered_map>
int main()
{
std::unordered_map<std::string, int> cuenta{{"mancha", 1}, {"hidalgo", 1}};
std::println("tamaño al principio: {}", cuenta.size());
// MAL: consultar con [] inserta las claves que no existen
if (cuenta["dulcinea"] > 0) std::println("sale Dulcinea");
if (cuenta["sancho"] > 0) std::println("sale Sancho");
std::println("tamaño tras consultar con []: {}", cuenta.size());
// BIEN: contains (C++20) o find no modifican el mapa
std::println("¿contiene \"rocinante\"? {}", cuenta.contains("rocinante"));
if (auto it = cuenta.find("mancha"); it != cuenta.end())
std::println("mancha aparece {} vez", it->second);
std::println("tamaño final: {}", cuenta.size());
}salidatamaño al principio: 2 tamaño tras consultar con []: 4 ¿contiene "rocinante"? false mancha aparece 1 vez tamaño final: 4
En un programa que recibe consultas de fuera, ese error convierte cada búsqueda fallida en memoria ocupada para siempre. Además, el operador [] no se puede usar con un mapa const, precisamente porque puede modificarlo. Para leer sin modificar: contains, find o at, que lanza std::out_of_range si la clave no existe.
¿map o unordered_map? Medido
Para comparar las dos opciones con un volumen realista, este programa genera un millón de palabras al azar, con semilla fija, de un vocabulario de 50,000, y cronometra cuánto tarda cada mapa en contarlas. Se compila con -O2, como se haría en producción:
#include <chrono>
#include <map>
#include <print>
#include <random>
#include <string>
#include <unordered_map>
#include <vector>
template <typename Mapa>
double contar_ms(const std::vector<std::string>& palabras, std::size_t& distintas)
{
auto inicio = std::chrono::steady_clock::now();
Mapa cuenta;
for (const auto& p : palabras) ++cuenta[p];
distintas = cuenta.size();
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - inicio).count();
}
int main()
{
std::mt19937 gen(2026); // semilla fija: siempre las mismas palabras
std::uniform_int_distribution<int> cual(0, 49'999);
std::vector<std::string> palabras;
palabras.reserve(1'000'000);
for (int i = 0; i < 1'000'000; ++i) palabras.push_back("p" + std::to_string(cual(gen)));
std::size_t d1, d2;
double t_map = contar_ms<std::map<std::string, int>>(palabras, d1);
double t_hash = contar_ms<std::unordered_map<std::string, int>>(palabras, d2);
std::println("palabras distintas: {} y {}", d1, d2);
std::println("std::map: {:6.1f} ms", t_map);
std::println("std::unordered_map: {:6.1f} ms", t_hash);
std::println("unordered_map, {:.1f} veces más rápido", t_map / t_hash);
}salida (los tiempos cambian en cada ejecución)palabras distintas: 50000 y 50000 std::map: 228.5 ms std::unordered_map: 31.8 ms unordered_map, 7.2 veces más rápido
Los milisegundos cambian en cada ejecución y en cada máquina, pero la proporción se mantiene: con muchas claves, std::unordered_map suele ser varias veces más rápido, porque std::map es un árbol y cada búsqueda compara la cadena con unas 16 claves por el camino. A cambio, std::map da el resultado ordenado y su peor caso está garantizado.
- std::mapstd::unordered_map
- EstructuraÁrbol rojo-negroTabla hash
- Buscar e insertarlog n comparacionesConstante de media; n en el peor caso
- Orden al recorrerAlfabético (por la clave)Ninguno garantizado
- La clave necesita
<std::hashy== - Úsalo cuandoQuieres el resultado ordenado o rangos de clavesSolo cuentas o buscas, y hay muchas claves
Todo el código de esta página se compiló con GCC 15.3 (C++23, -std=c++23 -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.