前言:
栈,这个东西可以想一下我们现在有一个杯子,我现在将一个一个相同大小的石头放进杯子中进行叠加,一层一层又一层,现在我们要把石头一块一块的拿出来,是不是要从杯口一块一块的拿出来,好了,我们一个栈的入栈和出栈就这样实现完了。总的来说就是后入先出。Are you 明白?
1、顺序栈:
(1)顺序栈的定义
都叫顺序栈了,那就是顺序结构存储的呗,还能拿啥存?拿裤子口袋存吗?
#define MAXSIZE 100 // 定义栈的最大容量
// 定义顺序栈结构体
typedef struct {
int data[MAXSIZE]; // 栈中元素存储的数组
int top; // 栈顶指针
} SeqStack;
栈的结构体就是一个元素数组和指针,没啥新鲜东西,记住就行。如果想要复杂点,还可以加一个栈底指针和栈容量,都是自己定义的。
(2)顺序栈的初始化
// 顺序栈的初始化
void InitStack(SeqStack *s) {
s->top = -1; // 初始化栈顶指针为-1,表示栈为空
}
这里有两种处理方式,可以栈顶指针设为-1,也可以设为0。为什么呢,因为我设为-1,意味着我插入数据进去,我的栈顶指针永远是指向我最后一个数据。如果我设为0呢?那就是说,我的栈顶指针永远指向我最后一个数据的后一个位置。
(3)顺序栈的入栈
// 顺序栈的入栈操作
bool Push(SeqStack *s, int x) {
if (s->top == MAXSIZE - 1) { // 栈满,无法入栈
return false;
}
s->data[++s->top] = x; // 先移动栈顶指针,再存入元素
return true;
}
当然先要判断是不是栈满了,栈满了肯定就不能插入数据了,杯子里的石头满了,还能装吗?(有人会说,石头满了,不是还能装水吗,我会说,你爬开,颠公都没你颠)
然后就是s->data[++s->top] = x;这句话是不是有点难理解,那就拆分一下。变成:int a = ++s->top;s->data[a] = x;看懂了没,没看懂就多看两遍。还不懂我就跟你讲一下,你看哈,这个s对象的top索引是不是我们的栈顶指针,这个栈顶指针相当于我们的索引,而不是真的指针地址,我们对他进行加加,然后用一个变量接受,然后我们再把我们要入栈的值放进我们s这个对象的data数组里,那么我们怎么知道这个数组哪个位置放哪个数呢?这个时候看到我们刚刚接收到的索引没?对,就是他,他就是给咱们的数组指明方向的那个光。你说巧妙不巧妙,嘿嘿嘿。
(4)顺序栈的出栈
// 顺序栈的出栈操作
bool Pop(SeqStack *s, int *x) {
if (s->top == -1) { // 栈空,无法出栈
return false;
}
*x = s->data[s->top--]; // 先取出栈顶元素,再移动栈顶指针
return true;
}
有了上面的插入操作,那看删除是不是感觉格外清晰,是这样的,学着学着就变成了,看什么都格外清晰,再学着学着,就是看什么代码都风韵犹存了,嘿嘿。
(5)顺序栈取栈顶元素
// 顺序栈取栈顶元素操作
bool GetTop(SeqStack *s, int *x) {
if (s->top == -1) { // 栈空,无法取栈顶元素
return false;
}
*x = s->data[s->top]; // 直接取出栈顶元素
return true;
}
注意这里传入的x是指针,为什么传指针,因为我们这个是函数,最后传回主函数里需要传址,当然你也可以做成一个传值的,没毛病老铁。
(6)顺序栈判断栈空
// 顺序栈判断栈空操作
bool StackEmpty(SeqStack *s) {
return s->top == -1; // 栈顶指针为-1,表示栈为空
}
2、链栈:
(1)链栈的定义
// 定义链栈的节点结构体
typedef struct StackNode {
int data; // 节点数据域
struct StackNode *next; // 节点指针域,指向下一个节点
} StackNode, *LinkStack;
觉不觉得链栈定义特别像什么东西?看不出来,请回去看第二章。
数据域,指针域,这部妥妥单链表的定义吗?你怕不是拿个单链表的定义来哄我哟。
是这样的,链栈就是单链表,但是又是特殊的单链表,为啥这样说,请看下文。
(2)链栈的初始化
// 链栈的初始化
LinkStack InitStack() {
LinkStack top = (LinkStack)malloc(sizeof(StackNode));
if (!top) {
exit(EXIT_FAILURE); // 内存分配失败,退出程序
}
top->next = NULL; // 初始化栈顶指针为NULL,表示空栈
return top;
}
给top节点分配内存,然后指针指向空,是不是特别像做出一个头节点?就是这么的神奇。
(3)链栈的入栈
// 链栈的入栈操作
bool Push(LinkStack top, int x) {
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (!newNode) {
return false; // 内存分配失败
}
newNode->data = x;
newNode->next = top->next; // 新节点插入到栈顶
top->next = newNode;
return true;
}
发现没,这个入栈操作,跟单链表的什么操作很像?是不是头插法?明明就一模一样,所以实在记不住,就记住,入栈等于头插法,top栈顶指针永远指向头节点。
(4)链栈的出栈
// 链栈的出栈操作
bool Pop(LinkStack top, int *x) {
if (top->next == NULL) {
return false; // 栈空,无法出栈
}
StackNode *temp = top->next;
*x = temp->data;
top->next = temp->next; // 修改栈顶指针,指向下一个节点
free(temp); // 释放原栈顶节点的内存
return true;
}
那么问题来了,不是top指针不是一直指向头节点么,那我要删除头节点后面的第一个节点岂不是很容易?我要先取出来需要删除的值,再删,后面不要忘记释放这个节点的空间哦。
(5)链栈取栈顶元素
// 链栈取栈顶元素操作
bool GetTop(LinkStack top, int *x) {
if (top->next == NULL) {
return false; // 栈空,无法取栈顶元素
}
*x = top->next->data; // 取栈顶节点的数据
return true;
}
都能删除了,取值肯定不用多说了。
(6)链栈判断栈空
// 链栈判断栈空操作
bool StackEmpty(LinkStack top) {
return top->next == NULL; // 栈顶指针的next为NULL,表示栈空
}
初始化怎么样,栈空就是怎么样的。
这里提一句,如果链栈看不懂,那就回去再看一遍单链表,核心是换汤不换药的。
3、栈与递归:
(1)递归的实质
int factorial(int n) {
if (n == 0 || n == 1) { // 基准情形
return 1;
} else { // 递归情形
return n * factorial(n - 1);
}
}
递归,说白了就是一个函数里,自己调用自己的函数,只要满足条件,就一层一层返回。所以,递归函数里一定要有终止条件和递归的部分,啥叫终止条件,就是上面这个函数里if (n == 0 || n == 1) { return 1; }的部分,啥叫递归部分,就是else { return n * factorial(n - 1); } 的部分,啥叫自己调用自己,就是递归部分调用的函数就是你原本定义的函数自身
(2)利用栈将递归转化为非递归
上面用了递归,接下来用一个栈的知识解决上述递归的代码。
int factorialIterative(int n) {
Stack stack = createStack();
int result = 1;
int temp;
// 将n压入栈中,模拟递归调用
push(&stack, n);
while (!isEmpty(stack)) {
// 弹出栈顶元素
temp = pop(&stack);
if (temp == 0 || temp == 1) {
// 基准情形,直接累乘到结果中
result *= temp;
} else {
// 递归情形,将temp-1压入栈中,等待后续处理
push(&stack, temp - 1);
// 当前temp乘以result,并保留到下一次循环
result *= temp;
}
}
return result;
}
其实和迭代法很像,只不过用到了栈结构。
(3)转化前的递归代码
#include <stdio.h>
// 递归实现的阶乘函数
int factorial(int n) {
if (n == 0 || n == 1) { // 基准情形
return 1;
} else { // 递归情形
return n * factorial(n - 1);
}
}
int main() {
int n;
printf("Enter a positive integer to calculate its factorial: ");
scanf("%d", &n);
if (n < 0) {
printf("Factorial is not defined for negative numbers.\n");
} else {
printf("Factorial of %d is %d\n", n, factorial(n));
}
return 0;
}
代码在这里,自己比较咯。
4、完整代码,复制粘贴自取:
(1)顺序栈
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXSIZE 100 // 定义栈的最大容量
// 定义顺序栈结构体
typedef struct {
int data[MAXSIZE]; // 栈中元素存储的数组
int top; // 栈顶指针
} SeqStack;
// 顺序栈的初始化
void InitStack(SeqStack *s) {
s->top = -1; // 初始化栈顶指针为-1,表示栈为空
}
// 顺序栈的入栈操作
bool Push(SeqStack *s, int x) {
if (s->top == MAXSIZE - 1) { // 栈满,无法入栈
return false;
}
s->data[++s->top] = x; // 先移动栈顶指针,再存入元素
return true;
}
// 顺序栈的出栈操作
bool Pop(SeqStack *s, int *x) {
if (s->top == -1) { // 栈空,无法出栈
return false;
}
*x = s->data[s->top--]; // 先取出栈顶元素,再移动栈顶指针
return true;
}
// 顺序栈取栈顶元素操作
bool GetTop(SeqStack *s, int *x) {
if (s->top == -1) { // 栈空,无法取栈顶元素
return false;
}
*x = s->data[s->top]; // 直接取出栈顶元素
return true;
}
// 顺序栈判断栈空操作
bool StackEmpty(SeqStack *s) {
return s->top == -1; // 栈顶指针为-1,表示栈为空
}
int main() {
SeqStack s;
int x;
// 初始化栈
InitStack(&s);
// 入栈操作
Push(&s, 1);
Push(&s, 2);
Push(&s, 3);
// 取栈顶元素
if (GetTop(&s, &x)) {
printf("栈顶元素为:%d\n", x);
} else {
printf("栈为空,无法取栈顶元素\n");
}
// 出栈操作
if (Pop(&s, &x)) {
printf("出栈元素为:%d\n", x);
} else {
printf("栈为空,无法出栈\n");
}
// 判断栈是否为空
if (StackEmpty(&s)) {
printf("栈为空\n");
} else {
printf("栈不为空\n");
}
return 0;
}
(2)链栈
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义链栈的节点结构体
typedef struct StackNode {
int data; // 节点数据域
struct StackNode *next; // 节点指针域,指向下一个节点
} StackNode, *LinkStack;
// 链栈的初始化
LinkStack InitStack() {
LinkStack top = (LinkStack)malloc(sizeof(StackNode));
if (!top) {
exit(EXIT_FAILURE); // 内存分配失败,退出程序
}
top->next = NULL; // 初始化栈顶指针为NULL,表示空栈
return top;
}
// 链栈的入栈操作
bool Push(LinkStack top, int x) {
StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
if (!newNode) {
return false; // 内存分配失败
}
newNode->data = x;
newNode->next = top->next; // 新节点插入到栈顶
top->next = newNode;
return true;
}
// 链栈的出栈操作
bool Pop(LinkStack top, int *x) {
if (top->next == NULL) {
return false; // 栈空,无法出栈
}
StackNode *temp = top->next;
*x = temp->data;
top->next = temp->next; // 修改栈顶指针,指向下一个节点
free(temp); // 释放原栈顶节点的内存
return true;
}
// 链栈取栈顶元素操作
bool GetTop(LinkStack top, int *x) {
if (top->next == NULL) {
return false; // 栈空,无法取栈顶元素
}
*x = top->next->data; // 取栈顶节点的数据
return true;
}
// 链栈判断栈空操作
bool StackEmpty(LinkStack top) {
return top->next == NULL; // 栈顶指针的next为NULL,表示栈空
}
int main() {
LinkStack s = InitStack(); // 初始化链栈
// 入栈操作
Push(s, 1);
Push(s, 2);
Push(s, 3);
// 取栈顶元素
int topElement;
if (GetTop(s, &topElement)) {
printf("栈顶元素为:%d\n", topElement);
} else {
printf("栈为空,无法取栈顶元素\n");
}
// 出栈操作
int poppedElement;
if (Pop(s, &poppedElement)) {
printf("出栈元素为:%d\n", poppedElement);
} else {
printf("栈为空,无法出栈\n");
}
// 判断栈是否为空
if (StackEmpty(s)) {
printf("栈为空\n");
} else {
printf("栈不为空\n");
}
// 释放链栈内存(此处仅释放栈顶节点,实际应遍历整个链表释放所有节点)
free(s);
return 0;
}
今天文章就写到这吧,我知道这些知识点很简单,有些人看到会说,这个人脑子有问题吧?那么简单的东西都拿来讲,其实我想说,我只希望我自己学到的东西能帮助到一些还在盲目穿梭于各种资料又渴望学习的人,不是每个人都能打破信息差,在一个信息冗余的时代,我的文章也是汪洋大海里的尘埃,但是如果真的有人有幸看到这篇文章得到帮助呢?我也想拯救前几年那个迷茫的自己,那个时候什么都不会,想学习又找不到门路,一打开网页就是各种各种知识点,我只能说我不是天才,甚至人才都不是,我只希望能帮助到各位,仅此而已。另外,本篇的代码,我都是用gpt跑出来的,允许我自己偷一点小小的懒,嘿嘿嘿。如果有幸你能看到这篇文章,那么谢谢你,你的支持就是我最大的动力,不支持也没关系,能点开看,已经是我最大的幸运了。文笔很糙,但是真情实意,谢谢各位。