每日LeetCode : 加一

111 阅读5分钟

问题描述

给定一个表示 大整数 的整数数组 digits,其中 digits[i] 是整数的第 i 位数字。这些数字按从左到右,从最高位到最低位排列。这个大整数不包含任何前导 0

将大整数加 1,并返回结果的数字数组。


输入: digits = [1,2,3]          
输出: [1,2,4]
解释: 输入数组表示数字 123, 加 1 后得到 123 + 1 = 124。
因此,结果应该是 [1,2,4]。


输入: digits = [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321, 加 1 后得到 4321 + 1 = 4322。
因此,结果应该是 [4,3,2,2]

解法一:基础模拟 - 直观的进位处理

这个解法是最直观也是最常见的解法,它的核心思想是模拟人工加法过程:即从最低位(数组末尾)开始加 1,处理进位,逐位计算。

function plusOne(digits) {
  const n = digits.length;
  let carry = 1; // 初始加1相当于最低位有一个进位1

  for (let i = n - 1; i >= 0; i--) {
    let sum = digits[i] + carry;
    digits[i] = sum % 10; // 当前位结果
    carry = Math.floor(sum / 10); // 计算新的进位
  }

  // 处理最高位进位导致数组需要扩容的情况 (如 999 + 1 = 1000)
  if (carry > 0) {
    digits.unshift(carry); // 在数组头部添加进位值
  }

  return digits;
}

解题思想:

  1. 逆向遍历: 加法从最低位开始计算。
  2. 进位传递: 使用 carry 变量记录并传递进位值。
  3. 处理扩容: 循环结束后检查最高位是否仍有进位,若有则在数组头部插入新元素 1

注意点:

  • 原地修改问题: 此解法直接修改了输入数组 digits,在要求不修改原数组的场景会出错(但是力扣本题允许),因此,安全做法应该是创建新数组或深拷贝
  • 忘记处理最高位进位: 遗漏最后的 if (carry > 0) 检查,导致类似 [9, 9] 的输入无法正确输出 [1, 0, 0]
  • 进位计算错误: carry = Math.floor(sum / 10) 是关键,错误写成 carry = sum / 10 会得到小数导致后续计算错误。

解法二:优化 - 提前终止循环

观察可以发现:一旦某一位不再产生进位,其左侧更高位的数字就不会再改变,那么,利用此特性,我们可以提前结束循环,以提升效率。

function plusOne(digits) {
  const n = digits.length;

  for (let i = n - 1; i >= 0; i--) {
    if (digits[i] < 9) {
      digits[i]++; // 当前位小于9,加1后不会进位,直接返回
      return digits;
    }
    digits[i] = 0; // 当前位是9,加1后变成0,进位1传递给更高位
  }

  // 循环执行完说明所有位都是9,全部变成了0,需要在最前面补1
  return [1, ...digits]; // 或 digits.unshift(1); return digits;
}

解题思想:

  1. 逆向寻找非 9 位: 从右向左遍历,找到第一个小于 9 的数字。
  2. 立即加一并返回: 将该位加 1,其右边所有位已设为 0 (由进位导致),此时结果已确定。
  3. 处理全 9 情况: 若遍历完所有位(说明原数全由 9 组成),则返回由 1n0 组成的新数组。

解法优点:

  • 效率提升: 大多数情况下(非全 9 的数)无需遍历整个数组。
  • 逻辑清晰: 代码更简洁,更直接体现了“加一”操作的短路特性。

注意点:

  • 边界条件 - 全 9: 必须正确处理所有位都是 9 的情况,返回 [1, 0, 0, ..., 0]
  • 修改原数组: 同样要注意是否允许修改原数组 digits,解法中 digits[i]++digits[i] = 0 修改了原数组,如果不允许,需先复制:
    let result = [...digits]; // 创建副本
    ... // 在result上操作
    

解法三:函数式编程风格 (JavaScript)

利用数组的 mapfindIndex 方法,结合扩展运算符,可写出更简洁的函数式风格代码(主要展示思路,效率非最高):

function plusOne(digits) {
  // 1. 尝试在最后一位直接加1
  const tryIncrement = [...digits];
  tryIncrement[digits.length - 1] += 1;

  // 2. 检查是否有任何位变成了10
  const hasOverflow = tryIncrement.some(digit => digit === 10);

  if (!hasOverflow) return tryIncrement; // 没有进位,直接返回

  // 3. 处理进位:从右向左找到第一个非9的位置
  const result = [...digits];
  let idx = result.length - 1;

  // 4. 逆向查找第一个可以安全加1的位置 (非9的位置)
  while (idx >= 0 && result[idx] === 9) {
    result[idx] = 0; // 将连续的9变为0
    idx--;
  }

  // 5. 处理找到的位置或全9的情况
  if (idx >= 0) {
    result[idx]++;
  } else {
    result.unshift(1); // 所有位都是9,在头部添加1
  }

  return result;
}

思想与易错点: 核心逻辑与 解法二 一致(寻找非 9 位、处理全 9),但是它的实现方式更函数式,而且也要注意: tryIncrement 只是试探,hasOverflow 判断仍需遍历数组,其效率其实不如 解法二 的直接循环。

核心解题思想总结

  1. 模拟人工计算: 加法的本质是从最低位开始逐位计算并处理进位。
  2. 逆向思维: 处理数字位操作(尤其是进位)时,从数组末尾(最低位)向开头(最高位)遍历通常更自然高效。
  3. 短路优化: 一旦进位停止传播,更高位无需再计算(解法 2 的核心优化点)。
  4. 边界处理: 特别注意所有位都是 9 的特殊情况,此时结果位数增加。