天天看点

巧算星期几

巧算星期几

基姆。拉尔森

基姆拥有计算机学科的博士学位。他对数据库,算法和数据结构有着浓厚的兴趣。他的联系地址是            (原文为丹麦文--译者注) 31,DK-5270,Odense N,Denmark,或发 E-mail 至 :[email protected]

简介

布鲁斯 施耐尔

“四,六,九,十一,三十天就齐……”儿歌是这么唱的;或许你也曾经掰着手指头翻来覆去地数,让赶上单数的指头代表只有30天的短月吧?这样的口诀对我们是很管用的(我就是念叨着这首傻乎乎的儿歌长大的),可是电脑就没有这份“灵感”了。当然,我们可以用一大堆IF-THEN-ELSES的语句或几个CASE来编写计算程序,让它计算某个指定日期是星期几。

不过我更喜欢基姆拉尔森在本月的“算法小径”中为我们带来的新技巧,因为他的方法另辟蹊径,从一个全新的方向着手解决日期计算的问题。其实,并没有什么数学公式能算出某个指定日期是星期几,不过我们可以试着拼凑一个,如果我们的尝试成功了,你就能拥有一个易于编程的数学公式,并能用它自动计算哪天是星期几了。

你有没有疑惑过你的电脑怎么就知道今天是星期三呢?就算你的电脑关机了,你重启后设定了新日期,它也能立即知道这天是星期几。

在你还是个孩子的时候,你可能见过一种纪录记录着年,月,日的表格,只要加上几个数字,和它相连的另一张表格就会告诉你这个日期是星期几。当然,计算机硬盘的操作系统里也可以加入这样的计算表。不过有一种简单的方法可以轻松地算出某天是星期几;而且这个方法只占用很少的内存空间,而那些只能推算几百年的表格可就太占地方了。

如果目前你的电脑还不具备推算与日期对应的星期数的功能,现在就不妨在自己的程序中试试下面的公式。

创建公式

首先,我们要用变量D,M和Y来表示日期。比如,1994年3月1日就用“D=1,M=3,Y=4”记录。我们的目标是让计算结果在0到6之间。0代表星期一,1代表星期二,2代表星期三,依此类推。

1994年3月1日是个星期二,那么“D mod 7(日期变量除以7的余数))))”这个公式对于整个三月份都有效。比如3月18日是星期五,18 mod 7=4;而4正代表星期五。别忘了,整数的除法和求模有着密切的关系。比方说,26除以7商3余5,这就是说,26除以7商数取整等于3,而26除以7求模(简写为26 mod 7)等于5。以上这些意味着19 mod 7=12 mod 7= 5 mod 7=5。在运算规则中,负数求模运算法相似,所以依此类推,-2 mod 7=5, -9 mod 7=5。

在更正式的表达法中,统一用任意整数n和k表达上述关系,那么这个过程可以表达为n=qk+r,这里的q和r的取值范围同样是整数和0。表1中列出了所有月份的变换数据(shift information此处试译为“档级数据”,还请进一步校对--译者注)。为了尽可能地得出规律,二月被排在最后,同理,一月也是如此。

例1(a)中的公式是仿照表1中的变换数据栏所描述的模式而创建的。这个公式中的除法一律是商数取整。所以得数是最接近真正商数的整数。表2得出了此功能得出的有趣的数值。凭直觉,我们不难发现,当M(代表月份的变量)的值以1为单位递增时,2M就成倍增长,而3(M+1)/5就以3/5为增长倍数。

这正是我们仿制3,2,3,2,3这个重复格式所需要的(表中右边的弯括号表明了这一点)。请注意,我们在以7为除数求模,那么从6到2的求模结果就会逐个增加3(顺序是6,0,1,2)。

现在,我们发现了适用于逐月向下推算的校正方法,并希望把它加入刚才的尝试中,就是那个mod7公式。还以1994年3月1日为例,这个日期的M=3。请注意,在例1(b)中,8 mod 7=1,所以当整个公式合并时,必须减去1。在做以7为除数求模的运算时,减1和加6是一样的,因为-1 mod 7=6 mod 7=6。

这样,例1(c)中的公式就可以计算这一年中剩下的月份了。其实,既然我们把一月和二月排在表1的最后,那么只要我们把它们看成是十三月和十四月,就能接着推算1995年的前两个月了。这是因为,虽然它们并不是一个完整的3,2,3,2,3结构,但恰好可以是这个结构的开始,为了使这个公式更完善,我们还是最好把一月和二月看成是上一年的十三月和十四月。

加入年份

顺着年份向下找,我们观察到1995年3月1日是星期三。这说明,每增加一年,我们公式的计算结果就会增加1。这太简单了,我们只要简单地把年份加上去就行了。再提醒你一次,我们必须确保出发点是正确的。由于1994 mod 7=6,我们在把Y加入已有的公式时就必须减去6。由此改进的例2(a)就更完善了。

1996年是个闰年,这带来了我们的下一个问题。这一年的3月1日是星期五,而不是刚才的公式推算出的星期四。所以每当我们碰上闰年时还得多加上1。判断闰年的规则是,能被4整除,并能被100和400同时整除的年份就是闰年。就这样,我们在原有的基础上添加Y/4--Y/100+Y/400。再强调一下,我们必须从一开始就确保正确。既然(1994/4--1994/100+1994/400) mod 7=(498--19+4) mod 7=483 mod 7=0,所以就不用再做任何调整了。这样,例2(b)就是我们最终的成果了。这个公式能一直工作下去,除非改变现行的日历系统。作为示例,让我们试着推算一下2000年7月4日:(4+2*3+(7+1)/5+2000+2000/4--2000/100+2000/400) mod 7= (4+14+2000+500--20+5) mod 7=2507 mod 7=1,所以那一天是星期二。

这个公式还能推算过去的日期;然而计算范围有限,让我们看看1752年9月14号这个星期四吧,我们的公式最远只能推算到这里了。不过像“1963年11月22日你在哪里”这样的日常问题中提到的日期还是可以轻松应对的:(22+2*11+3(11+1)/5+1963+1963/4--1963/100+1963/400) mod 7=(22+22+7+1963+490--19+4) mod 7=2489 mod 7=4。那天就是星期五。

例3例子3是一个C语言程序,按照把这个公式自动推算给定日期是星期几。

表1:每月变换数据

月份         天数         变换

三月          31            3

四月          30            2

五月          31            3

六月          30            2

七月          31            3

八月          31            3

九月          30            2

十月          31            3

十一月        30            2

十二月        31            3

一月          31            3

二月          28            3

表2:仿制变换数据形式的功能。例1中建立的公式可以适用于1994年。例2把这个公式的功能扩展到可以应用在不同的年份进行推算。

例3:用C语言程序表达上述公式

/*计算指定日期是星期几。默认输入的*/

/*数字代表正确的日期*/

/* 推算给定日期是星期几,假定输入是正确的数据 *

}

/*一月和二月被当作前一年的*/

/*十三月和十四月分别处理*/

 ////

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

    计算给定日期星期几好象是编程都会遇到的问题,最近论坛里也有人提到这个问题,并给出了一个公式: 

    W= (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400) mod 7 

    (要求将1、2月当作上一年的13、14月来计算) 

    去看了看这个公式的原帖            http://blog.csdn.net/ycrao/archive/2000/11/24/3825.aspx 

    其讲述的过程并不清楚,便想怎样自己推导出一个公式来,花了几个小时,总算是弄出来了,结果跟上面的公式一样:) 

下面我们完全按自己的思路由简单到复杂一步步进行推导…… 

推导之前,先作两项规定: 

①用 y, m, d, w 分别表示 年 月 日 星期(w=0-6 代表星期日-星期六 

②我们从 公元0年1月1日星期日 开始 

一、只考虑最开始的 7 天,即 d = 1---7 变换到 w = 0---6 

    很直观的得到: 

    w = d-1 

二、扩展到整个1月份 

    模7的概念大家都知道了,也没什么好多说的。不过也可以从我们平常用的日历中看出来,在周历里边每列都是一个按7增长的等差数列,如1、8、15、22的星期都是相同的。所以得到整个1月的公式如下: 

    w = (d-1) % 7  --------- 公式⑴ 

三、按年扩展 

    由于按月扩展比较麻烦,所以将年扩展放在前面说 

    ① 我们不考虑闰年,假设每一年都是 365 天。由于365是7的52倍多1天,所以每一年的第一天和最后一天星期是相同的。 

    也就是说下一年的第一天与上一年的第一天星期滞后一天。这是个重要的结论,每过一年,公式⑴会有一天的误差,由于我们是从0年开始的,所以只须要简单的加上年就可以修正扩展年引起的误差,得到公式如下: 

    w = (d-1 + y) % 7  

    ② 将闰年考虑进去 

    每个闰年会多出一天,会使后面的年份产生一天的误差。如我们要计算2005年1月1日星期几,就要考虑前面的已经过的2004年中有多少个闰年,将这个误差加上就可以正确的计算了。 

    根据闰年的定义(能被4整但不能被100整除或能被400整),得到计算闰年的个数的算式:y/4 - y/100 + y/400。 

    由于我们要计算的是当前要计算的年之前的闰年数,所以要将年减1,得到了如下的公式: 

    w = [d-1+y + (y-1)/4-(y-1)/100+(y-1)/400] % 7 -----公式⑵ 

    现在,我们得到了按年扩展的公式⑵,用这个公式可以计算任一年的1月份的星期 

四、扩展到其它月 

    考虑这个问题颇费了一翻脑筋,后来还是按前面的方法大胆假才找到突破口。 

    ①现在我们假设每个月都是28天,且不考虑闰年 

    有了这个假设,计算星期就太简单了,因为28正好是7的整数倍,每个月的星期都是一样的,公式⑵对任一个月都适用 :) 

    ②但假设终究是假设,首先1月就不是28天,这将会造成2月份的计算误差。1月份比28天要多出3天,就是说公式⑵的基础上,2月份的星期应该推后3天。 

    而对3月份来说,推后也是3天(2月正好28天,对3月的计算没有影响)。 

    依此类推,每个月的计算要将前面几个月的累计误差加上。 

    要注意的是误差只影响后面月的计算,因为12月已是最后一个月,所以不用考虑12月的误差天数,同理,1月份的误差天数是0,因为前面没有月份影响它。 

    由此,想到建立一个误差表来修正每个月的计算。 

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

月  误差 累计  模7 

1   3    0     0 

2   0    3     3 

3   3    3     3 

4   2    6     6 

5   3    8     1 

6   2    11    4 

7   3    13    6 

8   3    16    2 

9   2    19    5 

10  3    21    0 

11  2    24    3 

12  -    26    5 

    (闰年时2月会有一天的误差,但我们现在不考虑) 

    我们将最后的误差表用一个数组存放 

    在公式⑵的基础上可以得到扩展到其它月的公式 

    e[] = {0,3,3,6,1,4,6,2,5,0,3,5} 

    w = [d-1+y + e[m-1] + (y-1)/4-(y-1)/100+(y-1)/400] % 7 --公式⑶ 

    ③上面的误差表我们没有考虑闰年,如果是闰年,2月会一天的误差,会对后面的3-12月的计算产生影响,对此,我们暂时在编程时来修正这种情况,增加的限定条件是如果当年是闰年,且计算的月在2月以后,需要加上一天的误差。大概代码是这样的: 

    w = (d-1 + y + e[m-1] + (y-1)/4 - (y-1)/100 + (y-1)/400); 

    if(m>2 && (y%4==0 && y%100!=0 || y%400==0) && y!=0) 

        ++w; 

    w %= 7; 

    现在,已经可以正确的计算任一天的星期了。 

    注意:0年不是闰年,虽然现在大都不用这个条件,但我们因从公元0年开始计算,所以这个条件是不能少的。 

    ④ 改进 

    公式⑶中,计算闰年数的子项 (y-1)/4-(y-1)/100+(y-1)/400 没有包含当年,如果将当年包含进去,则实现了如果当年是闰年,w 自动加1。 

    由此带来的影响是如果当年是闰年,1,2月份的计算会多一天误差,我们同样在编程时修正。则代码如下 

    w = (d-1 + y + e[m-1] + y/4 - y/100 + y/400); ---- 公式⑷ 

    if(m<3 && (y%4==0 && y%100!=0 || y%400==0) && y!=0) 

        --w; 

    与前一段代码相比,我们简化了 w 的计算部分。 

    实际上还可以进一步将常数 -1 合并到误差表中,但我们暂时先不这样做。 

    至此,我们得到了一个阶段性的算法,可以计算任一天的星期了。 

public class Week { 

    public static void main(String[] args){ 

        int y = 2005; 

        int m = 4; 

        int d = 25; 

        int e[] = new int[]{0,3,3,6,1,4,6,2,5,0,3,5}; 

        int w = (d-1+e[m-1]+y+(y>>2)-y/100+y/400); 

        if(m<3 && ((y&3)==0 && y%100!=0 || y%400==0) && y!=0){ 

            --w; 

        } 

        w %= 7; 

        System.out.println(w); 

    } 

五、简化 

    现在我们推导出了自己的计算星期的算法了,但还不能称之为公式。 

    所谓公式,应该给定年月日后可以手工算出星期几的,但我们现在的算法需要记住一个误差表才能进行计算,所以只能称为一种算法,还不是公式。 

    下面,我们试图消掉这个误差表…… 

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

    消除闰年判断的条件表达式 

    由于闰年在2月份产生的误差,影响的是后面的月份计算。如果2月是排在一年的最后的话,它就不能对其它月份的计算产生影响了。可能已经有人联想到了文章开头的公式中为什么1,2月转换为上年的13,14月计算了吧 :) 

    就是这个思想了,我们也将1,2月当作上一年的13,14月来看待。 

    由此会产生两个问题需要解决: 

    1>一年的第一天是3月1日了,我们要对 w 的计算公式重新推导 

    2>误差表也发生了变化,需要得新计算 

    ①推导 w 计算式 

      1> 用前面的算法算出 0年3月1日是星期3 

         前7天, d = 1---7  ===>  w = 3----2 

         得到 w = (d+2) % 7 

         此式同样适用于整个三月份

继续阅读