其实无论是栈还是队列,实现的物理结构都是顺序表或者是链表
不过是“栈顶”,“队头”,“队尾”等变量赋予了这块存储空间(线性表)不同的使用方法
当然这种使用方法是方便于程序员解决问题
一.栈
1.栈基本定义
- 什么是栈(Stack)?:只允许在一端进行插入或者删除的线性表
- 有什么特点?:后进先出LIFO
- 什么是栈顶?:允许进行插入删除的一端。什么是栈底?:不允许进行插入删除的一端
- 什么是卡特兰数?:有n个不同元素进栈,出栈元素不同排列个数为:
2.栈基本操作
#define MaxSize 10//定义栈中最大元素的个数
typedef struct
{
int data[MaxSize];//初始化一片连续的栈空间
int top;//栈顶指针,指向数组序号
}SqStack;
//初始化栈
void InitStack(SqStack& S)
{
S.top=-1;//初始栈为空
}
//判断栈空
bool IsEmpty(SqStack S)
{
if(S.top==-1)//栈空
return true;
else
return false;
}
//新元素入栈
bool Push(SqStack& S,int e)
{
if(S.top==MaxSize-1)//栈顶指针指的位置是有元素的位置,如果指向maxsize-1那么栈满
return false;
S.data[++S.top]=e;
return true;
}
//新元素出栈
bool Pop(SqStack& S,int& x)
{
if(S.top==-1)//栈中没有元素无法出栈
return false;
x=S.data[S.top];//先把栈顶元素赋值
S.top--;//栈顶指针下移,上下两个操作取决于top初值是0还是1
return true;
}
//读栈顶元素操作
bool GetTop(SqStack S,int& x)
{
if(S.top==-1)//栈中没有元素无法读取
return false;
x=S.data[S.top];//把栈顶元素赋值
return true;
}
3.共享栈
- 什么是共享栈?:两个栈共享同一片存储空间,使用两个栈顶指针, 逻辑上是两个栈0和1,但是物理上是一片栈空间。
- 共享栈满的条件是?:top0+1==top1
- 如何初始化?:
typedef struct
{
int data[MaxSize];
int top0;//0号栈栈顶指针
int top1;//1号栈栈顶指针
}SqStack;
//初始化栈
void InitStack(SqStack& S)
{
S.top=-1;//初始栈为空
S.top1=MaxSize;
}
4.链栈
二.队列
1.队列的基本概念
- 什么是队列?:只允许在一端进行插入,在另一端进行删除的线性表
- 队列的特点?:先进先出FIFO
- 什么是队头?:允许进行删除的一端。什么是队尾?:允许进行插入的一端。
- 如何判断循环队列中有多少个元素?:(rear+MaxSize-front)%MaxSize
- 如何在不浪费存储空间的情况下判断队列有多少个元素?:在定义的时候定义一个size标记队列中元素的个数;或者使用tag来标记最近一次操作时插入还是删除。
2.队列的基本操作
#define MaxSize 10
typedef struct
{
int data[MaxSize];//定义一片连续的存储空间
int front,rear;//队头指针和队尾指针
}SqQueue;
//初始化队列
void InitQueue(SqQueue& Q)
{
Q.front=Q.rear=0;//初始时队头=队尾指针指向0
}
//判断队列是否为空
bool QueueEmpty(SqQueue Q)
{
if(Q.rear==Q.front)
return true;
else
return false;
}
//入队,为了真正考虑队满或者队不满,将队列看为循环队列
bool EnQueue(SqQueue& Q,int x)
{
if((Q.rear+1)%MaxSize==Q.front)//表示队列满,不让rear和front指向同一个位置是为了和队列空区分
return false;
Q.data[Q.rear]=x;//入队
Q.rear=(Q.rear+1)%MaxSize;//队尾指针+1取模
return true;
}
//出队, 队头元素移出之后返回队头
bool Dequeue(SqQueue &Q,int& x)
{
if(Q.rear==Q.front)
return false;//队空则报错
x=Q.data[Q.front];//赋值
Q.front=(Q.front+1)%MaxSize;
return false;
}
//获取队头元素
bool GetHead(SqQueue Q,int &e)
{
if(Q.front==Q.rear)
return false;//队列满
x=Q.data[Q.front];//返回
return true;
}
3.链队列:带头节点和不带头节点
4.双端队列
- 什么是双端队列?:只允许两端插入,两端删除的线性表
- 什么是输入受限的双端队列?:只允许一端插入,两端删除的线性表
- 什么是输出受限的双端队列?:只允许两端插入,两端删除的线性表
三.栈和队列的算法应用实例
1.括号匹配
- 括号匹配应用什么场景?:比如使用IDE编写代码的时候,IDE会自动检测括号匹配高亮问题
- 括号匹配算法流程?:使用栈来存放检测到的符号,依次检测符号串,如果是左括号则进栈,右括号则弹出栈顶元素进行匹配。匹配成功则进行下一个检测,匹配失败则算法停止;如果没有检测完毕但是栈空,则算法停止,匹配失败;如果检测完毕但是栈未空,算法停止,匹配失败。
bool bracketCheck(char str[],int length)
{
stack<char> Stack;//初始化栈
for(int i=0;i<length;++i)
{
if(str[i]=='('||str[i]=='{'||str[i]=='[')
Stack.push(str[i]);//左括号入栈
else
{
char out=Stack.top();//弹出栈顶左括号进行检查
if((out=='['&&str[i]!=']')||(out=='('&&str[i]!=')')||(out=='{'&&str[i]!='}'))
return false;//如果左右括号不匹配停止检查
}
}
return Stack.empty();//如果栈空,返回true,否则返回false
}
2.表达式求值问题
- 什么是中缀表达式?:运算符在两个操作数中间 a+b
- 什么是逆波兰表达式(后缀表达式)?:运算符在两个操作数后面 a b+
- 什么是波兰表达式(前缀表达式)?:运算符在两个操作数前面
- 如何用栈实现后缀表达式的计算?:从左往右扫描下一个元素,直到处理完所有元素,设置一个栈存放操作数;若扫描到操作数则压入栈,并回到第一步,否则执行下一步;若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到第一步。如果表达式合法,最终栈的元素就是结果。
- 中缀表达式转后缀表达式?:
- 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
- 从左到右处理各个元素,直到末尾。遇到操作数,直接加入后缀表达式
- 遇到界限符,遇到“(”直接入栈,遇到“)”则依次弹出栈内运算符,并加入后缀表达式,直到弹出“(”为止,但是“(”不加入后缀 表达式
- 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若遇到“(”或栈空则停止,之后再把当前运算符入栈。
- 如何用栈实现中缀表达式的计算?:
- 初始化两个栈,操作数栈和运算符栈
- 若扫描到操作数,则压入操作数栈
- 若扫描到运算符或者界限符,则按照中缀转后缀的逻辑压入运算符栈。(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
3.栈在递归中的应用
- 函数调用的特点?:最后被调用的函数最先执行结束(LIFO)
- 递归调用有什么缺点?:太多层递归可能会导致栈溢出,递归空间复杂度很高,递归太多层可能会出现重复计算的情况。