大家好,我是小卡皮巴拉
文章目录
目录
每篇前言
博客主页:小卡皮巴拉
咱的口号:🌹小比特,大梦想🌹
作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。
引言
在数据结构的大家庭中,栈以其独特的“后进先出”特性脱颖而出,就像是一个记录时间流逝的“时间胶囊”。每当有新数据加入时,它就被放置在栈顶,而最早加入的数据则静静地躺在栈底,等待被最后一个取出。
栈不仅是一个简单的数据结构,更是算法设计和程序优化中的得力助手。从函数调用栈的管理到递归问题的求解,栈的身影无处不在。它以其简洁明了的数据操作规则和高效的内存利用方式,成为了程序员们解决复杂问题时的首选工具。
在接下来的篇章中,我们将一起探索栈的奥秘,从定义到特性,再到丰富的应用场景,帮助大家真正掌握这一数据结构的核心知识。
一.栈的基本概念
1.1 定义
栈(Stack)是一种只允许在一端进行插入或删除的线性表。这端被称为栈顶(Top),另一端则被称为栈底(Bottom)。栈遵循后进先出(Last In First Out,LIFO)的原则,即最后插入的元素会第一个被删除。
1.2 特性
-
后进先出:栈中最后一个插入的元素首先被删除。这是栈最显著的特点,也是它与其他线性表(如数组、链表)的主要区别。
-
栈顶浮动,栈底固定:栈顶的位置随着元素的入栈和出栈而变化,而栈底则保持不变。
-
不支持随机访问:栈的结构决定了只能在栈顶进行插入和删除操作,无法直接访问和修改栈中间的元素。
1.3 基本操作
栈的基本操作主要包括以下几种:
-
初始化栈:创建一个空栈,并设置栈底指针和栈顶指针。
-
判断栈是否为空:检查栈中是否包含元素,若栈为空则返回true,否则返回false。
-
入栈操作(Push):将新元素添加到栈顶。若栈未满,则将新元素放入栈中,并更新栈顶指针。
-
出栈操作(Pop):从栈顶移除元素。若栈非空,则弹出栈顶元素,并更新栈顶指针。
-
读取栈顶元素(GetTop/Peek):返回栈顶元素的值,但不删除它。若栈非空,则返回栈顶元素的值。
-
销毁栈:释放栈占用的存储空间。
二.栈的实现方式
栈的实现方式主要有两种:
2.1 顺序栈
利用数组实现,附设一个指针(top)指示当前栈顶元素的位置。顺序栈需要预分配一块连续的存储空间,其优点是访问速度快,但缺点是存在栈满上溢的风险。
2.2 链栈
采用链表实现,通常使用单链表,并规定所有操作都在链表的表头进行。链栈的优点是便于多个栈共享存储空间、不存在栈满上溢的情况,且插入和删除操作的时间复杂度为O(1)。但缺点是需要额外的指针存储空间。
三.顺序栈的实现
定义顺序栈的结构
首先,我们定义了一个名为STDataType的类型别名,它基于int类型。这个类型别名将用于栈中存储的数据元素。
接着,我们定义了一个名为Stack的结构体(通过typedef,我们也可以用ST来引用它)。这个结构体代表了一个栈的数据结构,并包含以下三个成员:
arr:这是一个指向STDataType类型的指针。它用于动态分配数组,以存储栈中的元素。
top:这是一个整型变量,表示栈顶的位置。在栈中,我们通常将数组的最后一个有效元素的位置视为栈顶。注意,这里的top成员变量实际上存储的是栈顶元素的索引(即数组中的位置),而不是指向栈顶元素的指针。因此,栈为空时,top的值通常被初始化为-1或栈的容量减1(取决于栈的实现方式)。
- capacity:这是一个整型变量,表示栈的容量,即栈能够存储的最大元素数量。这个值在栈的初始化时被设定,并且决定了为arr成员动态分配多少内存。
函数代码:
typedef int STDataType; //定义栈的数据结构 typedef struct Stack { STDataType* arr; int top;//指向栈顶位置 int capacity; }ST;
初始化
STInit
函数用于初始化一个栈。它接收一个指向栈的指针ps
作为参数。
检查指针:首先,使用
assert
确保传入的栈指针ps
不是NULL
。设置初始状态:
- 将栈的数组指针
ps->arr
设置为NULL
,表示栈尚未分配存储空间。- 将栈顶位置
ps->top
设置为0,表示栈为空。- 将栈容量
ps->capacity
也设置为0,表示栈当前没有预设容量。这样,栈就被初始化为一个空栈,准备进行后续的栈操作。
函数代码:
//初始化 void STInit(ST* ps) { assert(ps); ps->arr = NULL; ps->top = ps->capacity = 0; }
入栈
StackPush
函数用于将一个元素压入栈顶。它接收两个参数:一个指向栈的指针ps
和要压入栈的元素x
。
检查指针:首先,使用
assert
确保传入的栈指针ps
不是NULL
。检查容量:接着,检查栈是否已满(即栈顶位置
ps->top
是否等于栈容量ps->capacity
)。
- 如果栈已满,则进行扩容操作。首先计算新的容量
newCapacity
,如果当前容量为0,则新容量为4(初始容量),否则新容量为当前容量的两倍。- 使用
realloc
函数尝试为栈的数组ps->arr
分配新的内存空间。如果realloc
失败(返回NULL
),则打印错误信息并退出程序。- 如果
realloc
成功,则更新栈的数组指针ps->arr
和栈容量ps->capacity
。入栈操作:在确认栈有足够的空间后,将元素
x
放入栈顶位置(即ps->arr[ps->top]
),并将栈顶位置ps->top
加1,以指向下一个空位置。这样,元素
x
就被成功压入栈顶,栈的栈顶位置和容量(如果需要的话)也得到了更新。函数代码:
void StackPush(ST* ps, STDataType x) { assert(ps); // 确保栈指针有效 // 检查栈是否已满 if (ps->top == ps->capacity) { // 扩容操作 int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail!\n"); exit(1); // 扩容失败,退出程序 } ps->arr = tmp; // 更新栈的数组指针 ps->capacity = newCapacity; // 更新栈的容量 } // 入栈操作 ps->arr[ps->top++] = x; // 将元素放入栈顶,并更新栈顶位置 }
检查栈是否为空
StackEmpty
函数用于判断一个栈是否为空。它接收一个指向栈的指针ps
作为参数,并返回一个布尔值。
检查指针:首先,使用
assert
确保传入的栈指针ps
不是NULL
。判断栈是否为空:然后,通过比较栈顶位置
ps->top
是否等于0来判断栈是否为空。如果ps->top
等于0,表示栈中没有元素,栈为空;否则,栈不为空。返回结果:最后,根据判断结果返回相应的布尔值。如果栈为空,则返回
true
(或等价地,返回非零值,但在C语言中通常使用true
或1
来表示真);如果栈不为空,则返回false
(或等价地,返回零值)。函数代码:
#include <stdbool.h> // 包含头文件以支持布尔值 bool StackEmpty(ST* ps) { assert(ps); // 确保栈指针有效 return ps->top == 0; // 判断栈是否为空,并返回结果 }
出栈
StackPop
函数用于从栈中移除栈顶元素。它接收一个指向栈的指针ps
作为参数,并不返回任何值(即返回类型为void
)。
检查栈是否为空:首先,使用
assert
宏和StackEmpty
函数来确保栈不为空。如果栈为空,则StackEmpty(ps)
将返回true
(或非零值),导致assert
失败,并可能终止程序(这取决于assert
宏的具体实现和编译器的设置)。这是一种调试时的安全措施,用于防止对空栈进行出栈操作。出栈操作:在确认栈不为空后,通过将栈顶位置
ps->top
减1来实现出栈操作。这实际上是将栈顶指针向下移动一位,从而“移除”了栈顶元素(但请注意,元素的值仍然保留在内存中,只是不再被视为栈的一部分)。函数代码:
#include <assert.h> // 包含头文件以支持assert宏 #include "stack.h" // 假设StackEmpty函数和ST结构体定义在这个头文件中 void StackPop(ST* ps) { assert(!StackEmpty(ps)); // 确保栈不为空 --ps->top; // 将栈顶位置减1,实现出栈操作 }
取栈顶元素
StackTop
函数用于获取栈顶元素的值,但不移除该元素。它接收一个指向栈的指针ps
作为参数,并返回栈顶元素的值。
检查栈是否为空:首先,使用
assert
宏和StackEmpty
函数来确保栈不为空。如果栈为空,则StackEmpty(ps)
将返回true
(或非零值),导致assert
失败,可能会终止程序(这取决于assert
宏的具体实现和编译器的设置)。这是一种调试时的安全措施,用于防止对空栈进行取栈顶元素操作。获取栈顶元素:在确认栈不为空后,通过访问栈数组
ps->arr
中栈顶位置ps->top
的前一个元素(即ps->arr[ps->top - 1]
)来获取栈顶元素的值。由于栈顶位置ps->top
是指向栈中下一个空位置的索引,因此栈顶元素实际上是位于ps->top - 1
的索引处。返回栈顶元素的值:最后,函数返回栈顶元素的值。
函数代码:
#include <assert.h> // 包含头文件以支持assert宏 #include "stack.h" // 假设StackEmpty函数、ST结构体和STDataType类型定义在这个头文件中 STDataType StackTop(ST* ps) { assert(!StackEmpty(ps)); // 确保栈不为空 return ps->arr[ps->top - 1]; // 返回栈顶元素的值 }
销毁
STDestroy
函数用于释放栈所占用的内存资源,并将栈结构体成员重置为初始状态。它接收一个指向栈的指针ps
作为参数,并不返回任何值(即返回类型为void
)。
释放栈数组内存:首先,函数检查栈数组指针
ps->arr
是否不为NULL
。如果不为NULL
,说明栈数组已经分配了内存,因此使用free
函数释放这块内存。这是为了防止内存泄漏,即程序不再需要的内存没有被释放回系统。重置栈结构体成员:在释放栈数组内存后,函数将栈结构体成员
ps->arr
重置为NULL
,表示栈数组指针不再指向任何有效的内存区域。同时,将栈的容量ps->capacity
和栈顶位置ps->top
都重置为0,表示栈现在是空的,且没有分配任何内存。函数代码:
#include <stdlib.h> // 包含头文件以支持free函数 #include "stack.h" // 假设ST结构体定义在这个头文件中 void STDestroy(ST* ps) { if (ps->arr != NULL) free(ps->arr); // 释放栈数组内存 ps->arr = NULL; // 重置栈数组指针为NULL ps->capacity = 0; // 重置栈容量为0 ps->top = 0; // 重置栈顶位置为0 }
四.链栈的实现
定义链栈的结构
我们定义了两个主要结构来支持链栈的实现:
链栈节点结构 (
StackNode
):
STDataType data;
:存储节点中的数据,STDataType
在此例中为int
类型。struct StackNode* next;
:指向链栈中下一个节点的指针。若当前节点为链栈末尾,则此指针为NULL
。链栈结构 (
LinkedStack
):
StackNode* top;
:栈顶指针,指向链栈的顶部节点(即最后压入的节点)。若链栈为空,则此指针为NULL
。// int capacity;
:对于链栈,由于其容量是动态的(随着节点的添加和删除而变化),因此这个容量字段在此场景中通常不需要。它已被注释掉,表明在链栈的实现中不考虑此字段。链栈(或链式栈)是基于链表实现的栈数据结构,它不需要预先分配固定大小的内存空间,能够动态地增长和缩小。数据压入(push)时,新节点被添加到栈顶;数据弹出(pop)时,栈顶节点被移除。
函数代码:
// 定义栈的数据类型 typedef int STDataType; // 定义链栈的节点结构 typedef struct StackNode { STDataType data; struct StackNode* next; } StackNode; // 定义链栈的结构,包含栈顶指针和栈的容量(对于链栈,容量通常不是必需的,但这里为了完整性保留) typedef struct LinkedStack { StackNode* top; // int capacity; // 对于链栈,容量是动态的,因此不需要这个字段 } LinkedStack;
初始化
函数名:
initStack
目的:初始化链栈,确保栈顶指针指向
NULL
,从而表明链栈当前为空。参数:
LinkedStack* stack
,这是一个指向链栈结构体的指针,用于访问和修改链栈的状态。函数体详细解析:
stack->top = NULL;
- 这行代码执行了链栈初始化的核心操作。
stack->top
是对链栈结构体中栈顶指针成员的访问。- 将
stack->top
设置为NULL
表示链栈当前不包含任何节点,即栈为空。函数代码:
// 初始化链栈 void initStack(LinkedStack* stack) { stack->top = NULL; // 栈顶指针初始化为NULL,表示空栈 }
入栈
函数名:
push
目的:将新数据压入链栈中,使其成为栈顶元素。
参数:
LinkedStack* stack
:指向链栈结构体的指针,用于访问和修改链栈的状态。STDataType data
:要压入链栈的数据。函数体详细解析:
- 创建新节点:
StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
- 这行代码通过
malloc
函数动态分配内存,用于存储新节点。sizeof(StackNode)
计算了StackNode
结构体所需的内存大小。(StackNode*)
是类型转换,将void*
类型的malloc
返回值转换为StackNode*
类型。- 内存分配失败处理:
- 检查
newNode
是否为NULL
,以判断内存分配是否成功。- 若内存分配失败,则打印错误信息并退出程序。
- 设置新节点的数据:
newNode->data = data;
- 将函数参数
data
的值赋给新节点的data
成员。- 链接新节点到链栈:
newNode->next = stack->top;
- 将新节点的
next
指针指向当前的栈顶节点。- 这一步实现了链栈的“后进先出”(LIFO)特性,因为新节点被放在了链栈的顶部。
- 更新栈顶指针:
stack->top = newNode;
- 将栈顶指针指向新节点,完成压栈操作。
函数代码:
// 压栈操作 void push(LinkedStack* stack, STDataType data) { StackNode* newNode = (StackNode*)malloc(sizeof(StackNode)); // 创建新节点 if (!newNode) { printf("Memory allocation failed\n"); exit(EXIT_FAILURE); } newNode->data = data; // 设置新节点的数据 newNode->next = stack->top; // 新节点的next指向当前的栈顶节点 stack->top = newNode; // 栈顶指针指向新节点 }
检查栈是否为空
函数名:
isEmpty
目的:判断链栈是否为空。
参数:
LinkedStack* stack
,指向链栈结构体的指针,用于访问链栈的栈顶指针。函数体详细解析:
- 判断链栈是否为空:
return stack->top == NULL;
- 这行代码通过比较栈顶指针
stack->top
是否为NULL
来判断链栈是否为空。- 若
stack->top
为NULL
,则链栈为空,函数返回1
- 若
stack->top
不为NULL
,则链栈不为空,函数返回0
函数代码:
// 判断链栈是否为空 int isEmpty(LinkedStack* stack) { return stack->top == NULL; }
出栈
函数名称:
pop
函数目的:从链栈中移除栈顶元素,并返回该元素的数据。
函数参数:
LinkedStack* stack
:指向链栈结构体的指针,包含了链栈的栈顶指针和可能的其他管理信息。函数返回值:
STDataType
:被移除的栈顶元素的数据。函数体解析:
- 检查链栈是否为空:
if (isEmpty(stack)) { ... }
- 调用
isEmpty
函数检查链栈是否为空。- 如果链栈为空,则打印错误信息“Stack underflow”,表示尝试从空栈中出栈。
- 通过
exit(EXIT_FAILURE)
终止程序。这是一种错误处理方式,也可以选择返回一个特殊值来表示错误,但这里选择直接退出程序。- 移除栈顶元素:
StackNode* tempNode = stack->top;
- 创建一个临时指针
tempNode
,并将其指向当前的栈顶节点。STDataType poppedData = tempNode->data;
- 从栈顶节点中获取数据,并将其存储在局部变量
poppedData
中。stack->top = tempNode->next;
- 将栈顶指针更新为指向下一个节点,这样原来的栈顶节点就被从链栈中移除了。
- 释放内存:
free(tempNode);
- 释放之前被移除的栈顶节点的内存。这是内存管理的重要步骤,防止内存泄漏。
- 返回数据:
return poppedData;
- 返回被移除的栈顶元素的数据。
函数代码:
// 出栈操作 STDataType pop(LinkedStack* stack) { if (isEmpty(stack)) { printf("Stack underflow\n"); exit(EXIT_FAILURE); // 或者返回一个特殊值表示错误,这里选择退出程序 } StackNode* tempNode = stack->top; // 临时节点指向栈顶节点 STDataType poppedData = tempNode->data; // 获取栈顶节点的数据 stack->top = tempNode->next; // 栈顶指针指向下一个节点 free(tempNode); // 释放栈顶节点的内存 return poppedData; // 返回出栈的数据 }
销毁
函数名称:
destroyStack
函数目的:释放链栈中所有节点的内存,从而销毁链栈。
函数参数:
LinkedStack* stack
:指向链栈结构体的指针,包含了链栈的栈顶指针和可能的其他管理信息。函数解析:
destroyStack
函数通过循环调用pop
函数来逐个移除链栈中的节点,并在每次移除节点时释放其内存。这个过程会一直持续到链栈为空。
- 循环检查链栈是否为空:
- 使用
while (!isEmpty(stack))
循环,只要链栈不为空,就继续执行循环体内的操作。- 出栈操作:
- 在循环体内,调用
pop(stack)
函数。pop
函数会移除链栈的栈顶节点,并释放其内存。同时,pop
函数还会返回被移除节点的数据,但在destroyStack
函数中,我们并不关心这个返回值。- 无需额外销毁操作:
- 由于
pop
函数已经负责释放了所有节点的内存,因此destroyStack
函数本身不需要进行额外的销毁操作。但是,为了保持接口的一致性(例如,与其他数据结构的销毁函数保持类似的签名),destroyStack
函数还是被保留了下来。- 函数结束:
- 当链栈为空时,
isEmpty(stack)
将返回true
,while
循环将终止,destroyStack
函数也随之结束。此时,链栈中的所有节点都已经被释放,链栈已被销毁。函数代码:
// 销毁链栈(释放所有节点的内存) void destroyStack(LinkedStack* stack) { while (!isEmpty(stack)) { pop(stack); // 通过出栈操作释放所有节点的内存 } // 对于链栈,由于我们已经在出栈时释放了所有节点的内存,因此不需要额外的销毁操作 // 但为了保持接口的一致性,这里还是保留了这个函数 }
兄弟们共勉!!!
码字不易,求个三连
抱拳了兄弟们!