一、引子问题:
一个简化的日期问题: 假设每一个月都是30天。假设盘古开天辟地的时候,是计算时间的开始(按照习惯,该天成为第1天,但是注意在程序中,表示的数字并不一定是1)。这里要计算的问题是:给定一个是从世界开始的一个总天数,让你计算该天属于从世界开始的第几个月,并且在该月中是第几天?
二、初步分析:
看了上面的问题,是不是觉得非常简单?是的,确实非常简单,问题就是一种从“总天数”到“月份”或者“日期”的映射关系,使用一个除法,加上一个取余操作就可以了。但是要知道这篇文章就是围绕这个问题展开的,所以这里面一定有一些需要观察和思考的问题。
上面关于“求解方法”(除法和取余)是正确的,但是当我们具体到写代码时,首先要做的就是确定这些信息如何表示,也就是编码问题。因为只有确定了编码问题,利用上面的方法,结果就一目了然了。下面我们讨论的问题就是有关“编码方式”的。
在此之前,要先明确一点:“整数除法”和“取余”的结果,取值范围都是从0开始的。例如:从0开始的正整数N,除以一个正整数M,那么其结果(N/M)是从0开始的; 取余的结果属于0<= N%M <= (M-1)。
三、编号方式详述:
首先编码的对象:有两个:一个是对日期的编码;一个是对总天数的编码。
其次是编码的方法:编码的方法也有两个:① 从0开始编码;② 从1开始编码。
具体的编码如下描述:
① 对于月份:开始的第一个月的编号可以是0,也可以是1;
② 对于一个月中的日期编码:取值范围可以是0<=day<=29,也就是第一天的编号是 0;也可以是1<=day<=30,也就是第一天编号是1;
③ 对于总天数:第一天的编号是0(也可以认为表示方法中总天数中不包括当前天),也可能第一天的编号为1(总天数中包括当前天);
四、举例:
为了清楚描述,将“总天数”记为sum,将“月份”记为month,将“一个月中的第几天(即日期)”记为day。
下面讨论第30天和第31天的表示:
第30天:如果sum从1开始,那么值为30;如果从0开始,值为29;
第31天:如果sum从1开始,那么值为31;如果从0开始,值为30。
计算月份 month:(使用除法)
1. 如果month从0开始编码: (总第30天月编号为0,总第31天月编号为1)
1.1 如果sum从0开始(不包括当前天):
总第30天(sum==29)属于的月编号为0,计算方法为: 29/30;
总第31天(sum==30)月编号为1,计算方法为: 30/30,即公式为 sum/30。
1.2 如果sum从1开始(包括当前天):
总第30天(sum==30)属于的月编号为0,计算方法为: (30-1)/30;
总 第31天(sum==31)月编号为1,计算方法为: (31-1)/30 ,即公式为(sum-1)/30。(注意:不是sum/30,否则第30天计算错误) ;
2. 如果month从1开始编码:(总第30天月编号为1,总第31天月编号为2)
2.1 如果sum从0开始(不包括当前天):
总第30天(sum==29)属于的月编号为1,计算方法为: 29/30 + 1;
总 第31天(sum==30)月编号为2,计算方法为: 30/30 +1,即公式为 sum/30 + 1。
2.2 如果sum从1开始(包括当前天):
总第30天(sum==30)属于的月编号为1,计算方法为: (30-1)/30 +1;
总第31天(sum==31)月编号为2,计算方法为: (31-1)/30 + 1,,即公式为(sum-1)/30 + 1 。
计算日期 day:(使用取余)
1. 如果day从0开始编码: (总第30天日期为29,总第31天日期为0)
1.1 如果sum从0开始(不包括当前天):
总第30天(sum==29)属于的日期为29,计算方法为: 29%30;
总第31天(sum==30)日期为0,计算方法为: 30%30,即公式为 sum%30。
1.2 如果sum从1开始(包括当前天):
总第30天(sum==30)属于的日期为29,计算方法为: (30-1)%30;
总第31天(sum==31)日期为0,计算方法为: (31-1)%30 。即公式为(sum-1)%30。(注意:不是sum%30,否则第30天计算错误)
2. 如果day从1开始编码:(总第30天日期为30,总第31天日期为1)
2.1 如果sum从0开始(不包括当前天):
总第30天(sum==29)属于的日期为30,计算方法为: 29%30 + 1;
总第31天(sum==30)日期为1,计算方法为: 30%30 +1,即公式为 sum%30 + 1。
2.2 如果sum从1开始(包括当前天):
总第30天(sum==30)属于的日期为30,计算方法为: (30-1)%30 +1;
总第31天(sum==31)日期为1,计算方法为: (31-1)%30 +1,即公式为(sum-1)%30 + 1 。
五、问题推广:
对于序列中的一个数n,要划入到一个集合中,每个集合中含有元素都是m个。其中n和m都既可以从0开始编号,也可以从1开始编号。要求计算n被分到了第几个集合,以及在该集合中的下标。
六、解法图示:
七、规律总结:
1. 对于月份和日期:如果它们从0开始,那么除法或者取余结果就是要求的结果;但是如果从1开始,需要在上面的加1;
原因:因为除法和取余的结果,范围都是从0开始的,所以,当月份和日期从1开始时,所以要加1。
2. 如果总天数 n 从0开始,只需要直接除以m 或对m取余(当然,还要根据月份和日期是否从1开始,来判断是否需要加1) ;若总天数从1开始,都要先减1,再除以m或者再对m取余;
3. 巧记公式:是否减1看“总天数”是否从1开始,是否加1看“月份或日期”是否从1开始。
八、联想:
出现本篇文章的直接原因:是因为整数除法和取余的结果范围是从0开始的。那么我们是否会联想到一个编程时常见的也是从0开始的东东——数组。那么,可能在我们刚刚学编程时,发现数组从0开始,是那么不习惯。可是到底为什么数组下标从0开始呢,也许你很可能会说因为数组首个元素的地址偏移量为0,将数组的下标从0开始,便于对数组进行按下标访问时的转化。确实是这样的,我无法和 Dennis M Ritchie沟通它设计C语言时,为何要这样设计,甚至或者他也是向当时已经存在的编程语言借鉴的这种做法。
这里,我想说的是,也许“整数除法和取余结果是从0开始的”也可能是数组下标从0开始,而不是从1开始的一个原因。
本文链接:http://blog.csdn.net/daheiantian/archive/2011/03/14/6534952.aspx