大家好,我是小卡皮巴拉
文章目录
目录
引言
一.栈的基本概念
1.1 定义
1.2 特性
1.3 基本操作
二.栈的实现方式
2.1 顺序栈
2.2 链栈
三.顺序栈的实现
定义顺序栈的结构
初始化
入栈
检查栈是否为空
出栈
销毁
四.链栈的实现
定义链栈的结构
初始化
入栈
检查栈是否为空
出栈
销毁
兄弟们共勉!!!
每篇前言
博客主页:小卡皮巴拉
咱的口号:🌹小比特,大梦想🌹
作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。
引言
在数据结构的大家庭中,栈以其独特的“后进先出”特性脱颖而出,就像是一个记录时间流逝的“时间胶囊”。每当有新数据加入时,它就被放置在栈顶,而最早加入的数据则静静地躺在栈底,等待被最后一个取出。
栈不仅是一个简单的数据结构,更是算法设计和程序优化中的得力助手。从函数调用栈的管理到递归问题的求解,栈的身影无处不在。它以其简洁明了的数据操作规则和高效的内存利用方式,成为了程序员们解决复杂问题时的首选工具。
在接下来的篇章中,我们将一起探索栈的奥秘,从定义到特性,再到丰富的应用场景,帮助大家真正掌握这一数据结构的核心知识。
一.栈的基本概念
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); // 通过出栈操作释放所有节点的内存 } // 对于链栈,由于我们已经在出栈时释放了所有节点的内存,因此不需要额外的销毁操作 // 但为了保持接口的一致性,这里还是保留了这个函数 }
兄弟们共勉!!!
码字不易,求个三连
抱拳了兄弟们!