大数的简单实现-程序员宅基地

技术标签: java  

Table of Contents

  1. 前言
  2. 字符数组的本质
  3. 整数数组与 1000000000 进制
  4. 小端模式存储
  5. 和 10 进制字符串之间的转换
  6. 大数加法
  7. 大数乘法
  8. 结语
  9. 参考链接

前言

大数的实现应该是很多人在初学编程不久后就会遇到的一个问题,常见的问题就是 大数加法 的实现,
更进一步便是 大数乘法.

初学时解决这两个问题的一般思路就是通过 字符数组 来表示一个大数,然后通过模拟人工的竖式计算来实现相关运算。

我们可以基于这种思路简单扩展一下大数的实现方式。

字符数组的本质

通过字符数组表示大数的本质是什么,比如像这样一个字符数组:

['1', '2', '3', '4', '5', '6', '7', '8', '9', '8', '7', '6', '5', '4', '3', '2', '1']

它表示大数 12345678987654321, 它是通过怎样的形式来表示的呢?

答:通过 10 进制数组 的形式来表示的,数组中每个元素的表示范围为 [0, 9].

但是很明显,就算是在 C 语言中,存储一个字符也至少需要一个字节,而一个字节能够表示 [0, 255], 我们却只拿来存储 [0, 9] 的数字,这显得有点奢侈。

而进制这个东西,是可以在计算过程中进行转换的,那么,我们是不是可以让一个字节多存储一点数据,比如说 [0, 99] 之类的?

那么更一般的,我们必须用 字符数组 吗?如果用 整数数组 又是怎样的?

整数数组与 1000000000 进制

通过前面的思考,我们可以发现,通过 字符数组 来实现大数的方式其实就是:

  • 通过一个 N 进制数组 来表示一个大数,然后通过模拟人工的竖式计算来实现相关的大数运算

那么,我们只需要通过一点简单的进制转换,就可以使用 整数数组 来替代 字符数组.

这里我们可以选择 1000000000 进制整数数组 来实现大数,这样选择的理由:

  • 10 进制N 次方可以很方便的实现和十进制之间的转换,而 1000000000 是一个整数能够表示的最大的 10 的 N 次方
  • 整数数组 的空间利用率比 字符数组 更高,同时用四个字节来表示大数:

    数组类型 进制 最大大数 - 数组形式 最大大数
    字符数组 100 [99, 99, 99, 99] 99999999
    整数数组 1000000000 [999999999] 999999999

    可以看到,即使字符数组使用 100 进制来表示大数,整数数组能够表示的最大大数也是字符数组的 10 倍。

因此,整数数组和 1000000000 是一个不错的选择。

小端模式存储

我们选择了整数数组和 1000000000 进制实现大数,其中,整数数组的每个元素最多存储 9 位十进制数字:

private final static int BASE = 1000000000;
private final static int BASE_DECIMAL_DIGITS = 9;

然后需要考虑的就是大数的存储模式,和计算机内存中数据的存储一样,这里我们有 大端小端 两种存储模式可以选择,其中,大端形式可以方便人的阅读,而小端可以方便相关大数计算的实现。

阅读方面可以选择通过直接将大数打印为十进制字符串,因此,这里选择小端的存储模式:

/**
 * Create large numbers based on small endian integer arrays
 *
 * Array [999999999, 1] representing large numbers 1999999999
 *
 * NOTE: The range of values ​​for each element of the array is [0, 999999999]
 */
public BigInteger(int... digits) {
  if (digits.length > 0) {
    for (int digit : digits) {
      if (digit < 0 || digit >= BASE) {
        throw new IllegalArgumentException(String.format("Digit %d out of range !", digit));
      }
    }
    this.digits = digits.clone();
  } else {
    this.digits = new int[] {0};
  }
}

和 10 进制字符串之间的转换

将 10 进制字符串转换为 1000000000 进制的整数数组时,只需要按 9 个数字一组的方式分割原字符串就可以了:

/**
 * Create large numbers based on decimal strings
 */
public BigInteger(String digitsString) {
  int stringLength = digitsString.length();
  // Array size required to store large numbers, equal ceil(digitsLength / BASE_DECIMAL_DIGITS)
  int digitsLength = (stringLength - 1) / BASE_DECIMAL_DIGITS + 1;
  // Length of the large number of heads
  int head = stringLength % BASE_DECIMAL_DIGITS;

  this.digits = new int[digitsLength];
  for (int i = 0; i < digitsLength; ++i) {
    String block = digitsString.substring(Math.max(head + (i - 1) * BASE_DECIMAL_DIGITS, 0), head + i * BASE_DECIMAL_DIGITS);
    this.digits[digitsLength - i - 1] = Integer.parseInt(block);
  }
}

而将 1000000000 进制的整数数组转换为 10 进制字符串也可以通过格式化字符串简单实现:

public class BigInteger {
  public String toString() {
    Formatter formatter = new Formatter();

    formatter.format("%d", digits[digits.length - 1]);
    for (int i = digits.length - 2; i >= 0; --i) {
      formatter.format("%09d", digits[i]);
    }

    return formatter.toString();
  }

效果:

>>> import BigInteger
>>> BigInteger("1234684654687654354896735454")
1234684654687654354896735454

注: 这里的测试是通过 Jython 完成的,用 Jython 来进行简单的 Java 测试是一个很不错的选择

大数加法

整数数组的大数加法和字符数组的大数加法在实现上是差不多的,所有就直接上代码好了:

public BigInteger plus(BigInteger other) {
  int[] result = new int[Math.max(this.digits.length, other.digits.length) + 1];
  System.arraycopy(this.digits, 0, result, 0, this.digits.length);

  int carry = 0;
  for (int i = 0; i < other.digits.length; ++i) {
    int sum = carry + result[i] + other.digits[i];
    result[i] = sum % BASE;
    carry = sum / BASE;
  }

  if (carry != 0) {
    for (int i = other.digits.length; i < result.length && carry != 0; ++i) {
      int sum = carry + result[i];
      result[i] = sum % BASE;
      carry = sum / BASE;
    }
  }

  if (result[result.length - 1] == 0) {
    result = Arrays.copyOfRange(result, 0, result.length - 1);
  }

  return new BigInteger(result);
}

测试:

>>> a = BigInteger("999999999999999999999999999999999999999999")
>>> b = BigInteger("111111111111111111111111111111111111111111")
>>> a.plus(b)
1111111111111111111111111111111111111111110

大数乘法

大数乘法我们可以借助 long 数组来辅助实现,因为,这样就不需要担心两个 int 相乘的溢出问题了,这也是为什么不选择 long 数组来实现大数的一个原因。

public BigInteger mul(BigInteger other) {
  long[] temp = new long[this.digits.length + other.digits.length];

  for (int i = 0; i < this.digits.length; ++i) {
    for (int j = 0; j < other.digits.length; ++j) {
      temp[i + j] += (long) this.digits[i] * (long) other.digits[j];
    }
  }

  for (int i = 0; i < temp.length; ++i) {
    if (temp[i] >= BASE) {
      temp[i + 1] += temp[i] / BASE;
      temp[i] = temp[i] % BASE;
    }
  }

  int zeroCount = 0;
  for (int i = temp.length - 1; i >= 0; --i) {
    if (temp[i] > 0) {
      break;
    }
    zeroCount++;
  }

  int[] result = new int[temp.length - zeroCount];
  for (int i = 0; i < result.length; ++i) {
    result[i] = (int) temp[i];
  }

  return new BigInteger(result);
}

测试:

>>> a = BigInteger("999999999999999999999999999999999999999999")
>>> b = BigInteger("111111111111111111111111111111111111111111")
>>> a.mul(b)
111111111111111111111111111111111111111110888888888888888888888888888888888888888889

结语

这里的大数实现是很不完善的,本来是想尝试用 2 进制流来实现,但是尝试后才发现,有点麻烦,于是就放弃了。

但是,如果真的用 2 进制流来实现的话,其实也就只是相当于 0xff 进制的字符数组或 0xffffffff 进制的整数数组,主要是和十进制之间的转换有点麻烦。

而有些操作是需要用 2 进制流来实现才好完成的,比如大数的位运算。

这里的实现还没有涉及大数减法和大数除法,有兴趣的可以去尝试一下。

完整代码链接:BigInteger.java

参考链接

转载于:https://www.cnblogs.com/rgbit/p/10357593.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30359021/article/details/96782811

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签