作为一名有着十年C语言开发经验的程序员,我深知数据结构在实际项目中的重要性。今天我想和大家分享链表、栈和队列这三种基础数据结构的C语言实现,这些都是我在实际项目中反复使用过的核心知识。
C语言作为系统级编程语言,能让我们更深入地理解数据结构的底层实现原理。与高级语言不同,在C中我们需要手动管理内存、处理指针,这虽然增加了复杂度,但也让我们对数据结构的理解更加透彻。
提示:学习数据结构的最好方式就是自己动手实现一遍。通过C语言实现,你能真正理解指针操作和内存管理的精髓。
单链表是链表家族中最基础的成员,我们先来看它的结构定义:
c复制typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域
} Node, *LinkedList;
这里我使用了typedef为结构体创建了两个别名:Node表示单个节点,LinkedList表示指向头节点的指针。这种命名方式能让代码更清晰。
初始化链表时,我们创建一个头节点:
c复制LinkedList initLinkedList() {
LinkedList head = (LinkedList)malloc(sizeof(Node));
if (head == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
head->next = NULL;
return head;
}
注意:一定要检查malloc的返回值,这是良好的编程习惯。我在项目中见过太多因为忽略这个检查而导致程序崩溃的案例。
链表的插入有三种常见方式:
c复制void insertHead(LinkedList head, int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = head->next;
head->next = newNode;
}
c复制void insertTail(LinkedList head, int data) {
Node* p = head;
while (p->next != NULL) {
p = p->next;
}
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
p->next = newNode;
}
c复制int insertMiddle(LinkedList head, int k, int data) {
Node* p = head;
int i = 0;
while (p != NULL && i < k-1) {
p = p->next;
i++;
}
if (p == NULL) return -1;
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = p->next;
p->next = newNode;
return 0;
}
实战经验:在需要频繁在头部插入数据的场景下,头插法是最佳选择;如果需要保持插入顺序,则应该使用尾插法。
删除节点时要注意内存管理:
c复制int deleteNode(LinkedList head, int k) {
Node* p = head;
int i = 0;
while (p->next != NULL && i < k-1) {
p = p->next;
i++;
}
if (p->next == NULL) return -1;
Node* delNode = p->next;
p->next = delNode->next;
free(delNode);
delNode = NULL; // 避免野指针
return 0;
}
重要提示:删除节点后一定要释放内存并将指针置为NULL,否则会导致内存泄漏和野指针问题。
双向链表在单链表的基础上增加了前驱指针,使得某些操作更加高效:
c复制typedef struct DNode {
int data;
struct DNode* prev;
struct DNode* next;
} DNode, *DLinkedList;
双向链表的删除操作特别高效,因为不需要遍历查找前驱节点:
c复制int deleteDByData(DLinkedList head, int data) {
DNode* p = head->next;
while (p != NULL) {
if (p->data == data) {
p->prev->next = p->next;
if (p->next != NULL) {
p->next->prev = p->prev;
}
free(p);
return 0;
}
p = p->next;
}
return -1;
}
数组实现的栈适合已知最大容量的场景:
c复制#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} ArrayStack;
void initArrayStack(ArrayStack* stack) {
stack->top = -1;
}
int pushArrayStack(ArrayStack* stack, int data) {
if (stack->top == MAX_SIZE - 1) return -1;
stack->data[++stack->top] = data;
return 0;
}
int popArrayStack(ArrayStack* stack) {
if (stack->top == -1) return -1;
return stack->data[stack->top--];
}
链表实现的栈可以动态扩容:
c复制typedef struct StackNode {
int data;
struct StackNode* next;
} StackNode, *LinkedStack;
void pushLinkedStack(LinkedStack* stack, int data) {
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
newNode->data = data;
newNode->next = *stack;
*stack = newNode;
}
int popLinkedStack(LinkedStack* stack) {
if (*stack == NULL) return -1;
StackNode* temp = *stack;
int data = temp->data;
*stack = temp->next;
free(temp);
return data;
}
性能对比:数组栈操作都是O(1),且没有内存分配开销;链表栈同样O(1),但需要动态内存管理。根据场景选择合适实现。
循环队列解决了普通数组队列的"假溢出"问题:
c复制#define QUEUE_SIZE 10
typedef struct {
int data[QUEUE_SIZE];
int front;
int rear;
} CircularQueue;
int enqueueCircular(CircularQueue* queue, int data) {
if ((queue->rear + 1) % QUEUE_SIZE == queue->front) return -1;
queue->data[queue->rear] = data;
queue->rear = (queue->rear + 1) % QUEUE_SIZE;
return 0;
}
int dequeueCircular(CircularQueue* queue) {
if (queue->front == queue->rear) return -1;
int data = queue->data[queue->front];
queue->front = (queue->front + 1) % QUEUE_SIZE;
return data;
}
链表队列天然支持动态扩容:
c复制typedef struct QueueNode {
int data;
struct QueueNode* next;
} QueueNode;
typedef struct {
QueueNode* front;
QueueNode* rear;
} LinkedQueue;
int enqueueLinked(LinkedQueue* queue, int data) {
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = data;
newNode->next = NULL;
if (queue->rear == NULL) {
queue->front = queue->rear = newNode;
} else {
queue->rear->next = newNode;
queue->rear = newNode;
}
return 0;
}
int dequeueLinked(LinkedQueue* queue) {
if (queue->front == NULL) return -1;
QueueNode* temp = queue->front;
int data = temp->data;
queue->front = temp->next;
if (queue->front == NULL) queue->rear = NULL;
free(temp);
return data;
}
c复制int evaluatePostfix(char* expr) {
CalcStack stack;
initCalcStack(&stack);
int i = 0;
while (expr[i] != '\0') {
if (isdigit(expr[i])) {
int num = 0;
while (isdigit(expr[i])) {
num = num * 10 + (expr[i] - '0');
i++;
}
pushCalc(&stack, num);
} else if (expr[i] != ' ') {
int op2 = popCalc(&stack);
int op1 = popCalc(&stack);
int result;
switch (expr[i]) {
case '+': result = op1 + op2; break;
case '-': result = op1 - op2; break;
case '*': result = op1 * op2; break;
case '/': result = op1 / op2; break;
}
pushCalc(&stack, result);
i++;
} else {
i++;
}
}
return popCalc(&stack);
}
c复制typedef struct Task {
int taskId;
char taskName[50];
} Task;
typedef struct TaskQueue {
TaskNode* front;
TaskNode* rear;
} TaskQueue;
void processTasks(TaskQueue* queue) {
while (queue->front != NULL) {
TaskNode* current = queue->front;
printf("Processing task %d: %s\n", current->task.taskId, current->task.taskName);
queue->front = current->next;
if (queue->front == NULL) queue->rear = NULL;
free(current);
}
}
| 操作 | 数组实现 | 链表实现 |
|---|---|---|
| 插入(头) | O(n) | O(1) |
| 插入(尾) | O(1) | O(1)* |
| 随机访问 | O(1) | O(n) |
| 删除(头) | O(n) | O(1) |
| 删除(尾) | O(1) | O(n) |
*链表尾插法如果维护尾指针可以达到O(1)
链表适用场景:
数组适用场景:
栈和队列选择:
内存泄漏检测:
bash复制valgrind --leak-check=full ./your_program
野指针问题:
空指针解引用:
边界条件处理:
c复制void printList(LinkedList head) {
Node* current = head->next;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
频繁的malloc/free会影响性能,可以考虑使用内存池:
c复制#define POOL_SIZE 1000
Node nodePool[POOL_SIZE];
int nextAvailable = 0;
Node* getNode() {
if (nextAvailable >= POOL_SIZE) return NULL;
return &nodePool[nextAvailable++];
}
void freeNode(Node* node) {
// 内存池通常不单独释放节点
}
如果需要多线程使用,需要添加锁:
c复制#include <pthread.h>
typedef struct {
LinkedList list;
pthread_mutex_t lock;
} ThreadSafeList;
void safeInsert(ThreadSafeList* tslist, int data) {
pthread_mutex_lock(&tslist->lock);
insertHead(tslist->list, data);
pthread_mutex_unlock(&tslist->lock);
}
在我参与的一个网络服务器项目中,我们使用链表来管理客户端连接。每个新连接都会创建一个节点插入链表,断开连接时移除节点。这种设计让我们能够高效地管理数千个并发连接。
另一个案例是在嵌入式系统中使用循环队列作为数据缓冲区。由于内存有限,我们精心计算了队列大小,确保既不会浪费内存,又不会因为缓冲区不足丢失数据。
经验之谈:数据结构的选择往往需要权衡多种因素,包括时间复杂度、空间复杂度、实现复杂度等。没有最好的数据结构,只有最适合特定场景的数据结构。
进阶数据结构:
算法优化:
实际应用:
掌握这些基础数据结构的实现原理后,你会发现学习更复杂的数据结构和算法会容易很多。记住,理解原理比死记硬背代码更重要。