代码、桌游、结肠息肉

代码

和郭老师在泰国过完了2017年春节,回深后就很快完成了三个月的试用期,成为一名正式工。我的工作内容也从开发变成了测试,强度相对低了一些,让我松了一口气,不过也面临很多新知识的学习。这段时间,深入学习了 Selenium/PhantomJS 等前端测试工具,感觉前端测试比后端难不少,主要在于测试用例很不容易写完善,往往代码写好了但因为各种网络原因就是无法通过、然后自己再去处理网络超时、页面加载错误之类的问题,似乎前端的不稳定性远远超过后端,需要对各种情况进行处理。
除了学习 Selenium,还在朱老师的推荐下买了一本《单元测试的艺术》,不过这本书以 C# 作为讲解语言,看起来略吃力。同时买了早就想一睹为快的《Python绝技:运用Python成为顶级黑客》,这本书绝对不能带到公司去,不然一定会被同事笑话……

桌游

前段时间由于还没适应程序员的工作强度,完全没有时间玩桌游,现在闲暇时间多起来,已经可以有机会约朋友玩桌游了。原本以为自己对桌游的热情在入坑一年多以后会慢慢消退,没想到不减反增,还有了更大的热情去探索自己早先并不了解的桌游分类。
三周前,加入DM熊猫的SDE(Super Dungeon Explore超级地城探险)团,玩了一次纯模型主题桌游。说实话并不感冒,玩起来和 DND/Pathfinder 的战斗阶段毫无二致,而且自由度低很多,不知道乐趣何在。一下午也没有完结一场战斗,节奏慢的要命,让我对这种战斗为主的模型游戏很失望。手机摄像头坏掉了,拍的照片很渣,将就看一下。
DSC_0604_meitu_1
DSC_0603_meitu_3
看着就很无聊吧?那你的感觉和我一样诶!
不过还好我自己最近也买了一套模型游戏,就是大名鼎鼎的《约德尔战斗学院Mechs VS Minions》!目前在BGG上居然排到主题类桌游第6名!相对价格不菲的SDE,这套桌游的超大箱体、超多模型、5个预喷涂的精致手办(4个英雄+1个超大Boss)简直是在以赔本价的450元人民币销售,难怪 Dice Tower 的节目里3个主持人大约花了1/3的篇幅在感慨《约德尔战斗学院》有多超值……
超大箱体入境!
mmexport1488284721287
mmexport1488284756062
mmexport1488284795099
mmexport1488284813244
mmexport1488284816149
mmexport1488284823344
mmexport1488284829504
mmexport1488284832627
《约德尔战斗学院》是合作类桌游,玩家扮演4个约德尔人驾驶机甲在战斗中完成特定任务。
两周前,张老师、郭老师和我在家中完成了这套游戏的第一个任务(11个任务分别密封在11个牛皮纸信封中),三人需要顶着小兵的猛攻下将炸弹推送至维修站。游戏规则简单,容易上手,同时也非常欢乐。强烈推荐用游戏配件中的沙漏进行计时,会让游戏进程变得异常紧凑,从而产生意想不到的状况。我们三人经常在沙漏的催促下手忙脚乱的做决定,使机甲颠三倒四的乱跑,确实符合约德尔人的不靠谱性格。
mmexport1488641701615
mmexport1488641704824
游戏机制为行动编程,很新颖的机制。由于是 Riot 公司官方出品的 LOL 主题桌游,粉丝量之巨大毋庸置疑,这也可能是这款桌游能在发售后迅速攀登至如此高位的重要原因。不过遗憾的是,游戏设计师在 Reddit 的 boardgame 板块和大家交流的时候表示,这款桌游仅仅是 Riot 的玩票之作,不会成为公司的利润点,言下之意就是估计不会有续作或扩展了,让人非常伤心。
mmexport1488641712168
mmexport1488641721340
除了《约德尔战斗学院》,还买了向往已久的《银河卡车司机》。这是一款欢乐向的策略游戏,玩家在规定时间内拼凑出一家太空卡车,然后进入太空装载货物,期间还会遇到人贩子、陨石等危险,灾难过后飞船往往濒临解体。到达终点时,再根据载货价值和船体损伤,计算每个人的分数。
mmexport1489673558292
mmexport1489673547014
mmexport1489673525801
《银河卡车司机 Galaxy Trucker》有安卓版 App,强烈推荐下载试玩一下,很有趣的游戏!由于刚刚入手,还没有机会试玩,因此没有游戏现场图片,偷一张国外桌游店的照片做演示,见下图。
137832221538

结肠息肉

其实半个月来最大的事情,是做了一场小手术,切除结肠息肉。大约一年前,我右侧肋骨下方在吃完饭后经常会有隐痛,剧烈的时候需要用手抵住,同时最近几个月一直拉肚子。一直以为是胃或胆的问题,2016年12月份做了胃肠镜才知道,胃口没什么问题(其实也有慢性胃炎),关键是结肠里长了一个大息肉,需要手术切除。于是上周二做了手术并一直在医院休养,这周也无法出门,只能在家里上班。
手术后无法进食饮水,完全靠点滴葡萄糖提供能量。连续3天水米未进的我居然并没有饥渴感,大为惊奇。术后4天才能进食流食,同时持续葡萄糖注射。从周一入院到周日出院,一直是郭老师悉心照顾并陪床,我的精神状态很好,反倒是郭老师在几天操劳下迅速憔悴了很多,令人心疼。出院后我俩都大舒一口气。
在家工作的日子也不轻松,除了需要按时坐到电脑前写代码、第一时间对同事的需求做出反馈外,还要时刻对抗自己的懒惰。所以说实话,这一周的工作效率还蛮低的,难怪大家还是要去办公室上班的,程序员可能更是如此。
算了算,手术后至今10天,居然瘦了10斤,主要原因是医嘱提到的饮食规范:不可吃粗纤维的蔬菜和肉,只能吃烂面条、粥等物。我本人的精力在这几天一直很充沛,可见目前的饮食也是可以提供充分的能量,减肥所谓『三分练七分吃』诚不余欺也!希望把这个减肥势头保持下去,不要浪费了这段强制控制饮食的经历。

其他

除了上述的几件事,还有其他一些功课压在我心头。例如一篇关于断网后如何利用互联网的稿件,我已经拖欠了约一个多月,每每动笔都会犯懒,不知为什么。
在泰国清迈期间一口气看了 Neil Gaiman 的《American Gods》前几章后,至今也没再看几页,也颇为焦急,希望自己早点看完。
几本积灰的技术书都还没看几页,总没有一个学习计划去填充自己的知识空白。
一直想写的微信群助手迄今没动笔,只是下载了几个库试着发了几条消息而已。
『编程随想』维护的 Resilio Sync 文件夹(就是那个翻墙软件大合集)里面的翻墙软件版本已经很老旧,我一直想搞一个能及时更新的新同步文件夹出来,也拖了好久。
要做的事情太多了,而我最想做的为什么永远只是玩……

Tagged : / / / / /

关于开发流程的一些体会

工作近5周,共完成了2个项目(第二个项目已经基本完成测试,准备收尾中)。第一个项目,是用爬虫抓取数据,然后做好 API 供用户使用。第二个项目,是扫描僵尸用户,发邮件唤醒,如未唤醒则关停僵尸用户的进程。
做这两个项目的过程中各有各的体会。
首先,是关于解决问题的方式。做第一个项目时,刚刚进入公司,很难适应工程师文化,加上和同事不熟悉,脸皮薄,不愿意问问题,甚至没有仔细确认需求和工程方式就动手开搞,最终耽误了不少时间。在工程上,沟通极为重要,工程师不是低着头闭门造车的,恰恰相反,工程师们是用集体合作的方式共同搭建一个或繁或简的系统,最终完成个体无法完成的大规模工程。这应该是工程师们在一起工作的最重要意义之一吧。
工程被分割成一张张工单,并不意味着其整体被切割成无关联的个体。在完成工作的时候需要尽可能多的理解工程的有机性。例如我将爬下来的时间序列存入数据库时,每条数据的时间都被我存成了str,于是在后期制作 API 的时候就遇到了一些问题,最后需要通过将其转成datetime来解决。类似的问题说明我在完成某件工作时,并不知道这件工作在工程中的位置与意义,这就需要多问和多做了,在对整体熟悉了以后,自然会逐渐清晰。
其次,是关于单元测试的重要性。其实『单元测试』这四个字,有弱化其意义的副作用。我在做第二个项目的时候,由于内部处理数据的逻辑比较复杂,导致大大小小的 Bug 一大堆,每次提交都认为自己已经做得差不多了,但还是在 code review 时被打脸,来来回回提交了若干次,花费大量时间,甚至同事成了我的人肉 Debug 处理器。最后同事说,你还是写一些测试用例吧,覆盖的情况越多越好。
果然,写了一百多行的测试用例后,将单元测试完成,自然发现了一些之前很难发现的 Bug,无论对我还是其他工程师来说,都节省了大量调试改错的时间,多灾多难的第二个任务也随之迎刃而解。经此一役,我对单元测试的意义恍然大悟,其实单元测试并不仅仅是『测试』这么简单。在『测试』的背后,其实是将代码化整为零、各个击破的写作过程,因此单元测试的写作时间,应当与程序本体同步,也即『写一个函数,就写一个测试』,二者几乎是同步完成的。这样看上去花了很多时间在设计测试用例上,其实是规范了自己的思路和代码,同时大大提高了后期的可维护性。
在明白了第二点(也就是单元测试的真正意义后),觉得自己仿佛突破了一个模糊边界——一个软件工程师和业余代码爱好者的界限。虽然现在代码依然很烂,但是本着对代码负责的态度和对自己职业的尊重,我会把以后写下来的所有代码都同步加上单元测试,配得上一个专业人士应有的严谨。
以上就是我在完成了两个项目后的些微体会。工程师的快乐,是由一个个微小的痛苦组成的。此生竟有幸成为工程师,真是一件幸福的事情。

Tagged : / /

工作三周的碎语

工作满三周,我的第一个任务也交付测试中,如无意外很快就能上线了。三周来,学了很多知识,也逐渐适应了和工程师们一起工作的气氛。刚开始的时候,我一个人憋着不好意思问同事,结果浪费了好几天时间,后来才知道其实工程师之间每天问问题是常态。后来逐渐和大家熟络起来,也就轻松很多了。我项目做得速度挺慢,但是好在同事们都很乐意帮我答疑解惑,倒也不会遇到死胡同。
同事里,有几个南方科技大学的金融、金融数学出身的实习生,水平很不错,读英文文献,写量化策略,甚至还写的一手好 Java/Python,让人刮目相看。南科大这所新学校的教学质量真让人佩服,准毕业生们的素质比当年的我强多了。
三周工作中,每天早晚通勤各一小时,没时间打游戏,也没时间写博客,在任务交付前期压力巨大,总觉得自己拖了团队的后腿。我估计这种感觉随着技术的进步会逐渐消退,但是紧迫感恐怕是所有工程师永远无法摆脱的心头巨石。
所以这周末我不加班了(前两个周末都在加班),好好的打开 steam 玩几盘游戏,看两部电影,陪老婆出去散散步。此时此刻,经常动辄一整年不工作的我才觉得,在打仗般的工程开发以外,温柔日常生活真的是最让人放松和热爱的。
Python 是个好东西,愈发觉得无论毕业于什么专业、目前做什么工作,都要好好学一学编程,编程会给自己提供极大的选择余地,有太多工作可以用代码来完成。今天代码在日程生活中的应用依然太少,如果做到每个人都能拿起编程这个武器来武装自己,那世界或许能美好很多。

Tagged : / /

我的求职经历

2015年下半年,我在一家外贸软件企业任运营总监,某天无意间看了一眼当时公司产品的前端代码,竟在已发布的产品中发现各种注释,而且注释都是51cto之类网站的教学文章链接。那时候想,如果让我来做的话,应该比他们做的更专业一些。在思考了几个月后,2015年11月,我申请离职,开始脱产学习 Python。
『他们不行,我上!』这个理由,其实是我学很多东西的动力。例如两年多前学爵士鼓,就是看侨城堂教会的鼓手实在太水、于是买了 Roland TP4K 开始练习的。后来没有坚持练下去,水平也就一般般,但临时顶场什么的已经无压力了。
断断续续学了大半年 Python,在闭门造车的做了几个小项目后,我从2016年9月开始投简历。最开始在拉勾上被拒了大概二十几遍,只拿到一个面试,是给一家网站做分布式爬虫,面试结果不好,而我对这种纯应用型的部门也不太感冒(虽然自己技术不好,但一直有个目标,就是要去技术型的公司)。
后来觉得这种海投策略不行,郭老师给我提意见,说写个 cover letter 吧。我乖乖的听话,而当时自己完全没想到这个举动会带来后面的巨大收获。

某日在 v2ex 上闲逛,发现 Ricequant 在招聘 Python 工程师,于是研究了一下这家创业公司,发现居然还开源了一个量化策略研究框架,创始人技术也很好,就写了一封长长的 cover letter 过去,居然也混到了一个面试(收到 Ricequant 面试通知电话的时候,我正在上文提到的那家公司面试中)。9月27日来到 Ricequant ,CTO 正和另一个求职者谈话,于是安排了一位工程师来面我,很快工程师觉得我基础太差无法胜任。CTO 此时也闲下来了,简单聊了一下后,留了学习排序算法的作业。后来我整个十一假期都在做这个作业。
十一结束后发邮件交付了作业,以为可以轻松一下了,但1个小时后就收到回信,要求用代码实现所有排序并提了一堆需求。二话不说,接下来一个星期就继续埋头苦干,把之前没搞懂的面向对象、递归、单元测试等等搞定。第2次作业交付后,长舒一口气,但是还是在2天后收到催命邮件,要求我继续优化代码。这一次把算法部分和界面部分分开,同时做多线程优化等等。就这样又忙了5天,交付了第三次作业,同时也第二次去 Ricequant 面试,并现场拿到 offer。我问了 CTO 为什么会这样反复测验和面试我这样的初学者而不是直接筛掉,他说你 cover letter 太有激情了。此处为郭老师鼓掌三分钟。
(在二面 Ricequant 之前,我还收到了腾讯云的电话面试,依旧因技术太差而被直接告知不行,但建议我转投运营开发岗位。面试我的工程师人也很好,但我没有再投腾讯。)
从开始学习 Python,到找到工作,刚好一年。一年间,我的学习速度很慢,又是零基础,对自己的要求也不严格,经常连续半个月没写几行代码。好在朱老师一直鼓励我,说小步快跑是坠吼的,不用强求速度。事实也证明我的确更适合这种轻松的学习状态,而不是苦大仇深的埋头苦学。
我在刚开始入门时报过开智的 Python 入门班,相信我,很垃圾,不要浪费钱。Python 的入门资料很丰富,看书、在 Stackoverflow 和 Google 上查资料、在 Github 上给牛人提 issue,就已经完爆你能找到的所有国内培训课程了,而这一切都是免费的。
学写代码是我这个习惯性半途而废者第一个坚持下来并让我进入职业圈子的事。我依旧是小白一个,要学的东西太多了,对未来很期待也很紧张。但一年的学习让我找到了一种自信,就是无论在哪里都要坚持下去,为了家人,为了自己,如果需要做律师,我就去读法学院,如果需要牙医,我就去读医学院,如果需要木匠,我就拜师学木艺。1年不够就2年,2年不够就10年,总会有实现目标的一天。
今天是2016年的1024程序员节,在耕耘一年后终于有了收获,我很骄傲。

Tagged : / /

Python 中 list 的传值问题

我在做一个小程序,需要生成一个随机数列表,然后将之赋值给2个 list,并需要这两个列表完全独立(即列表值指向不同的内存地址)。最开始是这样的:

>>> a = [5,4,3,2,1]
>>> b = a
>>> print(a,'-',id(a)) # id是 python 查看对象地址的方法
[5, 4, 3, 2, 1] - 4516275720
>>> print(b,'-',id(b))
[5, 4, 3, 2, 1] - 4516275720 # 显然,此时a和b指向同一个地址
>>> b.sort() # sort()是 python 的内置排序函数
>>> print(b,'-',id(b))
[1, 2, 3, 4, 5] - 4516275720
>>> print(a,'-',id(a))
[1, 2, 3, 4, 5] - 4516275720 # 我去,怎么回事???a 怎么也变了?

Python 的变量赋值机制并不是真的给变量赋值(实际上从头到尾都不存在 list 的容器),只是把变量名当做一个标签,贴在内存地址上,当值发生变化的时候,并不是改变值本身,而是将内存地址对应的标签『唰』的一下撕下来,然后贴到另一个地址上——此所谓『铁打的内存,流水的变量名』。
对于复制 list,按照官方文档的说法,应该这样操作:

b = a[:]
# https://docs.python.org/3/faq/programming.html#how-do-i-copy-an-object-in-python

此时是这样的:

>>> a = [5,4,3,2,1]
>>> b = a[:]
>>> print(a,'-',id(a))
[5, 4, 3, 2, 1] - 4516276488
>>> print(b,'-',id(b))
[5, 4, 3, 2, 1] - 4516276232

可见在 b = a[:] 这一过程中,Python 在内存中创建了一个新值,并将 b 的标签贴到了上面,迥异于我们最开始的过程。
其实上述过程并非发生在所有数据类型中。

>>> A = 5
>>> B = A
>>> id(A)
4540535120
>>> id(B)
4540535120
>>> B = 3
>>> A
5
>>> id(A)
4540535120
>>> id(B)
4540535056

关于这个问题,我还没有研究明白,还没搞懂到底哪些变量是这样传值的,算是一个小小的坑吧。
参考资料:

  1. http://stackoverflow.com/questions/8744113/python-list-by-value-not-by-reference
  2. https://docs.python.org/3/faq/programming.html#how-do-i-copy-an-object-in-python
Tagged : /

几种排序算法的比较

整个十一假期就在折腾这几个算法,这篇总结性文章就是简要的介绍了几个基础算法的特性,并附带了 Python 的实现。
不同算法适合不同情况的数组,但在不知道输入规律的时候,使用时间复杂度低算法的比较保险。
Big O:上界
Big Ω:下界
Big Θ:确界

Big O 比较

big_o_complexity
O(n²):冒泡排序(稳定)/选择排序(不稳定)/插入排序(稳定)
O(nlgn)~O(n²):希尔排序(不稳定)
O(nlgn):堆排序(不稳定)/归并排序(稳定)/快速排序(不稳定)

Bubble Sort 冒泡排序

def bubble_sort(arry):
    n = len(arry)                   #获得数组的长度
    for i in range(n):
        for j in range(1,n-i):
            if  arry[j-1] > arry[j] :       #如果前者比后者大
                arry[j-1],arry[j] = arry[j],arry[j-1]      #则交换两者
    return arry

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n)
稳定
辅助空间O(1)

特点

最优情况为全部正序时经过 n-1 次比较即可完成排序,最差情况是倒序。因此冒泡算法对数组的有序性很敏感,适合对规模较小、且比较有序的数据进行排序。[1]

优化方案

  1. 如果某一次遍历没有发生数据交换,则代表已完成排序,可停止迭代。
  2. 记录遍历时最后一次数据交换的位置,后面的数据已经有序,因此可以缩小下次循环的范围。

Selection Sort 选择排序

def select_sort(ary):
    n = len(ary)
    for i in range(0,n):
        min = i                             #最小元素下标标记
        for j in range(i+1,n):
            if ary[j] < ary[min] :
                min = j                     #找到最小值的下标
        ary[min],ary[i] = ary[i],ary[min]   #交换两者
    return ary

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n²)
不稳定
辅助空间O(1)

特点

运行时间与输入状态无关(随机排列与正序排列消耗相同的时间)。同时移动次数最少(与数组大小呈线性关系)。[1]

Insertion Sort 插入排序

def insert_sort(ary):
    n = len(ary)
    for i in range(1,n):
        if ary[i] < ary[i-1]:
            temp = ary[i]
            index = i           #待插入的下标
            for j in range(i-1,-1,-1):  #从i-1 循环到 0 (包括0)
                if ary[j] > temp :
                    ary[j+1] = ary[j]
                    index = j   #记录待插入下标
                else :
                    break
            ary[index] = temp
    return ary

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n)
稳定
辅助空间:O(1)

特点

输入状态会影响运行效率,对有序数组排序要比对随机排列、逆序排列的数组快,因此适用于部分有序的非随机数组。当『数组中每个元素距离它的最终位置都不愿』『一个有序的大数组接一个小数组』『数组中只有几个元素的位置不正确』时,插入算法的速度很可能比其他算法都快。[2]

Shell Sort 希尔排序

def shell_sort(ary):
    n = len(ary)
    gap = round(n/2)       #初始步长 , 用round四舍五入取整
    while gap > 0 :
        for i in range(gap,n):        #每一列进行插入排序 , 从gap 到 n-1
            temp = ary[i]
            j = i
            while ( j >= gap and ary[j-gap] > temp ):    #插入排序
                ary[j] = ary[j-gap]
                j = j - gap
            ary[j] = temp
        gap = round(gap/2)                     #重新设置步长
    return ary

时间复杂度、稳定性

平均情况:O(nlgn)~O(n²)
最坏情况:O(n²)
最好情况:O(n1.3)
不稳定
辅助空间:O(1)

特点

希尔排序将数组分成较短的子数组、并使之部分有序,很适合插入排序。希尔排序对中等大小数组的排序时间可以接受,代码量小,且不需要额外内存空间。当没有系统排序函数可用时,可以考虑先用希尔排序,再考虑是否值得替换为更复杂的排序算法。[3]

Merge Sort 归并排序

def merge_sort(ary):
    if len(ary) <= 1 : return ary
    num = int(len(ary)/2)       #二分分解
    left = merge_sort(ary[:num])
    right = merge_sort(ary[num:])
    return merge(left,right)    #合并数组
def merge(left,right):
    '''合并操作,
    将两个有序数组left[]和right[]合并成一个大的有序数组'''
    l,r = 0,0           #left与right数组的下标指针
    result = []
    while l<len(left) and r<len(right) :
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += left[l:]
    result += right[r:]
    return result

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(nlgn)
最好情况:O(nlgn)
稳定
辅助空间:O(n)

特点

归并排序在最坏的情况下复杂度为O(nlgn),和其他基于比较的排序算法所需的最小比较次数相同。

Quick Sort 快速排序

用递归在 Python 中实现快速排序会遇到 RuntimeError: maximum recursion depth exceeded 的错误提示,原因是 Python 的递归深度默认为1000(可以用 sys.getrecursionlimit() 来查看),有两种方式解决为题。一种是用循环重写算法,另一种比较简单粗暴,直接用 sys.setrecursionlimit(99999) 把递归深度设置为 99999 这种大数字,更详细可参见这里

def quick_sort(ary):
    return qsort(ary,0,len(ary)-1)
def qsort(ary,left,right):
    #快排函数,ary为待排序数组,left为待排序的左边界,right为右边界
    if left >= right : return ary
    key = ary[left]     #取最左边的为基准数
    lp = left           #左指针
    rp = right          #右指针
    while lp < rp :
        while ary[rp] >= key and lp < rp :
            rp -= 1
        while ary[lp] <= key and lp < rp :
            lp += 1
        ary[lp],ary[rp] = ary[rp],ary[lp]
    ary[left],ary[lp] = ary[lp],ary[left]
    qsort(ary,left,lp-1)
    qsort(ary,rp+1,right)
    return ary

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(n²)
最好情况:O(nlgn)
不稳定
辅助空间:O(nlgn)~O(n)

特点

在实际应用中,一般比其他算法快很多,内循环很小,原地排序(仅需要很小的辅助栈),且将长度为 N 的数组排序的时间与 NlgN 成正比。但很脆弱,实际性能会因某些错误变成平方级。[4]
快速排序和归并排序使用分治法和递归进行排序,但快排在合并子数组后是自然有序的大数组;归并在合并阶段则繁琐一些,还要再次进行比较。
在对数组进行切分不平衡时,会导致性能低效(例如第一次从最小元素切分、第二次从第二小的元素切分……)。解决该问题,可以对数组进行随机排序,避免性能下降到极低。

优化

  1. 快排在小数组中比插入排序慢,因此在排序小数组时使用插入排序。
  2. 三取样切分。
  3. 熵最优排序[5]

以上三种优化是《Algorithms 4th》中对于快排性能的优化,都是对算法本身做了一些改进。而最著名的优化则是《算法导论》中提到的『随机化快速排序』,与上述三种性能优化有很大不同。
随机化快速排序,一般来说都是将取主元的过程随机化。随机化快速排序的『优化』,并没有提高快排的性能,而是避免了某些序列使快排性能大幅降低到O(n²)的可能性、防止他人设计一个序列对服务器发起DoS攻击,使排序更加稳定和安全(我发现还有很多人有类似的误解)。事实上随机化快速排序和普通快速排序在时间复杂度上同为O(nlgn),实际实验中也没有显著差异。

知乎上一个用户对快排和随机快排的性能做了测试,但我猜他可能也理解错了,他所做的只是在优化随机快排的代码,但并没有显著提高算法性能或降低复杂度(知乎链接)。Thomas Cormen 在 Quora 中也回答了关于随机化快排和普通快排性能差异的问题(Quora链接),但是他本人在《算法导论》(中文版100页,英文版179页)中并没有把这个问题讲的很透彻,也许算是一个微小的失误。

Heap Sort 堆排序

def heap_sort(ary) :
    n = len(ary)
    first = int(n/2-1)       #最后一个非叶子节点
    for start in range(first,-1,-1) :     #构造大根堆
        max_heapify(ary,start,n-1)
    for end in range(n-1,0,-1):           #堆排,将大根堆转换成有序数组
        ary[end],ary[0] = ary[0],ary[end]
        max_heapify(ary,0,end-1)
    return ary
#最大堆调整:将堆的末端子节点作调整,使得子节点永远小于父节点
#start为当前需要调整最大堆的位置,end为调整边界
def max_heapify(ary,start,end):
    root = start
    while True :
        child = root*2 +1               #调整节点的子节点
        if child > end : break
        if child+1 <= end and ary[child] < ary[child+1] :
            child = child+1             #取较大的子节点
        if ary[root] < ary[child] :     #较大的子节点成为父节点
            ary[root],ary[child] = ary[child],ary[root]     #交换
            root = child
        else :
            break

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(nlgn)
最好情况:O(nlgn)
不稳定
辅助空间:O(1)
2. 特点
目前唯一的能最优利用时间与空间的算法,最坏情况下也能保证 2NlgN 次比较和恒定的辅助空间。当空间紧张时(例如嵌入式),堆排序用几行代码就能实现实现较好性能。
缺点是无法利用缓存,缓存未命中的次数远高于大多数在相邻元素间比较的算法。
sort

通过倍率实验预测程序的增长数量级

可以使用倍率实验来预测任意程序的增长数量级:
每次实验时使输入翻倍,计算时间,并求两次运行时间的比值。反复运行直到比值趋近于2的b次方。[6]

算法的『稳定性』是什么

稳定性指,当排序的元素中有相同的值时,这些具有相同值的元素在排序后的前后位置是否发生变化的性质。如果变化,则不稳定;如果不变化,则稳定。在 Bubble Sort 中,相邻元素互相交换,如果两个相邻元素相等则不需交换;如果两个相等的元素彼此之间有间隔,那么即便它们和相邻元素交换后,其相对的前后位置也不会发生变化,所以是稳定的。而在 Selection Sort 中,第一个元素与最小的元素交换、第二个元素与第二小元素交换等等,间隔有可能很大从而在交换时跳过了相同值的元素,进而造成相同值元素前后位置改变,因此是不稳定的。
排序的键值有可能只是元素的一个属性,如果元素本身还具有其他属性,那么键值相同的元素排序后的相对顺序还与其他属性有关。例如用稳定的算法对员工按工资排序,假如原数组是按年龄排序的,那么月薪同为7000元的3个人在按工资排序后仍然可以保持年龄正序,即最初的相对位置不变。

参考资料:

[1]《Algorithms 4th》 P248
[2]《Algorithms 4th》 P252
[3]《Algorithms 4th》 P262
[4]《Algorithms 4th》 P288
[5]《Algorithms 4th》 P296
[6]《Algorithms 4th》 P121

Tagged : / / / /

万用骰子脚本(跑团专用)

几个月前,我写过一个专门用来玩卡坦岛的命令行骰子工具,里面的骰子函数都是写死的,只能选2d6/2d10/3d4这几个,够用,但是不方便,适用性太差。最近入了《Pathfinder基础包》,准备重新开始跑团,看几个跑团QQ群里都有骰子机器人(方便大家开网团的时候投骰子),他们输入『.r 3d6』『.r 4d10』甚至『.r 1d97』这种实际中并不存在的骰子都可以得到值,自由度非常高。
于是我计划用Python来实现这种高自由度的骰子。首先遇到的问题就是如何让程序识别『3d6』『1d4』『1d8』这种跑团黑话。先普及一下,d4/d6/d8/d10/d20等都指骰子的面数,例如d4指的是四面骰,d20则指二十面骰,1d4指扔1个四面骰,2d6则指扔两个六面骰。普通游戏一般用不到这么多种类的骰子,而在以大量数值检定为核心的TRPG(桌面角色扮演游戏)中,这些骰子就不可或缺了。
首先想到的方法是用正则表达式来解析命令。以最常用的『1d6』为例,『1』指骰子个数,『6』是骰子类型(面数),『d』则是分割二者的分隔符。用正则表达式来写的话,应该是这样:

roll = input('> ')
match = re.search(r'(\d+)([Dd])(\d+)', roll)

先让用户输入命令,然后开始解析命令。命令的结构是『数字』+『D或d』+『数字』,正则表达式如上图。最早的版本里,是 r'(\d)([Dd])(\d)’ ,两个数字位都没『+』,后来发现第二个数字位必须带『+』(因为骰子类型有可能是两位数甚至三位数,例如1d20,1d100),于是我干脆把两个数字位都变成可以无限位取值的。
到此,解析命令完成。下一个问题发生在定义函数时的全局变量上。早期版本如下:

result = 0
def d(n):
    result = randint(1, n)

函数外部出现了变量 result,函数内部又给 result 赋值,电脑就懵圈了。在这里,我一直没搞懂的问题是,定义函数时的返回值,并不是返回给某个变量,而是对应了这个函数本身。result = 0 这个变量的初始化也可以删掉。在朱老师的指导下,终于搞明白这个问题,于是代码顺利改成这样:

def d(n):
    return randint(1, n)

最后,做好一个 for 循环来实现反复扔骰子的动作即可:

for i in range(m):
    result = d(n)
    dice.append(result)
    print(result)
print('和为: ', sum(dice))

至此,其实还没有写完,脚本还有很多地方需要完善,但是已经不再是当初那个被朱老师批评的『到处给全局变量赋值的超级烂代码』了。日拱一卒,余欣慰也。
Github地址
欢迎各位去围观我写的其他小脚本,帮我改改这些超级烂代码!

Tagged : / /

时刻准备着……跑团!?

大约半年前,我曾经跑过一个DND(龙与地下城)3R版本的新手团,用了几天时间读完了玩家手册,又用两个晚上做好了角色卡,然而游戏体验却不佳,跑了一次就坑了。最让我不爽的是团内一个老玩家一直要修改我的角色卡(我是一个人类女性法师,增加了很多魅力),说『让专业的人做专业的事』『法师要有输出』云云,后来我也按要求修改了角色属性,但整场游戏却因角色的大改而让我感到索然无味,当初创造角色的热情也荡然无存。
转眼半年过去,除了一些常规桌游,我还玩了万智牌这种深坑游戏,虽然也挺有趣,但总归比较
『一夜情』——两个玩家遵循同样的规则即可游戏,全程不需要社交,仅仅是竞技。适逢《 Pathfinder 开拓者核心规则书》众筹,我突然想起当初想要跑团的激情,于是买了一套《 Pathfinder 开拓者基础包》先体验一下,然后再看是否也支持一下核心规则。
(下图来自乐博睿官方)
DSC6230_meitu_1
基础包可以当做 Pathfinder 的一个体验版,用非常实惠的价格,提供了跑团必备的组件,通过基础包可以比较完整的理解『什么是跑团』。
1.pi
2.pi

基础包的内容包含:

  • 一套跑团用的骰子(共6枚)
  • 一包用来支撑人物和怪物指示物的底座
  • 简化版玩家手册
  • 简化版城主手册
  • 预设角色卡4份(已经做好的法师/战士/游荡者/牧师)
  • 空白角色卡4张
  • 双面可擦写地图1张
  • 指示物纸板(共4张,其中怪物指示物有2张,这是中文版福利)

预设角色卡4份(已经做好的法师/战士/游荡者/牧师)

4.pi
这四份预设角色卡已经完整录入了角色数据,可以拿来直接玩,对于不懂做卡、或懒得做卡的新人来说,非常友好。反观 DND 团,在推新中并无这样的便利方式(也因为 DND 没有中文官方支持),新人还不知道跑团的乐趣所在就被要求拿着一把300页的玩家手册自学创造角色,这是非常不友好、也不利于推广的。

指示物纸板(共4张,其中怪物指示物有2张,这是中文版福利)

 
5.pic
据说英文版基础包只有3张指示物纸板,中文版则多送了一张怪物指示物,且指示物纸板厚实、质量颇高,很有诚意(其实整个基础包都是诚意满满)。有这玩意,其实我真心觉得模型不必要了,因为未涂装或低质量的模型也并不敏感增加代入感,还不如硬纸片。

双面可擦写地图1张

6.pi
很大一张,正面是地城地图,背面是自定义地形地图,均可擦写。

简化版的玩家手册和城主手册

7.pi
两本书都仅有几十页,很快就可以看完。再次对比 DND 陡峭的学习曲线……(其实 DND 也有简化版的玩家手册,忘了是否是官方出品,但大多数玩家还是要求你先看完整版)

骰子和指示物底座

8.pi
指示物底座不多说了。骰子质量很一般,但是足够用。对于新人体验 Pathfinder 来说,确实没必要附带一套定制的华丽骰子,尽量用最低成本体验游戏是最重要的。这一点我很赞同。

关于跑团众

在深圳,可以很容易找到跑团众,每周都有固定的团。其他一线城市也都比较容易找到队友。如果实在找不到,就只能网团了。

关于我自己

我还没跑过 Pathfinder,简化版玩家手册的开篇提供一个单人团,可以让玩家自己先模拟跑一跑,今晚我就会用预设角色卡跑一下试试。看到其他网友的跑团日志说带着老婆孩子一起玩 TRPG,非常羡慕那种其乐融融的感觉。毕竟在完了不少单人桌游后,我愈发感觉到,一个人的游戏简直不能称之为游戏(仅仅是杀时间而已)。

Tagged : / / / /

[翻译]雷霆之石进阶版:单人变体规则

pic1196537_md

【最近买了《雷霆之石进阶版》,发现官方的单人规则太变态,几乎无法玩下去,于是上网查看玩家自创的规则。例如 Epic Thunderstone 就是玩家们创造的非常棒的一个玩法(已经被官方收录到进阶版说明书中)。今天我翻译的这个,是对雷霆之石进阶版单人规则的改进版。喜欢 solo 的朋友可以尝试一下。原文链接

这篇变体规则是 雷霆之石/雷霆之石进阶版 单人规则的修改版。首先声明,我是按照 Tom Vasel 的『史诗模式:雷霆之石』规则玩的(链接)。唯一的区别是,我在牌堆中放入了10-15张随机洗牌过的卡牌、而不是把所有卡牌都放进去。这样操作使得摧毁牌堆中的卡牌更有效率。这个变体规则也可以与 雷霆之石 常规单人游戏规则混用。之所以做出这个变体规则,是因为我一点都不喜欢游戏规则书中的单人变体规则——怪物们每回合向村庄不停前进,你根本没有时间构筑自己的牌库去击退它们保卫村庄。在我的变体规则中,你获得了构筑牌库的时间(这正是游戏的乐趣所在!),然后每隔一段时间与怪物们交战。

游戏背景:

村庄笼罩在被地牢怪物入侵的危险之中。幸运的是,一个保护咒语将怪物们封印在地牢中、阻止它们逃出来攻击乡村。然而,封印每隔七天就会弱化,怪物们将在这第七天逃出地牢、入侵村庄。你和你的探险队被招募来保卫村庄,每隔七天就需要进入地牢、消灭怪物。

步骤

  1. 按你的偏好设置 雷霆之石/雷霆之石进阶版。注意此时地牢大厅是空的。
  2. 需要一个用来计算天数的骰子。将骰子设置为『6』,标志着你到达村庄的第一天。
  3. 和普通游戏时一样,第一回合访问村庄。回合结束时重新抓手牌,并将骰子调整到『5』。此时一只怪物从地牢牌堆中前进一级,来到地牢大厅的最深处。
  4. 从这一回合开始,你可以按照对你最有利的策略,来选择『探索地牢』『访问村庄』『休养生息』和『整装待发』。
  5. 每回合结束时抓取新的手牌,将骰子调低1个点,然后将地牢大厅中的所有怪物前进一级,空出来的位置由怪物牌堆中新翻出来的怪物填满。
  6. 骰子上的每个数字代表着日期倒计时(『6』是第1天,『5』是第2天,『4』是第3天,『3』是第4天,『2』是第5天,『1』是第6天)。下面是第7天的内容:
  7. 如上所述,当骰子调整到『1』时,你处于第6天。在你抓完新手牌、此回合结束时,将骰子调整到『6』。这标志着第7天的到来,保护咒语开始失效,地牢大厅中的所有怪物倾巢出动、攻向村庄(如果你上个回合刚刚攻击过地牢,那么这回合将少一个怪物。你需要利用刚刚抓取的新手牌来对付所有的怪物)。
  8. 按顺序与每个怪物交战。因为怪物们已经离开地牢进入开阔地,所有怪物不再拥有漆黑度惩罚。
  9. 与第一个怪物交战。如果有任何英雄、武器、物品在战斗中被摧毁,那么在后面的战斗中将无法使用。在杀死所有从地牢大厅中跑出来的怪物、或者每个逃脱的怪物摧毁一张村庄中的卡牌之前,不能抓取新手牌。你和每个怪物按常规规则单独交战,唯一的区别是战斗后不抓取新手牌,你要用这些固定的卡牌来迎战所有怪物。
  10. 成功消灭怪物后,你会从每个被击败的怪物身上获取 XP,然后抓取新手牌,开始循环新的 7 天。由于刚刚的战斗发生在第 7 天,所以你将骰子维持在『6』,再用刚抓的手牌开始新的回合,继续另一个 7 天循环(见第 3 步)。
  11. 如果有些怪物你无法击败,那么每个未被击败的怪物将入侵村庄并摧毁任意一个牌堆顶部的卡牌。你自己来决定具体摧毁哪张卡(我通常用骰子来决定,『6』摧毁一个英雄,『5』摧毁一个武器,『4』摧毁一个村民,『3』摧毁一个咒语,『2』摧毁一件物品,『1』由我自己选择摧毁一张牌。所有逃脱的怪物攻击村庄后,我把骰子调到『6』开始新的 7 天循环)。
  12. 像常规单人游戏规则一样,所有攻击村庄的怪物都放到一起,将它们的 VP 加总,与你获得的 VP 进行比较。
  13. 『战利品』与『后果』在每次战斗后单独结算。任何允许你从村庄购买或获取一张卡的特殊能力,都可以在每次战斗中触发。
  14. 当所有怪物都被击败或逃脱到村庄时,游戏结束。如果你的 VP 大于入侵村庄的怪物 VP 总值,那么你成功保卫了村庄。如果怪物的 VP 大于你的 VP,那么你因未能拯救村庄而失败。

我真的很喜欢这样玩单人版 雷霆之石。 之所以设定在第 7 天与大量怪物交战,是为了提高游戏的挑战性。我曾把规则设置为只有一个怪物逃脱,但这样你就会知道怪物来袭的时间,然后『整装待发』。整场游戏如果只有 1 到 2 只怪物逃脱,赢得胜利就太过简单。如果每隔 7 天地牢大厅的所有怪物一起逃出地牢,你或许可以击败最前面的一两只,但最终会耗尽力量、无法阻止剩余的怪物攻击村庄。这样有助于维持游戏的平衡性。同时 7 天循环也给你足够的时间构筑牌库,而不至于疲于奔命、丧失游戏的乐趣。

Tagged : /

又开始学习新玩意:双拼输入法

多年来,我的个人兴趣呈发散状,遍布互联网的各个小众圈子。从电脑硬件到刀具弹弓、从桌游万智牌到开源硬件、从写代码到拆解机械表、从音响到尤克里里爵士鼓……兴趣爱好太多会带来一些问题,例如习惯了浅层学习、对深度学习有障碍,几乎所有爱好的领域都不算什么专家,都是票友水平。然而真的停不下来……
今天就新增加了一个小小的兴趣——双拼。
早就听说双拼能提高打字效率,一直没兴趣学,今天看了篇文章说学习时间很短,对输入速度提高很大,就用了一下午学习了一下。目前大概使用了两个小时,基本键位差不多记住了,打字速度还比较缓慢,但可以初步感受到双拼效率较全拼更高的原因了。
双拼的原理很简单,就是汉语拼音,其编码规则是把三个双声母 zh、ch、sh 和 35 个韵母安排到键盘的 26 个字母键位上,将所有汉字简化为『1个声母+1个韵母』,这样任何一个汉字都可以用最多2个按键来,而不必像全拼一样必须输入每个汉字的拼音的全部字符。双拼的缺点是,由于键位并不对应键盘字符,所以需要记忆键位(有点像五笔),那种『几个小时的学习让你秒杀全拼』的说法是不现实的。
双拼有多种布局,使用最广的自然码布局如下图。
自然码
举个例子,全拼输入『民主自由』的按键顺序是『min zhu zi you』,而在双拼中则是『mn vu zi yb』,从11次按键降低到8次;全拼输入『共产党』是『gong chan dang』,双拼则是『gs ij dh』,按键次数从12次降低到6次,输入效率竟提高了一倍。在我磕磕绊绊使用双拼的这两三个小时中,自己的输入速度已经在逐渐提高(虽然还不能达到最熟练的全拼的速度),相信随着练习时间的延长,会提高的越来越快。
可惜认识的人中没听说有人使用双拼输入法,否则可以交流下学习心得了。
(大半个月没更新博客,是自己懒惰了,最近代码也没写几行,非常内疚。)