插入、查找、删除
核心操作
插入、查找、删除 是 CoddyKit 上的免费 C Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。
三个核心操作
每个哈希表都支持三种操作:插入、查找和删除。使用良好的哈希函数并保持合理的负载因子时,这三种操作的平均时间复杂度都是 O(1)。
我们将逐步构建一个基于链接法的哈希表。
哈希表与节点类型
我们定义一个节点,用于存放复制后的键字符串和一个整数值;此外还定义一个哈希表结构,用于存放桶数组及其容量。
#include <stdio.h>
typedef struct Node {
char *key;
int value;
struct Node *next;
} Node;
typedef struct {
Node **buckets;
unsigned capacity;
unsigned size;
} HashTable;
int main(void) {
printf("types defined\n");
return 0;
}创建哈希表
使用 calloc 分配哈希表和已清零的桶数组,这样每个桶初始都是 NULL。
#include <stdio.h>
#include <stdlib.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
typedef struct { Node **buckets; unsigned capacity, size; } HashTable;
HashTable *ht_create(unsigned cap) {
HashTable *t = malloc(sizeof *t);
t->buckets = calloc(cap, sizeof(Node *));
t->capacity = cap; t->size = 0;
return t;
}
int main(void) {
HashTable *t = ht_create(16);
printf("capacity=%u size=%u\n", t->capacity, t->size);
return 0;
}哈希辅助函数
我们重新使用 DJB2,并将其结果缩小为桶索引。三个操作都会使用这个辅助函数。
#include <stdio.h>
unsigned long djb2(const char *s) {
unsigned long h = 5381; int c;
while ((c = (unsigned char)*s++)) h = ((h << 5) + h) + c;
return h;
}
unsigned bucket_of(const char *key, unsigned cap) {
return (unsigned)(djb2(key) % cap);
}
int main(void) {
printf("%u\n", bucket_of("name", 16));
return 0;
}插入:更新或插入到头部
插入时,先搜索对应的桶。如果键已存在,就更新它的值。否则分配一个新节点(通过 strdup 复制键),并将其插入链表头部。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
Node *insert(Node *head, const char *key, int val) {
for (Node *p = head; p; p = p->next)
if (strcmp(p->key, key) == 0) { p->value = val; return head; }
Node *n = malloc(sizeof *n);
n->key = strdup(key); n->value = val; n->next = head;
return n;
}
int main(void) {
Node *b = NULL;
b = insert(b, "a", 1);
b = insert(b, "a", 99); /* update */
printf("%s=%d\n", b->key, b->value);
return 0;
}查找
查找操作会先计算键的哈希值,然后遍历桶链表,并使用 strcmp 比较键。找到时返回指向值的指针;如果不存在,则返回 NULL。
#include <stdio.h>
#include <string.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
int *lookup(Node *head, const char *key) {
for (Node *p = head; p; p = p->next)
if (strcmp(p->key, key) == 0) return &p->value;
return NULL;
}
int main(void) {
Node n2 = {"y", 20, NULL};
Node n1 = {"x", 10, &n2};
int *v = lookup(&n1, "y");
printf("%d\n", v ? *v : -1);
return 0;
}删除:重新链接链表
删除操作遍历桶,同时保存指向前一个节点的指针,然后绕过目标节点重新连接链表,并释放目标节点(包括复制的键和节点本身)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
Node *delete_key(Node *head, const char *key) {
Node *prev = NULL, *cur = head;
while (cur) {
if (strcmp(cur->key, key) == 0) {
if (prev) prev->next = cur->next; else head = cur->next;
free(cur->key); free(cur);
return head;
}
prev = cur; cur = cur->next;
}
return head;
}
int main(void) {
Node *b = malloc(sizeof *b);
b->key = strdup("a"); b->value = 1; b->next = NULL;
b = delete_key(b, "a");
printf("%s\n", b ? "left" : "empty");
return 0;
}整合起来
完整的哈希表会先计算桶索引,再将操作交给链表辅助函数。下面展示一个完整的小型哈希表运行示例。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
unsigned long djb2(const char *s){unsigned long h=5381;int c;while((c=(unsigned char)*s++))h=((h<<5)+h)+c;return h;}
#define CAP 16
Node *table[CAP];
void put(const char *k, int v) {
unsigned i = djb2(k) % CAP;
Node *n = malloc(sizeof *n);
n->key = strdup(k); n->value = v; n->next = table[i];
table[i] = n;
}
int get(const char *k) {
for (Node *p = table[djb2(k) % CAP]; p; p = p->next)
if (!strcmp(p->key, k)) return p->value;
return -1;
}
int main(void) {
put("age", 30); put("score", 95);
printf("age=%d score=%d\n", get("age"), get("score"));
return 0;
}为什么要复制键
我们使用 strdup 存储键,使哈希表拥有自己的副本。如果直接存储调用者的指针,键可能在我们使用期间被修改或释放,从而破坏查找。
这也意味着删除时必须对复制的键调用 free。
时间复杂度
当哈希函数分布均匀,并将负载因子保持在 0.75 附近时:
- 插入:平均 O(1)
- 查找:平均 O(1)
- 删除:平均 O(1)
最坏情况下,如果所有键都冲突到同一个桶中,复杂度为 O(n)。
释放整个哈希表
为避免内存泄漏,请释放每个桶中的所有节点,然后释放桶数组,最后释放哈希表结构。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node { char *key; int value; struct Node *next; } Node;
void free_bucket(Node *head) {
while (head) { Node *nx = head->next; free(head->key); free(head); head = nx; }
}
int main(void) {
Node *b = malloc(sizeof *b);
b->key = strdup("k"); b->value = 1; b->next = NULL;
free_bucket(b);
printf("freed\n");
return 0;
}快速检查
测试您对核心操作的理解。
回顾
您使用链接法实现了哈希表的三个核心操作。
- 插入会更新节点,或将节点插入链表头部
- 查找使用
strcmp遍历桶链表 - 删除会重新链接链表,并释放键和节点
- 使用
strdup管理键的所有权,并在销毁时释放所有内容
常见问题解答
「插入、查找、删除」课时是免费的吗?
是的 — 「插入、查找、删除」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。
「插入、查找、删除」这节课中我会学到什么?
核心操作 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「插入、查找、删除」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C Academy 课中编写并运行代码吗?
能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。