一、扫雷进阶留下的问题
我们先来看看之前讲扫雷基础的时候留下的一些问题:
-
是否可以选择游戏难度
◦ 简单 9 * 9 棋盘,10个雷
◦ 中等 16 * 16棋盘,40个雷
◦ 困难 30 * 16棋盘,99个雷 -
如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚
-
是否可以标记雷
-
是否可以加上排雷总共耗费的时间
要注意的一点是选择游戏难度以目前我们讲过的内容还暂时写不出来,在后面学习到相关内容时,我们再进行讲解,接下来就解决后三个问题
我们的进阶还是在之前写过的基础版之上添加,如果还没有看过之前基础版,可以在我的主页查看,现在我们开始通过我们学过的知识解决这三个进阶需求
二、坐标非雷扩展周围函数
这个函数我们的目标就是实现:如果输入的坐标不是雷,那么查看周围是否有雷,如果至少有一个雷,那么就直接显示周围雷的个数,如果没有雷,把那个坐标换成字符’ ',也就是字符空格,而不再显示0,这样更加好看,随后向周围扩展,扩展到该坐标周围至少有一个雷时
我们需要做一点更改,就是把输入的当前坐标,查看周围有几个雷放入这个函数,方便递归,现在可能有点懵,后面会慢慢解释
这个函数实现起来有一定难度,必须要悟透递归的使用方法,同时还要注意数组越界等等问题,还要使用一点点超纲内容,就是指针,但是是最简单的指针内容,并且篇幅只有一点点,主要还是要体会循环递归的方式,以及递归的两个必要条件:
- 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续,如果没有限制,可能会陷入死递归
- 每次递归调⽤之后越来越接近这个限制条件
接下来我们就进入这个函数的设计:
-
函数的命名:我将其命名为exdboard,exd是extend的缩写,含义是扩展,函数名就理解为扩展棋盘,也可自行命名
-
函数参数:
(1)由于需要操作数组show和数组hide,所以要将这两个数组传过去
(2) 由于需要扩展周围的坐标,我们可以直接把当前坐标也一次性涵盖进去,就是先对当前坐标进行操作,查看周围是否有雷,如果没有雷就开始对周围坐标递归,所以我们需要传过去当前玩家输入的坐标,也就是x,y
(3)然后由于我们要避免函数递归导致数组越界访问,所以我们要把行和列传过去,用来判断递归坐标的合法性
(4)最后由于我们每递归一次,可能就会扩展一个坐标,而每扩展一次坐标我们需要win++一次,但是由于直接传win过去属于传值调用,在exdboard里面对win进行更改不会对真实的win产生影响,所以我们需要使用指针进行传址调用,这里听不懂没关系,后面会专门详细的为大家介绍指针,现在只需要跟着使用就行,并且在该函数它只有一个作用,那就是每次递归,如果扩展了一个坐标,就对win++一次,所以函数exdboard的最后一个参数是win的地址
(5)所以函数exdboard的参数有:show和hide数组,玩家输入的坐标x,y,棋盘行和列,win的地址 -
函数的声明:我们要将函数设计好后,直接放在函数findboard中,所以可以直接在函数findboard上方进行实现,免去了在game.h里声明
-
函数的实现:
(1)首先我们需要获取玩家输入的坐标位置周围雷的个数,如果没有雷的话,也就是show[x][y]==‘0’时,把这个位置改成字符空格,这样更加好看
(2)执行完(1)的操作,相当于就是已经排查了一个坐标,我们要win++一次,如果还不了解指针的可以先照抄,学习后再来理解,反正在这里只用了很简单的指针知识,如下:
//指针变量int* ptr
//用来接收win传来的地址
//对它解引用就找到了win
(*ptr)++
(3)随后我们开始思考该怎么递归,我们可以这样想,想要实现这个函数的功能,我们不能一口气直接完成,需要一步一步来,也就是把大事化小,如果当前坐标周围没有雷,我们可以以当前坐标为中心,进行扩展,一个一个找出周围的坐标,将它们当作新的中心进行扩展,如图:
这样我们就将大问题化为了小问题,把扩展中心1周围的雷,化解为找多个中心,扩展它们周围的雷,形成递归,因为解决中心1和解决中心2的扩展是同类问题,方法相似,只要写出解决中心1的扩展,也就解决了其它中心的扩展,难点在于怎么找其它的中心呢?
(4)我们可以定义一个i和j,表示新的中心的行和列,这时候我们可以用一个循环,找出中心1周围的坐标的行和列,然后将它们作为新的中心进行递归,如下:
for(i=x-1;i<=x+1;i++)
{
for(j=y-1;j<=y+1;j++)
{
}
}
这下我们就找出了坐标为x,y周围的所有坐标,可以将它们当作新的中心
(5)我们现在开始思考整个递归的模型,我们说过递归一定要有尽头,有限制条件,每进行一次递归就要越来越靠近这个条件,我们可以称为递归的出口,经过思考,我们一定是要坐标周围没有雷,也就是show[x][y]!=‘ ’这个条件,如果这个条件满足,说明周围至少有一个雷,那么我们就可以返回了,不要递归下去了,由于每个中心的周围都只有8个坐标,再加上有雷的存在,所以迟早遇到某一个中心周围有雷,那么递归就开始返回了,不会死递归,如:
if (show[x][y]!=' ')
{
return;
}
(6)接下来思考从哪里开始递归,也就是递归的入口,经过前面的分析,很显而易见,我们要把递归的入口放在刚刚那个循环里面,这样构成了循环递归,将每一个周围的坐标作为中心,向周围扩展,但我们需要注意一些问题
- 我们的递归不能让数组越界,也就是我们的新中心show[i][j],不能越界,必须满足
i>=1 && i<=row && j>=1 && j<=col - 我们不能重复递归,比如2,4这个坐标已经做过中心了,如果递归递归着,2,4突然又做了中心,就重复递归了,就像使用递归求第n个斐波那契数一样,重复递归太多次导致效率太低,甚至低到我们不能接受,所以我们使用了循环,也就是迭代,那我们怎么避免重复递归呢?我们只需要判断一下show[i][j]是不是字符 *,如果是字符 ,说明这个坐标肯定还没有迭代过,可以放心迭代,所以我们可以再加一个条件show[i][j]=='',然后进入递归,如:
//循环递归:
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col)
{
//递归入口:
exdboard(show, hide, i, j, row, col, ptr);
}
}
}
(7)经过我们的努力分析,现在我们基本上可以将这个函数构建出来了,参考代码如下:
//查看排查雷的那个坐标周围是否有雷,如果至少有一个雷就直接显示有几个雷
//如果周围没有雷,那么就对周围进行扩展
void exdboard(char show[Rows][Cols], char hide[Rows][Cols], int x,int y ,int row,int col, int* ptr)
{
int i = 0;
int j = 0;
int ret = getcount(hide, x, y);
show[x][y] = ret + '0';
(*ptr)++;
if (show[x][y] == '0')
{
show[x][y] = ' ';
}
//递归出口:
if (show[x][y]!=' ')
{
return;
}
else
{
//循环递归:
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col)
{
//递归入口:
exdboard(show, hide, i, j, row, col, ptr);
}
}
}
}
}
- 函数的使用:我们可以直接放在排查雷函数中,如图:
三、标记雷函数
简单思路就是,每当玩家排查一次雷后,就询问是否要