- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
导读:本文以 CodeGuide 仓库中《数学,离一个程序员有多近?》一文为主体,系统讲解数学与 Java 编程的内在联系——从 2004 年谷歌 101 公路招聘谜题,到《编程之美》中"1 出现的次数"的 for 循环与数学算法对决,再到 HashMap 扰动函数、ThreadLocal 斐波那契散列、梅森旋转算法等隐藏在 JDK 源码中的数学应用。读完本文,你将理解数学如何支撑数据结构与算法设计,并能结合仓库源码与中间件实例,把散列、寻址等数学能力落地到数据库路由等真实场景中。
一、前言:代码是对数学逻辑的具体实现
数学离程序员有多近?
ifelse 也好、for 循环也罢,代码可以说就是对数学逻辑的具体实现。所以敲代码的程序员几乎就离不开数学,难易不同而已。
那数学不好就写不了代码吗?不,一样可以写代码,可以写出更多的CRUD出来。但你不要总觉得是产品需求简单所以你的实现过程才变成了增删改查,往往也是因为你还不具备可扩展、易维护、高性能的代码实现方案落地能力,才使得你小小年纪写出了更多的CRUD!
与一锥子买卖的小作坊相比,大厂和超级大厂更会注重数学能力。
1. 2004 年谷歌的 101 公路数学招聘谜题
2004 年,在硅谷的交通动脉 101 公路上突然出现一块巨大的广告牌,上面是一道数学题:{e 的连续数字中最先出现的 10 位质数}.com。
广告中的 e 是数学常数,自然对数的底数,无限不循环小数。这道题的意思就是,找出 e 中最先出现的 10 位质数,然后可以得出一个网址。进入这个网址会看到 Google 为你出的第二道数学题,成功解锁这步 Google 会告诉你,"我们或许是志同道合的人",你可以将简历发到这个邮箱,我们一起做点改变世界的事情。
计算 e 值可以通过泰勒公式推导出来:
e^x ≈ 1 + x + x^2/2! + x^3/3! + …… + x^n/n!。推导计算过程还包括埃拉托色尼筛选法(the Sieve of Eratosthenes)、线性筛选法的使用。感兴趣的小伙伴可以用代码实现下。
这道题把"找质数"这一数论问题直接转化为了一道招聘门槛,也恰好印证了仓库中《程序员数学 v2.0》开篇的总结:有数学才有编程之美,代码是对数学逻辑的具体实现,有了数学支撑才让编程逻辑具有灵魂(详见 docs/md/algorithm/logic/math/math.md)。
二、把代码写好的四步:数据结构、算法逻辑、设计模式、系统架构
业务提需求、产品定方案、研发做实现。最终这个系统开发的怎么样是由三方共同决定的!
小傅哥用一个盖房子的比喻,把代码工程的分层讲得非常透彻:
- 地基挖的不好,楼就盖不高
- 砖头摆放不巧,楼就容易倒
- 水电走线不妙,楼就危险了
- 格局设计不行,楼就卖不掉
这里的地基、砖头、水电、格局,对应的就是:数据结构、算法逻辑、设计模式、系统架构。从下到上相互依赖、相互配合,只有这一层做好,下一层才好做!
- 数据结构:高矮胖瘦、长宽扁细,数据的存放方式,是一套程序开发的核心基础。不合理的设计往往是从数据结构开始的,哪怕你仅仅是使用数据库存放业务信息,也一样会影响到将来各类数据的查询、汇总等实现逻辑的难易。
- 算法逻辑:是对数据结构的使用,合适的数据结构会让算法实现过程降低时间复杂度。可能你现在的多层 for 循环在合适的算法过程下,能被优化为更简单的方式获取数据。注意:算法逻辑实现,并不一定就是排序、归并,还有你实际业务的处理流程。
- 设计模式:可以这么说,不使用设计模式你一样能写代码。但你愿意看到满屏幕的 ifelse 判断调用,还是喜欢像膏药一样的代码,粘贴来复制去?设计模式这套通用场景的解决方案,就是为你剔除掉代码实现过程中的恶心部分,让整套程序更加易维护、易扩展。就是开发完一个月,你看它你还认识!
- 系统架构:描述的是三层 MVC,还是四层 DDD。MVC 是我们经常用的大家都熟悉,DDD 无非就是家里多了个书房,把各自属于哪一个屋子的摆件规整到各自屋子里。那么乱放是什么效果呢,就是自动洗屁屁马桶给按到厨房了,再贵也格楞子!好,那么我们再延展下,如果你的卫生间没有流出下水道咋办?这个位置的数据结构就是设计缺失的,而到后面再想扩展就难了吧!
所以,研发在承接业务需求、实现产品方案的时候,压根就不只是在一个房子的三居或者四居格局里,开始随意码砖。
没有合理的数据结构、没有优化的算法逻辑、没有运用的设计模式,最终都会影响到整个系统架构变得臃肿不堪,调用混乱。在以后附加、迭代、新增的需求下,会让整个系统问题不断地放大,当你想用重构时,就有着千丝万缕般的调用关系——重构就不如重写了!
三、for 循环没算法快:《编程之美》"1 出现的次数"问题
在《编程之美》一书中,有这样一道题:求 1~n 中,1 出现的次数。比如:1~10,1 出现了两次。
这一节我们分别用"暴力 for 循环"和"数学规律算法"两种方式实现,直观对比它们的耗时差异。
1. for 循环实现
long startTime = System.currentTimeMillis(); int count = 0; for (int i = 1; i <= 10000000; i++) { String str = String.valueOf(i); for (int j = 0; j < str.length(); j++) { if (str.charAt(j) == 49) { count++; } } } System.out.println("1的个数:" + count); System.out.println("计算耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");使用 for 循环的实现过程很好理解,就是往死了循环。之后把循环到的数字按照字符串拆解,判断每一位是不是数字,是就 +1。这个过程很简单,但是时间复杂度很高——对 1 千万个数逐一遍历、逐位拆解,计算量呈线性甚至超线性增长。
2. 算法逻辑实现
其实我们能发现,这个 1 的个数在 100、1000、10000 中是有规则的循环出现的。11、12、13、14 或者 21、31、41、51,以及单个的 1 出现。最终可以得出通用公式:abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000...,abcd 代表位数。另外在实现的过程还需要考虑比如不足 100 等情况,例如 98、1232 等。
实现过程
long startTime = System.currentTimeMillis(); int num = 10000000, saveNum = 1, countNum = 0, lastNum = 0; int copyNum = num; while (num != 0) { lastNum = num % 10; num /= 10; if (lastNum == 0) { // 如果是0那么正好是少了一次所以num不加1了 countNum += num * saveNum; } else if (lastNum == 1) { // 如果是1说明当前数内少了一次所以num不加1,而且当前1所在位置 // 有1的个数,就是去除当前1最高位,剩下位数,的个数。 countNum += num * saveNum + copyNum % saveNum + 1; } else { // 如果非1非0.直接用公式计算 // abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000... countNum += (num + 1) * saveNum; } saveNum *= 10; } System.out.println("1的个数:" + countNum); System.out.println("计算耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");这段算法的核心思想是逐位统计:从个位到最高位,用saveNum(1、10、100……)标记当前统计的位权,lastNum取当前位的数字,num是去掉当前位后的高位,copyNum保留原始值用于计算低位部分。分三种情况累加:
| 当前位 lastNum | 累加规则 | 说明 |
|---|---|---|
0 | countNum += num * saveNum | 高位出现 1 的次数就是num * saveNum |
1 | countNum += num * saveNum + copyNum % saveNum + 1 | 高位贡献之外,还要加上当前位为 1 时的低位部分 |
| 其他 | countNum += (num + 1) * saveNum | 直接用公式(num + 1) * saveNum |
例如计算 1~10:个位为 0,贡献1 * 1 = 1;十位为 1,贡献0 * 10 + 10 % 10 + 1 = 1;合计2,与"1 和 10 中出现两次"完全吻合。整个算法的时间复杂度只有 O(log n),与 n 的大小无关。
在《编程之美》一书中还不只这一种算法,感兴趣的小伙伴可以查阅,但自己折腾实现后的兴奋感更强哦!
3. 耗时曲线对比
按照两种不同方式的实现逻辑,来计算 1000、10000、10000 到一个亿,求 1 出现的次数,对比两种方式的耗时曲线:
- for 循环:随着数量的不断增大后,已经趋近于无法使用了。
- 算法逻辑:依靠的是计算公式,所以无论增加多少基本都会在 1~2 毫秒内计算完成。
那么,你的代码中是否也有类似的地方?如果使用算法逻辑配合适合的数据结构,是否可以替代一些 for 循环的计算方式,来使整个实现过程的时间复杂度降低。
四、Java 中的算法运用:藏在 JDK 源码里的数学
在 Java 的 JDK 实现中有很多数学知识的运用,包括数组、链表、红黑树的数据结构以及相应的实现类 ArrayList、LinkedList、HashMap 等。当你深入地了解这些类的实现后,会发现它们其实就是使用代码来实现数学逻辑而已,就像你使用数学公式来计算数学题一样。
接下来就介绍几个隐藏在代码中的数学知识。
1. HashMap 的扰动函数
扰动函数公式
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }- 描述:以上这段代码是 HashMap 中用于获取 hash 值的扰动函数实现代码。HashMap 通过哈希值与桶定位坐标,那么直接获取哈希值就好了,这里为什么要做一次扰动呢?
- 作用:为了证明扰动函数的作用,可以使用 10 万单词计算哈希值分布在 128 个格子里,之后把这 128 个格子中的数据做图表展示。从实现数据可以看到,在使用扰动函数后,曲线更加平稳了。那么,也就是扰动后哈希碰撞会更小。
- 用途:当你有需要把数据散列分散到不同格子或者空间时,又不希望有太严重的碰撞,那么使用扰动函数就非常有必要了。比如你做的一个数据库路由,在分库分表时也是尽可能的要做到散列的。
为什么需要扰动?从源码层面看,HashMap 默认初始容量是DEFAULT_INITIAL_CAPACITY = 1 << 4(16),而 hashCode 的取值范围是[-2147483648, 2147483647],有将近 40 亿的长度,谁也不能把数组初始化的这么大。所以获取的哈希值需要与数组长度做取模运算得到一个下标值。(h = key.hashCode()) ^ (h >>> 16)把哈希值右移 16 位,正好是它长度的一半,再与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性,让数据元素更加均衡地散列,减少碰撞。这段源码在仓库 面经手册 · 第3篇《HashMap核心知识,扰动函数、负载因子、扩容链表拆分,深度学习》 中有完整的推导、实验数据与 Excel 图表素材说明;更深入的插入、查找、扩容源码分析可以参考 面经手册 · 第4篇《HashMap数据插入、查找、删除、遍历,源码分析》。
扰动函数在数据库路由中的真实落地:仓库 基于 Hash 散列,数据库路由组件设计 一文,把 HashMap 的扰动函数直接移植到了分库分表的路由计算上:
// 扰动函数,加强散列 int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16)); // 库表索引 int dbIdx = idx / dbRouterConfig.getTbCount() + 1; int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);在这套路由组件中,一条数据被 AOP 切面拦截后,先通过扰动函数计算散列索引,再拆分出库索引与表索引,最后通过ThreadLocal传递数据源信息(DBContextHolder.setDBKey/setTBKey)。这就是"学完源码造火箭"的典型案例:HashMap 源码里的数学方法,直接决定了分库分表后数据能否均匀散列,否则数据全部集中在某个库的某张表,就失去了分库分表的意义。
2. 斐波那契(Fibonacci)散列法:ThreadLocal 的神奇 0x61c88647
- 描述:在 ThreadLocal 类中的数据存放,使用的是斐波那契(Fibonacci)散列法 + 开放寻址。之所以使用斐波那契数列,是为了让数据更加散列,减少哈希碰撞。具体来自数学公式的计算求值,公式:
f(k) = ((k * 2654435769) >> X) << Y,对于常见的 32 位整数而言,也就是f(k) = (k * 2654435769) >> 28。 - 作用:与 HashMap 相比,ThreadLocal 的数据结构只有数组,并没有链表和红黑树部分。而且经过测试验证,斐波那契散列的效果更好,也更适合 ThreadLocal。
- 用途:如果你的代码逻辑中需要存储类似 ThreadLocal 的数据结构,又不想有严重哈希碰撞,那么就可以使用斐波那契(Fibonacci)散列法。其实除此之外还有
除法散列法、平方散列法、随机数法等。
神秘的数字是怎么来的?查看 ThreadLocal 源码,设置元素时有一段计算哈希值的代码:
private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }其实这是一个哈希值的黄金分割点,也就是0.618。计算方式如下:
// 黄金分割点:(√5 - 1) / 2 = 0.6180339887 1.618:1 == 1:0.618 System.out.println(BigDecimal.valueOf(Math.pow(2, 32) * 0.6180339887).intValue()); // -1640531527- 学过数学都应该知道,黄金分割点是
(√5 - 1) / 2,取 10 位近似0.6180339887。 - 之后用
2^32 * 0.6180339887,得到的结果是-1640531527,也就是 16 进制的0x61c88647。这个数呢也就是这么来的。
也就是说,Josh Bloch和Doug Lea两位大神选择使用斐波那契数列计算哈希值,是为了更好地散列、减少哈希碰撞。详细的黄金分割推导、散列验证代码与开放寻址原理,在仓库 面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 中有完整展开;仓库 算法逻辑 · 斐波那契 一文还给出了循环、递归、比奈公式三种斐波那契计算方式,并对比了除法散列、乘法散列、斐波那契散列等不同散列算法的适用场景。
值得思考的边界:斐波那契散列虽然让 ThreadLocal 的数据分布极其均匀,但仓库 斐波那契篇 特别指出——它并不能用于数据库路由算法,因为斐波那契散列不满足严格的雪崩标准(SAC),而数据库路由通常采用的是整数模除法散列。这也说明:数学方法没有绝对的优劣,只有适用场景的匹配。
3. 梅森旋转算法(Mersenne Twister)
// Initializes mt[N] with a simple integer seed. This method is // required as part of the Mersenne Twister algorithm but need // not be made public. private final void setSeed(int seed) { // Annoying runtime check for initialisation of internal data // caused by java.util.Random invoking setSeed() during init. // This is unavoidable because no fields in our instance will // have been initialised at this point, not even if the code // were placed at the declaration of the member variable. if (mt == null) mt = new int[N]; // ---- Begin Mersenne Twister Algorithm ---- mt[0] = seed; for (mti = 1; mti < N; mti++) { // 注:原文此处为 "^"(按位异或),即 // mt[mti] = (MAGIC_FACTOR1 * (mt[mti-1] ^ (mt[mti-1] >>> 30)) + mti); mt[mti] = (MAGIC_FACTOR1 * (mt[mti-1] ^ (mt[mti-1] >>> 30)) + mti); } // ---- End Mersenne Twister Algorithm ---- }梅森旋转算法(Mersenne Twister)是一个伪随机数发生算法。由松本真和西村拓士在 1997 年开发,基于有限二进制字段上的矩阵线性递归。可以快速产生高质量的伪随机数,修正了古典随机数发生算法的很多缺陷。最为广泛使用 Mersenne Twister 的一种变体是 MT19937,可以产生 32 位整数序列。
- 描述:梅森旋转算法分为三个阶段——获得基础的梅森旋转链、对于旋转链进行旋转算法、对于旋转算法所得的结果进行处理。
- 用途:梅森旋转算法是 R、Python、Ruby、IDL、Free Pascal、PHP、Maple、Matlab、GNU 多重精度运算库和 GSL 的默认伪随机数产生器。从 C++11 开始,C++ 也可以使用这种算法。在 Boost C++、Glib 和 NAG 数值库中,作为插件提供。
五、程序员数学入门:从概念到验证的学习路径
与接触到一个有难度的知识点学起来辛苦相比,是自己不知道自己不会什么!就像上学时候老师说,你不会的就问我。我不会啥?我从哪问?一样一样的!
代码是对数学逻辑的实现,简单的逻辑调用关系是很容易看明白的。但还有那部分你可能不知道的数学逻辑时,就很难看懂了。比如:扰动函数、负载因子、斐波那契(Fibonacci)等,这些知识点的学习都需要对数学知识进行验证,否则也就学个概念,背个理论。
书到用时方恨少,在下还是个宝宝!
1. 从《程序员数学入门》到《程序员数学 v2.0》
科技博主 Jeremy Kun 花了 4 年时间写成一本书**《程序员数学入门》**。这本书为程序员提供了大量精简后的数学知识,包括:多项式、集合、图论、群论、微积分和线性代数等。同时在 wiki 部分还包括了抽象代数、离散数学、傅里叶分析和拓扑学等。作者表示,如果你本科学过一些数学知识,那么本书还是挺适合你的,不会有什么难度。书中的前三章是基础数学内容,往后的难度依次递增。
而在 CodeGuide 仓库中,小傅哥同样整理了一份**《程序员数学 v2.0》**(见 docs/md/algorithm/logic/math/math.md),全书约 5 章 28 节,涵盖 4 类 14 种数据结构(链表、数组、队列、堆栈、哈希表、堆、字典树、二分搜索树、平衡二叉树、2-3 树、红黑树、并查集、图、布隆过滤器)以及数学部分 14 章(二进制、阶乘、斐波那契、RSA、割圆术、傅立叶变换等)。仓库内可直接查阅的数学章节包括:
- 《程序员数学:斐波那契》——为什么不能用斐波那契散列,做数据库路由算法?
- 《程序员数学》v2.0 总览
- 数据结构篇:数据结构总览(含链表、数组、队列、栈、哈希表、堆、字典树、树、AVL、2-3 树、红黑树、图、并查集、布隆过滤器等系列文章)
2. 推荐的学习路径
对于想深入学习的读者,建议按下面的顺序循序渐进:
- 先动手验证:把本文"1 出现的次数"的两种实现、ThreadLocal 斐波那契散列、HashMap 扰动函数这三段代码,全部亲手跑一遍,用数据说服自己;
- 再读源码:对照 HashMap 面经手册第 3 篇 与 ThreadLocal 面经手册第 12 篇,把散列、寻址、开放定址的原理吃透;
- 最后落地场景:阅读 基于 Hash 散列的数据库路由组件设计 与 路由组件 roadmap:db-router,把数学散列能力真正用到分库分表、抽奖系统等业务中间件中。
六、总结
- Programming is one of the most difficult branches of applied mathematics; the poorer mathematicians had better remain pure mathematicians.
- 单纯的只会数学写不了代码,能写代码的不懂数学只能是 CRUD 码农。数学知识帮助你设计数据结构和实现算法逻辑,代码能力帮你驾驭设计模式和架构模型。多方面的知识结合和使用才是码农和工程师的主要区别,也是是否拥有核心竞争力的关键点。
- 学习知识有时候看不到前面的路有多远,但哪怕是个泥坑,只要你不停地蠕动、折腾、翻滚,也能抓出一条泥鳅。
知识的路上是发现知识的快乐,还是学会知识的成就感,不断地促使你前行。
回到最初的问题:数学离一个程序员有多近?答案就藏在每一次哈希计算、每一次循环优化、每一处数据散列里。从谷歌的 101 公路广告牌,到 HashMap 的扰动函数,再到 ThreadLocal 的黄金分割数——数学不是程序员的选修课,而是写出高性能、可扩展、易维护代码的地基。不妨从本文的三段代码开始,亲手验证一次数学的力量。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
《Hello 算法》哈希算法(ハッシュアルゴリズム)精讲:从哈希函数设计到素数取模与内置哈希实现
《Hello 算法》哈希算法(ハッシュアルゴリズム)精讲:从哈希函数设计到素数取模与内置哈希实现 本篇基于《Hello 算法》日文版「ハッシュアルゴリズム」章节
教程文档示例工程教育pytorch-fid深度解析:揭秘Fréchet距离在图像生成评估中的应用
pytorch fid深度解析:揭秘Fréchet距离在图像生成评估中的应用 pytorch fid是一个基于PyTorch实现的Fréchet Incepti
人工智能模型评测计算机视觉LaMa图像修复入门:克隆后一条命令修复整批图片
LaMa图像修复入门:克隆后一条命令修复整批图片 LaMa是基于傅里叶卷积的大掩码图像修复模型(WACV 2022),解决大面积缺失区域"糊、断裂"的问题。读完
人工智能计算机视觉深度学习图像处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考