前言
- 最近保持一周三更,出十篇数据结构的博客之后会出Java的。
- 博客排版会逐步规范。
- 代码排版会沿用我自己的风格并且继续改进。代码注释会反复考虑,如果过于复杂就会分块排版代码。
- 这篇博客之后会上一个栈的基本操作的文件和一篇随机生成迷宫的程序博客。
- 后续会在GitHub上面找项目一起看,虽然没几个人看🙃
最近看到一句区别栈和队列很形象的话:
栈是先进后出,队列是先进先出
所以吃多了吐就是栈,吃多了拉就是队列
问题概述
如上面的迷宫,用栈实现求迷宫路径的程序,所求路径不需要是最短路径,但必须是简单路径,即在求得的路径上不能重复出现同一通道块。比如当走到(1,8)时,发现无路可走了,则依次退栈,退到能走通的通道快,而不是往回走(即(2,8)再次入栈)。
程序思路
穷举求解
计算机求解迷宫问题的方法就是穷举求解,即从入口出发,顺某一方向向前探索,若能走通,则继续往前走;否则沿原路返回,换一个方向再继续探索,直至所有可能的通路都探索为止。为了保证在任何位置上都能沿原路退回,就需要用到一个先进先出的结构来保存从入口到当前位置的路径,那就是栈。
假设”当前位置“指的是”在搜索过程中某一时刻所在图中某个方块的位置“,则求解迷宫问题的基本思路就是:若”当前位置“可通”,则纳入”当前位置“(入栈),并继续朝”下一位置”探索,即切换“下一位置”为“当前位置”,如此重复直至到达出口;若“当前位置”不可通“,则应顺着“来向”退回到“前一通道快”(当前栈顶元素退栈后的栈顶元素),然后朝着除“来向”(前栈顶元素的方向)之外的其他方向继续探索;若该通道快的四周4个方块均“不可通”,则应从“当前路径”上删除该通道快(再次退栈)。
若以栈S记录“当前路径”,则栈顶中存放的是“当前路径上最后一个通道快”(当栈顶为出口时即求解完成),最后的求得的栈S从栈底到栈顶即是求得的路径,但是输出的路径用序号在迷宫图上表示。
代码实现
自定义一个迷宫
main函数里首先要创建一个自定义的迷宫。用一个二维数组来记录迷宫,把墙的值设为0,即不可通,把通道的值设为1,即可通。这里的程序把迷宫的四周都设定为墙,然后设定迷宫内部墙的位置,再指定迷宫的入口和出口。
int X, Y, i, j, n;
cout << "输入迷宫的行数,列数:" << endl;
cin >> X >> Y;
for (i = 0; i < Y; i++) // 定义上下边的墙值为0
{
m[0][i] = 0;
m[X - 1][i] = 0;
}
for (j = 1; j < X - 1; j++) //定义左右边除了第一行和最后一行的墙值为0(第一行和最后一行在上个循环中定义过)
{
m[j][0] = 0;
m[j][Y - 1] = 0;
}
for (i = 1; i < X - 1; i++) //定义所有内墙值为1(即通路)
for (j = 1; j < Y - 1; j++)
m[i][j] = 1;
cout << "请输入迷宫的内墙数:" << endl;
cin >> n;
for (i = 0; i < n; i++)
{
int x, y; //输入内墙的坐标值,以左上角坐标为(1,1)计算
cout << "输入第" << i + 1 << "个内墙的位置" << endl;
cin >> x >> y;
m[x][y] = 0;
}
数据的存储结构
typedef int MazeType[MAXLENGTH][MAXLENGTH]; // 迷宫数组[行][列]
MazeType m; //声明一个全局迷宫对象m
typedef int Status;
typedef struct //坐标
{
int x;
int y;
} PosType;
typedef struct
{
int ord; //通道块在路径上的“序号”
PosType seat; //通道块在迷宫中的“坐标位置”
int di; //从从此通道块走向下一通道块的“方向”
} SElemType; //栈的元素类型
typedef struct
{
SElemType *base; //栈底指针
SElemType *top; //栈顶指针
int stacksize; //栈的大小
} Stack;
栈的操作
Status InitStack(Stack &S); // 构造一个空栈S
Status Push(Stack &S, SElemType e); //入栈
Status Pop(Stack &S, SElemType &e); //出栈,移除当前栈顶元素
Status StackEmpty(Stack S); //检测是否为空栈
迷宫的操作
void Print(int x, int y); // 输出迷图,0表示墙,1表示通路
void FootPrint(PosType a); // 使迷宫m的a点的序号变为足迹(curstep)
PosType NextPos(PosType c, int di); // 根据当前位置及移动方向,返回下一位置
void MarkPrint(PosType b); // 使迷宫m的b点的序号变为-1(不能通过的路径)
Status MazePath(PosType start, PosType end); // 若迷宫maze中存在从入口start到出口end的通道,则求得一条存放在栈中(从栈底到栈顶),并返回TRUE;否则返回FALSE
代码汇总
#include <iostream>
#include <stdlib.h> //realloc函数头文件部分编译器不需要此行代码
using namespace std;
const int ERROR = 0;
const int OK = 1;
const int FALSE = 0;
const int TRUE = 1;
const int STACK_INIT_SIZE = 100;
const int STACKINCREMENT = 10;
const int MAXLENGTH = 25; // 设迷宫的最大行列为25
int curstep = 1; // 当前足迹,初值为1
typedef int MazeType[MAXLENGTH][MAXLENGTH]; // 迷宫数组[行][列]
MazeType m; //声明一个全局迷宫对象m
typedef int Status;
typedef struct
{
int X;
int Y;
} PosType;
typedef struct
{
int ord; //通道块在路径上的“序号”
PosType seat; //通道块在迷宫中的“坐标位置”
int di; //从从此通道块走向下一通道块的“方向”
} SElemType; //栈的元素类型
typedef struct
{
SElemType *base;
SElemType *top;
int stacksize;
} Stack;
void Print(int x, int y) // 输出迷宫图,0表示墙,1表示通路
{
int i, j;
for (i = 0; i < x; i++)
{
for (j = 0; j < y; j++)
printf("%3d", m[i][j]);
printf("\n");
}
}
Status InitStack(Stack &S) // 构造一个空栈S
{
if (!(S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType))))
exit(ERROR); // 存储分配失败
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return OK;
}
Status Push(Stack &S, SElemType e) //入栈
{ // 插入元素e为新的栈顶元素
if (S.top - S.base >= S.stacksize) // 栈满,追加存储空间
{
S.base = (SElemType *)realloc(S.base, (S.stacksize + STACKINCREMENT) * sizeof(SElemType));
if (!S.base)
exit(ERROR); // 存储分配失败
S.top = S.base + S.stacksize; //确保S满栈的情况,top在base上方的当前栈长处
S.stacksize += STACKINCREMENT;
}
*(S.top)++ = e;
/*或者
*S.top = e;
*S.top++;
*/
return OK;
}
Status Pop(Stack &S, SElemType &e) //出栈,移除当前栈顶元素
{
if (S.top == S.base)
return ERROR;
e = *--S.top;
return OK;
}
Status StackEmpty(Stack S) //检测是否为空栈
{
if (S.top == S.base)
return OK;
else
return ERROR;
}
Status Pass(PosType b) // 当迷宫m的b点的序号为1(可通过路径),return OK; 否则,return ERROR。
{
if (m[b.X][b.Y] == 1)
return OK;
else
return ERROR;
}
void FootPrint(PosType a) // 使迷宫m的a点的序号变为足迹(curstep)
{
m[a.X][a.Y] = curstep;
}
PosType NextPos(PosType c, int di) // 根据当前位置及移动方向,返回下一位置
{
PosType direc[4] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // {行增量,列增量}
// 移动方向,依次为东南西北
c.X += direc[di].X;
c.Y += direc[di].Y;
return c;
}
void MarkPrint(PosType b) // 使迷宫m的b点的序号变为-1(不能通过的路径)
{
m[b.X][b.Y] = -1;
}
Status MazePath(PosType start, PosType end) // 若迷宫maze中存在从入口start到出口end的通道,则求得一条存放在栈中(从栈底到栈顶),并返回TRUE;否则返回FALSE
{
Stack S;
PosType curpos;
SElemType e;
InitStack(S);
curpos = start; //把入口设置为当前位置
do
{
if (Pass(curpos))
{ // 当前位置可以通过,即是未曾走到过的通道块
FootPrint(curpos); // 留下足迹
e.ord = curstep;
e.seat.X = curpos.X;
e.seat.Y = curpos.Y;
e.di = 0;
Push(S, e); // 入栈当前位置及状态
curstep++; // 足迹加1
if (curpos.X == end.X && curpos.Y == end.Y) // 到达终点(出口)
return TRUE;
curpos = NextPos(curpos, e.di); //将当前位置变为e.di方向上的临近通道块
}
else
{ // 当前位置不能通过
if (!StackEmpty(S))
{
Pop(S, e); // 退栈到前一位置
curstep--;
while (e.di == 3 && !StackEmpty(S)) // 前一位置处于最后一个方向(北),即当前位置的四周都无法通过
{
MarkPrint(e.seat); // 留下不能通过的标记(-1)
Pop(S, e); // 退回一步
curstep--;
}
if (e.di < 3) // 没到最后一个方向(北)
{
e.di++; // 换下一个方向探索
Push(S, e);
curstep++;
curpos = NextPos(e.seat, e.di); // 设定当前位置是该新方向上的相邻块
}
}
}
} while (!StackEmpty(S));
return FALSE;
}
int main()
{
PosType begin, end;
int X, Y, i, j, n;
cout << "输入迷宫的行数,列数:" << endl;
cin >> X >> Y;
for (i = 0; i < Y; i++) // 定义上下边的墙值为0
{
m[0][i] = 0;
m[X - 1][i] = 0;
}
for (j = 1; j < X - 1; j++) //定义左右边除了第一行和最后一行的墙值为0(第一行和最后一行在上个循环中定义过)
{
m[j][0] = 0;
m[j][Y - 1] = 0;
}
for (i = 1; i < X - 1; i++) //定义所有内墙值为1(即通路)
for (j = 1; j < Y - 1; j++)
m[i][j] = 1;
cout << "请输入迷宫的内墙数:" << endl;
cin >> n;
for (i = 0; i < n; i++)
{
int x, y; //输入内墙的坐标值,以左上角坐标为(1,1)计算
cout << "输入第" << i + 1 << "个内墙的位置" << endl;
cin >> x >> y;
m[x][y] = 0;
}
Print(X, Y);
printf("请输入起点的行数,列数:");
cin >> begin.X >> begin.Y;
printf("请输入终点的行数,列数:");
cin >> end.X >> end.Y;
if (MazePath(begin, end)) // 求得一条通路
{
printf("此迷宫从入口到出口的一条路径如下:\n");
Print(X, Y); // 输出此通路
}
else
printf("此迷宫没有从入口到出口的路径\n");
}
反思总结
标志变量的使用
常常在程序中要使用到标志变量,更好的运用标志变量可以使程序更优秀。
- 冒泡排序中会用到一个标志变量,在最后一次变量后,没产生冒泡动作,标志变量变为0,for语句结束循环。
- https://blog.csdn.net/weixin_44858668/article/details/103091385在链接中的程序的Match函数中,用到了两个标志变量Count_L和Count_R可以帮程序更方便的判断结果。
- 在此程序中,用到的curstep标志变量,可以用来记录每一步的序号。