数据结构例题--迷宫

迷宫

 问题要求:定义一个二维数组N*M(其中2<=N<=10,2<=M<=10),如5x5数组,如下所示:

int maze[5][5] = 
{

0,1,0,0,0,
0,1,1,1,0,
0,0,0,0,0,
0,1,1,1,0,
0,0,0,1,0,
};

他表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或者竖着走,不能斜着走,也就是只能走上下左右。这个程序要求找出从左上角到右下角的最短路线。入口点为[0,0],即第一个是可以走的路。也就是坐标是[0,0]的是入口,坐标[N,M]是出口。且只有唯一的路径可以从起点走向终点。

思路:

我们可以先按上下左右的顺序依次寻找每一次我们可以走的路,按照上下左右的顺序找路,找到了就走,那么就有一个问题,如果我们上一次往下走了,那么下一次我们先按上来走,那么我们就会回到我们上一次走过的结点,如下图所示:

数据结构例题--迷宫_第1张图片当我们从[0,0]这个结点走到[1,0]这个结点的时候,因为我们是按照上下左右的顺序来找路的,我们先按上找到了“0”这条路就往上走了。

所以我们在此就多定义一个值---2 ,这个“2”表示我们走过的路,每一次只要走过一个结点就把这个结点的值从“0”改为“2”,这样表示我们走过的路。

那么,我们寻路走路的方式找到了,那么当我们遇到死胡同的时候怎么办呢?

其实,在数据结构里有一个遍历二叉树的知识,比如用中序遍历二叉树就是先遍历左子树,在遍历根结点,最后遍历右子树。那么当我们左右子树遍历到叶子结点,也就是最后一个结点的时候,我们怎样返回去在再遍历根结点和右子树呢?

我们数据结构用来一种很经典的方式来实现,就是用函数递归。我们用函数递归来遍历二叉树,当我们要访问下一个结点的,但下一个结点是空的时候,就return返回上一次调用的函数。

那么二叉树是一个方向判断是否为空的遍历--返回,那这个迷宫问题就是四个方向的遍历--返回的问题。当我们的上下左右都不能走(即:上下左右都不是“0”,要么是“1”,要么是“2”)的时候,我们就返回上一个结点,如果返回之后还是上下左右都不能走,那么再返回,直到返回到有路可走为止。

或者是返回到起点,那么就表示这个迷宫没有路可以走到终点。

那么如何找出这个结点的上下左右是否可以走呢?我们发现,假设我们所在的结点的坐标是[n,m]那么这个结点的:

上面一个结点的坐标是[n-1,m]; 下面一个结点的坐标是[n+1,m];  左边一个结点的坐标是[n,m-1]; 右边一个结点的坐标是[n,m+1]。

如此我们就从这个结点的上下左右顺序判断出这个结点是否有可以走的路。

如此就可以暴力的选出路径出来。

但是这个题目还要我们把这个路径坐标输出出来,也就是按照解决路径的顺序把每一个结点的坐标都输出出来。我们想到用栈来存储这些数据,因为栈是先进后出,满足我们遍历找路的过程。

当我们没走一个结点,就把这个结点的坐标值传入这个栈里面(入栈)进行存储。

如图:

数据结构例题--迷宫_第2张图片当我们遍历到这个死胡同的时候,这时候栈里面就帮我们存储了这样一个路径,我们之前说过,遇到死胡同就返回上一个结点,那么我们返回一个结点就把这个结点给出栈(删除掉)。比如现在我们要从[5,3]返回到[5,2],返回之后就把栈里的[5,3]这个结点给删掉。然后一直这样操作到有路位置,这样我们在删除和创建路径坐标的时候就好操作了。问题:栈是先进后出的,那么我们在打印栈的数据的时候,如左图就只能倒着从终点往前打印,那么我们为了美观,我们想从起点开始打印,该如何做呢?

我们可以再创建一个栈,把之前栈里的数据导入到这个新的栈里面,这样就实现了栈里数据的逆序。

 最后,当我们找到这个终点的时候,就表示我们找到了解决的路径,就把栈里面存储的路径结点的值输出出去,就完成了路径结点的输出。

在这需要注意的地方是,我们在申请数组空间的时候,题目是需要用变长数组的,如下所示就是一个变长数组:

int main()
{
 int n = 0, m = 0;
 scanf("%d%d",&n,&m);
 int a[n][m];
}

 但是这个语法只支持C99以上版本的c语言编译器,以下都是不支持的。

如果不支持,就需要动态开辟空间,动态开辟一个二维数组空间:

我们先要开辟一个指针数组空间,用来存储每一个一维数组的首元素地址,也就是说这个数组里面存储的是指向每一行数组首元素地址的指针。如图所示:

数据结构例题--迷宫_第3张图片

 同样的,每一次动态开辟空间再不用的时候都要把他释放掉,释放的时候注意,不能先释放指针数组,要不然后面的数组就失去了找到它的空间的地址,这样就内存泄漏了。我们要先把开辟的每一行的一维数组的空间先释放掉,在释放指针数组空间。

代码实现动态开辟二维数组:

int N = 0;   //行数
int M = 0;   //每一行的元素个数

//开辟指针数组的空间
int** maze = (int**)malloc(sizeof(int*)*N);  //N是一维数组的个数,相当于是二维数组的行数

//开辟每一行的数组空间
for(int i = 0;i

 在我们实现记录路径的时候,我们是用栈来存储结点的坐标,我们现在在c语言中实现栈:

typedef char STDataType;
typedef struct stack
{
     STDataType* a;
     int top;
     int capacity;
}ST;
 //初始化栈数据
void StackInit(ST* ps)
{
       assert(ps);
       ps->a = (STDataType*)malloc(sizeof(STDatraType)*4);
       
       if (ps->a == NULL)             //判断malloc函数是否使用成功
       {
          printf("realloc fail\n");
          exit(-1);
       }     
       
       ps->top = 0;          //top的初始化
       ps->capacity = 4;      //capacity的初始化
}
//栈的栈顶数据的插入
void StackPush(ST* ps,STDataType x)
{
       assert(ps);
        
       if(ps->top == ps->capacity)
    {
        STDataType* tmp = (STDataType*)realloc(ps->a,ps->capacity*2sizeof(STDataType));
        if (tmp == NULL)             //判断realloc函数是否使用成功
       {
          printf("realloc fail\n");
          exit(-1);
       } 
        else
        {
          ps->a = tmp;
          ps->capacity *= 2;
        }
 
    ps->a = tmp;                         //对开辟的空间进行分布
    ps->capacity = newCapacity;       
    }

       ps->a[ps->top] = x;             
       ps->top++;
}
//对于整个栈进行删除
void StackDestroy(ST* ps)
{
       assert(ps);
       free(ps->a);
       ps->a = NULL;
       ps->capacity = ps->top = 0;
}
// 栈的栈顶数据进行删除
void StackPop(ST* ps)
{
     assert(ps);
     assert(ps->top > 0);       //判断栈是否为空
 
     ps->top--;
}
//取栈顶的元素
STDataType StackTop(ST* ps)
{
       assert(ps); 
       assert(ps->top > 0);         //判断这个栈是否为空,为空就报错
     
       return ps-a[ps->top - 1];
}

迷宫的代码实现:

//定义一个结构体来类型,用来储存每一次遍历的结点的坐标
typedef struct postion
{
   int row; //X坐标
   int col; //Y坐标
}PT;


//打印二维数组的函数
void PrintMaze(int** maze,int N, int M)
{
  for(int i = 0;i= 0 && pos.row < M 
      && pos.col >= 0 && pos.col < N
      && maze[row][col] == 0)
     {
       return true;
     }
     else
     {
       return false;
     } 
}

//迷宫中找路的算法实现函数
bool GetMazePath(int** maze,int N,int M,PT cur)
{
   StackPush(&path,cur);
   
   //判断如果到了终点就返回ture
   if(cur.row == N-1 && cur.col == M-1)
   {
        return true;
   }
   
   PT next;
   maze[cur.row][cur.col] = 2;  //走过的结点就把这个结点的值改成2,代表我们走过了

   //判断上面的结点能不能走
   next = cur;
   cur.col -= 1;
   if(IsPass(maze,N,M,next))
   {
      if(GetMazePath(int** maze,int N,int M,next))
         return true;
   }

   //判断下面的结点能不能走
   next = cur;
   cur.col += 1;
   if(IsPass(maze,N,M,next))
   {
      if(GetMazePath(int** maze,int N,int M,next))
         return true;
   }
   
  
   //判断左面的结点能不能走
   next = cur;
   cur.row -= 1;
   if(IsPass(maze,N,M,next))
   {
      if(GetMazePath(int** maze,int N,int M,next))
         return true;
   }
   

   //判断右面的结点能不能走
   next = cur;
   cur.col += 1;
   if(IsPass(maze,N,M,next))
   {
      if(GetMazePath(int** maze,int N,int M,next))
         return true;
   }

   return false;
   
}

int main()
{
   int N = 0;   //行数
   int M = 0;   //每一行的元素个数

  while(scanf("%d%d",&N,&M) != EOF)
 {

   //开辟指针数组的空间
   int** maze = (int**)malloc(sizeof(int*)*N);  //N是一维数组的个数,相当于是二维数组的行数

   //开辟每一行的数组空间
   for(int i = 0;i

我们在这个函数里 “bool  GetMazePath(int** maze,int N,int M,PT cur)” 使用了bool这个变量,当我们走到了终点就返回ture,没找到返回起点的时候就返回false。

然后我们就实现了迷宫的代码。

进阶版迷宫

 有一只小青蛙不小心落入了一个地下迷宫,小青蛙希望用自己仅剩的体力值P跳出这个地下迷宫。为了让问题简单,假设这个是一个n*m的格子迷宫,迷宫的每一个位置为0或者是1,0就代表着是墙,1代表的是路;[0,0]这个坐标位置就是起点,[0,m-1]是终点(这两个位置都是1,且一定有路径可以从起点到终点)。小青蛙在迷宫中移动的一个单位距离需要消耗一点体力值,向上爬一个单位距离需要消耗3个单位的体力值,向下移动不消耗体力值,当小青蛙的体力值为0的时候,小青蛙没有到达出口,就表示小青蛙无法逃离迷宫。现在你需要帮助小青蛙计算出能否用剩余的体力值跳出迷宫(即到达[0.m-1]这个位置)。

我们发现,虽然这道题没有明确表明要求算出最短路径,但是实际上这道题是要求我们算出最短路径的,因为小青蛙有体力值这个限制。这个题目的整体逻辑和我们之前实现的迷宫是基本一致的,比之前多出来的就是小青蛙的体力值和要求我们算出最短路径,那么我们就在我们之前实现的代码基础之上来进行修改。

其他的修改都很简单,加一个体力值和体力值的运算;0  1 路和墙壁换一下;等等这些都容易修改,我们主要要处理最短路径如何实现。

首先对于寻找最短路径我们就把可以从起点走到终点的路径都走一遍,然后我们计算一下我们的路径长度,算出每一条可以走的路径的路径长度,进行比较。这就是基本思路。

数据结构例题--迷宫_第4张图片假如按照我们之前实现的迷宫算法,他是按照上下左右的顺序徐寻找路径的,那么他应该是这样一个路线,我们发现,这不是最短的路径。

首先每一次走完一次之后,不管这条路是不是最短路径,我都用递归的return进行回溯操作,像之前走到死路回溯到可以走的岔路口一样,回溯到这个岔路口往另一边再继续走,如此反复,直到最后没有新的路径可以走,我们就把所有可以走的路径都选出来了。

数据结构例题--迷宫_第5张图片


我们之前走过的结点,是吧这个结点的值改为2,那么我们在之后回溯寻找路径的时候,如果这个路径上的结点还是2,那么我们就会进入死胡同找不到路,如图所示:

数据结构例题--迷宫_第6张图片

 我们要消除“2”的标记,但是在岔路口之前的“2”标记不能删掉,如图所示的标记:

数据结构例题--迷宫_第7张图片

 基于这样的删除标记,我们想到:每一次递归回溯一次就删除这个结点的标记来实现,因为我们首先寻找的顺序是按照上下左右的顺序来寻找的,那么就像上图一样,在那个岔路口我们先往下走了,那么递归回溯到这个岔路口的时候就会按左右这个顺序来寻找。

我们之前实现这个寻路的递归的时候是靠返回true和false来判断这个路是不是通路的,那么现在为了实现这个回溯操作,

随后,因为我们为了寻找最短路径,我们会多次走到出口,那么我们再在每一次在某结点判断寻找上下左右能不能走之前,我们先判断一下这个结点是不是结点,如果是结点,我们就回溯操作。


我们这样就实现了回溯重新找其他路径的操作,那么我们现在还需要解决一个问题,就是路径总是会找完的,如何判断路径找完的情况,然后选出最短的路径呢?

我们之前存储路径是用了一个栈来实现的,这样方便我们输出路径结点坐标,由此我们在创建一个栈--minpart,用来存储最短路径。

我们在判断minpart栈和part栈的数据个数大小的之后,如果记录出的part栈的数据个数比minpart栈的数据个数小,那么我们应该吧part栈的数据个数传过去,就行了,这就是我们创建两个栈的作用,如果是一个栈的话,我们在回溯的时候,每回溯一个结点就会把part栈里的这个结点的坐标Pop掉(删除掉),如果我们把这这两个栈存储在一个内存块下,就不能达到这个目的了。

数据结构例题--迷宫_第8张图片

 就是这样一个样子,如果我们直接把这个part的数据传给minpart的话,我们这两个变量的类型是我们自己定义的一个结构体类型,这个结构体类型里面有这样几个元素:

数据结构例题--迷宫_第9张图片a是栈的空间的元素地址,top是这个栈的元素个数·········      那么当我们直接把part这个变量赋值给他的时候,不仅仅是这个栈里的数据赋值给他,还是把part栈空间的地址赋值给他,他们在赋值之后相当于是指向的是同一块栈空间。

那么当我们在对part栈空间里的结点坐标进行修改的时候,也是相当于对minpart栈空间进行修改,那么就又可以会出现错误,所以我们还得用我们实现的栈的操作函数来对minpart进行修改,用栈操作函数把part里面的数据传到minpart里面。         而且还有一个因素,出现这样的错误的时候,我们还会以为我们创建的是两个栈,那么我们都要使用完栈的时候把栈给用我们自己实现的删除整个栈的函数来把这个栈给删除掉,那么我们删除两次也会出现问题。

 那么我们如何解决这个问题呢?在这里我们使用一个深拷贝问题,也就是说,当我们创建好了这两个栈空间之后,后面我需要把part栈里的数据的值拷贝到minpart栈里的时候,我要先把minpart栈的空间给释放掉,然后再让minpart创建一个与此时part栈空间一样大的空间,现在需要注意的是,我们不能直接用我们之前实现的栈中数据入栈的操作来实现数据的拷贝,因为栈是先进后出,我们把part栈里的数据入栈到minpart栈里之后,这个路径的坐标顺序就发想看逆序。

所以我们在这里实现一个栈拷贝的函数,把part栈里的数据之间按顺序拷贝到minpart栈里:

#include a = (STDataType*)malloc(sizeof(STDataType*)*ppath->capacity);
  
  //把ppath栈里空间的值(这里存储的是结点的坐标)传给pcopy栈
  //sizeof(STDataType)*ppath->top表示ppath这块空间的有多少字节
  memcpy(pcopy->a,ppath->a,sizeof(STDataType)*ppath->top);
 
  //把ppath栈中响应的参数传给pcopy
  pcopy->top = ppath->top;
  pcopy->capacity = ppath->capacity;
}

  //判断如果path栈里面的数据个数如果小于minpart栈里面的数据就执行下面代码
        if(StackEmpty(&minpath
           || StackSize(&path) < StackSize(&minpath))
        {
          StackDestory(&minpart);  //先把minpart栈的空间释放掉
          StackCopy(&part, &minapart); //再让minpart栈创建一个和part栈空间一样大的空间,再把 
                                       //part里的值拷贝进去
        }

 以上就是实现把part栈里的数据传给minpart栈,我们后面实现的这个方法叫做深拷贝,而我们之前的错误示范叫做浅浅拷贝。

体力值实现

 我们在这个程序输入的时候除了会输入这个迷宫的长宽和迷宫的组成之外,还会输入一个值就是体力值,当我们找到了最短路径不代表小青蛙能走到终点,如果途中体力值不足的话也不能走到终点。当我们体力值不够的时候就输出“can not escape!” 表示体力值不够了,无法逃出去。

实现也很简单,我们之前是按照上下左右的顺序来寻找路的,那么我们的体力值的扣除规则也是根据不同的寻找方式有不同的扣除规则,我们的上下左右找路也是分开找的,小青蛙在迷宫中移动的一个单位距离需要消耗一点体力值,向上爬一个单位距离需要消耗3个单位的体力值,向下移动不消耗体力值,那么我们在成功找到路之后就要往这个结点走,一走就按照规则扣除体力值就行了。

首先是存储路径的栈的更新,只有当体力值大于0的时候我们才更新,不大于0我们就不更新了,如下:

 //判断如果path栈里面的数据个数如果小于minpart栈里面的数据
 //并且此时体力值不为0,那么就就执行下面代码
if(p >= 0 && StackEmpty(&minaprt)
   || SatckSize(&path) < StackSize(&minpart(&minpart))
  {
    StackDestory(&minpart);
    StackCopy(&part,&minpart);
    
  }

 那么这个的意思是有体力值就去更新minpart这个栈,那么当我们从一开始寻找最短路径的时候,每一次都走不到终点,在途中体力值就减为0了;找到最后都走不到终点,说明我们的体力值不够,走不到终点,那么就打印“can not escape!”。

意思就是只要我们找得到路可以走到终点,此时满足这个路径长度小于我们之前minpart记录的最小路径长度,且当我们到达终点的时候,体力值不为0,那么根据上面的if条件判断语句,我们就会把这个地方的part存放的路径更新到minpart里存放。

//这个判断语句在我们实现迷宫算法函数之外使用
//当minpart栈里不为空,说明我们找到了最短路径
//当minpart栈里为空,说明我们在每一次找最短路径的时候,体力值都不够,minpart栈没有更新
//那么就打印找不到
if(!StackEmpty(&minpart))
{
  printpath(&minpart);
}
else
{
 printf("can not escape!");
}

最后代码实现:

#include
#include
#include
#include
#include

typedef struct Postion
{
	int row;
	int col;
}PT;
/
typedef PT STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST, Stack;

void StackInit(ST* ps);
void StackDestory(ST* ps);
// 入栈
void StackPush(ST* ps, STDataType x);
// 出栈
void StackPop(ST* ps);
STDataType StackTop(ST* ps);

int StackSize(ST* ps);
bool StackEmpty(ST* ps);

void StackInit(ST* ps)
{
	assert(ps);

	ps->a = (STDataType*)malloc(sizeof(STDataType) * 4);
	if (ps->a == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	ps->capacity = 4;
	ps->top = 0;
}

void StackDestory(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

// 入栈
void StackPush(ST* ps, STDataType x)
{
	assert(ps);

	// 满了-》增容
	if (ps->top == ps->capacity)
	{
		STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			ps->a = tmp;
			ps->capacity *= 2;
		}
	}

	ps->a[ps->top] = x;
	ps->top++;
}

// 出栈
void StackPop(ST* ps)
{
	assert(ps);
	// 栈空了,调用Pop,直接中止程序报错
	assert(ps->top > 0);

	//ps->a[ps->top - 1] = 0;
	ps->top--;
}

STDataType StackTop(ST* ps)
{
	assert(ps);
	// 栈空了,调用Top,直接中止程序报错
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}

int StackSize(ST* ps)
{
	assert(ps);

	return ps->top;
}

bool StackEmpty(ST* ps)
{
	assert(ps);

	return ps->top == 0;
}

Stack path;
Stack minpath;


void PrintMaze(int** maze, int N, int M)
{
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < M; ++j)
		{
			printf("%d ", maze[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}

// 输出栈里面的坐标路径
void PirntPath(Stack* ps)
{
	// path数据倒到rPath
	Stack rPath;
	StackInit(&rPath);
	while (!StackEmpty(ps))
	{
		StackPush(&rPath, StackTop(ps));
		StackPop(ps);
	}

	while (StackSize(&rPath) > 1)
	{
		PT top = StackTop(&rPath);
		printf("[%d,%d],", top.row, top.col);
		StackPop(&rPath);
	}

	PT top = StackTop(&rPath);
	printf("[%d,%d]", top.row, top.col);
	StackPop(&rPath);

	StackDestory(&rPath);
}

bool IsPass(int** maze, int N, int M, PT pos)
{
	if (pos.row >= 0 && pos.row < N
		&& pos.col >= 0 && pos.col < M
		&& maze[pos.row][pos.col] == 1)
	{
		return true;
	}
	else
	{
		return false;
	}
}

void StackCopy(Stack* ppath, Stack* pcopy)
{
	pcopy->a = (STDataType*)malloc(sizeof(STDataType*)*ppath->capacity);
	memcpy(pcopy->a, ppath->a, sizeof(STDataType)*ppath->top);
	pcopy->top = ppath->top;
	pcopy->capacity = ppath->capacity;
}

void GetMazePath(int** maze, int N, int M, PT cur, int P)
{
	StackPush(&path, cur);

	// 如果走到出口
	if (cur.row == 0 && cur.col == M - 1)
	{
		// 找到了更短的路径,更新minpath;
		if (P >= 0 && StackEmpty(&minpath)
			|| StackSize(&path) < StackSize(&minpath))
		{
			StackDestory(&minpath);
			StackCopy(&path, &minpath);
		}
	}

	// 探测cur位置得上下左右四个方向
	PT next;
	maze[cur.row][cur.col] = 2;

	// 上
	next = cur;
	next.row -= 1;
	if (IsPass(maze, N, M, next))
	{
		GetMazePath(maze, N, M, next, P - 3);

	}

	// 下
	next = cur;
	next.row += 1;
	if (IsPass(maze, N, M, next))
	{
		GetMazePath(maze, N, M, next, P);
	}


	// 左
	next = cur;
	next.col -= 1;
	if (IsPass(maze, N, M, next))
	{
		GetMazePath(maze, N, M, next, P - 1);
	}

	// 右
	next = cur;
	next.col += 1;
	if (IsPass(maze, N, M, next))
	{
		GetMazePath(maze, N, M, next, P - 1);
	}

	// 恢复一下
	maze[cur.row][cur.col] = 1;
	StackPop(&path);
}

int main()
{
	int N = 0, M = 0, P = 0;
	while (scanf("%d%d%d", &N, &M, &P) != EOF)
	{
		// int a[n][m]; // vs2013 不支持
		// 动态开辟二维数组
		int** maze = (int**)malloc(sizeof(int*)*N);
		for (int i = 0; i < N; ++i)
		{
			maze[i] = (int*)malloc(sizeof(int)*M);
		}

		// 二维数组得输入
		for (int i = 0; i < N; ++i)
		{
			for (int j = 0; j < M; ++j)
			{
				scanf("%d", &maze[i][j]);
			}
		}

		StackInit(&path);
		StackInit(&minpath);
		// PrintMaze(maze, N, M);
		PT entry = { 0, 0 };
		GetMazePath(maze, N, M, entry, P);
        
        if(!StackEmpty(&minpath))
        {
            PirntPath(&minpath);
        }
        else
        {
            printf("Can not escape!\n");
        }
		

		StackDestory(&path);
		StackDestory(&minpath);

		for (int i = 0; i < N; ++i)
		{
			free(maze[i]);
		}
		free(maze);
		maze = NULL;
	}

	return 0;
}

你可能感兴趣的:(算法,数据结构,c语言)