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
             此式同样适用于整个三月份

     

  • 相关阅读:
    3.Appium运行时出现:Original error: Android devices must be of API level 17 or higher. Please change your device to Selendroid or upgrade Android on your device
    3.Python连接数据库PyMySQL
    2.Python输入pip命令出现Unknown or unsupported command 'install'问题解决
    2.Linux下安装Jenkins
    5.JMeter测试mysql数据库
    Android 4学习(7):用户界面
    Android 4学习(6):概述
    Android 4学习(5):概述
    Android 4学习(4):概述
    Android 4学习(3):概述
  • 原文地址:https://www.cnblogs.com/xd502djj/p/1833566.html
Copyright © 2011-2022 走看看