Keywords: C++ | HashMap | std::unordered_map | Hash Table | Performance Optimization
Abstract: This article delves into the usage of HashMap in C++, focusing on the std::unordered_map container, including basic operations, performance characteristics, and practical examples. It compares std::map and std::unordered_map, explains underlying hash table implementation principles such as hash functions and collision resolution strategies, providing a thorough technical reference for developers.
Introduction
In C++ programming, a hash map (HashMap) is an efficient key-value storage structure that maps keys to storage locations via hash functions, enabling fast insertion, deletion, and lookup operations. The Standard Template Library (STL) provides two main map containers: std::map and std::unordered_map. This article explores their usage, performance differences, and underlying implementation principles in detail.
Comparison of std::map and std::unordered_map
std::map is an ordered map container, typically implemented internally with a red-black tree, ensuring elements are sorted by key. Its insertion and access operations have a time complexity of O(log n), making it suitable for scenarios requiring ordered traversal. For example, in the following code, we declare a std::map and insert key-value pairs:
#include <map>
#include <iostream>
#include <cassert>
int main() {
std::map<std::string, int> m;
m["hello"] = 23;
if (m.find("world") != m.end()) {
std::cout << "map contains key world!" << std::endl;
}
std::cout << m["hello"] << std::endl;
auto i = m.find("hello");
assert(i != m.end());
std::cout << "Key: " << i->first << " Value: " << i->second << std::endl;
return 0;
}Output:
23 Key: hello Value: 23
In contrast, std::unordered_map is an unordered map based on a hash table, with an average time complexity of O(1), ideal for high-performance scenarios. Its API is similar to std::map; simply replace map with unordered_map. For instance:
#include <unordered_map>
#include <iostream>
int main() {
std::unordered_map<std::string, int> umap;
umap["Apple"] = 10;
umap["Mango"] = 20;
umap["Cherry"] = 30;
for (const auto& pair : umap) {
std::cout << pair.first << " " << pair.second << std::endl;
}
return 0;
}Possible output:
Cherry 30 Mango 20 Apple 10
Note that std::unordered_map has been part of the standard since C++11; older compilers may require std::tr1::unordered_map or the Boost library.
Underlying Implementation Principles of Hash Tables
The core of a hash table lies in the hash function, which converts keys into array indices. For example, using the FNV-1a hash function to compute the hash of a string key:
uint64_t hash_key(const char* key) {
uint64_t hash = FNV_OFFSET;
for (const char* p = key; *p; p++) {
hash ^= (uint64_t)(unsigned char)(*p);
hash *= FNV_PRIME;
}
return hash;
}Hash collisions, where different keys map to the same index, are common. Linear probing is a resolution strategy: when a collision occurs, sequentially check the next available slot. For example, when inserting key "x", if index 7 is occupied, check index 8, 9, etc., until an empty slot is found.
Hash table performance depends on the load factor (ratio of elements to capacity). When the load factor is too high, the array must be expanded, typically by doubling the capacity and rehashing all elements. Here is a simple expansion function example:
bool ht_expand(ht* table) {
size_t new_capacity = table->capacity * 2;
ht_entry* new_entries = calloc(new_capacity, sizeof(ht_entry));
if (!new_entries) return false;
for (size_t i = 0; i < table->capacity; i++) {
if (table->entries[i].key) {
ht_set_entry(new_entries, new_capacity, table->entries[i].key, table->entries[i].value, NULL);
}
}
free(table->entries);
table->entries = new_entries;
table->capacity = new_capacity;
return true;
}Practical Application Examples
Hash tables are highly practical in scenarios like word frequency counting. The following C program uses a custom hash table to count word frequencies from input:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ht.h"
void exit_nomem() {
fprintf(stderr, "out of memory");
exit(1);
}
int main() {
ht* counts = ht_create();
if (!counts) exit_nomem();
char word[101];
while (scanf("%100s", word) != EOF) {
void* value = ht_get(counts, word);
if (value) {
(*(int*)value)++;
} else {
int* pcount = malloc(sizeof(int));
if (!pcount) exit_nomem();
*pcount = 1;
if (!ht_set(counts, word, pcount)) exit_nomem();
}
}
hti it = ht_iterator(counts);
while (ht_next(&it)) {
printf("%s %d", it.key, *(int*)it.value);
free(it.value);
}
printf("%d", (int)ht_length(counts));
ht_destroy(counts);
return 0;
}This program reads words, uses the hash table to track occurrences, and outputs the results.
Performance Analysis and Best Practices
std::unordered_map offers O(1) average-case time complexity for operations, but worst-case can degrade to O(n). When choosing, consider: if ordered elements are needed, use std::map; otherwise, std::unordered_map is more efficient. In C++, prefer standard library containers unless specific needs like custom hash functions or memory optimization arise.
For underlying implementations, linear probing is simple and efficient but may cause clustering. Other strategies like chaining reduce clustering but increase memory overhead. In real projects, select the appropriate method based on data characteristics and performance requirements.
Conclusion
HashMap is a powerful data tool in C++, with std::unordered_map providing an efficient hash table implementation. Understanding its principles and usage helps optimize program performance. Through examples and analysis in this article, developers can better apply HashMap to solve practical problems.