(huawei)43. 字符串相乘

字符串相乘(LeetCode经典题)------ 详细思路与C++实现

文章目录

题目

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

注意:不能使用任何内置的 BigInteger 库或直接将输入转换为整数。

示例 1:

输入: num1 = "2", num2 = "3"

输出: "6"

示例 2:

输入: num1 = "123", num2 = "456"

输出: "56088"

提示:

1 <= num1.length, num2.length <= 200

num1 和 num2 只能由数字组成。

num1 和 num2 都不包含任何前导零,除了数字0本身。

一、引言:为什么需要"字符串相乘"?

在日常编程中,我们计算两个整数相乘时,直接用intlong long即可,但如果遇到超大数字 (比如超过10^18,超出long long的范围),内置数据类型就会出现"溢出"问题。此时,我们需要用字符串来存储大数字,并通过模拟手工乘法的逻辑实现计算------这就是"字符串相乘"问题的核心场景。

该问题是LeetCode中的经典面试题(如LeetCode 43. 字符串相乘),不仅考察对字符串的处理能力,还能检验对"模拟算法"的理解,是初学者必须掌握的基础算法之一。本文将从思路拆解到代码实现,逐步讲解如何解决这个问题。

二、问题分析:明确输入与核心需求

1. 输入输出定义

  • 输入 :两个非负整数的字符串(如num1 = "123"num2 = "45"
  • 输出 :两个数乘积的字符串(如"5535"
  • 约束:不能直接将字符串转换为整数/长整数计算(否则会溢出,违背题目的核心考察点)。

2. 核心难点

  • 如何将"手工乘法"的步骤转化为代码逻辑?
  • 如何处理乘法过程中的"进位"和"数位对齐"?
  • 如何避免结果中多余的前导0(如计算"0" * "123"时,不能返回"0000",而要返回"0")?

三、算法原理:模拟手工乘法

我们先回忆一下手工计算两个数相乘 的步骤:

123 * 45 为例:

  1. 先算 123 * 5 = 615(个位相乘)
  2. 再算 123 * 4 = 492(十位相乘,结果需左移1位,即实际是4920
  3. 最后将两个结果相加:615 + 4920 = 5535

对应到字符串处理,我们可以用一个数组 存储每一位的乘积结果,再统一处理进位。关键结论,:

num1长度为mnum2长度为n,则它们的乘积长度最多为m + n (如999 * 999 = 998001,3位×3位=6位)。因此,我们可以初始化一个长度为m + n的数组res,用于存储每一位的临时结果。

核心步骤拆解

步骤1:初始化结果数组

创建vector<int> res(m + n),初始值为0(数组索引从0到m+n-1,对应乘积的"高位到低位")。

步骤2:双重循环计算每一位的乘积

num1num2末尾(个位) 开始遍历(因为手工乘法从个位算起):

  • 对于num1[i]im-1到0),转换为数字a = num1[i] - '0'(字符转数字的常用技巧)。

  • 对于num2[j]jn-1到0),转换为数字b = num2[j] - '0'

  • 计算乘积a * b,并累加到数组的i + j + 1位置------为什么是i + j + 1

    举个例子:num1 = "12"i=1对应'2',i=0对应'1'),num2 = "34"j=1对应'4',j=0对应'3'):

    • i=1j=1时,2*4=8,应存在"个位",对应数组索引1+1+1=3(数组长度4,索引3是最后一位,即个位)。
    • i=1j=0时,2*3=6,应存在"十位",对应数组索引1+0+1=2
    • i=0j=1时,1*4=4,应存在"百位",对应数组索引0+1+1=2(与上一步的6累加,得到10)。
    • i=0j=0时,1*3=3,应存在"千位",对应数组索引0+0+1=1

    最终数组在步骤2后的值为[0, 3, 10, 8],正好对应手工计算的中间结果(3000 + 1000 + 8 = 4008,后续处理进位即可)。

步骤3:处理进位,生成结果字符串

从数组的末尾(低位) 开始遍历,处理每一位的进位:

  • 计算当前位的总数值:current = res[k] + carrycarry是上一位的进位,初始为0)。
  • 当前位的实际值:current % 10(取余),转换为字符后加入结果字符串res_str
  • 更新进位:carry = current / 10(整除)。
  • 特殊处理:若遍历到数组首位(k=0)且res[0] + carry == 0,说明结果是0,直接跳过(避免添加多余的前导0)。
步骤4:反转结果字符串

由于步骤3是从"低位到高位"添加字符(如4008会先添加8004),因此需要反转res_str,得到最终结果。

四、示例演示:手把手走流程

num1 = "123"(m=3)、num2 = "45"(n=2)为例,完整演示流程:

步骤1:初始化结果数组

res长度为3+2=5,初始值:[0, 0, 0, 0, 0]

步骤2:双重循环计算乘积

  • i=2(num1[2]='3'):
    • j=1(num2[1]='5'):3*5=15 → res[2+1+1=4] +=15 → res变为[0,0,0,0,15]
    • j=0(num2[0]='4'):3*4=12 → res[2+0+1=3] +=12 → res变为[0,0,0,12,15]
  • i=1(num1[1]='2'):
    • j=1(num2[1]='5'):2*5=10 → res[1+1+1=3] +=10 → res变为[0,0,0,22,15]
    • j=0(num2[0]='4'):2*4=8 → res[1+0+1=2] +=8 → res变为[0,0,8,22,15]
  • i=0(num1[0]='1'):
    • j=1(num2[1]='5'):1*5=5 → res[0+1+1=2] +=5 → res变为[0,0,13,22,15]
    • j=0(num2[0]='4'):1*4=4 → res[0+0+1=1] +=4 → res变为[0,4,13,22,15]

步骤2结束后,res = [0,4,13,22,15]

步骤3:处理进位

  • k=4(res[4]=15):current=15+0=15 → 字符'5'加入res_str(res_str="5"),carry=1。
  • k=3(res[3]=22):current=22+1=23 → 字符'3'加入(res_str="53"),carry=2。
  • k=2(res[2]=13):current=13+2=15 → 字符'5'加入(res_str="535"),carry=1。
  • k=1(res[1]=4):current=4+1=5 → 字符'5'加入(res_str="5355"),carry=0。
  • k=0(res[0]=0):current=0+0=0 → 跳过(因res[0]+carry=0)。

步骤3结束后,res_str = "5355",carry=0。

步骤4:反转结果字符串

反转"5355""5535",即为最终结果(123*45=5535)。

五、代码解读:逐行理解C++实现

以下是完整的C++代码,结合注释逐段解读:

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;

class Solution {
public:
    string multiply(string num1, string num2) {
        // 步骤1:初始化结果数组(长度m+n,默认0)
        int m = num1.size(), n = num2.size();
        vector<int> res(m + n);

        // 边界情况:若有一个数是"0",直接返回"0"(避免多余计算)
        if (num1 == "0" || num2 == "0") {
            return "0";
        }

        // 步骤2:双重循环计算每一位的乘积(从字符串末尾遍历)
        for (int i = m - 1; i > -1; i--) { // i遍历num1(从个位到高位)
            for (int j = n - 1; j > -1; j--) { // j遍历num2(从个位到高位)
                int a = num1[i] - '0'; // 字符转数字
                int b = num2[j] - '0';
                res[i + j + 1] += a * b; // 乘积累加到对应位置
            }
        }

        // 步骤3:处理进位,生成结果字符串
        string res_str;
        int carry = 0; // 进位初始化为0
        for (int k = res.size() - 1; k > -1; k--) { // 从数组末尾(低位)遍历
            // 特殊处理:数组首位且无进位时,跳过(避免前导0)
            if (k == 0 && res[0] == 0 && carry == 0) {
                continue;
            }
            int current = res[k] + carry; // 当前位总数值(含进位)
            res_str.push_back((current % 10) + '0'); // 取余得当前位字符
            carry = current / 10; // 整除得新进位
        }

        // 步骤4:反转字符串(因步骤3是从低位到高位添加字符)
        reverse(res_str.begin(), res_str.end());

        // 兜底:若结果为空(理论上不会触发,因已处理"0"情况),返回"0"
        return res_str.empty() ? "0" : res_str;
    }
};

// 测试代码
int main() {
    Solution sol;
    string num1 = "123", num2 = "45";
    cout << "乘积结果:" << sol.multiply(num1, num2) << endl; // 输出5535
    return 0;
}

六、注意事项:避坑指南

  1. 边界情况处理 :必须先判断num1num2是否为"0",否则会返回"000..."等错误结果。
  2. 数组索引对应 :牢记res[i + j + 1]的位置逻辑,这是模拟数位对齐的核心,也是最容易出错的点。
  3. 进位处理顺序:必须从数组末尾(低位)开始处理进位,若从高位开始会导致进位传递错误。
  4. 前导0移除 :步骤3中k==0的判断是为了避免结果开头出现多余的0(如"0" * "0"不会返回空字符串,而是通过num1=="0"直接返回"0")。

七、复杂度分析

  • 时间复杂度 :O(m * n),其中mn分别是num1num2的长度。双重循环遍历所有字符对,后续处理进位的循环是O(m + n),可忽略。
  • 空间复杂度 :O(m + n),主要用于存储结果数组res(长度m + n)和结果字符串res_str(长度最多m + n)。

八、总结与拓展

本文的核心思路是**"模拟手工乘法"**,通过数组存储临时结果、分步骤处理乘积和进位,完美解决了大数字溢出问题。这种"模拟算法"的思想不仅适用于字符串相乘,还可推广到"字符串相加""字符串相减"等大数字处理问题(如LeetCode 415. 字符串相加)。

掌握该算法后,建议尝试以下拓展练习:

  1. 优化时间复杂度:能否用"快速傅里叶变换(FFT)"将时间复杂度降至O((m + n)log(m + n))?(适合处理极长字符串)
  2. 实现字符串相减(需处理负数和借位)。

希望本文能帮助你彻底理解字符串相乘的逻辑,在面试中轻松应对这类问题!

相关推荐
广然9 小时前
跨厂商(华为 & H3C)防火墙 IPSec 隧道部署
服务器·网络·华为
wwlsm_zql9 小时前
华为科大讯飞携手,低成本AI革新教育农业应用
人工智能·华为
爱笑的眼睛1113 小时前
深入探讨HarmonyOS ArkTS中的日期时间处理技巧
华为·harmonyos
ifeng091821 小时前
HarmonyOS分布式任务调度——跨设备智能任务分配与迁移
分布式·华为·harmonyos
冰冷的bin1 天前
【Harmony】鸿蒙相机拍照使用简单示例
数码相机·华为·harmonyos
爱笑的眼睛112 天前
HarmonyOS RemoteWindow远程窗口组件的分布式能力深度解析
华为·harmonyos
鲜枣课堂2 天前
华为最新光通信架构AI-OTN,如何应对AI浪潮?
人工智能·华为·架构
爱笑的眼睛112 天前
HarmonyOS Badge徽标组件:深入消息提示的实现与优化
华为·harmonyos
爱笑的眼睛112 天前
HarmonyOS List组件性能优化:从基础到高级实践
华为·harmonyos