大数乘法——学会问题分解,一切迎刃而解

经典问题大数乘法

给两个字符串格式的十进制数字,求这两个数的乘积,以字符串格式返回。
leetcode问题链接
本篇教你看一遍永远忘不了的大数乘法解法,以及如何将运行时间优化到0ms

思路

既然是大数,无法放到整数类型的变量中,这时我们的小学知识终于派上了用场!还记得怎么笔算乘法吗?如果不记得,好好回忆一下整个步骤。


乘法笔算

我们要做的,就是把这种笔算方法转成代码,就可以解决这个问题了,听上去一点也不难吧?

为什么面试做不出来

然而在面试过程中,很多人都做不出来这道题,这是为什么?
其实思路大家都想得到,只是这看似简单的笔算,其实包含了很多步骤。如果没有把他们理清楚,一步一步地去实现,就很容易把自己绕晕。

步骤拆解

第一步:格式转换

算法的核心是把多位数的乘法转化成很多一位数的乘法,然后加起来。于是我们需要先吧字符串的每一位转成数字,方便计算乘法。
例如:字符串 "123456789" 要转成整数列表 [1,2,3,4,5,6,7,8,9]

第二步:计算大数与一位数的乘积

对应上图笔算乘法,就是要计算 145 * 2 = 290,还有145 * 1 = 145 这两个步骤。
只要按照从低位到高位的顺序去乘就好了,注意处理进位。

第三步:计算大数加法

对应上图笔算中把290与145相加的步骤。
没错,要做大数乘法,首先得会做大数加法。其实很简单,只要从低位到高位一位一位地相加,注意进位就可以了。

第四步:格式转换

前面计算过程中全部用的是整数列表的格式,计算完成后再把格式转成字符串就可以了。

开始撸代码吧,记得按步骤来

下面代码使用go语言

1. 格式转换

这一步没什么好说的

func convertStringToIntDigitList(stringNum string) []int64 {
    digitList := make([]int64, 0)
    for i:=0; i<len(stringNum); i++ {
        digitList = append(digitList, int64(stringNum[i]-'0'))
    }
    return digitList
}

2. 计算大数与一位数的乘积

这一步要注意的是:以及处理好进位,包括最高位的进位。

const base=10
func multiplyDigitListByDigit(digitList []int64, digit int64) []int64 {
    if digit == 0 { // 处理边界情况
        return make([]int64, 0)
    }
    var c int64
    product := make([]int64, len(digitList))
    for i := len(digitList) - 1; i>=0; i-- {
        p := digitList[i] * digit + c // 加上低位的进位
        c = p / base // 进位
        product[i] = p % base
    }
    if c > 0 {
        // 如果还有进位,在最高位加一位
        product = append([]int64{c}, product...)
    }
    return product
}

3. 计算大数加法

const base=10
func addTwoDigitLists(digitList1, digitList2 []int64) []int64 {
    sum := make([]int64, 0)
    var c int64
    for i := 0; i<len(digitList1) || i < len(digitList2); i++ {
        var d1, d2 int64
        if i < len(digitList1) {
            d1 = digitList1[len(digitList1)-i-1]
        }
        if i < len(digitList2) {
            d2 = digitList2[len(digitList2)-i-1]
        }
        s := d1+d2+c
        sum = append([]int64{s%base}, sum...)
        c = s / base
    }
    if c > 0 {
        sum = append([]int64{c}, sum...)
    }
    return sum
}

4. 格式转换回字符串

func convertDigitListToString(digitList []int64) string {
    numStr := ""
    for i:=0; i<len(digitList); i++ {
        numStr += string(byte(digitList[i] + '0'))
    }
    // 注意处理前导0和结果为0的情况
    numStr = strings.TrimLeft(numStr, "0")
    if numStr == "" {
        return "0"
    }
    return numStr
}

5. 最后,将这些步骤整合

func multiply(numStr1, numStr2 string) string {
    num1 := convertStringToIntDigitList(numStr1)
    num2 := convertStringToIntDigitList(numStr2)
    products := make([][]int64, 0)
    for i:=0; i<len(digitList2); i++ {
        prod := multiplyDigitListByDigit(digitList1, digitList2[len(digitList2)-1-i])
        product := append(prod, make([]int64, i)...) // 末尾补0
        products = append(products, product)
    }
    result := make([]int64, 0)
    for _, product := range products {
        result = addTwoDigitLists(product, result)
    }
    return convertDigitListToString(result)
}

总结

虽然代码行数看起来也不少,但步骤清晰,其中每一步都很简单,看过一遍之后,你还会忘记吗?这就是问题分解的魅力,大题化小,小题化了,一个问题分解成多个很简单的小问题。
当然,我们也可以将这些函数全部合并到同一个函数里面,如果这样做,不仅代码行数减少了,而且代码中会少很多内存分配的步骤,导致内存占用和运行时间都会减少,但代价是更不容易记忆,这当然算追求极致,但对于以面试为目的同学,记得牢才更重要。

进阶,如何优化运行时间?

上面介绍的算法,便于记忆且写法相对简单,但性能还有很大的提升空间。我们代码中用int64来存储数字,但只用来进行一位数的计算。如果能一次计算多位,就能减少计算次数。
可能有人已经注意到,我在代码中定义了一个base的常量为10,代表每个int64只存储一位数。如果将base改成10000,每个int64就可以存储四位数。但同时,两个convert函数也需要修改,四位四位地转换。

优化后的格式转化函数

func convertStringToIntDigitList(stringNum string) []int64 {
    digitList := make([]int64, 0)
    for i:=len(stringNum)-1; i>=0; i-=4 {
        a := 0
        if i >= 4 {
            a = i-4+1
        }
        s := stringNum[a:i+1]
        d, _ := strconv.ParseInt(s, 10, 64)
        digitList = append([]int64{d}, digitList...)
    }
    return digitList
}

func convertDigitListToString(digitList []int64) string {
    numStr := ""
    for i:=0; i<len(digitList); i++ {
        numStr += fmt.Sprintf("%04d", digitList[i]) // 如果不够4位,需要在前面补0
    }
    strings.TrimLeft(numStr, "0")
    if numStr == "" {
        return "0"
    }
    return numStr
}

到底能减少多少时间?

这样依赖,我们可以减少多少次计算呢?
假设两个大数分别有m位和n位,按照原来的算法需要计算m * n 次乘法,以及n次加法,而新的算法只需要(m/4) * (n/4)次乘法和(n/4)次加法,如果只看乘法,计算次数只有原来的1/16!
同时,存储所需的空间也会减少。原来的算法需要为大数分配(m+n)个int64空间,新的算法只需要四分之一。

优化的极限

按照这种思路,最多一组几位数呢?那就要看int64能装下多少了。众所周知,int64能表示的最大整数是2^63-1也就是9223372036854775807,十进制是19位数。但需要注意的是,在我们的算法中会出现两个int64相乘的计算,也就是我们最多只能存9位数,这样两个9位数相乘最多得到18位数,不会超出int64的范围。
于是我们把base改成1e9(10的9次方),两个convert函数也做相应的修改,就可以得到打败100%网友的代码了。


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容