目前,我们的哈希表有固定数量的桶。随着更多的项被插入,表开始填满。这是有问题的,原因有二:
哈希表的性能随着高碰撞率而降低
我们的哈希表只能存储固定数量的项。如果我们试图存储更多的数据,插入函数就会失败。
为了缓解这个问题,我们可以在项目数组太满时增加它的大小。我们将散列表中存储的项数存储在表的count属性中。在每次插入和删除操作中,我们都要计算表的“负载”,即满桶与总桶的比率。如果它高于或低于某些值,我们将调整桶的大小。
我们将调整:
up, if load > 0.7
down, if load < 0.1
为了调整大小,我们创建一个新的哈希表,其大小大约是当前哈希表的一半或两倍,并将所有未删除的项插入其中。
新的数组大小应该是一个质数,大约是当前大小的两倍。找到新的数组大小并不是一件小事。为此,我们存储一个基本大小(我们希望数组是这个大小),然后将实际大小定义为第一个大于基本大小的素数。为了调整大小,我们将基本大小加倍,并找到第一个较大的素数。
我们的基本尺寸从50开始。而不是存储
我们使用蛮力方法来寻找下一个质数,方法是检查每个连续的数是否是质数。虽然蛮力听起来令人担忧,但我们实际上必须检查的值的数量很低,而且所花费的时间与重新哈希表中每个项目所花费的时间相比是微不足道的。
首先,我们定义一个函数来寻找下一个质数。我们将在两个新文件中完成,prime.h和prime.c。
// prime.h
int is_prime(const int x);
int next_prime(int x);
// prime.c
#include
#include "prime.h"
/*
* Return whether x is prime or not
*
* Returns:
* 1 - prime
* 0 - not prime
* -1 - undefined (i.e. x < 2)
*/
int is_prime(const int x) {
if (x < 2) { return -1; }
if (x < 4) { return 1; }
if ((x % 2) == 0) { return 0; }
for (int i = 3; i <= floor(sqrt((double) x)); i += 2) {
if ((x % i) == 0) {
return 0;
}
}
return 1;
}
/*
* Return the next prime after x, or x if x is prime
*/
int next_prime(int x) {
while (is_prime(x) != 1) {
x++;
}
return x;
}
接下来,我们需要更新ht_new函数,以支持创建特定大小的散列表。为此,我们将创建一个新函数ht_new_sized。我们将ht_new更改为使用默认的初始大小调用ht_new_sized。
// hash_table.c
static ht_hash_table* ht_new_sized(const int base_size) {
ht_hash_table* ht = xmalloc(sizeof(ht_hash_table));
ht->base_size = base_size;
ht->size = next_prime(ht->base_size);
ht->count = 0;
ht->items = xcalloc((size_t)ht->size, sizeof(ht_item*));
return ht;
}
ht_hash_table* ht_new() {
return ht_new_sized(HT_INITIAL_BASE_SIZE);
}
现在,我们有了所有需要编写resize函数的部分。
在我们的resize函数中,我们检查以确保我们没有试图将哈希表的大小降低到最小值以下。然后,我们初始化一个具有所需大小的新散列表。所有非空或已删除的项都插入到新散列表中。然后在删除旧哈希表之前交换新哈希表和旧哈希表的属性。
// hash_table.c
static void ht_resize(ht_hash_table* ht, const int base_size) {
if (base_size < HT_INITIAL_BASE_SIZE) {
return;
}
ht_hash_table* new_ht = ht_new_sized(base_size);
for (int i = 0; i < ht->size; i++) {
ht_item* item = ht->items[i];
if (item != NULL && item != &HT_DELETED_ITEM) {
ht_insert(new_ht, item->key, item->value);
}
}
ht->base_size = new_ht->base_size;
ht->count = new_ht->count;
// To delete new_ht, we give it ht's size and items
const int tmp_size = ht->size;
ht->size = new_ht->size;
new_ht->size = tmp_size;
ht_item** tmp_items = ht->items;
ht->items = new_ht->items;
new_ht->items = tmp_items;
ht_del_hash_table(new_ht);
}
为了简化调整大小,我们定义了两个用于调整大小的小函数。
// hash_table.c
static void ht_resize_up(ht_hash_table* ht) {
const int new_size = ht->base_size * 2;
ht_resize(ht, new_size);
}
static void ht_resize_down(ht_hash_table* ht) {
const int new_size = ht->base_size / 2;
ht_resize(ht, new_size);
}
要执行调整大小,我们检查插入和删除时哈希表的负载。如果它高于或低于0.7和0.1的预定义限制,我们将分别调整大小。
为了避免做浮点运算,我们将计数乘以100,然后检查它是大于还是小于70或10。
// hash_table.c
void ht_insert(ht_hash_table* ht, const char* key, const char* value) {
const int load = ht->count * 100 / ht->size;
if (load > 70) {
ht_resize_up(ht);
}
// ...
}
void ht_delete(ht_hash_table* ht, const char* key) {
const int load = ht->count * 100 / ht->size;
if (load < 10) {
ht_resize_down(ht);
}
// ...
}