栈与队列核心篇(上):从原理到代码,吃透栈结构

栈与队列核心篇(上):从原理到代码,吃透栈结构

前言

嗨,我们又见面了(✪ω✪),相信大家已经学完顺序表和链表的相关知识,接下来我们又要迎接新的朋友——栈和队列,它和顺序表、链表、数组一样都属于线性表结构。今天我们主要介绍栈,让我们去了解它吧!

一、栈的概念栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。相关操作压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。出栈:栈的删除操作叫做出栈。出数据也在栈顶。

以下是相关操作图片展示,方便大家理解。

栈操作过程 简单类比:栈就像叠盘子,先放的在最底下,最后放的在最上面,拿的时候也得从最上面先拿,完美契合“后进先出”。

二、 栈的实现方式栈的实现一般可以使用数组或者链表实现

栈的逻辑结构为线性的,而栈的物理结构由二选择的方式所决定

对比项

数组实现的栈(顺序栈)

链表实现的栈(链栈)

存储方式

用连续的数组空间存储元素,通过栈顶指针标记栈顶位置

用链表节点(每个节点含数据域和指针域)存储元素,栈顶对应链表头节点

空间效率

若数组预分配空间过大,会造成空间浪费;若空间不足,会触发“栈溢出”

空间动态分配,无预分配浪费,但每个节点有指针域开销

时间效率

入栈(Push)、出栈(Pop)、查看栈顶(Peek)均为O(1);支持数组的随机访问(但栈逻辑不常用)

入栈、出栈、查看栈顶均为O(1);不支持随机访问

扩容/溢出

数组容量固定,满栈后无法再入栈(栈溢出),需手动扩容(如重新申请更大数组并拷贝元素)

无栈溢出风险,元素数量可动态增长,只需不断创建新节点即可

适用场景

适合元素数量已知或范围可控、追求存取效率的场景(如括号匹配、表达式求值)

适合元素数量不确定、需要灵活扩容的场景(如函数调用栈的动态管理)

实现复杂度

实现较简单,只需维护数组和栈顶指针

需处理链表节点的指针操作,实现稍复杂(但逻辑清晰)

通过这个表格相信大家一定有自己的判断了,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。所以下面我们通过数组来实现栈。

三、 栈的实现 下面通过Stack.h通过声明,Stack.c来实现功能,test.c来进行测试

开始前的操作(头文件)栈的实现主要通过初始化、销毁、入栈、出栈、取栈顶数据、判空、获取数据个数等方式来实现,这里先通过写头文件来先确定实现框架,后面就不进行重复展示了

头文件Stack.h

代码语言:javascript复制#pragma once

#include

#include

#include

#include

typedef int STDataType;

typedef struct Stack

{

STDataType* a;

int top;//栈顶

int capacity;//栈的空间大小

}ST;

// 初始化和销毁

void STInit(ST* pst);

void STDestroy(ST* pst);

// 入栈 出栈

void STPush(ST* pst, STDataType x);

void STPop(ST* pst);

// 取栈顶数据

STDataType STTop(ST* pst);

// 判空

bool STEmpty(ST* pst);

// 获取数据个数

int STSize(ST* pst);1、初始化Stack.c

代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1

#include"Stack.h"

// 初始化

void STInit(ST* pst)

{

assert(pst);

pst->a = NULL;

// top指向栈顶数据的下一个位置

pst->top = 0;

// top指向栈顶数据

//pst->top = -1;

pst->capacity = 0;

}注意这里top如果初始化为0,代表指向栈顶的下一个元素=元素个数,如果初始化为-1,就代表指向栈顶元素=元素个数-1,以下是两种方式的图示,更方便大家理解

两种都可以,这里我用top=0来进行操作

test.c

代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1

#include"Stack.h"

void cm()

{

ST oj;

STInit(&oj);//初始化

}

int main()

{

cm();

return 0;

}2、栈的销毁在一系列操作之后,一定是要销毁的,以下是栈的销毁,和初始化相似

Stack.c

代码语言:javascript复制// 销毁栈(释放栈占用的内存,重置栈状态)

void STDestroy(ST* pst)

{

assert(pst); // 确保栈指针pst不为空(防止传入空指针导致的非法访问)

free(pst->a); // 释放栈的底层数组空间(数组是栈存储元素的实际载体)

pst->a = NULL;// 将数组指针置空,避免野指针(防止后续误访问已释放的内存)

pst->top = pst->capacity = 0; // 重置栈顶指针和容量为0,标记栈已销毁(逻辑上清空栈)

}test.c

代码语言:javascript复制#include"Stack.h"

void cm()

{

ST oj;

STInit(&oj);//初始化

STDestroy(&oj);//栈的销毁

}

int main()

{

cm();

return 0;

}3、入栈由于只能从栈顶插入,由于我们通过数组实现,也就相当于在数组尾部插入数据,还需要判断是否空间满,扩容操作,因为top初始化为0,执行判断top与capacity是否相等就可以了,如下

Stack.c

代码语言:javascript复制// 向栈中插入元素(入栈操作)

// pst:指向栈结构的指针,x:要插入的元素值

void STPush(ST* pst, STDataType x)

{

assert(pst); // 确保栈指针pst不为空(防止对空指针解引用,避免程序崩溃)

// 检查栈是否已满(栈顶指针等于容量时,无法直接插入,需先扩容)

if (pst->top == pst->capacity)

{

// 计算新容量:若初始容量为0(栈为空),则初始化为4;否则容量翻倍(高效扩容策略,减少频繁扩容开销)

int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;

// 重新分配内存:用realloc扩容底层数组,新大小为newcapacity个STDataType类型元素

STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));

if (tmp == NULL) // 检查内存分配是否失败(如内存不足)

{

perror("realloc fail"); // 打印错误信息(如"realloc fail: Out of memory")

return; // 分配失败则无法入栈,直接返回

}

pst->a = tmp; // 更新栈的底层数组指针为新分配的内存

pst->capacity = newcapacity; // 更新栈的容量为新容量

}

// 将元素x放入栈顶位置(当前top指向的索引是下一个可插入位置)

pst->a[pst->top] = x;

pst->top++; // 栈顶指针上移一位,指向新的可插入位置(栈大小+1)

}4、出栈出栈比较简单,只需要把指向栈顶下一个元素的top(也就是元素个数)-1就行了

如下

Stack.c

代码语言:javascript复制// 从栈中移除栈顶元素(出栈操作)

// pst:指向栈结构的指针

void STPop(ST* pst)

{

assert(pst); // 确保栈指针pst不为空(防止对空指针解引用,避免程序崩溃)

assert(pst->top > 0); // 确保栈不为空(top > 0表示栈中有元素),避免对空栈执行出栈操作

pst->top--; // 栈顶指针减一:通过移动栈顶指针,使原栈顶元素不再被栈包含,实现出栈(无需实际删除元素内存,后续入栈会覆盖)

}5、取栈顶元素因为我们top初始化为0指向栈顶的下一个元素,所以取的是top-1

Stack.c

代码语言:javascript复制// 获取栈顶元素的值(仅查看,不删除栈顶元素)

// pst:指向栈结构的指针,返回值为栈顶元素的数据(类型为STDataType)

STDataType STTop(ST* pst)

{

assert(pst); // 确保栈指针pst不为空(防止对空指针解引用,避免程序崩溃)

assert(pst->top > 0); // 确保栈不为空(top > 0表示栈中有元素),避免访问空栈的"栈顶"(会导致数组越界)

// 返回栈顶元素:栈顶指针top指向"下一个可插入位置",因此当前栈顶元素的索引为top - 1

return pst->a[pst->top - 1];

}测试结合上面我们来测试一下,避免错误

先初始化,再入栈,取栈顶元素打印,再出栈,再打印,最后销毁

test.c

代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1

#include"Stack.h" // 包含栈的头文件,其中定义了栈的结构体(ST)及相关操作函数(初始化、入栈、出栈等)

// 测试栈的各种操作的函数

void cm()

{

ST oj; // 定义栈实例(ST为栈结构体类型,oj为具体的栈变量)

STInit(&oj); // 初始化栈:为栈分配初始空间,设置栈顶指针和容量初始值(如top=0,capacity=0或初始大小)

// 入栈操作:向栈中压入元素2和1(栈遵循LIFO,此时栈中元素从底到顶为[2,1],栈顶是1)

STPush(&oj, 2);

STPush(&oj, 1);

// 取栈顶元素并打印:此时栈顶是1,输出"1 "

printf("%d ", STTop(&oj));

// 出栈操作:移除栈顶元素1(栈中剩余元素[2],栈顶变为2)

STPop(&oj);

// 取栈顶元素并打印:此时栈顶是2,输出"2 "

printf("%d ", STTop(&oj));

// 继续入栈:压入3和5(栈中元素从底到顶为[2,3,5],栈顶是5)

STPush(&oj, 3);

STPush(&oj, 5);

// 取栈顶元素并打印:此时栈顶是5,输出"5 "

printf("%d ", STTop(&oj));

// 销毁栈:释放栈底层数组占用的内存,重置栈顶指针和容量为0,避免内存泄漏

STDestroy(&oj);

}

int main()

{

cm(); // 调用测试函数,执行栈的各项操作

return 0;

}结果为,正确

在这里插入图片描述6、判空这里只需要判断top是否为0就行啦,代码如下

Stack.c

代码语言:javascript复制// 判空

bool STEmpty(ST* pst)

{

assert(pst);

return pst->top == 0;

}7、获取元素个数因为top指的也是元素个数,所以返回top的值就行啦

Stack.c

代码语言:javascript复制// 获取数据个数

int STSize(ST* pst)

{

assert(pst);

return pst->top;

}8、最后测试代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1

#include"Stack.h"

void cm()

{

ST oj;

STInit(&oj);

STPush(&oj, 2);

STPush(&oj, 1);

printf("%d ", STTop(&oj));

STPop(&oj);

printf("%d ", STTop(&oj));

STPush(&oj, 3);

STPush(&oj, 5);

printf("%d ", STTop(&oj));

STPop(&oj);

STPop(&oj);

int a=STEmpty(&oj);

if (a == 0)

{

printf("空\n");

}

else

{

printf("非空\n");

}

STPop(&oj);

a = STEmpty(&oj);

if (a == 0)

{

printf("空\n");

}

else

{

printf("非空\n");

}

STDestroy(&oj);

}

int main()

{

cm();

return 0;

}结果为,证明正确

在这里插入图片描述9、总代码Stack.h

代码语言:javascript复制#pragma once

#include

#include

#include

#include

typedef int STDataType;

typedef struct Stack

{

STDataType* a;

int top;

int capacity;

}ST;

// 初始化和销毁

void STInit(ST* pst);

void STDestroy(ST* pst);

// 入栈 出栈

void STPush(ST* pst, STDataType x);

void STPop(ST* pst);

// 取栈顶数据

STDataType STTop(ST* pst);

// 判空

bool STEmpty(ST* pst);

// 获取数据个数

int STSize(ST* pst);Stack.c

代码语言:javascript复制#include"Stack.h"

// 初始化和销毁

void STInit(ST* pst)

{

assert(pst);

pst->a = NULL;

// top指向栈顶数据的下一个位置

pst->top = 0;

// top指向栈顶数据

//pst->top = -1;

pst->capacity = 0;

}

void STDestroy(ST* pst)

{

assert(pst);

free(pst->a);

pst->a = NULL;

pst->top = pst->capacity = 0;

}

// 入栈 出栈

void STPush(ST* pst, STDataType x)

{

assert(pst);

// 扩容

if (pst->top == pst->capacity)

{

int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;

STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));

if (tmp == NULL)

{

perror("realloc fail");

return;

}

pst->a = tmp;

pst->capacity = newcapacity;

}

pst->a[pst->top] = x;

pst->top++;

}

void STPop(ST* pst)

{

assert(pst);

assert(pst->top > 0);

pst->top--;

}

// 取栈顶数据

STDataType STTop(ST* pst)

{

assert(pst);

assert(pst->top > 0);

return pst->a[pst->top - 1];

}

// 判空

bool STEmpty(ST* pst)

{

assert(pst);

return pst->top == 0;

}

// 获取数据个数

int STSize(ST* pst)

{

assert(pst);

return pst->top;

}test.c

代码语言:javascript复制#include"Stack.h"

void cm()

{

ST oj;

STInit(&oj);

STPush(&oj, 2);

STPush(&oj, 1);

printf("%d ", STTop(&oj));

STPop(&oj);

printf("%d ", STTop(&oj));

STPush(&oj, 3);

STPush(&oj, 5);

printf("%d ", STTop(&oj));

STPop(&oj);

STPop(&oj);

int a=STEmpty(&oj);

if (a == 0)

{

printf("空\n");

}

else

{

printf("非空\n");

}

STPop(&oj);

a = STEmpty(&oj);

if (a == 0)

{

printf("空\n");

}

else

{

printf("非空\n");

}

STDestroy(&oj);

}

int main()

{

cm();

return 0;

}四、栈的应用(相关算法题)题目链接有效的括号

思路这题可以通过栈来实现左括号入栈,判断右括号与前一个左括号(利用取栈顶元素)是否匹配

方法1:我们已经写出来栈的实现,就可以以它作为模板,复制粘贴利用,我们再定义bool类型的函数,利用上面思路,匹配出栈,不匹配直接错误,最后判断是否为空,为空正确,非空错误

参考代码:

代码语言:javascript复制#include

#include

#include

#include

typedef int STDataType;

typedef struct Stack

{

STDataType* a;

int top;

int capacity;

}ST;

void STInit(ST* pst);

void STDestroy(ST* pst);

void STPush(ST* pst, STDataType x);

void STPop(ST* pst);

STDataType STTop(ST* pst);

bool STEmpty(ST* pst);

int STSize(ST* pst);

void STInit(ST* pst)

{

assert(pst);

pst->a = NULL;

pst->top = 0;

pst->capacity = 0;

}

void STDestroy(ST* pst)

{

assert(pst);

free(pst->a);

pst->a = NULL;

pst->top = pst->capacity = 0;

}

void STPush(ST* pst, STDataType x)

{

assert(pst);

if (pst->top == pst->capacity)

{

int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;

STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));

if (tmp == NULL)

{

perror("realloc fail");

return;

}

pst->a = tmp;

pst->capacity = newcapacity;

}

pst->a[pst->top] = x;

pst->top++;

}

void STPop(ST* pst)

{

assert(pst);

assert(pst->top > 0);

pst->top--;

}

STDataType STTop(ST* pst)

{

assert(pst);

assert(pst->top > 0);

return pst->a[pst->top - 1];

}

bool STEmpty(ST* pst)

{

assert(pst);

return pst->top == 0;

}

int STSize(ST* pst)

{

assert(pst);

return pst->top;

}

bool isValid(char* s)

{

ST st1;

STInit(&st1);

while (*s)

{

if (*s == '(' || *s == '[' || *s == '{')

{

STPush(&st1, *s);

}

else

{

if (STEmpty(&st1))

{

STDestroy(&st1);

return false;

}

char ret = STTop(&st1);

if ((ret == '(' && *s != ')')

|| (ret == '[' && *s != ']')

|| (ret == '{' && *s != '}'))

{

STDestroy(&st1);

return false;

}

STPop(&st1);

}

s++;

}

if (STEmpty(&st1))

{

STDestroy(&st1);

return true;

}

STDestroy(&st1);

return false;

}方法2:与前面思路一样,只是利用栈的思想来实现,直接利用数组遍历字符串,遇到左括号(( [ {)就将其压入栈中暂存;遇到右括号时,检查栈顶是否有对应的左括号 —— 若栈为空(无左括号可匹配)或类型不匹配,直接判定为无效;若匹配,则弹出栈顶左括号(完成一对匹配)。遍历结束后,若栈为空,说明所有左括号都被对应右括号匹配,字符串有效;反之,存在未匹配的左括号,字符串无效。

代码如下

代码语言:javascript复制bool isValid(char* s)

{

char st[1000005]; // 栈数组,存储左括号

int t = 0; // 栈顶指针(t=0 表示栈空,t的值即栈中元素数量)

for (int i = 0; s[i]; i++) { // 遍历字符串中的每个字符

if (s[i] == '(' || s[i] == '[' || s[i] == '{')

{

st[t++] = s[i]; // 左括号入栈,栈顶指针上移

} else

{

if (t == 0)

{ // 栈空却遇到右括号,无匹配左括号 → 无效

return false;

}

// 检查右括号与栈顶左括号是否匹配

if ((s[i] == ')' && st[t-1] == '(') ||

(s[i] == ']' && st[t-1] == '[') ||

(s[i] == '}' && st[t-1] == '{')) {

t--; // 匹配成功,栈顶左括号出栈

} else {

return false; // 类型不匹配 → 无效

}

}

}

return t == 0; // 遍历结束后栈为空 → 所有括号匹配,否则存在未匹配左括号

}相似题目牛客上的相似题目,几乎一样,大家可以尝试

链接括号序列

其中一种如下

代码语言:javascript复制上面是对方法1的调用,这里忽略不写了

int main()

{

char arr[1000001];

scanf("%s", arr);

int a = isValid(arr);

if (a != 0)

{

printf("Yes\n");

}

else

printf("No\n");

return 0;

}另一种和方法2一样,只是稍作修改,这里就不展示了

总结本章到这里就结束啦(▽),介绍了栈的实现方式和应用题目,希望大家有所收获,下一节我们就要介绍队列啦,敬请期待,本文有写的不好的地方希望大家提出,我会积极改正,感谢大家的支持!

相关推荐