哈希表

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define INIT_CAP 8
#define LOAD_FACTOR 0.75

typedef struct Entry {
    char *key;           // 拥有所有权(strdup 复制)
    int value;
    struct Entry *next;  // 链地址法
} Entry;

typedef struct {
    Entry **buckets;
    size_t cap;    // 桶数量
    size_t size;   // 键值对数量
} Dict;

/* ---------- 哈希函数:djb2 ---------- */
static unsigned long hash_str(const char *s) {
    unsigned long h = 5381;
    int c;
    while ((c = *s++))
        h = ((h << 5) + h) + c;   // h * 33 + c
    return h;
}

/* ---------- 创建/销毁 ---------- */
Dict *dict_new(void) {
    Dict *d = malloc(sizeof(Dict));
    if (!d) return NULL;
    d->cap = INIT_CAP;
    d->size = 0;
    d->buckets = calloc(d->cap, sizeof(Entry *));
    if (!d->buckets) { free(d); return NULL; }
    return d;
}

void dict_free(Dict *d) {
    if (!d) return;
    for (size_t i = 0; i < d->cap; i++) {
        Entry *e = d->buckets[i];
        while (e) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
    }
    free(d->buckets);
    free(d);
}

/* ---------- 扩容 ---------- */
static void dict_resize(Dict *d, size_t new_cap) {
    Entry **new_buckets = calloc(new_cap, sizeof(Entry *));
    if (!new_buckets) return;   // 扩容失败则保持原样

    for (size_t i = 0; i < d->cap; i++) {
        Entry *e = d->buckets[i];
        while (e) {
            Entry *next = e->next;
            size_t idx = hash_str(e->key) % new_cap;
            e->next = new_buckets[idx];
            new_buckets[idx] = e;
            e = next;
        }
    }
    free(d->buckets);
    d->buckets = new_buckets;
    d->cap = new_cap;
}

/* ---------- 插入/更新 ---------- */
void dict_put(Dict *d, const char *key, int value) {
    size_t idx = hash_str(key) % d->cap;

    // 已存在则更新
    for (Entry *e = d->buckets[idx]; e; e = e->next) {
        if (strcmp(e->key, key) == 0) {
            e->value = value;
            return;
        }
    }

    // 头插新节点
    Entry *e = malloc(sizeof(Entry));
    if (!e) return;
    e->key = strdup(key);       // 复制键(POSIX;C23 也标准化了)
    if (!e->key) { free(e); return; }
    e->value = value;
    e->next = d->buckets[idx];
    d->buckets[idx] = e;
    d->size++;

    // 负载因子检查
    if ((double)d->size / d->cap > LOAD_FACTOR)
        dict_resize(d, d->cap * 2);
}

/* ---------- 查找 ---------- */
int dict_get(Dict *d, const char *key, int *out) {
    size_t idx = hash_str(key) % d->cap;
    for (Entry *e = d->buckets[idx]; e; e = e->next) {
        if (strcmp(e->key, key) == 0) {
            *out = e->value;
            return 1;   // 找到
        }
    }
    return 0;           // 没找到
}

/* ---------- 删除 ---------- */
int dict_remove(Dict *d, const char *key) {
    size_t idx = hash_str(key) % d->cap;
    Entry **pp = &d->buckets[idx];
    while (*pp) {
        Entry *e = *pp;
        if (strcmp(e->key, key) == 0) {
            *pp = e->next;
            free(e->key);
            free(e);
            d->size--;
            return 1;
        }
        pp = &e->next;
    }
    return 0;
}

/* ---------- 测试 ---------- */
int main(void) {
    Dict *d = dict_new();

    dict_put(d, "apple", 10);
    dict_put(d, "banana", 20);
    dict_put(d, "cherry", 30);
    dict_put(d, "apple", 99);   // 更新

    int v;
    if (dict_get(d, "apple", &v))  printf("apple = %d\n", v);   // 99
    if (dict_get(d, "banana", &v)) printf("banana = %d\n", v);  // 20
    if (!dict_get(d, "grape", &v)) printf("grape 不存在\n");

    dict_remove(d, "banana");
    if (!dict_get(d, "banana", &v)) printf("banana 已删除\n");

    printf("size = %zu, cap = %zu\n", d->size, d->cap);
    dict_free(d);
    return 0;
}

  malloc 只分配、不清零;calloc 分配并清零,还带乘法溢出检查。
const char *s 表示"s 指向的字符不可通过 s 修改"。加它是为了:① 承诺函数只读不写;② 能接收字符串字面量和 const 变量(否则传 "apple" 会警告/报错);③ 编译期防止误改;④ 自文档化。

static + const char* 组合是 C 里工具函数的经典写法:私有 + 只读输入。

 

while ((c = *s++))
        h = ((h << 5) + h) + c;   // h * 33 + c
// 等价的"笨"写法,帮你理解
while (1) {
    c = *s;      // 读当前字符
    s++;         // 指针后移
    if (c == 0)  // 遇到 '\0'
        break;
    h = ((h << 5) + h) + c; // << 5 是左移 5 位,等价于 h * 2^5 = h * 32。
}

   /*5381 是 djb2 的作者 Daniel J. Bernstein 选的魔数。选它的原因:

  • 是质数

  • 二进制 1010100000101,位模式"分散",减少碰撞

  • 经验上分布效果好

没有严格数学证明,就是实践中表现优秀的经验值。*/

  

for (Entry *e = d->buckets[idx]; e; e = e->next)
    等价于
Entry *e = d->buckets[idx];
while (e != NULL) {
    // 循环体
    e = e->next;
}

for 版的优点:
e 作用域仅限循环,不污染函数其余部分
三要素(初始化/条件/步进)一眼看全,遍历意图明确

  

posted @ 2026-10-22 16:46  古锁阳关  阅读(8)  评论(0)    收藏  举报