libguf/src/guf_dict.c
2025-01-03 10:38:57 +01:00

650 lines
22 KiB
C
Executable File

// #include <stdint.h>
// #include <stdbool.h>
// #include <stdlib.h>
// #include <string.h>
// #include <stdio.h>
// #include "guf_common.h"
// #include "guf_dict.h"
// /*
// FNV-1a (32 bit) hash function.
// Generally, you should always call csr_hash with GUF_HASH_INIT as the hash argument, unless you want to create "chains" of hashes.
// cf. http://www.isthe.com/chongo/tech/comp/fnv/ (last retrieved: 2023-11-30)
// */
// uint32_t guf_hash32(const void *data, size_t num_bytes, uint32_t hash)
// {
// GUF_ASSERT_RELEASE(data);
// const unsigned char *data_bytes = (const unsigned char*)data; // This does not break strict-aliasing rules I think...
// const uint32_t FNV_32_PRIME = 16777619ul;
// for (size_t i = 0; i < num_bytes; ++i) {
// hash ^= data_bytes[i];
// hash *= FNV_32_PRIME;
// }
// return hash;
// }
// uint64_t guf_hash64(const void *data, size_t num_bytes, uint64_t hash)
// {
// GUF_ASSERT_RELEASE(data);
// const unsigned char *data_bytes = (const unsigned char*)data; // This does not break strict-aliasing rules I think...
// const uint64_t FNV_64_PRIME = 1099511628211ull;
// for (size_t i = 0; i < num_bytes; ++i) {
// hash ^= data_bytes[i];
// hash *= FNV_64_PRIME;
// }
// return hash;
// }
// static inline size_t find_next_power_of_two(size_t num)
// {
// GUF_ASSERT_RELEASE(num > 0);
// size_t pof2 = 1;
// while (pof2 < num) {
// GUF_ASSERT_RELEASE(pof2 * 2 > pof2);
// pof2 *= 2;
// }
// return pof2;
// }
// static const guf_dict_kv_id KV_ID_NULL = GUF_DICT_HASH_MAX;
// static const guf_dict_kv_id KV_ID_TOMBSTONE = GUF_DICT_HASH_MAX - 1;
// static const guf_dict_kv_id KV_ID_MAX = GUF_DICT_HASH_MAX - 2;
// const guf_dict GUF_DICT_UNINITIALISED = {
// .capacity_kv_status = 0,
// .size = 0,
// .num_tombstones = 0,
// .keys = NULL,
// .vals = NULL,
// .val_funcs = {.eq = NULL, .hash = NULL, .cpy = NULL, .move = NULL, .free = NULL, .type_size = 0},
// .kv_status = NULL,
// .probe_t = 0,
// .max_load_fac_fx10 = 0,
// .max_probelen = 0,
// };
// static inline void *cpy_key(guf_dict *ht, void *dst, const void *src, bool default_cpy)
// {
// if (default_cpy || ht->key_funcs.cpy == NULL) { // Default copy.
// return memcpy(dst, src, ht->key_funcs.type_size);
// } else {
// return ht->key_funcs.cpy(dst, src);
// }
// }
// static inline void *cpy_val(guf_dict *ht, void *dst, const void *src, bool default_cpy)
// {
// if (dst == NULL || src == NULL) {
// GUF_ASSERT_RELEASE(ht->val_funcs.type_size == 0);
// return NULL;
// }
// if (default_cpy || ht->val_funcs.cpy == NULL) { // Default copy.
// return memcpy(dst, src, ht->val_funcs.type_size);
// } else {
// return ht->val_funcs.cpy(dst, src);
// }
// }
// static inline void *cpy_or_move_val(guf_dict *ht, void *dst, void *src, guf_dict_insert_opt opts)
// {
// if (dst == NULL || src == NULL) {
// GUF_ASSERT_RELEASE(ht->val_funcs.type_size == 0);
// return NULL;
// }
// if ((opts & GUF_DICT_MOVE_VAL)) {
// GUF_ASSERT_RELEASE(ht->val_funcs.move);
// return ht->val_funcs.move(dst, src);
// } else { // Default copy.
// return cpy_val(ht, dst, src, false);
// }
// }
// static inline void *cpy_or_move_key(guf_dict *ht, void *dst, void *src, guf_dict_insert_opt opts)
// {
// if ((opts & GUF_DICT_MOVE_KEY)) {
// GUF_ASSERT_RELEASE(ht->key_funcs.move);
// return ht->key_funcs.move(dst, src);
// } else {
// return cpy_key(ht, dst, src, false);
// }
// }
// static inline guf_hash_size_t key_hash(const guf_dict *ht, const void *key)
// {
// if (ht->key_funcs.hash) {
// return ht->key_funcs.hash(key);
// } else { // Default hash function.
// return guf_hash(key, ht->key_funcs.type_size, GUF_HASH_INIT);
// }
// }
// static inline bool kv_stat_occupied(guf_dict_kv_status kv_stat) {
// return kv_stat.kv_id != KV_ID_NULL && kv_stat.kv_id != KV_ID_TOMBSTONE;
// }
// static void *key_get(guf_dict *ht, guf_dict_kv_id idx)
// {
// GUF_ASSERT_RELEASE(idx <= KV_ID_MAX);
// char *ptr = (char*)ht->keys;
// return ptr + idx * ht->key_funcs.type_size;
// }
// static void *val_get(guf_dict *ht, guf_dict_kv_id idx)
// {
// if (ht->val_funcs.type_size == 0) {
// return NULL;
// }
// GUF_ASSERT_RELEASE(idx <= KV_ID_MAX);
// char *ptr = (char*)ht->vals;
// return ptr + idx * ht->val_funcs.type_size ;
// }
// static inline bool key_eq(guf_dict *ht, const void *key_a, const void *key_b)
// {
// if (ht->key_funcs.eq) {
// return ht->key_funcs.eq(key_a, key_b);
// } else { // Default equality function.
// return 0 == memcmp(key_a, key_b, ht->key_funcs.type_size);
// }
// }
// static inline bool key_eq_at(guf_dict *ht, size_t idx, const void *key, guf_hash_size_t hash_of_key)
// {
// GUF_ASSERT(idx < ht->capacity_kv_status);
// GUF_ASSERT(kv_stat_occupied(ht->kv_status[idx]));
// if (ht->kv_status[idx].k_hash == hash_of_key) { // Hashes match -> we check if the keys are actually equal.
// return key_eq(ht, key_get(ht, ht->kv_status[idx].kv_id), key);
// } else {
// // Hashes don't match -> early exit (we save a memory load from ht->kv_elems).
// return false;
// }
// }
// static inline size_t probe_offset(size_t probe_len, guf_dict_probe_type probe_t)
// {
// GUF_ASSERT(probe_len > 0);
// switch (probe_t)
// {
// case GUF_DICT_PROBE_LINEAR:
// default:
// return 1;
// case GUF_DICT_PROBE_QUADRATIC:
// /*
// Guaranteed to visit each index once for capacities which are powers of two.
// cf. https://fgiesen.wordpress.com/2015/02/22/triangular-numbers-mod-2n/ (last-retrieved 2024-07-29)
// */
// return probe_len * (probe_len + 1) / 2; // 1, 3, 6, 10, 15, ... (starting from probe_len == 1)
// }
// }
// static inline size_t mod_pow2(size_t a, size_t b) // a mod b (with b being a power of two.)
// {
// GUF_ASSERT(b > 0);
// return a & (b - 1);
// }
// static size_t find_idx(guf_dict *ht, const void *key, bool *key_exists, bool find_first_free)
// {
// const guf_hash_size_t hash = key_hash(ht, key);
// size_t idx = mod_pow2(hash, ht->capacity_kv_status); // hash % ht->capacity
// const size_t start_idx = idx;
// size_t probe_len = 1;
// size_t first_tombstone_idx = SIZE_MAX;
// do {
// if (ht->kv_status[idx].kv_id == KV_ID_NULL) { // 1.) Empty.
// if (first_tombstone_idx != SIZE_MAX) {
// idx = first_tombstone_idx;
// }
// ht->max_probelen = GUF_MAX(probe_len, ht->max_probelen);
// GUF_ASSERT(!kv_stat_occupied(ht->kv_status[idx]));
// *key_exists = false;
// return idx;
// } else if (ht->kv_status[idx].kv_id == KV_ID_TOMBSTONE) { // 2.) Tombstone.
// if (first_tombstone_idx == SIZE_MAX) {
// first_tombstone_idx = idx;
// }
// if (find_first_free) {
// goto end;
// } else {
// goto probe;
// }
// } else if (key_eq_at(ht, idx, key, hash)) { // 3.) Key already exists.
// ht->max_probelen = GUF_MAX(probe_len, ht->max_probelen);
// GUF_ASSERT(kv_stat_occupied(ht->kv_status[idx]));
// *key_exists = true;
// return idx;
// } else { // 4.) Have to probe due to hash-collision (idx is already occupied, but not by the key).
// probe:
// idx = mod_pow2(idx + probe_offset(probe_len, ht->probe_t), ht->capacity_kv_status);
// ++probe_len;
// GUF_ASSERT_RELEASE(probe_len < UINT32_MAX);
// }
// } while (idx != start_idx);
// end:
// if (first_tombstone_idx != SIZE_MAX) { // Edge case: No empty slots, but found tombstone.
// ht->max_probelen = GUF_MAX(probe_len, ht->max_probelen);
// GUF_ASSERT(!kv_stat_occupied(ht->kv_status[first_tombstone_idx]));
// *key_exists = false;
// return first_tombstone_idx;
// }
// *key_exists = false;
// return SIZE_MAX; // Failed to find an idx.
// }
// static void insert_kv(guf_dict *ht, void *key, void *val, size_t idx, guf_dict_insert_opt opts, bool default_cpy_key, bool default_cpy_val)
// {
// GUF_ASSERT_RELEASE(idx < ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(ht->kv_status[idx].kv_id == KV_ID_NULL || ht->kv_status[idx].kv_id == KV_ID_TOMBSTONE);
// GUF_ASSERT_RELEASE(!kv_stat_occupied(ht->kv_status[idx]));
// if (!default_cpy_key) {
// if (!cpy_or_move_key(ht, key_get(ht, idx), key, opts)) {
// cpy_key(ht, key_get(ht, idx), key, true);
// }
// } else {
// cpy_key(ht, key_get(ht, idx), key, true);
// }
// if (!default_cpy_val) {
// if (!cpy_or_move_val(ht, val_get(ht, idx), val, opts)) {
// cpy_val(ht, val_get(ht, idx), val, true);
// }
// } else {
// cpy_val(ht, val_get(ht, idx), val, true);
// }
// if (ht->kv_status[idx].kv_id == KV_ID_TOMBSTONE) {
// GUF_ASSERT_RELEASE(ht->num_tombstones > 0);
// --ht->num_tombstones;
// }
// ht->kv_status[idx].k_hash = key_hash(ht, key_get(ht, idx));
// ++ht->size;
// }
// static void update_v(guf_dict *ht, void *val, size_t idx, guf_dict_insert_opt opts)
// {
// GUF_ASSERT_RELEASE(idx < ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(kv_stat_occupied(ht->kv_status[idx]));
// if (ht->val_funcs.free) {
// ht->val_funcs.free(val_get(ht, idx));
// }
// cpy_or_move_val(ht, val_get(ht, idx), val, opts);
// }
// bool guf_dict_init(guf_dict *ht, size_t start_capacity, const guf_dict_kv_funcs *key_funcs, const guf_dict_kv_funcs *val_funcs)
// {
// GUF_ASSERT_RELEASE(ht);
// GUF_ASSERT_RELEASE(ht->capacity_kv_status == 0 && ht->size == 0 && ht->num_tombstones == 0 && ht->max_probelen == 0);
// GUF_ASSERT_RELEASE(ht->keys == NULL && ht->vals == NULL);
// GUF_ASSERT_RELEASE(key_funcs && val_funcs);
// ht->key_funcs = *key_funcs;
// ht->val_funcs = *val_funcs;
// if (val_funcs->type_size == 0) {
// // TODO: is a set!
// }
// if (start_capacity < 1) {
// start_capacity = 1;
// }
// ht->capacity_kv_status = find_next_power_of_two(start_capacity);
// ht->size = ht->num_tombstones = 0;
// ht->max_probelen = 0;
// GUF_ASSERT_RELEASE(ht->key_funcs.type_size > 0);
// // GUF_ASSERT_RELEASE(ht->val_funcs.type_size > 0);
// ht->probe_t = GUF_DICT_PROBE_QUADRATIC;
// ht->max_load_fac_fx10 = GUF_DICT_MAX_LOAD_FAC_FX10_DEFAULT;
// GUF_ASSERT_RELEASE(ht->max_load_fac_fx10 != 0 && ht->max_load_fac_fx10 <= 1024);
// ht->keys = calloc(ht->capacity_kv_status, ht->val_funcs.type_size);
// if (!ht->keys) {
// return false;
// }
// ht->vals = NULL;
// if (ht->val_funcs.type_size > 0) {
// ht->vals = calloc(ht->capacity_kv_status, ht->val_funcs.type_size);
// if (!ht->vals) {
// free(ht->keys);
// return false;
// }
// }
// ht->kv_status = calloc(ht->capacity_kv_status, sizeof(uint8_t));
// if (!ht->kv_status) {
// free(ht->keys);
// free(ht->vals);
// return false;
// }
// for (size_t i = 0; i < ht->capacity_kv_status; ++i) {
// GUF_ASSERT(ht->kv_status[i].kv_id == KV_ID_NULL);
// }
// return ht;
// }
// bool guf_dict_insert(guf_dict *ht, void *key, void *val, guf_dict_insert_opt opts)
// {
// GUF_ASSERT_RELEASE(ht);
// GUF_ASSERT_RELEASE(ht->capacity_kv_status > 0 && ht->size <= ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(ht->keys && ht->vals && ht->kv_status);
// if ((opts & GUF_DICT_MOVE_KEY) && ht->key_funcs.move == NULL) {
// // Ignore -Wunused-value.
// fprintf(stderr, "guf_dict_insert: key_funcs.move is NULL while GUF_DICT_MOVE_KEY is set\n");
// GUF_ASSERT(false);
// return false;
// }
// if ((opts & GUF_DICT_MOVE_VAL) && ht->val_funcs.move == NULL) {
// // Ignore -Wunused-value.
// fprintf(stderr, "guf_dict_insert: val_funcs.move is NULL while GUF_DICT_MOVE_VAL is set\n");
// GUF_ASSERT(false);
// return false;
// }
// if (guf_dict_load_factor_fx10(ht) > ht->max_load_fac_fx10 || ht->capacity_kv_status == ht->size) { // Handle growth:
// const size_t new_cap = 2 * ht->capacity_kv_status; // TODO: Limit to MAX_CAPACITY
// bool overflow = new_cap <= ht->capacity_kv_status;
// GUF_ASSERT(!overflow);
// if (overflow) {
// return false;
// }
// void *new_keys = calloc(new_cap, ht->key_funcs.type_size);
// if (!new_keys) {
// return false;
// }
// void *new_vals = NULL;
// if (ht->val_funcs.type_size > 0) {
// new_vals = calloc(new_cap, ht->val_funcs.type_size);
// if (!new_vals) {
// free(new_keys);
// return false;
// }
// } else {
// GUF_ASSERT_RELEASE(ht->vals == NULL);
// }
// guf_dict_kv_status *new_kv_status = calloc(new_cap, sizeof(guf_dict_kv_status)); // No realloc here!
// if (!new_kv_status) {
// free(new_keys);
// free(new_vals);
// return false;
// }
// for (size_t i = 0; i < new_cap; ++i) {
// new_kv_status[i].kv_id = KV_ID_NULL;
// new_kv_status[i].k_hash = 0;
// }
// guf_dict ht_new = *ht;
// ht_new.kv_status = new_kv_status;
// ht_new.keys = new_keys;
// ht_new.vals = new_vals;
// ht_new.size = 0;
// ht_new.capacity_kv_status = new_cap;
// ht_new.max_probelen = 0;
// size_t new_size = 0;
// for (size_t i = 0; i < ht->capacity_kv_status; ++i) {
// if (kv_stat_occupied(ht->kv_status[i])) {
// bool key_exists = false;
// const size_t new_idx = find_idx(&ht_new, key_get(ht, i), &key_exists, true);
// GUF_ASSERT_RELEASE(new_idx != SIZE_MAX);
// GUF_ASSERT_RELEASE(!key_exists);
// bool dumb_copy_key = ht->key_funcs.move == NULL;
// bool dumb_copy_val = ht->val_funcs.move == NULL;
// insert_kv(&ht_new, key_get(ht, i), val_get(ht, i), new_idx, GUF_DICT_MOVE_KEY | GUF_DICT_MOVE_VAL, dumb_copy_key, dumb_copy_val);
// ++new_size;
// }
// }
// GUF_ASSERT_RELEASE(new_size == ht->size);
// free(ht->kv_status);
// free(ht->keys);
// free(ht->vals);
// *ht = ht_new;
// }
// bool key_exists = false;
// const size_t idx = find_idx(ht, key, &key_exists, true);
// GUF_ASSERT_RELEASE(idx != SIZE_MAX);
// GUF_ASSERT_RELEASE(!kv_stat_occupied(ht->kv_status[idx]));
// if (key_exists) {
// update_v(ht, val, idx, opts);
// } else {
// insert_kv(ht, key, val, idx, opts, false, false);
// }
// return true;
// }
// bool guf_dict_contains_key(const guf_dict *ht, const void *key)
// {
// GUF_ASSERT_RELEASE(ht);
// GUF_ASSERT_RELEASE(ht->capacity_kv_status > 0 && ht->size <= ht->capacity_kv_status);
// GUF_ASSERT_RELEASE (ht->keys && ht->vals && ht->kv_status);
// bool key_exists = false;
// const size_t idx = find_idx((guf_dict*)ht, key, &key_exists, false); // TODO: const cast
// GUF_ASSERT_RELEASE(!(key_exists && idx == SIZE_MAX));
// return key_exists;
// }
// void *guf_dict_get_val(guf_dict *ht, const void *key)
// {
// GUF_ASSERT_RELEASE(ht);
// GUF_ASSERT_RELEASE(ht->capacity_kv_status > 0 && ht->size <= ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(ht->keys && ht->vals && ht->kv_status);
// if (ht->capacity_kv_status == 0 || ht->size == 0) {
// return NULL;
// }
// bool key_exists = false;
// const size_t idx = find_idx(ht, key, &key_exists, false);
// GUF_ASSERT_RELEASE(idx != SIZE_MAX);
// if (!key_exists) {
// return NULL;
// } else {
// GUF_ASSERT(kv_stat_occupied(ht->kv_status[idx]));
// return val_get(ht, idx);
// }
// }
// bool guf_dict_remove(guf_dict *ht, const void *key)
// {
// GUF_ASSERT_RELEASE(ht);
// GUF_ASSERT_RELEASE(ht->capacity_kv_status > 0 && ht->size <= ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(ht->keys && ht->vals && ht->kv_status);
// if (ht->size == 0) {
// return false;
// }
// bool key_exists = false;
// const size_t idx = find_idx(ht, key, &key_exists, false);
// if (!key_exists) {
// return false;
// }
// if (ht->key_funcs.free) {
// ht->key_funcs.free(key_get(ht, idx));
// }
// if (ht->val_funcs.free) {
// ht->val_funcs.free(val_get(ht, idx));
// }
// ht->kv_status[idx].kv_id = KV_ID_TOMBSTONE;
// --ht->size;
// ++ht->num_tombstones;
// GUF_ASSERT(ht->size + ht->num_tombstones <= ht->capacity_kv_status);
// return true;
// }
// uint32_t guf_dict_load_factor_fx10(const guf_dict *ht)
// {
// const uint64_t fx10_scale = 1024; // 2^10 (represents 1 in fx10 fixed point format).
// GUF_ASSERT_RELEASE(ht->capacity_kv_status >= ht->size);
// if (ht->capacity_kv_status == 0) {
// return 0;
// }
// size_t size = ht->size + ht->num_tombstones;
// // <=> size * fx_scale * fx_scale) / (ht->capacity * fx_scale)
// GUF_ASSERT(size <= ht->capacity_kv_status);
// uint64_t load_fac = (size * fx10_scale) / (uint64_t)(ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(load_fac <= fx10_scale);
// return (uint32_t)load_fac;
// }
// double guf_dict_load_factor_double(const guf_dict *ht)
// {
// GUF_ASSERT_RELEASE(ht->capacity_kv_status >= ht->size);
// if (ht->capacity_kv_status == 0 || ht->capacity_kv_status == 0) {
// return 0.f;
// }
// size_t size = ht->size + ht->num_tombstones;
// GUF_ASSERT(size <= ht->capacity_kv_status);
// return size / (double)(ht->capacity_kv_status);
// }
// void guf_dict_free(guf_dict *ht)
// {
// if (!ht) {
// return;
// }
// if (!ht->kv_status && !ht->keys && !ht->vals && ht->size == 0) {
// return;
// }
// for (size_t i = 0; i < ht->size; ++i) { // TODO: ht->size, not ht->capacity ?
// if (ht->keys && ht->key_funcs.free) {
// ht->key_funcs.free(key_get(ht, i));
// }
// if (ht->vals && ht->val_funcs.free) {
// ht->val_funcs.free(val_get(ht, i));
// }
// }
// free(ht->keys);
// free(ht->vals);
// free(ht->kv_status);
// ht->keys = NULL;
// ht->vals = NULL;
// ht->kv_status = NULL;
// ht->capacity_kv_status = ht->size = ht->num_tombstones = ht->max_probelen = 0;
// *ht = GUF_DICT_UNINITIALISED;
// }
// guf_dict_iter guf_dict_iter_begin(guf_dict *ht)
// {
// guf_dict_iter iter = {.elems_seen = ht->size + 1, .idx = 0, .ht = ht, .key = NULL, .val = NULL};
// if (ht->size == 0) {
// return iter; // end iter
// }
// for (size_t idx = 0; idx < ht->capacity_kv_status; ++idx) {
// if (kv_stat_occupied(ht->kv_status[idx])) {
// iter.idx = idx;
// iter.elems_seen = 1;
// iter.key = key_get(iter.ht, iter.idx);
// iter.val = val_get(iter.ht, iter.idx);
// GUF_ASSERT_RELEASE(iter.key != NULL);
// return iter;
// }
// }
// return iter; // end iter
// }
// bool guf_dict_iter_is_end(guf_dict_iter *iter)
// {
// return iter->elems_seen == iter->ht->size + 1;
// }
// void guf_dict_iter_advance(guf_dict_iter *iter)
// {
// if (guf_dict_iter_is_end(iter)) {
// return;
// }
// if (iter->elems_seen == iter->ht->size) {
// ++iter->elems_seen;
// iter->key = NULL;
// iter->val = NULL;
// return;
// }
// GUF_ASSERT_RELEASE(iter->elems_seen < iter->ht->size);
// GUF_ASSERT_RELEASE(iter->idx < iter->ht->capacity_kv_status);
// GUF_ASSERT_RELEASE(iter->key);
// for (size_t idx = iter->idx + 1; idx < iter->ht->capacity_kv_status; ++idx) {
// if (kv_stat_occupied(iter->ht->kv_status[idx])) {
// iter->idx = idx;
// iter->key = key_get(iter->ht, iter->idx);
// iter->val = val_get(iter->ht, iter->idx);
// ++iter->elems_seen;
// return;
// }
// }
// GUF_ASSERT_RELEASE(false);
// iter->elems_seen = iter->ht->size + 1;
// }
// /*
// Removal of keys without tombstones (only would work for linear probing I think).
// cf. https://stackoverflow.com/questions/9127207/hash-table-why-deletion-is-difficult-in-open-addressing-scheme/24886657#24886657 (last-retrieved 2024-07-26)
// The following del function from https://github.com/attractivechaos/klib/blob/6f73c80c6409d6f91cdf66ec1a002177274da2e7/cpp/khashl.hpp#L142-L150 (last-retrieved 2024-07-26)
// int del(khint_t i) {
// khint_t j = i, k, mask, nb = n_buckets();
// if (keys == 0 || i >= nb) return 0;
// mask = nb - khint_t(1);
// while (1) {
// j = (j + khint_t(1)) & mask;
// if (j == i || !__kh_used(used, j)) break; //j==i only when the table is completely full
// k = __kh_h2b(Hash()(keys[j]), bits);
// if (k <= i || k > j)
// keys[i] = keys[j], i = j;
// }
// __kh_set_unused(used, i);
// --count;
// return 1;
// }
// cf. https://en.wikipedia.org/w/index.php?title=Hash_table&oldid=95275577 (last-retrieved 2024-07-26)
// Note:
// - For all records in a cluster, there must be no vacant slots between their natural hash position
// and their current position (else lookups will terminate before finding the record).
// - i is a vacant slot that might be invalidating this property for subsequent records in the cluster.
// - j is such a subsequent record.
// - k is the raw hash where the record at j would naturally land in the hash table if there were no collisions.
// - This test is asking if the record at j is invalidly positioned with respect
// to the required properties of a cluster now that i is vacant.
// */