既已有基础材料,撸起袖子做作业。
什么都别说,先把用C++写出伪LISP代码(一)写的代码include进来:
作业要求
问题描述:
设计一个程序实现两个任意长的正数(包括正数和负数)、任意精度实数的算术运算;
提示:
(1)用动态链表存储数据,每节点含一个整型变量,表示若干位数;
(2)整数输入和输出按中国对于长整数的习惯表示,每3位1组,组间用逗号隔开;
(3)实现长整数的加、减运算;
(4)程序运行界面清晰实用;
选做:
(1)求两长整数之商、积
(2)高精度实数算术运算
最终成果
实现基础
要求3位3位地分割大数,对于我来说,很容易想到这是1000进制的加减乘除问题。因此问题转换成:如何实现任意进制的加减乘除。问题变得更通用,解决起来更有趣味和意义。
数据结构
既然是用伪LISP代码来完成,首先明确结构就应该是list
,用每个盒子的head
来存储该每3位数。定义如下:
(*注:以下所有C++代码均采用C++14标准)
补码
接触过计算机组成原理的读者都知道,计算机内部采用二进制补码形式表示数字。用补码来表示数字,可以有效简化一些运算。
遗憾的是,用补码并不能解决溢出问题,两个很大的正数相加,有可能会造成溢出,导致数位的循环,得到负数的结果。造成这样的结果原因是每个数字都只由有限的数位表示,一旦某些运算企图逾越这些数位的限制,便要承担运算错误的风险。
我决定也采用补码来进行进制运算,来获得运算的简化。但尽管我是在模拟进制运算,也采用补码,却由于可以动态申请内存,因此消除了溢出的风险,从而实现长整数运算。
至于什么是补码?简单地说:
- 正数采用原有表示,但最高位添加符号位,为0。
- 0用0表示。
- 负数无视符号位,将所有位取反,再加1,最后最高位添加符号位,为进制最大表示数。
想更为深入地了解何谓补码、为何用补码运算能简化计算,推荐阅读《编码》。
在这里,用一个list
来存储一个补码表示的数字,最前面为最低位,list
最深处则为最高位。
因此直接通过最高位来判断补码的正负性:
位操作
补充与抛弃
|
|
补充
补充低位操作,这个较简单,直接往最前面填需要填充的数字即可:
补充高位操作,则需要翻转一下list
,然后利用上面刚实现的函数:
抛弃
抛弃高位,由于有自带的ListHead函数,可以直接调用:
抛弃低位,同理:
算术左移与右移
|
|
算术左移
可以直接抛弃高位,低位补0:
算术右移
抛弃低位,高位补充符号位:
raise_digits
是用于提升位数的,这里只是借用。提升位数本质上是补充符号位的操作,补充符号位并不影响数值表示大小,下面来实现:
位数提升
|
|
注意,如果目标位数长度大于原位数长度,则不进行提升,直接返回原数。
取反
取反的意思是将每一位都取成进制最大表示数与其之差。取反需要遍历,遍历需要fold
:
进位
进位的重要性不言而喻,要保证进位后每一个位都不能超过进制最大表示数:
取负
这个大家都应该很熟悉了,取负的操作 —— 取反加一!
补码构造
实现了各种位操作,终于可以用来构造补码了。
用户输入的正数形式为123, 456, 789
,存储成list
即为(123, 456, 789)
,正好与补码形式相反,因此只需补充符号位,再翻转list
即可。
负数形式为-123, 456, 789
,存储成list
即为(-123, 456, 789)
,这里将第一位变成正数,借用构造正数的过程,最后再取负。
实现如下:
加减乘除
|
|
该来的还是来了。
加法
补码加法就是可以为所欲为,只需要位数相同,无视正负数直接加。当位数不同时,提升位数后再加。学过全加器的读者应该能看出add(carry(x->tail(), new_carry)
是多么亲切。
减法
传说中的开挂来了,不多说。
乘法
先实现一位乘以多位,接着实现多位乘以多位。计算过程和手动乘法差不多,注意到一位乘以多位可以直接用Map
,多位乘以多位用到了Fold
配合左移。
有读者可能会问,这不是补码吗,为什么这里感觉像是直接原码乘。是的,这里实际上采用的是补码一位乘的校正法,是我从华工编写的《计算机组成原理与汇编语言》里面找来的。最后还需要看情况补上一个余项。至于这个余项怎么算出来,读者可以自己翻书(笔者数学不好实在是搞不懂)。
除法
到了除法,我已经越来越懒于找更精妙的算法了。全部转成正数,直接仿照人工手动除法,最后再调整符号。quo_prime
和rem_prime
两个函数用于最初级的除法形式:被除数减除数,重复减直至负数,减的次数即为商。
当然不可能真的这样来计算商,我们可以先通过calc_digits
计算商的位数,然后模仿人工除法,把除数左移,然后再利用quo_prime
和rem_prime
一位一位算。
到这里,所有核心代码已经交代完毕,剩余的就是些交互代码了。交互代码可以放松对函数式的执着,毕竟全是在跟副作用打交道!
交互代码
最后再来实现一个REPL(Read–Eval–Print Loop)。
读取(Read)
|
|
求值(Eval)
|
|
打印(Print)
打印函数是整个程序中第二长的函数,可见为了用户体验,程序员付出了多少汗水。
ONE LOOP
|
|
REPL 以及错误处理
|
|
作业一到此为止,作业二更有意思,让我们拭目以待。
完整代码:https://github.com/zhongzc/C-/blob/master/longnum.cpp