前言
在上一篇文章《从“一碰就碎”到“铜墙铁壁”》中,我的扫雷游戏 V3.0 算是彻底竣工了。但是在回顾布雷模块时,我产生了好奇:同样是随机生成 10 个雷,到底哪种算法最快?
大一新生往往只知道“能跑就行”,但作为软件工程专业的学生,我觉得有必要用数据说话。于是,我掏出 clock() 函数,对三种经典的布雷算法进行了一次“真刀真枪”的性能实测。
🎯 参赛选手介绍
本次测试的三位选手分别是:
- 随机碰撞法(Random Collision):最符合直觉的写法,闭着眼睛扔飞镖。随机生成坐标,如果有雷就重试,直到凑够数量。
//1.随机碰撞法
void set_mine_collision(char address[TOTAL])
{
int count = MINECOUNT;
while (count)
{
int pos = rand() % TOTAL;
if (address[pos] != '1')
{
address[pos] = '1';
count--;
}
}
}
- Knuth-Durstenfeld 洗牌法(Fisher-Yates):现代洗牌的标配。先把前 N 个格子标记为雷,然后从后往前,每次与前面(包括自己)的随机位置进行等概率交换。
//2.洗牌算法(Knuth-Durstenfeld Shuffle)(等概率)
void set_mine_shuffle(char address[TOTAL])
{
for (int i = 0;i < MINECOUNT;i++)
{
address[i] = '1';
}
for (int i = TOTAL - 1;i > 0;i--)
{
int j = rand() % (i + 1);
char temp = address[i];
address[i] = address[j];
address[j] = temp;
}
}
- Naive 换牌法(朴素换牌):很多人以为的“洗牌”,实际上是把所有格子随机打乱,遍历每一个位置,与全局随机的一个位置交换。
//3.换牌算法(Naive Shuffle)(不严格等概率)
void set_mine_swap(char address[TOTAL])
{
for (int i = 0;i < MINECOUNT;i++)
{
address[i] = '1';
}
for (int i = 0;i < TOTAL;i++)
{
int j = rand() % TOTAL;
char temp = address[i];
address[i] = address[j];
address[j] = temp;
}
}
💡 设计思考:为什么布雷算法要用一维数组?
看到这里的代码,可能有读者会问:扫雷明明是一个二维棋盘,为什么不直接在二维数组上布雷,反而要多此一举用一个一维数组 address 呢?
这其实是为了隔离变量并让算法更纯粹,主要有以下三个考量:
- 算法与数据的降维解耦:
无论是随机碰撞还是洗牌算法,它们在本质上处理的都是线性的连续数据。使用一维数组,我们可以直接通过rand() % TOTAL获得一个纯粹的随机索引,洗牌算法的swap操作也只需要最简单的temp = address[i]。 - 保证性能测试的公平性:
如果直接在二维数组上布点,生成一次随机坐标需要rand() % ROW + 1和rand() % COL + 1两次取模运算。为了避免生成二维坐标带来的额外运算开销干扰我们对“布雷算法本身”的性能测试,一维数组是最公平的基准线。 - 一维到二维的完美映射:
一维数组布的雷,可以通过简单的数学公式r = i / COL + 1(除法代表“满几个换一行”),c = i % COL + 1(取余代表“该行排第几个”) 完美映射回二维数组的对应位置。这种“在一维空间算,在二维空间用”的降维打击思路,在图形学、矩阵计算中是非常常见的高效做法(虽然我还没用过)。
打个通俗的比方:一维数组就像是一副洗好的扑克牌,洗牌算法处理它最顺手;而二维数组是摊在桌面上的牌阵,用来给玩家点选。我们现在的操作就是“在后台洗完牌,再整齐地摆到桌面上”。
🛠️ 搭建测试框架(没有指针的无奈)
因为还没学函数指针,我只能用最原始但也最直观的方法:把每种算法跑 10000 次,用 clock() 记录总耗时。
小贴士:在每次调用布雷算法前,必须用 init_board 把棋盘全置为 '0',否则碰撞法在第二次循环时会面对一个充满雷的棋盘,直接死循环。
int test_times = 10000;
clock_t start, end;
double ms;
//==========1.随机碰撞法==========
start = clock();
for (int i = 0;i < test_times;i++)
{
init_board(mine, '0');
init_address(address);
set_mine_collision(address);
get_mine(mine, address);//布雷
}
end = clock();
ms = (double)(end - start) / CLOCKS_PER_SEC * 1000.0;
printf("随机碰撞法:测试 %d 次,耗时 %.2lf 毫秒\n", test_times, ms);
//==========2.洗牌算法(Knuth-Durstenfeld Shuffle)==========
start = clock();
for (int i = 0;i < test_times;i++)
{
init_board(mine, '0');
init_address(address);
set_mine_shuffle(address);
get_mine(mine, address);//布雷
}
end = clock();
ms = (double)(end - start) / CLOCKS_PER_SEC * 1000.0;
printf("Knuth洗牌法:测试 %d 次,耗时 %.2lf 毫秒\n", test_times, ms);
//==========3.换牌算法(Naive Shuffle)==========
start = clock();
for (int i = 0;i < test_times;i++)
{
init_board(mine, '0');
init_address(address);
set_mine_swap(address);
get_mine(mine, address);//布雷
}
end = clock();
ms = (double)(end - start) / CLOCKS_PER_SEC * 1000.0;
printf("Naive换牌法:测试 %d 次,耗时 %.2lf 毫秒\n", test_times, ms);
🤔 玄学现场:为什么测出来全是 0?
第一次按下运行键,结果直接给我整不会了。三种算法跑 100 次,耗时全是 0.00 毫秒。

破案分析:
- 精度问题:Windows 下
clock()的精度是毫秒级(CLOCKS_PER_SEC等于 1000)。现在的 CPU 跑 100 次扫雷布雷连 1 毫秒都用不到,自然测不出来。 - Debug 模式:VS 默认是 Debug 模式,编译器塞了一大堆安全检查代码,拖慢了速度。
将测试次数改成 10000 次后,终于抓到了时间差:

🚀 极限施压:当碰撞法“原形毕露”
在低密度(81 个格子 10 个雷)时,三种方法耗时其实大差不差。因为碰撞法撞墙的概率很低,while 循环重试的次数极少。
但是,当我把 MINE_COUNT 提高到 70(相当于 81 个格子放 70 个雷,丧心病狂的高密度)时,差距瞬间拉开:
· 洗牌法依然稳定在 16ms 左右,因为它的循环次数是固定的 O(N)。
· Naive 换牌法依然稳如老狗,也是 16ms。
· 随机碰撞法耗时直接起飞(或卡顿明显)。因为雷密度高达 86%,每次找随机位置几乎必定撞墙,重试次数呈指数级上升。

🔬 硬核探讨:每次运行结果都不一样?
在测试过程中我发现,即便是同样的代码,每次跑出来的结果都会有 1~2 毫秒的浮动。

这其实是所有做性能测试的人都会遇到的“玄学”:
- 操作系统调度:后台挂着微信、浏览器,CPU 随时会被其他进程抢走时间片。
- 时钟精度限制:1 毫秒的误差在 1000 次的样本下被放大。
- 随机数的薛定谔状态:碰撞法的运行时间完全取决于
rand()函数的运气。
🏆 最终结论与心得
通过这次实测,我不仅坐实了 洗牌算法 O(N) 的绝对稳定性,也更深刻理解了算法选择对程序健壮性的影响。
在平时的小游戏里,随便用 while 撞墙也许没关系;但在实际工业级的高并发、高密度数据场景下,任何不可控的概率重试,都可能成为系统崩溃的导火索。
最后,用我在上一篇博客里的话结个尾:
代码是写出来的,但好代码是“改”出来的。 从碰撞到洗牌,变的是几行代码,提升的是对底层逻辑的敬畏心。
—— 期待早点学完指针,用函数指针把测试框架重构得更优雅的 大一新生。
本文通过C语言实现并实测三种布雷算法:随机碰撞法、Knuth-Durstenfeld洗牌法(Fisher-Yates)和朴素换牌法。测试表明,在低雷密度下三者性能接近,但高密度时随机碰撞法因重试次数激增导致性能急剧下降,而两种洗牌法保持O(N)稳定时间复杂度。实验采用一维数组建模以消除二维坐标计算干扰,强调算法设计对工业级系统健壮性的影响。

301

被折叠的 条评论
为什么被折叠?



