今天我们来学习栈与栈的应用,并以此来学习栈在递归算法,四则运算表达式中的应用
文章目录
一、栈的定义
大家在写程序的时候应该都遇到过一个异常叫做“栈溢出”那这个栈和我们今天要学习的栈有什么异同呢?
栈溢出就是缓冲区溢出的一种。
由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。缓冲区长度一般与用户自己定义的缓冲变量的类型有关。栈溢出就是缓冲区溢出的一种。
我们今天要学习的栈是一种存储数据的结构,它表示,在存储和删除数据的时候只能在栈顶进行添加和删除的操作
栈的定义:栈是限定仅在表尾进行插入和删除操作的线性表。
二、栈的抽象数据类型
ADT Stack {
数据对象:
D = {ai | ai ∈ ElemSet,i = 1,2,3,....,n, n ≥ 0} // ElemSet 表示元素的集合
数据关系:
R1={<ai-1, ai> | ai-1 , ai∈D,i=2,...,n} // ai-1为前驱,ai为后继
约定 an 端为栈顶,a1 端为栈底
基本操作:初始化、进栈、出栈、取栈顶元素等
} ADT Stack
InitStack(&S)初始化操作
操作结果:构造一个空栈 S
DestroyStack(&S)销毁栈操作
初始条件:栈S已存在
操作结果:栈S被销毁
StackEmpty(S)判定栈是否为空栈
初始条件:栈S已存在
操作结果:若栈S为空栈,则返回TRUE,
若栈S为非空,则返回FALSE。
StackLength(S)求栈的长度
初始条件:栈S已存在
操作结果:返回栈S的元素个数,即栈的长度
GetTop(S,&e)取栈顶元素
初始条件:栈S已存在且非空
操作结果:用e返回S的栈顶元素
ClearStack(&S)栈指控操作
初始条件:栈S已存在
操作结果:将S清为空栈
Push(&S,e)入栈操作
初始条件:栈S已存在
操作结果:插入元素e为新的栈顶元素
Pop(&S,&e)出栈操作
初始条件:栈S存在且非空
操作结果:删除栈SD栈顶元素an,并用e返回其值
三、栈的顺序存储结构(C语言、Java)
顺序存储结构我们在线性表的时候已经了解过了,栈也是一种特殊的线性表,只不过它只能在栈顶进行插入和删除的操作,而其他操作,例如查找等则没有这个限制,因此,在实现栈的时候,与实现线性表并没有太大差别。
而对于顺序存储结构,可以使用数组来实现。我们将数组的0角标的元素作为栈底,方便我们进行存储。这里只需要增加一个top变量来进行标识栈顶元素即可。
上代码,基本和线性表相似,只有插入和删除不一样,不过你可以理解为线性表的表尾进行插入和删除操作即可。
我觉得线性表弄懂了这个应该很好理解
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
/* 顺序栈结构 */
typedef struct
{
SElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}SqStack;
Status visit(SElemType c)
{
printf("%d ",c);
return OK;
}
/* 构造一个空栈S */
Status InitStack(SqStack *S)
{
/* S.data=(SElemType *)malloc(MAXSIZE*sizeof(SElemType)); */
S->top=-1;
return OK;
}
/* 把S置为空栈 */
Status ClearStack(SqStack *S)
{
S->top=-1;
return OK;
}
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */
Status StackEmpty(SqStack S)
{
if (S.top==-1)
return TRUE;
else
return FALSE;
}
/* 返回S的元素个数,即栈的长度 */
int StackLength(SqStack S)
{
return S.top+1;
}
/* 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR */
Status GetTop(SqStack S,SElemType *e)
{
if (S.top==-1)
return ERROR;
else
*e=S.data[S.top];
return OK;
}
/* 插入元素e为新的栈顶元素 */
Status Push(SqStack *S,SElemType e)
{
if(S->top == MAXSIZE -1) /* 栈满 */
{
return ERROR;
}
S->top++; /* 栈顶指针增加一 */
S->data[S->top]=e; /* 将新插入元素赋值给栈顶空间 */
return OK;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(SqStack *S,SElemType *e)
{
if(S->top==-1)
return ERROR;
*e=S->data[S->top]; /* 将要删除的栈顶元素赋值给e */
S->top--; /* 栈顶指针减一 */
return OK;
}
/* 从栈底到栈顶依次对栈中每个元素显示 */
Status StackTraverse(SqStack S)
{
int i;
i=0;
while(i<=S.top)
{
visit(S.data[i++]);
}
printf("\n");
return OK;
}
int main()
{
int j;
SqStack s;
int e;
if(InitStack(&s)==OK)
for(j=1;j<=10;j++)
Push(&s,j);
printf("栈中元素依次为:");
StackTraverse(s);
Pop(&s,&e);
printf("弹出的栈顶元素 e=%d\n",e);
printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
GetTop(s,&e);
printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
ClearStack(&s);
printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
return 0;
}
看完了书上的C语言版的栈,我们来自己写一下Java版的栈
package Text;
public class Stack {
private int top = -1;
private final int DEFAULT_SIZE = 6;
private String[] arr = new String[DEFAULT_SIZE];
public boolean push(String value) {
if(top == arr.length -1) {
return false;
}
++top;
arr[top] = value;
return true;
}
public boolean pop() {
if(top == -1) {
return false;
}
String temp = arr[top];
top--;
return true;
}
public boolean isEmpty() {
return top == -1;
}
public String getTop() {
if(top == -1) { //栈为空
return null;
}
return arr[top];
}
public static void main(String[] args) {
Stack stack = new Stack();
System.out.println(stack.isEmpty());
stack.push("abc"); // 进栈
stack.push("def");
stack.push("ggg");
System.out.println(stack.isEmpty());
System.out.println(stack.getTop());
System.out.println(stack.pop()); // 出栈
System.out.println(stack.getTop()); // 获取栈顶元素
}
}
四、栈的链式存储结构(C语言、Java)
还是一样的,与线性表的链式存储结构相似,只不过这里的指针变量不是指向后一个元素的了,而是指向前一个元素,一直指到第一个元素,第一个元素的指针域为空,至于出栈操作,很简单,只要把top指针指向要删除元素的底下一个元素即可,然后把要删除的栈顶元素释放内存即可。上代码
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
/* 链栈结构 */
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
typedef struct
{
LinkStackPtr top;
int count;
}LinkStack;
Status visit(SElemType c)
{
printf("%d ",c);
return OK;
}
/* 构造一个空栈S */
Status InitStack(LinkStack *S)
{
S->top = (LinkStackPtr)malloc(sizeof(StackNode));
if(!S->top)
return ERROR;
S->top=NULL;
S->count=0;
return OK;
}
/* 把S置为空栈 */
Status ClearStack(LinkStack *S)
{
LinkStackPtr p,q;
p=S->top;
while(p)
{
q=p;
p=p->next;
free(q);
}
S->count=0;
return OK;
}
/* 若栈S为空栈,则返回TRUE,否则返回FALSE */
Status StackEmpty(LinkStack S)
{
if (S.count==0)
return TRUE;
else
return FALSE;
}
/* 返回S的元素个数,即栈的长度 */
int StackLength(LinkStack S)
{
return S.count;
}
/* 若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR */
Status GetTop(LinkStack S,SElemType *e)
{
if (S.top==NULL)
return ERROR;
else
*e=S.top->data;
return OK;
}
/* 插入元素e为新的栈顶元素 */
Status Push(LinkStack *S,SElemType e)
{
LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top; /* 把当前的栈顶元素赋值给新结点的直接后继,见图中① */
S->top=s; /* 将新的结点s赋值给栈顶指针,见图中② */
S->count++;
return OK;
}
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR */
Status Pop(LinkStack *S,SElemType *e)
{
LinkStackPtr p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top; /* 将栈顶结点赋值给p,见图中③ */
S->top=S->top->next; /* 使得栈顶指针下移一位,指向后一结点,见图中④ */
free(p); /* 释放结点p */
S->count--;
return OK;
}
Status StackTraverse(LinkStack S)
{
LinkStackPtr p;
p=S.top;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
int main()
{
int j;
LinkStack s;
int e;
if(InitStack(&s)==OK)
for(j=1;j<=10;j++)
Push(&s,j);
printf("栈中元素依次为:");
StackTraverse(s);
Pop(&s,&e);
printf("弹出的栈顶元素 e=%d\n",e);
printf("栈空否:%d(1:空 0:否)\n",StackEmpty(s));
GetTop(s,&e);
printf("栈顶元素 e=%d 栈的长度为%d\n",e,StackLength(s));
ClearStack(&s);
printf("清空栈后,栈空否:%d(1:空 0:否)\n",StackEmpty(s));
return 0;
}
再来写一下Java实现的栈的链式存储结构,这因为也是线性表,所以怎么用Java实现的线性表可以去我之前的博客中查看。当然,Java中一般是用源码的,也可以看源码。
package text;
public class LinkStack {
private Node header = new Node(); // 创建头节点
class Node {
String value;
Node next;
}
public boolean push(String value) {
Node n = new Node();
n.value = value;
if(header.next == null) {
header.next = n;
return true;
}
n.next = header.next;
header.next = n;
return true;
}
public boolean pop() {
if(header.next == null) {
return false;
}
header.next = header.next.next;
return true;
}
public String getTop() {
if(header.next != null) {
return header.next.value;
}
return null;
}
public boolean isEmpty() {
return header.next == null ? true : false;
}
public static void main(String[] args) {
LinkStack stack = new LinkStack();
System.out.println(stack.isEmpty());
stack.push("aaa"); // 进栈
stack.push("bbb");
stack.push("ccc");
stack.push("ddd");
System.out.println(stack.isEmpty());
System.out.println(stack.getTop());
System.out.println(stack.pop());
System.out.println(stack.getTop());
}
}
上面的Java代码没有实现泛型化,也可以把它写成泛型,可以更加贴近Java源代码。
五、顺序存储结构与链式存储结构的对比
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它变化在可控范围内,建议使用顺序栈会好一点。
六、栈的作用
刚学栈的时候,可能会有疑问,为什么要设计栈这种有限制性的数据结构呢,直接用线性表不好吗?我也有过这样的疑问,但是当你从实际问题出发的时候,你就会发现它的用处了,比如网页的返回上一个网页,撤销等操作的时候,我们操作的对象都在栈顶,所以对于栈的其他元素我们不需要关注,因此,我们就不用费大力气去使用链表,造成麻烦还不利于程序运行。
七、栈的应用一(递归、斐波那切数列)
什么是递归,相信大家在学习C语言的时候都学过这个,就是函数自己本身调用自己本身。那么递归和栈有什么联系呢?
递归如何执行它的前进和退回阶段,这里不展开说明了,详细说明可以去看一下专门的讲解,这里着重讲一下栈与递归的联系。递归过程退回的顺序是它前进顺序的逆序。在退回 过程中可能要执行某些操作,包括恢复在前进过程中存储起来的某些数据。这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,这很显然是符合栈的数据结构。可以将递归的前进过程理解为压栈,退回过程理解为处栈,现在的高级语言不需要编写者来管理这个栈,由系统来管理了,想了解的可以去看一下其他资料。
我们来写一下斐波那契数列吧
#include "stdio.h"
/* 斐波那契的递归函数 */
int Fbi(int i)
{
if( i < 2 )
return i == 0 ? 0 : 1;
return Fbi(i - 1) + Fbi(i - 2); /* 这里Fbi就是函数自己,等于在调用自己 */
}
int main()
{
int i;
int a[40];
printf("迭代显示斐波那契数列:\n");
a[0]=0;
a[1]=1;
printf("%d ",a[0]);
printf("%d ",a[1]);
for(i = 2;i < 40;i++)
{
a[i] = a[i-1] + a[i-2];
printf("%d ",a[i]);
}
printf("\n");
printf("递归显示斐波那契数列:\n");
for(i = 0;i < 40;i++)
printf("%d ", Fbi(i));
return 0;
}
八、栈的应用二(四则运算表达式)
看到四则运算的时候,你有没有疑惑,这四则运算为什么要用栈来实现,不是直接输入进去就行了吗?这里我们要了解的四则运算表达式,是计算机计算四则运算的底层原理。计算机如何计算复杂的四则运算。我们现在编写代码的时候,这个四则运算是已经写好的,不需要我们再去用代码来实现四则运算了。
对于四则运算是有优先级和先后顺序的,而这个先后顺序,先进行的运算会再进行下一步运算,那我们在遍历一个四则运算表达式的时候,进行优先级最大的计算,例如,遇到前括号,我们就进栈,遇到后括号的时候就出栈,将括号中的结果放出来进行下一步计算,这个过程不就是栈的概念吗?
当然,还有后缀表达式,中缀表达式的概念,这个就不展开说了。。。我也不太会。