是否存在连续存储的散列映射数据结构

Is there a contiguously stored hash map data structure?

本文关键字:映射 数据结构 存在 连续 存储 是否      更新时间:2023-10-16

考虑不同类型的集合,如Position, Color, Name。这些实例可以通过在集合中使用相同的键来连接。密钥是64位长度的全局唯一标识符。目前,我使用哈希映射,但这不是理想的。

// custom types
struct Position { float x, y, z; bool static; };
enum Color { RED, BLUE, GREEN };
// collections
std::unordered_map<uint64_t, Position> positions;
std::unordered_map<uint64_t, Color> colors;
std::unordered_map<uint64_t, std::string> names;
// add some data
// ...
// only access positions collection,
// so the key is not needed here
for (auto i = positions.begin(); i != positions.end(); ++i) {
    if (i->second.static) continue;
    i->second.x = (rand() % 1000) / 1000.f;
    i->second.y = (rand() % 1000) / 1000.f;
    i->second.z = (rand() % 1000) / 1000.f;
}
// access to all three collections,
// so we need the key here
for (auto i = positions.begin(); i != positions.end(); ++i) {
    uint64_t id   = *i->first;
    auto position = i->second;
    auto color    = colors.find(id);
    auto name     = names.find(id);
    if (color == colors.end() || name == names.end()) continue;
    draw(*name, *position, *color);
}

我尝试分离集合,但是正如您所看到的,同时也需要多个集合的收集实例。当然,我还需要不时地添加或删除单个元素,但这些情况对性能并不重要。

现在我想优化单个集合的迭代。因此,我尝试让集合连续存储,这是面向数据设计思想的一部分。但是,我仍然需要非常快速地访问各个实例。直接使用数组不工作,因为这会分配太多的内存,并不是一种类型的所有实例都有另一种类型的对应物。另一方面,哈希映射不是迭代的最佳选择。

我认为数据结构必须在内部使用数组。我应该在这里使用哪种数据类型?在c++标准库中实现了这样的数据结构吗?

创建unordered_map以偏移到std::vector

将元素存储在std::vector中。当您想要删除一个元素时,将它与std::vector的最后一个元素交换,然后删除最后一个元素。检查unordered_map s,将索引存储到std::vector中,并修复那些指向最后一个元素的索引,使其指向最后一个元素的位置。

删除一个元素现在是O(n)。如果你一次删除一堆元素,你可以用一点创造力在一次O(n)次中完成所有元素。

添加一个元素仍然是0(1)。

遍历所有元素包括遍历std::vector。如果在迭代时需要哈希值,可以将其冗余存储在那里,或者动态计算。

std::vectorstd::unordered_map封装在强制上述规则的类型后面,否则不变量将永远不会持续存在,这将在外部暴露类似map的接口。

作为O(n)移除的替代方案,创建一个并行的std::vector来存储std::unordered_map< ??>::iterator年代。仔细跟踪std::unordered_map中发生重新散列的时间(重新散列是确定的,但您必须自己计算它何时发生),以及何时重新构建它(因为所有iterator都会因重新散列而失效)。重复哈希很少发生,所以它有一个平摊常数代价1。当您想要擦除时,现在可以在0(1)时间内更新unordered_map中的索引(记住也要更新第二个std::vector——将位置从最后一个交换到被删除的元素)。您可以将它存储在与原始数据相同的std::vector中,但这会使它变得很大(这会减慢迭代速度)。


1当容器达到max_load_factor()*bucket_count()大小时发生重列。然后bucket_count呈指数增长,元素四处移动。就像std::vector的增长算法一样,这保证了移动的元素总数是线性的——所以它保持了平摊常数插入。手动重建反向表并不比移动所有现有元素更昂贵,因此它还有助于平摊常数插入时间因子。