长整数的乘法运算

概述

都知道, 计算机中存储整数是存在着位数限制的, 所以如果需要计算100位的数字相乘, 因为编程本身是不支持存储这么大数字的, 所以就需要自己实现, 当然了, 各个编程语言都有大数的工具包, 何必重复造轮子, 但我还是忍不住好奇他们是如何实现的, 虽然最终没有翻到他们的底层源码去, 但查询的路上还是让我大吃一惊, 来吧, 跟我一起颠覆你的小学数学.

长乘运算

当然, 如果自己实现这样一个大数, 用数组来存储每一位是我当前想到的方法. 那如何进行乘法运算呢? 因为用数组来存储数字, 那么数字的加法也要采用每一位进位的方式来进行, 所以下面为了方便说明算法的效率, 以一次个位数的运算视为一个运算单位.

上小学知识:

  • 4∗5=204*5=2045=20
    • 个位数相乘, 一次运算
  • 14∗5=(4∗5)+(1∗5)∗10=7014*5=(4*5)+(1*5)*10=70145=(45)+(15)10=70
    • 2位数乘1位数, 分解后共: 2次乘法和2位数的加法, 4次运算(乘10可看做移位操作)
  • 134∗6=(4∗6)+(3∗6)∗10+(1∗6)∗100=804134*6=(4*6)+(3*6)*10+(1*6)*100=8041346=(46)+(36)10+(16)100=804
    • 3位数乘1位数, 分解后共: 3次乘法, 3位数的加法(不要看两个加号, 可以乘法运算完后做连加运算, 当然, 也可能连加之后发生溢出, 暂不考虑. 此处简化只看加法的位数即可), 6次运算.
  • 1234∗7=(4∗7)+(3∗7)∗10+(2∗7)∗100+(1∗7)∗1000=86381234*7 = (4*7) + (3*7)*10 + (2*7)*100 + (1*7)*1000 = 863812347=(47)+(37)10+(27)100+(17)1000=8638
    • 4位数乘1位数, 8次运算.

通过上面可总结规律, n位数乘一位数, 需要 2n 次运算. 将 n 位数乘1位数的运算称作短乘. 然后下面再看一下 n 位数乘 n 位数.

  • 14∗13=(14∗3)+(14∗1)∗10=18214*13=(14*3) + (14*1)*10=1821413=(143)+(141)10=182
    • 两位数相乘, 2次短乘, 4位数加法(99*9*10 最差情况). 共: 2∗(2n)+4=122*(2n) + 4 = 122(2n)+4=12 次运算
  • 132∗256=(132∗6)+(132∗5)∗10+(132∗2)∗100=33792132*256=(132*6)+(132*5)*10+(132*2)*100=33792132256=(1326)+(1325)10+(1322)100=33792
    • 三位数相乘: 3次短乘, 6位数加法(最差情况), 共: 3∗(2n)+6=243*(2n) + 6=243(2n)+6=24次运算.

通过上面, 总结规律, n位数相乘(长乘)的运算次数是: n∗(2n)+2n=2n2+2nn*(2n) + 2n = 2n^2+2nn(2n)+2n=2n2+2n 次运算. 当然, 这就是我们从小接受的进行乘法运算的方法, 所以写成这样还好, 比较合乎常理. 时间复杂度是 O(n^2)

但是, 他还可以更快么? 我以为就这样了, 是我小看了伟大的数学家. .

Karatsuba方法

由简入难, 先看一下两位数的乘法:

12*34, 为了方便初中方程未知数的思维, 我们将这两个数字拆解一下:

KaTeX parse error: No such environment: align* at position 8: \begin{̲a̲l̲i̲g̲n̲*̲}̲ 12 &= 10a+b (其…

则,

KaTeX parse error: No such environment: align* at position 8: \begin{̲a̲l̲i̲g̲n̲*̲}̲ & 12*34 \\ =& …

当化简到这里, 2位数相乘需要几次运算? 来算一下:

  • 10(am+bn)10(am + bn)10(am+bn) : 共2次乘法, 2位数加法, 共4次运算.
  • an 和 bm : 共2次乘法, 共2次运算
  • 剩下最外层的加法, 最差情况: (100∗9∗9100*9*910099 4位数, 10∗(9∗9+9∗9)10*(9*9 + 9*9)10(99+99) 4位数), 共4次运算
  • 则总计, 4+4+2=104+4+2=104+4+2=10次运算.

此时, 需要的运算次数已经较之前的12次少一些了, 但是别急, 容我把公式再变换一下.

令:

u=anw=bms=(b−a)∗(m−n)u=an \\ w=bm \\ s=(b-a)*(m-n) u=anw=bms=(ba)(mn)

公式:

100u+(u+w−s)∗10+w=100an+(an+bm−(b−a)∗(m−n))∗10+bm=100an+(an+bm−bm+bn+am−an)∗10+bm=100an+(bn+am)∗10+bm100u + (u+w-s)*10+w \\ = 100an + (an + bm - (b-a) * (m-n)) *10 + bm \\ = 100an + (an + bm - bm + bn + am - an)*10 + bm \\ = 100an + (bn + am) * 10 + bm 100u+(u+ws)10+w=100an+(an+bm(ba)(mn))10+bm=100an+(an+bmbm+bn+aman)10+bm=100an+(bn+am)10+bm

是不是和上面的公式一样了呢? 是的, 那转换公式是为了什么呢? 当然是为了减少运算次数啦. 算一下:

  • 计算u : 1次运算
  • 计算w: 1次运算
  • 计算 s: 3次运算
  • 计算 u+w-s: 2位数运算, 2次运算
  • 计算最外层加法: 3位数运算, 3次运算
  • 共: 10次运算.

这和我刚才计算的不也是10次么? 不过个位数的乘法换成加法就会变快了么? 不要小看这个一次乘法运算的减少, 从上面能够看出, 乘法运算的运算次数是随位数成指数增长的, 而加法运算则随位数成线性增长, 等看了下面的多位数相乘, 你就知道减少的这一次乘法运算有什么用了.

不过下面才是重头戏, 数字多了之后, 此算法就明显比传统的快的多了.

4位数乘法

计算: 1234∗56781234*567812345678

设:

1234=100a+b(其中a=12,b=34)5678=100n+m(其中n=56,m=78)1234+5678=(100a+b)∗(100n+m)1234=100a+b (其中 a=12, b=34) \\ 5678=100n+m (其中 n=56, m=78) \\ 1234+5678 = (100a + b) * (100n + m) 1234=100a+b(a=12,b=34)5678=100n+m(n=56,m=78)1234+5678=(100a+b)(100n+m)

套用上面的公式:

令:

u=anw=bms=(b−a)∗(m−n)u=an \\ w=bm \\ s=(b-a)*(m-n) u=anw=bms=(ba)(mn)

则结果为: 10000u+(u+w−s)∗100+w10000u + (u+w-s)*100+w10000u+(u+ws)100+w

此次进行了几次运算呢? 算一下:

  • 计算 u: 两位数乘法, 10次运算
  • 计算w: 10次运算
  • 计算s: 两位数减法两次, 一次乘法, 14次运算
  • 计算整体: 8位数相加(99∗99∗1000099*99*10000999910000), 8次运算
  • 整体: 10+10+14+8=3210+10+14+8=3210+10+14+8=32次运算.

32次运算, 之前长乘的方式需要几次呢? 2∗(4∗4)+2∗4=402*(4*4) + 2*4=402(44)+24=40. 是不是少了.

也就是说, 4位数的乘法, 其中用到了3次两位数乘法, 2次两位数减法, 1次8位数加法.

8位数乘法

8位数乘法就不展开了, 直接套用4位数乘法得出的结论, 其运算次数为:

  • 3次4位数乘法: 3∗32=963*32=96332=96
  • 2次4位数减法: 2∗4=82*4=824=8
  • 1次 9999∗9999∗1000000009999*9999*10000000099999999100000000 位数加法: 17次
  • 共: 96+8+17=12196+8+17=12196+8+17=121次运算.

原来的长乘需要几次呢? 2∗(8∗8)+2∗8=1442*(8*8) + 2*8=1442(88)+28=144次.

是不是有一种动态规划, 分而治之的感觉? 可以利用函数递归来实现.

问题

想必此算法的问题也很明显了, 为了每次都能将数字拆成左右两部分, 所以只能够计算位数是2的 n 次方的数字, 如果位数不足, 则需要在前边进行补0.

算法比较

为了比较两个算法的运算次数, 让我们忽略运算的低次幂以及常数项, 则(以下 n 为2的幂):

长乘

f(n)={1,n== 12∗(2n)2,elsef(n) = \begin{cases} 1, \text{ $n$ == 1} \\ 2 * (2^n)^2, \text{else} \end{cases} f(n)={1, n == 12(2n)2,else

Karatsuba:

f(n)={3,n==13∗f(n−1),elsef(n) = \begin{cases} 3, \text{$n$==1} \\ 3*f(n-1), \text{else} \end{cases} f(n)={3,n==13f(n1),else

分别进行计算:

2的幂/数字位数长乘Karatsuba
20=12^0=120=111
21=22^1 = 221=283
210=10242^{10}=1024210=10242097152
220=10485762^{20}=1048576220=104857621990232555521162261467
250=11258999068426242^{50}=1125899906842624250=11258999068426242535301200456458802993406410752239299329230617529590083
2100=12676506002282294014967032053762^{100}=12676506002282294014967032053762100=12676506002282294014967032053763213876088517980551083924184682325205044405987565585670602752171792506910670443678820376588540424234035840667

可以看出来, 当数字的位数越大, 则两个算法之间的差距越明显.


有没有被颠覆的感觉? 是不是自己知道了20多年的乘法运算, 根本没有想到还有其他计算乘法的运算规则? 我也没想到, 涨见识了…

果然, 没有什么是伟大的科学家们做不到的, 这算法我看了近乎整整一天, 草稿纸废了四十张, 总算是略知一二了.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/508416.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何将数字转换成口语中的文本串

概述 今天突发奇想, 写一个将数字转换成中文字符串的函数. 并不是将 1234 转成 1234 , 而是将 1234 转成 一千二百三十四. 本来以为很简单, 写下来之后发现还是有些坑的. 尝试 因为我是在写完最终版本, 回过头来整理的这篇文章, 所以中间很多尝试的步骤会有所遗漏. 以下简单…

设计模式故事会

想着用讲故事的方式对各个设计模式进行一个稍微形象一些的描述. 当然, 某些因为没有想到特别好的例子, 可能比喻的并不是那么的恰当, 望赐教. 绞尽脑汁啊, 尽力贴近设计模式其本来的意图. 主要为了加强一些自己对设计模式的理解. 进入正题: 小王是一名心怀梦想的90后青年, 他独…

有了 elseif 为什么还要 switch case

引出 你有没有想过既然有了if elseif, 为什么还要设计一个switch case的语法出来呢? 按理说, 一个语言的设计角度来说, 关键词越少越好吧, 而且多出来一种选择分支也没有看出太大用处. 以下几种switch case均可以写成if else的形式(java 代码): // 形式一 switch(a){case 1:…

计算矩阵中全1子矩阵的个数

前言 最近被我大哥安利了一道算法题, 这道题说难, 还不至于我做不出来, 说简单吧, 我还想不到最优解, 等把最优解告诉我之后, 我还正好能理解. 我甚至曾经怯怯的认为, 这题就是我哥专门给我找的, 嘿嘿, 心中说不出的小欢喜. 题来了, 此题出自力扣, 原题链接: https://leetco…

GO 的方法集

前言 之前在写 GOdemo 的时候, 写了这么一段程序(大概意思): package maintype Test struct { }func (test *Test) print() {println("test fun") }func main() {Test{}.print() }结果一编译就报错了: cannot call pointer method on Test literal 差不多意思是不…

GO 切片实力踩坑

概述 GO 语言的切片这两天用了用, 可以支持切割数组的中间部分. 但今天使用中, 出了 bug, 查了半天, 发现是切片的问题, 简单写个 demo 复现当时的情况: package mainimport "fmt"func main() {a : []int{1, 2, 3, 4, 5}b : a[2:4]b[0] 9fmt.Println(a) }你以为输…

搭建本地 HTTPS 环境

前言 之前写自己的网站时, 申请过免费的https证书. 最近想在自己本地搭一个, 结果忘了当初证书是怎么来的了. 本来想着去申请个免费的证书, 但想了想, 我只需要在自己本地能使用就行了, 我自己的环境, 那当然是我说了算了. 只要能够将证书构造出来, 安装到本地就可以识别了. 搜…

《人人都是产品经理》读后感

之前读书, 一般看的都是技术、算法、数学、哲学这些方面的, 为什么突然想看这本书呢? 身为一个开发人员, 每天打交道最多的, 除了开发和测试, 就是产品了. 还记得我刚来公司的时候, 在和产品的沟通过程中, 听到最多的话估计就是: 你要懂产品, 你懂了就不会做错. 确实有很多需求…

nginx 端口转发

概述 这两天在写 go 项目, 一个 HTTP 服务器. 之前写的是 php 项目, nginx 监听80端口, 根据域名将请求分配给不同项目. 现在换了 go, 自然也想延续这个操作, 毕竟都是跑在同一台服务器上. 那么问题来了, 我的nginx 监听80端口的同时, go 服务器是无法同样监听80端口的. 这该如…

2020-08-20

前段时间有些迷茫, 有时看着自己写的代码感觉就像一坨屎, 有时写了一段逻辑总是感觉有问题, 测了一遍又一遍, 逻辑也要理好几次. 就像是进入了一种不可描述的状态, 这感觉让我很不自在. 敲代码的时间说长不长, 说短不短, 从大二开始, 到现在也小四年了, 还没有碰到过这种情况. …

beego 优雅重启

前言 最近在写 go 的项目, http 用的 beego 框架. 因为 go 不想 php, 每次代码改动都需要重启服务, 所以代码发上线之后, 如何重启服务就成了一个问题. 如果强行重启的话, 不光在重启期间的所有访问都被拒绝了, 而且在杀掉进程的时候处理中的请求也挂了. 对于一个向用户正常提…

由 go orm 引发的探索

前言 今天遇到了一个 bug, 是 golang 的orm导致的. 使用了gorm框架. 通过实现Scan与Value可以将数据库中的 json 内容解析出来, 免除了 字符串再解码的步骤. 当时报错的代码大概是这样的: type TestContent struct {Id intContent Content // 数据库中的 json 结构 }type Con…

MySQL 数据恢复

MySQL 数据恢复 前言 前两天因为没注意的误操作, 直接把某个数据表清掉了, 心慌慌. 怪自己学艺不精, 当时整了一下午也没把数据找回来. 当晚回来闭关研究, 终于在凌晨1点多整出来了, 特此记录, 以备不时之需. 对于 MySQL 数据的备份, 主要有两种: 全量备份和增量备份. 全量…

码云 Pages 搭建

因为一直在写博客, 就向着搭个 GithubPages 来展示, 一直都听说别人用它来搭建个人博客, 但一直停留在听说的阶段. 最近想着没事搞一搞, 也看看它到底是个什么东东. 不过咱一个写中文博客的, 就想着在码云上搭一个, 顺便还能被百度收录, 嘿嘿. 说干就干, 开搞. (Pages 服务只能…

crontab 指令笔记

前言 之前有个定时任务, 需要每天的9点和21点定时跑. 当时我是这么写的: 0 9 * * * XXX 0 21 * * * XXX当时经过高人指点, 发现这两条命令可以写成一条: 0 9,21 * * * XXX好羞耻… 于是, 我决定整理一篇crontab的语法. 防止下次再出现这尴尬的场景… 指令 命令行 crontab…

golang 多协程的同步方法总结

之前用 go 写一个小工具的时候, 用到了多个协程之间的通信, 当时随手查了查, 结果查出来一大坨, 简单记录一下. golang中多个协程之间是如何进行通信及数据同步的嘞. 共享变量 一个最简单, 最容易想到的, 就是通过全局变量的方式, 多个协程读写同一个变量. 但对同一个变量的更…

golang chan 探究

前言 之前在看golang多线程通信的时候, 看到了go 的管道. 当时就觉得这玩意很神奇, 因为之前接触过的不管是php, java, Python, js, c等等, 都没有这玩意, 第一次见面, 难免勾起我的好奇心. 所以就想着看一看它具体是什么东西. 很明显, 管道是go实现在语言层面的功能, 所以我以…

计算机网络-信道复用技术

还记得计算机网络中的信道复用技术么? 来来来, 一起复习一下. why 问: 什么是信道复用. 在回答这个问题之前先看这样一个场景: 其中u1 u2是两个用户, 如果这两个用户之间连通的信道在他们使用过程中, 被他们完全占用了, 其他人就只能等着了. 那有人说了, 那就多架设信道不就…

IP 数据报首部分析

来来来, 爷们. 不是一直说纸上得来终觉浅么. 今咱就抓个数据报具体看一看真实网络中的 IP 报首部. 操作方法很简单, 使用wireshark进行抓包. 抓包后随便找个包看一下就行, 毕竟所有通信的包都需要经过网络层.(同时, wireshark会对协议的相关信息给出标识, 更方便我们查看) 其中…

git 子模块在项目中的使用

在公司的项目中, 经常会遇到一些公共的内容, 多个项目中间通用的, 不可能每次都将整个代码复制一遍, 遇到这种情况有很多不同的解决方案, 一般来说, 项目是通过 git 来管理的, 巧了, git 也同样支持子模块. 创建子模块 git submodule add gitgitee.com:hujingnb/submodule_so…