上次我们复习了静态链表,它需要系统提前分配一定大小的内存空间,同时它和单链表一样有一个指针(游标)指向下一个节点的数组下标索引。它不具有顺序表随机访问的功能,但它可以像单链表一样插入删除时不需要移动其他元素,只需要改变游标就可以实现。
栈的概念
栈是一种只能在一端进行插入或删除的线性表,通常这一端叫做栈顶,另一端通常固定不变叫做栈底,它的主要特点就是后进先出(FILO),按照存储结构可以分为顺序栈和链栈。
这张图就简单表示了栈的结构,先放入的元素会放在栈底,依次叠加,当要取出元素时只能先从最上面的元素开始,而我们如何知道这个栈的大小以及对其进行操作呢,那就得用到栈底和栈顶了。前面说过栈底是保持不变的,所以我们只需要通过移动栈顶就可以完成所需要的功能了。在数据结构中,栈顶通常用一个栈顶指针top进行表示。
这里顺带还要提一下栈的数学性质,大家可以记一下:
当n个元素以某种顺序入栈时,且可在任意时刻出栈,所获得的出栈序列的数目N为
顺序栈的初始化
首先来讲一下顺序栈,顺序栈就跟顺序表一样,一开始要定义栈的空间大小,定义一个数组用来表示栈中存储的元素,定义一个top指向此时栈顶的位置。最后用一个结构体进行封装。
#define Maxsize 10 //定义栈的大小
typedef struct
{
int data[Maxsize]; //存储数据
int top; //指向栈顶对栈进行操作
}stack;
一开始栈顶和栈底相等,整个栈内没有任何元素。
通常来讲,我们会把栈顶指针指向-1这个位置,当然有些题会让它指向0这个位置,这时候就需要注意审题。
这里我先讲栈顶指针指向-1时。
void initstack(stack *s)
{
s->top = -1;
}
顺序栈入栈
当栈顶指针指向-1,因为我们存入的数据要从下标为0开始存,所以我们入栈的操作是先让指针移动到下一个位置,再进行插入数据。当然,在正式插入数据还需要判断栈是否还有空间,否则会出现溢出的情况。栈满的条件应该是栈顶指针指向maxsize-1的位置,因为每次入栈之后此时的栈顶指针指向的位置和数组下标是一致的,当数据存满之后,也就是push函数执行了maxsize次时,栈顶指针指向a[maxsize-1]。
bool push(stack *s,int data)
{
if(s->top == Maxsize-1) //如果栈空间已满
return false;
s->data[++s->top] = data; /*等同于s->top = s->top + 1;
s->data[s->top]=data;*/
return true;
}
顺序栈出栈
出栈一样要先判断栈是否已经为空,栈为空的话栈顶指针是指向-1这个位置的,然后将弹出的元素先赋值给data,指针再向下移动。
bool pop(stack *s,int *data)
{
if(s->top == -1) //如果栈为空
return false;
*data = s->data[s->top--]; /*等同于data = s->data[s->top];
s->top = s->top - 1;*/
return true;
}
如果是初始化是top指针指向0,入栈出栈和判断栈满栈空的条件都会不一样。
由于初始化top指针为0,我们从数组下标为0开始插入,所以需要先插入数据再移动栈顶指针。另外,每次入栈后top指向的位置是数组下标的下一位,所以当栈满时top应指向Maxsize这个位置。
bool push(stack *s,int data)
{
if(s->top == Maxsize)
return false;
s->data[s->top++] = data; //先赋值再移动指针
return true;
}
同理,出栈时要先移动指针再进行赋值,栈空时栈顶指针是指向0的。
bool pop(stack *s,int *data)
{
if(s->top == 0)
return false;
*data = s->data[--s->top]; //先移动指针再赋值
return true;
}
共享栈
共享栈其实就是两个栈共享同一片内存空间,它与普通的栈相比更有效地利用了存储空间,并且时间复杂度与普通的栈一样入栈出栈都是O(1)。
两个栈的栈底一开始分别指向共享空间的两端。
入栈时第一步还是得先判断栈空间是否溢出。以这张图为例就是top[0]+1=top[1]。然后判断元素进入的是哪个栈,然后进行相应的入栈操作。
出栈时先判断是哪一个栈要出栈,然后判断它是否为空。
#define Maxsize 10
typedef struct
{
int data[Maxsize];
int top0; //第一个栈的头指针
int top1; //第二个栈的头指针
}stack;
void initstack(stack *s)
{
s->top0 == -1;
s->top1 == Maxsize;
}
bool push(stack *s,int i,int data) // 多了一个i判断是入哪个栈
{
if(s->top0+1 == s->top1||i<0||i>1) //栈已满或者输入不合法数据
return false;
if(i == 0)
s->data[++s->top0] = data;
else
s->data[--s->top1] = data;
return true;
}
bool pop(stack *s,int i,int *data)
{
if(s->top0 == -1 && i==0 || s->top1==Maxsize && i==1 || i<0 || i>1)
return false;
if(i == 0)
*data = s->data[s->top0--];
else
*data = s->data[s->top1--];
return true;
}
所以顺序栈只需要记住两个状态和两个操作就可以,两个状态就是栈空和栈满的状态,两个操作就是先移动指针还是先赋值。
链栈初始化
链栈就和单链表类似,初始化基本可以照搬。
typedef struct node
{
struct node *next;
int data;
}node,*stack;
同样也需要分为有头节点和无头结点的情况。
这里我先讲有头结点的情况:
void initstack(stack *s)
{
*s = (node *)malloc(sizeof(node));
(*s)->next = NULL;
}
链栈入栈
链栈入栈其实就跟单链表的头插法一样,栈顶指针只需要指向头节点的下一节点就可以了。重新回顾一下头插法的过程。
代码基本也是一样的。大家可以自己手敲一下就当复习了。
void push(stack *s,int data)
{
node *p = *s;
node *q = (node *)malloc(sizeof(node));
q->data = data;
q->next = p->next;
p->next = q;
}
链栈出栈
出栈其实就等于将单链表第一个带数据的节点删除。
bool pop(stack *s,int *data)
{
if((*s)->next==NULL) //如果栈为空
{
return false;
}
node *p = (*s)->next;
*data = p->data;
(*s)->next = p->next;
free(p);
return true;
}
接下来是没有头节点的情况(代码过于简单不再赘述,下面仅作参考):
void initstack(stack *s)
{
*s = NULL;
}
void push(stack *s,int data)
{
node *p = (node *)malloc(sizeof(node));
p->data = data;
p->next = *s;
*s = p;
}
bool pop(stack *s,int *data)
{
if(s == NULL)
return false;
node *p = *s;
*data = p->data;
*s = (*s)->next;
free(p);
return true;
}
栈的应用
这里我就讲一下考研当中容易考的栈的一个应用——中缀表达式转换为后缀表达式以及表达式求值。
在讲之前简单讲一下什么是表达式吧。
表达式分为三种,在生活中我们常用的算数表达式就是中缀表达式,例如3*(4+2)-5等等。但是计算机是怎么实现这些算术操作的呢,一般以我们的角度而言肯定会先进行括号里的运算,但是计算机怎么知道要这么干呢总不可能给它设置很多分支吧,所以不知道哪位计算机大神想出来可以利用一个古老的波兰数学家卢卡西维奇提出来的后缀表达式或前缀表达式,这么做的好处就是不需要知道算数运算符的优先级顺序,简化了逻辑,适宜计算机操作。
此时我的心情就是:
说了一些废话,还是直接上干货(用栈转换的方法,其实还可以用二叉树后面再讲):
- 先初始化一个栈和一个数组,栈用来存放运算符,数组用来存放表达式。
- 从左到右依次遍历中序表达式。如果是字符直接放进数组。
- 如果是运算符,就得分类讨论:
- 如果是加减乘除符号,首先要判断栈里是否有元素,栈里没有元素直接入栈。若有比较栈顶元素与该符号的优先级,若栈顶元素优先级低于该符号,该符号入栈。若栈顶元素优先级等于或高于该符号,弹出栈顶元素至数组中直至栈为空或者出现栈顶元素优先级低于该符号,然后该符号再入栈。
- 如果是左括号,则直接入栈。如果是右括号,则弹出栈顶元素至数组中直至遇见左括号,然后将左右括号抛弃。
最后得到的数组就是后缀表达式。
以a+b-a*((c+d)/e-f)+g为例:
1、指向a,不是运算符进入数组
2、指向+,运算符,此时栈为空入栈
3、指向b,不是运算符进入数组
4、指向-,运算符,优先级与+相等,+出栈进入数组,栈空,-入栈
5、指向a,不是运算符进入数组
6、指向*,运算符,优先级比-高,入栈
7、指向(,直接入栈
8、指向(,直接入栈
9、指向c,不是运算符进入数组
10、指向+,优先级比(高,入栈
11、指向d,不是运算符进入数组
12、指向),弹出栈元素进入数组直至出现(
13、指向/,运算符,优先级比(高,入栈
14、指向e,不是运算符进入数组
15、指向-,运算符,优先级比/低,/出栈进入数组,优先级比(高,入栈
16、指向f,不是运算符进入数组
17、指向),弹出栈元素进入数组直至出现(
18、指向+,运算符,优先级比*低,优先级与-相同,依次弹出*,-,栈空入栈
19、指向g,不是运算符进入数组
20、遍历完成依次弹出栈顶元素至数组。
用代码实现如下:
#include<stdio.h>
#include<stdbool.h>
int priority(char a,char b) //判断优先级,a>b返回1,否则返回0
{
//a只可能是+,-,*,/
if(a == b) //两个字符相同不可以入栈
{
return 0;
}
else //两个字符不相同
{
if(b == '(' ) //左括号优先级最小,任何元素都可入栈
{
return 1;
}
else if (b == '*'|| b == '/') //当b是*或/a不可能优先级比它高
{
return 0;
}
else //当b是+或者-
{
return (a == '*' || a == '/') ? 1 : 0;
}
}
}
void convert(char a[],char b[])
{
int i,count = 0,top = -1;
char stack[20]; //定义一个栈
for(i=0;a[i]!='\0';i++) //从左向右遍历
{
if(a[i] == '+'|| a[i] == '-' || a[i] == '*' || a[i] == '/')
{
if(top == -1 || priority(a[i],stack[top]) == 1) //如果栈为空或者栈顶元素优先级小于该运算符
{
stack[++top] = a[i]; //入栈
}
else
{
//出栈直至遇见优先级小于该运算符或栈为空
while(top!=-1&&priority(a[i],stack[top]) == 0)
{
b[count] = stack[top--];
count++;
}
stack[++top] = a[i];
}
}
else if(a[i] == '(')
{
stack[++top] = a[i]; //直接入栈
}
else if(a[i] == ')')
{
while(stack[top]!='(' && top!=-1 ) //一直出栈直至遇见左括号或栈空
{
b[count++] = stack[top--];
}
if(top!=-1)
{
top--; //去除左括号
}
}
else //如果是字符直接进入数组
{
b[count] = a[i];
count++;
}
}
while (top != -1) { // 如果还有元素在栈中,则全部出栈
b[count++] = stack[top--];
}
b[count] = '\0';
}
int main()
{
char a[20],b[20]; //a数组存储中缀表达式
//b数组存储后缀表达式
int i=0;
while( (a[i]=getchar()) !='\n') //输入表达式到数组中
{
i++;
}
a[i] = '\0';
convert(a,b);
for (i = 0; b[i] != '\0'; i++) {
printf("%c", b[i]);
}
}
如果要化为前缀表达式的话,其实原理差不多,我这里就不多解释了,简单介绍一下方法就行了,有愿意了解的可以自己动手实现代码。
- 先初始化一个栈和一个数组,栈用来存放运算符,数组用来存放表达式。
- 从右到左依次遍历中序表达式。如果是字符直接放进数组。
- 如果是运算符,就得分类讨论:
- 如果是加减乘除符号,首先要判断栈里是否有元素,栈里没有元素直接入栈。若有比较栈顶元素与该符号的优先级,若栈顶元素优先级低于或等于该符号,该符号入栈。若栈顶元素优先级高于该符号,弹出栈顶元素至数组中直至栈为空或者出现栈顶元素优先级低于或等于该符号,然后该符号再入栈。
- 如果是右括号,则直接入栈。如果是左括号,则弹出栈顶元素至数组中直至遇见右括号,然后将左右括号抛弃。
- 最后将结果倒置
当化成前缀或后缀表达式之后,就可以进行运算了。
运算规则就是(这里以后缀表达式为主)
- 将数组内容从左到右依次存入栈中
- 若存入的是字符则直接入栈
- 若存入的是运算符则连续出栈两次,出栈的第一个元素放在右边,出栈的第二个元素放在左边进行相应的运算,得到的结果再存入栈
假设我们输入的都是合法输入,这里的数字只能是0~9,且除法得到的结果都是整数。
#include<stdio.h>
int op(char a,char b,char c)
{
switch (b) {
case '+':
return a + c;
case '-':
return a - c;
case '*':
return a * c;
case '/':
if (c == 0) {
// 返回一个表示错误的特殊值
return -65535;
} else {
return a / c;
}
default:
// 如果运算符不是 +、-、* 或 /
return -65535;
}
}
int carculate(char array[])
{
int stack[20];
int top = -1; //定义一个栈
int result; //得到一次运算的结果
for(int i=0;array[i]!='\0';i++) //从左到右遍历
{
if(array[i] == '+'||array[i] == '-'||array[i] == '*'||array[i] == '/')
{
//如果是运算符
int c = stack[top--]; //第一个栈顶元素放右边
int a = stack[top--]; //第二个栈顶元素放左边
result = op(a,array[i],c);
stack[++top] = result;
}
else{
array[i] = array[i] - '0'; // 将字符型数字改为数字值
stack[++top] = array[i];
}
}
return stack[top];
}
int main()
{
char data[20]; //键入你想要输入的后缀表达式
int i=0;
while((data[i]=getchar())!='\n')
{
i++;
}
data[i] = '\0';
printf("%d",carculate(data));
}