高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210 后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢? 高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记上标注着:5343,因此可算出那天是:1791年12月15日。高斯获得博士学位的那天日记上标着:8113 。现在请算出高斯获得博士学位的年月日?

天才少年

可能大部分同学都听说过一个知名的故事。一位小学老师,为了让同学们停止吵闹,给出了一道数据题    1+2+3+…+100 = ?  原本以为可以让他们安静二三十分钟,结果1分钟不到,就有一个小朋友举手回答了出来,老师漫不经心的看了一眼答案,万万没想到竟然是正确的。

这个小朋友只有9岁,他就是高斯。

 

日记里的秘密

高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210

后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?

高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记上标注着:5343,因此可算出那天是:1791年12月15日。高斯获得博士学位的那天日记上标着:8113 。现在请算出高斯获得博士学位的年月日?

 

解题思路

我们来抽象一下问题,本质上是要求取某年某月某日后的n天,是哪年哪月哪日?

方案一:循环遍历法

比较容易想到的就是遍历的方式,循环n次,就可以推导出n天后的日期了,我们来尝试一下。

public static void calc(int year, int month, int day, int n) {

        for (int i = 0; i < n; i++) {
            // 日子一天天过
            day++;
            // 如果过到月底,需要把日期重置,月份+1
            if(day > getMonthLastDay(year,month)){
                day = 1;
                month++;

                // 如果过到年底,需要把月份和日期同时重置,年份+1
                if(month >12){
                    month = 1;
                    day = 1;
                    year++;
                }
            }
        }
        System.out.println( n + "天后的日期为:"+year + "-" + month + "-" +day);

    }

先把已知条件带入,在main函数中执行  calc(1777,4,30,5343); 

可以得到结果为1791年12月16日,和题目上的1791年12月15日相差一天,这是什么原因呢?

细想可以察觉,高斯生日的这一天其实被算作了第一天的,也就是我们带入方法时,需要传入1777年4月29日

     calc(1777,4,29,5343);  的运行结果是   1791年12月15日

而  calc(1777,4,29,8113);   的运行结果是   1799年7月16日,可以想见高斯在22岁时就已获得博士学位了,真正的年少得志。

 

方案二:善用工具法

jdk8给我们提供了一系列很友好的日期api,让我们求解此类问题时,可以快速拿到答案。

// 创建一个高斯生日前一天的日期
LocalDate  date = LocalDate.of(1777,4,29);
// 调用增加天数的方法,能够直接获得n天后的日期
System.out.println(date.plusDays(8113));

方案比较

既然有我们自己来书写的方案,还有jdk提供的方案,那么问题来了,哪种方案执行更快、效率更好呢?

long start = System.currentTimeMillis();
calc(1777, 4, 29, 8113);
long cost = System.currentTimeMillis() - start;
System.out.println("耗时" + cost);

System.out.println("===========");

start = System.currentTimeMillis();
//日期工具  jdk8   joda time
LocalDate date = LocalDate.of(1777, 4, 29);
System.out.println(date.plusDays(8113));
cost = System.currentTimeMillis() - start;
System.out.println("耗时" + cost);

我们通过毫秒计算工具来测试一下,得到结果如下

高斯日记用java编写 高斯日记算法讲解_当前日期

 注意,这里的时间单位是ms,两种方案实际相差为0.1s左右。

大跌你的眼镜吧,我们自己写的实现竟然比jdk提供的实现方式快,这是为什么呢?

 

源码分析

我们深入LocalDate类的实现看一下,和我们自己实现的有何区别?

public LocalDate plusDays(long daysToAdd) {
        if (daysToAdd == 0) {
            return this;
        }
        // addExact就是一个简单的加法运算
        // toEpochDay() 其中epoch代表的是元年,计算机元年是1970年1月1日,这里计算的是当前日期距离元年的天数
        // 当前日期距离元年的天数 + 当前日期过后的n天 =  n天后距离元年的天数   
        long mjDay = Math.addExact(toEpochDay(), daysToAdd);
        // ofEpochDay(int n) 是计算距离元年n天的日期 
        return LocalDate.ofEpochDay(mjDay);
    }

再点击进 toEpochDay() 看一下

public long toEpochDay() {
        long y = year;
        long m = month;
        long total = 0;
        total += 365 * y;
        if (y >= 0) {
            total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400;
        } else {
            total -= y / -4 - y / -100 + y / -400;
        }
        total += ((367 * m - 362) / 12);
        total += day - 1;
        if (m > 2) {
            total--;
            if (isLeapYear() == false) {
                total--;
            }
        }
        return total - DAYS_0000_TO_1970;
    }

可以看到,这个方法都是通过公式来计算的,而 ofEpochDay() 也同样如此,所以我们可以简单把工具法等价为公式法。

 

总结

为什么循环法比公式法还快呢,我们拉长一下这个问题。当要计算更多天数之后的日期时,两者的表现如何呢?

我们计算 8113333 天后的日期,可以发现的循环法耗时和公式法耗时基本一致,都在0.1s左右。再增加到81133333 天后呢,可以明显的看到公式法仍然保持在0.1s,而循环法的耗时变为1s以上,大幅提高。

这说明在计算次数增加之后,循环法的耗时会成斜线增长,而公式法的耗时基本保持在水平直线上,这也是jdk的设计者们所做的权衡。对我们自身来讲,不同的应用场景下,可以选择不同的实现方式,没有最好的,只有最适合的,你get到了吗?