当前位置: 代码迷 >> 综合 >> m数据结构 day5 栈:后进先出,只能在表尾插入和删除的线性表
  详细解决方案

m数据结构 day5 栈:后进先出,只能在表尾插入和删除的线性表

热度:45   发布时间:2024-02-04 20:02:36.0

文章目录

  • 现实应用举例(只要用了先进后出,后进先出思想的都是栈的示例)
  • 栈的抽象数据类型
  • 顺序栈:栈的顺序存储结构,用数组实现
    • 进栈:O(1)
    • 出栈:O(1)
    • 两个同数据类型的栈共享空间:缓解数组长度必须事先确定的麻烦,适合于两栈的空间需求相反的情况
      • 进栈
      • 出栈
  • 链栈: 栈顶结点替代头结点;不会满栈
    • 进栈:O(1)
    • 出栈:O(1)
    • 链栈 VS 顺序栈
  • 栈的应用
    • 递归(函数调用自己,调用自己和调用别的函数并没有什么不同)
      • 经典递归例子:斐波那契数列 Fibonacci
        • 迭代式代码,使用循环结构
        • 递归式代码,使用选择结构
    • 四则运算表达式求值(原来如此!)
      • 对于有括号的表达式:遇到左括号就入栈,遇到右括号就让栈顶左括号出栈
      • 没括号表达式:逆波兰表示法reverse Polish Notation RPN
        • 示例
        • 如何把中缀表达式转为后缀表达式

栈和队列都是 特殊的线性表(本质上是线性表,因为他们的相邻元素也是一对一的线性关系,前驱后继,且同类型),所以栈和队列也可以用顺序结构和链式结构两种方式实现。他们的 特殊之处就在于限制了插入操作和删除操作的位置,栈的插入操作也叫做压栈,入栈,进栈;栈的删除操作也叫做出栈,弹栈。

栈顶top:允许插入和删除的一端
栈底bottom:固定,最先进栈的元素在栈底

既然栈可以用数组和链表很简单地实现,那为什么我们还要专门把他实现出来,比如C++, Java等语言基本都把栈结构封装好了的,可以直接使用,我们为什么不在需要用的时候自己写呢,毕竟写起来并不难?

这是因为把栈结构实现出来,有助于我们划分不同的关注层次,有助于缩小我们的思考范围,有助于我们去聚焦于问题的核心。如果没把栈封装好,每次都自己写,不利于代码重用,还需要分散精力去考虑实现细节,掩盖了问题的本质。我们可以从这个问题出发,以小见大,看到封装的
好处,把一些常用的结构或者功能封装好,就可以划分关注层次,聚焦问题核心,减小精力的分散和浪费。

现实应用举例(只要用了先进后出,后进先出思想的都是栈的示例)

就因为后进先出,所以栈也被叫做LIFO结构,那队列自然就是FIFO结构拉

  • 手枪,,这例子牛逼。先放进去的子弹要后被发射出来,后放的子弹却先被发射出来。
  • 浏览器网页回退,先回退到上一个观看的网页,再退到上上个网页,但是上个网页比上上个网页后进来
  • word,photoshop等软件的undo撤回操作

栈的抽象数据类型

ADT Stack
Data//元素具有相同类型,相邻元素具有前驱后继的一对一的线性关系
OperationInitStack(*S);//初始化,建立一个空栈DestroyStack(*S);//销毁栈ClearStack(*S);//清空栈StackEmpty(*S);//是否为空GetTop(*S, *e);//把栈顶元素返回到e指向的内存中Push(*S, e);//把新元素e压栈Pop(*S, *e);//把栈顶元素出栈,放入e指向的内存对象StackLength(*S);
endADT

顺序栈:栈的顺序存储结构,用数组实现

顺序结构的线性表用数组实现,顺序结构的栈当然也使用数组实现。但是用数组哪一端作为栈顶呢?

答案是:下标为0的那端。让栈顶不断增长,用一个top变量表示栈顶元素在数组中的位置

#define SIZE 100
typedef struct
{ElemType data[SIZE];int top;//作为栈顶指针使用
}SqStack;

这里说的指针不是指针类型(指针类型是指向内存的),而是起到一各指向元素的作用,一个形象的说法,此指针非彼指针。这里用了int来起到指针的作用,即指向某个元素,只不过这里指向的不是内存,之前静态链表中使用int类型的cur游标表示指针,和这里类似。

进栈:O(1)

Status Push(SqStack * S, ElemType e)
{if (S->top == SIZE)return ERROR;//满栈S->data[S->top] = e;++(S->top);return OK;
}

出栈:O(1)

Status Pop(SqStack * S, ElemType * e)
{if (S->top == 0)return ERROR;--(S->top);*e = S->data[S->top];return OK;
}

两个同数据类型的栈共享空间:缓解数组长度必须事先确定的麻烦,适合于两栈的空间需求相反的情况

想法新颖,思路清奇,用这种手段去尽量避免数组长度设置不合理带来的痛苦。真的还是很聪明的,为了克服问题,什么办法都能想出来,非常有技巧性。

这个图特别形象:两个数组的端点一起向中间延伸,延伸的终止条件就是两个栈顶指针相遇(top1 + 1 == top2)。
在这里插入图片描述
这种共享很适合用于两个栈的空间需求基本相反的情况,即一个增加则另一个一般会减少,这样则可以最大化利用这个数组的空间。如果两个栈的空间需求一样,两个都同时增加或者同时减少,那要么很快就溢出要么句都空置,和单个栈不共享没有分别。

typedef struct
{ElemType data[SIZE];int top1;//栈1的指针从0开始增长,直到SIZEint top2;//栈2的指针从SIZE-1开始下降,直到-1
}SqDoubleStack;

一个指针增大,一个减小,但都指向各自栈的栈顶,即下一个可存储元素的位置

进栈

Status Push(SqDoubleStack *S, ElemType e, int StackNumber)
{if (S->top1+1 == S->top2)return ERROR;//栈满if (StackNumber == 1)S->data[(S->top1)++] = e;else if (StackNumber == 2)S->data[(S->top2)--] = e;return OK;
}

出栈

Status Pop(SqDoubleStack * S, ElemType * e, int StackNumber)
{if (StackNumber == 1){if (S->top1 == 0)return ERROR;//栈1是空栈*e = S->data[--(S->top1)];return OK;}else if (StackNumber == 2){if (S->top2 == SIZE - 1)return ERROR;//栈2是空栈*e = S->data[++(S->top2)];return OK;}
}

链栈: 栈顶结点替代头结点;不会满栈

栈只需在一端插入和删除,选择链表的头部还是尾部呢?头部有一个头结点,那何不把头结点和栈顶结点合二为一呢?所以链式栈不需要链表的头结点。

由于用链表实现栈,插入新结点会动态分配新内存,所以不会出现满栈情况,除非堆内存真的被程序用完了。

在这里插入图片描述

typedef struct StackNode//栈结点
{ElemType data;struct StackNode * next;
}StackNode, *LinkStackPtr;//LinkStackPtr是struct StackNode *,指向StackNode结点的指针类型typedef struct LinkStack//链栈
{LinkStackPtr top;int count;//链栈的元素个数
}LinkStack;

进栈:O(1)

进栈出栈都没有循环,时间复杂度都是O(1)

Status Push(LinkStack *S, ElemType e)
{LinkStackPtr s = (LinkStackPtr)malloc(sizeof(StackNode));s->data = e;s->next = S->top;S->top = s;++(S->count);return OK;
}

出栈:O(1)

Status Pop(LinkStack * S, ElemType * e)
{if (S->count == 0)//空栈return ERROR;*e = S->top->data;LinkStackPtr p = S->top;//改变栈顶前保存当前栈顶,便于后面释放S->top = S->top->next;//栈顶改为原来的第二个元素--(S->count);free(p);return OK;
}

链栈 VS 顺序栈

  1. 时间上: 进栈出栈的时间复杂度一样,均为O(1)
  2. 空间上:
  • 顺序栈
    优点:存取时定位很方便
    缺点:需要实现确定数组大小,可能浪费空间或者不够用, 不够用则容易导致栈溢出 stack overflow
  • 链栈
    优点:栈的长度没有限制
    缺点:要求每个元素都有指针域,增加了空间开销

二者进栈出栈都无需移动元素

所以如果栈的使用过程中大小不怎么变化且长度是基本可以预测的,就用顺序栈,否则就用链栈

栈的应用

看了这两个应用之后才发现,栈这种数据结构是多么地有用,并不只是咱们自己写代码时有用,而且计算机领域已经很多地方都使用了栈的原理和结构了,比如函数调用,表达式求值。都是非常常见但又不引人注意的地方,原来都隐藏了栈的身影!

递归(函数调用自己,调用自己和调用别的函数并没有什么不同)

但是递归最怕的就是无穷递归,永不结束。所以,递归最重要的就是要至少有一个递归结束条件,一旦满足这个条件就不再调用自己,而是返回上一级并传返回值。

经典递归例子:斐波那契数列 Fibonacci

在这里插入图片描述
在这里插入图片描述

迭代式代码,使用循环结构

#include <iostream>
int main()
{int i;int a[20];a[0] = 0;a[1] = 1;std::cout << a[0] << '\n';std::cout << a[1] << '\n';for (i = 2; i < 20; ++i){a[i] = a[i - 1] + a[i - 2];std::cout << a[i] << '\n';}return 0;
}
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181

递归式代码,使用选择结构

递归的优点:使得程序结构更加清晰,简洁,易于理解
缺点:每一级递归调用都会建立一个函数的副本,需要耗费大量的时间和空间。

递归调用中有前进和回退两个阶段。回退阶段的顺序是前进阶段的逆序。前行阶段,每一层递归的函数副本的局部变量,参数值,返回地址都被压栈,回退阶段里,位于栈顶的函数副本的局部变量,参数值和返回地址被弹出,以恢复调用层次中执行的其余部分,所以实际上编译器是使用栈来实现递归的。只不过对于高级语言来说,这个栈不需要程序员自己管理,编译器和系统会管代劳。
在这里插入图片描述

#include <iostream>
int Fibo(int n)
{if (n < 2)return n==0 ? 0: 1;return Fibo(n-1) + Fibo(n-2);
}
int main()
{int i;for (i = 0; i < 20; ++i)std::cout << Fibo(i) << '\n';return 0;
}

四则运算表达式求值(原来如此!)

对于有括号的表达式:遇到左括号就入栈,遇到右括号就让栈顶左括号出栈

栈顶左括号出栈前的数字们参与运算

没括号表达式:逆波兰表示法reverse Polish Notation RPN

也叫做后缀表示法,因为所有的符号都在数字后面出现,于是就不需要用到括号了,人类看这种表达式是很不方便,但是 计算机很喜欢

逆,是因为每次运算时,都是要出栈栈顶的两个数,且第一个出栈的作为第二操作数,所以是逆序的。

波兰,是因为发明这个表示法的科学家是在这里插入图片描述

示例

原表达式(这种平时咱们使用的标准四则运算表达式叫做中缀表达式):
在这里插入图片描述
其后缀表达式(用后缀表示法表示的中缀表达式就是后缀表达式):
在这里插入图片描述

计算规则:

在这里插入图片描述

计算过程:
在这里插入图片描述
在这里插入图片描述
注意第一个出栈的数作为减数,第二个出栈的是被减数
在这里插入图片描述
然后是符号/,第一个出栈的2作为除数,第二个出栈的10是被除数,两者相除得到5,入栈。

然后是符号+,第一个出栈的是5,第二个是15,相加得到20,进栈。

后缀表达式遍历完毕,出栈20作为结果,栈为空。

如何把中缀表达式转为后缀表达式

平时咱们书写的表达式就是中缀表达式,比如
在这里插入图片描述
刚才展示了如何利用栈把后缀表达式的结果计算出来,通过例子可以看到确实是很方便计算,不担心括号了,加减乘除的计算顺序也处理得很正确。但是如何把中缀表达式转为后缀表达式呢?

答案就在下面这段话中。
在这里插入图片描述

即只有符号会进栈出栈,数字不会进栈

在这里插入图片描述在这里插入图片描述
然后数字1输出,表达式变为 9 3 1
然后遇到符号“)”,右括号,所以要去匹配之前的左括号,于是把栈顶元素依次出栈并输出,直到第一个左括号出栈为止。所以先出栈第一个“-”,表达式变为9 3 1 -, 然后出栈左括号,但是注意左括号出栈但不进入表达式,他只是和右括号匹配上了就好了。

然后是符号“ ? * ”,它比栈顶符号“+”的优先级高,所以直接进栈。

然后是数字3,输出。表达式变为:9 3 1 - 3

然后遇到“+”, 它比当前栈顶符号“*”的优先级低,所以栈中符号依次出栈,直到栈顶符号优先级低于新遇到的这个“+”,但是当前栈中只有两个符号,从栈底到栈顶分别是“+”和“ ? * ”,没人比新遇到的“+”优先级低,所以全部出栈并输出。表达式变为:9 3 1 - 3 ? * +。然后再把当前新的“+”入栈。

然后是数字10,输出,表达式:9 3 1 - 3 ? * + 10

然后是符号“ / / ”,除号优先级高于栈顶符号“+”,进栈。

最后一个数字2,输出,表达式变为:9 3 1 - 3 ? * + 10 2

由于中缀表达式遍历完毕,所以栈中符号全部出栈并输出,表达式变为:9 3 1 - 3 ? * + 10 2 / / +

总结发现,计算机计算表达式的两个关键步骤都要用到栈,中缀转后缀时栈中存符号;后缀计算结果时栈中存数字。之前还真不知道计算机原来是这么计算表达式的,只是直到肯定要想个办法去计算,原来用了后缀表达式。
在这里插入图片描述

  相关解决方案