戳一戳!和我一起走进信息学的世界
导读
信息学能够有助于孩子未来工作发展,提升孩子的综合能力。
从这节课开始我们走进数据结构的世界,了解栈和队列的理论和代码实战,栈是什么结构呢?栈有什么特点呢?栈怎么实现呢?让我们走进文章一起了解一下吧!
往期回顾
【NOIP竞赛/CSP认证】
▶ 赛前必看!信息学名师带你复习NOIP竞赛初赛及CSP认证初赛
【信息学精华帖】
▶ 收藏!交流会内容全公开,让你陪孩子更好地学习信息学
▶ 信息学的万般好处!附C++必备基础知识总结
▶ 信息学提高班知识体系详解与家长常见问题解答!让孩子赢在提高班学习的起跑线!
▶ 再回首,最全提高班知识总结,做更优秀的自己!
▶ 早知道!信息学集训班大揭秘!你想知道的的都在这!
【信息学集训】
▶ 01 温故知新,以更好状态学习数据结构和算法
▶ 02 信息学初赛必备计算机知识大串讲
▶ 03 位运算与进制初步
▶ 04 进制进阶与编码
▶ 05 字符串进阶操作
【数据结构前导课】
▶ 1 温故知新——一篇文章领略信息学C++知识结构
▶ 2 披荆斩棘——只学C++,可以做哪些竞赛题
▶ 3 运筹帷幄——一篇文章,让指针学起来也很简单!
▶ 4 初试锋芒——顺序表写起来也很简单
▶ 5 小试牛刀——STL库之vector数组
▶ 6 触类旁通——链表基本理论与信息学竞赛必考点
▶ 7 更进一步——STL库之List链表
▶ 8 知“人”善任——深入理解顺序表和链表的区别与应用
【C++提高班教程】
▶ C++强化 | 01 新学期再出发!温故知新!
▶ C++强化 | 02 继续前行,三大结构终极介绍
▶ C++强化 | 03 一维数组入门
▶ C++强化 | 04 数组越界
▶ C++强化 | 05 一维数组经典应用
▶ C++强化 | 06 一篇文章带你掌握字符数组
▶ C++强化 | 07 二维数组
▶ C++强化 | 08 二维数组经典案例
▶ C++强化 | 09 一篇文章带你探索函数的奥秘
▶ C++强化 | 10 函数进阶必备
▶ C++强化 | 11 这样学递归,才不会觉得难
▶ C++强化 | 12 格式化输入输出与文件操作
▶ C++强化 | 13 结构体入门
▶ C++强化 | 14 结构体进阶
【C++基础班教程】
▶ C++总结 | 01 程序的世界
▶ C++总结 | 02 输出、换行与注释
▶ C++总结 | 03 变量定义、赋值与运算
▶ C++总结 | 04 算术运算符与赋值运算符
▶ C++总结 | 05 cin语句
▶ C++总结 | 06 程序中的数据类型
▶ C++总结 | 07 数据类型补充
▶ C++总结 | 08 顺序结构
▶ C++总结 | 09 if 和 if-else
▶ C++总结 | 10 if嵌套与逻辑运算符
▶ C++总结 | 11 开关语句switch-case
▶ C++总结 | 12 for循环及其应用
▶ C++总结 | 13 数据范围与数据类型
▶ C++总结 | 14 break与continue
▶ C++总结 | 15 while与do-while
▶ C++总结 | 16 循环嵌套及其应用
1 栈-引入
前面我们讲了有关于数据结构的相关知识,讲了顺序表和链表。
这些都是数据结构中最基本的内容,我们通过顺序表和链表能够更好地理解顺序结构和链式结构。
这节课开始我们要学习受限制的线性表。这节课我们来讲栈。我们通过一个例子来理解
我们现在有一个桶,我们把砖块依次放进去,我们按照ABC的顺序依次放,就如下图所示:
我们发现,先放的砖块放在底下,后放的砖块放在上面。如果我们想取出来,得从上往下依次取。先取C,再取B,最后取A。
2 栈
上面的这个小例子,就是我们今天要讲的主要内容,接下来我们一起来看一下吧!
1 什么是栈
通过上面的示例,我们能够发现,这种结构只能从某一端插入或者删除数据。
这种结构就是栈,栈是一种操作受限制的线性表,普通的栈只能在某端插入或者删除数据。不能在另一端操作数据(包括:插入,删除,访问,修改等操作。),也不能在中间部分操作数据。
与栈有关的概念如下:
允许插入、删除等数据操作的一端,我们称之为栈顶;另一端称之为栈底。
插入数据的操作称之为进栈(或压栈);删除数据的操作称之为出栈(或退栈)。
当栈中没有元素的时候,我们称之为栈空;当栈中元素占满了栈空间,我们称之为栈满。
2 栈结构特点
我们研究栈结构特点,以普通栈结构为例,在实际项目中,由于项目需要,可能会提出一些特殊栈结构,但基本也是普通栈的简单调整,如双端栈,是将两个栈的栈底拼接起来,可以两端插入或者删除数据。
由于只能从一端进或者出,每次进栈的数据会成为新的栈顶,每次出栈的数据是栈顶的数据,栈顶元素相对于其他栈中元素,是后进入的,但是是最先出去的,所以栈结构的特点为“后进先出”。
3 入栈出栈顺序
根据栈结构特点,一类非常有趣的题目就出现了,即根据入栈情况推测可能的出栈情况或者根据出栈情况推测可能的入栈情况。我们通过两个例子来分析一下。
1、可能的出栈情况
若元素:1、2、3的进栈顺序为123,可能的出栈情况有:
第一种情况,1进去,2进去,3进去,3出来,2出来,1出来。出栈顺序为:321。
第二种情况,1进去,2进去,2出来,3进去,3出来,1出来。出栈顺序为:231。
第三种情况,1进去,1出来,2进去,2出来,3进去,3出来。出栈顺序为:123。
第四种情况,1进去,1出来,2进去,3进去,3出来,2出来。出栈顺序为:132。
第五种情况,1进去,2进去,2出来,1出来,3进去,3出来。出栈顺序为:213。
1、可能的进栈情况
若元素:1、2、3的出栈顺序为123,分析和上面一样,需要一定的逆向思维,可能的进栈情况有:
第一种情况,1进去,1出来,2进去,2出来,3进去,3出来。进栈顺序为:123。
第二种情况,3进去,1进去,1出来,2进去,2出来,3出来。进栈顺序为:312。
第三种情况,3进去,2进去,1进去,1出来,2出来,3出来。进栈顺序为:321。
第四种情况,1进去,1出来,3进去,2进去,2出来,3出来。进栈顺序为:132。
第五种情况,2进去,1进去,1出来,2出来,3进去,3出来。进栈顺序为:213。
掌握了上面这些,以后我们需要具体的题目就不用担心了,比如我们看下面这道真题:
【2017提高组初赛-不定项选择题】对于入栈顺序为 a, b, c, d, e, f, g 的序列,下列不可能是合法的出栈序列是()。
A、 a,b,c,d,e,f,g
B、 a,d,c,b,e,g,f
C、 a,d,b,c,g,f,e
D、 g,f,e,d,c,b,a
对于A选项,我们知道入栈和出栈一样,那就是入栈一个元素,出栈一个元素,所以A选项是合法的出栈序列。
对于B选项,先a入栈,然后a出栈,然后bcd入栈,然后dcb逆序出栈,然后e入栈,然后e出栈,然后fg入栈,最后gf逆序出栈。所以B也是合法出栈序列。
对于C选项,先a入栈,然后a出栈,然后bcd入栈,然后d逆序出栈,接下来我们知道,栈中元素为bc,如果c没有出栈,b就无法出栈,所以先出b,后出c是错误的,所以C选项不是合法的出栈序列。
对于D选项,入栈和出栈序列相反,那就是一起入栈,然后一起出栈。所以D选项也是合法的序列。
综上,只有C选项是不合法的出栈序列,这道题目选择C。
3 栈操作
上面的内容,就是初赛会涉及到的内容,下面我们来考虑代码,也就是复赛会涉及到的内容!
我们可以使用顺序结构来实现栈,也可以用链式结构来实现栈;因为我们是做信息学竞赛,不是做项目,做工程,所以,我们追求的是简洁。顺序结构要比链式结构更简洁,所以我们具体实战中,考虑的就是顺序栈结构,也就是顺序结构的栈结构。
1 栈的定义与初始化
首先我们要先定义栈,并能初始化一个栈。
栈是某一端做操作,这一端叫栈顶,所以我们需要一个顺序结构来存储栈数据,还需要一个指向栈顶元素的指针,我们可以使用这个指针操作栈顶元素。
我们使用数组来当作顺序结构存储数据,用整型变量top当作栈顶指针。
struct Stack { //栈stack,stk
int data[10];
int top;
};
定义栈结构变量,和普通的结构体定义变量是一致的。
Stack S;
有了栈结构,我们就可以定义栈结构变量,并写初始化函数了。初始化函数有很多写法,可以让top = 0,也可以让top = -1。如果top = 0,我们一般都表示栈顶指针指向的是栈顶元素的后一个位置(指向空位置)。如下图:
对于这种写法,初始化函数这么写:
void initStk(Stack &S){
S.top = 0;
}
对于另外一种情况,栈顶元素就指向栈顶位置,如果栈顶指针指向0,就表示索引为0的位置是有数据的,那如果想要表示空,就要让栈顶指针=-1。
当然,有些人不习惯这种用-1的写法,就可以让栈顶指针为0的时候表示空,栈顶指针为1的时候表示第一个元素。这种写法,容易理解,但是会浪费掉索引为0的空间。就和数组中,让索引为1的位置存放第一个数据一样。
这三种方法,没有谁对谁错,只要大家能理解其逻辑,写对对应的代码就可以了。我们这里以第一种情况为例。即:top = 0时,栈为空,top指针永远指向栈顶元素后一个位置。
2 栈的置x空与判空
前面我们知道,top指向0表示栈为空,所以我们将栈置空,就是将top指针变为0,如果想判断空,就判断top是否为0即可,判空我们可以使用这种最简单的方法:
void clear(Stack &S){ //置空
S.top = 0;
}
bool isEmpty(Stack S){ //判空
return !S.top;
}
3 求栈顶元素
想获取栈顶元素,我们需要通过top指针,top指向的是栈顶元素的后一个位置,所以我们想获取栈顶元素,就要访问top-1的位置。
int getTop(Stack S){
return S.data[S.top-1];
}
4 进栈
进栈,就是插入一个新的元素,因为栈顶指针指向的是栈顶元素的后一个位置,也就是未来要插入的位置,所以我们进栈,就是给栈顶指针所指向的位置赋值。然后,栈顶指针要继续指向新栈顶元素的后一个位置,也就是让top++。
void push(Stack &S, int e){
S.data[S.top] = e;
S.top++;
}
注意,从工程角度来讲,我们入栈之前是要做判断的, 判断是否栈满,如果栈满,要重新分配更大的空间。但是从信息学竞赛的角度来讲,我们考虑的是算法逻辑和算法复杂度,不考虑这些特殊情况,所以不用写。
5 出栈
出栈只需要将栈顶指针减一即可。
void pop(Stack &S){
S.top--;
}
注意,从工程角度来讲,我们出栈之前是要做判断的, 判断是否栈空,如果栈空,就不能出栈了。但是从信息学竞赛的角度来讲,我们考虑的是算法逻辑和算法复杂度,不考虑这些特殊情况,所以不用写。
一般来说,我们自己写的代码要避免栈空和栈满的情况。
6 判断栈满
如果栈满,栈中最后一个数据的索引是栈容量-1,(因为栈索引是从0开始的)。又因为栈顶指针指向位置是栈顶元素索引后一位,所以栈顶指针指向位置 = 容量的时候,就是栈满。简单来说,栈容量使我们自己定义的一个常量,比如我们例子中的10。
bool isFull(Stack S){
return S.top == 10; //具体容量看自己代码情况
}
7 求栈元素个数
当我们只有一个元素的时候,栈顶元素所在位置是0,栈顶指针指向的位置是索引为1的位置。所以,指针的值,就是栈元素个数:
int getSize(Stack S){
return S.top;
}
4 练习
我们做一个练习,应用一下上面的代码!
1 栈元素逆序
栈中存放了五个元素:1,2,3,4,5,将这五个元素逆序存放在该栈中。要求只能用栈结构来做数据转换(也就是说从栈中取出的数据,要么删除,要么放到另一个栈中)。
思路:我们发现,从一个栈到另一个栈会将元素逆序,但是再存回原栈中,还是原来的顺序,我们再找一个辅助栈,也就是一共找两个辅助栈,就可以实现逆序了,如下图:
所以我们只要根据这个原则进行进栈和出栈即可,代码如下。
#include<iostream>
using namespace std;
struct Stack { //栈stack,stk
int data[10];
int top;
};
void initStk(Stack &S){
S.top = 0;
}
void clear(Stack &S){ //置空
S.top = 0;
}
bool isEmpty(Stack S){ //判空
return !S.top;
}
int getTop(Stack S){
return S.data[S.top-1];
}
void push(Stack &S, int e){
S.data[S.top] = e;
S.top++;
}
void pop(Stack &S){
S.top--;
}
bool isFull(Stack S){
return S.top == 10; //具体容量看自己代码情况
}
int getSize(Stack S){
return S.top;
}
int main(){
//1、定义并初始化
Stack S1,S2,S3;
initStk(S1);
initStk(S2);
initStk(S3);
if(isEmpty(S1)) cout<<"插入前:空栈"<<endl;
else cout<<"插入前:非空栈"<<endl;
//2、元素输入
for(int i =1;i<=5;i++){
push(S1,i);
}
if(isEmpty(S1)) cout<<"插入后:空栈"<<endl;
else cout<<"插入后:非空栈"<<endl;
//据输出,只是为了验证交换的正确性,正常是不能这样访问栈数据的。
cout<<"交换前:";
for(int i =0;i<5;i++){
cout<<S1.data[i]<<" ";
}
cout<<endl;
//3、数据交换
for(int i =1;i<=5;i++){
pop(S1);
push(S2,S1.data[S1.top]);
}
for(int i =1;i<=5;i++){
pop(S2);
push(S3,S2.data[S2.top]);
}
for(int i =1;i<=5;i++){
pop(S3);
push(S1,S3.data[S3.top]);
}
//数据输出,只是为了验证交换的正确性,正常是不能这样访问栈数据的。
cout<<"交换后:";
for(int i =0;i<5;i++){
cout<<S1.data[i]<<" ";
}
cout<<endl;
return 0;
}
注意,这里面的输出方式是不对的,正常的栈,是没有索引可以直接从中间访问数据的,只能通过栈顶指针访问数据,想要访问数据,就要一直让栈顶指针-1,然后访问这个时候的栈顶数据。我们这里使用只是为了验证交换的正确性,正常是不需要的。
6 作业
本节课的作业,就是复习上面的所有知识,并完成下面题目!
1 括号匹配
判断下面的括号是否是匹配的,可能有的括号为大括号、中括号和小括号:
{}{}[]()
((([[])]))
{}[]()(())
上面的括号中,第一个和第三个是匹配的,第二个是不匹配的。
AI与区块链技术
长按二维码关注