跳到主要内容
C 语言指针与复杂数据结构:链表、栈与队列实现 | 极客日志
C 算法
C 语言指针与复杂数据结构:链表、栈与队列实现 综述由AI生成 深入讲解 C 语言中指针与结构体的结合应用,涵盖单链表、双向链表的创建与操作(插入、删除、反转、环检测),以及数组栈、链式栈和循环队列、链式队列的实现原理。通过括号匹配、生产者 - 消费者模型及学生成绩管理系统等案例,演示了动态内存管理、边界条件处理及常见错误避坑指南,帮助读者掌握复杂数据结构的底层逻辑与工程实践。
t ag 发布于 2026/3/28 更新于 2026/5/26 26 浏览一、指针与复杂数据结构:链表、栈与队列实战
1.1 学习目标与重点
[提示] 掌握指针结合结构体实现线性数据结构的核心原理,理解链表、栈、队列的存储特性与访问规则;
[提示] 精通单链表、双向链表的创建、插入、删除、查找等基础操作,能够解决链表环检测、反转等进阶问题;
[提示] 熟练使用数组栈、链式栈及循环队列、链式队列的实现逻辑,明确不同结构的适用场景;
[提示] 结合实际项目案例,体会指针在复杂数据结构中的内存管理技巧,提升代码的模块化与高效性。
1.2 指针与结构体:复杂数据结构的基础
在 C 语言中,结构体用于封装多个不同类型的数据,而指针则负责连接这些数据单元,形成灵活的复杂数据结构。指针与结构体的结合,是实现链表、栈、队列等动态数据结构的核心基础——通过结构体存储数据,指针指向结构体实例,实现数据单元的链式关联。
1.2.1 结构体与指针的基本操作
结构体指针的声明与使用是基础,其核心语法为:
struct 结构体名 {
成员类型 成员名;
};
struct 结构体名 *指针变量名;
[提示] 技巧:通过 -> 运算符访问结构体指针的成员(等价于 (*指针变量名).成员名),是后续复杂数据结构操作的常用语法。
示例 1:结构体指针的基本使用
#include <stdio.h>
#include <stdlib.h>
struct Student {
int id;
char name[20 ];
float score;
struct Student * next ;
};
int main () {
struct Student stu1 = { , , , };
&stu1;
( , pStu1->id, (*pStu1).name, pStu1->score);
( Student*) ( ( Student));
(pStu2 == ) {
( );
;
}
pStu2->id = ;
(pStu2->name, );
pStu2->score = ;
pStu2->next = ;
( , pStu2->id, pStu2->name, pStu2->score);
(pStu2);
pStu2 = ;
;
}
101
"张三"
92.5
NULL
struct Student * pStu1 =
printf
"学号:%d,姓名:%s,成绩:%.1f\n"
struct Student * pStu2 =
struct
malloc
sizeof
struct
if
NULL
printf
"[注意] 内存分配失败!\n"
return
-1
102
strcpy
"李四"
88.0
NULL
printf
"学号:%d,姓名:%s,成绩:%.1f\n"
free
NULL
return
0
学号:101,姓名:张三,成绩:92.5
学号:102,姓名:李四,成绩:88.0
[注意] 堆上的结构体必须通过 malloc 分配内存,且使用后需通过 free 释放,否则会导致内存泄漏;释放后需将指针置空,避免野指针访问。
1.2.2 typedef 简化结构体指针声明 频繁使用 struct 结构体名 * 声明指针会显得繁琐,通过 typedef 为结构体类型起别名,可极大简化代码:
struct Student {
int id;
char name[20 ];
};
typedef struct Student Student ;
typedef struct Student {
int id;
char name[20 ];
struct Student * next ;
} Student, *PStudent;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Student {
int id;
char name[20 ];
float score;
struct Student * next ;
} Student, *PStudent;
PStudent createStudent (int id, const char * name, float score) {
PStudent pStu = (PStudent)malloc (sizeof (Student));
if (pStu == NULL ) {
printf ("[注意] 节点创建失败!\n" );
return NULL ;
}
pStu->id = id;
strcpy (pStu->name, name);
pStu->score = score;
pStu->next = NULL ;
return pStu;
}
void printStudent (PStudent pStu) {
if (pStu != NULL ) {
printf ("学号:%d,姓名:%s,成绩:%.1f\n" , pStu->id, pStu->name, pStu->score);
}
}
int main () {
PStudent pStu1 = createStudent(101 , "王五" , 95.0 );
PStudent pStu2 = createStudent(102 , "赵六" , 89.5 );
printStudent(pStu1);
printStudent(pStu2);
free (pStu1);
free (pStu2);
pStu1 = NULL ;
pStu2 = NULL ;
return 0 ;
}
学号:101,姓名:王五,成绩:95.0
学号:102,姓名:赵六,成绩:89.5
[成功] 结论:typedef 不仅简化了结构体指针的声明,还提升了代码的可读性和可维护性,是复杂数据结构开发中的必备技巧。
1.3 链表:动态数据结构的核心 链表是一种线性数据结构,其数据单元(节点)通过指针串联,节点在内存中无需连续存储——这种特性使其支持动态扩容(无需预先指定大小),插入、删除操作效率高(无需移动大量元素)。链表主要分为单链表、双向链表、循环链表,本节重点讲解最常用的单链表和双向链表。
1.3.1 单链表的实现与基础操作 单链表的每个节点包含'数据域'和'指针域':数据域存储数据,指针域(next)指向后一个节点,最后一个节点的 next 指针为 NULL(表示链表结尾)。
单链表的核心操作
链表初始化:创建头节点(或空链表);
节点插入:头插法、尾插法、指定位置插入;
节点删除:按位置删除、按值删除;
链表遍历:遍历所有节点并访问数据;
链表查找:按值查找、按位置查找;
链表销毁:释放所有节点的内存。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct ListNode {
int data;
struct ListNode * next ;
} ListNode, *LinkList;
LinkList initLinkList () {
return NULL ;
}
int insertTail (LinkList *head, int data) {
ListNode *newNode = (ListNode *)malloc (sizeof (ListNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = NULL ;
if (*head == NULL ) {
*head = newNode;
return 0 ;
}
ListNode *p = *head;
while (p->next != NULL ) {
p = p->next;
}
p->next = newNode;
return 0 ;
}
int insertHead (LinkList *head, int data) {
ListNode *newNode = (ListNode *)malloc (sizeof (ListNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = *head;
*head = newNode;
return 0 ;
}
int insertPos (LinkList *head, int pos, int data) {
if (pos < 1 || (*head == NULL && pos > 1 )) {
printf ("[注意] 插入位置非法!\n" );
return -1 ;
}
if (pos == 1 ) {
return insertHead(head, data);
}
ListNode *p = *head;
int count = 1 ;
while (p != NULL && count < pos - 1 ) {
p = p->next;
count++;
}
if (p == NULL ) {
printf ("[注意] 插入位置超出链表长度!\n" );
return -1 ;
}
ListNode *newNode = (ListNode *)malloc (sizeof (ListNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = p->next;
p->next = newNode;
return 0 ;
}
void traverseLinkList (LinkList head) {
if (head == NULL ) {
printf ("[注意] 链表为空!\n" );
return ;
}
ListNode *p = head;
printf ("链表元素:" );
while (p != NULL ) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
int findByValue (LinkList head, int data) {
if (head == NULL ) {
printf ("[注意] 链表为空!\n" );
return 0 ;
}
ListNode *p = head;
int pos = 1 ;
while (p != NULL ) {
if (p->data == data) {
return pos;
}
p = p->next;
pos++;
}
return 0 ;
}
ListNode *findByPos (LinkList head, int pos) {
if (head == NULL || pos < 1 ) {
printf ("[注意] 链表为空或位置非法!\n" );
return NULL ;
}
ListNode *p = head;
int count = 1 ;
while (p != NULL && count < pos) {
p = p->next;
count++;
}
return p;
}
int deleteByValue (LinkList *head, int data) {
if (*head == NULL ) {
printf ("[注意] 链表为空,无法删除!\n" );
return -1 ;
}
ListNode *p = *head;
ListNode *prev = NULL ;
if (p->data == data) {
*head = p->next;
free (p);
p = NULL ;
return 0 ;
}
while (p != NULL && p->data != data) {
prev = p;
p = p->next;
}
if (p == NULL ) {
printf ("[注意] 未找到值为%d的节点!\n" , data);
return -1 ;
}
prev->next = p->next;
free (p);
p = NULL ;
return 0 ;
}
int deleteByPos (LinkList *head, int pos) {
if (*head == NULL || pos < 1 ) {
printf ("[注意] 链表为空或位置非法!\n" );
return -1 ;
}
ListNode *p = *head;
ListNode *prev = NULL ;
if (pos == 1 ) {
*head = p->next;
free (p);
p = NULL ;
return 0 ;
}
int count = 1 ;
while (p != NULL && count < pos) {
prev = p;
p = p->next;
count++;
}
if (p == NULL ) {
printf ("[注意] 删除位置超出链表长度!\n" );
return -1 ;
}
prev->next = p->next;
free (p);
p = NULL ;
return 0 ;
}
void destroyLinkList (LinkList *head) {
ListNode *p = *head;
while (p != NULL ) {
ListNode *temp = p;
p = p->next;
free (temp);
temp = NULL ;
}
*head = NULL ;
printf ("[成功] 链表销毁成功!\n" );
}
int getLinkListLength (LinkList head) {
int len = 0 ;
ListNode *p = head;
while (p != NULL ) {
len++;
p = p->next;
}
return len;
}
int main () {
LinkList head = initLinkList();
insertTail(&head, 10 );
insertTail(&head, 20 );
insertTail(&head, 30 );
printf ("尾插法插入后:" );
traverseLinkList(head);
insertHead(&head, 5 );
printf ("头插法插入后:" );
traverseLinkList(head);
insertPos(&head, 3 , 15 );
printf ("位置 3 插入 15 后:" );
traverseLinkList(head);
int pos = findByValue(head, 20 );
if (pos != 0 ) {
printf ("值为 20 的节点位置:%d\n" , pos);
} else {
printf ("未找到值为 20 的节点\n" );
}
ListNode *pNode = findByPos(head, 4 );
if (pNode != NULL ) {
printf ("位置 4 的节点数据:%d\n" , pNode->data);
}
deleteByValue(&head, 15 );
printf ("删除值为 15 的节点后:" );
traverseLinkList(head);
deleteByPos(&head, 2 );
printf ("删除位置 2 的节点后:" );
traverseLinkList(head);
printf ("链表当前长度:%d\n" , getLinkListLength(head));
destroyLinkList(&head);
traverseLinkList(head);
return 0 ;
}
尾插法插入后:链表元素:10 20 30
头插法插入后:链表元素:5 10 20 30
位置 3 插入 15 后:链表元素:5 10 15 20 30
值为 20 的节点位置:4
位置 4 的节点数据:20
删除值为 15 的节点后:链表元素:5 10 20 30
删除位置 2 的节点后:链表元素:5 20 30
链表当前长度:3
[成功] 链表销毁成功!
[注意] 链表为空!
[提示] 技巧:单链表操作的核心是'找到目标节点的前驱节点'——插入时需让前驱节点指向新节点,新节点指向原后继;删除时需让前驱节点指向目标节点的后继,再释放目标节点内存。
1.3.2 单链表进阶问题:反转与环检测
问题 1:单链表反转 链表反转是面试高频题,核心思路是'遍历链表,逐个改变节点的 next 指针方向',需要三个指针(prev、curr、next)记录前驱、当前、后继节点。
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode {
int data;
struct ListNode * next ;
} ListNode, *LinkList;
void createLinkList (LinkList *head, int arr[], int len) {
*head = NULL ;
for (int i = 0 ; i < len; i++) {
ListNode *newNode = (ListNode *)malloc (sizeof (ListNode));
newNode->data = arr[i];
newNode->next = NULL ;
if (*head == NULL ) {
*head = newNode;
} else {
ListNode *p = *head;
while (p->next != NULL ) p = p->next;
p->next = newNode;
}
}
}
void traverse (LinkList head) {
ListNode *p = head;
while (p != NULL ) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
LinkList reverseLinkList (LinkList head) {
if (head == NULL || head->next == NULL ) {
return head;
}
ListNode *prev = NULL ;
ListNode *curr = head;
ListNode *next = NULL ;
while (curr != NULL ) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
int main () {
int arr[] = {1 , 2 , 3 , 4 , 5 };
int len = sizeof (arr)/sizeof (arr[0 ]);
LinkList head = NULL ;
createLinkList(&head, arr, len);
printf ("反转前链表:" );
traverse(head);
LinkList reversedHead = reverseLinkList(head);
printf ("反转后链表:" );
traverse(reversedHead);
return 0 ;
}
反转前链表:1 2 3 4 5
反转后链表:5 4 3 2 1
问题 2:单链表环检测 链表环是指链表的尾节点 next 指针不指向 NULL,而是指向链表中的某个节点,导致遍历无法结束。检测链表环的经典算法是'快慢指针法'(Floyd 算法):
定义两个指针:快指针(fast)每次走 2 步,慢指针(slow)每次走 1 步;
若链表无环,快指针会先到达 NULL;
若链表有环,快指针会追上慢指针(两者指向同一节点)。
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode {
int data;
struct ListNode * next ;
} ListNode, *LinkList;
void createCycleLinkList (LinkList *head, int arr[], int len, int pos) {
*head = NULL ;
ListNode *cycleNode = NULL ;
ListNode *tail = NULL ;
for (int i = 0 ; i < len; i++) {
ListNode *newNode = (ListNode *)malloc (sizeof (ListNode));
newNode->data = arr[i];
newNode->next = NULL ;
if (*head == NULL ) {
*head = newNode;
} else {
tail->next = newNode;
}
tail = newNode;
if (i + 1 == pos) {
cycleNode = newNode;
}
}
if (cycleNode != NULL ) {
tail->next = cycleNode;
}
}
int hasCycle (LinkList head) {
if (head == NULL || head->next == NULL ) {
return 0 ;
}
ListNode *slow = head;
ListNode *fast = head->next;
while (slow != fast) {
if (fast == NULL || fast->next == NULL ) {
return 0 ;
}
slow = slow->next;
fast = fast->next->next;
}
return 1 ;
}
int main () {
int arr[] = {1 , 2 , 3 , 4 , 5 };
int len = sizeof (arr)/sizeof (arr[0 ]);
LinkList head1 = NULL , head2 = NULL ;
createCycleLinkList(&head1, arr, len, 0 );
printf ("链表 1 是否有环:%s\n" , hasCycle(head1) ? "是" : "否" );
createCycleLinkList(&head2, arr, len, 2 );
printf ("链表 2 是否有环:%s\n" , hasCycle(head2) ? "是" : "否" );
return 0 ;
}
[成功] 结论:快慢指针法是链表环检测的最优解,时间复杂度 O(n),空间复杂度 O(1),无需额外存储节点信息。
1.3.3 双向链表的实现与优势 单链表的节点只有一个 next 指针,只能从前往后遍历;双向链表的每个节点增加了一个 prev 指针(指向前一个节点),支持双向遍历,插入、删除操作更灵活(无需遍历查找前驱节点)。
双向链表的核心操作 #include <stdio.h>
#include <stdlib.h>
typedef struct DListNode {
int data;
struct DListNode * prev ;
struct DListNode * next ;
} DListNode, *DLinkList;
DLinkList initDLinkList () {
DListNode *head = (DListNode *)malloc (sizeof (DListNode));
if (head == NULL ) {
printf ("[注意] 头节点内存分配失败!\n" );
return NULL ;
}
head->prev = NULL ;
head->next = NULL ;
return head;
}
int insertTail (DLinkList head, int data) {
if (head == NULL ) {
printf ("[注意] 链表未初始化!\n" );
return -1 ;
}
DListNode *newNode = (DListNode *)malloc (sizeof (DListNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->prev = NULL ;
newNode->next = NULL ;
DListNode *p = head;
while (p->next != NULL ) {
p = p->next;
}
p->next = newNode;
newNode->prev = p;
return 0 ;
}
int insertHead (DLinkList head, int data) {
if (head == NULL ) {
printf ("[注意] 链表未初始化!\n" );
return -1 ;
}
DListNode *newNode = (DListNode *)malloc (sizeof (DListNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = head->next;
newNode->prev = head;
if (head->next != NULL ) {
head->next->prev = newNode;
}
head->next = newNode;
return 0 ;
}
void traverseForward (DLinkList head) {
if (head == NULL || head->next == NULL ) {
printf ("[注意] 链表为空!\n" );
return ;
}
DListNode *p = head->next;
printf ("正向遍历:" );
while (p != NULL ) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
void traverseBackward (DLinkList head) {
if (head == NULL || head->next == NULL ) {
printf ("[注意] 链表为空!\n" );
return ;
}
DListNode *p = head;
while (p->next != NULL ) {
p = p->next;
}
printf ("反向遍历:" );
while (p != head) {
printf ("%d " , p->data);
p = p->prev;
}
printf ("\n" );
}
int deleteByValue (DLinkList head, int data) {
if (head == NULL || head->next == NULL ) {
printf ("[注意] 链表为空,无法删除!\n" );
return -1 ;
}
DListNode *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);
p = NULL ;
return 0 ;
}
p = p->next;
}
printf ("[注意] 未找到值为%d的节点!\n" , data);
return -1 ;
}
void destroyDLinkList (DLinkList *head) {
if (*head == NULL ) {
return ;
}
DListNode *p = *head;
while (p != NULL ) {
DListNode *temp = p;
p = p->next;
free (temp);
temp = NULL ;
}
*head = NULL ;
printf ("[成功] 双向链表销毁成功!\n" );
}
int main () {
DLinkList head = initDLinkList();
insertTail(head, 10 );
insertTail(head, 20 );
insertTail(head, 30 );
traverseForward(head);
traverseBackward(head);
insertHead(head, 5 );
traverseForward(head);
traverseBackward(head);
deleteByValue(head, 20 );
traverseForward(head);
traverseBackward(head);
destroyDLinkList(&head);
traverseForward(head);
return 0 ;
}
正向遍历:10 20 30
反向遍历:30 20 10
正向遍历:5 10 20 30
反向遍历:30 20 10 5
正向遍历:5 10 30
反向遍历:30 10 5
[成功] 双向链表销毁成功!
[注意] 链表为空!
[提示] 技巧:双向链表通常设计带头节点(不存储数据),可避免处理头节点为空的特殊情况,简化插入、删除逻辑;prev 指针使反向遍历和节点删除更高效(无需查找前驱节点)。
1.4 栈:先进后出(LIFO)的数据结构 栈是一种遵循'先进后出'(Last In First Out)规则的线性数据结构,仅允许在一端(栈顶)进行插入(入栈)和删除(出栈)操作。栈的实现有两种方式:数组栈(基于数组)和链式栈(基于链表)。
1.4.1 数组栈的实现 数组栈基于固定大小的数组实现,优点是实现简单、访问速度快;缺点是容量固定,扩容不便。核心操作包括:初始化、入栈、出栈、取栈顶元素、判空、判满。
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} ArrayStack;
void initArrayStack (ArrayStack *stack ) {
stack ->top = -1 ;
}
int isEmpty (ArrayStack *stack ) {
return stack ->top == -1 ? 1 : 0 ;
}
int isFull (ArrayStack *stack ) {
return stack ->top == MAX_SIZE - 1 ? 1 : 0 ;
}
int push (ArrayStack *stack , int data) {
if (isFull(stack )) {
printf ("[注意] 栈满,无法入栈!\n" );
return -1 ;
}
stack ->top++;
stack ->data[stack ->top] = data;
return 0 ;
}
int pop (ArrayStack *stack ) {
if (isEmpty(stack )) {
printf ("[注意] 栈空,无法出栈!\n" );
return -1 ;
}
int data = stack ->data[stack ->top];
stack ->top--;
return data;
}
int getTop (ArrayStack *stack ) {
if (isEmpty(stack )) {
printf ("[注意] 栈空,无栈顶元素!\n" );
return -1 ;
}
return stack ->data[stack ->top];
}
void traverseArrayStack (ArrayStack *stack ) {
if (isEmpty(stack )) {
printf ("[注意] 栈空!\n" );
return ;
}
printf ("栈元素(栈顶->栈底):" );
for (int i = stack ->top; i >= 0 ; i--) {
printf ("%d " , stack ->data[i]);
}
printf ("\n" );
}
int getStackSize (ArrayStack *stack ) {
return stack ->top + 1 ;
}
int main () {
ArrayStack stack ;
initArrayStack(&stack );
push(&stack , 10 );
push(&stack , 20 );
push(&stack , 30 );
push(&stack , 40 );
printf ("入栈 4 个元素后:" );
traverseArrayStack(&stack );
printf ("栈当前大小:%d\n" , getStackSize(&stack ));
int topVal = getTop(&stack );
if (topVal != -1 ) {
printf ("当前栈顶元素:%d\n" , topVal);
}
int popVal = pop(&stack );
if (popVal != -1 ) {
printf ("弹出的栈顶元素:%d\n" , popVal);
}
printf ("出栈 1 个元素后:" );
traverseArrayStack(&stack );
printf ("栈当前大小:%d\n" , getStackSize(&stack ));
for (int i = 50 ; i <= 100 ; i += 10 ) {
push(&stack , i);
}
printf ("多次入栈后:" );
traverseArrayStack(&stack );
printf ("栈当前大小:%d\n" , getStackSize(&stack ));
push(&stack , 110 );
return 0 ;
}
入栈 4 个元素后:栈元素(栈顶-> 栈底):40 30 20 10
栈当前大小:4
当前栈顶元素:40
弹出的栈顶元素:40
出栈 1 个元素后:栈元素(栈顶-> 栈底):30 20 10
栈当前大小:3
多次入栈后:栈元素(栈顶-> 栈底):100 90 80 70 60 50 30 20 10
栈当前大小:9
[注意] 栈满,无法入栈!
[注意] 数组栈的容量由 MAX_SIZE 固定,若实际需求超过容量,需手动修改 MAX_SIZE 或实现动态扩容(通过 realloc 重新分配数组内存)。
1.4.2 链式栈的实现 链式栈基于单链表实现,优点是容量动态扩展(无需预先指定大小),缺点是插入、删除操作需额外的指针操作。链式栈通常将链表头部作为栈顶(入栈、出栈操作更高效)。
#include <stdio.h>
#include <stdlib.h>
typedef struct StackNode {
int data;
struct StackNode * next ;
} StackNode, *LinkStack;
void initLinkStack (LinkStack *top) {
*top = NULL ;
}
int isLinkStackEmpty (LinkStack top) {
return top == NULL ? 1 : 0 ;
}
int linkStackPush (LinkStack *top, int data) {
StackNode *newNode = (StackNode *)malloc (sizeof (StackNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = *top;
*top = newNode;
return 0 ;
}
int linkStackPop (LinkStack *top) {
if (isLinkStackEmpty(*top)) {
printf ("[注意] 栈空,无法出栈!\n" );
return -1 ;
}
StackNode *temp = *top;
int data = temp->data;
*top = temp->next;
free (temp);
temp = NULL ;
return data;
}
int linkStackGetTop (LinkStack top) {
if (isLinkStackEmpty(top)) {
printf ("[注意] 栈空,无栈顶元素!\n" );
return -1 ;
}
return top->data;
}
void traverseLinkStack (LinkStack top) {
if (isLinkStackEmpty(top)) {
printf ("[注意] 栈空!\n" );
return ;
}
StackNode *p = top;
printf ("栈元素(栈顶->栈底):" );
while (p != NULL ) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
int getLinkStackSize (LinkStack top) {
int size = 0 ;
StackNode *p = top;
while (p != NULL ) {
size++;
p = p->next;
}
return size;
}
void destroyLinkStack (LinkStack *top) {
StackNode *p = *top;
while (p != NULL ) {
StackNode *temp = p;
p = p->next;
free (temp);
temp = NULL ;
}
*top = NULL ;
printf ("[成功] 链式栈销毁成功!\n" );
}
int main () {
LinkStack top;
initLinkStack(&top);
linkStackPush(&top, 10 );
linkStackPush(&top, 20 );
linkStackPush(&top, 30 );
printf ("入栈 3 个元素后:" );
traverseLinkStack(top);
printf ("栈当前大小:%d\n" , getLinkStackSize(top));
int topVal = linkStackGetTop(top);
if (topVal != -1 ) {
printf ("当前栈顶元素:%d\n" , topVal);
}
int popVal = linkStackPop(&top);
if (popVal != -1 ) {
printf ("弹出的栈顶元素:%d\n" , popVal);
}
printf ("出栈 1 个元素后:" );
traverseLinkStack(top);
printf ("栈当前大小:%d\n" , getLinkStackSize(top));
linkStackPush(&top, 40 );
linkStackPush(&top, 50 );
linkStackPush(&top, 60 );
printf ("继续入栈 3 个元素后:" );
traverseLinkStack(top);
printf ("栈当前大小:%d\n" , getLinkStackSize(top));
destroyLinkStack(&top);
traverseLinkStack(top);
return 0 ;
}
入栈 3 个元素后:栈元素(栈顶-> 栈底):30 20 10
栈当前大小:3
当前栈顶元素:30
弹出的栈顶元素:30
出栈 1 个元素后:栈元素(栈顶-> 栈底):20 10
栈当前大小:2
继续入栈 3 个元素后:栈元素(栈顶-> 栈底):60 50 40 20 10
栈当前大小:5
[成功] 链式栈销毁成功!
[注意] 栈空!
[成功] 结论:数组栈和链式栈各有优劣——数组栈适合元素数量固定、追求高效访问的场景;链式栈适合元素数量不确定、需要动态扩容的场景。
1.4.3 栈的应用场景:括号匹配 栈的'先进后出'特性使其非常适合解决括号匹配问题:
遍历字符串,遇到左括号((、{、[)则入栈;
遇到右括号()、}、]),弹出栈顶元素,判断是否为对应的左括号;
遍历结束后,若栈为空且所有右括号都找到匹配的左括号,则匹配成功;否则匹配失败。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_STACK_SIZE 100
typedef struct {
char data[MAX_STACK_SIZE];
int top;
} Stack;
void initStack (Stack *stack ) {
stack ->top = -1 ;
}
int push (Stack *stack , char c) {
if (stack ->top == MAX_STACK_SIZE - 1 ) {
return -1 ;
}
stack ->data[++stack ->top] = c;
return 0 ;
}
char pop (Stack *stack ) {
if (stack ->top == -1 ) {
return '\0' ;
}
return stack ->data[stack ->top--];
}
int isEmpty (Stack *stack ) {
return stack ->top == -1 ? 1 : 0 ;
}
int isMatch (char left, char right) {
return (left == '(' && right == ')' ) ||
(left == '{' && right == '}' ) ||
(left == '[' && right == ']' );
}
int bracketMatch (const char * str) {
Stack stack ;
initStack(&stack );
int len = strlen (str);
for (int i = 0 ; i < len; i++) {
char c = str[i];
if (c == '(' || c == '{' || c == '[' ) {
push(&stack , c);
}
else if (c == ')' || c == '}' || c == ']' ) {
char left = pop(&stack );
if (left == '\0' || !isMatch(left, c)) {
return 0 ;
}
}
else {
continue ;
}
}
return isEmpty(&stack ) ? 1 : 0 ;
}
int main () {
const char * str1 = "({[]})" ;
const char * str2 = "({[)]}" ;
const char * str3 = "((()))" ;
const char * str4 = "(()" ;
printf ("字符串\"%s\"括号匹配:%s\n" , str1, bracketMatch(str1) ? "成功" : "失败" );
printf ("字符串\"%s\"括号匹配:%s\n" , str2, bracketMatch(str2) ? "成功" : "失败" );
printf ("字符串\"%s\"括号匹配:%s\n" , str3, bracketMatch(str3) ? "成功" : "失败" );
printf ("字符串\"%s\"括号匹配:%s\n" , str4, bracketMatch(str4) ? "成功" : "失败" );
return 0 ;
}
字符串"({[]})" 括号匹配:成功
字符串"({[)]}" 括号匹配:失败
字符串"((()))" 括号匹配:成功
字符串"(()" 括号匹配:失败
[提示] 技巧:括号匹配是栈的经典应用,类似场景还包括表达式求值(前缀、中缀、后缀表达式转换)、函数调用栈等。
1.5 队列:先进先出(FIFO)的数据结构 队列是一种遵循'先进先出'(First In First Out)规则的线性数据结构,允许在一端(队尾)插入元素(入队),在另一端(队头)删除元素(出队)。队列的实现有两种方式:循环队列(基于数组)和链式队列(基于链表)。
1.5.1 循环队列的实现 普通数组队列存在'假溢出'问题(队尾到达数组末尾,但队头前仍有空闲空间),循环队列通过'取模运算'将数组首尾相连,解决假溢出问题,提高内存利用率。
定义队头指针 front(指向队头元素)和队尾指针 rear(指向队尾元素的下一个位置);
队空条件:front == rear;
队满条件:(rear + 1) % MAX_SIZE == front(预留一个空闲位置区分队空和队满);
元素个数:(rear - front + MAX_SIZE) % MAX_SIZE。
#include <stdio.h>
#include <stdlib.h>
#define MAX_QUEUE_SIZE 5
typedef struct {
int data[MAX_QUEUE_SIZE];
int front;
int rear;
} CircularQueue;
void initCircularQueue (CircularQueue *queue ) {
queue ->front = 0 ;
queue ->rear = 0 ;
}
int isQueueEmpty (CircularQueue *queue ) {
return queue ->front == queue ->rear ? 1 : 0 ;
}
int isQueueFull (CircularQueue *queue ) {
return (queue ->rear + 1 ) % MAX_QUEUE_SIZE == queue ->front ? 1 : 0 ;
}
int enQueue (CircularQueue *queue , int data) {
if (isQueueFull(queue )) {
printf ("[注意] 队列满,无法入队!\n" );
return -1 ;
}
queue ->data[queue ->rear] = data;
queue ->rear = (queue ->rear + 1 ) % MAX_QUEUE_SIZE;
return 0 ;
}
int deQueue (CircularQueue *queue ) {
if (isQueueEmpty(queue )) {
printf ("[注意] 队列为空,无法出队!\n" );
return -1 ;
}
int data = queue ->data[queue ->front];
queue ->front = (queue ->front + 1 ) % MAX_QUEUE_SIZE;
return data;
}
int getQueueFront (CircularQueue *queue ) {
if (isQueueEmpty(queue )) {
printf ("[注意] 队列为空,无队头元素!\n" );
return -1 ;
}
return queue ->data[queue ->front];
}
void traverseCircularQueue (CircularQueue *queue ) {
if (isQueueEmpty(queue )) {
printf ("[注意] 队列为空!\n" );
return ;
}
int i = queue ->front;
printf ("队列元素(队头->队尾):" );
while (i != queue ->rear) {
printf ("%d " , queue ->data[i]);
i = (i + 1 ) % MAX_QUEUE_SIZE;
}
printf ("\n" );
}
int getQueueSize (CircularQueue *queue ) {
return (queue ->rear - queue ->front + MAX_QUEUE_SIZE) % MAX_QUEUE_SIZE;
}
int main () {
CircularQueue queue ;
initCircularQueue(&queue );
enQueue(&queue , 10 );
enQueue(&queue , 20 );
enQueue(&queue , 30 );
enQueue(&queue , 40 );
printf ("入队 4 个元素后:" );
traverseCircularQueue(&queue );
printf ("队列当前大小:%d\n" , getQueueSize(&queue ));
enQueue(&queue , 50 );
int frontVal = getQueueFront(&queue );
if (frontVal != -1 ) {
printf ("当前队头元素:%d\n" , frontVal);
}
int deVal = deQueue(&queue );
if (deVal != -1 ) {
printf ("出队的元素:%d\n" , deVal);
}
printf ("出队 1 个元素后:" );
traverseCircularQueue(&queue );
printf ("队列当前大小:%d\n" , getQueueSize(&queue ));
enQueue(&queue , 50 );
printf ("入队元素 50 后:" );
traverseCircularQueue(&queue );
printf ("队列当前大小:%d\n" , getQueueSize(&queue ));
deQueue(&queue );
deQueue(&queue );
printf ("连续出队 2 个元素后:" );
traverseCircularQueue(&queue );
printf ("队列当前大小:%d\n" , getQueueSize(&queue ));
return 0 ;
}
入队 4 个元素后:队列元素(队头-> 队尾):10 20 30 40
队列当前大小:4
[注意] 队列满,无法入队!
当前队头元素:10
出队的元素:10
出队 1 个元素后:队列元素(队头-> 队尾):20 30 40
队列当前大小:3
入队元素 50 后:队列元素(队头-> 队尾):20 30 40 50
队列当前大小:4
连续出队 2 个元素后:队列元素(队头-> 队尾):40 50
队列当前大小:2
[提示] 技巧:循环队列的核心是'取模运算',通过 (index + 1) % MAX_SIZE 实现指针的循环移动,解决普通数组队列的假溢出问题。
1.5.2 链式队列的实现 链式队列基于单链表实现,队头指向链表的头节点,队尾指向链表的尾节点,入队操作在队尾添加节点,出队操作删除队头节点。链式队列无容量限制,适合元素数量不确定的场景。
#include <stdio.h>
#include <stdlib.h>
typedef struct QueueNode {
int data;
struct QueueNode * next ;
} QueueNode;
typedef struct {
QueueNode *front;
QueueNode *rear;
} LinkQueue;
void initLinkQueue (LinkQueue *queue ) {
queue ->front = (QueueNode *)malloc (sizeof (QueueNode));
if (queue ->front == NULL ) {
printf ("[注意] 头节点内存分配失败!\n" );
return ;
}
queue ->front->next = NULL ;
queue ->rear = queue ->front;
}
int isLinkQueueEmpty (LinkQueue *queue ) {
return queue ->front == queue ->rear ? 1 : 0 ;
}
int linkQueueEnQueue (LinkQueue *queue , int data) {
QueueNode *newNode = (QueueNode *)malloc (sizeof (QueueNode));
if (newNode == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
newNode->data = data;
newNode->next = NULL ;
queue ->rear->next = newNode;
queue ->rear = newNode;
return 0 ;
}
int linkQueueDeQueue (LinkQueue *queue ) {
if (isLinkQueueEmpty(queue )) {
printf ("[注意] 队列为空,无法出队!\n" );
return -1 ;
}
QueueNode *temp = queue ->front->next;
int data = temp->data;
queue ->front->next = temp->next;
if (queue ->rear == temp) {
queue ->rear = queue ->front;
}
free (temp);
temp = NULL ;
return data;
}
int linkQueueGetFront (LinkQueue *queue ) {
if (isLinkQueueEmpty(queue )) {
printf ("[注意] 队列为空,无队头元素!\n" );
return -1 ;
}
return queue ->front->next->data;
}
void traverseLinkQueue (LinkQueue *queue ) {
if (isLinkQueueEmpty(queue )) {
printf ("[注意] 队列为空!\n" );
return ;
}
QueueNode *p = queue ->front->next;
printf ("队列元素(队头->队尾):" );
while (p != NULL ) {
printf ("%d " , p->data);
p = p->next;
}
printf ("\n" );
}
int getLinkQueueSize (LinkQueue *queue ) {
int size = 0 ;
QueueNode *p = queue ->front->next;
while (p != NULL ) {
size++;
p = p->next;
}
return size;
}
void destroyLinkQueue (LinkQueue *queue ) {
QueueNode *p = queue ->front->next;
while (p != NULL ) {
QueueNode *temp = p;
p = p->next;
free (temp);
temp = NULL ;
}
free (queue ->front);
queue ->front = NULL ;
queue ->rear = NULL ;
printf ("[成功] 链式队列销毁成功!\n" );
}
int main () {
LinkQueue queue ;
initLinkQueue(&queue );
linkQueueEnQueue(&queue , 10 );
linkQueueEnQueue(&queue , 20 );
linkQueueEnQueue(&queue , 30 );
printf ("入队 3 个元素后:" );
traverseLinkQueue(&queue );
printf ("队列当前大小:%d\n" , getLinkQueueSize(&queue ));
int frontVal = linkQueueGetFront(&queue );
if (frontVal != -1 ) {
printf ("当前队头元素:%d\n" , frontVal);
}
int deVal = linkQueueDeQueue(&queue );
if (deVal != -1 ) {
printf ("出队的元素:%d\n" , deVal);
}
printf ("出队 1 个元素后:" );
traverseLinkQueue(&queue );
printf ("队列当前大小:%d\n" , getLinkQueueSize(&queue ));
linkQueueEnQueue(&queue , 40 );
linkQueueEnQueue(&queue , 50 );
printf ("继续入队 2 个元素后:" );
traverseLinkQueue(&queue );
printf ("队列当前大小:%d\n" , getLinkQueueSize(&queue ));
linkQueueDeQueue(&queue );
linkQueueDeQueue(&queue );
printf ("连续出队 2 个元素后:" );
traverseLinkQueue(&queue );
printf ("队列当前大小:%d\n" , getLinkQueueSize(&queue ));
destroyLinkQueue(&queue );
traverseLinkQueue(&queue );
return 0 ;
}
入队 3 个元素后:队列元素(队头-> 队尾):10 20 30
队列当前大小:3
当前队头元素:10
出队的元素:10
出队 1 个元素后:队列元素(队头-> 队尾):20 30
队列当前大小:2
继续入队 2 个元素后:队列元素(队头-> 队尾):20 30 40 50
队列当前大小:4
连续出队 2 个元素后:队列元素(队头-> 队尾):40 50
队列当前大小:2
[成功] 链式队列销毁成功!
[注意] 队列为空!
[成功] 结论:循环队列适合元素数量固定、追求内存高效利用的场景;链式队列适合元素数量不确定、需要动态扩容的场景,且无需处理循环指针,实现更直观。
1.5.3 队列的应用场景:生产者 - 消费者模型 队列是'生产者 - 消费者模型'的核心数据结构——生产者线程向队列中添加数据(入队),消费者线程从队列中获取数据(出队),队列起到缓冲和同步的作用,解耦生产者和消费者。
示例 11:模拟生产者 - 消费者模型(单线程简化版)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_QUEUE_SIZE 5
typedef struct {
char data[MAX_QUEUE_SIZE][20 ];
int front;
int rear;
} MsgQueue;
void initMsgQueue (MsgQueue *queue ) {
queue ->front = 0 ;
queue ->rear = 0 ;
}
int isMsgQueueEmpty (MsgQueue *queue ) {
return queue ->front == queue ->rear ? 1 : 0 ;
}
int isMsgQueueFull (MsgQueue *queue ) {
return (queue ->rear + 1 ) % MAX_QUEUE_SIZE == queue ->front ? 1 : 0 ;
}
int produce (MsgQueue *queue , const char * msg) {
if (isMsgQueueFull(queue )) {
printf ("[注意] 队列满,生产者等待...\n" );
return -1 ;
}
strcpy (queue ->data[queue ->rear], msg);
queue ->rear = (queue ->rear + 1 ) % MAX_QUEUE_SIZE;
printf ("[入队] 生产者生产:%s\n" , msg);
return 0 ;
}
int consume (MsgQueue *queue , char * msg) {
if (isMsgQueueEmpty(queue )) {
printf ("[注意] 队列空,消费者等待...\n" );
return -1 ;
}
strcpy (msg, queue ->data[queue ->front]);
queue ->front = (queue ->front + 1 ) % MAX_QUEUE_SIZE;
printf ("[出队] 消费者消费:%s\n" , msg);
return 0 ;
}
int main () {
MsgQueue queue ;
initMsgQueue(&queue );
char msg[20 ];
produce(&queue , "任务 1" );
produce(&queue , "任务 2" );
produce(&queue , "任务 3" );
produce(&queue , "任务 4" );
sleep(1 );
consume(&queue , msg);
consume(&queue , msg);
sleep(1 );
produce(&queue , "任务 5" );
produce(&queue , "任务 6" );
sleep(1 );
consume(&queue , msg);
consume(&queue , msg);
consume(&queue , msg);
sleep(1 );
consume(&queue , msg);
return 0 ;
}
[入队] 生产者生产:任务 1
[入队] 生产者生产:任务 2
[入队] 生产者生产:任务 3
[入队] 生产者生产:任务 4
[出队] 消费者消费:任务 1
[出队] 消费者消费:任务 2
[入队] 生产者生产:任务 5
[注意] 队列满,生产者等待...
[出队] 消费者消费:任务 3
[出队] 消费者消费:任务 4
[出队] 消费者消费:任务 5
[注意] 队列空,消费者等待...
[提示] 技巧:在多线程环境中,生产者 - 消费者模型需要添加互斥锁(如 pthread_mutex_t)和条件变量(如 pthread_cond_t)保证线程安全,避免并发读写冲突。队列的缓冲特性使生产者和消费者可以异步工作,无需等待对方立即响应,提升系统吞吐量。
1.6 实际项目案例:基于链表的学生成绩管理系统
1.6.1 案例需求
新增学生信息(学号、姓名、三门课程成绩);
查找学生信息(按学号查找);
修改学生成绩(按学号修改指定课程成绩);
删除学生信息(按学号删除);
统计功能(计算平均分、排序输出、统计及格人数);
遍历输出所有学生信息;
销毁系统(释放所有内存)。
1.6.2 案例实现 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct Student {
char id[20 ];
char name[20 ];
float scores[3 ];
float avgScore;
struct Student * next ;
} Student, *StudentList;
float calculateAvg (float scores[]) {
return (scores[0 ] + scores[1 ] + scores[2 ]) / 3.0f ;
}
bool isValidScore (float score) {
return score >= 0.0f && score <= 100.0f ;
}
StudentList initStudentList () {
Student *head = (Student *)malloc (sizeof (Student));
if (head == NULL ) {
printf ("[注意] 链表初始化失败!\n" );
exit (-1 );
}
head->next = NULL ;
return head;
}
int addStudent (StudentList head, const char * id, const char * name, float scores[]) {
Student *p = head->next;
while (p != NULL ) {
if (strcmp (p->id, id) == 0 ) {
printf ("[注意] 学号%s已存在,新增失败!\n" , id);
return -1 ;
}
p = p->next;
}
for (int i = 0 ; i < 3 ; i++) {
if (!isValidScore(scores[i])) {
printf ("[注意] 成绩%.1f 非法(需在 0-100 之间),新增失败!\n" , scores[i]);
return -1 ;
}
}
Student *newStudent = (Student *)malloc (sizeof (Student));
if (newStudent == NULL ) {
printf ("[注意] 节点内存分配失败!\n" );
return -1 ;
}
strcpy (newStudent->id, id);
strcpy (newStudent->name, name);
memcpy (newStudent->scores, scores, sizeof (scores));
newStudent->avgScore = calculateAvg(scores);
newStudent->next = NULL ;
p = head;
while (p->next != NULL ) {
p = p->next;
}
p->next = newStudent;
printf ("[成功] 学号%s学生信息新增成功!\n" , id);
return 0 ;
}
Student *findStudentByID (StudentList head, const char * id) {
Student *p = head->next;
while (p != NULL ) {
if (strcmp (p->id, id) == 0 ) {
return p;
}
p = p->next;
}
printf ("[注意] 未找到学号%s的学生!\n" , id);
return NULL ;
}
int modifyStudentScore (StudentList head, const char * id, int courseIdx, float newScore) {
if (courseIdx < 0 || courseIdx > 2 ) {
printf ("[注意] 课程索引非法(0=语文,1=数学,2=英语)!\n" );
return -1 ;
}
if (!isValidScore(newScore)) {
printf ("[注意] 成绩%.1f 非法(需在 0-100 之间)!\n" , newScore);
return -1 ;
}
Student *student = findStudentByID(head, id);
if (student == NULL ) {
return -1 ;
}
float oldScore = student->scores[courseIdx];
student->scores[courseIdx] = newScore;
student->avgScore = calculateAvg(student->scores);
const char * courseNames[] = {"语文" , "数学" , "英语" };
printf ("[成功] 学号%s学生的%s成绩修改成功:%.1f → %.1f,新平均分为%.1f\n" , id, courseNames[courseIdx], oldScore, newScore, student->avgScore);
return 0 ;
}
int deleteStudentByID (StudentList head, const char * id) {
Student *prev = head;
Student *curr = head->next;
while (curr != NULL ) {
if (strcmp (curr->id, id) == 0 ) {
prev->next = curr->next;
free (curr);
curr = NULL ;
printf ("[成功] 学号%s学生信息删除成功!\n" , id);
return 0 ;
}
prev = curr;
curr = curr->next;
}
printf ("[注意] 未找到学号%s的学生,删除失败!\n" , id);
return -1 ;
}
void traverseStudentList (StudentList head) {
Student *p = head->next;
if (p == NULL ) {
printf ("[注意] 系统中暂无学生信息!\n" );
return ;
}
printf ("\n==================== 学生成绩列表 ====================\n" );
printf ("%-10s %-10s %-8s %-8s %-8s %-8s\n" , "学号" , "姓名" , "语文" , "数学" , "英语" , "平均分" );
printf ("------------------------------------------------------\n" );
while (p != NULL ) {
printf ("%-10s %-10s %-8.1f %-8.1f %-8.1f %-8.1f\n" , p->id, p->name, p->scores[0 ], p->scores[1 ], p->scores[2 ], p->avgScore);
p = p->next;
}
printf ("======================================================\n\n" );
}
void sortStudentsByAvg (StudentList head) {
Student *p = head->next;
if (p == NULL || p->next == NULL ) {
printf ("[注意] 学生数量不足 2 人,无需排序!\n" );
return ;
}
bool swapped;
Student *ptr1;
Student *lptr = NULL ;
do {
swapped = false ;
ptr1 = head->next;
while (ptr1->next != lptr) {
if (ptr1->avgScore < ptr1->next->avgScore) {
Student temp = *ptr1;
*ptr1 = *(ptr1->next);
*(ptr1->next) = temp;
Student *tempNext = ptr1->next;
ptr1->next = tempNext->next;
tempNext->next = ptr1;
swapped = true ;
}
ptr1 = ptr1->next;
}
lptr = ptr1;
} while (swapped);
printf ("[成功] 学生按平均分降序排序完成!\n" );
traverseStudentList(head);
}
void countPassedStudents (StudentList head) {
Student *p = head->next;
if (p == NULL ) {
printf ("[注意] 系统中暂无学生信息,无法统计!\n" );
return ;
}
int passCount[3 ] = {0 };
int totalCount = 0 ;
while (p != NULL ) {
totalCount++;
for (int i = 0 ; i < 3 ; i++) {
if (p->scores[i] >= 60.0f ) {
passCount[i]++;
}
}
p = p->next;
}
const char * courseNames[] = {"语文" , "数学" , "英语" };
printf ("\n==================== 及格人数统计 ====================\n" );
for (int i = 0 ; i < 3 ; i++) {
float passRate = (float )passCount[i] / totalCount * 100 ;
printf ("%s:及格人数%d/%d,及格率%.1f%%\n" , courseNames[i], passCount[i], totalCount, passRate);
}
printf ("======================================================\n\n" );
}
void destroyStudentList (StudentList *head) {
Student *p = *head;
while (p != NULL ) {
Student *temp = p;
p = p->next;
free (temp);
temp = NULL ;
}
*head = NULL ;
printf ("[成功] 学生成绩管理系统销毁成功,所有内存已释放!\n" );
}
void printMenu () {
printf ("\n===== 学生成绩管理系统 =====\n" );
printf ("1. 新增学生信息\n" );
printf ("2. 查找学生信息\n" );
printf ("3. 修改学生成绩\n" );
printf ("4. 删除学生信息\n" );
printf ("5. 遍历所有学生\n" );
printf ("6. 按平均分排序\n" );
printf ("7. 统计及格人数\n" );
printf ("0. 退出系统\n" );
printf ("===========================\n" );
printf ("请选择功能(0-7):" );
}
int main () {
StudentList head = initStudentList();
int choice;
char id[20 ], name[20 ];
float scores[3 ], newScore;
int courseIdx;
Student *student;
while (1 ) {
printMenu();
scanf ("%d" , &choice);
getchar();
switch (choice) {
case 1 :
printf ("请输入学号:" );
scanf ("%s" , id);
printf ("请输入姓名:" );
scanf ("%s" , name);
printf ("请输入语文、数学、英语成绩(用空格分隔):" );
scanf ("%f %f %f" , &scores[0 ], &scores[1 ], &scores[2 ]);
addStudent(head, id, name, scores);
break ;
case 2 :
printf ("请输入要查找的学号:" );
scanf ("%s" , id);
student = findStudentByID(head, id);
if (student != NULL ) {
printf ("\n找到学生信息:\n" );
printf ("学号:%s,姓名:%s\n" , student->id, student->name);
printf ("语文:%.1f,数学:%.1f,英语:%.1f,平均分:%.1f\n" , student->scores[0 ], student->scores[1 ], student->scores[2 ], student->avgScore);
}
break ;
case 3 :
printf ("请输入要修改的学生学号:" );
scanf ("%s" , id);
printf ("请选择要修改的课程(0=语文,1=数学,2=英语):" );
scanf ("%d" , &courseIdx);
printf ("请输入新成绩(0-100):" );
scanf ("%f" , &newScore);
modifyStudentScore(head, id, courseIdx, newScore);
break ;
case 4 :
printf ("请输入要删除的学生学号:" );
scanf ("%s" , id);
deleteStudentByID(head, id);
break ;
case 5 :
traverseStudentList(head);
break ;
case 6 :
sortStudentsByAvg(head);
break ;
case 7 :
countPassedStudents(head);
break ;
case 0 :
destroyStudentList(&head);
printf ("[再见] 感谢使用学生成绩管理系统,再见!\n" );
return 0 ;
default :
printf ("[注意] 无效的功能选择,请重新输入!\n" );
break ;
}
}
}
1.6.3 案例测试与运行结果 ===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):1
请输入学号:2024001
请输入姓名:张三
请输入语文、数学、英语成绩(用空格分隔):85 92 78
[成功] 学号 2024001 学生信息新增成功!
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):1
请输入学号:2024002
请输入姓名:李四
请输入语文、数学、英语成绩(用空格分隔):72 68 95
[成功] 学号 2024002 学生信息新增成功!
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):5
==================== 学生成绩列表 ====================
学号 姓名 语文 数学 英语 平均分
------------------------------------------------------
2024001 张三 85.0 92.0 78.0 85.0
2024002 李四 72.0 68.0 95.0 78.3
======================================================
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):3
请输入要修改的学生学号:2024002
请选择要修改的课程(0=语文,1=数学,2=英语):1
请输入新成绩(0-100):75
[成功] 学号 2024002 学生的数学成绩修改成功:68.0 → 75.0,新平均分为 80.7
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):6
[成功] 学生按平均分降序排序完成!
==================== 学生成绩列表 ====================
学号 姓名 语文 数学 英语 平均分
------------------------------------------------------
2024001 张三 85.0 92.0 78.0 85.0
2024002 李四 72.0 75.0 95.0 80.7
======================================================
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):7
==================== 及格人数统计 ====================
语文:及格人数 2/2,及格率 100.0%
数学:及格人数 2/2,及格率 100.0%
英语:及格人数 2/2,及格率 100.0%
======================================================
===== 学生成绩管理系统 =====
1. 新增学生信息
2. 查找学生信息
3. 修改学生成绩
4. 删除学生信息
5. 遍历所有学生
6. 按平均分排序
7. 统计及格人数
0. 退出系统
===========================
请选择功能(0-7):0
[成功] 学生成绩管理系统销毁成功,所有内存已释放!
[再见] 感谢使用学生成绩管理系统,再见!
[成功] 案例总结:该系统基于单链表实现,充分利用了链表'动态扩容、插入删除高效'的特性,支持学生信息的全生命周期管理。核心亮点包括:
数据校验(学号唯一、成绩合法),提升系统稳定性;
模块化设计(新增、查找、修改等功能独立封装),便于维护和扩展;
实用统计功能(排序、及格率统计),贴合实际应用场景;
严格的内存管理(销毁时释放所有节点),避免内存泄漏。
1.7 常见问题与避坑指南
1.7.1 链表操作中的野指针与内存泄漏
野指针问题
错误场景 1:节点 next 指针未初始化,指向随机地址;
错误场景 2:释放节点后未将指针置空,后续误访问。
新建节点时,next 指针立即初始化为 NULL;
释放节点后,将指向该节点的指针(如 curr、temp)置空;
遍历链表时,判断指针非 NULL 后再访问成员。
内存泄漏问题
错误场景:只删除链表节点的引用,未调用 free 释放内存。
任何通过 malloc 分配的节点,在删除时必须调用 free;
链表销毁时,需遍历所有节点逐一释放,不可只释放头节点;
复杂操作(如排序、反转)后,确保所有节点都能被正常访问和释放。
1.7.2 栈与队列的边界条件处理
栈的边界问题
错误场景 1:栈空时执行出栈或取栈顶操作;
错误场景 2:数组栈满时执行入栈操作。
所有栈操作前先判断栈空/栈满状态;
数组栈可通过 realloc 实现动态扩容,避免固定容量限制;
链式栈无需判满,但需检查 malloc 节点是否成功。
队列的边界问题
错误场景 1:循环队列队空/队满判断混淆;
错误场景 2:链式队列出队时未处理'队列只有一个元素'的情况。
循环队列严格遵循'队空 front==rear,队满 (rear+1)%MAX_SIZE==front';
链式队列出队时,若删除的是最后一个元素,需将 rear 指针重置为头节点;
入队/出队操作后,及时更新指针位置,避免指针越界。
1.7.3 数据结构选择误区 场景需求 推荐数据结构 不推荐数据结构 原因分析 频繁插入/删除,元素数量不固定 链表、链式栈/队列 数组、数组栈/队列 链表插入删除无需移动元素,动态扩容更灵活 频繁访问元素,追求高效查询 数组、数组栈/队列 链表、链式栈/队列 数组支持随机访问(O(1)),链表需遍历(O(n)) 元素需先进先出 队列 栈 队列天然支持 FIFO,栈为 LIFO,逻辑不符 元素需先进后出 栈 队列 栈天然支持 LIFO,队列为 FIFO,逻辑不符 需双向遍历或快速查找前驱节点 双向链表 单链表 双向链表 prev 指针直接获取前驱,单链表需遍历
[提示] 技巧:数据结构的选择需结合'操作频率'和'数据特性'——高频插入删除选链表,高频访问选数组;按顺序存取选栈/队列,按逻辑选对应结构。
1.7.4 复杂操作中的逻辑错误
链表反转逻辑错误
迭代法反转时,必须用 next 指针临时保存当前节点的后继;
反转完成后,更新头节点指针,确保链表入口正确。
循环队列指针移动错误
错误场景:指针移动时未使用取模运算,导致指针越界。
队头/队尾指针移动时,严格使用 (index + 1) % MAX_SIZE;
计算元素个数时,使用 (rear - front + MAX_SIZE) % MAX_SIZE,避免负数。
1.8 本章总结 本章围绕'指针 + 结构体'核心,深入讲解了 C 语言中三大基础复杂数据结构——链表、栈、队列,核心要点如下:
指针与结构体的结合是实现复杂数据结构的基础,typedef 可简化结构体指针声明,提升代码可读性;
链表支持动态扩容,插入删除高效,单链表适合单向遍历场景,双向链表支持双向遍历,循环链表适用于环形数据存储;
栈遵循 LIFO 规则,数组栈实现简单、访问高效,链式栈容量灵活,经典应用包括括号匹配、表达式求值;
队列遵循 FIFO 规则,循环队列解决数组假溢出问题,链式队列无容量限制,经典应用包括生产者 - 消费者模型、任务调度;
实际开发中,需根据场景选择合适的数据结构,同时注重内存管理和边界条件处理,避免野指针、内存泄漏等问题。
[提示] 学习建议:复杂数据结构的核心是'指针操作'和'逻辑建模'——建议先手动模拟链表反转、栈入栈出栈、队列入队出队等操作的内存变化过程,再通过大量案例练习巩固。后续学习中,可基于本章知识扩展到更复杂的数据结构(如二叉树、哈希表),为 C 语言进阶开发(如嵌入式编程、内核开发)打下坚实基础。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online