一、栈
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈最直观形象的模型为弹匣“压弹”、“退弹” ,栈是一种遵循“先进后出”的出栈原则
一、栈操作数据元素的方法
- 数据元素用栈的数据结构存储起来,称为“入栈”,也叫“压栈”。
- 数据元素由于某种原因需要从栈结构中提取出来,称为“出栈”,也叫“弹栈”
二、 栈的两种表示方式
因为栈也属于线性表,故其也有链式结构,称为“链栈”,本篇只针对顺序栈
三、栈的“上溢”和“下溢”
上溢”:在栈已经存满数据元素的情况下,如果继续向栈内存入数据,栈存储就会出错。
“下溢”:在栈内为空的状态下,如果对栈继续进行取数据的操作,就会出错。
四、顺序栈的表达
//栈
//@date 8.18
//----------ADT Stack 的表示与实现------------
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
#define StackInitSize 100 //初次空间分配大小
#define StackIncrement 10 //空间增量大小
typedef struct {
int *base;//栈底指针
int *top;//栈顶指针
int stacksize;//stack指示栈的当前可使用的最大容量
}SqStack;
//构造一个空栈
void InitStack(SqStack &S)//S为栈
{
S.base = (int*)malloc(StackInitSize*sizeof(int));//为栈开辟空间.且S.base得到的是开辟空间的首地址
if(!S.base)//S.base指针始终指向栈底,若哦S.base = NULL则说明栈不存在
{
cout<<"error-1,内存空间开辟失败"<<endl;
exit;
}
S.top = S.base;//空栈,top为栈顶指针,其初值指向栈底,当插入新的栈顶元素时,top+1
/*
if(!S.base)
{
cout<<"duigqwigvwuiqeguiwegv"<<endl;
}
*/
S.stacksize = StackInitSize;//栈的最大容量为定义的StackInitSize
if(S.top == S.base)
{
cout<<"空栈构建成功"<<endl;
}
else
cout<<"空栈构建失败"<<endl;
}
//若栈存在,则销毁栈
void DestroyStack(SqStack &S)
{
if(S.base != NULL) free(S.base);
else cout<<"栈不存在,不需要进行销毁"<<endl;
if(S.base == NULL) cout<<"栈销毁成功"<<endl;
else cout<<"栈销毁失败"<<endl;
}
//判断栈是否为空,若栈不空则用e返回S的栈顶元素
void GetTop(SqStack S,int e)
{
/*
注意栈为空与栈不存在不同,前者只是未赋值
栈为空的判断方式 S.base == S.top
栈不存在的判断方式 S.base == NULL
*/
if(S.base == S.top)
{
cout<<"栈为空"<<endl;
exit;
}
cout<<"栈非空"<<endl;
//在非空栈中的栈顶始终在栈顶元素的下一个位置上
e = *(S.top-1);
}
//把栈置为空栈
void ClearStack(SqStack &S)
{
if(S.base != S.top)
{
cout<<"栈为非空"<<endl;
S.base = S.top;
}
if(S.base == S.top) cout<<"栈置空成功"<<endl;
else cout<<"置空失败"<<endl;
}
//若栈非空则返回真,否则则返回假
bool StackEmpty(SqStack S)
{
if(S.base == S.top)
{
cout<<"栈为空"<<endl;
return true;
}
else return false;
}
//返回S的元素个数,即栈的长度
int StackLength(SqStack S)
{
return S.top - S.base;
}
//插入e元素为新的栈顶元素,压栈操作,即对栈进行赋值操作
void Push(SqStack &S,int e)
{
//当栈满时候
if(S.top-S.base >= S.stacksize)
{
/*
开辟空间首先释放原内存空间S.base,再将内存空间在原基础上增加StackIncrement的4倍
sizeof(int*) 64,32位均为4位
*/
S.base = (int*)realloc(S.base,(S.stacksize+StackIncrement)*sizeof(int*));
if(S.base == NULL)
{
cout<<"内存空间开辟失败"<<endl;
exit;
}
/*
如果在追加空间的时候改变了指针S.base的值,那么必须保证S.top也作响应的改变
*/
S.top = S.base+S.stacksize;//在重新开辟空间时,与顺序表类似要重新定位指针的位置,即S.top要上移
S.stacksize+=StackIncrement;//需要注意的是,在每次重新分配内存空间时,其指针与数据的内存结构都要发生改变
}
cout<<"栈顶e的元素值为"<<endl;
scanf("%d",&e);
//cin>>e;
*S.top++ = e;//先执行*S.top=e,再S.top++,即指针上移
}
//获取栈顶元素
//若栈非空,则用e返回其值
void Pop(SqStack &S,int e)
{
if(S.base == S.top)
{
cout<<"栈空,无法返回其值"<<endl;
exit;
}
e = *--S.top;//运算符优先级为++(--) 然后是* 最后是=,所以--S.top先运算, 其次是*(--S.top),最后是e=*(--S.top)。
cout<<"栈顶元素e值为"<<e<<endl;
}
//对栈的函数调用测试
int main()
{
SqStack S;
int e;
//free(S.base);
//构建个新的空栈
InitStack(S);
//判断栈是否为空
StackEmpty(S);
//返回栈的长度
cout<<"当前栈长度为"<<":"<<StackLength(S)<<endl;
Push(S,e);
//返回赋值后的栈长度
cout<<"当前栈长度为"<<":"<<StackLength(S)<<endl;
Pop(S,e);
ClearStack(S);
StackEmpty(S);
}
二、栈的应用 进制转换 迷宫求解 表达式求值
一、进制转换
这个比较简单,取余数发现规律即可,此段代码是引入了c++ STL库,相同的编写完操作函数后,引入头文件,也可以达到相同的目的
//stcak库进行进制转换
#include <iostream>
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <stack>
using namespace std;
int main()
{
int N;
stack<int>S;
scanf("%d",&N);
while(N)
{
S.push(N%8);
N = N/8;
}
while(!S.empty())
{
printf("%d",S.top());
S.pop();
}
return 0;
}
二、迷宫求解
这个问题起初我很困惑,不理解为什么迷宫求解要利用栈,仔细想想,迷宫求解的目的就是利用“穷举法”在所有路径中正确走出迷宫后将正确的路径输出,这一点非常符合栈“先进后出”的特点。
编写此程序时应先构出流程图,这点是非常重要的
/*----------------主要思路----------------
首先需要确定的是循环主体为while,循环终止条件为栈不空,只要栈不空就一直执行,当栈为空时代表找到出口
1、初始化:创建结构体与贪吃蛇相同需要首先确定当前位置与入口位置
2、if(若可通) do(将当前位置进行压栈,若为出口则结束,否则进行切换位置,将右邻的位置作为新的位置)
3、else({if(若栈不空且除东邻外还有其他位置未进行搜索):切换方向进行下一次寻找}
if(若栈不空但栈顶的四周不通):删去此时的栈顶位置,重新测试新的栈顶位置,直至找到可通的相邻块或出至栈空)
----------------主要思路----------------*/
1、构建保存的数据结构体
//栈中的数据元素
typedef struct{
int x,y;
int di;//di为方向变量
}Box;
//创建结构体栈变量,为保存坐标与方向做准备
stack<Box>S;
//初始化
Box temp;
temp = {1,1,-1};
2、构建方向变量
构建方向变量的目的很明确,对其变量数值的改变从而达到改变方向的目的,继而完成对每个方向的“寻路”
typedef struct{
int incX;
int incY;
}Direction;
Direction direct[4];
/*----------方向参数---------*/
direct[0] = {0,1};//右
direct[1] = {1,0};//下
direct[2] = {0,-1};//左
direct[3] = {-1,0};//上
/*----------方向参数---------*/
3、构建试探通路坐标元素
其中x,y,di与line,col的关系为x为要压栈的坐标位置, 即x是可通坐标,而line可理解为道路是否可通的试探坐标,对其line,col的改变与迷宫坐标是否为0从而判断道路是否可通,可通则压入,不可通则改变方向
int line,col;
4、完整代码
//寻找路径
bool findPath(int maze[M+2][N+2],Direction direct[],stack<Box>&S)
{
direct[0] = {0,1};//右
direct[1] = {1,0};//下
direct[2] = {0,-1};//左
direct[3] = {-1,0};//上
//Box为要入栈的数据分别为:当前坐标位置x,y 方向dir
Box temp;
//x,y,dir为迷宫格子当前处理的坐标与方向
int x,y,di;
//line,col为迷宫数组的下一单元坐标
int line,col;
//将起始位置设置为-1,则之后每当通过一个单元都将其赋值为-1,进行标记
maze[1][1] = -1;
//栈的初始化
temp = {1,1,-1};
//创建数据类型为结构体的栈
stack<Box>S;
S.push(temp);
while(!S.empty())
{
S.pop();
temp = S.top();
//改变方向,查找通路,方向由direct[]数组值确定
/*其中x,y,di与line,col的关系为x为要压栈的坐标位置,
即x是可通坐标,而line可理解为道路是否可通的试探坐标,对其line,col
的改变与迷宫坐标是否为0从而判断道路是否可通,可通则压入,不可通则
改变方向,若div>4解释如下
*/
x = temp.x;//将退栈后的位置赋给x,y,di即执行后退操作
y = temp.y;
di = temp.di+1;
/*方向未查找完,此while循环目的在于查找四个方向,
若有放向可通(迷宫坐标仅为0时可通,若为-1也不可通),
当di>4时候,证明四个方向都已查找完且无通路则进行退栈,
即退出第二个while循环,进入第一个循环进行退栈
返回上一个位置进行查找,当有通路时候标记-1,赋值temp,压栈
*/
while(di<4)
{
/*改变x,y的位置寻找可通路径,依次寻找,寻找方向为右(0)下(1)左(2)上(3)*/
line = x+direct[di].incX;
col = y+direct[di].incY;
//若当前迷宫坐标为0时候,则可通,赋值temp压栈,记录位置与方向
if(maze[line][col]==0)
{
temp = {x,y,di};
S.push(temp);
//将已通过的路径在迷宫上标记-1
x = line;
y = col;
maze[line][col] = -1;
//---------end--------------
//判断是否到达地图边界,即判断是否走出迷宫
if(x==M&&y==N)
{
return true;
}else
{
di = 0;
return false;
}
}else
di++;
}
}
}
三、队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
1、队列(Queue)与栈一样,是一种线性存储结构,它具有如下特点:
(1)队列中的数据元素遵循“先进先出”(First In First Out)的原则,简称FIFO结构;
(2)在队尾添加元素,在队头删除元素。
2、队列的相关概念:
(1)队头与队尾: 允许元素插入的一端称为队尾,允许元素删除的一端称为队头;
(2)入队:队列的插入操作;
(3)出队:队列的删除操作。
3、队列的操作:
(1)入队: 通常命名为push()
(2)出队: 通常命名为pop()
(3)求队列中元素个数
(4)判断队列是否为空
(5)获取队首元素
4、队列的分类:
(1)基于数组的循环队列(循环队列)
(2)基于链表的队列(链队列)
本篇主要针对链队列进行说明
一、链队列结构的构建
typedef struct QNode//结点结构与链表类似此结构不影响链队列结构
{
int data;
struct QNode *next;//指针结点
}QNode,*QueuePtr;//数据域,指针域
typedef struct //队列的链表结构
{
QueuePtr front;//链队列的头指针
QueuePtr rear;//链队列的尾指针
}LinkQueue;
二、空链队列
//构建空的链队列
bool INitQueue(LinkQueue &Q)
{
//为头指针开辟空间大小为QueuePtr类型大小为QNode节点的一倍
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
if(!Q.front)
return false;
Q.front = Q.rear->next = NULL;
return true;
//让头尾指针指向一个结点,并赋值为NULL
}
三、出队
注意出队只能是队列第一个而不能是中的某一个,思路也比较简单只需将头指针赋给头结点的下一个结点,从而达到“越过”的操作即达到了出队,这个方式与链表的插入思路类似,都是对指针指向的改变
//出队
/*注意出队只能是队列第一个而不能是中的某一个,思路也比较简单只需将头指针赋给头结点的下一个结点,
从而达到“越过”的操作即达到了出队
*/
int DeQueue(LinkQueue &Q,int e)
{
QueuePtr p;
if(Q.front == Q.rear)
return false;
else{
//将头指针指向第一个结点,做好出队准备
p = Q.front->next;
e = p->data;
/*--------core----------*/
Q.front->next = p->next;//将节点的后继节点指针赋给头指针达到“越过”的目的
/*--------core----------*/
if(Q.rear==p)
//若当此时队列只有一个元素,出队将恢复到空队列的状态,故执行Q.front = Q.rear;
Q.front = Q.rear;
free(p);
}
return e;
}
四、入队
与出队类似,入队只能是从尾部入队,即将新插入的结点作为尾结点,尾指针指向该结点,结点指针指向NULL
//入队,插入元素e为新的队尾元素
//Q->next实质与链表头结点一样,均指向第一个结点
bool EnQueue(LinkQueue &Q,int e)
{
QueuePtr p = (QueuePtr)malloc(sizeof(QNode));
if(!p)
return false;
else{
p->data = e;
p->next = NULL;//将p的指针部分赋为NULL,作为尾结点
Q.front->next = p;//将第一个结点指向新插入的结点
Q.rear = p;
}
return true;
}
五、完整代码
//链队列操作函数
//@date: 2022.9.21
//@special:队列添加一个头结点,并令队头指针指向头结点,队尾指针指向链表中最后一个结点
//判断空队列 Q.front->next = NULL
#include <iostream>
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <stack>
#include <string>
using namespace std;
typedef struct QNode//结点结构与链表类似此结构不影响链队列结构
{
int data;
struct QNode *next;//指针结点
}QNode,*QueuePtr;//数据域,指针域
typedef struct //队列的链表结构
{
QueuePtr front;//链队列的头指针
QueuePtr rear;//链队列的尾指针
}LinkQueue;
//构建空的链队列
bool INitQueue(LinkQueue &Q)
{
//为头指针开辟空间大小为QueuePtr类型大小为QNode节点的一倍
Q.front = Q.rear = (QueuePtr)malloc(sizeof(QNode));
if(!Q.front)
return false;
Q.front = Q.rear->next = NULL;
return true;
//让头尾指针指向一个结点,并赋值为NULL
}
//销毁链队列
bool DestroyQueue(LinkQueue &Q)
{
//若链队列存在,front前,rear后
while(Q.front)
{
//头指针依次指向每一个结点,释放其内存
Q.front = Q.rear->next;
free(Q.front);
Q.front = Q.rear;
}
return true;
}
//将Q清空为空队列,销毁结点指针,但保留头尾指针
bool ClearQueue(LinkQueue &Q)
{
QueuePtr q,p;
if(Q.rear->next != NULL)
cout<<"此为非空链队列"<<endl;
//此时要对其队列结点进行改变
while(!Q.front)
{
//结构结点指向头结点的下一个
p = Q.front->next;
Q.front->next = NULL;
//p->data = NULL;
/*--------对其带有数据结点进行的操作-----------*/
/*此时要对带有数据的结点操作应该用结点数据类型定义变量
此时变量类型与队列无关,与链表类似
*/
while (p)
{
p = q->next;
free(p);
q = p;
}
/*--------对其带有数据结点进行的操作-----------*/
Q.rear = Q.front;
}
return true;
}
//判断是否为空链队列
bool QueueEmpty(LinkQueue Q)
{
if(Q.front->next = NULL)
return true;
else
return false;
}
//返回队列的长度,即队列的长度
int QueueLength(LinkQueue Q)
{
QueuePtr q,p;
int Length = 0;
while (!Q.front)
{
//初始化使得Q.front头指针指向下一结点为q,不断深入查找下一个结点直到为空,循环结束
p = Q.front->next;
while (q)
{
q = p->next;
Length++;
}
}
return Length;
}
//若队列不空则返回队头元素
bool GetHead(LinkQueue Q,int e)
{
QueuePtr p;
if(Q.front == Q.rear)
return false;
else{
p = Q.front->next;
e = p->data;
}
return e;
}
//入队,插入元素e为新的队尾元素
//Q->next实质与链表头结点一样,均指向第一个结点
bool EnQueue(LinkQueue &Q,int e)
{
QueuePtr p = (QueuePtr)malloc(sizeof(QNode));
if(!p)
return false;
else{
p->data = e;
p->next = NULL;//将p的指针部分赋为NULL,作为尾结点
Q.front->next = p;//将第一个结点指向新插入的结点
Q.rear = p;
}
return true;
}
//出队
/*注意出队只能是队列第一个而不能是中的某一个,思路也比较简单只需将头指针赋给头结点的下一个结点,
从而达到“越过”的操作即达到了出队
*/
int DeQueue(LinkQueue &Q,int e)
{
QueuePtr p;
if(Q.front == Q.rear)
return false;
else{
//将头指针指向第一个结点,做好出队准备
p = Q.front->next;
e = p->data;
/*--------core----------*/
Q.front->next = p->next;//将节点的后继节点指针赋给头指针达到“越过”的目的
/*--------core----------*/
if(Q.rear==p)
//若当此时队列只有一个元素,出队将恢复到空队列的状态,故执行Q.front = Q.rear;
Q.front = Q.rear;
free(p);
}
return e;
}
int main()
{
return 0;
}