在计算机科学领域,链表、栈和队列是最基础也是最重要的数据结构。虽然现代编程语言的标准库通常都提供了这些数据结构的现成实现,但作为一名C语言开发者,亲手实现它们有着不可替代的价值:
首先,通过从零开始构建这些数据结构,你能真正理解它们的内部工作原理。就像汽车修理工必须了解发动机的构造一样,程序员也需要知道数据结构是如何在内存中组织和操作的。这种理解会让你在使用更高级数据结构时更加得心应手。
其次,C语言的标准库相对精简,不像C++的STL或Java的集合框架那样提供丰富的数据结构。很多时候,你需要根据具体需求定制自己的数据结构实现。比如嵌入式系统中,你可能需要针对有限的内存资源进行优化;在高性能计算场景,你可能需要调整数据结构以更好地利用CPU缓存。
我在实际工作中就遇到过这样的情况:一个使用标准库链表实现的程序在性能测试中表现不佳,通过自定义实现并优化内存访问模式后,性能提升了近40%。这种优化机会只有在你深入理解数据结构实现细节时才能把握。
对于这个项目,我们只需要最基本的C开发环境:
提示:如果你在Windows上开发,可以考虑使用WSL2来获得Linux开发环境,或者使用MinGW。
良好的项目结构能让代码更易于维护。我建议采用如下目录结构:
code复制data_structures/
├── include/ # 头文件
│ ├── linked_list.h
│ ├── stack.h
│ └── queue.h
├── src/ # 实现文件
│ ├── linked_list.c
│ ├── stack.c
│ └── queue.c
├── tests/ # 测试代码
│ ├── test_linked_list.c
│ ├── test_stack.c
│ └── test_queue.c
└── Makefile
这种分离接口和实现的方式是C项目的常见做法,也便于后续扩展其他数据结构。
链表的核心是节点,我们先定义链表节点的结构:
c复制typedef struct ListNode {
int data; // 存储的数据
struct ListNode *next; // 指向下一个节点的指针
} ListNode;
这里我们使用typedef创建了ListNode类型,包含一个整型数据字段和一个指向下一个节点的指针。在实际应用中,你可能需要根据需求调整数据类型。
c复制ListNode* create_list(int values[], int n) {
if (n <= 0) return NULL;
ListNode *head = (ListNode*)malloc(sizeof(ListNode));
head->data = values[0];
head->next = NULL;
ListNode *current = head;
for (int i = 1; i < n; i++) {
current->next = (ListNode*)malloc(sizeof(ListNode));
current = current->next;
current->data = values[i];
current->next = NULL;
}
return head;
}
这个函数接收一个数组和它的长度,然后创建一个对应的链表。注意我们使用了malloc动态分配内存,记得在使用完毕后释放这些内存。
在链表中插入节点需要考虑多种情况:
c复制void insert_node(ListNode **head, int index, int value) {
ListNode *new_node = (ListNode*)malloc(sizeof(ListNode));
new_node->data = value;
// 插入到头部
if (index == 0) {
new_node->next = *head;
*head = new_node;
return;
}
// 找到插入位置的前一个节点
ListNode *current = *head;
for (int i = 0; i < index-1 && current != NULL; i++) {
current = current->next;
}
if (current == NULL) {
free(new_node); // 索引超出范围,释放新节点
return;
}
new_node->next = current->next;
current->next = new_node;
}
注意:我们使用
ListNode**而不是ListNode*作为头指针参数,这样可以直接修改调用者的头指针。
删除节点同样需要考虑边界条件:
c复制void delete_node(ListNode **head, int index) {
if (*head == NULL) return;
// 删除头节点
if (index == 0) {
ListNode *temp = *head;
*head = (*head)->next;
free(temp);
return;
}
// 找到要删除节点的前一个节点
ListNode *current = *head;
for (int i = 0; i < index-1 && current->next != NULL; i++) {
current = current->next;
}
if (current->next == NULL) return; // 索引超出范围
ListNode *temp = current->next;
current->next = temp->next;
free(temp);
}
反转链表是经典的面试题,也是理解指针操作的好例子:
c复制ListNode* reverse_list(ListNode *head) {
ListNode *prev = NULL;
ListNode *current = head;
ListNode *next = NULL;
while (current != NULL) {
next = current->next; // 保存下一个节点
current->next = prev; // 反转指针
prev = current; // 移动prev
current = next; // 移动current
}
return prev; // prev现在是新的头节点
}
检测链表中是否存在环是另一个常见问题:
c复制int has_cycle(ListNode *head) {
if (head == NULL) return 0;
ListNode *slow = head;
ListNode *fast = head->next;
while (fast != NULL && fast->next != NULL) {
if (slow == fast) return 1; // 快慢指针相遇,存在环
slow = slow->next;
fast = fast->next->next;
}
return 0; // 快指针到达末尾,无环
}
这个算法使用快慢指针技巧,时间复杂度O(n),空间复杂度O(1),非常高效。
栈是一种后进先出(LIFO)的数据结构,只允许在一端(栈顶)进行插入和删除操作。我们有两种实现方式:数组实现和链表实现。这里我们展示更通用的链表实现。
c复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top;
int size;
} Stack;
我们定义了两个结构体:StackNode表示栈的节点,Stack包含栈顶指针和当前栈的大小。
c复制void stack_init(Stack *stack) {
stack->top = NULL;
stack->size = 0;
}
c复制void stack_push(Stack *stack, int value) {
StackNode *new_node = (StackNode*)malloc(sizeof(StackNode));
new_node->data = value;
new_node->next = stack->top;
stack->top = new_node;
stack->size++;
}
c复制int stack_pop(Stack *stack) {
if (stack->top == NULL) {
fprintf(stderr, "Error: stack underflow\n");
exit(EXIT_FAILURE);
}
StackNode *temp = stack->top;
int value = temp->data;
stack->top = temp->next;
free(temp);
stack->size--;
return value;
}
c复制int stack_peek(Stack *stack) {
if (stack->top == NULL) {
fprintf(stderr, "Error: stack is empty\n");
exit(EXIT_FAILURE);
}
return stack->top->data;
}
栈在计算机科学中有广泛应用,比如括号匹配检查:
c复制int is_balanced(const char *expr) {
Stack stack;
stack_init(&stack);
for (int i = 0; expr[i] != '\0'; i++) {
if (expr[i] == '(' || expr[i] == '[' || expr[i] == '{') {
stack_push(&stack, expr[i]);
} else if (expr[i] == ')' || expr[i] == ']' || expr[i] == '}') {
if (stack.size == 0) return 0;
char top = stack_pop(&stack);
if ((expr[i] == ')' && top != '(') ||
(expr[i] == ']' && top != '[') ||
(expr[i] == '}' && top != '{')) {
return 0;
}
}
}
return stack.size == 0;
}
队列是一种先进先出(FIFO)的数据结构,插入操作在队尾进行,删除操作在队首进行。我们同样使用链表实现。
c复制typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
typedef struct {
QueueNode *front;
QueueNode *rear;
int size;
} Queue;
Queue结构体包含队首指针、队尾指针和队列当前大小。
c复制void queue_init(Queue *queue) {
queue->front = NULL;
queue->rear = NULL;
queue->size = 0;
}
c复制void queue_enqueue(Queue *queue, int value) {
QueueNode *new_node = (QueueNode*)malloc(sizeof(QueueNode));
new_node->data = value;
new_node->next = NULL;
if (queue->rear == NULL) {
queue->front = queue->rear = new_node;
} else {
queue->rear->next = new_node;
queue->rear = new_node;
}
queue->size++;
}
c复制int queue_dequeue(Queue *queue) {
if (queue->front == NULL) {
fprintf(stderr, "Error: queue underflow\n");
exit(EXIT_FAILURE);
}
QueueNode *temp = queue->front;
int value = temp->data;
queue->front = queue->front->next;
if (queue->front == NULL) {
queue->rear = NULL;
}
free(temp);
queue->size--;
return value;
}
c复制int queue_peek(Queue *queue) {
if (queue->front == NULL) {
fprintf(stderr, "Error: queue is empty\n");
exit(EXIT_FAILURE);
}
return queue->front->data;
}
对于固定大小的队列,数组实现更高效。下面是循环队列的实现:
c复制typedef struct {
int *data;
int front;
int rear;
int capacity;
} CircularQueue;
void circular_queue_init(CircularQueue *queue, int capacity) {
queue->data = (int*)malloc(sizeof(int) * capacity);
queue->front = 0;
queue->rear = 0;
queue->capacity = capacity;
}
int circular_queue_is_full(CircularQueue *queue) {
return (queue->rear + 1) % queue->capacity == queue->front;
}
int circular_queue_is_empty(CircularQueue *queue) {
return queue->front == queue->rear;
}
void circular_queue_enqueue(CircularQueue *queue, int value) {
if (circular_queue_is_full(queue)) {
fprintf(stderr, "Error: queue is full\n");
return;
}
queue->data[queue->rear] = value;
queue->rear = (queue->rear + 1) % queue->capacity;
}
int circular_queue_dequeue(CircularQueue *queue) {
if (circular_queue_is_empty(queue)) {
fprintf(stderr, "Error: queue is empty\n");
exit(EXIT_FAILURE);
}
int value = queue->data[queue->front];
queue->front = (queue->front + 1) % queue->capacity;
return value;
}
为了验证我们的实现是否正确,我们需要编写测试代码。可以使用简单的assert宏:
c复制#include <assert.h>
void test_linked_list() {
int values[] = {1, 2, 3, 4, 5};
ListNode *head = create_list(values, 5);
assert(head != NULL);
assert(head->data == 1);
assert(head->next->data == 2);
insert_node(&head, 2, 10);
assert(head->next->next->data == 10);
delete_node(&head, 1);
assert(head->next->data == 10);
// 更多测试...
}
void test_stack() {
Stack stack;
stack_init(&stack);
assert(stack.size == 0);
stack_push(&stack, 10);
assert(stack.size == 1);
assert(stack_peek(&stack) == 10);
stack_push(&stack, 20);
assert(stack_peek(&stack) == 20);
assert(stack_pop(&stack) == 20);
assert(stack_pop(&stack) == 10);
assert(stack.size == 0);
// 更多测试...
}
void test_queue() {
Queue queue;
queue_init(&queue);
assert(queue.size == 0);
queue_enqueue(&queue, 10);
assert(queue.size == 1);
assert(queue_peek(&queue) == 10);
queue_enqueue(&queue, 20);
assert(queue_peek(&queue) == 10);
assert(queue_dequeue(&queue) == 10);
assert(queue_dequeue(&queue) == 20);
assert(queue.size == 0);
// 更多测试...
}
int main() {
test_linked_list();
test_stack();
test_queue();
printf("All tests passed!\n");
return 0;
}
对于C语言项目,内存泄漏是需要特别注意的问题。可以使用valgrind工具检查:
bash复制valgrind --leak-check=full ./your_program
确保所有分配的内存都被正确释放。例如,在链表测试结束后,应该添加代码释放所有节点:
c复制void free_list(ListNode *head) {
ListNode *current = head;
while (current != NULL) {
ListNode *temp = current;
current = current->next;
free(temp);
}
}
传统的链表节点在内存中可能是分散的,这会导致缓存命中率低。我们可以实现一个更缓存友好的版本:
c复制typedef struct {
int capacity;
int count;
ListNode *nodes;
ListNode *free_list;
} ListNodePool;
void pool_init(ListNodePool *pool, int capacity) {
pool->capacity = capacity;
pool->count = 0;
pool->nodes = (ListNode*)malloc(sizeof(ListNode) * capacity);
pool->free_list = NULL;
// 初始化空闲列表
for (int i = capacity-1; i >= 0; i--) {
pool->nodes[i].next = pool->free_list;
pool->free_list = &pool->nodes[i];
}
}
ListNode* pool_alloc(ListNodePool *pool) {
if (pool->free_list == NULL) return NULL;
ListNode *node = pool->free_list;
pool->free_list = node->next;
pool->count++;
return node;
}
void pool_free(ListNodePool *pool, ListNode *node) {
node->next = pool->free_list;
pool->free_list = node;
pool->count--;
}
这种实现方式将所有节点预先分配在一个连续的内存区域,提高了缓存局部性。
目前的实现只支持int类型数据。我们可以使用void指针实现泛型:
c复制typedef struct ListNode {
void *data;
struct ListNode *next;
} ListNode;
使用时需要自行管理内存:
c复制int *value = malloc(sizeof(int));
*value = 42;
insert_node(&head, 0, value);
记得在使用完毕后释放数据内存:
c复制free(node->data);
free(node);
如果需要在多线程环境中使用,需要添加同步机制。下面是一个简单的线程安全栈实现:
c复制#include <pthread.h>
typedef struct {
StackNode *top;
int size;
pthread_mutex_t lock;
} ThreadSafeStack;
void ts_stack_init(ThreadSafeStack *stack) {
stack->top = NULL;
stack->size = 0;
pthread_mutex_init(&stack->lock, NULL);
}
void ts_stack_push(ThreadSafeStack *stack, int value) {
pthread_mutex_lock(&stack->lock);
StackNode *new_node = (StackNode*)malloc(sizeof(StackNode));
new_node->data = value;
new_node->next = stack->top;
stack->top = new_node;
stack->size++;
pthread_mutex_unlock(&stack->lock);
}
int ts_stack_pop(ThreadSafeStack *stack) {
pthread_mutex_lock(&stack->lock);
if (stack->top == NULL) {
pthread_mutex_unlock(&stack->lock);
fprintf(stderr, "Error: stack underflow\n");
exit(EXIT_FAILURE);
}
StackNode *temp = stack->top;
int value = temp->data;
stack->top = temp->next;
free(temp);
stack->size--;
pthread_mutex_unlock(&stack->lock);
return value;
}
栈非常适合用于表达式求值。下面是后缀表达式(逆波兰表示法)求值的实现:
c复制int evaluate_postfix(const char *expr) {
Stack stack;
stack_init(&stack);
for (int i = 0; expr[i] != '\0'; i++) {
if (isdigit(expr[i])) {
int num = 0;
while (isdigit(expr[i])) {
num = num * 10 + (expr[i] - '0');
i++;
}
i--; // 补偿for循环的i++
stack_push(&stack, num);
} else if (expr[i] == ' ') {
continue;
} else {
int b = stack_pop(&stack);
int a = stack_pop(&stack);
int result;
switch (expr[i]) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/': result = a / b; break;
default:
fprintf(stderr, "Unknown operator: %c\n", expr[i]);
exit(EXIT_FAILURE);
}
stack_push(&stack, result);
}
}
return stack_pop(&stack);
}
队列常用于实现消息传递系统。下面是一个简单的消息队列实现:
c复制typedef struct {
char *message;
int priority;
} Message;
typedef struct MessageNode {
Message data;
struct MessageNode *next;
} MessageNode;
typedef struct {
MessageNode *front;
MessageNode *rear;
} MessageQueue;
void message_queue_init(MessageQueue *queue) {
queue->front = queue->rear = NULL;
}
void message_queue_enqueue(MessageQueue *queue, Message msg) {
MessageNode *new_node = (MessageNode*)malloc(sizeof(MessageNode));
new_node->data = msg;
new_node->next = NULL;
if (queue->rear == NULL) {
queue->front = queue->rear = new_node;
} else {
// 简单优先级处理:高优先级插入到前面
if (msg.priority > queue->front->data.priority) {
new_node->next = queue->front;
queue->front = new_node;
} else {
MessageNode *current = queue->front;
while (current->next != NULL &&
msg.priority <= current->next->data.priority) {
current = current->next;
}
new_node->next = current->next;
current->next = new_node;
if (new_node->next == NULL) {
queue->rear = new_node;
}
}
}
}
Message message_queue_dequeue(MessageQueue *queue) {
if (queue->front == NULL) {
fprintf(stderr, "Error: queue is empty\n");
exit(EXIT_FAILURE);
}
MessageNode *temp = queue->front;
Message msg = temp->data;
queue->front = queue->front->next;
if (queue->front == NULL) {
queue->rear = NULL;
}
free(temp);
return msg;
}
链表结合哈希表可以实现高效的LRU缓存:
c复制typedef struct LRUNode {
int key;
int value;
struct LRUNode *prev;
struct LRUNode *next;
} LRUNode;
typedef struct {
int capacity;
int size;
LRUNode *head;
LRUNode *tail;
LRUNode **hash_map; // 简化的哈希表,实际应用中可能需要更复杂的实现
} LRUCache;
void lru_init(LRUCache *cache, int capacity) {
cache->capacity = capacity;
cache->size = 0;
cache->head = cache->tail = NULL;
cache->hash_map = (LRUNode**)calloc(capacity, sizeof(LRUNode*));
}
void lru_move_to_head(LRUCache *cache, LRUNode *node) {
if (node == cache->head) return;
// 从当前位置移除节点
if (node->prev) node->prev->next = node->next;
if (node->next) node->next->prev = node->prev;
// 如果是尾节点,需要更新tail
if (node == cache->tail) {
cache->tail = node->prev;
}
// 将节点移动到头部
node->prev = NULL;
node->next = cache->head;
if (cache->head) cache->head->prev = node;
cache->head = node;
// 如果缓存为空,更新tail
if (cache->tail == NULL) {
cache->tail = node;
}
}
int lru_get(LRUCache *cache, int key) {
int index = key % cache->capacity;
LRUNode *node = cache->hash_map[index];
while (node != NULL && node->key != key) {
node = node->next; // 处理哈希冲突
}
if (node == NULL) return -1; // 未找到
// 将访问的节点移动到头部
lru_move_to_head(cache, node);
return node->value;
}
void lru_put(LRUCache *cache, int key, int value) {
int index = key % cache->capacity;
// 检查key是否已存在
LRUNode *node = cache->hash_map[index];
while (node != NULL && node->key != key) {
node = node->next;
}
if (node != NULL) {
// 更新现有节点的值并移动到头部
node->value = value;
lru_move_to_head(cache, node);
return;
}
// 创建新节点
node = (LRUNode*)malloc(sizeof(LRUNode));
node->key = key;
node->value = value;
node->prev = NULL;
node->next = cache->hash_map[index];
if (cache->hash_map[index]) cache->hash_map[index]->prev = node;
cache->hash_map[index] = node;
// 将新节点添加到链表头部
node->next = cache->head;
if (cache->head) cache->head->prev = node;
cache->head = node;
if (cache->tail == NULL) cache->tail = node;
cache->size++;
// 如果超出容量,移除最久未使用的节点
if (cache->size > cache->capacity) {
LRUNode *tail = cache->tail;
int tail_index = tail->key % cache->capacity;
// 从哈希表中移除
if (tail->prev) {
tail->prev->next = tail->next;
} else {
cache->hash_map[tail_index] = tail->next;
}
if (tail->next) {
tail->next->prev = tail->prev;
}
// 从链表中移除
cache->tail = tail->prev;
if (cache->tail) cache->tail->next = NULL;
free(tail);
cache->size--;
}
}
空指针解引用:在访问node->next或node->data前,总是检查node != NULL。
内存泄漏:每个malloc都应该有对应的free。特别是在删除节点时,确保释放节点内存。
头指针更新:在插入或删除头节点时,记得更新头指针。这就是为什么我们使用ListNode**参数。
循环引用:在复杂链表操作中,可能会意外创建循环引用,导致无限循环或内存泄漏。
调试技巧:
printf打印链表内容:1 -> 2 -> 3 -> NULL栈下溢:在空栈上调用pop或peek。总是检查size或top是否为NULL。
队列下溢:在空队列上调用dequeue。检查front是否为NULL。
循环队列满/空判断:循环队列中,front == rear可能表示空或满,需要额外处理。
调试技巧:
初始化指针:定义指针变量时立即初始化为NULL。
检查malloc返回值:malloc可能失败返回NULL,特别是在嵌入式系统中。
释放后置空:释放指针后将其设为NULL,避免悬垂指针。
所有权明确:每个内存块应该有明确的拥有者,负责最终释放。
使用工具检测:
掌握了这些基础数据结构后,你可以继续深入学习:
更复杂的数据结构:
算法优化:
标准库实现:
实际项目应用:
相关书籍推荐:
我在实际项目中最深刻的体会是:数据结构的选型和实现细节会极大影响程序的性能和可维护性。一个简单的链表实现可能只需要几十分钟,但要写出高效、健壮、易维护的版本,需要不断实践和优化。建议你多写代码,多思考不同实现的优缺点,逐渐培养出对数据结构的直觉。