news 2026/11/4 2:31:57

高精度计算入门与immense数据模型精讲(四)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高精度计算入门与immense数据模型精讲(四)

跳转链接

  • 高精度计算入门与immense数据模型精讲(一)
  • 高精度计算入门与immense数据模型精讲(二)
  • 高精度计算入门与immense数据模型精讲(三)
  • 高精度计算入门与immense数据模型精讲(五)
  • 高精度计算入门与immense数据模型精讲(六)
  • 高精度计算入门与immense数据模型精讲(七)
  • 高精度计算入门与immense数据模型精讲(八)
  • 高精度计算入门与immense数据模型精讲(九)
  • immense大实数数据库

前言:从比较到运算,竖式思想的第一次完整落地

前面三篇我们搭建了存储模型、搞定了归一化规则、讲透了比较逻辑,完成了高精度库的全部基础准备。从本篇开始,我们正式进入运算模块。加减是所有竖式运算的起点,逻辑最直观,也最能体现「模拟人手计算」的核心思想。

很多人接触高精度都是从「大数加法」的算法题开始的,但真正写好支持小数的实数加减并不简单:小数点怎么对齐?进位如何跨整数与小数传递?借位不够怎么办?正负数混合运算如何统一逻辑?这些问题处理不好,代码就会充满分支,bug丛生。

本篇依然严格遵循「通用原理先行,代码实现对照」的教学节奏:先把十进制高精度加减的通用方法论讲透,从无符号整数到实数,再到带符号运算的统一处理;再对照immense的源码,逐行拆解Add、Less、Centre_AL三个核心函数,看它们如何把通用思想落地。


一、通用原理:高精度加减的核心方法论

1.1 本质:把小学竖式搬进代码

高精度加法和减法的底层逻辑,和我们小学学的手算完全一致。

人手算加法的标准步骤:

  1. 右对齐:个位对个位、十位对十位,保证相同权重的数位对齐
  2. 从低位算起:从最右侧的个位开始,逐位相加
  3. 满十进一:每一位相加超过10,就向高位进1
  4. 最高位进位:所有位算完还有进位,结果多一位

减法的步骤对称:

  1. 右对齐,从低位算起
  2. 不够减就向高位借1当10
  3. 必须保证被减数大于等于减数,否则交换位置,结果加负号

计算机做高精度加减,就是把这个过程用数组/字符串复刻出来。唯一的变量是存储顺序——存储方式不同,对齐和遍历的方向也不同。

1.2 无符号整数加法:通用算法

前提约定

两个非负整数,采用倒序存储(低位在下标0)。这是高精度整数最主流的存储方式。

标准算法步骤
  1. 以较长的数为基准,较短的数高位等效补零(循环超出长度按0处理)
  2. 初始化进位变量carry = 0
  3. 从下标0开始正序遍历每一位:
    • 当前位和 = 甲位数值 + 乙位数值 + 低位进位
    • 当前位结果 = 和 % 10
    • 新的进位 = 和 / 10
  4. 遍历结束后,如果carry > 0,在结果末尾追加一位进位
  5. 结果天然为倒序,无需额外反转
时间复杂度

O(n),n 为较长数的位数。加法是线性时间运算,没有更快的通用算法。

教学要点:为什么倒序存储是加法的最佳选择?
因为最低位在下标0,两个数的最低位天然对齐,循环从0开始正序走即可;进位产生的新位直接追加到字符串末尾,均摊O(1)。如果是正序存储,不仅要从末尾往前遍历,进位还要插入到字符串头部,时间复杂度变成O(n),麻烦很多。

1.3 无符号整数减法:通用算法

减法比加法多一条铁律:必须保证被减数 ≥ 减数,否则会出现无限借位的逻辑错误。

标准算法步骤
  1. 前置判断:若被减数 < 减数,交换两者,标记结果为负
  2. 初始化借位变量borrow = 0
  3. 从下标0开始正序遍历每一位:
    • 被减数当前位先减去低位来的借位
    • 如果被减数位 < 减数位:被减数位 += 10,新借位 = 1
    • 否则:新借位 = 0
    • 结果位 = 被减数位 - 减数位
  4. 遍历结束后,清理结果的前导零(倒序存储即清理末尾的零)
  5. 根据标记决定是否给结果加负号
重点:连续借位的处理

比如计算1000 - 1,需要从千位一路借位到个位,中间连续三位都是0。算法中的borrow变量会逐位传递,只要循环完整走完,就能自动处理连续借位,不需要额外写特殊分支。这是很多初学者容易卡壳的地方。

1.4 实数加减:小数点对齐是核心

从整数扩展到实数,唯一新增的复杂度就是小数点对齐。只有小数点对齐了,相同权重的数位才能正确相加减。

通用对齐方法

两个实数相加减,先统一小数位数:

  • 小数位数少的,尾部补零,让两者小数长度相同
  • 对齐之后,小数部分和整数部分就可以分别按整数加减处理
  • 加法的进位会从小数部分传递到整数部分,减法的借位会从整数部分传递到小数部分

举个直观例子:12.34 + 5.678

  1. 统一小数位数:12.340 + 5.678
  2. 小数部分相加:340 + 678 = 1018,保留018,进位1传给整数
  3. 整数部分相加:12 + 5 + 1 = 18
  4. 合并结果:18.018

教学要点:实数加减的本质,就是「对齐小数点 → 当成大整数加减 → 还原小数点」。所有实数高精度加减,最终都会退化成整数加减。小数点对齐是实数库独有的额外工作量。

1.5 带符号运算:符号中心化统一处理

前面讲的都是无符号数的加减。但实际使用中数有正有负,加法和减法还可以互相转化。如果每种符号组合都写一套逻辑,代码会极其冗余,也容易出错。

工程上的通用最佳实践叫符号中心化:

所有带符号的加法和减法,都先取绝对值,转化成两个无符号数的运算;根据两个数的符号决定最终做加法还是减法;运算结束后再回填正确的符号。

具体来说,对于任意a op b(op 可以是 + 或 -):

  1. 先统一成a + (±b)的形式(减法等价于加相反数)
  2. 取两者绝对值 |a|、|b|
  3. 同号:结果 = 无符号加(|a|, |b|),符号与 a 相同
  4. 异号:用大的绝对值减小的绝对值,符号与绝对值大的一方相同

这样一来,无论正加正、正减负、负加正、负减负,全部复用同一套无符号 Add 和 Less 函数。外层只处理符号调度,内层只专心算数值。这就是「符号与数值分离」设计的最大价值。

1.6 必须覆盖的边界案例

写好加减运算,一定要用以下边界用例验证:

  1. 零加任何数 = 任何数
  2. 相等两数相减 = 0
  3. 全进位:999 + 1 = 1000,位数增加
  4. 全借位:1000 - 1 = 999,位数减少
  5. 小数跨整数进位:0.9 + 0.2 = 1.1
  6. 跨小数点借位:1.0 - 0.1 = 0.9
  7. 异号相加结果为零:123 + (-123) = 0

这些案例覆盖了进位、借位、符号、零值所有关键路径,是检验加减实现正确性的试金石。


二、immense 实现对照:加减三函数逐行精讲

immense的加减体系严格遵循上述通用原理,采用经典的三层架构:

  1. 底层运算层:Add(无符号实数加)、Less(无符号实数减)
  2. 中层调度层:Centre_AL(符号中心化调度)
  3. 上层接口层:运算符重载(+、-、+=、-=、自增自减)

我们从底层往上层逐段拆解。

2.1 Add 函数:无符号实数加法

完整源码:

immense Add(immense local,immense other){ std::string x=local.DCMp,y=other.DCMp; while(x.size()<y.size()) x.push_back('0'); while(y.size()<x.size()) y.push_back('0'); int carry=0,p,q; for(int i=(int)y.size()-1;i>=0;i--){ p=x[i]-'0'; q=y[i]-'0'; x[i]=(p+q+carry)%10+'0'; carry=(p+q+carry)/10; } local.DCMp=x; x=local.LTGp,y=other.LTGp; if(x.size()<y.size()) std::swap(x,y); int i; for(i=0;i<y.size();i++){ p=x[i]-'0'; q=y[i]-'0'; x[i]=(p+q+carry)%10+'0'; carry=(p+q+carry)/10; } while(i<x.size()&&carry){ int t=x[i]-'0'; x[i]=(t+carry)%10+'0'; carry=(t+carry)/10; ++i; } if(carry)x=x+'1'; local.LTGp=x; local.rationalize(); return local; }

对照通用原理,拆解为三个阶段:

第一阶段:小数部分对齐与相加
std::string x=local.DCMp,y=other.DCMp; while(x.size()<y.size()) x.push_back('0'); while(y.size()<x.size()) y.push_back('0');

先对小数部分补零对齐,短的尾部补零,让两者长度一致。完全对应「小数点对齐」的通用步骤。

for(int i=(int)y.size()-1;i>=0;i--){ p=x[i]-'0'; q=y[i]-'0'; x[i]=(p+q+carry)%10+'0'; carry=(p+q+carry)/10; }

小数是正序存储,最低位在字符串末尾,所以从后往前遍历。逐位相加,记录进位。
这里产生的carry会从小数最低位一直传递到小数最高位(十分位),然后继续传递给整数部分,实现跨小数点的进位衔接。

第二阶段:整数部分相加
x=local.LTGp,y=other.LTGp; if(x.size()<y.size()) std::swap(x,y);

整数部分是倒序存储,低位在下标0。先把长的放前面,保证循环基准统一。

for(i=0;i<y.size();i++){ p=x[i]-'0'; q=y[i]-'0'; x[i]=(p+q+carry)%10+'0'; carry=(p+q+carry)/10; }

从下标0开始正序遍历,对应从低位到高位。这里的carry就是小数部分传过来的进位,天然衔接。完全匹配通用整数加法算法。

while(i<x.size()&&carry){ int t=x[i]-'0'; x[i]=(t+carry)%10+'0'; carry=(t+carry)/10; ++i; } if(carry)x=x+'1';

短的数遍历完后,如果还有进位,就继续处理长数的剩余位。最后如果仍有进位,在字符串末尾追加一位。
对应通用算法的「剩余位处理 + 最终进位」两步。

第三阶段:归一化收尾
local.rationalize(); return local;

运算完成后统一调用归一化,同步长度变量,清理可能产生的无效零。这是immense所有运算的标准收尾动作。

2.2 Less 函数:无符号实数减法

完整源码:

immense Less(immense local,immense other){ if(local==other)return immense(long(0)); if(local<other) return -Less(other,local); int carry=0,x=0,y=0; char ch; while((int)local.DCMp.size()<other.DCMn)local.DCMp.push_back('0'); while((int)other.DCMp.size()<local.DCMp.size()) other.DCMp.push_back('0'); for(int i=(int)other.DCMn-1;i>=0;i--){ x=local.DCMp[i]-'0'; y=other.DCMp[i]-'0'; local.DCMp[i]=(x-carry-y+10)%10+'0'; carry=(x-carry-y)<0; } int i=0; for(;i<other.LTGn;i++){ x=local.LTGp[i]-'0'; y=other.LTGp[i]-'0'; local.LTGp[i]=(x-carry-y+10)%10+'0'; carry=(x-carry-y)<0; } while(carry){ if(i>=(int)local.LTGp.size()) local.LTGp.push_back('0'); ch=static_cast<unsigned char>(local.LTGp[i])-1,carry=0; if(ch<'0') ch+=10; local.LTGp[i] = static_cast<char>(ch); ++i; } local.rationalize(); return local; }

对照通用减法原理,分步拆解:

第一步:前置校验与交换保证
if(local==other)return immense(long(0)); if(local<other) return -Less(other,local);

这是减法的标准前置处理:

  • 两数相等直接返回0
  • 若被减数小于减数,交换位置递归调用,结果取负
  • 保证进入后续逻辑时,被减数一定 ≥ 减数,不会出现负结果

完全对应通用减法的前提保证。

第二步:小数部分对齐与相减
while((int)local.DCMp.size()<other.DCMn)local.DCMp.push_back('0'); while((int)other.DCMp.size()<local.DCMp.size()) other.DCMp.push_back('0');

和加法一样,先补零对齐小数位数。

for(int i=(int)other.DCMn-1;i>=0;i--){ x=local.DCMp[i]-'0'; y=other.DCMp[i]-'0'; local.DCMp[i]=(x-carry-y+10)%10+'0'; carry=(x-carry-y)<0; }

从小数末尾(最低位)开始往前减。carry在这里是借位标记,1表示需要借位,0表示不需要。
技巧:(x - carry - y + 10) % 10这个写法,无论够不够减都能算出正确的当前位,不用写 if-else 分支,简洁且不易出错。

第三步:整数部分相减
int i=0; for(;i<other.LTGn;i++){ x=local.LTGp[i]-'0'; y=other.LTGp[i]-'0'; local.LTGp[i]=(x-carry-y+10)%10+'0'; carry=(x-carry-y)<0; }

整数倒序存储,从下标0(低位)开始正序遍历。借位从小数部分传过来,天然衔接。计算逻辑和小数部分完全一致。

第四步:连续借位处理
while(carry){ if(i>=(int)local.LTGp.size()) local.LTGp.push_back('0'); ch=static_cast<unsigned char>(local.LTGp[i])-1,carry=0; if(ch<'0') ch+=10; local.LTGp[i] = static_cast<char>(ch); ++i; }

减数遍历完后,如果还有借位,就继续处理被减数的剩余高位。
这就是处理「连续借位」的逻辑,比如1000 - 1,借位会一路传递下去。因为前置已经保证了被减数更大,所以借位最终一定会清零,不会无限循环。

第五步:归一化收尾

减法一定会产生前导零(倒序存储即末尾的零),比如1000 - 1 = 0999,所以必须调用rationalize清理前导零,同步长度。

2.3 Centre_AL:符号中心化调度器

Centre_AL是加减运算的中枢,对应通用原理的「符号中心化」思想,负责把所有带符号运算调度到无符号的Add和Less上。

完整源码:

immense Centre_AL(immense x,bool AmS,immense y){ if(!AmS)return Centre_AL(x,true,-y); immense result; bool p=x.symbol,q=y.symbol; x.abs(),y.abs(); if(p){ if(q) result=Add(x,y); else result=Less(x,y); } else{ if(q) result=Less(y,x); else result=-(Add(x,y)); } return result; }
参数说明
  • x:第一个操作数
  • AmS:true表示加法,false表示减法
  • y:第二个操作数
第一步:减法转加法
if(!AmS)return Centre_AL(x,true,-y);

如果是减法,就转化成「x 加 -y」,递归调用加法版本。
这样所有运算最终都统一成加法形式,后面只需要处理同号、异号两种情况,分支数量减半。

第二步:符号与数值分离
bool p=x.symbol,q=y.symbol; x.abs(),y.abs();

保存两个数的符号,然后取绝对值,后续只计算绝对值。

第三步:四分支调度
if(p){ if(q) result=Add(x,y); // 正 + 正 = 直接加 else result=Less(x,y); // 正 + 负 = 正减正 } else{ if(q) result=Less(y,x); // 负 + 正 = 正减负 else result=-(Add(x,y));// 负 + 负 = 相加取负 }

四种符号组合,对应四种调度:

  1. 正+正:直接相加,结果为正
  2. 正+负:大数减小数,结果符号随大数
  3. 负+正:交换顺序,逻辑同上
  4. 负+负:绝对值相加,结果取负

Less函数内部已经处理了「谁大谁小」的问题,所以这里直接传参即可,无需额外判断大小。

2.4 运算符层:统一接口封装

上层的所有运算符全部调用Centre_AL,代码极其简洁:

inline immense immense::operator+(const immense other)const{ immense t=other; return Centre_AL(*this,true,t); } inline immense immense::operator-(const immense other)const{ immense t=other; return Centre_AL(*this,false,t); }

复合赋值、自增自减也是同理复用:

inline void immense::operator+=(const immense other){ immense t=other; *this=Centre_AL(*this,true,t); } inline immense &immense::operator++(){ *this=Centre_AL(*this,true,immense("1")); return *this; }

自增自减本质就是加/减1,完全复用同一套逻辑。

这种分层设计的优势非常明显:

  • 底层Add/Less只关心无符号运算,逻辑纯粹
  • 中层Centre_AL只关心符号调度,不碰具体运算
  • 上层运算符只做接口包装,零逻辑冗余
  • 出问题排查方便,改bug只改对应层

三、拓展:加减运算的性能与设计权衡

3.1 时间复杂度分析

加减运算都是线性时间 O(n),n 为总位数。

  • 加法:一次遍历完成,常数因子很小
  • 减法:同样一次遍历,但前置有一次比较判断大小,常数因子略大

在高精度运算体系里,加减是最快的基础运算,也是所有高级运算的基石——乘法、除法、开方、级数内部都会大量调用加减。

3.2 关键设计权衡点评

  1. 整数倒序 + 小数正序的不对称存储
    • 优点:小数点平移方便,进位借位的传递方向与遍历方向天然匹配
    • 缺点:整数和小数遍历方向相反,增加少量心智负担
    • 综合来看,对于支持小数的实数库,是利大于弊的选择
  2. 值传递而非常量引用Add和Less的参数都是值传递,函数内部直接修改副本返回。
    • 优点:代码书写直观,不用创建额外临时变量
    • 缺点:会触发一次字符串拷贝
    • 对于教学级、工具级库来说,可读性优先,这个取舍非常合理
  3. 中心化统一调度
    用Centre_AL把所有加减统一调度,是非常经典的工程做法。虽然多了一层函数调用,但代码复用率极高,符号逻辑只写一次,大幅降低了出错概率。

3.3 自我验证练习

学到这里,你可以自己推演几个案例,对照代码逻辑走一遍,检验是否真正理解:

  1. immense("0.9") + immense("0.2")→ 小数进位到整数,结果 1.1
  2. immense("1.0") - immense("0.1")→ 跨小数点借位,结果 0.9
  3. immense("999.99") + immense("0.01")→ 全进位,结果 1000.0
  4. immense("-123") + immense("45")→ 异号相减,结果 -78

如果都能正确推演,说明你已经完全掌握了高精度加减的核心逻辑。


四、本篇小结与下篇预告

4.1 核心知识点回顾

通用原理部分:

  1. 加减本质是模拟竖式计算,核心是进位和借位的传递处理
  2. 倒序存储的整数最适合加减,低位天然对齐,进位追加方便
  3. 实数加减的关键是小数点对齐,统一小数位数后转成整数加减
  4. 带符号运算的最佳实践是符号中心化:取绝对值 → 调度无符号运算 → 回填符号
  5. 减法必须保证被减数 ≥ 减数,否则交换位置结果取负

实现对照部分:

  1. Add函数:对齐小数 → 小数带进位相加 → 整数带进位相加 → 最终进位处理 → 归一化
  2. Less函数:前置保证被减数大 → 对齐小数 → 小数借位相减 → 整数借位相减 → 连续借位处理 → 归一化
  3. Centre_AL:减法转加法 → 符号分离 → 四分支调度 → 返回带符号结果
  4. 运算符层全部复用中心化函数,分层清晰,无逻辑冗余

4.2 下篇预告

加减是线性运算,乘法就不一样了。普通竖式乘法是 O(n²),对于大数来说性能很差。工业级高精度库都会用更快的乘法算法,比如Karatsuba 分治乘法,可以把时间复杂度降到 O(n^1.585)。

下一篇我们就来讲乘法:先讲普通竖式乘法的通用原理,再讲 Karatsuba 分治乘法的算法思想与复杂度推导;最后对照immense的源码,看它如何实现普通竖式与 Karatsuba 的自动切换,以及如何处理实数乘法的小数点位置。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 2:31:57

LabVIEW 入门快是优势,也是最大的坑

很多人学 LabVIEW 的第一周&#xff0c;就能拖出带按钮、带图表的界面&#xff0c;看着波形在屏幕上动起来&#xff0c;特别有成就感。这确实是 LabVIEW 最迷人的地方——别的语言还在配环境、调编译&#xff0c;你已经把数据采集跑通了。但今天想泼一盆不太凉的冷水&#xff1…

作者头像 李华
网站建设 2026/11/4 2:31:56

【赵渝强老师】AI for PostgreSQL数据库

《AI for PostgreSQL》课程详细描述 &#x1f4da; 课程讲师&#xff1a;赵渝强&#xff5c;&#x1f6e0; 技术方向&#xff1a;大模型Agent Skill PostgreSQL 15.3实战 课程简介 《AI for PostgreSQL》是面向PostgreSQL DBA、后端开发工程师、数据库运维人员打造的实战课程…

作者头像 李华
网站建设 2026/11/4 2:31:51

YOLO在工业缺陷检测中的落地实践

YOLO在工业缺陷检测中的落地实践 工业缺陷检测(Industrial Defect Detection)并不是把通用目标检测模型换个数据集那么简单。产线现场同时存在成像窗口窄、缺陷尺度极端、良品与不良品比例悬殊、节拍以毫秒计算、误判需要可追溯等约束。很多在公开数据集上表现优异的模型,一…

作者头像 李华
网站建设 2026/11/4 2:31:04

stm32u5a5HAL库study-点个灯吧 顺便阅读一些函数

ok&#xff0c;我们今天先通过STM32U5A5这块开发板点个灯&#xff0c;并通过按键去控制led的亮灭。首先&#xff0c;我们先看一下这块开发板的原理图。 绿色led红色与蓝色led按键button ok&#xff0c;我们现在打开cubemx去进行一下GPIO的配置我们将各个GPIO口添加上Labels&…

作者头像 李华
网站建设 2026/11/4 2:30:41

HDPE户外家具挤塑微发泡板材螺钉握持性能不足的技术归因分析

在HDPE户外家具工业化生产场景中&#xff0c;存在一种隐蔽性强、危害程度高的典型失效模式&#xff1a;板材下线时的常规力学性能指标均符合规范要求&#xff0c;但在成品装配、长距离物流运输、终端用户拆装服役过程中&#xff0c;自攻螺钉旋入后易发生滑牙、拔出及连接界面松…

作者头像 李华