数据结构实战:链表、栈与队列的 C 语言实现
链表、栈与队列是 C 语言核心数据结构。本文详解单链表、双向链表的增删改查原理与实现,涵盖数组栈、链表栈及循环队列、链表队列的动态实现。通过后缀表达式求值与任务调度队列实战案例,展示数据结构在表达式解析与任务管理中的应用。最后对比各类结构特性与性能,提供选型建议与常见问题优化方案,帮助开发者根据场景选择合适方案并掌握动态内存分配技巧。

链表、栈与队列是 C 语言核心数据结构。本文详解单链表、双向链表的增删改查原理与实现,涵盖数组栈、链表栈及循环队列、链表队列的动态实现。通过后缀表达式求值与任务调度队列实战案例,展示数据结构在表达式解析与任务管理中的应用。最后对比各类结构特性与性能,提供选型建议与常见问题优化方案,帮助开发者根据场景选择合适方案并掌握动态内存分配技巧。

💡 单链表的插入、删除操作(尤其是头插法、尾插法和中间节点操作) 💡 栈的"先进后出"和队列的"先进先出"特性在代码中的体现 💡 基于链表的栈/队列实现(解决数组实现的容量限制问题) 💡 实战案例中的数据结构选型思路与代码优化技巧
链表是一种非线性数据结构,由若干个"节点"通过指针连接形成链式结构,每个节点包含两部分:
💡 与数组相比,链表的优势在于:
C 语言中实现链表的关键是自定义结构体(封装数据域和指针域)和动态内存分配(malloc/free 函数),这也是本章的核心技术点。
单链表是最简单的链表结构,每个节点仅包含一个指针域,指向其后继节点,尾节点的指针域为 NULL(表示链表结束)。
首先定义单链表节点的结构体,以存储 int 类型数据为例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 单链表节点结构体定义
typedef struct Node {
int data; // 数据域:存储 int 类型数据
struct Node* next; // 指针域:指向后续节点的地址
} Node, *LinkedList;
// Node 为结构体别名,LinkedList 为结构体指针别名
⚠️ 注意:结构体内部的指针必须声明为 struct Node*,不能直接用 Node*(因为 typedef 别名在结构体定义完成后才生效)
初始化的目标是创建一个空链表(仅包含头节点,无数据节点),头节点不存储实际数据,仅用于指向链表的第一个数据节点,方便操作:
// 初始化单链表:创建头节点,返回链表头指针
LinkedList initLinkedList() {
// 分配头节点内存
LinkedList head = (LinkedList)malloc(sizeof(Node));
if (head == NULL) {
// 内存分配失败检查
printf("内存分配失败!\n");
exit(EXIT_FAILURE); // 终止程序
}
head->next = NULL; // 空链表,头节点后续无节点
return head;
}
✅ 初始化后的链表状态:head -> NULL(仅头节点,无数据)
插入操作分为三种场景:头插法(插入到链表头部)、尾插法(插入到链表尾部)、中间插入(插入到指定位置),实际开发中头插法和尾插法最常用。
头插法是将新节点插入到头节点和第一个数据节点之间,步骤如下: ① 创建新节点并赋值 ② 新节点的 next 指向头节点的 next(原第一个数据节点) ③ 头节点的 next 指向新节点
// 头插法:向链表头部插入数据
void insertHead(LinkedList head, int data) {
// ① 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data; // 给新节点赋值
// ② 新节点指向原第一个数据节点
newNode->next = head->next;
// ③ 头节点指向新节点,完成插入
head->next = newNode;
}
💡 头插法的特点:插入顺序与链表存储顺序相反(如插入 1、2、3,链表中为 3->2->1),适合需要快速插入且不关心顺序的场景。
尾插法是将新节点插入到链表的最后一个节点之后,步骤如下: ① 创建新节点并赋值 ② 遍历链表,找到尾节点(next 为 NULL 的节点) ③ 尾节点的 next 指向新节点 ④ 新节点的 next 设为 NULL
// 尾插法:向链表尾部插入数据
void insertTail(LinkedList head, int data) {
// ① 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = NULL; // 新节点作为尾节点,next 为 NULL
// ② 遍历找到尾节点
Node* p = head; // 遍历指针,从 head 开始
while (p->next != NULL) {
// 找到 next 为 NULL 的节点(尾节点)
p = p->next;
}
// ③ 尾节点指向新节点
p->next = newNode;
}
💡 尾插法的特点:插入顺序与链表存储顺序一致(插入 1、2、3,链表中为 1->2->3),适合需要保持插入顺序的场景,但遍历过程会影响效率(可通过增加尾指针优化为 O(1))。
中间插入是将新节点插入到第 k 个数据节点之前,步骤如下: ① 检查插入位置的合法性(k≥1,且不超过链表长度) ② 遍历找到第 k-1 个节点(插入位置的前驱节点) ③ 新节点的 next 指向第 k 个节点 ④ 第 k-1 个节点的 next 指向新节点
// 中间插入:在第 k 个数据节点前插入数据(k 从 1 开始)
int insertMiddle(LinkedList head, int k, int data) {
// 检查 k 的合法性(k≥1)
if (k < 1) {
printf("插入位置不合法!\n");
return -1; // 插入失败返回 -1
}
// 遍历找到第 k-1 个节点(前驱节点)
Node* p = head;
int i = 0;
while (p != NULL && i < k - 1) {
p = p->next;
i++;
}
// 若 p 为 NULL,说明 k 超过链表长度
if (p == NULL) {
printf("插入位置超出链表范围!\n");
return -1;
}
// 创建新节点并插入
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = p->next; // 新节点指向原第 k 个节点
p->next = newNode; // 前驱节点指向新节点
return 0; // 插入成功返回 0
}
⚠️ 中间插入的关键是找到"前驱节点"(第 k-1 个节点),若直接找第 k 个节点,会无法关联前驱节点导致插入失败。
删除操作同样分为三种场景:删除头节点后的第一个数据节点、删除尾节点、删除指定数据或指定位置的节点。
删除第 k 个数据节点的步骤: ① 检查删除位置的合法性 ② 遍历找到第 k-1 个节点(前驱节点)和第 k 个节点(待删除节点) ③ 前驱节点的 next 指向待删除节点的 next ④ 释放待删除节点的内存(避免内存泄漏)
// 删除第 k 个数据节点(k 从 1 开始)
int deleteNode(LinkedList head, int k) {
if (k < 1) {
printf("删除位置不合法!\n");
return -1;
}
// 找到第 k-1 个节点(前驱节点)和第 k 个节点
Node* p = head; // 前驱节点指针
int i = 0;
while (p->next != NULL && i < k - 1) {
p = p->next;
i++;
}
// 若 p->next 为 NULL,说明 k 超过链表长度
if (p->next == NULL) {
printf("删除位置超出链表范围!\n");
return -1;
}
// 待删除节点
Node* delNode = p->next;
// 前驱节点指向待删除节点的后继节点
p->next = delNode->next;
// 释放内存
free(delNode);
delNode = NULL; // 避免野指针
return 0;
}
⚠️ 必须调用 free(delNode) 释放内存,否则会造成内存泄漏(动态分配的内存不会自动回收);同时将 delNode 设为 NULL,避免野指针(指向已释放内存的指针)。
删除第一个值为 data 的节点,步骤与指定位置删除类似,只需增加数据匹配判断:
// 删除第一个值为 data 的节点
int deleteByData(LinkedList head, int data) {
Node* p = head; // 前驱节点指针
// 遍历查找数据为 data 的节点的前驱
while (p->next != NULL) {
if (p->next->data == data) {
break; // 找到,退出循环
}
p = p->next;
}
// 若 p->next 为 NULL,说明未找到该数据
if (p->next == NULL) {
printf("未找到值为%d的节点!\n", data);
return -1;
}
// 删除节点(同指定位置删除逻辑)
Node* delNode = p->next;
p->next = delNode->next;
free(delNode);
delNode = NULL;
return 0;
}
遍历是链表操作的基础,通过指针移动依次访问每个节点:
// 遍历链表并输出所有数据
void traverseLinkedList(LinkedList head) {
if (head->next == NULL) {
// 空链表
printf("链表为空!\n");
return;
}
Node* p = head->next; // 从第一个数据节点开始
printf("链表数据:");
while (p != NULL) {
printf("%d ", p->data);
p = p->next; // 指针移动到下一个节点
}
printf("\n");
}
返回第一个值为 data 的节点的位置(从 1 开始),未找到返回 -1:
// 查找值为 data 的节点,返回位置(从 1 开始),未找到返回 -1
int findNode(LinkedList head, int data) {
if (head->next == NULL) {
return -1;
}
Node* p = head->next;
int index = 1;
while (p != NULL) {
if (p->data == data) {
return index; // 找到,返回位置
}
p = p->next;
index++;
}
return -1; // 未找到
}
链表使用完毕后,需要释放所有节点的内存(包括头节点),避免内存泄漏:
// 销毁链表,释放所有节点内存
void destroyLinkedList(LinkedList* head) {
Node* p = *head; // 遍历指针
Node* temp = NULL;
while (p != NULL) {
temp = p; // 保存当前节点
p = p->next; // 指针移动到下一个节点
free(temp); // 释放当前节点
}
*head = NULL; // 头指针设为 NULL,避免野指针
}
⚠️ 销毁函数的参数是 LinkedList*(结构体指针的指针),因为需要修改头指针本身(将其设为 NULL),若使用 LinkedList head(值传递),修改不会影响外部头指针。
将上述操作整合,编写测试代码验证功能:
int main() {
// 1. 初始化链表
LinkedList head = initLinkedList();
printf("初始化后:");
traverseLinkedList(head); // 输出:链表为空!
// 2. 尾插法插入数据
insertTail(head, 10);
insertTail(head, 20);
insertTail(head, 30);
printf("尾插 10、20、30 后:");
traverseLinkedList(head); // 输出:10 20 30
// 3. 头插法插入数据
insertHead(head, 5);
insertHead(head, 3);
printf("头插 5、3 后:");
traverseLinkedList(head); // 输出:3 5 10 20 30
// 4. 中间插入:在第 3 个位置插入 15
insertMiddle(head, 3, 15);
printf("第 3 个位置插入 15 后:");
traverseLinkedList(head); // 输出:3 5 15 10 20 30
// 5. 查找数据
int pos = findNode(head, 10);
if (pos != -1) {
printf("数据 10 的位置:第%d个\n", pos); // 输出:第 4 个
} else {
printf("未找到数据 10\n");
}
// 6. 删除操作:删除第 4 个节点(数据 10)
deleteNode(head, 4);
printf("删除第 4 个节点后:");
traverseLinkedList(head); // 输出:3 5 15 20 30
// 7. 删除指定数据:删除 20
deleteByData(head, 20);
printf("删除数据 20 后:");
traverseLinkedList(head); // 输出:3 5 15 30
// 8. 销毁链表
destroyLinkedList(&head);
printf("销毁后:");
traverseLinkedList(head); // 输出:链表为空!(此时 head 为 NULL,函数内部处理)
return 0;
}
✅ 运行结果与预期一致,说明单链表的实现正确。
双向链表的每个节点包含两个指针域:一个指向后继节点(next),一个指向前驱节点(prev),相比单链表,双向链表支持双向遍历,删除节点时无需查找前驱节点(效率更高),但结构更复杂,占用内存更多。
// 双向链表节点结构体
typedef struct DNode {
int data; // 数据域
struct DNode* prev; // 前驱指针
struct DNode* next; // 后继指针
} DNode, *DLinkedList;
// 初始化双向链表(带头节点)
DLinkedList initDLinkedList() {
DLinkedList head = (DLinkedList)malloc(sizeof(DNode));
if (head == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
head->prev = NULL; // 头节点无前驱
head->next = NULL; // 头节点无后继
return head;
}
// 双向链表尾插法
void insertDTail(DLinkedList head, int data) {
// 创建新节点
DNode* newNode = (DNode*)malloc(sizeof(DNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = NULL;
// 找到尾节点
DNode* p = head;
while (p->next != NULL) {
p = p->next;
}
// 插入逻辑(双向关联)
p->next = newNode; // 原尾节点的 next 指向新节点
newNode->prev = p; // 新节点的 prev 指向原尾节点
}
// 双向链表删除指定数据的节点
int deleteDByData(DLinkedList head, int data) {
DNode* p = head->next; // 从第一个数据节点开始查找
while (p != NULL) {
if (p->data == data) {
break;
}
p = p->next;
}
if (p == NULL) {
printf("未找到值为%d的节点!\n", data);
return -1;
}
// 双向关联修改(无需前驱节点指针)
p->prev->next = p->next; // 前驱节点的 next 指向后继节点
if (p->next != NULL) { // 若不是尾节点,后继节点的 prev 指向前驱节点
p->next->prev = p->prev;
}
free(p);
p = NULL;
return 0;
}
💡 双向链表删除的优势:找到待删除节点后,通过 p->prev 直接获取前驱节点,无需像单链表那样提前遍历查找,效率更高(O(1) vs O(n))。
栈是一种受限的线性数据结构,仅允许在一端(栈顶)进行插入和删除操作,另一端(栈底)固定不动,遵循"先进后出"(Last In First Out,LIFO)原则。
栈的核心操作:
💡 生活中的栈示例:叠盘子(只能从最上面拿盘子,新盘子也只能放在最上面)、浏览器的前进/后退功能(最新打开的页面先关闭)。
数组实现的栈结构简单、效率高,但容量固定(需预先指定最大大小),适合已知数据量的场景。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100 // 栈的最大容量
// 数组栈结构体
typedef struct {
int data[MAX_SIZE]; // 存储栈元素的数组
int top; // 栈顶指针(-1 表示栈空,top==MAX_SIZE-1 表示栈满)
} ArrayStack;
// 初始化栈(栈空)
void initArrayStack(ArrayStack* stack) {
stack->top = -1; // 栈顶指针设为 -1,表示无元素
}
// 入栈:向栈顶添加元素
int pushArrayStack(ArrayStack* stack, int data) {
// 判满
if (stack->top == MAX_SIZE - 1) {
printf("栈满,无法入栈!\n");
return -1;
}
stack->top++; // 栈顶指针上移
stack->data[stack->top] = data; // 存入数据
return 0;
}
// 出栈:从栈顶移除元素,返回移除的元素
int popArrayStack(ArrayStack* stack) {
// 判空
if (stack->top == -1) {
printf("栈空,无法出栈!\n");
return -1; // 用 -1 表示出栈失败(假设数据不包含 -1)
}
int data = stack->data[stack->top]; // 取出栈顶元素
stack->top--; // 栈顶指针下移
return data;
}
// 取栈顶元素(不删除)
int getTopArrayStack(ArrayStack* stack) {
if (stack->top == -1) {
printf("栈空,无栈顶元素!\n");
return -1;
}
return stack->data[stack->top];
}
// 判断栈是否为空(空返回 1,非空返回 0)
int isEmptyArrayStack(ArrayStack* stack) {
return stack->top == -1 ? 1 : 0;
}
// 遍历栈(从栈顶到栈底)
void traverseArrayStack(ArrayStack* stack) {
if (isEmptyArrayStack(stack)) {
printf("栈空!\n");
return;
}
printf("栈元素(栈顶->栈底):");
for (int i = stack->top; i >= 0; i--) {
printf("%d ", stack->data[i]);
}
printf("\n");
}
int main() {
ArrayStack stack;
initArrayStack(&stack);
// 入栈
pushArrayStack(&stack, 10);
pushArrayStack(&stack, 20);
pushArrayStack(&stack, 30);
pushArrayStack(&stack, 40);
printf("入栈 10、20、30、40 后:");
traverseArrayStack(&stack); // 输出:40 30 20 10
// 取栈顶元素
int top = getTopArrayStack(&stack);
printf("当前栈顶元素:%d\n", top); // 输出:40
// 出栈
int popData = popArrayStack(&stack);
printf("出栈元素:%d\n", popData); // 输出:40
printf("出栈后:");
traverseArrayStack(&stack); // 输出:30 20 10
// 继续入栈到满
for (int i = 50; i <= 100; i += 10) {
pushArrayStack(&stack, i);
}
printf("多次入栈后:");
traverseArrayStack(&stack); // 输出:100 90 80 70 60 50 30 20 10(共 9 个元素,MAX_SIZE=100,未装满)
// 测试栈满
for (int i = 110; i <= 200; i += 10) {
pushArrayStack(&stack, i);
}
printf("栈满后尝试入栈 200:");
pushArrayStack(&stack, 200); // 输出:栈满,无法入栈!
return 0;
}
链表实现的栈无需预先指定容量,可动态扩容,适合数据量不确定的场景,核心是利用单链表的头插法(入栈)和头删法(出栈),效率均为 O(1)。
#include <stdio.h>
#include <stdlib.h>
// 链表栈节点结构体
typedef struct StackNode {
int data; // 数据域
struct StackNode* next; // 指针域(指向栈底方向的节点)
} StackNode, *LinkedStack;
// 初始化栈(栈空,头指针为 NULL)
void initLinkedStack(LinkedStack* stack) {
*stack = NULL; // 栈顶指针为 NULL,表示栈空
}
// 入栈:向栈顶添加节点(头插法)
int pushLinkedStack(LinkedStack* stack, int data) {
// 创建新节点
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = *stack; // 新节点指向原栈顶节点
*stack = newNode; // 栈顶指针指向新节点
return 0;
}
// 出栈:移除栈顶节点,返回数据
int popLinkedStack(LinkedStack* stack) {
// 判空
if (*stack == NULL) {
printf("栈空,无法出栈!\n");
return -1;
}
StackNode* temp = *stack; // 保存栈顶节点
int data = temp->data; // 取出数据
*stack = temp->next; // 栈顶指针指向栈底方向的下一个节点
free(temp); // 释放栈顶节点内存
temp = NULL;
return data;
}
// 取栈顶元素(不删除)
int getTopLinkedStack(LinkedStack* stack) {
if (*stack == NULL) {
printf("栈空,无栈顶元素!\n");
return -1;
}
return (*stack)->data;
}
// 遍历栈(从栈顶到栈底)
void traverseLinkedStack(LinkedStack* stack) {
if (*stack == NULL) {
printf("栈空!\n");
return;
}
StackNode* p = *stack;
printf("栈元素(栈顶->栈底):");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 销毁链表栈,释放所有节点内存
void destroyLinkedStack(LinkedStack* stack) {
StackNode* p = *stack;
StackNode* temp = NULL;
while (p != NULL) {
temp = p;
p = p->next;
free(temp);
}
*stack = NULL;
}
int main() {
LinkedStack stack;
initLinkedStack(&stack);
// 入栈
pushLinkedStack(&stack, 5);
pushLinkedStack(&stack, 15);
pushLinkedStack(&stack, 25);
pushLinkedStack(&stack, 35);
printf("入栈 5、15、25、35 后:");
traverseLinkedStack(&stack); // 输出:35 25 15 5
// 取栈顶
int top = getTopLinkedStack(&stack);
printf("栈顶元素:%d\n", top); // 输出:35
// 出栈
int pop1 = popLinkedStack(&stack);
int pop2 = popLinkedStack(&stack);
printf("出栈两次,元素分别为:%d、%d\n", pop1, pop2); // 输出:35、25
printf("出栈后:");
traverseLinkedStack(&stack); // 输出:15 5
// 继续入栈(动态扩容)
for (int i = 45; i <= 105; i += 10) {
pushLinkedStack(&stack, i);
}
printf("动态入栈后:");
traverseLinkedStack(&stack); // 输出:105 95 85 75 65 55 45 15 5(无容量限制)
// 销毁栈
destroyLinkedStack(&stack);
printf("销毁后:");
traverseLinkedStack(&stack); // 输出:栈空!
return 0;
}
后缀表达式(逆波兰表达式)是一种不依赖括号的数学表达式表示法,运算符在操作数之后(如"3 4 + 5 *"等价于"(3+4)*5"),适合用栈进行求值,步骤如下:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h> // 包含 isdigit 函数(判断是否为数字)
#define MAX_EXPR_LEN 100 // 表达式最大长度
#define STACK_SIZE 50 // 栈容量
// 栈结构体(简化版数组栈)
typedef struct {
int data[STACK_SIZE];
int top;
} CalcStack;
// 初始化栈
void initCalcStack(CalcStack* stack) {
stack->top = -1;
}
// 入栈
int pushCalc(CalcStack* stack, int data) {
if (stack->top == STACK_SIZE - 1) {
printf("栈满!\n");
return -1;
}
stack->data[++stack->top] = data;
return 0;
}
// 出栈
int popCalc(CalcStack* stack) {
if (stack->top == -1) {
printf("栈空!\n");
return -1;
}
return stack->data[stack->top--];
}
// 后缀表达式求值(表达式以空格分隔,如"3 4 + 5 *")
int evaluatePostfix(char* expr) {
CalcStack stack;
initCalcStack(&stack);
int i = 0;
while (expr[i] != '\0') {
// 跳过空格
if (expr[i] == ' ') {
i++;
continue;
}
// 若为数字(可能是多位数)
if (isdigit(expr[i])) {
int num = 0; // 处理多位数
while (isdigit(expr[i])) {
num = num * 10 + (expr[i] - '0');
i++;
}
pushCalc(&stack, num);
} else {
// 若为运算符,出栈两个操作数
int op2 = popCalc(&stack); // 右操作数(后入栈,先出栈)
int op1 = popCalc(&stack); // 左操作数(先入栈,后出栈)
int result = 0;
// 根据运算符计算
switch (expr[i]) {
case '+': result = op1 + op2; break;
case '-': result = op1 - op2; break;
case '*': result = op1 * op2; break;
case '/':
if (op2 == 0) {
printf("除数不能为 0!\n");
exit(EXIT_FAILURE);
}
result = op1 / op2; break;
default: printf("非法运算符:%c\n", expr[i]); exit(EXIT_FAILURE);
}
pushCalc(&stack, result); // 结果入栈
i++;
}
}
// 最终栈中只剩一个元素,即为结果
return popCalc(&stack);
}
int main() {
char expr[MAX_EXPR_LEN];
printf("请输入后缀表达式(操作数和运算符以空格分隔):");
fgets(expr, MAX_EXPR_LEN, stdin); // 读取表达式(包含空格)
// 移除 fgets 读取的换行符
int len = strlen(expr);
if (expr[len - 1] == '\n') {
expr[len - 1] = '\0';
}
int result = evaluatePostfix(expr);
printf("表达式结果:%d\n", result);
return 0;
}
测试输入:3 4 + 5 *,输出结果:35(正确,(3+4)*5=35);
测试输入:10 5 2 / - 3 *,输出结果:24(正确,(10 - 5/2)*3= (10-2)*3=24,整数除法 5/2=2)。
✅ 该案例充分体现了栈"先进后出"的特性,是栈在实际开发中的典型应用(如编译器的表达式解析、计算器功能等)。
队列是另一种受限的线性数据结构,允许在一端(队尾)进行插入操作,在另一端(队头)进行删除操作,遵循"先进先出"(First In First Out,FIFO)原则。
队列的核心操作:
💡 生活中的队列示例:排队买票(先排队的先买票)、打印机的打印任务队列(先提交的任务先打印)、消息队列(先发送的消息先处理)。
普通数组队列存在"假溢出"问题(队尾已到数组末尾,但队头前仍有空闲空间),因此实际开发中通常使用循环队列(数组首尾相连),充分利用数组空间。
#include <stdio.h>
#include <stdlib.h>
#define QUEUE_SIZE 10 // 队列最大容量(实际可用容量为 QUEUE_SIZE-1,用于区分空和满)
typedef struct {
int data[QUEUE_SIZE]; // 存储队列元素的数组
int front; // 队头指针(指向队头元素)
int rear; // 队尾指针(指向队尾元素的下一个位置)
} CircularQueue;
⚠️ 循环队列的关键设计:用(rear + 1) % QUEUE_SIZE == front表示队列满,用front == rear表示队列空,因此数组实际可用容量为QUEUE_SIZE - 1(牺牲一个位置避免空满混淆)。
// 初始化循环队列(空队列)
void initCircularQueue(CircularQueue* queue) {
queue->front = 0;
queue->rear = 0;
}
// 判断队列是否为空(空返回 1,非空返回 0)
int isEmptyCircularQueue(CircularQueue* queue) {
return queue->front == queue->rear ? 1 : 0;
}
// 判断队列是否已满(满返回 1,未满返回 0)
int isFullCircularQueue(CircularQueue* queue) {
return (queue->rear + 1) % QUEUE_SIZE == queue->front ? 1 : 0;
}
// 入队:向队尾添加元素
int enqueueCircular(CircularQueue* queue, int data) {
if (isFullCircularQueue(queue)) {
printf("队列满,无法入队!\n");
return -1;
}
queue->data[queue->rear] = data; // 存入队尾位置
queue->rear = (queue->rear + 1) % QUEUE_SIZE; // 队尾指针循环后移
return 0;
}
// 出队:从队头移除元素,返回元素值
int dequeueCircular(CircularQueue* queue) {
if (isEmptyCircularQueue(queue)) {
printf("队列为空,无法出队!\n");
return -1;
}
int data = queue->data[queue->front]; // 取出队头元素
queue->front = (queue->front + 1) % QUEUE_SIZE; // 队头指针循环后移
return data;
}
// 取队头元素(不删除)
int getFrontCircular(CircularQueue* queue) {
if (isEmptyCircularQueue(queue)) {
printf("队列为空,无队头元素!\n");
return -1;
}
return queue->data[queue->front];
}
// 遍历队列(从队头到队尾)
void traverseCircularQueue(CircularQueue* queue) {
if (isEmptyCircularQueue(queue)) {
printf("队列为空!\n");
return;
}
int i = queue->front;
printf("队列元素(队头->队尾):");
while (i != queue->rear) {
printf("%d ", queue->data[i]);
i = (i + 1) % QUEUE_SIZE; // 循环移动
}
printf("\n");
}
int main() {
CircularQueue queue;
initCircularQueue(&queue);
// 入队
enqueueCircular(&queue, 10);
enqueueCircular(&queue, 20);
enqueueCircular(&queue, 30);
enqueueCircular(&queue, 40);
printf("入队 10、20、30、40 后:");
traverseCircularQueue(&queue); // 输出:10 20 30 40
// 取队头
int front = getFrontCircular(&queue);
printf("当前队头元素:%d\n", front); // 输出:10
// 出队
int deq1 = dequeueCircular(&queue);
int deq2 = dequeueCircular(&queue);
printf("出队两次,元素分别为:%d、%d\n", deq1, deq2); // 输出:10、20
printf("出队后:");
traverseCircularQueue(&queue); // 输出:30 40
// 继续入队(测试循环复用空间)
enqueueCircular(&queue, 50);
enqueueCircular(&queue, 60);
enqueueCircular(&queue, 70);
enqueueCircular(&queue, 80);
printf("入队 50、60、70、80 后:");
traverseCircularQueue(&queue); // 输出:30 40 50 60 70 80(QUEUE_SIZE=10,可用 9 个位置,当前 6 个元素)
// 测试队列满
enqueueCircular(&queue, 90);
enqueueCircular(&queue, 100);
printf("入队 90、100 后(队列满):");
traverseCircularQueue(&queue); // 输出:30 40 50 60 70 80 90 100(8 个元素,剩余 1 个位置为空闲但标记为满)
enqueueCircular(&queue, 110); // 输出:队列满,无法入队!
return 0;
}
链表实现的队列无需考虑容量限制,动态扩容,队头和队尾分别用指针指向,入队操作在队尾,出队操作在队头,效率均为 O(1)。
#include <stdio.h>
#include <stdlib.h>
// 队列节点结构体
typedef struct QueueNode {
int data; // 数据域
struct QueueNode* next; // 指针域(指向队尾方向的下一个节点)
} QueueNode;
// 队列结构体(包含队头和队尾指针)
typedef struct {
QueueNode* front; // 队头指针(指向队头节点)
QueueNode* rear; // 队尾指针(指向队尾节点)
} LinkedQueue;
// 初始化队列(空队列,队头和队尾均为 NULL)
void initLinkedQueue(LinkedQueue* queue) {
queue->front = NULL;
queue->rear = NULL;
}
// 入队:向队尾添加节点
int enqueueLinked(LinkedQueue* queue, int data) {
// 创建新节点
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = NULL; // 队尾节点的 next 为 NULL
// 若队列为空,队头和队尾都指向新节点
if (queue->front == NULL) {
queue->front = newNode;
queue->rear = newNode;
} else {
// 队尾节点的 next 指向新节点,队尾指针后移
queue->rear->next = newNode;
queue->rear = newNode;
}
return 0;
}
// 出队:从队头移除节点,返回数据
int dequeueLinked(LinkedQueue* queue) {
// 判空
if (queue->front == NULL) {
printf("队列为空,无法出队!\n");
return -1;
}
QueueNode* temp = queue->front; // 保存队头节点
int data = temp->data; // 取出数据
// 若队列中只有一个节点,出队后队头和队尾均为 NULL
if (queue->front == queue->rear) {
queue->front = NULL;
queue->rear = NULL;
} else {
queue->front = temp->next; // 队头指针后移
}
free(temp); // 释放队头节点内存
temp = NULL;
return data;
}
// 取队头元素(不删除)
int getFrontLinked(LinkedQueue* queue) {
if (queue->front == NULL) {
printf("队列为空,无队头元素!\n");
return -1;
}
return queue->front->data;
}
// 遍历队列(从队头到队尾)
void traverseLinkedQueue(LinkedQueue* queue) {
if (queue->front == NULL) {
printf("队列为空!\n");
return;
}
QueueNode* p = queue->front;
printf("队列元素(队头->队尾):");
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 销毁链表队列,释放所有节点内存
void destroyLinkedQueue(LinkedQueue* queue) {
QueueNode* p = queue->front;
QueueNode* temp = NULL;
while (p != NULL) {
temp = p;
p = p->next;
free(temp);
}
queue->front = NULL;
queue->rear = NULL;
}
int main() {
LinkedQueue queue;
initLinkedQueue(&queue);
// 入队
enqueueLinked(&queue, 1);
enqueueLinked(&queue, 2);
enqueueLinked(&queue, 3);
enqueueLinked(&queue, 4);
printf("入队 1、2、3、4 后:");
traverseLinkedQueue(&queue); // 输出:1 2 3 4
// 取队头
int front = getFrontLinked(&queue);
printf("当前队头元素:%d\n", front); // 输出:1
// 出队
int deq1 = dequeueLinked(&queue);
int deq2 = dequeueLinked(&queue);
printf("出队两次,元素分别为:%d、%d\n", deq1, deq2); // 输出:1、2
printf("出队后:");
traverseLinkedQueue(&queue); // 输出:3 4
// 继续入队(动态扩容)
for (int i = 5; i <= 10; i++) {
enqueueLinked(&queue, i);
}
printf("入队 5-10 后:");
traverseLinkedQueue(&queue); // 输出:3 4 5 6 7 8 9 10(无容量限制)
// 销毁队列
destroyLinkedQueue(&queue);
printf("销毁后:");
traverseLinkedQueue(&queue); // 输出:队列为空!
return 0;
}
在嵌入式系统、服务器开发中,任务调度是常见场景,多个任务按提交顺序依次执行,适合用队列实现。以下是一个简化的任务调度队列案例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 任务结构体(存储任务 ID 和任务描述)
typedef struct Task {
int taskId; // 任务 ID
char taskName[50]; // 任务名称
} Task;
// 队列节点结构体(存储任务数据)
typedef struct TaskNode {
Task task; // 任务数据
struct TaskNode* next; // 指针域
} TaskNode;
// 任务队列结构体
typedef struct {
TaskNode* front; // 队头指针
TaskNode* rear; // 队尾指针
} TaskQueue;
// 初始化任务队列
void initTaskQueue(TaskQueue* queue) {
queue->front = NULL;
queue->rear = NULL;
}
// 任务入队
int enqueueTask(TaskQueue* queue, int taskId, const char* taskName) {
TaskNode* newNode = (TaskNode*)malloc(sizeof(TaskNode));
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
// 赋值任务数据
newNode->task.taskId = taskId;
strncpy(newNode->task.taskName, taskName, sizeof(newNode->task.taskName) - 1);
newNode->task.taskName[sizeof(newNode->task.taskName) - 1] = '\0'; // 确保字符串结束
newNode->next = NULL;
if (queue->front == NULL) {
queue->front = newNode;
queue->rear = newNode;
} else {
queue->rear->next = newNode;
queue->rear = newNode;
}
printf("任务入队:ID=%d, 名称=%s\n", taskId, taskName);
return 0;
}
// 任务出队(执行任务)
int dequeueTask(TaskQueue* queue) {
if (queue->front == NULL) {
printf("无待执行任务!\n");
return -1;
}
TaskNode* temp = queue->front;
Task task = temp->task; // 执行任务(简化为打印任务信息)
printf("执行任务:ID=%d, 名称=%s\n", task.taskId, task.taskName);
// 更新队列
if (queue->front == queue->rear) {
queue->front = NULL;
queue->rear = NULL;
} else {
queue->front = temp->next;
}
free(temp);
temp = NULL;
return 0;
}
// 遍历待执行任务
void traverseTaskQueue(TaskQueue* queue) {
if (queue->front == NULL) {
printf("待执行任务为空!\n");
return;
}
TaskNode* p = queue->front;
printf("待执行任务列表:");
while (p != NULL) {
printf("[ID=%d, 名称=%s] ", p->task.taskId, p->task.taskName);
p = p->next;
}
printf("\n");
}
// 销毁任务队列
void destroyTaskQueue(TaskQueue* queue) {
TaskNode* p = queue->front;
TaskNode* temp = NULL;
while (p != NULL) {
temp = p;
p = p->next;
free(temp);
}
queue->front = NULL;
queue->rear = NULL;
printf("任务队列已销毁!\n");
}
int main() {
TaskQueue taskQueue;
initTaskQueue(&taskQueue);
// 提交任务(入队)
enqueueTask(&taskQueue, 1, "系统初始化");
enqueueTask(&taskQueue, 2, "读取配置文件");
enqueueTask(&taskQueue, 3, "连接数据库");
enqueueTask(&taskQueue, 4, "处理用户请求");
enqueueTask(&taskQueue, 5, "生成日志文件");
// 查看待执行任务
traverseTaskQueue(&taskQueue);
// 执行任务(出队)
printf("\n开始执行任务:\n");
dequeueTask(&taskQueue);
dequeueTask(&taskQueue);
dequeueTask(&taskQueue);
// 查看剩余任务
printf("\n剩余待执行任务:\n");
traverseTaskQueue(&taskQueue);
// 继续执行剩余任务
printf("\n继续执行任务:\n");
dequeueTask(&taskQueue);
dequeueTask(&taskQueue);
// 尝试执行无任务
dequeueTask(&taskQueue);
// 销毁队列
destroyTaskQueue(&taskQueue);
return 0;
}
运行结果:
任务入队:ID=1, 名称=系统初始化 任务入队:ID=2, 名称=读取配置文件 任务入队:ID=3, 名称=连接数据库 任务入队:ID=4, 名称=处理用户请求 任务入队:ID=5, 名称=生成日志文件 待执行任务列表:[ID=1, 名称=系统初始化] [ID=2, 名称=读取配置文件] [ID=3, 名称=连接数据库] [ID=4, 名称=处理用户请求] [ID=5, 名称=生成日志文件] 开始执行任务: 执行任务:ID=1, 名称=系统初始化 执行任务:ID=2, 名称=读取配置文件 执行任务:ID=3, 名称=连接数据库 剩余待执行任务: 待执行任务列表:[ID=4, 名称=处理用户请求] [ID=5, 名称=生成日志文件] 继续执行任务: 执行任务:ID=4, 名称=处理用户请求 执行任务:ID=5, 名称=生成日志文件 无待执行任务! 任务队列已销毁!
✅ 该案例模拟了真实的任务调度场景,任务按提交顺序依次执行,充分体现了队列"先进先出"的特性,类似的实现可用于服务器的请求处理、嵌入式系统的任务调度等场景。
| 数据结构 | 核心特性 | 核心操作 | 典型应用场景 |
|---|---|---|---|
| 单链表 | 动态扩容、链式存储 | 增删 O(1)(指定前驱)、查找 O(n) | 数据量不确定、频繁增删的场景 |
| 双向链表 | 双向遍历、增删高效 | 增删 O(1)、查找 O(n) | 需要双向遍历的场景(如 LRU 缓存) |
| 数组栈 | 静态容量、随机访问 | 入栈/出栈 O(1) | 数据量固定、对效率要求高的场景 |
| 链表栈 | 动态扩容、链式存储 | 入栈/出栈 O(1) | 数据量不确定、无需随机访问的场景 |
| 循环队列 | 静态容量、空间高效 | 入队/出队 O(1) | 数据量固定、需要循环复用空间的场景 |
| 链表队列 | 动态扩容、链式存储 | 入队/出队 O(1) | 数据量不确定、频繁入队出队的场景 |
💡 若数据量固定,优先选择数组实现(栈/队列),效率更高(数组随机访问速度快,无指针开销); 💡 若数据量不确定,优先选择链表实现(栈/队列/链表),避免溢出问题; 💡 若需要双向遍历或频繁删除指定节点,优先选择双向链表; 💡 若需要循环复用空间(如缓冲区),优先选择循环队列; 💡 若需要"先进后出"特性(如表达式求值、递归调用),选择栈; 💡 若需要"先进先出"特性(如任务调度、消息传递),选择队列。
free 释放,解决方案:使用完毕后遍历链表,逐个释放节点;head->next == NULL,双向链表需判断 head->next == NULL && head->prev == NULL;prev 指针获取前驱,效率更高。front == rear(空)和 (rear+1)%size == front(满),避免混淆;本章详细讲解了 C 语言中三种核心数据结构:链表、栈和队列,从原理到实现,再到实战应用,逐步深入,重点内容包括:
💡 数据结构是 C 语言开发的核心基础,掌握链表、栈、队列的实现与应用,能为后续学习更复杂的数据结构(如树、图、哈希表)打下坚实基础。实际开发中,应根据具体场景选择合适的数据结构,兼顾效率和空间开销。
建议读者多动手编写代码,反复调试本章的案例,重点掌握动态内存分配(malloc/free)和指针操作,这是 C 语言实现复杂数据结构的关键。同时,尝试将这些数据结构应用到自己的项目中(如实现一个简单的缓存系统、消息队列等),加深理解和记忆。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online