news 2026/11/3 19:29:25

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

作者头像

张小明

前端开发工程师

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

跳转链接

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

前言:比较运算,所有高级运算的基石

学完归一化与类型转换,我们已经能让一个高精度数保持规范、正确地进出系统。接下来自然要进入运算部分,但在讲加减乘除之前,必须先把比较运算讲透。

很多人觉得比较很简单,不就是逐位比大小吗?但实际上,比较是高精度库中最基础、也最容易藏细节bug的模块。它不只是用来给用户判断大小——除法的二分试商要靠它、开方的迭代终止要靠它、排序去重要靠它、取整取模也要靠它。可以说,所有高级运算的正确性,都建立在比较运算的正确性之上。

本篇我们依然遵循「通用原理先行,代码实现对照」的节奏:先把高精度比较的通用法则讲透,让你明白“为什么要这么比”;再对照immense的源码,看它是如何把四层裁决逻辑落地成具体代码的。读完你会发现,短短几十行的比较函数里,藏着非常多的设计考量。


一、通用原理:高精度比较的核心法则

1.1 比较的本质:高位优先原则

我们人手比较两个数的大小,直觉就是「先看谁位数多,位数一样就从左往右一位一位比」。这个直觉背后,是一条最核心的原则:高位优先原则。

一个数位的权重,越靠左越大。比如百位的 1 比十位的 9 还大。所以只要高位分出了大小,后面低位是什么都不重要了。

对于高精度实数,完整的比较优先级是:

  1. 符号> 2.整数位数> 3.整数逐位> 4.小数逐位> 5.小数长度

越靠前的裁决,权重越高,能提前分出结果就立刻终止,不用再比后面。这既是正确性的保证,也是性能优化的关键。

教学要点:所有高精度比较算法,无论整数还是实数,无论十进制还是二进制,都严格遵守高位优先。这是一条通用铁律。

1.2 第一层裁决:符号优先

两个数比较,第一步永远是看符号。这是成本最低、区分度最高的判断。

通用规则只有三条:

  • 正数 > 负数
  • 都是正数,继续比绝对值
  • 都是负数,继续比绝对值,但最终结果要翻转(绝对值大的负数反而更小)

绝大多数异号比较,在这一步就能直接出结果,时间复杂度 O(1)。

这里要特别提醒一个经典坑:零值的符号。如果库允许负零存在,那么0和-0符号不同,但数值相等,会导致相等判断失效。这也是第二篇强调「零值符号必须统一为正」的原因——比较运算重度依赖这个不变式。

1.3 第二层裁决:整数位数

符号相同之后,先比整数部分的位数。

道理很简单:两位数一定大于一位数,三位数一定大于两位数。整数位数多的那个,绝对值一定更大。

  • 整数位数多 → 绝对值大
  • 整数位数少 → 绝对值小
  • 位数相等 → 进入下一层逐位比较

教学要点:这一步成立的前提,是整数部分没有前导零。如果带着前导零,"00123"位数是 5,"456"位数是 3,会误判前者更大。所以归一化是比较正确的前提——没有归一化,比较逻辑全是空中楼阁。

1.4 第三层裁决:整数逐位比对

整数位数相同,就从最高位开始,往最低位逐位比对。

  • 遇到第一位不相等的,就能分出大小
  • 全部相等,进入小数部分比较

这里有一个非常容易出错的细节:遍历方向由存储顺序决定。

存储方式最高位位置遍历方向
正序存储(高位在前)字符串下标 0从左往右(i 从 0 到 n-1)
倒序存储(低位在前)字符串末尾从右往左(i 从 n-1 到 0)

很多初学者记不住,写出来的比较逻辑方向反了,结果变成个位决定大小,完全错误。记住一个简单的判断方法:

永远从最高位那一侧开始比。正序最高位在左,就从左往右;倒序最高位在右,就从右往左。

1.5 第四层裁决:小数部分比对

整数部分完全相等,才需要比小数。小数的比较逻辑和整数对称,但方向是从左往右(十分位 → 百分位 → 千分位),因为十分位是小数的最高位。

这里会遇到一个新问题:两个数的小数长度不一样怎么办?

通用处理方法:短的一方补零等效。比如1.2和1.234比较,等价于1.200和1.234比较。

  • 先比对公共长度内的每一位
  • 如果公共部分全部相等,再看谁的小数更长:更长的那个更大(因为多出来的位都是有效数字,至少有一位非零,归一化保证了尾部没有零)

为什么归一化之后可以这么判断?因为如果小数尾部有零,归一化会删掉。所以只要小数更长,多出来的部分一定以非零数字开头,所以数值一定更大。这又是归一化带来的便利。

1.6 负数的结果翻转

前面所有比出来的都是「绝对值大小」。如果两个数都是负数,最后要把结果反过来:

  • 绝对值大的负数,数值更小
  • 绝对值小的负数,数值更大

工程上有两种常见写法:

  1. 先算abs(a) > abs(b),再根据符号决定是否取反
  2. 把符号和结果合并成一个逻辑表达式,一步返回

两种写法等价,第二种更简洁,第一种更易读。

1.7 工程最佳实践:六向比较只实现两个核心

完整的比较运算符有六个:==、!=、>、<、>=、<=。

但成熟的库绝不会把六个函数各写一遍逻辑——那样不仅代码冗余,还容易出现逻辑不一致的bug。

通用做法是:只核心实现==和>,剩下四个全部用这两个推导出来。

  • a != b等价于!(a == b)
  • a < b等价于!(a >= b)
  • a >= b等价于a > b || a == b
  • a <= b等价于!(a > b)

这样做的好处:

  1. 核心逻辑只有一处,改bug只改一个地方
  2. 六个运算符逻辑绝对一致,不会出现自相矛盾
  3. 代码量大幅减少,维护成本低

这是运算符重载的经典设计范式,几乎所有C++数值类库都会遵守。

1.8 常见坑点盘点

初学者写比较运算,高频踩坑集中在这几个地方:

  1. 前导零干扰位数判断
    • 现象:"0012"被当成 4 位数,比"34"大
    • 根源:没有做归一化,比较前没清前导零
  2. 遍历方向搞反
    • 现象:倒序存储的整数,从下标 0 开始比,变成个位决定大小
    • 根源:没搞清楚最高位在哪一侧
  3. 负零导致相等失效
    • 现象:0 == -0返回 false
    • 根源:零值符号不统一
  4. 小数长度不同时直接判等
    • 现象:1.2和1.20被判断为不相等
    • 根源:忽略了尾部零,或者没做归一化
  5. 负数忘记翻转结果
    • 现象:-3 > -2返回 true
    • 根源:只比了绝对值,忘记负数结果相反

理解了这些通用原理和坑点,再去看任何一个高精度库的比较代码,都会一目了然。


二、immense 实现对照:比较运算符逐行精讲

immense的比较运算严格遵循上述通用法则:核心实现operator==和operator>,剩下四个全部是一行推导。我们从最简单的相等判断讲起,再深入核心的大于判断。

2.1 相等判断 operator==:归一化带来的简洁

源码非常短,只有一行:

inline bool immense::operator==(const immense& other)const{ return (this->symbol==other.symbol) && (this->DCMp==other.DCMp) && (this->LTGp==other.LTGp); }

三个条件同时满足就相等:符号相同、整数串相同、小数串相同。

为什么可以直接比字符串?因为归一化保证了存储的唯一性:

  • 没有前导零,所以同一个整数只有一种字符串表示
  • 没有尾部零,所以同一个小数只有一种字符串表示
  • 零值符号统一为正,不会出现负零

所以只要字符串相等,数值就一定相等。反过来,数值相等,字符串也一定相等。

这就是归一化的巨大价值——它让相等判断变成了简单的字符串比较,O(n) 但代码极其简洁,几乎不可能写错。

2.2 大于判断 operator>:四层裁决的完整落地

operator>是整个比较体系的核心,所有大小关系都由它派生。我们分段拆解,对应前面讲的四层裁决。

完整源码:

bool immense::operator>(const immense& other)const{ if(*this==other)return false; if(this->symbol>other.symbol)return true; if(this->symbol<other.symbol)return false; bool x=this->symbol,y=true; if(this->LTGn!=other.LTGn){ if(this->LTGn>other.LTGn)y=true; else y=false; goto finish; } for(int i=this->LTGn-1;i>=0;i--){ if(this->LTGp[i]>other.LTGp[i]){ y=true; goto finish; } if(this->LTGp[i]<other.LTGp[i]){ y=false; goto finish; } } for(int i=0;i<std::min(this->DCMn,other.DCMn);i++){ if(this->DCMp[i]-'0'>other.DCMp[i]-'0'){ y=true; goto finish; } if(this->DCMp[i]-'0'<other.DCMp[i]-'0'){ y=false; goto finish; } } if(this->DCMn>other.DCMn)y=true; if(this->DCMn<other.DCMn)y=false; finish: ; return x==y; }

我们一层一层对应。

第一步:相等快速返回
if(*this==other)return false;

相等的两个数,谁也不大于谁,直接返回 false。这是前置快速路径。

第二步:符号裁决
if(this->symbol>other.symbol)return true; if(this->symbol<other.symbol)return false;

symbol是 bool 类型,true代表正、false代表负。在C++里,true > false成立。

  • 本号为正、对方为负 → 直接返回 true(正数大于负数)
  • 本号为负、对方为正 → 直接返回 false(负数小于正数)

对应第一层裁决,符号不同一步出结果。

第三步:整数位数裁决
bool x=this->symbol,y=true; if(this->LTGn!=other.LTGn){ if(this->LTGn>other.LTGn)y=true; else y=false; goto finish; }

符号相同之后,先比整数位数。

  • y变量存的是「绝对值比较的结果」,y=true表示本数绝对值更大
  • 位数多 → 绝对值大 → y=true
  • 位数少 → 绝对值小 → y=false

分出结果直接goto finish,不用再比后面。这就是高位优先的提前终止优化。

第四步:整数逐位裁决
for(int i=this->LTGn-1;i>=0;i--){ if(this->LTGp[i]>other.LTGp[i]){ y=true; goto finish; } if(this->LTGp[i]<other.LTGp[i]){ y=false; goto finish; } }

这里对应第三层裁决,重点看遍历方向:

  • LTGp是倒序存储,最高位在字符串末尾
  • 所以循环从LTGn-1(最后一位)开始,往 0 走
  • 遇到第一位不同的,立刻设置 y 并跳出

对照教学要点:倒序存储从右往左比,最高位优先。这行代码完美对应通用原理。

第五步:小数逐位裁决
for(int i=0;i<std::min(this->DCMn,other.DCMn);i++){ if(this->DCMp[i]-'0'>other.DCMp[i]-'0'){ y=true; goto finish; } if(this->DCMp[i]-'0'<other.DCMp[i]-'0'){ y=false; goto finish; } }

小数是正序存储,最高位(十分位)在下标 0,所以从 0 开始正序遍历。

  • 只遍历公共长度部分
  • 第一位不同就分出结果,立刻跳出

完全对应第四层裁决的通用逻辑。

第六步:小数长度裁决
if(this->DCMn>other.DCMn)y=true; if(this->DCMn<other.DCMn)y=false;

公共部分全部相等,就看谁的小数更长。

  • 本数小数更长 → 绝对值更大 → y=true
  • 更短 → y=false

成立的前提依然是归一化:尾部没有零,更长就意味着多出来的部分有有效数字。

第七步:符号与结果合并
finish: ; return x==y;

这是整个函数最巧妙的一行。x是本数的符号,y是绝对值比较结果。

  • 当x=true(两数都是正数):y 为 true 就返回 true,符合预期
  • 当x=false(两数都是负数):y 为 true 说明本数绝对值更大,负数绝对值大反而小,所以返回 false

用一个x==y的表达式,同时处理了正数和负数两种情况,不需要写 if-else 分支。非常简洁的技巧。

2.3 四个衍生运算符:极简复用

剩下四个运算符全部是一行推导,严格遵守「核心只写两个」的工程原则:

inline bool immense::operator>=(const immense& other)const{ return (*this>other||*this==other); } inline bool immense::operator<=(const immense& other)const{ return (!(*this>other)); } inline bool immense::operator<(const immense& other)const{ return (!(*this>=other)); } inline bool immense::operator!=(const immense& other)const{ return (!(*this==other)); }

对应关系:

  • >=:大于 或 等于
  • <=:不大于
  • <:不大于等于
  • !=:不相等

全部由==和>派生,逻辑完全一致,没有冗余。

2.4 设计细节点评

  1. 大量 goto 提前终止:每分出一次结果就goto finish,避免无效遍历。对于高精度大数来说,提前终止带来的性能收益非常明显。
  2. 符号与数值分离:先算绝对值结果 y,最后统一和符号 x 合并。逻辑分层清晰,不容易写错负数分支。
  3. 完全依赖归一化:整个函数没有任何清理前导零、尾部零的逻辑,默认输入都是规范的。这就是「集中归一化」带来的代码简洁性。

三、拓展:比较运算的性能与应用价值

3.1 时间复杂度分析

比较运算的时间复杂度是O(n),n 为总位数。但这是最坏情况——两个数几乎完全一样,要比到最后一位才能分出结果。

实际场景中,绝大多数比较在前几层就终止了:

  • 符号不同:O(1)
  • 位数不同:O(1)
  • 前几位就分出大小:远小于 O(n)

所以比较运算在实际使用中通常非常快。

3.2 为什么说比较是高级运算的基础

后面讲除法的时候你会看到,immense的除法采用「二分试商法」:每一位商都在 0~9 之间二分查找,而二分判断的依据,就是「余数和除数 × 商的大小比较」。

除此之外:

  • 取整函数需要和 0.5 比较
  • 泰勒级数需要判断余项是否小于精度阈值
  • 开方迭代需要判断前后两次结果的差值
  • 排序、去重、查找更不用说

可以说,比较运算就是高精度算法的「基础设施」。它写起来不难,但一旦写错,后面所有运算都会跟着错。

3.3 一个验证小练习

学到这里,你可以做一个简单的验证:自己手算几个案例,再对照代码逻辑走一遍,看结果是否一致。

  • immense("123") > immense("45")→ 位数不同,true
  • immense("-123") > immense("-45")→ 都是负数,绝对值大的更小,false
  • immense("1.23") > immense("1.234")→ 整数相同,小数公共部分相同,短的更小,false
  • immense("0") == immense("-0")→ 归一化后负零转正,true

如果全部算对,说明你已经真正理解了比较逻辑。


四、本篇小结与下篇预告

4.1 核心知识点回顾

通用原理部分:

  1. 高位优先原则:符号 > 整数位数 > 整数逐位 > 小数逐位 > 小数长度
  2. 负数比较:先比绝对值,结果翻转
  3. 工程最佳实践:只核心实现==和>,其余四个派生复用
  4. 四大常见坑:前导零、遍历方向、负零、小数尾部零

实现对照部分:

  1. operator==靠归一化保证,直接比字符串即可
  2. operator>完整实现了四层裁决,用 goto 做提前终止优化
  3. 最后用x==y的技巧统一处理正负号结果
  4. 四个衍生运算符全部由核心两个派生,无冗余逻辑

4.2 下篇预告

比较是基础,加减是进阶。下一篇我们将进入真正的竖式运算——加法与减法。

我们会讲清楚:小数点怎么对齐、进位和借位怎么处理、为什么要做「符号中心化」、带符号加减如何降维成无符号加减。同时对照immense里的Add、Less、Centre_AL三个函数,把加减运算的每一步都拆解透彻。

加减是竖式运算的第一个完整案例,吃透了它,后面学乘法会轻松很多。

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

[ adb ] adb常用调试命令

&#xff01;&#xff01;此文章会不定时更新adb的命令&#xff01;&#xff01;adb root &#xff08;获取超级管理员权限&#xff09;重启设备&#xff08;手机、车机&#xff09;里的 ADB 守护进程&#xff08;adbd&#xff09;&#xff0c;并让它以 root 权限运行重点&…

作者头像 李华
网站建设 2026/11/3 19:27:53

把 启元机器人Q1 的训练搬到昇腾 910B:从 MuJoCo 到 PPO 的一次完整迁移

把 Q1 的训练搬到昇腾 910B 从物理仿真到 PPO 的完整拆解&#xff0c;以及还没做完的部分 2026-10-30 GewisLab 目录 文章目录把 Q1 的训练搬到昇腾 910B目录[toc]一、启元这套东西是怎么搭的1.1 硬件与控制栈1.2 仓库都放了什么1.3 训练链路怎么串起来1.4 环境设计比想象中…

作者头像 李华
网站建设 2026/11/3 19:27:24

制造企业班车:从成本项变成招工竞争力

制造企业班车&#xff1a;从成本项变成招工竞争力 厂区门口摆摊招工的人都知道一件事&#xff1a;候选人问的第一个问题不是工资&#xff0c;是"有没有车"。 厂区在市郊、公交到不了、打车太贵&#xff0c;三个条件凑齐&#xff0c;通勤就成了招聘的硬门槛。同一片工…

作者头像 李华
网站建设 2026/11/3 19:26:12

Ego Lite怎样让AI操作网页?用一次真实表单演示看懂快照与回读

AI操作网页&#xff0c;不必每一步都盯着截图猜坐标。Ego Lite向Agent提供页面快照、填写、点击和结果读取等能力&#xff1a;模型先获得页面里有哪些控件&#xff0c;再调用工具操作浏览器&#xff0c;最后读取页面变化&#xff0c;决定是否继续。 下面用一个无账号、无外部请…

作者头像 李华
网站建设 2026/11/3 19:25:54

海康双光谱枪球一体机如何接入?双光取流、云台巡航、专家测温与烟火识别的 ISAPI 透传详解

一、为什么枪球一体机值得单独讲一次接入 做防火巡检、周界热成像、工业测温类应用的开发者&#xff0c;大多踩过同一个坑&#xff1a;两台设备、两套逻辑。 可见光球机负责画面观察&#xff0c;热成像枪机负责测温和火情感知&#xff0c;两边的取流参数、云台控制、告警事件各…

作者头像 李华