目录
一、栈的应用场景
二、栈的基本概念和结构
2.1 栈的基本概念
2.2 栈的结构
2.3 栈的实现方式
三、顺序栈的接口函数实现
3.0 顺序栈的概念和结构
3.1 顺序栈的接口函数
3.2 顺序栈的设计(结构体)
3.3 顺序栈的初始化
3.4 入栈(相当于顺序表的尾插)
3.5 出栈(相当于顺序表的尾删)
3.6 获取栈顶元素值
3.7 获取有效元素个数
3.8 判空
3.9 判满
3.10 扩容
3.11 打印
3.12 清空
3.13 销毁
四、总结
一、栈的应用场景
栈是一种非常常见的数据结构,在计算机科学和软件工程中有许多应用场景。以下是一些常见的应用场景:
1. 函数调用和返回:栈被广泛应用于函数调用和返回的过程中。每次函数调用时,当前函数的执行上下文(如局部变量、函数参数、返回地址等)都被压入栈中,在函数返回时再从栈中弹出。
2. 表达式求值:栈可用于中缀表达式到后缀表达式的转换以及后缀表达式的求值过程。在转换过程中,栈用于暂时存储运算符,并根据运算符的优先级进行排序。在求值过程中,栈用于存储操作数,以便进行运算。
3. 递归算法:许多递归算法使用栈来管理递归调用的状态。每次递归调用时,函数的参数和局部变量都被保存在栈中,直到递归结束才被弹出。
4. 浏览器历史记录:浏览器的后退和前进功能可以通过栈来实现。每次访问一个新页面时,该页面的 URL 可以被推入栈中,当用户点击后退按钮时,最近访问的页面 URL 从栈中弹出。
5. 文本编辑器的撤销操作:文本编辑器通常使用栈来实现撤销和重做操作。每次进行编辑操作时,编辑器将操作的内容推入栈中,当用户执行撤销操作时,最近的编辑操作可以从栈中弹出并还原。
6. 括号匹配检查:栈可用于检查括号是否匹配的问题。遍历字符串时,遇到左括号时将其推入栈中,遇到右括号时将栈顶的左括号弹出,最后检查栈是否为空,若为空则说明括号匹配正确。
7. 迷宫求解:在深度优先搜索(DFS)算法中,栈可以用来实现迷宫的求解。每次选择一个方向探索时,将当前位置推入栈中,并在探索结束后回溯时弹出栈顶的位置。
二、栈的基本概念和结构
2.1 栈的基本概念
它是一种受到限制的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶(top),另一端称为栈底(bottom)。并且它的特点是:栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。没有数据的话叫做空栈。现实生活中的例子:弹夹,盘子的摆放。
基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:
- 向栈中添加元素,此过程被称为"进栈"(入栈或压栈),入数据在栈顶;
- 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈),出数据也在栈顶;
2.2 栈的结构
栈是一种只能从表的一端存取数据且遵循 "先进后出" 原则的线性存储结构。
2.3 栈的实现方式
栈是一种 "特殊" 的线性存储结构,因此栈的具体实现有以下两种方式:
- 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构;
- 链栈:采用链式存储结构实现栈结构;
两种实现方式的区别,仅限于数据元素在实际物理空间上存放的相对位置,顺序栈底层采用的是动态数组(不定长顺序表),链栈底层采用的是单链表。
三、顺序栈的接口函数实现
3.0 顺序栈的概念和结构
顺序栈,即用顺序表(数组)实现栈存储结构。为保证栈的数据元素的后进先出的高效性,那么我们将栈顶应该设计在数组的哪一端呢?学过顺序表,我们可以知道,顺序表的尾插和尾删的效率非常高,时间复杂度为O(1),因此,参考顺序表,我们可以将栈顶设计为数组的末端,通过指针指向它。(实际中用一个整型变量标记栈顶即可,更加简单方便),因此,我们可以这么通俗的理解顺序栈:限定只能尾插和尾删的顺序表!
3.1 顺序栈的接口函数
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stack>//系统自带的栈
#include "Stack.h"
//初始化
void Init_stack(struct Stack* st);
//入栈
void Push(struct Stack* st, ELEM_TYPE val);
//出栈
void Pop(struct Stack* st);
//获取栈顶元素值
ELEM_TYPE Top(struct Stack* st);
//获取有效元素个数
int Get_length(struct Stack* st);
//判空
bool IsEmpty(struct Stack* st);
//判满
bool IsFull(struct Stack* st);
//扩容
void Inc_Stack(struct Stack* st);
//打印
void Show(struct Stack* st);
//清空
void Clear(struct Stack* st);
//销毁
void Destroy(struct Stack* st);
3.2 顺序栈的设计(结构体)
本质和顺序表的设计差不多,主要为3个成员,栈底指针(相当于顺序表中用来申请内存空间的指针),栈顶指针标记栈顶的位置,简单起见,用整型变量top即可(相当于顺序表中记录有效元素个数的cursize),记录栈的总容量的stacksize(相当于顺序表中的capacity)
typedef struct Stack
{
ELEM_TYPE *base;//栈底指针 指向动态数组的指针,用来扩容
int top;//栈顶指针,
//标记栈顶位置的指针,此处用整型变量表示栈顶的下标更加方便(当前栈的有效元素个数,也代表下一个元素插入的下标,相当于顺序表中的cursize)
int stacksize;//当前总的容量大小
}Stack, *PStack;
stacksize指示栈的最大容量; base为栈底指针,它始终指向栈底位置,若base的值为
NULL,则表明栈结构不存在; top为栈顶指针,其初值指向栈底,即top=base可作为栈空的
标记。每当插入新的栈顶元素时,指针top增加1;删除栈顶元素时,指针top减1.因此,非空
栈中的栈顶指针始终在栈顶元素的下一个位置上。
base == top或者top==0是栈空的重要标志!
3.3 顺序栈的初始化
顺序栈在创建以后必须要进行初始化,否则内部为随机值,无法使用。顺序栈的初始化主要是对结构体成员赋初值。
//栈的顺序存储结构体设计
typedef int ELEM_TYPE;
#define INIT_SIZE 10
//初始化
void Init_stack(struct Stack* st)
{
st->base = (ELEM_TYPE *)malloc(INIT_SIZE * sizeof(ELEM_TYPE));
st->top = 0;
st->stacksize = INIT_SIZE;
}
3.4 入栈(相当于顺序表的尾插)
入栈相当于是顺序表的尾插操作,尾插数据的主要思路如下:
第0步:指针参数断言;
第1步:进行判满操作,同时如果顺序栈已满,则要进行扩容,同时要保证扩容成功;
第2步:尾插不需要移动数据,只需要在最后一个元素的下一个格子放入一个元素
第3步:更新顺序栈的有效元素个数变量, st->top++;
//入栈
void Push(struct Stack* st, ELEM_TYPE val)
{
//0.参数检测st!=NULL
assert(st!=NULL);
//1.判满,得有空间进行入栈
if(IsFull(st))
{
Inc_Stack(st); //扩容
}
//2.往top位置进行入栈
st->base[st->top] = val;
//3.对应的栈顶指针top需要自增,动一下
st->top++;
}
3.5 出栈(相当于顺序表的尾删)
出栈相当于是顺序表的尾删操作,尾删数据的主要思路如下:
第0步:指针参数断言;
第1步:进行判空操作,同时如果顺序栈为空,无法删除,结束程序;
第2步:尾删不需要移动数据;
第3步:更新顺序栈的有效元素个数变量, st->top++。
只要把最后一个元素数量减掉,最后一个数据去掉,size--,并不影响查找等功能。
//出栈
void Pop(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
//1.判空
if(IsEmpty(st))
{
return;
}
//2.只需要将栈顶指针top向下挪动一位
st->top--;
}
3.6 获取栈顶元素值
栈顶元素对应的下标为top-1,因此直接返回该下标对应的数组元素即可。
//获取栈顶元素值
ELEM_TYPE Top(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
return st->base[st->top-1];
}
3.7 获取有效元素个数
整型变量top标记的栈顶指针的位置,同时也是有效元素个数,直接返回即可!
//获取有效元素个数
int Get_length(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
return st->top;
}
3.8 判空
判空操作很简单,只需要比较base == top或者top==0即可,若相等,则无法进行删除操作!!
//判空
bool IsEmpty(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
return st->top == 0;
}
3.9 判满
判满操作很简单,只需要比较top和stacksize是否相等即可,若相等,表明顺序栈已满,此时要进行扩容操作!
//判满
bool IsFull(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
return st->stacksize == st->top;
}
3.10 扩容
当我们插入数据,若空间不够,将要扩容操作:使用
realloc
函数。
//扩容
void Inc_Stack(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
st->base = (ELEM_TYPE *)realloc(st->base, st->stacksize*sizeof(ELEM_TYPE) * 2);
st->stacksize *= 2;
}
3.11 打印
打印数组元素,其实和之前用指针方式访问数组一样,for循环遍历通过下标即可,
st->base[i] 。
//打印
void Show(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
for(int i=0; i<st->top; i++)
{
printf("%d ", st->base[i]);
}
printf("\n");
}
3.12 清空
顺序栈的有效数据元素个数,是通过top进行记录的,即使数组容量有10个,只要将top清0,就代表数组为空。 虽然这十个单元格子是存在内存空间的,但是编程者认为没有有效的数据,这便是清空顺序栈!
//清空
void Clear(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
st->top = 0;
}
3.13 销毁
因为顺序栈是动态内存分配的,是在堆区利用
realloc函数
分配的空间,不销毁会有内存泄漏的风险。因此,使用完毕必须要释放,同时释放完后要将指针置空,防止出现野指针!
//销毁
void Destroy(struct Stack* st)
{
//0.参数检测st!=NULL
assert(st!=NULL);
st->top = 0;
free(st->base);
st->base=NULL;
}
四、总结
从以上分析,我们可以知道顺序栈只是顺序表的一种特殊情况,限定了数据元素的插入和删除位置,顺序栈是一种基于数组实现的栈,它具有一些优点:
1. 内存连续性:顺序栈的元素在内存中是连续存储的,这使得访问元素非常高效。由于数组支持随机访问,因此可以直接通过索引访问栈中的任何元素,而不需要下一节讲的链式栈那样遍历链表。
2. 简单高效: 顺序栈的实现比较简单,不需要额外的指针来维护元素之间的关系,只需要一个数组和一个指向栈顶的索引即可。这使得其在空间和时间复杂度上都比较高效。
3. 适用性广泛:顺序栈适用于大多数栈的应用场景,无论是简单的数据处理还是复杂的算法实现,都可以方便地使用顺序栈来完成。
4. 易于实现:由于其基于数组的实现方式,顺序栈的操作相对简单,包括入栈、出栈、查看栈顶元素等操作都很容易实现,不需要复杂的指针操作或链表遍历。
以上便是我为大家带来的顺序栈设计内容,若有不足,望各位大佬在评论区指出,谢谢大家!下一节继续进行链式栈的内容,感兴趣的你可以留下你们的点赞、收藏和关注,这是对我极大的鼓励,我也会更加努力创作更优质的作品。再次感谢大家!