今日学习笔记:双向链表、循环链表、栈
核心目标:掌握双向链表、循环链表的常用操作,理解栈的特性及顺序栈的实现,熟练编写各操作函数,规避常见逻辑错误(如野指针、内存泄漏、循环异常)。
一、双向链表(补充学习)
前置基础:双向链表每个节点包含前驱指针(prev)和后继指针(next),通常采用"带头节点(哨兵节点)"结构,简化空链表、首尾操作的逻辑,头节点不存储有效数据,仅用于占位和统一操作。
节点及链表结构定义:
c
#include <stdio.h>
#include <stdlib.h>
// 双向链表节点结构
typedef struct Node {
int data; // 节点存储的数据(int可替换为其他类型)
struct Node *prev; // 前驱指针,指向前一个节点
struct Node *next; // 后继指针,指向后一个节点
} Node;
// 双向链表(带头节点)结构
typedef struct {
Node *head; // 头节点指针(哨兵节点)
} DoubleList;
1. 核心操作及实现要点
(1)头删除(删除第一个有效节点)
操作逻辑:
-
- 判空:头节点的next为NULL(无有效节点),直接返回失败;
-
- 定义临时指针temp,指向待删除节点(phead->next);
-
- 关联后续节点:头节点的next指向temp的next(temp->next);
-
- 若待删除节点不是最后一个节点,需更新其下一个节点的prev,指向头节点;
-
- 释放temp节点内存,避免内存泄漏。
完整代码实现:
c
// 头删除函数(参数:双向链表指针,返回值:1成功,0失败)
int double_list_delete_head(DoubleList *list) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head->next == NULL) {
printf("双向链表为空,无法执行头删除!\n");
return 0;
}
// 2. 定义temp指针,指向待删除的第一个有效节点
Node *temp = list->head->next;
// 3. 头节点指向待删除节点的下一个节点,跳过待删除节点
list->head->next = temp->next;
// 4. 若待删除节点不是最后一个节点,更新其下一个节点的prev
if (temp->next != NULL) {
temp->next->prev = list->head;
}
// 5. 释放待删除节点内存,避免内存泄漏
free(temp);
temp = NULL; // 置空,避免野指针
printf("双向链表头删除成功!\n");
return 1;
}
关键注意:不可直接删除phead->next后忽略prev指针的更新,否则会导致链表断裂。
(2)尾删除(删除最后一个有效节点)
操作逻辑:
-
- 判空:phead->next为NULL,返回失败;
-
- 定义指针p遍历链表,找到尾节点(p->next == NULL);
-
- 定义临时指针temp保存尾节点,找到尾节点的前驱节点(p->prev);
-
- 前驱节点的next置为NULL,断开与尾节点的关联;
-
- 释放temp节点内存。
完整代码实现:
c
// 尾删除函数(参数:双向链表指针,返回值:1成功,0失败)
int double_list_delete_tail(DoubleList *list) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head->next == NULL) {
printf("双向链表为空,无法执行尾删除!\n");
return 0;
}
// 2. 遍历链表,找到尾节点(p->next == NULL)
Node *p = list->head->next;
while (p->next != NULL) {
p = p->next;
}
// 3. 保存尾节点,找到其前驱节点
Node *temp = p;
Node *prev_node = p->prev;
// 4. 前驱节点的next置为NULL,断开与尾节点的关联
prev_node->next = NULL;
// 5. 释放尾节点内存
free(temp);
temp = NULL;
printf("双向链表尾删除成功!\n");
return 1;
}
简化技巧:无需从头遍历,可通过尾节点的prev快速定位前驱,但需确保prev指针指向正确。
(3)删除指定值节点
操作逻辑:
-
- 判空:phead->next为NULL,返回失败;
-
- 定义指针p遍历链表(从phead->next开始),查找data与指定值匹配的节点;
-
- 若未找到匹配节点,返回失败;
-
- 找到节点后,关联其前驱和后继:p->prev->next = p->next;若p不是最后一个节点,p->next->prev = p->prev;
-
- 释放p节点内存。
完整代码实现:
c
// 删除指定值节点(参数:链表指针、待删除值,返回值:1成功,0失败)
int double_list_delete_val(DoubleList *list, int val) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head->next == NULL) {
printf("双向链表为空,无法删除指定值节点!\n");
return 0;
}
// 2. 遍历链表,查找与val匹配的节点
Node *p = list->head->next;
while (p != NULL && p->data != val) {
p = p->next;
}
// 3. 未找到匹配节点
if (p == NULL) {
printf("未找到值为%d的节点,删除失败!\n", val);
return 0;
}
// 4. 关联前驱和后继节点,跳过当前节点p
p->prev->next = p->next;
// 若p不是最后一个节点,更新其下一个节点的prev
if (p->next != NULL) {
p->next->prev = p->prev;
}
// 5. 释放当前节点内存
free(p);
p = NULL;
printf("删除值为%d的节点成功!\n", val);
return 1;
}
注意:若有多个相同值节点,需明确需求(删除第一个/所有匹配节点),此处默认删除第一个。
(4)销毁链表
操作逻辑(核心:避免野指针和内存泄漏):
-
- 判空:phead为NULL或phead->next为NULL,直接处理头节点后返回;
-
- 定义指针p(指向phead->next)和临时指针temp;
-
- 循环遍历:temp保存当前节点p,p移动到p->next,释放temp,直至p为NULL;
-
- 释放头节点内存,将phead置为NULL(避免后续误操作访问野指针)。
完整代码实现:
c
// 销毁双向链表(参数:双向链表指针,释放所有节点内存)
void double_list_destroy(DoubleList *list) {
// 1. 判空:链表为空,无需销毁
if (list == NULL || list->head == NULL) {
printf("双向链表为空,无需销毁!\n");
return;
}
// 2. 定义指针p(指向第一个有效节点)和temp(保存当前节点)
Node *p = list->head->next;
Node *temp = NULL;
// 3. 循环遍历,释放所有有效节点
while (p != NULL) {
temp = p; // 保存当前节点
p = p->next; // 移动到下一个节点(释放前先保存下一个节点地址)
free(temp); // 释放当前节点
temp = NULL; // 置空,避免野指针
}
// 4. 释放头节点内存,将链表头指针置空
free(list->head);
list->head = NULL;
printf("双向链表销毁成功!\n");
}
常见错误:仅释放有效节点,忘记释放头节点;或释放节点前未保存下一个节点地址,导致遍历中断。
2. 总结
双向链表的核心是"双向指针联动",所有操作需同时维护prev和next的指向,避免断裂;带头节点结构可统一首尾操作逻辑,减少判空冗余;销毁和删除操作必须释放内存,杜绝内存泄漏。
二、循环链表
核心特性:与双向链表类似,通常带头节点,尾节点的next不指向NULL,而是指向头节点,形成闭环;遍历链表时,从任意节点出发均可遍历所有节点,首尾相连。
节点及链表结构定义:
c
#include <stdio.h>
#include <stdlib.h>
// 循环链表节点结构(单向循环,带头节点)
typedef struct Node {
int data; // 节点存储的数据
struct Node *next; // 后继指针,尾节点next指向头节点
} Node;
// 循环链表结构
typedef struct {
Node *head; // 头节点指针(哨兵节点)
} CircleList;
1. 核心操作及实现要点
(1)创建链表
操作逻辑:
-
- 动态分配头节点内存,判断malloc是否成功(避免内存分配失败导致野指针);
-
- 初始化头节点:prev(双向循环可加)置为NULL,data不赋值(无有效数据);
-
- 闭环初始化:头节点的next指向自身(phead->next = phead),此时为"空循环链表"(无有效节点)。
完整代码实现:
c
// 创建空循环链表(返回值:创建成功的链表指针,失败返回NULL)
CircleList *circle_list_create() {
// 1. 动态分配链表结构内存
CircleList *list = (CircleList *)malloc(sizeof(CircleList));
if (list == NULL) {
printf("链表结构内存分配失败,创建链表失败!\n");
return NULL;
}
// 2. 动态分配头节点内存,判断是否成功
list->head = (Node *)malloc(sizeof(Node));
if (list->head == NULL) {
printf("头节点内存分配失败,创建链表失败!\n");
free(list); // 释放已分配的链表结构内存
list = NULL;
return NULL;
}
// 3. 初始化头节点,闭环设置(头节点next指向自身)
list->head->data = 0; // 头节点无有效数据,可赋值为0占位
list->head->next = list->head; // 空循环链表,闭环初始化
printf("循环链表创建成功(空链表)!\n");
return list;
}
(2)头插(在第一个有效节点前插入新节点)
操作逻辑(核心:维持闭环):
-
- 判空:phead为NULL,返回失败;
-
- 动态分配新节点p_new,赋值data;
-
- 找到尾节点:遍历链表,找到p->next == phead的节点(尾节点);
-
- 关联新节点:p_new->next = phead->next(指向原第一个有效节点);phead->next = p_new;
-
- 维持闭环:尾节点的next指向p_new(确保尾节点始终指向头节点的下一个节点,形成闭环)。
完整代码实现:
c
// 循环链表头插(参数:链表指针、插入数据,返回值:1成功,0失败)
int circle_list_insert_head(CircleList *list, int data) {
// 1. 判空:链表或头节点为空
if (list == NULL || list->head == NULL) {
printf("循环链表为空,无法执行头插!\n");
return 0;
}
// 2. 动态分配新节点,赋值data
Node *p_new = (Node *)malloc(sizeof(Node));
if (p_new == NULL) {
printf("新节点内存分配失败,头插失败!\n");
return 0;
}
p_new->data = data;
// 3. 找到尾节点(p->next == 头节点,即为尾节点)
Node *p = list->head;
while (p->next != list->head) {
p = p->next;
}
// 4. 关联新节点,插入到头节点之后(第一个有效节点位置)
p_new->next = list->head->next;
list->head->next = p_new;
// 5. 维持闭环:尾节点next指向新节点
p->next = p_new;
printf("数据%d头插成功!\n", data);
return 1;
}
常见错误:忘记更新尾节点的next,导致闭环断裂;或头插后未更新phead->next,新节点未真正插入。
(3)打印链表
操作逻辑:
-
- 判空:phead->next == phead(无有效节点),提示空链表;
-
- 定义指针p从phead->next开始遍历,循环条件为p != phead(避免遍历死循环);
-
- 打印当前节点data,p移动到p->next,直至回到头节点。
完整代码实现:
c
// 打印循环链表(参数:循环链表指针)
void circle_list_print(CircleList *list) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head == NULL || list->head->next == list->head) {
printf("循环链表为空,无数据可打印!\n");
return;
}
// 2. 遍历链表,从第一个有效节点开始,终止条件:p == 头节点
Node *p = list->head->next;
printf("循环链表数据(闭环):");
while (p != list->head) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
关键:循环终止条件必须是p == phead,否则会陷入无限循环。
(4)查找指定值节点
操作逻辑:
-
- 判空:phead->next == phead,返回NULL(未找到);
-
- 定义指针p从phead->next开始遍历,循环条件p != phead;
-
- 若p->data == 指定值,返回当前节点指针;
-
- 遍历结束未找到,返回NULL。
完整代码实现:
c
// 查找指定值节点(参数:链表指针、查找值,返回值:找到返回节点指针,失败返回NULL)
Node *circle_list_find(CircleList *list, int val) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head == NULL || list->head->next == list->head) {
printf("循环链表为空,无法查找节点!\n");
return NULL;
}
// 2. 遍历链表,查找值为val的节点
Node *p = list->head->next;
while (p != list->head && p->data != val) {
p = p->next;
}
// 3. 判断是否找到(p == 头节点表示遍历结束,未找到)
if (p == list->head) {
printf("未找到值为%d的节点!\n", val);
return NULL;
}
printf("找到值为%d的节点!\n", val);
return p;
}
(5)头删除(删除第一个有效节点)
操作逻辑:
-
- 判空:phead->next == phead,返回失败;
-
- 定义temp指向待删除节点(phead->next),找到尾节点(p->next == phead);
-
- 头节点的next指向temp->next(跳过待删除节点);
-
- 维持闭环:尾节点的next指向phead->next;
-
- 释放temp节点内存。
完整代码实现:
c
// 循环链表头删除(参数:链表指针,返回值:1成功,0失败)
int circle_list_delete_head(CircleList *list) {
// 1. 判空:链表为空或无有效节点
if (list == NULL || list->head == NULL || list->head->next == list->head) {
printf("循环链表为空,无法执行头删除!\n");
return 0;
}
// 2. 定义temp指向待删除节点(第一个有效节点),找到尾节点
Node *temp = list->head->next;
Node *p = list->head;
while (p->next != list->head) {
p = p->next;
}
// 3. 头节点跳过待删除节点,指向其下一个节点
list->head->next = temp->next;
// 4. 维持闭环:尾节点next指向新的第一个有效节点
p->next = list->head->next;
// 5. 释放待删除节点内存
free(temp);
temp = NULL;
printf("循环链表头删除成功!\n");
return 1;
}
(6)销毁链表
操作逻辑:
-
- 判空:phead为NULL,直接返回;
-
- 定义指针p = phead->next,temp用于保存当前节点;
-
- 循环遍历:temp = p,p = p->next,释放temp,直至p == phead(遍历完所有有效节点);
-
- 释放头节点内存,将phead置为NULL,彻底销毁闭环。
完整代码实现:
c
// 销毁循环链表(参数:循环链表指针,释放所有节点内存)
void circle_list_destroy(CircleList *list) {
// 1. 判空:链表为空,无需销毁
if (list == NULL || list->head == NULL) {
printf("循环链表为空,无需销毁!\n");
return;
}
// 2. 定义指针p(指向第一个有效节点)和temp(保存当前节点)
Node *p = list->head->next;
Node *temp = NULL;
// 3. 循环遍历,释放所有有效节点(终止条件:p == 头节点)
while (p != list->head) {
temp = p; // 保存当前节点
p = p->next; // 移动到下一个节点
free(temp); // 释放当前节点
temp = NULL;
}
// 4. 释放头节点内存,将链表指针和头节点指针置空
free(list->head);
list->head = NULL;
free(list);
list = NULL;
printf("循环链表销毁成功!\n");
}
2. 总结
循环链表的核心是"闭环维护",所有首尾操作(头插、头删、销毁)都需确保尾节点的next始终指向头节点;遍历、查找操作的终止条件是"回到头节点",避免死循环;与双向链表的区别的是,循环链表侧重"闭环遍历",双向链表侧重"双向指针联动"。
三、栈
核心特性:先进后出(LIFO),仅允许在栈顶进行操作(入栈、出栈),栈底元素固定,最先入栈的元素最后出栈;本次学习"顺序栈"(数组实现),结构简单,操作高效。
顺序栈结构定义(所有操作通用):
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100 // 顺序栈的最大容量(可根据需求修改)
// 顺序栈结构定义(数组实现)
typedef struct {
int data[MAX_SIZE]; // 数组存储栈元素(int可替换为其他类型)
int top; // 栈顶指针,用数组下标表示,初始为-1(空栈)
} ArrayStack;
1. 核心概念
-
栈顶:栈中最后入栈、最先出栈的元素,用top指针(数组下标)标记;
-
栈底:栈中最先入栈、最后出栈的元素,数组实现中固定为下标0;
-
空栈:top == -1(无任何元素);
-
栈满:top == MAX_SIZE - 1(MAX_SIZE为栈的最大容量,数组固定);
-
顺序栈:用数组存储栈元素,top标记栈顶位置,操作均围绕top展开。
2. 核心操作及实现要点
(1)定义栈结构
代码模板:
c
#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE]; // 数组存储栈元素(int可替换为其他类型)
int top; // 栈顶指针,下标,初始为-1(空栈)
} ArrayStack; // 顺序栈结构名
(2)创建顺序栈(初始化)
操作逻辑:将栈顶top置为-1,初始化栈为空,无需分配额外内存(数组静态分配)。
关键:初始化必须置top = -1,否则会导致栈空判断失效,出现非法访问。
完整代码实现:
c
// 初始化顺序栈(参数:顺序栈指针,将栈置为空栈)
void array_stack_init(ArrayStack *stack) {
// 判空:栈指针为空,无法初始化
if (stack == NULL) {
printf("栈指针为空,无法初始化!\n");
return;
}
// 初始化栈顶top为-1,表示空栈
stack->top = -1;
printf("顺序栈初始化成功(空栈)!\n");
}
(3)打印栈
操作逻辑:
-
- 判空:top == -1,提示空栈;
-
- 从栈顶(top)向下遍历至栈底(0),依次打印元素;
-
- 打印顺序:栈顶→栈底(符合栈的查看习惯)。
完整代码实现:
c
// 打印顺序栈(参数:顺序栈指针,从栈顶到栈底打印)
void array_stack_print(ArrayStack *stack) {
// 1. 判空:栈指针为空或空栈
if (stack == NULL || stack->top == -1) {
printf("顺序栈为空,无数据可打印!\n");
return;
}
// 2. 从栈顶到栈底打印元素
printf("顺序栈数据(栈顶→栈底):");
for (int i = stack->top; i >= 0; i--) {
printf("%d ", stack->data[i]);
}
printf("\n");
}
(4)入栈(push)
操作逻辑(核心:栈顶上移,存入数据):
-
- 判错:栈指针为NULL,或栈满(top == MAX_SIZE - 1),返回失败;
-
- 栈顶上移:top++(先移动下标,再存数据,避免覆盖);
-
- 存入数据:stack->data[top] = 待入栈数据;
-
- 返回成功。
完整代码实现:
c
// 顺序栈入栈(参数:栈指针、入栈数据,返回值:1成功,0失败)
int array_stack_push(ArrayStack *stack, int data) {
// 1. 判错:栈指针为空或栈满
if (stack == NULL) {
printf("栈指针为空,无法入栈!\n");
return 0;
}
if (stack->top == MAX_SIZE - 1) {
printf("栈已满,无法入栈数据%d!\n", data);
return 0;
}
// 2. 栈顶上移,存入数据(先移下标,再存数据)
stack->top++;
stack->data[stack->top] = data;
printf("数据%d入栈成功!\n", data);
return 1;
}
常见错误:先存数据再top++,会导致top指向的栈顶元素与实际存储位置偏差1。
(5)出栈(pop)
操作逻辑(核心:读取栈顶,栈顶下移,移除元素):
-
- 判错:栈指针为NULL,或空栈(top == -1),返回失败;
-
- 读取栈顶数据:将stack->data[top]赋值给目标变量(使用数据);
-
- 栈顶下移:top--(移除栈顶元素,栈不再管理该元素,无需释放内存,数组静态分配);
-
- 返回成功。
完整代码实现:
c
// 顺序栈出栈(参数:栈指针、存储出栈数据的变量指针,返回值:1成功,0失败)
int array_stack_pop(ArrayStack *stack, int *data) {
// 1. 判错:栈指针为空、数据指针为空或空栈
if (stack == NULL || data == NULL) {
printf("参数非法,无法出栈!\n");
return 0;
}
if (stack->top == -1) {
printf("栈为空,无法出栈!\n");
return 0;
}
// 2. 读取栈顶数据,存入data指向的变量
*data = stack->data[stack->top];
// 3. 栈顶下移,移除栈顶元素(无需释放内存,数组静态分配)
stack->top--;
printf("数据%d出栈成功!\n", *data);
return 1;
}
关键理解:出栈不是"仅读取数据",而是"读取+移除",top--后,原栈顶元素不再被栈管理,后续入栈会覆盖该位置;若仅读取不移动top,属于"查看栈顶",不是出栈。
2. 总结
顺序栈的核心是"top指针的操作",所有操作(入栈、出栈、打印)都围绕top展开;入栈判满、出栈判空是必须的,避免数组越界和非法访问;顺序栈操作效率高(时间复杂度O(1)),但容量固定,适合数据量已知、操作简单的场景;核心原则:先进后出,仅栈顶可操作。
四、今日重点回顾与易错点
1. 共性易错点
-
内存泄漏:链表的删除、销毁操作,未释放节点内存;
-
野指针:指针未初始化、节点释放后未置空、访问NULL指针的成员;
-
逻辑断裂:双向链表未同步维护prev和next,循环链表未维持闭环;
-
死循环:循环链表遍历终止条件错误(未判断p == phead)。
2. 重点记忆
-
双向链表:双向指针联动,带头节点简化操作,删改必维护prev和next;
-
循环链表:闭环维护,首尾操作必更新尾节点next,遍历终止于头节点;
-
顺序栈:先进后出,top=-1为空栈,入栈top++存数据,出栈读数据top--,判满判空是前提。