惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

谁是人民? 倒计时器 · 中断原理 向死而生 爱,死亡与机器人 什么是真正的哲学 LG V30 开源之旅 VENI VIDI VICI 那些不曾察觉的事物 十八之行
算盘上的童年
Exister · 2026-02-27 · via

我已经记不清我从什么时候开始学习珠心算了,但我整个小学六年都在练习珠心算,应该算得上「老资历」了。现在大多数人对珠心算几乎一无所知,有人将其视为古老的智慧结晶,能培育出天赋异禀的神童;也有人对其嗤之以鼻,认为它不过是无用的技艺。或神化,或污名化,我认为这都是无知的体现。本文就从我学习珠心算的经历出发,写写我对珠心算的感受。

我的经历

幼儿园

我的家乡珠心算教育还是推广得很好的,有一些幼儿园教珠心算,我上的幼儿园也在其中。不过,幼儿园也教普通的计算方法,但因为我一开始就觉得珠心算的算法好、快,根本没学普通算法,所以只会珠心算。现在想来,可能也算是有些天赋。很多孩子虽说也跟着学了珠心算,但并不熟练,也不将其作为主要算法在日常中使用,渐渐地都忘了。由于我珠心算水平在班上名列前茅,幼儿园毕业的时候老师将我推荐给珠算协会的老师,让我继续「深造」。

我小时候是个内向的人。我还记得我第一次到老师家,她测试我的能力,认可了我。然后,她问我喜不喜欢珠心算,如此内向的我当然是含含糊糊地答应了,这以后我便成为了她的学生。如果没记错的话,第一次去上珠心算课那天,当被告知要去上珠心算课,我十分震惊,不知道那样答应了一声就要来上课了。而那一声根本不会在意到的答应,将影响我整个小学时光。

小学

在我上小学的时代,学生们上课外班的现象应该还不算严重,只有少数学习好的或是差的学生会去上补习班,其余同学还是有大把课余时间的。尤其是放学时间非常早,那时候作业也不多,语文便是抄抄字词,数学写两页册子,英语最多是读五遍课文。不过,每天晚上放学我都得去「打珠心算」,结束时天都黑了,我妈妈骑着自行车来校门口接我,我坐在她车上回家。然后便是写作业,写完作业差不多就睡觉了。

小学时候的我应该算是家长们眼中的「乖孩子」,学习成绩好、作息规律、不和「野孩子」们瞎玩。但事实上,我极其向往那些放学后能疯玩的孩子。晚上回到家写作业,院子里的小孩又笑又叫。有时候他们的父母来问两句:「作业写完了吗?」——「早写完了。」一阵辛酸总是涌上我的心头。每天学校里的放学铃声响起,学生们晃晃悠悠地收拾东西,而我则匆匆地背上书包,去珠心算教室。珠心算教室就在学校里。我们学校与珠算协会的老师合作,学校为珠心算提供教室,老师则负责教学校的学生珠心算。来到教室后,我会把包放下,然后借口去上厕所,去外面溜达一圈。操场上,一些学生还在毫无顾虑地玩,令我十分羡慕。

低年级的时候,学习珠心算是一件风光事。因为数学课上基本就学计算,而我早已熟练得不能再熟练了。计算题不过是一两位数的加减法,班上还没把卷子发完,先拿到卷子的我就做完了。学校还会举办「速算比赛」,出现第一个做完所有题的同学就停止比赛,那些普通学生和学珠心算的学生是没法比的。同学们对我崇拜,我也自然得意。

不过,渐渐地珠心算对我来说没有什么吸引力了。计算能力好像也没有什么太大的提升,我对坐在那里花费大把时间计算永远也算不完的数字感到无聊。记得大概是三年级的时候,我报名了学校的足球课。教练很认可我的能力,我也喜欢上了足球。从此我放学后经常以球队训练、踢比赛之类的理由不去打珠心算。为了防止老师给我妈妈打电话,我操作我妈妈的手机,把老师的电话号码拉进了黑名单。不过,最终有一天事情暴露了。老师给我妈妈打电话一直提示「正在拨号中」,觉得可疑,换了一个老师的手机去打,就拨通了。那天晚上,我走出校门,我妈妈冲我发了很大的火,在校门口就把我狠狠揍了一顿,然后也不管我了,直接吊着脸、骑上车,转身就走,我在后面追。想着想着她又觉得气,停车转过身来又把我揍了一顿,路人都劝她不要打了。就这样我跟着她跑着回家了。

那段时间我将要去参加一个珠心算比赛,属于「邀请赛」,没什么含金量,掏钱就能去,她花了很多钱让我参加。显然她因为我不上进而感到愤怒。于是,自然而然地,我不再去踢球了,而是继续去打珠心算。

那次比赛只获得了二等奖,因为那时候的水平确实不怎么样。此后又参加了几次邀请赛,都获得了最高的奖项(有的是一等奖,有的是特等奖)。邀请赛着实没有含金量。真正有含金量的比赛,是全国比赛和世界比赛,还有一个少数民族比赛,只有少数民族能参加,难度较易。这些比赛需要经过选拔,参加比赛的路费、住宿费等均由公家报销。我只参加过一次全国比赛,只取得了三等奖。

比赛前的训练是十分痛苦的。在假期,我甚至要早上训练 4 小时,下午训练 4 小时。做题。掐时间(用秒表计时)。对答案。「错了几个?纠错。」「来,下一套,开始!」卷子一套一套发下来,写答案的纸条一条一条堆在箱子里,慢慢变高。渐渐地,比赛的日期接近了,不堪回首的日子总算熬出头了。然而,比赛过后,生活还要继续。

我十分羡慕那些不用上补习班、能自在地玩的孩子。我将我的生活归结于珠心算,并时常向我妈妈委婉地表达我不想再学下去。一年级的时候,同年级有两个跟我一起学珠心算的同学,后来都没再学了。我已经受了这么多苦,我也想放弃了。但我妈妈始终不愿意,现在回想起来,当时她花那么多钱让我去参加邀请赛,或许就是想激励我学下去。

不过准备比赛的时候我也没好好练。小学的时候下午的课基本都是「副课」(也就是除语数英之外的课),珠心算老师给我们班主任说好了让我在上副课时来训练。于是,我骗珠心算老师说某节课是主课,然后到操场玩。但干坏事时间久了很难不被发现的,有时候我正玩着呢,被出来上厕所的同学看到了。其中就有同学告班主任了,但最后班主任也没收拾我。

参加完全国比赛,我彻底躺平了。直到我小学毕业,都不会再有这种比赛了。尽管我妈妈还是让我来打珠心算,但我每天都混日子。那段时间学校里在搞「培优补差」,放学后把学生留在教室里,班主任安排学习任务。那程度着实夸张,甚至有一次珠心算的放学时间都过了,我们班的「培优补差」还没结束。有了这依据,我便光明正大地晚来。就算没有培优补差,放学了先操场上玩半个小时再说。老师问为什么来得这么晚,我便说「培优补差」,反正我们年级都没有别的打珠心算的小孩,没人知道我什么时候放学。然后坐在教室里翻出一本题慢腾腾地做,或是发呆,随便画点东西,装作笔在动的样子……

在我们学校的珠心算班,坚持不下去而放弃的人是大多数,许多人会在三年级左右的时候不学了。老师有时会在班里强调,那些能坚持到最后的孩子不一样,将来会有出色的表现。此刻回想,我表示怀疑,也有可能是她为了让我们学下去才这么说的。不过,虽然她这么说,还是有很多孩子没有坚持下来。

总之,最后的日子就这样悠悠地过去了。小学毕业后,我正式与这段时光诀别,也终于有资格成为她口中「不一样的孩子」。

速算的代价

打珠心算给我带来的巨大影响就是与人的疏离,因为我们年级没有别的小孩学这个。下午放学后不着急回家,和同学四处溜达,在校门口的小吃摊买点零食一起分享,路上聊学校里发生的事,这是我从未有过的体验,只存在于我的想象中,因为我下午总要去打珠心算。或许我从小就没学会怎么与人交往。打珠心算还侵占了我很多课余时间,我觉得都是白白浪费掉了,因为我认为到后期我的运算能力几乎没有提升,就只是坐在那里不停练习来「保持熟练度」罢了。每天花费几小时用来算术我觉得实在不值。再加上没有同龄人和我一起学珠心算,这给我带来了心理上的巨大不平衡:凭什么别人都不用学,就我要学这个?

不过,实事求是地说,打珠心算也是有好处的。虽然现在已经很久没练珠心算,加上年纪增长、想象力衰退,对珠心算已经生疏了许多,但我的计算能力还是能秒杀普通人。此外,我很有耐心、不浮躁。练珠心算着实是一件考验意志的事情,从我上文说「很多孩子坚持不下来」的情况便可见一斑。

珠心算的原理

在这里我还想简单讲讲珠心算,因为似乎现在社会上大多数人对珠心算印象不佳,我认为这很可能是因为许多从事珠心算教育行业的人只是纯粹为了商业利益,很多老师可能自己都不会珠心算,学了两套口诀就来开班赚钱了。我小学的时候就在学校旁边就开了一家珠心算培训班,没过多久就关门了。但幸运的是,我是跟随珠算协会的老师学习的。虽然我其实并没有遵从她们教的方法,但所处的环境相对纯粹。

珠算

算盘中间有一道横梁,每一颗上方的珠子表示 5,每一颗下方的珠子表示 1。算盘的每一列为一「档」,和阿拉伯数字一样,使用「位值制」,不同档的权值(基数的幂)不同,因而可以很方便地在珠算图与阿拉伯数字之间进行转换。下图是古代的算盘,上方有两颗珠子,下方有五颗珠子,这种是十六进制算盘,现在已经很少使用了。此外还有上一下五的十一进制算盘,这种算盘就比较奇怪了,不过我也见过。如今大多数学生学习的大多是一四珠算盘(上一下四),正好就是十进制。不同进制的算盘指法完全不同,学新式算盘的人是不会拨老算盘的。

二五珠算盘,「猫猫的笔记本」拍摄,CC BY-SA 3.0,原始链接:https://commons.wikimedia.org/wiki/File:China_Abacuses_Museum_02_2013-01.JPG

使用算珠表示数字的优势在于,十分易于处理数字的变化,因为算珠可以很容易拨动,其表示的数字随之变化。在加减的过程中,「进位」与「借位」的情况是十分频繁的。传统的计算方法一般是从低位向高位计算,因为高位结果会受到低位的影响,低位结果却不受高位影响。但珠算与之完全相反,从高位向低位计算,因为对于珠算可以随时「进位」与「借位」。这与人的语言、阅读顺序是相同的。如果你「念」一道计算题,你说的时候我就在算,你说完我也就算完了。事实上,语言的速度很难跟上计算的速度。因此,如果你想通过这种方法来评判一个人珠心算水平如何,那你大概率是要失败的。

珠心算每一次加法或减法,称作一「笔」,比如十五笔就是十五次加减法的意思。珠心算的乘除题目一般只乘或除一次,因而没有「笔」的概念。

下图为「四级」难度的珠心算试题:

十五笔加减法

乘法

除法

珠算的本质是将每一位中存在的所有计算情形都转化为指法并「记下来」,通过大量的训练形成反射,最终看到一个算式就能立刻知道该怎么拨算珠。以加法为例,一位加法只有 81 种情形:1+1,1+2,…,1+9,…,9+9。如果我能将这 81 种情形都牢记于心,我也就能计算一切加法。减法、乘法、除法也类似。这种「记忆」最开始是依靠口诀的,口诀以「加数」/「减数」/「乘数」/「除数」划分(也就是 a?b 中的 b),比如「+4」的口诀就很可能是「上五下一」,「-9」的口诀可能是「借一还一」。乘法和除法的口诀就要难许多,首先它没有加减法口诀那么直观,其次不仅需要记忆在这一位的结果是什么,还需要记忆进位或借位的个数,并且不同乘数和除数的口诀之间没有相似性。每次教学只会教一类口诀,然后就是大量针对这种情形的练习,将口诀转化为一种反射,看到就知道该这么算,此时口诀也就不重要了。

道理是这样讲,但具体怎么操作还是有很多方法的。比如说我的乘除法就是用加法算的,没有用老师教我的方法。因为我在幼儿园就学会了加减法,上了小学才来珠算协会学的乘除法。刚开始老师教我们乘法的口诀,由于我不熟练,算得很吃力,但我是属于那种比较聪明的小孩,我知道加减法我熟啊!于是我用加减法算乘法:

  • 小数累加:乘二就是加一次,乘三就是加两次,乘四就是先加一次,然后把得到的数再加一次。
  • 以五为界:乘五就是把数字「取一半」,乘六就是乘五的基础上再加一次,乘七就是乘五的基础上加两次。
  • 大数用「十」减:乘九就是乘十再减一次,乘八就是是乘十再减两次。

除法运算则靠「试」。因为我的乘法就是加出来的,做除法时,判断一下被除数最高位和除数的大小关系:要是小于五倍,累加就好了;要是大于五倍,则在除数乘五的基础上累加;要是比五倍还要大很多,就从十倍开始减。找到一个倍数,使其与除数相乘,最高位当好等于被除数最高位,这个倍数就是一位结果了。然后把乘积从被除数上减掉,接着计算下一位结果。珠心算中的除法计算一般保留两位小数,多余四舍五入,因此一直这样算到小数点后两位,判断一下第三位与 5 的关系即可。

听老师说有些地方的老师教的「乘九」和「乘八」的算法和我一样,但老师认为这种方法没有她教的快,确实如此。关于这种算法,我会在下文「心算」的部分继续讲。

心算

心算,一般认为是依靠想象力。但与常人的认识不同,这并不只是依赖于视觉上的想象力(即想像有珠子这么一回事),我认为,还依赖于触觉上的想象力。和我一起训练的同学,有很多即使是在心算,也会手做出拨动算珠的动作。老师对此批评,让他们改掉。我虽说没有这种动作,但在心算的时候也能感觉到隐隐约约有一种拨算珠的感觉,像是假想的手在动。在打算盘的过程中,其实视觉并不重要,手指都把算珠遮住了,而我闭着眼都能打算盘。相反,更重要的其实是触觉。每一次拨动算珠那坚实的触感都在手指尖上回馈。经过漫长训练,这种触觉反馈逐渐加深,成为一种「想象力」。就像是键盘盲打,我想很少有人能说出某个字母在什么位置,但把手放在键盘上,自然就知道每个字母的位置在哪里。

不过,当「珠算」转化为「心算」,它的一个弊端就显现出来了:人能想像的珠算图位数是有限的,过长图像就会模糊,计算就不再准确。对于加减法,珠心算为了快,不使用逐位计算的方法,而是一下子计算所有数位。我们老师说:「正常人心算的位数都能达到 9 位」,不过我大概只有 6 位,7 位以上虽然可以想象出来,但很难做许多笔的计算了。但老师的说法大概率是夸张,因为和我一起训练的同学也没比我好多少(但我确实在其中不算好的)。

上文讲到我通过累加的方法计算乘除,这种算法在学习初期很有优势,不过在后期我就比不上那些用老师教的算法的孩子了。因为熟练后那种算法看到数字就能出答案(条件反射),而我还要去算、去试。此外,随着乘数和除数位数增大,老师的算法仍然可以轻松地解决,因为大数乘除都分解为一位与一位的乘除;但我的算法运算难度骤增:我需要将许多位的乘数和除数「记下来」,一并做加减运算。这种差距随着训练的增加越来越大,我也非常恐惧,这也可能是我不愿意继续打珠心算的原因之一。不过,也得益于我的算法,我的加减法比他们都好。

解放军在武汉有一支打珠心算的队伍,我们叫它「解放军队」。据老师说其中汇聚了全国珠心算顶尖的孩子,那个教练也非常优秀,有很多好的训练方法。老师还想让我去解放军队,一次比赛的时候直接把我拉到那个教练跟前,向他「介绍」我,给我吓坏了。我妈妈也想让我去,但她尊重我的意见。我对此是相当抵触的,我解释说到了那边可能不适应,而且整天打珠心算也会很枯燥。但实际上,一个很重要的原因就是:我怕。我当时就已经感觉到,依靠我的算法,乘除运算的速度是很难再提高的了。而我的老师并不知道,她预计着我的速度还能再快。到了解放军队,题目只会更难,甚至乘数和除数大到我无法想像那么多位的珠算图,更别说进行累加运算。我怕我到那边就露馅了。

最终我也是没去解放军队。生命中的许多抉择,尽管当时并没有理解其中的深意,但现在回过头来看,我都是做对了的。我为此感到庆幸。

一种开方的算法

提到珠心算,总有人要嘲讽一番:你来给我算个三角函数、自然常数看看啊?但事实上,我能算加减乘除就够了。别的不说,我算的速度就是比你快,算的数字就是比你大。只要位数不是太多,超过我能想像的珠算图位数,你还在按计算器我就算完了,就是这么简单。说这种话的人有一种典型的酸葡萄心理。在上文我就说过,珠心算的本质是将所有计算情形都「记下来」形成「反射」,与「九九乘法表」这样的方法并无本质区别珠算的优势在于能更好地处理「进位」与「借位」,心算的优势在于经过训练的人能直接进行大数运算。所以能不能用珠心算处理一个运算,要看能不能将这个运算所有的计算情形分类化简,总结为一套简单的规则。此外,这种运算要足够常用,值得人花费许多练习来形成反射。不过,除了加减乘除,珠心算还能开方。这里就记录一个我曾经学过的开方算法,这种算法并不局限于珠心算。

阿拉伯数字的一个优势是「位值制」,比如 387387 就是 3×102+8×101+7×1003 \times 10^2 + 8 \times 10^1 +7 \times 10^0,当我们说 132=16913^2 = 169,实际上是在说 (1×101+3×100)2=169(1 \times 10^1 + 3 \times 10^0)^2 = 169。这种开方算法就用到了这个性质,使用完全平方公式将结果按数位进行拆分。

nn 位数平方后,幂次由 10n110^{n - 1} 变为 102(n1)10^{2(n - 1)}。因此,这种算法将数字「两位一划分」。一个 nn 位数的平方,结果是 2n2n2n12n-1 位数,倘若位数为奇数,则要在最高位前补 00。接下来我就简单讲讲这个算法:

一个三位数 mnpmnp,认为 a=m×102a = m \times 10^2b=n×101b = n \times 10^1c=p×100c = p \times 10^0,就可以拆解为 (a+b+c)2(a + b + c)^2,这满足一种完全平方公式:

完全平方公式

这个结论可以推广 (a1+a2+a3+a4+)2=(a12+a22+a32+a42+)+2[a2a1+a3(a1+a2)+a4(a1+a2+a3)+](a_1 + a_2 + a_3 + a_4 + \cdots)^2 = (a_1^2 + a_2^2 + a_3^2 + a_4^2 + \cdots) + 2[a_2a_1 + a_3(a_1 + a_2) + a_4(a_1 + a_2 + a_3) + \cdots],因此可以使用这个式子对任意位数进行开方。

接下来以 149769149769 为例,演示这种算法的使用:

开方算法

这种算法按照 a2a^2ababb2b^2bcbcc2c^2 的顺序,幂次递减,因此每计算一步,分组都要向右移一位。

使用珠心算搭配这种算法计算起来是非常快的,当然前提是熟练。

这种算法并不限制结果必须为整数,甚至是无理数都可以,只要你愿意一直算下去:

根号 2

不过,随着迭代次数的增加,计算的位数也会增加。后期的计算会十分困难。

因为想练习一下 C 语言,所以我编写代码用计算机实现了这个算法。不过必须声明:这种算法非常不适合计算机。受限于数据类型的长度,无法进行多次迭代计算。代码只作为练习,没有实用价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
#include <stdio.h>
#include <stdint.h>
#include <string.h> //< 包含字符串操作库,用于 memset

/**
* @brief 最大迭代次数,确保计算过程中不发生溢出
*
* 第 1 次迭代需要计算的位数最大为 3,此后每迭代 1 次位数增加 2,(2n + 1) <= 19
*/
#define MAX_ITERATIONS 9

/**
* @brief 预计算的 10 的幂次数组
*
* 存储 10 的 0 到 19 次幂的值,用于快速查找
* 对于 uint64_t,其整数部分最多有 20 位
*/
static const uint64_t powers_of_10[] = {1ULL,
10ULL,
100ULL,
1000ULL,
10000ULL,
100000ULL,
1000000ULL,
10000000ULL,
100000000ULL,
1000000000ULL,
10000000000ULL,
100000000000ULL,
1000000000000ULL,
10000000000000ULL,
100000000000000ULL,
1000000000000000ULL,
10000000000000000ULL,
100000000000000000ULL,
1000000000000000000ULL,
10000000000000000000ULL};

/**
* @brief 存储开平方算法预处理阶段的输入信息
*
* 计算开始前对被开方数进行分析和分解,为后续的迭代计算提供数据
*/
typedef struct {
uint64_t input_num; ///< 原始输入数值,即被开方数
uint8_t integer_groups; ///< 整数部分的位数分组数(每 2 位一组)
uint8_t highest_group_num; ///< 最高组的数值
} sqrt_input_t;

/**
* @brief 存储开平方算法在迭代过程中的计算状态
*
* 记录了每一步计算后的中间结果
*/
typedef struct {
uint64_t remainder; ///< 当前计算步的余数
uint64_t result; ///< 当前计算出的平方根结果。每完成一步,此结果会更新一位
uint8_t current_group; ///< 当前的组序号
uint8_t group[10]; ///< 分组数组
} sqrt_calc_t;

/**
* @brief 存储最终的开平方结果,分为整数和小数部分
*
* 由于该算法不受数位限制,因此使用整数以避免浮点数
*/
typedef struct {
uint64_t int_part;
uint64_t frac_part;
} sqrt_result_t;

/**
* @brief 初始化计算,确定平方根的最高位
* @param[in] input 指向预处理后的输入信息结构体
* @param[out] calc 指向要初始化的计算状态结构体
*
* 使用试商法处理第一组数字
*/
void cycle_init(sqrt_input_t* input, sqrt_calc_t* calc) {
// 计算最高位结果
for (uint8_t i = 9; i > 0; i--) {
if (input->highest_group_num >= i * i) {
calc->result = i;
calc->remainder = input->highest_group_num - i * i; //< 计算剩余数字
return;
}
}
}

/**
* @brief 执行一步迭代计算,确定平方根的下一位
* @param[in,out] calc 指向当前计算状态的结构体
*
* 公式:(20 * a + b) * b <= remainder
*/
void cycle_calc(sqrt_calc_t* calc) {
uint8_t group = calc->group[calc->current_group++];
calc->remainder =
calc->remainder * 100 + group; //< remainder 左移两位 + 下一组数字
uint64_t a = calc->result;
for (int8_t b = 9; b >= 0; b--) {
uint64_t test_val = (20 * a + b) * b;
int64_t new_remainder = (int64_t)calc->remainder - (int64_t)test_val;
if (new_remainder >= 0) {
calc->result = a * 10 + b;
calc->remainder = (uint64_t)new_remainder; //< 计算剩余数字
return;
}
}
}

/**
* @brief 处理计算结果,根据小数点位置拆分为整数和小数部分
* @param[in] input 指向预处理后的输入信息结构体
* @param[in] calc 指向计算完成后的状态结构体
* @return sqrt_result_t 包含整数和小数部分的最终结果
*
* 算法得到的 `calc->result` 是一个纯整数,
* 此函数负责分离整数和小数部分
*/
sqrt_result_t process_dot(sqrt_input_t* input, sqrt_calc_t* calc) {
uint8_t integer_groups = input->integer_groups;
sqrt_result_t result;
result.int_part = 0;
result.frac_part = 0;
uint64_t temp = calc->result;
uint8_t digit = 0; //< result 的位数
int8_t over_digit = 0; //< 与最终结果相比,result 多(少)的位数

for (uint8_t i = 1; i <= 20; i++) {
if (temp < 10) {
digit = i;
break;
} else {
temp /= 10;
}
}

over_digit = (int8_t)digit - (int8_t)integer_groups;
if (over_digit >= 0) {
result.int_part = calc->result / powers_of_10[over_digit];
result.frac_part = calc->result % powers_of_10[over_digit];
} else { //< 开方结果位数极大,迭代次数极小,可能出现这种情况
uint8_t under_digit = -over_digit;
result.int_part = calc->result * powers_of_10[under_digit];
result.frac_part = 0;
}

return result;
}

/**
* @brief 初始化输入结构体,对被开方数进行预处理
* @param[out] input 指向要初始化的输入结构体
* @param[in] num 用户提供的被开方数
*
* 将被开方数按两位一组进行分组,确定整数部分的分组数和最高位组的具体数值
*/
void sqrt_init_input(sqrt_input_t* input, uint64_t num) {
// 使用 memset 将整个 input 结构体清零,确保所有成员都处于已知初始状态
memset(input, 0, sizeof(sqrt_input_t));
input->input_num = num;

// 如果输入为 0,则平方根为 0,直接返回,无需进一步计算
if (num == 0) {
return;
}

// 分组
uint64_t temp = num;

for (uint8_t i = 0; i <= 20; i++) {
if (temp < 100) {
input->integer_groups = i + 1;
// 计算最高组的实际值
input->highest_group_num =
num / powers_of_10[2 * i]; //< 1 组占 2 位,10^2
break;
}
temp /= 100;
}
}

/**
* @brief 初始化计算状态结构体
* @param[in] input 指向预处理后的输入信息结构体
* @param[out] calc 指向要初始化的计算状态结构体
*
* 根据 `sqrt_init_input` 的结果,将被开方数分解为具体的
* 两位数组,并存入 `calc->group` 数组中,为迭代计算做准备
*/
void sqrt_init_calc(sqrt_input_t* input, sqrt_calc_t* calc) {
// 使用 memset 将整个 calc 结构体清零,确保所有成员都处于已知初始状态
memset(calc, 0, sizeof(sqrt_calc_t));

// 数组赋值
uint8_t group_num = input->integer_groups;
uint64_t temp = input->input_num;
while (group_num--) {
uint8_t num = temp % 100;
calc->group[group_num] = num;
temp /= 100;
}

calc->current_group = 1; //< 忽略第 0 项,从第 1 项开始
}

int main(void) {
uint8_t times = 0;
uint64_t num_to_sqrt;

printf("请输入被开方数(正整数):");
scanf("%llu", &num_to_sqrt);
printf("请输入迭代次数(正整数):");
scanf("%u", &times);

// 1. 初始化 input 结构体
sqrt_input_t input;
sqrt_init_input(&input, num_to_sqrt);

if (times > MAX_ITERATIONS) {
printf("------\n");
printf("警告:迭代次数过大,最高为 %d\n", MAX_ITERATIONS);
times = MAX_ITERATIONS;
}

// 2. 初始化 calc 结构体
sqrt_calc_t calc;
sqrt_init_calc(&input, &calc);
cycle_init(&input, &calc);

// 3. 开始迭代计算
for (uint8_t i = 0; i < times; i++) {
cycle_calc(&calc);
}

// 4. 处理小数点并输出结果
sqrt_result_t result = process_dot(&input, &calc);
// 计算小数位数,防止小数以 0 开头导致错误
uint8_t frac_digit = times + 1 - input.integer_groups;
printf("------\n");
printf("最终结果为:%llu.%0*llu \n", result.int_part, frac_digit,
result.frac_part);

return 0;
}

运行结果示例:

1
2
3
4
5
6
请输入被开方数(正整数):2
请输入迭代次数(正整数):10
------
警告:迭代次数过大,最高为 9
------
最终结果为:1.414213562