zoukankan      html  css  js  c++  java
  • 巧算星期几

    巧算星期几

    基姆。拉尔森

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

    简介

    布鲁斯 施耐尔

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

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

    顺便说一句,如果你已经设计出更巧妙的算法,或是在已有的方法上有了新突破的话,不妨告诉我,我一定洗耳恭听。我的联系方法是schneier@chinet.com,或者在DJJ编辑部给我留张便条就行。

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

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

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

    创建公式

    首先,我们要用变量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语言程序表达上述公式

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

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

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

    #include 
    char *name[] = { "Monday",
                     "Tuesday",
                     "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday"
                   };
    void main(){
      int D,M,Y,A;
      printf("Day: "); fflush(stdout);
      scanf("%d",&D);
      printf("Month: "); fflush(stdout);
      scanf("%d",&M);
      printf("Year: "); fflush(stdout);
      scanf("%d",&Y);
    /* January and February are treated as month 13 and 14, */
    /* respectively, from the year before.                  */
      if ((M == 1) || (M == 2)){
        M += 12;
        Y--;
      }
      A = (D + 2*M + 3*(M+1)/5 + Y + Y/4 - Y/100 + Y/400) % 7;
      printf("It's a %s.\n",name[A]);

    }

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

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

     ////

    ========================================================
        计算给定日期星期几好象是编程都会遇到的问题,最近论坛里也有人提到这个问题,并给出了一个公式:
        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 %= 7;
        
        与前一段代码相比,我们简化了 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
             此式同样适用于整个三月份

     

  • 相关阅读:
    SQLSERVER2008数据库增量备份还原方式
    使用VS2003遇到“无法显示进程。没有正确安装调试器。请运行安装程序安装或修复调试器。”的解决方法
    IIS7下配置最大上传附件大小需要注意的事项
    运行常用指令
    跨库查询推荐使用的方法
    获取客户端IP需要注意的一个问题
    如何判断一个表是否建立索引约束等信息的SQL语句
    SQLServer2005重建索引前后对比
    一个鼠标滚轮控制大小的缩放类。
    全兼容的纯CSS级联菜单
  • 原文地址:https://www.cnblogs.com/xd502djj/p/1833566.html
Copyright © 2011-2022 走看看