前段时间做了一道题,要求实现汉诺塔游戏的自动解题动画:

汉诺塔游戏应该都了解规则:

1、将盘子全部移动到塔C
2、每次只能移动一个圆盘;
3、大盘不能叠在小盘上面。

要求由用户输入盘子的数量,绘制盘子和塔,点击开始后自动解题,并以动画移动盘子的形式演示。

觉得还挺有意思的,而且在做的过程中也踩了一些坑,用了一些技巧和优化,因此记录下来。

效果:

汉诺塔解法

这道题中汉诺塔的解法本身并不是难点。

1、如果只有一个盘子,那就直接从A移动到C;
2、如果有两个盘子,那就要先把小盘子移动到B,然后大盘子移动到C,再把小盘子移动到C;
3、如果有三个盘子,那就要先把上面两个盘子移动到B(借助C的辅助),然后把底下的大盘子移动到C,然后把B上的两个盘子借助A移动到C;
……
4、如果有n个盘子,那就要先把上面n-1个盘子移动到B(借助C的辅助),然后把底下的大盘子移动到C,然后把B上的n-1盘子借助A移动到C。

综上所述,除了一个盘子的情况直接移动,其余都需要借助其他盘子的帮助,复杂情况虽然不一样,但是过程是递归不断重复的。

递归代码如下:

// 确定提交
- (void)submit {if ([self.numberField.text isEqualToString:@""]) {NSLog(@"未输入内容");UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:@"您还未输入层数!" preferredStyle:UIAlertControllerStyleAlert];UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {}];[alertController addAction:okAction];[self presentViewController:alertController animated:YES completion:nil];} else {self.diskNumber = [self.numberField.text integerValue];self.moveCount = 0;[self hanoiWithDisk:self.diskNumber towers:@"A" :@"B" :@"C"];NSLog(@">>移动了%ld次", self.moveCount);}
}// 移动算法
- (void)hanoiWithDisk:(NSInteger)diskNumber towers:(NSString *)towerA :(NSString *)towerB :(NSString *)towerC {if (diskNumber == 1) {// 只有一个盘子则直接从A塔移动到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 递归把A塔上编号1~diskNumber-1的盘子移动到B塔,C塔辅助[self move:diskNumber from:towerA to:towerC];// 把A塔上编号为diskNumber的盘子移动到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 递归把B塔上编号1~diskNumber-1的盘子移动到C塔,A塔辅助}
}// 移动过程
- (void)move:(NSInteger)diskIndex from:(NSString *)fromTower to:(NSString *)toTower {NSLog(@"第%ld次移动:把%ld号盘从%@移动到%@", ++self.moveCount, diskIndex, fromTower, toTower);
}

三层盘子时:

四层盘子时:

可见算法是正确的,接下来就是实现绘制和动画的问题。

绘制塔和盘子

解决了算法的问题,下一步我们要绘制图形了。

这里为了方便我决定全部用UIView来做,比如塔就是一横一竖两个UIView,每个盘子都是一个UIView。

为了方便给盘子编号,创建一个继承自UIView的盘子类,加上编号属性:

#pragma mark - Disk Model
// 自定义的盘子模型,在UIView基础上加上编号属性
@interface OXDiskModel : UIView
@property NSInteger index;
@end@implementation OXDiskModel@end

因为这个代码很短,没必要新开一个文件,直接在绘制图形的ViewController.m文件中加上这个代码就可以实现了。

对于塔,一开始我直接在界面上绘制三个塔的6条线,很简单,但是在涉及到动画的时候,需要频繁用到每个塔的位置以及塔上已有的盘子数量才能确定盘子移动到的位置,这就很麻烦,而且不稳定,代码很复杂。

后来我改成把塔也抽象出来成一个塔类,在其类中绘制两条线,并且加上塔名称以及塔上盘子数量的属性,这样就可以直接调用了,在递归算法中,我们可以直接传递三个塔对象,可以很方便地计算,减少了大量的代码,代码结构也更加清晰。

塔的绘制代码和属性就不写出来了,有单独的类文件,可以直接在工程中看,这里直说思想,对于一些适合抽离出来的对象,我们应该尽可能抽象成对应的类,将它的操作、行为、属性等放在类中写,可以极大地简化代码、使代码结构更清晰。

这样,我们就可以根据屏幕大小算出每个塔合适的大小,然后去创建三个塔对象,添加到界面上就好了。

// 三座塔
- (void)initThreeTower {// 添加三座塔NSInteger height = (SCREENHEIGHT - 150)/3 - 30;for (int i = 0; i < 3; i++) {OXTowerView *tower = [[OXTowerView alloc] initWithFrame:CGRectMake((SCREENWIDTH-250)/2, 130 + (height+30)*i, 250, height+5)];tower.diskNumber = 0;[self.view addSubview:tower];[self.towerArray addObject:tower];// 塔号UILabel *towerLabel = [[UILabel alloc] initWithFrame:CGRectMake(12, tower.frame.origin.y + height + 5, SCREENWIDTH-24, 15)];switch (i) {case 0:towerLabel.text = @"A";tower.towerId = @"A";tower.diskNumber = self.diskNumber;// 一开始盘子都在塔A上break;case 1:towerLabel.text = @"B";tower.towerId = @"B";break;case 2:towerLabel.text = @"C";tower.towerId = @"C";break;default:break;}towerLabel.textColor = [UIColor darkGrayColor];towerLabel.textAlignment = NSTextAlignmentCenter;towerLabel.font = [UIFont systemFontOfSize:14];[self.view addSubview:towerLabel];}
}

然后根据输入的盘子层数,动态算出每个盘子合适的高度以及每个盘子的宽度(从大到小),放在第一个塔上:

// 初始放置盘子
- (void)initWithDiskPut {NSInteger towerHeight = (SCREENHEIGHT - 150)/3 - 40;NSInteger diskHeight = towerHeight / self.diskNumber;// 盘子高度// 依次放置盘子for (int i = 0; i < self.diskNumber; i++) {NSInteger diskWeight = 230 - 30*i;// 盘子宽度// 自定义的盘子模型类OXDiskModel *disk = [[OXDiskModel alloc] initWithFrame:CGRectMake((SCREENWIDTH-diskWeight)/2, 140 + diskHeight*(self.diskNumber-i-1), diskWeight, diskHeight)];disk.backgroundColor = [UIColor yellowColor];disk.layer.borderColor = [[UIColor darkGrayColor] CGColor];disk.layer.borderWidth = 1;disk.index = self.diskNumber - i;[self.view addSubview:disk];[self.diskArray addObject:disk];}
}

动画解题

在绘制过程中我们充分利用了面向对象编程的思想。现在来到最后一个问题,把算法和动画结合起来。

算法还是那个算法,在之前的算法中,我们传递的参数只是简单的字符串来代替三个塔,盘子也只是用盘子编号来代替,这里我们就要用我们的塔对象和盘子对象来作为真正的参数传递了。

对于塔,我们直接传递塔对象;对于盘子,我们传参还是传盘子编号,但是我们用一个数组记录所有盘子,然后循环找到当前要移动的对应编号的盘子。

盘子的移动动画我们使用简单的UIView动画就可以实现了,关于UIView基础动画可以看这篇文章:传送门:iOS基础动画教程。

在动画block中,我们去改变盘子的center,也就是中心点的Y坐标,来达到移动的目的,如何计算出要移动到哪呢?从参数中我们可以知道要移动到哪个塔,根据塔的属性可以知道塔上现在有多少个盘子,那么就可以根据塔的坐标、塔上盘子的数量、每个盘子的高度来计算出这个盘子要移动到哪个坐标了。

UIView动画有一个completion block,用来在动画完成后执行一些操作,上面我们要用到塔上的盘子数量,那在移动完后我们一定也要更新每座塔的数量,移走的塔数量减一,移到的塔数量加一。

这里就可以体现把塔作为对象的好处了, 试想一下不这么做,我们如果要知道每座塔的坐标以及每座塔上的盘子数量,一定要用数组去记录,而且传参时我们只能像最开始一样传递塔名字符串,那还得根据这个字符串来判断改变数组中的第几个元素的塔数量,获取哪个塔坐标,这都增加了很多代码量。但是有了塔对象,我们可以直接作为参数传递,也可以直接获取盘子数量去修改,太方便了。

// 开始
- (void)start {self.moveCount = 0;[self hanoiWithDisk:self.diskNumber towers:@"A" :@"B" :@"C"];NSLog(@">>移动了%ld次", self.moveCount);
}// 移动算法
- (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC {if (diskNumber == 1) {// 只有一个盘子则直接从A塔移动到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 递归把A塔上编号1~diskNumber-1的盘子移动到B塔,C塔辅助[self move:diskNumber from:towerA to:towerC];// 把A塔上编号为diskNumber的盘子移动到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 递归把B塔上编号1~diskNumber-1的盘子移动到C塔,A塔辅助}
}// 移动过程
- (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {NSLog(@"第%ld次移动:把%ld号盘从%@移动到%@", ++self.moveCount, diskIndex, fromTower, toTower);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {[UIView animateWithDuration:1.0 animations:^{// 计算改变盘子位置} completion:^(BOOL finished) {if (finished) {// 动画完成// 更新塔上的盘子数量fromTower.diskNumber--;toTower.diskNumber++;}}];}}
}

这里有一个有意思的点可以看一下移动算法中后面三个塔参数前面是没有文字的,只有一个冒号,OC支持定义方法时参数前不需要一定要有文字,只不过为了方便理解都会加一个参数说明。

到此,是不是问题都解决了?不是的,如果你直接这么写,运行后会发现所有动画都一起移动到塔C,根本没有过程!这是为什么?

因为算法运行得很快,而动画需要时间,这就导致还没开始动画,所有的算法都计算完了,最后只会把所有盘子一起移动到塔C,因为那就是算法最后算出来的目标位置。

这时我想到的第一个方法是用dispatch_semaphore_t来做为信号量,控制算法等待动画完毕后再进行,用法说明可以看这篇文章:传送门:iOS之利用GCD信号量控制并发网络请求,比如像下面这样:

// 移动过程
- (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {dispatch_semaphore_t sema = dispatch_semaphore_create(0);// 初始化信号量为0NSLog(@"第%ld次移动:把%ld号盘从%@移动到%@", ++self.moveCount, diskIndex, fromTower, toTower);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {[UIView animateWithDuration:1.0 animations:^{// 计算改变盘子位置} completion:^(BOOL finished) {if (finished) {// 动画完成// 更新塔上的盘子数量fromTower.diskNumber--;toTower.diskNumber++;dispatch_semaphore_signal(sema);// 增加信号量,结束等待}}];break;}}dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// 信号量若没增加,则一直等待,直到动画完成
}

运行后会发现动画干脆都不动了,为什么?因为动画在主线程,信号量等待也在主线程,那就造成了“信号量等待信号才能继续往下进行<–>动画在主线程中被信号量卡主等待,无法进行,但是进行完了才能给出信号量”的循环等待。

这怎么解决?其实看上面的解释就能够想到办法了,把算法放到分线程去跑,动画放在主线程!这样信号量等待是让分线程等待,不会影响主线程,这样就不会阻塞,同时可以实现算法等待动画完毕后再进行的效果,完美:

// 开始移动
- (void)beginMove {self.moveCount = 0;WeakSelfdispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{// 到分线程去处理算法StrongSelfif (strongSelf) {[strongSelf hanoiWithDisk:strongSelf.diskNumber towers:[strongSelf.towerArray objectAtIndex:0] :[strongSelf.towerArray objectAtIndex:1] :[strongSelf.towerArray objectAtIndex:2]];}});//    NSLog(@">>移动了%ld次", self.moveCount);}// 移动算法
- (void)hanoiWithDisk:(NSInteger)diskNumber towers:(OXTowerView *)towerA :(OXTowerView *)towerB :(OXTowerView *)towerC {if (diskNumber == 1) {// 只有一个盘子则直接从A塔移动到C塔[self move:1 from:towerA to:towerC];} else {[self hanoiWithDisk:diskNumber-1 towers:towerA :towerC :towerB];// 递归把A塔上编号1~diskNumber-1的盘子移动到B塔,C塔辅助[self move:diskNumber from:towerA to:towerC];// 把A塔上编号为diskNumber的盘子移动到C塔[self hanoiWithDisk:diskNumber-1 towers:towerB :towerA :towerC];// 递归把B塔上编号1~diskNumber-1的盘子移动到C塔,A塔辅助}
}// 移动过程
- (void)move:(NSInteger)diskIndex from:(OXTowerView *)fromTower to:(OXTowerView *)toTower {dispatch_semaphore_t sema = dispatch_semaphore_create(0);// 初始化信号量为0NSLog(@"第%ld次移动:把%ld号盘从塔%@移动到塔%@", ++self.moveCount, diskIndex, fromTower.towerId, toTower.towerId);for (OXDiskModel *disk in self.diskArray) {if (disk.index == diskIndex) {WeakSelfdispatch_async(dispatch_get_main_queue(), ^{// 切回主线程进行移动动画[UIView animateWithDuration:1.0 animations:^{StrongSelfif (strongSelf) {// 改变盘子的位置CGPoint diskCenter = disk.center;NSInteger towerY = 10 + toTower.frame.origin.y;NSInteger towerHeight = toTower.frame.size.height-15;NSInteger diskHeight = towerHeight / strongSelf.diskNumber;// 每个盘子高度NSInteger hasDiskHieght = diskHeight * toTower.diskNumber;// 已放置了的盘子高度diskCenter.y = towerY + (towerHeight - hasDiskHieght) - diskHeight/2;disk.center = diskCenter;}} completion:^(BOOL finished) {if (finished) {// 动画完成StrongSelfif (strongSelf) {// 改变fromTower的盘子数量fromTower.diskNumber--;// 改变toTower的盘子数量toTower.diskNumber++;dispatch_semaphore_signal(sema);// 增加信号量,结束等待}}}];});break;}}dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);// 信号量若没增加,则一直等待,直到动画完成
}

这时候再运行就可以完美实现效果了:

为了解决阻塞的问题,还尝试过延迟执行、动画队列等方法,但都不如这个方法简单有效。

在做这个的过程中,用到了很多小技巧,也多次优化了代码,对于我自己来说代码越来越赏心悦目,实在是一次很好的学习训练的经验。

而且看着自己做的汉诺塔游戏自动动画解题很有意思不是嘛!


示例工程:https://github.com/Cloudox/OXHanoiDemo
版权所有:http://blog.csdn.net/cloudox_

汉诺塔自动解题动画中的iOS开发技巧相关推荐

  1. 汉诺塔实践python_汉诺塔的python 动画演示

    1.简介 古代有一座汉诺塔,塔内有3个座A.B.C,A座上有n个盘子,盘子大小不等,大的在下,小的在上,如图所示.有一个和尚想把这n个盘子从A座移到C座,但每次只能移动一个盘子,并且自移动过程中,3个 ...

  2. python汉诺塔递归编程_python中的汉诺塔递归算法的具体运算过程是怎样的?

    关键点:不要多想,不要像人类一样整体来看,盲人摸象即可. 以最开始提出此猜想的 [传说越南河内某间寺院有三根银棒,上串 64 个金盘] 为例. 三根银棒我们设为A,B,C. 欲将A棒上的64个金盘移动 ...

  3. hannoi塔java程序_基于Java实现的Hannoi汉诺塔自动演示程序

    1 设计要求 设计GUI界面的Hannoi塔.汉诺塔中有三个座,名字分别为A,B,C.初始状态时A塔上有3个大小不等的盘子,这些盘子从座底到座顶按着大小顺序依次摆放在A座上.用户可以用鼠标选中盘子,然 ...

  4. 汉诺塔c语言源程序步骤,汉诺塔问题的算法分析及C语言演示程序的实现

    摘要:该文对经典的"汉诺塔"问题进行了详细的分析,并用C语言实现.通过问题的具体实现,使学习者了解问题的全过程,推广到一般. 关键词:汉诺塔;递归;C语言 中图分类号:TP301. ...

  5. Python 算法之递归与尾递归,斐波那契数列以及汉诺塔的实现

    文章目录 递归概念 递归要素 递归与迭代的区别 示例一:阶乘 示例二:斐波那契数列 示例三:汉诺塔问题 尾递归 Python 中尾递归的解决方案 递归概念 递归:程序调用自身的编程技巧称为递归( re ...

  6. 汉诺塔递归的c语言实现(递归)

    对于递归来讲, 汉诺塔实际是经典到不能再经典的例子了,   每个数据结构的教材对会提到. 但是到最后只给出一段类似下面的一段代码: #include<stdio.h>void move(i ...

  7. 汉诺塔+汉诺四塔(C/C++)

    目录 汉诺塔 1  简介 2  代码思路 2.1  对于次数的理解 2.2  对于移动的理解 3  代码 4  加深理解 汉诺四塔 1  思路 2  代码 汉诺塔 1  简介 汉诺塔(Tower of ...

  8. 算法:递归启蒙-汉诺塔

    基本所有的讲递归的书和视频都会以汉诺塔作为开始,因为它足够经典 汉诺塔问题要求整个挪动的过程中都符合小压大的原则,就是如果同一个柱子上有超过1个的话,那必须下面是最大的,上面依次变小,不能出现大盘压小 ...

  9. 汉诺塔(Hanoi)问题归纳总结

    一.汉诺塔问题及其递归算法 1.问题阐述 经典汉诺塔: 外文算法书对汉诺塔问题的描述: 2.算法步骤 三阶汉诺塔问题解题步骤 共需7步. 四阶汉诺塔问题解题步骤 共需15步 五阶汉诺塔问题解题步骤 可 ...

最新文章

  1. MySQL group replication
  2. Tomcat server.xml配置文件介绍
  3. Kotlin 和Spring WebFlux
  4. 在一个list中增加一个节点
  5. 互联网产品之百万级高并发技术整体架构
  6. 新手入坑自动驾驶,从 JetBot 机器人小车开始,能学到什么?
  7. 64位浮点数_浮点数误差
  8. win7卡在正在启动windows界面_Windows系统电脑卡在开机界面进不去系统解决方法
  9. 计算机音乐我还是曾经那个少年,我还是从前那个少年是什么歌
  10. python二元一次方程组用鸡兔同笼的思路来写编程_3应用二元一次方程组——鸡兔同笼教学设计...
  11. django 1.11 文档
  12. 上海交通大学学生生存手册
  13. 时代落在英伟达身上的是粒什么沙,国产GPU的机会又在哪?
  14. 在unbuntu16.04上安装网易云音乐
  15. 稀释消费次元壁,华为的移动生活新范式
  16. 厦大C语言上机 1367 几何级数的计算机验证法
  17. 【javascript】设计模式
  18. 电脑播放器哪个最好用
  19. What Is An NFT? Non-Fungible Tokens Explained
  20. cmd cd 无法切换目录_CMD 常用命令总结

热门文章

  1. 博弈论基础知识: 巴什博奕+斐波那契博弈+威佐夫博奕+尼姆博弈(及Staircase)
  2. AtCoder Beginner Contest 190 D - Staircase Sequences
  3. [SSD核心技术:FTL 13] 不求同生,但求同死?固态硬盘闪存磨损均衡技术详解
  4. matlab 读取bin文件显示图片
  5. mysql 3306_允许远程链接mysql,开放3306端口
  6. 刺猬文│以太坊交易信息解析方法
  7. Word动态输出多表格方案(以工资条为例)
  8. Forsage系统源码分析
  9. APP用户界面设计六基本原则
  10. 加油站都需要什么手续_企业自备加油站需要办理哪些手续