mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-22 15:40:19 +00:00
c08d137fcd
Instead of rehashing on collisions, we use Robin Hood hashing: a simple linear probe where we keep track of the distance between the bucket and its ideal position. On insertion, we allow a new bucket to "steal" the position of "rich" buckets (those near their ideal position) and move them further down. On removal, we shift buckets back up into the freed slot, decrementing their distance while doing so. This behavior automatically optimizes the number of required probes for any value, and removes the need for periodic rehashing (except when expanding the capacity).
384 lines
9.6 KiB
C++
384 lines
9.6 KiB
C++
/*
|
|
* Copyright (c) 2021, thislooksfun <tlf@thislooks.fun>
|
|
* Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibTest/TestCase.h>
|
|
|
|
#include <AK/DeprecatedString.h>
|
|
#include <AK/HashTable.h>
|
|
#include <AK/NonnullOwnPtr.h>
|
|
|
|
TEST_CASE(construct)
|
|
{
|
|
using IntTable = HashTable<int>;
|
|
EXPECT(IntTable().is_empty());
|
|
EXPECT_EQ(IntTable().size(), 0u);
|
|
}
|
|
|
|
TEST_CASE(basic_move)
|
|
{
|
|
HashTable<int> foo;
|
|
foo.set(1);
|
|
EXPECT_EQ(foo.size(), 1u);
|
|
auto bar = move(foo);
|
|
EXPECT_EQ(bar.size(), 1u);
|
|
EXPECT_EQ(foo.size(), 0u);
|
|
foo = move(bar);
|
|
EXPECT_EQ(bar.size(), 0u);
|
|
EXPECT_EQ(foo.size(), 1u);
|
|
}
|
|
|
|
TEST_CASE(move_is_not_swap)
|
|
{
|
|
HashTable<int> foo;
|
|
foo.set(1);
|
|
HashTable<int> bar;
|
|
bar.set(2);
|
|
foo = move(bar);
|
|
EXPECT(foo.contains(2));
|
|
EXPECT(!bar.contains(1));
|
|
EXPECT_EQ(bar.size(), 0u);
|
|
}
|
|
|
|
TEST_CASE(populate)
|
|
{
|
|
HashTable<DeprecatedString> strings;
|
|
strings.set("One");
|
|
strings.set("Two");
|
|
strings.set("Three");
|
|
|
|
EXPECT_EQ(strings.is_empty(), false);
|
|
EXPECT_EQ(strings.size(), 3u);
|
|
}
|
|
|
|
TEST_CASE(range_loop)
|
|
{
|
|
HashTable<DeprecatedString> strings;
|
|
EXPECT_EQ(strings.set("One"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.set("Two"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.set("Three"), AK::HashSetResult::InsertedNewEntry);
|
|
|
|
int loop_counter = 0;
|
|
for (auto& it : strings) {
|
|
EXPECT_EQ(it.is_null(), false);
|
|
++loop_counter;
|
|
}
|
|
EXPECT_EQ(loop_counter, 3);
|
|
}
|
|
|
|
TEST_CASE(table_remove)
|
|
{
|
|
HashTable<DeprecatedString> strings;
|
|
EXPECT_EQ(strings.set("One"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.set("Two"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.set("Three"), AK::HashSetResult::InsertedNewEntry);
|
|
|
|
EXPECT_EQ(strings.remove("One"), true);
|
|
EXPECT_EQ(strings.size(), 2u);
|
|
EXPECT(strings.find("One") == strings.end());
|
|
|
|
EXPECT_EQ(strings.remove("Three"), true);
|
|
EXPECT_EQ(strings.size(), 1u);
|
|
EXPECT(strings.find("Three") == strings.end());
|
|
EXPECT(strings.find("Two") != strings.end());
|
|
}
|
|
|
|
TEST_CASE(remove_all_matching)
|
|
{
|
|
HashTable<int> ints;
|
|
|
|
ints.set(1);
|
|
ints.set(2);
|
|
ints.set(3);
|
|
ints.set(4);
|
|
|
|
EXPECT_EQ(ints.size(), 4u);
|
|
|
|
EXPECT_EQ(ints.remove_all_matching([&](int value) { return value > 2; }), true);
|
|
EXPECT_EQ(ints.remove_all_matching([&](int) { return false; }), false);
|
|
|
|
EXPECT_EQ(ints.size(), 2u);
|
|
|
|
EXPECT(ints.contains(1));
|
|
EXPECT(ints.contains(2));
|
|
|
|
EXPECT_EQ(ints.remove_all_matching([&](int) { return true; }), true);
|
|
|
|
EXPECT(ints.is_empty());
|
|
|
|
EXPECT_EQ(ints.remove_all_matching([&](int) { return true; }), false);
|
|
}
|
|
|
|
TEST_CASE(case_insensitive)
|
|
{
|
|
HashTable<DeprecatedString, CaseInsensitiveStringTraits> casetable;
|
|
EXPECT_EQ(DeprecatedString("nickserv").to_lowercase(), DeprecatedString("NickServ").to_lowercase());
|
|
EXPECT_EQ(casetable.set("nickserv"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(casetable.set("NickServ"), AK::HashSetResult::ReplacedExistingEntry);
|
|
EXPECT_EQ(casetable.size(), 1u);
|
|
}
|
|
|
|
TEST_CASE(many_strings)
|
|
{
|
|
HashTable<DeprecatedString> strings;
|
|
for (int i = 0; i < 999; ++i) {
|
|
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
|
|
}
|
|
EXPECT_EQ(strings.size(), 999u);
|
|
for (int i = 0; i < 999; ++i) {
|
|
EXPECT_EQ(strings.remove(DeprecatedString::number(i)), true);
|
|
}
|
|
EXPECT_EQ(strings.is_empty(), true);
|
|
}
|
|
|
|
TEST_CASE(many_collisions)
|
|
{
|
|
struct StringCollisionTraits : public GenericTraits<DeprecatedString> {
|
|
static unsigned hash(DeprecatedString const&) { return 0; }
|
|
};
|
|
|
|
HashTable<DeprecatedString, StringCollisionTraits> strings;
|
|
for (int i = 0; i < 999; ++i) {
|
|
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
|
|
}
|
|
|
|
EXPECT_EQ(strings.set("foo"), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.size(), 1000u);
|
|
|
|
for (int i = 0; i < 999; ++i) {
|
|
EXPECT_EQ(strings.remove(DeprecatedString::number(i)), true);
|
|
}
|
|
|
|
EXPECT(strings.find("foo") != strings.end());
|
|
}
|
|
|
|
TEST_CASE(space_reuse)
|
|
{
|
|
struct StringCollisionTraits : public GenericTraits<DeprecatedString> {
|
|
static unsigned hash(DeprecatedString const&) { return 0; }
|
|
};
|
|
|
|
HashTable<DeprecatedString, StringCollisionTraits> strings;
|
|
|
|
// Add a few items to allow it to do initial resizing.
|
|
EXPECT_EQ(strings.set("0"), AK::HashSetResult::InsertedNewEntry);
|
|
for (int i = 1; i < 5; ++i) {
|
|
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.remove(DeprecatedString::number(i - 1)), true);
|
|
}
|
|
|
|
auto capacity = strings.capacity();
|
|
|
|
for (int i = 5; i < 999; ++i) {
|
|
EXPECT_EQ(strings.set(DeprecatedString::number(i)), AK::HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(strings.remove(DeprecatedString::number(i - 1)), true);
|
|
}
|
|
|
|
EXPECT_EQ(strings.capacity(), capacity);
|
|
}
|
|
|
|
TEST_CASE(basic_remove)
|
|
{
|
|
HashTable<int> table;
|
|
table.set(1);
|
|
table.set(2);
|
|
table.set(3);
|
|
|
|
EXPECT_EQ(table.remove(3), true);
|
|
EXPECT_EQ(table.remove(3), false);
|
|
EXPECT_EQ(table.size(), 2u);
|
|
|
|
EXPECT_EQ(table.remove(1), true);
|
|
EXPECT_EQ(table.remove(1), false);
|
|
EXPECT_EQ(table.size(), 1u);
|
|
|
|
EXPECT_EQ(table.remove(2), true);
|
|
EXPECT_EQ(table.remove(2), false);
|
|
EXPECT_EQ(table.size(), 0u);
|
|
}
|
|
|
|
TEST_CASE(basic_contains)
|
|
{
|
|
HashTable<int> table;
|
|
table.set(1);
|
|
table.set(2);
|
|
table.set(3);
|
|
|
|
EXPECT_EQ(table.contains(1), true);
|
|
EXPECT_EQ(table.contains(2), true);
|
|
EXPECT_EQ(table.contains(3), true);
|
|
EXPECT_EQ(table.contains(4), false);
|
|
|
|
EXPECT_EQ(table.remove(3), true);
|
|
EXPECT_EQ(table.contains(3), false);
|
|
EXPECT_EQ(table.contains(1), true);
|
|
EXPECT_EQ(table.contains(2), true);
|
|
|
|
EXPECT_EQ(table.remove(2), true);
|
|
EXPECT_EQ(table.contains(2), false);
|
|
EXPECT_EQ(table.contains(3), false);
|
|
EXPECT_EQ(table.contains(1), true);
|
|
|
|
EXPECT_EQ(table.remove(1), true);
|
|
EXPECT_EQ(table.contains(1), false);
|
|
}
|
|
|
|
TEST_CASE(capacity_leak)
|
|
{
|
|
HashTable<int> table;
|
|
for (size_t i = 0; i < 10000; ++i) {
|
|
table.set(i);
|
|
table.remove(i);
|
|
}
|
|
EXPECT(table.capacity() < 100u);
|
|
}
|
|
|
|
TEST_CASE(non_trivial_type_table)
|
|
{
|
|
HashTable<NonnullOwnPtr<int>> table;
|
|
|
|
table.set(make<int>(3));
|
|
table.set(make<int>(11));
|
|
|
|
for (int i = 0; i < 1'000; ++i) {
|
|
table.set(make<int>(-i));
|
|
}
|
|
for (int i = 0; i < 10'000; ++i) {
|
|
table.set(make<int>(i));
|
|
table.remove(make<int>(i));
|
|
}
|
|
|
|
EXPECT_EQ(table.remove_all_matching([&](auto&) { return true; }), true);
|
|
EXPECT(table.is_empty());
|
|
EXPECT_EQ(table.remove_all_matching([&](auto&) { return true; }), false);
|
|
}
|
|
|
|
TEST_CASE(floats)
|
|
{
|
|
HashTable<float> table;
|
|
table.set(0);
|
|
table.set(1.0f);
|
|
table.set(2.0f);
|
|
EXPECT_EQ(table.size(), 3u);
|
|
EXPECT(table.contains(0));
|
|
EXPECT(table.contains(1.0f));
|
|
EXPECT(table.contains(2.0f));
|
|
}
|
|
|
|
TEST_CASE(doubles)
|
|
{
|
|
HashTable<double> table;
|
|
table.set(0);
|
|
table.set(1.0);
|
|
table.set(2.0);
|
|
EXPECT_EQ(table.size(), 3u);
|
|
EXPECT(table.contains(0));
|
|
EXPECT(table.contains(1.0));
|
|
EXPECT(table.contains(2.0));
|
|
}
|
|
|
|
TEST_CASE(reinsertion)
|
|
{
|
|
OrderedHashTable<DeprecatedString> map;
|
|
map.set("ytidb::LAST_RESULT_ENTRY_KEY");
|
|
map.set("__sak");
|
|
map.remove("__sak");
|
|
map.set("__sak");
|
|
}
|
|
|
|
TEST_CASE(clear_with_capacity_when_empty)
|
|
{
|
|
HashTable<int> map;
|
|
map.clear_with_capacity();
|
|
map.set(0);
|
|
map.set(1);
|
|
VERIFY(map.size() == 2);
|
|
}
|
|
|
|
TEST_CASE(iterator_removal)
|
|
{
|
|
HashTable<int> map;
|
|
map.set(0);
|
|
map.set(1);
|
|
|
|
auto it = map.begin();
|
|
map.remove(it);
|
|
EXPECT_EQ(it, map.end());
|
|
EXPECT_EQ(map.size(), 1u);
|
|
}
|
|
|
|
TEST_CASE(ordered_insertion_and_deletion)
|
|
{
|
|
OrderedHashTable<int> table;
|
|
EXPECT_EQ(table.set(0), HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(table.set(1), HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(table.set(2), HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(table.set(3), HashSetResult::InsertedNewEntry);
|
|
EXPECT_EQ(table.size(), 4u);
|
|
|
|
auto expect_table = [](OrderedHashTable<int>& table, Span<int> values) {
|
|
auto index = 0u;
|
|
for (auto it = table.begin(); it != table.end(); ++it, ++index) {
|
|
EXPECT_EQ(*it, values[index]);
|
|
EXPECT(table.contains(values[index]));
|
|
}
|
|
};
|
|
|
|
expect_table(table, Array<int, 4> { 0, 1, 2, 3 });
|
|
|
|
EXPECT(table.remove(0));
|
|
EXPECT(table.remove(2));
|
|
EXPECT(!table.remove(4));
|
|
EXPECT_EQ(table.size(), 2u);
|
|
|
|
expect_table(table, Array<int, 2> { 1, 3 });
|
|
}
|
|
|
|
TEST_CASE(ordered_deletion_and_reinsertion)
|
|
{
|
|
OrderedHashTable<int> table;
|
|
table.set(1);
|
|
table.set(3);
|
|
table.remove(1);
|
|
EXPECT_EQ(table.size(), 1u);
|
|
|
|
// By adding 1 again but this time in a different position, we
|
|
// test whether the bucket's neighbours are reset properly.
|
|
table.set(1);
|
|
EXPECT_EQ(table.size(), 2u);
|
|
|
|
auto it = table.begin();
|
|
EXPECT_EQ(*it, 3);
|
|
++it;
|
|
EXPECT_EQ(*it, 1);
|
|
++it;
|
|
EXPECT_EQ(it, table.end());
|
|
}
|
|
|
|
TEST_CASE(ordered_pop)
|
|
{
|
|
OrderedHashTable<int> table;
|
|
table.set(1);
|
|
table.set(2);
|
|
table.set(3);
|
|
|
|
EXPECT_EQ(table.pop(), 3);
|
|
EXPECT_EQ(table.pop(), 2);
|
|
EXPECT_EQ(table.pop(), 1);
|
|
EXPECT(table.is_empty());
|
|
}
|
|
|
|
TEST_CASE(ordered_iterator_removal)
|
|
{
|
|
OrderedHashTable<int> map;
|
|
map.set(0);
|
|
map.set(1);
|
|
|
|
auto it = map.begin();
|
|
map.remove(it);
|
|
EXPECT_EQ(it, map.end());
|
|
EXPECT_EQ(map.size(), 1u);
|
|
}
|