一维数组的动态和 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,3,4]
- 输出
- [1,3,6,10]
- 输入
- nums=[1,1,1,1,1]
- 输出
- [1,2,3,4,5]
- 输入
- nums=[3,1,2,10,1]
- 输出
- [3,4,6,16,17]
最优解:为什么这么做
一句话答案:LeetCode 1480 一维数组的动态和:新数组第 i 位是 nums 前 i+1 个数之和,靠前缀和边扫边累加、每格只在上一格结果上添一个新数不重算,时间 O(n)、空间 O(1)。
动态和的每一格该填什么
给一个数组 nums,要返回一个等长的新数组,第 i 个位置放的是 nums 从开头一直加到第 i 个的总和:第 0 位就是 nums[0],第 1 位是 nums[0]+nums[1],越往后加进来的数越多。题面 nums=[1,2,3,4] 得 [1,3,6,10],nums=[3,1,2,10,1] 得 [3,4,6,16,17]。
每格从头加一遍,慢在哪里
到第 i 位就从 nums[0] 加到 nums[i],每格都当成一道独立的求和题——第 0 位加 1 个数、第 1 位加 2 个,直到末位加满 n 个。这些次数摞起来是 1+2+…+n,落在 O(n²) 量级。数组短时无所谓,一旦长到上千,同一段开头被反复重加,大多加法都在白做。
前缀和凭什么每格只加一次
相邻两格的和其实只差一个数。第 i 位是前 i+1 个数的和,第 i−1 位是前 i 个数的和,两者之间只差最后进来的那个 nums[i]。上一格已把前面整段加好,算这一格拿它再补一个 nums[i] 就够,不必回头重算。这种累计下来的和就叫前缀和,滚动公式是 ans[i] = ans[i-1] + nums[i]:累加器从左滚到右扫一遍,每格只做一次加法,前面的和一次都不重来。
累加器从左滚到右,每步做两件事
走起来只有两个动作。先定累加器的起点:第 0 位左边没有数,前缀和就是 nums[0] 自己,相当于从 0 加上 nums[0]。然后从第 1 位起,每到一格就把累加器,也就是上一格的前缀和,加上当前的 nums[i];得到的新和写进这一格,也顺势成为下一格的累加器。一路推进到末位,前缀和就都填满了。
题面两组数,逐格滚给你看
先走 nums=[1,2,3,4]。累加器从 0 起步:第 0 位 0+1=1;第 1 位 1+2=3;第 2 位 3+3=6;第 3 位 6+4=10,得到 [1,3,6,10]。每步都是拿左边刚算出的结果,只添一个新数。
再走 nums=[3,1,2,10,1]。第 0 位就是 3;第 1 位 3+1=4;第 2 位 4+2=6;第 3 位 6+10=16;第 4 位 16+1=17,最终是 [3,4,6,16,17]。哪怕中间夹着大得多的 10,也只是在上一格 6 上加一次,前面几个数不必再碰。
别漏了第 0 位,也别从右往左滚
另开新数组时最易漏的,是忘了先把第 0 位设成 nums[0] 就从第 1 位开填,开头空一格、整串全错位;原地写法从下标 1 起让 nums[i] 加上 nums[i-1],正好绕开——nums[0] 本就是它自己。方向也不能反:从左往右时读到的 nums[i-1] 已是算好的前缀和,当得起累加器,一改成从右往左,左邻居还没更新就取错了。复杂度上只一趟扫描、每格一次加法,时间 O(n);原地写回 nums 时空间 O(1),Python 的 accumulate 会另开长度 n 的列表作输出。边界同样一套滚法:单个元素时输出同输入;夹负数时前缀和先降再升;全 0 时动态和也全 0。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢一句:ans[i] = ans[i-1] + nums[i],第 0 位就是 nums[0]。一个累加器从左滚到右,扫一遍全部前缀和都出来。下面每帧都在套这句。
- 4先看清画面。上面是源数组 nums = [2,4,1,3,2,5,1,4,2,3],一共 10 个数。右边那张表是要填的动态和 ans,现在还是空的。我们准备一个累加器,起始值是 0,然后让紫色指针从下标 0 开始一格一格往右走,每走一格就把当前数加进累加器,得到的就是这一格的前缀和,记进右边的表。先从第 0 位开始。
- 5从第 0 位开始。紫色指针落在源数组下标 0,值是 2。它前面没有任何数,所以这一格的动态和起点就是它自己,累加器此刻是 0,等会儿 0 加上 2 就是 ans[0]。
- 6把 0 加上 2 得到 2,写进动态和的第 0 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2]。这个 2 又会成为下一格的累加器 prev,继续往右滚。
- 7轮到第 1 位。紫色指针落在源数组下标 1,值是 4。右边表里上一格 ans[0] 已经是 2,它就是前 1 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 4。
- 8把 2 加上 4 得到 6,写进动态和的第 1 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6]。这个 6 又会成为下一格的累加器 prev,继续往右滚。
- 9轮到第 2 位。紫色指针落在源数组下标 2,值是 1。右边表里上一格 ans[1] 已经是 6,它就是前 2 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 1。
- 10把 6 加上 1 得到 7,写进动态和的第 2 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7]。这个 7 又会成为下一格的累加器 prev,继续往右滚。
- 11轮到第 3 位。紫色指针落在源数组下标 3,值是 3。右边表里上一格 ans[2] 已经是 7,它就是前 3 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 3。
- 12把 7 加上 3 得到 10,写进动态和的第 3 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10]。这个 10 又会成为下一格的累加器 prev,继续往右滚。
- 13轮到第 4 位。紫色指针落在源数组下标 4,值是 2。右边表里上一格 ans[3] 已经是 10,它就是前 4 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 2。
- 14把 10 加上 2 得到 12,写进动态和的第 4 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12]。这个 12 又会成为下一格的累加器 prev,继续往右滚。
- 15轮到第 5 位。紫色指针落在源数组下标 5,值是 5。右边表里上一格 ans[4] 已经是 12,它就是前 5 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 5。
- 16把 12 加上 5 得到 17,写进动态和的第 5 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12,17]。这个 17 又会成为下一格的累加器 prev,继续往右滚。
- 17轮到第 6 位。紫色指针落在源数组下标 6,值是 1。右边表里上一格 ans[5] 已经是 17,它就是前 6 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 1。
- 18把 17 加上 1 得到 18,写进动态和的第 6 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12,17,18]。这个 18 又会成为下一格的累加器 prev,继续往右滚。
- 19轮到第 7 位。紫色指针落在源数组下标 7,值是 4。右边表里上一格 ans[6] 已经是 18,它就是前 7 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 4。
- 20把 18 加上 4 得到 22,写进动态和的第 7 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12,17,18,22]。这个 22 又会成为下一格的累加器 prev,继续往右滚。
- 21轮到第 8 位。紫色指针落在源数组下标 8,值是 2。右边表里上一格 ans[7] 已经是 22,它就是前 8 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 2。
- 22把 22 加上 2 得到 24,写进动态和的第 8 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12,17,18,22,24]。这个 24 又会成为下一格的累加器 prev,继续往右滚。
- 23轮到第 9 位。紫色指针落在源数组下标 9,值是 3。右边表里上一格 ans[8] 已经是 24,它就是前 9 个数的和,我把它当作累加器 prev 拿过来,接下来只要再加上当前这个 3。
- 24把 24 加上 3 得到 27,写进动态和的第 9 位,源数组里这一格标成绿色表示已经处理过。右边 ans 现在是 [2,6,7,10,12,17,18,22,24,27]。十个位置全部算完了。
- 25十个位置都填满了,回看一遍:累加器从 0 出发,一路是 2、6、7、10、12、17、18、22、24、27,每一步都是在上一步的结果上只加一个新数,绝不回头重算。最终动态和就是 [2,6,7,10,12,17,18,22,24,27]。整道题的窍门就一句:ans[i] = ans[i-1] + nums[i],一个累加器从左扫一遍,线性时间就搞定。
⚠️ 容易写错的地方
✗ 错:每算一格都从 nums[0] 重新累加到 nums[i],写成两层循环
✓ 对:利用上一格的结果滚动:ans[i] = ans[i-1] + nums[i],只加一次
前缀和的精髓就是复用。ans[i-1] 已经把前 i 个数加好了,算 ans[i] 时只差最后那个 nums[i],没必要把前面再加一遍。从头重加会让时间从 O(n) 退化成 O(n^2),数据一大就慢
✗ 错:忘了第 0 位,直接从 i=1 开始填,导致 ans[0] 空着
✓ 对:ans[0] 前面没有数,起点就是 nums[0] 本身(相当于 prev = 0)
第 0 个前缀和没有左邻居可加,它的值就是 nums[0]。原地写法里 nums[0] 本来就是它自己,循环从 i=1 开始正好不动它;但如果你另开数组,一定要先把 ans[0] 设成 nums[0] 再往后滚
✗ 错:原地覆盖时担心 nums[i-1] 被改坏了会算错
✓ 对:从左往右的顺序保证读到 nums[i-1] 时它已是前缀和,正是我们要的
原地写法里 nums[i-1] 确实被改成了前缀和,但这恰好是我们需要的累加器。只要严格从左到右推进,每次读的左邻居都是已经算好的结果,顺序一变(比如从右往左)才会出错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def runningSum(self, nums: List[int]) -> List[int]:
return list(accumulate(nums))C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> runningSum(vector<int>& nums) {
for (int i = 1; i < nums.size(); ++i) nums[i] += nums[i - 1];
return nums;
}
};Java
import java.util.*;
class Solution {
public int[] runningSum(int[] nums) {
for (int i = 1; i < nums.length; ++i) {
nums[i] += nums[i - 1];
}
return nums;
}
}复杂度
时间
O(n)
n 是数组长度。一个指针从左扫到右,每个位置只做一次加法和一次写入,都是常数操作,总共 n 次,所以是线性的 O(n)。本题 n 最大 1000,毫无压力。注意这正是滚动累加的价值:若每格都从头重加一遍会变成 O(n^2)
空间
O(1) / O(n)
按峰值算。C++ 和 Java 原地把结果写回 nums,只用了一个循环变量,额外空间是常数 O(1)。Python 用 accumulate 会另开一个长度 n 的新列表,这部分是 O(n);但那是要交出去的输出,若不计返回结果,核心累加逻辑同样只需常数额外空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 一维数组的动态和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
算好这一串前缀和,除了这道题还能拿来干嘛?+
它最大的用处是把区间求和压成常数时间。前缀和 ans 预处理好之后,想知道 nums 里某一段 [l, r] 的和,直接用 ans[r] 减 ans[l-1] 就行,其中 l 等于 0 时区间从头开始、直接取 ans[r],不必把这段重新加一遍。花一次 O(n) 预处理,换来之后任意多次区间求和都是 O(1)。子数组和、二维区域和这类看着复杂的题,底层大多靠它加速,所以值得专门记牢。
这题必须另开一个数组吗,能原地做省空间吗?+
能,C++ 和 Java 的参考写法就是原地的:从下标 1 开始,让 nums[i] 加上 nums[i-1],把前缀和直接覆盖回原数组,最后返回 nums,额外空间只有一个循环变量,是 O(1)。覆盖之所以不出错,是因为从左往右推进时,左边那一格早已被改成前缀和了,正好拿来当累加器。Python 也能这么原地循环,参考解图省事用了 accumulate,代价是另生成一个长度 n 的新列表。
Python、C++、Java 三种写法差在哪?+
逻辑三家完全一样,都是滚动累加,差别只在实现细节。Python 最短,一行 list(accumulate(nums)) 把累加交给标准库,代价是生成一个新列表;C++ 和 Java 几乎相同,一个 for 循环从下标 1 起执行 nums[i] += nums[i-1],原地把答案写回 nums 再返回,不额外开空间。三家读起来不同,骨子里都是同一句滚动累加。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 一维数组的动态和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。