2011年4月11日星期一

一次谷歌面试趣事

本文原始地址:一次谷歌面试趣事


很多年前我进入硅谷人才市场,当时是想找一份高级工程师的职位。如果你有一段时间没有面试过,根据经验,有个非常有用的提醒你应该接受,就是:你往往会在前几次面试中的什么地方犯一些错误。简单而言就是,不要首先去你梦想的公司里面试。面试中有多如牛毛的应该注意的问题,你可能全部忘记了,所以,先去几个不太重要的公司里面试,它们会在这些方面对你起教育(再教育)作用。

我第一家面试的公司叫做gofish.com,据我所知,gofish这家公司如今的情况跟我当时面试时完全的不同。我几乎能打保票的说,当时我在那遇到的那些人都已不再那工作了,这家公司的实际情况跟我们这个故事并不是很相关。但在其中的面试却是十分相关的。对我进行技术性面试的人是一个叫做Guy的家伙。

Guy穿了一条皮裤子。众所周知,穿皮裤子的面试官通常是让人"格外"恐怖的。而Guy也没有任何让人失望的意思。他同样也是一个技术难题终结者。而且是一个穿皮裤子的技术难题终结者 —— 真的,我做不到他那样。

我永远不会忘记他问我的一个问题。事实上,这个问题是非常的普通 —— 在当时也是硅谷里标准的面试题。

问题是这样的:

假设这有一个各种字母组成的字符串,假设这还有另外一个字符串,而且这个字符串里的字母数相对少一些。从算法是讲,什么方法能最快的查出所有小字符串里的字母在大字符串里都有?

比如,如果是下面两个字符串:

String 1: ABCDEFGHLMNOPQRS

String 2: DCGSRQPOM

答案是true,所有在string2里的字母string1也都有。如果是下面两个字符串:

String 1: ABCDEFGHLMNOPQRS

String 2: DCGSRQPOZ

答案是false,因为第二个字符串里的Z字母不在第一个字符串里。

当他问题这个问题时,不夸张的说,我几乎要脱口而出。事实上,对这个问题我很有信心。(提示:我提供的答案对他来说显然是最糟糕的一种,从面试中他大量的各种细微表现中可以看出来)。

对于这种操作一种幼稚的做法是轮询第二个字符串里的每个字母,看它是否同在第一个字符串里。从算法上讲,这需要O(n*m)次操作,其中n是string1的长度,m是string2的长度。就拿上面的例子来说,最坏的情况下将会有16*8 = 128次操作。

一个稍微好一点的方案是先对这两个字符串的字母进行排序,然后同时对两个字串依次轮询。两个字串的排序需要(常规情况)O(m log m) + O(n log n)次操作,之后的线性扫描需要O(m+n)次操作。同样拿上面的字串做例子,将会需要16*4 + 8*3 = 88加上对两个字串线性扫描的16 + 8 = 24的操作。(随着字串长度的增长,你会发现这个算法的效果会越来越好)

最终,我告诉了他一个最佳的算法,只需要O(n+m)次操作。方法就是,对第一个字串进行轮询,把其中的每个字母都放入一个Hashtable里(成本是O(n)或16次操作)。然后轮询第二个字串,在Hashtable里查询每个字母,看能否找到。如果找不到,说明没有匹配成功。这将消耗掉8次操作 —— 这样两项操作加起来一共只有24次。不错吧,比前面两种方案都要好。

Guy没有被打动。他把他的皮裤子弄的沙沙响作为回应。"还有没有更好的?"他问道。

我的天?这个家伙究竟想要什么?我看看白板,然后转向他。"没有了,O(n+m)是你能得到的最好的结果了 —— 我是说,你至少要对每个字母至少访问一次才能完成这项操作 —— 而这个方案是刚好是对每个字母只访问一次"。我越想越确信我是对的。

他走到白板前,"如果这样呢 —— 假设我们有一个一定个数的字母组成字串 —— 我给每个字母分配一个素数,从2开始,往后类推。这样A将会是2,B将会是3,C将会是5,等等。现在我遍历第一个字串,把每个字母代表的素数相乘。你最终会得到一个很大的整数,对吧?然后 —— 轮询第二个字符串,用每个字母除它。如果除的结果有余数,这说明有不匹配的字母。如果整个过程中没有余数,你应该知道它是第一个字串恰好的子集了。这样不行吗?"

每当这个时候 —— 当某个人的奇思异想超出了你的思维模式时,你真的需要一段时间来跟上他的思路。现在他站在那里,他的皮裤子并没有帮助我理解他。

现在我想告诉你 —— Guy的方案(不消说,我并不认为Guy是第一个想出这招的人)在算法上并不能说就比我的好。而且在实际操作中,你很可能仍会使用我的方案,因为它更通用,无需跟麻烦的大型数字打交道。但从"巧妙水平"上讲,Guy提供的是一种更、更、更有趣的方案。

我没有得到这份职位。也许是因为我拒绝了他们提供给我的一些讨厌的工作内容和其它一些东西,但这都无所谓了。我还有更大更好的目标呢。

接着,我应聘了become.com。在跟CTO的电话面试中,他给我布置了一道"编程作业"。这个作业有点荒唐 —— 现在回想起来,大概用了我3天的时间去完成。我得到了面试,得到了那份工作 —— 但对于我来说,最大的收获是这道编程作业强迫我去钻研并有所获。我需要去开发一个网页爬虫,一个拼写检查/纠正器,还有一些其它的功能。不错的东西。然而,最终,我拒绝了这份工作。

终于,我来到了Google面试。我曾说过Google的面试过程跟外面宣传的很一致。冗长 —— 严格,但诚实的说,相当的公平。他们在各种面试过程中尽最大的努力去了解你、你的能力。并不是说他们在对你做科学研究,但我确信他们是努力这样做。

我在Google的第四场面试是一个女工程师,老实话,是一场很无聊的面试。在前面几场面试中我表现的很好,感觉到我的机会非常的大。我相信如果不做出什么荒唐事情来,十拿九稳我能得到这份工作。

她问了我一些关于排序或设计方面的非常简单的问题,我记不清了。但就在45分钟的面试快要结束时,她对我说"我还有一个问题。假设你有一个一定长度的由字母组成的字符串。你还有另外一个,短些。你如何才能知道所有的在较短的字符串里的字母在长字符串里也有?"

哇塞。Guy附身了。

现在,我完全可以马上结束这场面试。我可以对她说"哈哈,几个星期前我就知道答案啦!",这是事实。但就是在几个星期前被问到这个问题时 —— 我给出的也是正确的答案。这是我本来就知道答案的问题。看起来就好像是Guy为我的这次面试温习过功课一样。而且,可恶,人们通常是通过上网来搜集面试问题 —— 而我,我可以毫不客气的说,对于这些问题,我不需要任何"作弊"。我自己知道这些答案!

现在你们可能认为——就在她问出了问题之后,在我准备开始说出在脑海里构思完成的最后的演讲之前——你们可能会想,我应该是,当然该,从情理上讲,镇定的回答出这个问题,并且获得赞赏。可糟糕的是,事实并不是这样。打个比喻,就像是她问出来问题后,我在闹子里立即举起了手,并大叫着"我!嗨!嗨!我知道!让我来回答吧!"我的大脑试图夺走我对嘴巴的控制权(这事经常发生),幸亏我坚强的毅力让我镇定下来。

于是我开始回答。平静的。带着不可思议的沉着和优雅。带着一种故意表现出来的 —— 带着一种,我认为,只有那种完全的渊博到对古今中外、不分巨细的知识都精通的人才能表现出来的自信。

我轻描淡写的说出来那种很幼稚的方案,就好象是这种方案毫无价值。我提到了给它们排序,就好像是在给早期的《星际迷航》中的一个场景中的人物穿上红T恤似的。最后,平淡的,就好像是我决定了所有事情的好坏、算法上的效率,我说出了O(n+m)一次性方案。

我要告诉你——尽管我表明上的平静——这整个过程我却在做激烈的挣扎,内心里我在对自己尖着——"你个笨蛋,赶紧告诉她素数方案!"

当我完成了对一次性算法的解释后,她完全不出意外的认可的点了下头,并开始在笔记本上记录。这个问题她以前也许问过了一百次,我想大部分的人都能回答上来。她也许写的是"回答正确。无聊的面试。正确的回答了无聊的字符串问题。没有惊喜。无聊的家伙,但可以留下。"

我等了一会。我让这种焦灼的状态持续的尽可能的长。我可以发誓的说,如果再耽搁一分钟,我一定会憋出脑血栓、脱口说出关于素数的未解之谜。

我打破了沉默。"你知道吗,还有另外一个,可能是更聪明的算法。"

她二目空空的抬头看了一眼,仅在瞬间闪现过一丝希望。

"假设我们有一定长度的字符串。我们可以给每个字母分配一个素数,从2开始。然后我们把大字串中的每个字母代表的素数相乘得出一个数,用小字串中的每个字母代表的素数去除它。如果除的过程中没有产生余数,则小字串是大字串的一个子集。"

在此时,我猜,她看起来就像是Guy当时把相同的话说给我听时我表现出来的样子。而我演讲时泰然自若的表情没了,眼睛瞪大,说话时稍微带出来一些唾沫星子。

一会儿后,她不得不说了,"可是…等一下,有可能…是的,可以这样!可是如何…如果…噢,噢,可行!简洁!"

我得意洋洋的吸了一口气。我在我的面试记录里写下了"她给了我一个'简洁'的评语!"在她提出这个问题之前我就确信能得到这份工作,现在我更加确信了。还有一点我十分确信的是,我(更准确的说是Guy)给了她今天的好心情。

我在Google干了3年,生活的十分愉快。我在2008年辞职去到一个小公司里做CTO,之后又开办了一个自己的公司。大概是一年前,我偶然的在一个创业论坛会上遇到了Guy,他记不得我了,当我向他细述这段往事时,他对他那条皮裤子大笑不已。

话说回来,如果这个故事里有什么教育意义的话——永远不要冒失的首先去应聘你梦想的公司,应先去应聘那些你不看好的职位。你除了能从这些面试中获得经验外,你指不定能遇到某个能为你的更重要的面试铺路的人呢。事实上,这个经验在你生活中的很多其它事情上也适应。

说正经的,如果你有机会想找一个解决问题的高手——雇佣Guy比谁都强。那个家伙很厉害。

(在这些陈年旧账里发现的一点技术瑕疵:字母有可能重复而字符串可能会很长,所以必须要有统计。用那个最幼稚的解决方案时,当在大字符串里找到一个字符后就把它删掉,当这样仍然是 O(n*m)次。在Hashtable里我们会有一个key->value的计数。Guy的方案在这种情况下仍然好用。)

修改:11/30/10 —— 本文中提到的Guy看到了这篇文章,并在评论中做了澄清。值得一读。


2011年4月3日星期日

如何计算某一天是星期几?—— 蔡勒(Zeller)公式

来源:原文地址




历史上的某一天是星期几?未来的某一天是星期几?关于这个问题,有很多计算公式(两个通用计算公式和一些分段计算公式),其中最著名的是蔡勒(Zeller)公式。即w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1



公式中的符号含义如下,w:星期;c:世纪-1;y:年(两位数);m:月(m大于等于3,小于等于14,即在蔡勒公式中,某年的1、2月要看作上一年的13、14月来计算,比如2003年1月1日要看作2002年的13月1日来计算);d:日;[ ]代表取整,即只要整数部分。(C是世纪数减一,y是年份后两位,M是月份,d是日数。1月和2月要按上一年的13月和 14月来算,这时C和y均按上一年取值。)算出来的W除以7,余数是几就是星期几。如果余数是0,则为星期日。



以2049年10月1日(100周年国庆)为例,用蔡勒(Zeller)公式进行计算,过程如下:

蔡勒(Zeller)公式:w=y+[y/4]+[c/4]-2c+[26(m+1)/10]+d-1

=49+[49/4]+[20/4]-2×20+[26× (10+1)/10]+1-1

=49+[12.25]+5-40+[28.6]

=49+12+5-40+28

=54 (除以7余5)

即2049年10月1日(100周年国庆)是星期5。



你的生日(出生时、今年、明年)是星期几?不妨试一试。



不过,以上公式只适合于1582年10月15日之后的情形(当时的罗马教皇将恺撒大帝制订的儒略历修改成格里历,即今天使用的公历)。过程的推导:(对推理不感兴趣的可略过不看)

星期制度是一种有古老传统的制度。据说因为《圣经·创世纪》中规定上帝用了六天时间创世纪,第七天休息,所以人们也就以七天为一个周期来安排自己的工作和生活,而星期日是休息日。从实际的角度来讲,以七天为一个周期,长短也比较合适。所以尽管中国的传统工作周期是十天(比如王勃《滕王阁序》中说的“十旬休暇”,即是指官员的工作每十日为一个周期,第十日休假),但后来也采取了西方的星期制度。



在日常生活中,我们常常遇到要知道某一天是星期几的问题。有时候,我们还想知道历史上某一天是星期几。通常,解决这个方法的有效办法是看日历,但是我们总不会随时随身带着日历,更不可能随时随身带着几千年的万年历。假如是想在计算机编程中计算某一天是星期几,预先把一本万年历存进去就更不现实了。这时候是不是有办法通过什么公式,从年月日推出这一天是星期几呢?



答案是肯定的。其实我们也常常在这样做。我们先举一个简单的例子。比如,知道了2004年5月1日是星期六,那么2004年5月31日“世界无烟日”是星期几就不难推算出来。我们可以掰着指头从1日数到31日,同时数星期,最后可以数出5月31日是星期一。其实运用数学计算,可以不用掰指头。我们知道星期是七天一轮回的,所以5月1日是星期六,七天之后的5月8日也是星期六。在日期上,8-1=7,正是7的倍数。同样,5月15日、5月22日和5月29日也是星期六,它们的日期和5月1日的差值分别是14、21和28,也都是7的倍数。那么5月31日呢?31-1=30,虽然不是7的倍数,但是31除以7,余数为2,这就是说,5月31日的星期,是在5月1日的星期之后两天。星期六之后两天正是星期一。



这个简单的计算告诉我们计算星期的一个基本思路:首先,先要知道在想算的日子之前的一个确定的日子是星期几,拿这一天做为推算的标准,也就是相当于一个计算的“原点”。其次,知道想算的日子和这个确定的日子之间相差多少天,用7除这个日期的差值,余数就表示想算的日子的星期在确定的日子的星期之后多少天。如果余数是0,就表示这两天的星期相同。显然,如果把这个作为“原点”的日子选为星期日,那么余数正好就等于星期几,这样计算就更方便了。



但是直接计算两天之间的天数,还是不免繁琐。比如1982年7月29日和2004年5月1日之间相隔7947天,就不是一下子能算出来的。它包括三段时间:一,1982年7月29日以后这一年的剩余天数;二,1983-2003这二十一个整年的全部天数;三,从2004年

元旦到5月1日经过的天数。第二段比较好算,它等于21*365+5=7670天,之所以要加5,是因为这段时间内有5个闰年。第一段和第三段就比较麻烦了,比如第三段,需要把5月之前的四个月的天数累加起来,再加上日期值,即31+29+31+30+1=122天。同理,第一段需要把7月之后的五个月的天数累加起来,再加上7月剩下的天数,一共是155天。所以总共的相隔天数是122+7670+155=7947天。



仔细想想,如果把“原点”日子的日期选为12月31日,那么第一段时间也就是一个整年,这样一来,第一段时间和第二段时间就可以合并计算,整年的总数正好相当于两个日子的年份差值减一。如果进一步把“原点”日子选为公元前1年12月31日(或者天文学家所使用的公元0年12月31日),这个整年的总数就正好是想算的日子的年份减一。这样简化之后,就只须计算两段时间:一,这么多整年的总天数;二,想算的日子是这一年的第几天。巧的是,按照公历的年月设置,这样反推回去,公元前1年12月31日正好是

星期日,也就是说,这样算出来的总天数除以7的余数正好是星期几。那么现在的问题就只有一个:这么多整年里面有多少闰年。这就需要了解公历的置闰规则了。



我们知道,公历的平年是365天,闰年是366天。置闰的方法是能被4整除的年份在2月加一天,但能被100整除的不闰,能被400整除的又闰。因此,像1600、2000、2400年都是闰年,而1700、1800、1900、2100年都是平年。公元前1年,按公历也是闰年。



因此,对于从公元前1年(或公元0年)12月31日到某一日子的年份Y之间的所有整年中的闰年数,就等于



[(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400],



[...]表示只取整数部分。第一项表示需要加上被4整除的年份数,第二项表示需要去掉被100整除的年份数,第三项表示需要再加上被400整除的年份数。之所以Y要减一,这样,我们就得到了第一个计算某一天是星期几的公式:



W = (Y-1)*365 + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] + D.              (1)



其中D是这个日子在这一年中的累积天数。算出来的W就是公元前1年(或公元0年)12月31日到这一天之间的间隔日数。把W用7除,余数是几,这一天就是星期几。比如我们来算2004年5月1日:



W = (2004-1)*365 + [(2004-1)/4] - [(2004-1)/100] + [(2004-1)/400] +

   (31+29+31+30+1)

  = 731702,



731702 / 7 = 104528……6,余数为六,说明这一天是星期六。这和事实是符合的。



上面的公式(1)虽然很准确,但是计算出来的数字太大了,使用起来很不方便。仔细想想,其实这个间隔天数W的用数仅仅是为了得到它除以7之后的余数。这启发我们是不是可以简化这个W值,只要找一个和它余数相同的较小的数来代替,用数论上的术语

来说,就是找一个和它同余的较小的正整数,照样可以计算出准确的星期数。



显然,W这么大的原因是因为公式中的第一项(Y-1)*365太大了。其实,



(Y-1)*365 = (Y-1) * (364+1)

          = (Y-1) * (7*52+1)

          = 52 * (Y-1) * 7 + (Y-1),



这个结果的第一项是一个7的倍数,除以7余数为0,因此(Y-1)*365除以7的余数其实就等于Y-1除以7的余数。这个关系可以表示为:



(Y-1)*365 ≡ Y-1 (mod 7).



其中,≡是数论中表示同余的符号,mod 7的意思是指在用7作模数(也就是除数)的情况下≡号两边的数是同余的。因此,完全可以用(Y-1)代替(Y-1)*365,这样我们就得到了那个著名的、也是最常见到的计算星期几的公式:



W = (Y-1) + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] + D.                  (2)



这个公式虽然好用多了,但还不是最好用的公式,因为累积天数D的计算也比较麻烦。是不是可以用月份数和日期直接计算呢?答案也是肯定的。我们不妨来观察一下各个月的日数,列表如下:



月  份:1月 2月  3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

--------------------------------------------------------------------------

天  数: 31  28(29)  31   30   31   30   31   31   30   31    30    31



如果把这个天数都减去28(=4*7),不影响W除以7的余数值。这样我们就得到另一张表:



月  份:1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

------------------------------------------------------------------------

剩余天数: 3   0(1)  3    2    3    2    3    3    2    3     2     3

平年累积: 3   3     6    8   11   13   16   19   21   24    26    29

闰年累积: 3   4     7    9   12   14   17   20   22   25    27    30



仔细观察的话,我们会发现除去1月和2月,3月到7月这五个月的剩余天数值是3,2,3,2,3;8月到12月这五个月的天数值也是3,2,3,2,3,正好是一个重复。相应的累积天数中,后一月的累积天数和前一月的累积天数之差减去28就是这个重复。正是因为这种规律的存在,平年和闰年的累积天数可以用数学公式很方便地表达:



    ╭ d;                 (当M=1)

D = {  31 + d;                 (当M=2)           (3)

    ╰ [ 13 * (M+1) / 5 ] - 7 + (M-1) * 28 + d + i.  (当M≥3)



其中[...]仍表示只取整数部分;M和d分别是想算的日子的月份和日数;平年i=0,闰年i=1。对于M≥3的表达式需要说明一下:[13*(M+1)/5]-7算出来的就是上面第二个表中的平年累积值,再加上(M-1)*28就是想算的日子的月份之前的所有月份的总天数。这是一个很巧妙的办法,利用取整运算来实现3,2,3,2,3的循环。比如,对2004年5月1日,有:



D = [ 13 * (5+1) / 5 ] - 7 + (5-1) * 28 + 1 + 1

  = 122,



这正是5月1日在2004年的累积天数。



假如,我们再变通一下,把1月和2月当成是上一年的“13月”和“14月”,不仅仍然符合这个公式,而且因为这样一来,闰日成了上一“年”(一共有14个月)的最后一天,成了d的一部分,于是平闰年的影响也去掉了,公式就简化成:



D = [ 13 * (M+1) / 5 ] - 7 + (M-1) * 28 + d.        (3≤M≤14)        (4)



上面计算星期几的公式,也就可以进一步简化成:



W = (Y-1) + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] + [ 13 * (M+1) / 5 ] - 7

   + (M-1) * 28 + d.



因为其中的-7和(M-1)*28两项都可以被7整除,所以去掉这两项,W除以7的余数不变,公式变成:



W = (Y-1) + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] + [ 13 * (M+1) / 5 ] + d.

(5)



当然,要注意1月和2月已经被当成了上一年的13月和14月,因此在计算1月和2月的日子的星期时,除了M要按13或14算,年份Y也要减一。比如,2004年1月1日是星期四,用这个公式来算,有:



W = (2003-1) + [(2003-1)/4] - [(2003-1)/100] + [(2003-1)/400] + [13*(13+1)/5]

   + 1

  = 2002 + 500 - 20 + 5 + 36 + 1

  = 2524;

2524 / 7 = 360……4.这和实际是一致的。



公式(5)已经是从年、月、日来算星期几的公式了,但它还不是最简练的,对于年份的处理还有改进的方法。我们先来用这个公式算出每个世纪第一年3月1日的星期,列表如下:



年份:  1(401,801,…,2001)                   101(501,901,…,2101)

--------------------------------------------------------------------

星期: 4                                      2

====================================================================

年份:201(601,1001,…,2201)                  301(701,1101,…,2301)

--------------------------------------------------------------------

星期:  0                                      5



可以看出,每隔四个世纪,这个星期就重复一次。假如我们把301(701,1101,…,2301)年3月1日的星期数看成是-2(按数论中对余数的定义,-2和5除以7的余数相同,所以可以做这样的变换),那么这个重复序列正好就是一个4,2,0,-2的等差数列。据此,我们可以得到下面的计算每个世纪第一年3月1日的星期的公式:



W = (4 - C mod 4) * 2 - 4.                                              (6)



式中,C是该世纪的世纪数减一,mod表示取模运算,即求余数。比如,对于2001年3月1日,C=20,则:



W = (4 - 20 mod 4) * 2 - 4

  = 8 - 4

  = 4.



把公式(6)代入公式(5),经过变换,可得:



(Y-1) + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400] ≡ (4 - C mod 4) * 2 - 1

(mod 7).                                                               (7)



因此,公式(5)中的(Y-1) + [(Y-1)/4] - [(Y-1)/100] + [(Y-1)/400]这四项,在计算每个世纪第一年的日期的星期时,可以用(4 - C mod 4) * 2 - 1来代替。这个公式写出来就是:



W = (4 - C mod 4) * 2 - 1 + [13 * (M+1) / 5] + d.                       (8)



有了计算每个世纪第一年的日期星期的公式,计算这个世纪其他各年的日期星期的公式就很容易得到了。因为在一个世纪里,末尾为00的年份是最后一年,因此就用不着再考虑“一百年不闰,四百年又闰”的规则,只须考虑“四年一闰”的规则。仿照由公式(1)

简化为公式(2)的方法,我们很容易就可以从式(8)得到一个比公式(5)更简单的计算任意

一天是星期几的公式:



W = (4 - C mod 4) * 2 - 1 + (y-1) + [y/4] + [13 * (M+1) / 5] + d.       (9)



式中,y是年份的后两位数字。



如果再考虑到取模运算不是四则运算,我们还可以把(4 - C mod 4) * 2进一步改写成只含四则运算的表达式。因为世纪数减一C除以4的商数q和余数r之间有如下关系:



4q + r = C,



其中r即是 C mod 4,因此,有:



r = C - 4q

  = C - 4 * [C/4].                                                     (10)







(4 - C mod 4) * 2 = (4 - C + 4 * [C/4]) * 2

                  = 8 - 2C + 8 * [C/4]

                  ≡ [C/4] - 2C + 1 (mod 7).                           (11)



把式(11)代入(9),得到:



W = [C/4] - 2C + y + [y/4] + [13 * (M+1) / 5] + d - 1.                 (12)



这个公式由世纪数减一、年份末两位、月份和日数即可算出W,再除以7,得到的余数是几就表示这一天是星期几,唯一需要变通的是要把1月和2月当成上一年的13月和14月,C和y都按上一年的年份取值。因此,人们普遍认为这是计算任意一天是星期几的最好的公式。这个公式最早是由德国数学家克里斯蒂安·蔡勒(Christian Zeller, 1822-1899)在1886年推导出的,因此通称为蔡勒公式(Zeller’s Formula)。为方便口算,式中的[13 * (M+1) / 5]也往往写成[26 * (M+1) / 10]。



现在仍然让我们来算2004年5月1日的星期,显然C=20,y=4,M=5,d=1,代入蔡勒公式,有:



W = [20/4] - 40 + 4 + 1 + [13 * (5+1) / 5] + 1 - 1

  = -15.



注意负数不能按习惯的余数的概念求余数,只能按数论中的余数的定义求余。为了方便计算,我们可以给它加上一个7的整数倍,使它变为一个正数,比如加上70,得到55。再除以7,余6,说明这一天是星期六。这和实际是一致的,也和公式(2)计算所得的结果一致。



最后需要说明的是,上面的公式都是基于公历(格里高利历)的置闰规则来考虑的。对于儒略历,蔡勒也推出了相应的公式是:



W = 5 - C + y + [y/4] + [13 * (M+1) / 5] + d - 1.                      (13)



这样,我们终于一劳永逸地解决了不查日历计算任何一天是星期几的问题。