第10.9章: Swiss 表:无序且可扩容

English

1. 它有什么不同

str_swiss_set 与 str_swiss_map 是哈希表。它们计算键的哈希,把元素放进由该哈希选定的槽位,而不是让键保持有序。三个后果决定了你该怎么用它们:


#include <fast_io_dsal/str_swiss_set.h>
#include <fast_io.h>

using namespace ::fast_io::io;

int main()
{
	::fast_io::str_swiss_set s;
	for (auto k : {::fast_io::cstring_view("hello"), ::fast_io::cstring_view("world"),
				   ::fast_io::cstring_view("foo")})
	{
		s.insert_key(k);
	}

	println("size: ", s.size(), " capacity: ", s.capacity());
	println("contains foo: ", s.contains("foo"));
	for (auto const &e : s) { println("  ", e); }
}

size: 3 capacity: 15
contains foo: 1
  world
  hello
  foo

注意遍历顺序:既不是有序的,也不是插入顺序。

2. size()、capacity()、reserve()

哈希表必须知道自己有多满 — 它就是这样决定何时增长的。所以计数本来就在手边,你免费拿到它:


println(s.size());       // elements currently stored
println(s.capacity());   // slots currently allocated
println(s.is_empty());   // size() == 0

capacity() 与 size() 不是同一个数:表会让一部分槽位保持空闲,使探测保持简短。当表被填满时,它会增长,并把每个键重新哈希进一张更大的表。

如果你知道要来多少个键,reserve(n) 可以一次在开头把表扩好,这样后续插入不会反复触发增长:


println(s.capacity());   // 15
s.reserve(100zu);
println(s.capacity());   // 127 — big enough for 100 keys
println(s.size());       // 3 — reserve never changes the elements

15
127
3
批量装载之前先 reserve。读取一个大小未知的文件正是你无法 reserve 的情形 — 但当你知道大小时(文件头写着“1000 条记录”,或者你已经数过行数),reserve 能把一串“重新哈希并复制”的步骤变成一次分配。

3. 插入与删除

插入函数返回一个小结构体而不是裸 bool,因为哈希表知道自己碰了哪个槽位,并把迭代器交给你:


auto r{s.insert_key("alpha")};
println(r.inserted);      // 1 — the key was new
println(*r.position);     // alpha — iterator to the element

auto dup{s.insert_key("alpha")};
println(dup.inserted);    // 0 — already present, nothing changed

删除仍然是 B 树容器那套 erase_key / erase(it) / erase(first, last) 家族,边遍历边删除的循环形状也一样(第10.6章):


for (auto it{s.cbegin()}; it != s.cend();)
{
	::fast_io::string_view k{*it};
	if (k.starts_with(::fast_io::string_view("w")))
	{
		it = s.erase(it);   // returns the next occupied slot
	}
	else
	{
		++it;
	}
}

size after erase: 2
  foo
  hello

从哈希表中删除可能把某个元素搬进它留下的空洞,所以你在删除之前保存的任何迭代器或键视图都已经失效(第10.11章)。

4. 读写映射值

读是 find_key,写是赋值函数。两次调用,两种含义:


::fast_io::str_swiss_map<::std::size_t> m;
m.emplace_key("a", 1zu);                       // insert if new
m.insert_key_or_assign("a", 100zu);            // replace either way
println(m.find_key("a")->mapped());            // 100
println(m.size());                             // 1

要就地更新值而不是替换它,就用非 const 的映射做遍历 — 非 const 迭代器暴露可写的 mapped():


for (auto &e : m)
{
	e.mapped() += 1zu;
}
println(m.find_key("a")->mapped());   // 101

find_key 即使在非 const 映射上也返回 const_iterator,所以 m.find_key(k)->mapped() 只能读不能写。规则就是这些:查找只读,所有修改要么来自插入/赋值函数,要么来自遍历。

哈希器是类型的一部分(默认是 ::fast_io::native_swiss_hasher)。你很少需要写出它;如果要写,它是第三个模板参数,而 hash_function() 返回它的一份副本。

5. 什么时候 Swiss 表是正确的选择

当每个问题的答案都是“这个确切的键”时,就该用 Swiss 表:

而当问题涉及范围或顺序时,就留在 B 树这边:“所有以这个前缀开头的键”、“最小的那个”、“这个之后的下一个”。哈希表不扫遍全部就无法回答其中任何一个。

要点