最近在跟coursera上斯坦福大学的算法专项课,其中开篇提到了两个整数相乘的问题,其中最简单的方法就是模拟我们小学的整数乘法,可想而知这不是比较好的算法,这门课可以说非常棒,带领我们不断探索更优的算法,然后介绍可以通过使用分而治之的思想来解决这个问题。下面对该问题的方法以及实现进行介绍。

问题定义

输入:2个n位的整数x和y
输出:x * y

如求: 1234567891011121314151617181*2019181716151413121110987654 的结果

求解该问题要注意的是由于整数的位数可能超过基本类型的表示范围,所以一种方式是将其转化为字符串进行表示,另一种方式是可以使用一些语言自带的大整数类型(如Java的BigInteger)。参考一些资料才发现,该问题的解法其实有很多种,主要列举以下:

1.模拟小学乘法: 竖式乘法累加。

2.分治乘法: 最简单的是Karatsuba乘法,一般化以后有Toom-Cook乘法.

3.快速傅里叶变换

4.中国剩余定理

我们主要介绍模拟乘法累加以及使用分治思想的Karatsuba乘法,最后使用Java进行实现。

模拟小学乘法

      7 8 9 6 5 2
× 3 2 1 1
-----------------
7 8 9 6 5 2 <---- 第1趟
7 8 9 6 5 2 <---- 第2趟
.......... <---- 第n趟
-----------------
? ? ? ? ? ? ? ? <---- 最后的值用另一个数组表示

如上所示,需要将乘数与被乘数逐位相乘,最后再进行累加,时间复杂度为{O(n^2)}.模拟乘法累加还有一个改进版,上述方法在实现时,每次计算乘法都需要考虑进位,最后在加法时也需要进位,比较麻烦。一种改进的版本如下:

        9  8
× 2 1
-------------
(9)(8) <---- 第1趟: 98×1的每一位结果
(18)(16) <---- 第2趟: 98×2的每一位结果
-------------
(18)(25)(8) <---- 这里就是相对位的和,还没有累加进位

改进的方法先不算任何的进位,也就是说,将每一位相乘,相加的结果保存到同一个位置,到最后才计算进位。我们可以先将结果保存到一个数组中(不考虑进位),最后对数组从右向左进行遍历,大于10进行进位。Java实现如下:

public class BigNumMul {
//simple method 模拟乘法累加
public static String bigNumberMul(String num1, String num2) {
// 分配一个空间,用来存储运算的结果,num1长的数 * num2长的数,结果不会超过num1+num2长
int[] res = new int[num1.length() + num2.length()];
// 先不考虑进位问题,根据竖式的乘法运算,num1的第i位与num2的第j位相乘,结果应该存放在结果的第i+j位上
for (int i=0; i<num1.length(); i++) {
int a = num1.charAt(i) - '0';
for (int j=0; j<num2.length(); j++) {
int b = num2.charAt(j) - '0';
res[i+j] += a * b; //max: num1.length()+num2.length()-2
}
}
StringBuilder sb = new StringBuilder();
//单独处理进位
int carry = 0;
//最多就到res.length-2, 最后一个元素没有被占用,还是初始值0
for (int k=res.length-2; k >= 0; k--) {
int digit = (res[k] + carry) % 10;
carry = (res[k] + carry) / 10;
sb.insert(0, digit);
}
if (carry > 0) {
sb.insert(0, carry);
} String str = sb.toString().replaceFirst("^0*", ""); return str.substring(0,str.length()-1);
}
}

显然使用O(n^2)的算法是不够好的,我们应该想一下有没有更好的算法,就像这门课上经常说的一句:Can we do better ?

分治:Karatsuba算法

分治算法的主要思想是能将问题分解为输入规模更小的子问题,然后递归求解子问题,最后将子问题的结果合并得到原问题的结果,最典型的如归并排序算法。为了得到规模更小的子问题,就要将较大的整数拆分为位数较少的两个整数,参考coursera上的算法专项课,主要计算过程如下:

如上图所示,将每个数分别拆分为两部分,分别计算ac, bd, 以及(a+b)(c+d),最后再减去前面两个,将其组合成最终的结果。我们采用更一般的方式将其表达出来,相应计算方法如下:

上述给出了更通用的写法,将x和y同分解后的更小的整数进行表示,最后通过递归的计算ac, ad, bc, bd就可以得到x*y的结果。上述是没有优化过的分治算法,每次递归需要4次乘法,合并结果需要O(n)时间复杂度,所以可以得到时间复杂度的表示:

{T(n) = 4T(n/2) + O(n)}

通过主方法,可以求得上述时间复杂度为O(n^2),并没有得到好的改善。

Karatsuba算法将上述的4次乘法优化为3次从而减少了时间复杂度。具体过程如下:

可以看到上述利用(a+b)(c+d)的结果减去ac和bd得到ad+bc的结果,从而只需要计算三次乘法,其时间复杂度可以表示为:

T(n)=3T(n/2)+6n=O(n^{log_{2}3})

根据上述算法,使用Java进行实现代码如下:

    //Karatsuba乘法
//此种情况使用long时,数过大可能出现越界,应考虑使用BigInteger
public static long karatsuba(long num1, long num2) {
//递归终止条件
if (num1 < 10 || num2 < 10) {
return num1 * num2;
} // 计算拆分长度
int size1 = String.valueOf(num1).length();
int size2 = String.valueOf(num2).length();
int halfN = Math.max(size1, size2) / 2; /* 拆分为a, b, c, d */
long a = Long.valueOf(String.valueOf(num1).substring(0, size1-halfN));
long b = Long.valueOf(String.valueOf(num1).substring(size1-halfN));
long c = Long.valueOf(String.valueOf(num2).substring(0, size2-halfN));
long d = Long.valueOf(String.valueOf(num2).substring(size2-halfN)); // 计算z2, z0, z1, 此处的乘法使用递归
long z1 = karatsuba(a, c);
long z2 = karatsuba(b, d);
long z3 = karatsuba((a + b), (c + d)) - z1 - z2; return (long)(z1 * Math.pow(10, 2*halfN) + z2 + z3 * Math.pow(10, halfN));
}

注意上述递归的终止条件以及如何表示a, b, c, d. 上述实现使用的是Java中的long类型,但是当整数变大时,使用long类型可能会发生溢出,这里可以使用String来模拟整数的加法及乘法,或者使用Java的BigInteger类型,其实BigInteger内部也是使用的String进行存储,我使用的是BigInteger类型,实现代码如下:

   //使用BigInteger的karatsuba算法
//注意BigInteger的运算没有操作符重载
//参考: coursera算法专项1
public static BigInteger karatsuba(BigInteger num1, BigInteger num2) {
if (num1.compareTo(BigInteger.valueOf(10)) < 0 || num2.compareTo(BigInteger.valueOf(10)) < 0) {
return num1.multiply(num2);
} int n = Math.max(num1.toString().length(), num2.toString().length());
int halfN = n / 2 + n % 2; //另一种划分方法 //返回num1 / halfN 和 num1 % halfN
BigInteger[] a_b = num1.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger a = a_b[0];
BigInteger b = a_b[1];
BigInteger[] c_d = num2.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger c = c_d[0];
BigInteger d = c_d[1]; BigInteger step1 = karatsuba(a, c);
BigInteger step2 = karatsuba(b, d);
BigInteger step3 = karatsuba(a.add(b), c.add(d));
BigInteger step4 = step3.subtract(step2).subtract(step1); //step3-step2-step1
BigInteger res = step1.multiply(BigInteger.valueOf(10).pow(2*halfN)).add(step2)
.add(step4.multiply(BigInteger.valueOf(10).pow(halfN))); return res;
}

最后的测试代码如下:

    public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String a = sc.next();
String b = sc.next(); // 开始计算
//String str = BigNumMul.bigNumberMul(a, b);
//long res = BigNumMul.karatsuba(Long.valueOf(a), Long.valueOf(b));
//String str = Long.toString(res);
BigInteger res = BigNumMul.karatsuba(new BigInteger(a), new BigInteger(b));
String str = res.toString();
System.out.println(a + " * " + b + " = " + str);
}

总结与感想

(1)对一个问题要深入调研和分析,多尝试不同的解决方法。

(2)可以多分析一些诸如此类的经典问题,还是比较有意思的。

参考资料:

1.coursera算法专项

2.大数乘法问题及其高效算法

3.https://stackoverflow.com/questions/17531042/karatsuba-algorithm-without-biginteger-usage

4.https://chenyvehtung.github.io/2017/03/02/about-multiplication.html

大整数相乘问题总结以及Java实现的更多相关文章

  1. 【老鸟学算法】大整数乘法——算法思想及java实现

    算法课有这么一节,专门介绍分治法的,上机实验课就是要代码实现大整数乘法.想当年比较混,没做出来,颇感遗憾,今天就把这债还了吧! 大整数乘法,就是乘法的两个乘数比较大,最后结果超过了整型甚至长整型的最大 ...

  2. 华为OJ机试题目:两个大整数相乘(纯C语言实现两个大整数相乘,两种方法实现大数相乘)

    题目描述: 输出两个不超过100位的大整数的乘积. 输入: 输入两个大整数,如1234567 123 输出: 输出乘积,如:151851741 样例输入: 1234567 123 样例输出: 1518 ...

  3. 大整数相乘的C实现

    //之前有个测试这个题没做完,现在把它做完,通过这个程序可以对乘法了解更深刻.分析:运用整数乘法,当然进制越高越好,考虑到乘法不要越界,故考虑进制底数N应该满 //足,N^2<2^32次方.所以 ...

  4. 算法笔记_034:大整数乘法(Java)

    目录 1 问题描述 2 解决方案 2.1 蛮力法   1 问题描述 计算两个大整数相乘的结果. 2 解决方案 2.1 蛮力法 package com.liuzhen.chapter5; import ...

  5. Coefficient Computation (大整数、Java解决)

    Coefficient Computation UVALive8265 题意:计算组合数C(n,k)的值并将值按给定的进制输出. 思路:Java大整数类硬上. PS:刚刚学完Java的大整数类,结果却 ...

  6. Java实现大整数乘法

    1 问题描述 计算两个大整数相乘的结果. 2 解决方案 2.1 蛮力法 package com.liuzhen.chapter5; import java.math.BigInteger; publi ...

  7. 大整数乘法python3实现

    因为python具有无限精度的int类型,所以用python实现大整数乘法是没意义的,可是思想是一样的.利用的规律是:第一个数的第i位和第二个数大第j位相乘,一定累加到结果的第i+j位上,这里是从0位 ...

  8. 剑指offer第12题打印从1到n位数以及大整数加法乘法

       字符和数字加减就是字符的ASCII码和数字直接加减. 方法一: 1)在字符串操作中给一个整形数字加(字符0)就是把它转化为字符,当然给一个字符减去(字符0)就可以把它转化为数字了:如果确实是最后 ...

  9. 【九度OJ】题目1190:大整数排序 解题报告

    [九度OJ]题目1190:大整数排序 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1190 题目描述: 对N个长度最长可达 ...

随机推荐

  1. spark集群搭建整理之解决亿级人群标签问题

    最近在做一个人群标签的项目,也就是根据客户的一些交易行为自动给客户打标签,而这些标签更有利于我们做商品推荐,目前打上标签的数据已达5亿+, 用户量大概1亿+,项目需求就是根据各种组合条件寻找标签和人群 ...

  2. NET 泛型,详细介绍

    今天的文章是因为再给一个朋友讲这个的时候随手记录下整理出来的.说白了就是把前辈们曾经给我吹过的我又吹了出去. 泛型:是C# FrameWork 2.0 时代 加入进来的,可以说对与Net开发人员来说泛 ...

  3. 全球第一免费开源ERP Odoo PM OKR项目管理操作指南

    概览 ​ Odoo项目允许你和整个项目团队一起管理项目, 与项目和任务成员中的任何一个人沟通. 它与包含可定制阶段的任务的项目一起工作.项目可以是内部的或客户导向的.任务是项目执行的一部分.你可以给这 ...

  4. HIVE扩展GIS函数

        按项目日益增长的gis数据量要求,需要在大数据集群中部署HIVE的扩展函数.     Apache Hive是一个建立在Hadoop架构之上的数据仓库.它能够提供数据的精炼,查询和分析.([引 ...

  5. 通过 mysqlbinlog 和 grep 命令定位binlog文件中指定操作

    1.binlog日志基本知识 MySQL的二进制日志binlog可以说是MySQL最重要的日志,它记录了所有的DDL和DML语句(除了数据查询语句select),以事件形式记录,还包含语句所执行的消耗 ...

  6. CDI服务

    前言 CDI(Contexts and Dependency Injection 上下文依赖注入),是JAVA官方提供的依赖注入实现,可用于Dynamic Web Module中,将依赖注入IOC/D ...

  7. mpvue小程序开发之 城市定位

    背景: 在进行小程序开发时,有一个定位城市的需求,下面就来讲讲怎么实现这个功能的吧 解决方案: 小程序的wx.getLocation()获得是经纬度并不包含地名,所以要通过经纬度用相应的地图转换出地名 ...

  8. PHP全栈学习笔记8

    面向对象的基本概念,面向对象编程,oop,面向对象,面向对象的分析,面向对象的设计,面向对象的编程,什么是类. 类,属性和方法,类,对象,面向对象编程的三大特点.特点,封装性,继承性,多态性. 封装性 ...

  9. unity协程coroutine浅析

    转载请标明出处:http://www.cnblogs.com/zblade/ 一.序言 在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下 ...

  10. 用python读文件如.c文件生成excel文件

    记录一下,如何实现的,代码如下: #!/usr/bin/env python # coding=utf-8 # 打开文件 import xlwt import re import sys bookfi ...