这个经典练习是一个教材上看到的, 在队列(queue)部分呈现。
第一次根据书上的代码实现失败「本以为很容易」,过了一段时间再次看时候意识到书上的为代码太繁杂了,它的思路是用两个“事件”类型的LinkedList,一个装“到达事件”(有顾客到),另一个装“离开事件”(办完走了)。然而每个时间class要储存时间类型、到达或离开时间、业务办理时间,还要在到达之后转换成离开事件并在“离开事件”里重新排队。这么一折腾一个数据结构转换的细节出问题整个模拟就不能成功。
我的想法是把原书以队列结构为基础的实现变成时间线上的事件先后,模拟一个以时间线为基础的事情人们自然想到的是时间一点一滴地走,但除非直接利用具体时间间隔比如每秒、每分钟「生物系统、微观粒子变化」,时间线给我们的是时间前后的比较。编程练习完成后了解到原来这是“事件驱动”的范式!
把所有事件排序,一次处理一个,如果最临近的是到达就处理到达,最临近的是离开就处理离开。到达肯定先于离开处理玩,之后就只剩离开的。
跟原书不变的一点是,所以到达事件都是实现知道的。
在数据类型的处理上,把到达和离开的时间用一个数值来表示,然后如何区分呢?到达事件放在到达队列里,离开事件放在离开队列里,通过时间所在队列加以区分,原书的思路是所有事件排成一个队列,我的试验经验是:时事件的排队反而是最关键最难的,要把它作为程序运行的主线。
这样程序利用“双队列”通过最近事件的时间(到达或离开)进行比较,就完成了模拟——版本1。
版本2是1的升级版,加入了等待时间统计和模拟休息「很多事业单位非常喜欢休息,动不动就牌子一放,写着休息xx分钟/x点x分回来」,这样复杂性比1高了不少。休息/服务暂停让随后的离开事件全部发生变化,等待时间全加上了休息时间,而且考虑到避免大批修改队列造成的运算成本,这样版本1基于所有事件排好序「有到达就排上离开」的模式就不适用了。
于是版本2变成了“一次处理一个事件”的模式,其实是1的升级版,预处理临近的到达和离开,把最临近的处理,然后如果刚刚处理的是到达就基于这个时间把它转换成离开事件「“时间”修改成 最后处理完成时间+该事件的等待时间」。然后“离开事件”的队列成了若干临近到达事件处理完后的缓存,这样大大方便了随机休息的模拟——已在离开队列中的事件全部顺延即可,没有处理的到达依旧处理「假设休息期间仍可以到达」。
在这个大体思路基础上的代码实现可谓一波三折,尤其是版本2,具体细节如果小伙伴看了代码「希望有人看~~」后有兴趣再探讨,简单总结主要经历的问题:
- 版本2同样基于所有到达事件先于离开时间处理完,而由于只有“最后处理时间”和“当前时间”(后者基于随机休息而产生),如果像版本1那样通过出队列(remove)来处理则无法在全部到达后存在为最后一个到达正确生成离开事件等问题,于是到达队列中保留所有到达事件并引入了事件序号指针ptr「写本文时意识到这种做法过于繁琐,引入全部达到这个boolean<调试过程中已作为权宜之计引入>和“最后到达时间”int即可!!」,然后出现了很多空指针、指针超出序号范围等问题。。。
--这个问题一度纠结了一个多小时
2 测试过程中发现版本2存在版本1不存在的一种可能:下一个临近到达晚于最近的离开,而此时无人排队,按照程序逻辑此时最近事件仍然是离开,而缺少队列为空,调用离开队列导致空队列异常。解决方法是遇到这种情况就人为赋值一个t2(代表最近离开事件的时间,值为最近到达事件+1,该指会在下一次循环中被覆盖)这样控制流程会转向下一个「写本文时意识到这是由于事件触发条件描述不周密引起的,可以从全局优化处理而不要采取权宜之计」
这两个问题在写本文时意思到都是整体考虑不周全引起的,而2又是1引起的,1的出现是一个设计使用cloneable的细节,本可以迂回解决但当时没想周全就引入了ptr,导致一系列问题。
基于以上,心得如下:
1 伪代码有时给人造成误导,具体表现为一些功能实现的细节要求引入一些变量,最终导致整个程序逻辑出现问题。这在使用库(包括自带库)的时候尤为明显。
2 更清楚stackoverflow这个网站的名称由来了:数组型数据结构(栈、队、列等等)的索引问题常常是较深层逻辑错误导致的,关系程序设计全局。
3 严肃对待每一个变量,避免解决局部问题而引入影响全局的变量,如果看起来非得这么做不可那有可能是全局设计有问题,要重新考虑整体而不是死扣细节。
4 延迟优化,在一些比较复杂的细节中多几行代码有时候让问题更清楚。子程序(subroutine)和数据复用是所有人都会的,忘不了。
5 编程的妙趣之一在于,新产生的问题常常是代码打出来之前没想到的。
放上这两个版本,如果第二个版本修改出了更好的会加上。