C 语言的优雅回归:从零手造数据结构

目录

一、优雅的链表设计:统一的容器结构

[1. 核心理念:面向对象的结构体嵌入](#1. 核心理念:面向对象的结构体嵌入)

[2. 双向链表节点 (list_node_t)](#2. 双向链表节点 (list_node_t))

[3. 插入操作的优雅性(例如:在头部插入)](#3. 插入操作的优雅性(例如:在头部插入))

二、极致的哈希表:开放寻址与探查序列

[1. 核心理念:紧凑数组与状态标记](#1. 核心理念:紧凑数组与状态标记)

[2. 桶结构定义](#2. 桶结构定义)

[3. 二次探查函数](#3. 二次探查函数)

[4. 插入逻辑(查找与放置)](#4. 插入逻辑(查找与放置))

总结:低层代码的"优雅"


如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。

在 C 语言的世界里,"造轮子"并非指重复劳动,而是对底层机制的深刻理解和极致掌控的体现。现代编程语言提供了强大的抽象,但唯有亲手实现链表和哈希表,我们才能真正体会到内存管理、指针操作和数据结构设计的精妙。

以下将分享实现双向链表开放寻址哈希表 时,将所追求的"优雅"设计哲学:清晰的接口(API)、最小化的冗余、以及对内存对齐和错误处理的关注。

一、优雅的链表设计:统一的容器结构

实现链表的难点在于节点(Node)的管理。为了实现通用性,我们应将数据与节点结构解耦。

1. 核心理念:面向对象的结构体嵌入

在 C 语言中,实现面向对象式的多态和继承,最好的方式是将链表节点结构嵌入到具体的数据结构中

2. 双向链表节点 (list_node_t)

// list.h

typedef struct list_node_t {

struct list_node_t *prev;

struct list_node_t *next;

// 实际数据指针或容器指针将在这里

} list_node_t;

// 链表头结构

typedef struct list_head_t {

size_t size;

list_node_t *first;

list_node_t *last;

} list_head_t;

// 宏技巧:获取包含该节点的上层容器指针 (最优雅的技巧之一)

#define LIST_CONTAINER_OF(ptr, type, member) \

((type *)((char *)(ptr) - offsetof(type, member)))

设计亮点:

  • list_node_t 极简:只包含指针,不关心数据类型。
  • LIST_CONTAINER_OF:这是 C 语言中实现"向上转型"的精髓。通过指针算术,从内部节点指针直接获取其所属的外部结构体指针,极大地简化了遍历时的逻辑。
3. 插入操作的优雅性(例如:在头部插入)

插入操作应尽量减少条件判断,利用 NULL 检查的简洁性。

void list_add_head(list_head_t *head, list_node_t *node) {

node->prev = NULL;

node->next = head->first;

if (head->first) {

head->first->prev = node;

} else {

// 链表为空,同时设置 last

head->last = node;

}

head->first = node;

head->size++;

}

二、极致的哈希表:开放寻址与探查序列

哈希表的优雅在于其查找效率和对内存的紧凑利用。我选择实现开放寻址法(Open Addressing),使用**二次探查法(Quadratic Probing)**来解决冲突,以避免链表带来的额外开销和缓存不友好问题。

1. 核心理念:紧凑数组与状态标记

我们不使用指针数组(分离链接法),而是使用一个大的结构体数组作为桶(Bucket)。

2. 桶结构定义

typedef enum {

BUCKET_EMPTY, // 未使用

BUCKET_OCCUPIED, // 活跃数据

BUCKET_DELETED // 已删除(用于维持探查序列)

} bucket_state_t;

typedef struct {

uint64_t hash_value;

void *key;

void *value;

bucket_state_t state;

} hash_bucket_t;

typedef struct {

hash_bucket_t *buckets;

size_t capacity;

size_t count;

} hash_table_t;

设计亮点:

  • 状态机 :引入 BUCKET_DELETED 至关重要。如果简单地将删除的桶标记为 EMPTY,后续的探查操作会错误地认为找到了"头",导致查找失败。
3. 二次探查函数

优雅的探查序列应快速覆盖数组,并尽量避免聚集。

对于二次探查,我们通常取 ,即:

static inline size_t hash_probe(size_t initial_index, size_t attempt, size_t capacity) {

// i^2 探查:避免了线性探查的初级聚集问题

return (initial_index + attempt * attempt) % capacity;

}

4. 插入逻辑(查找与放置)

插入时,我们查找第一个BUCKET_OCCUPIED 的位置(即 EMPTYDELETED),并利用预先计算好的 hash_value 存储。

优雅之处在于: 探查循环中,一旦找到一个可用的槽位(EMPTYDELETED),就立即停止,并在该位置放置新数据。这是开放寻址法的核心效率所在。

总结:低层代码的"优雅"

C 语言中实现数据结构,其优雅并非依赖于复杂的语法糖,而是体现在:

  1. 指针的精确控制 :对 void*char* 的熟练运用,实现通用的数据容器。
  2. 状态的清晰表达 :使用枚举(如哈希表的 bucket_state_t)来清晰地定义每种内存状态。
  3. 宏的智慧运用 :如 CONTAINER_OF 宏,用预处理器实现编译期的"语法糖",提高代码的可读性和复用性。

这样的设计,既保证了极高的运行效率,又保持了代码逻辑的清晰可追溯性,是 C 语言底层编程的精髓体现。

如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。

相关推荐
2的n次方_30 分钟前
Runtime 执行提交机制:NPU 硬件队列的管理与任务原子化下发
c语言·开发语言
凡人叶枫1 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
小妖6662 小时前
js 实现快速排序算法
数据结构·算法·排序算法
凡人叶枫3 小时前
C++中输入、输出和文件操作详解(Linux实战版)| 从基础到项目落地,避坑指南
linux·服务器·c语言·开发语言·c++
傻乐u兔4 小时前
C语言进阶————指针3
c语言·开发语言
独好紫罗兰5 小时前
对python的再认识-基于数据结构进行-a003-列表-排序
开发语言·数据结构·python
wuhen_n5 小时前
JavaScript内置数据结构
开发语言·前端·javascript·数据结构
2401_841495645 小时前
【LeetCode刷题】二叉树的层序遍历
数据结构·python·算法·leetcode·二叉树··队列
CodeSheep程序羊5 小时前
拼多多春节加班工资曝光,没几个敢给这个数的。
java·c语言·开发语言·c++·python·程序人生·职场和发展
独好紫罗兰5 小时前
对python的再认识-基于数据结构进行-a002-列表-列表推导式
开发语言·数据结构·python