向字符串添加空格 图解题解
这道题到底在问什么
- 输入
- s="LeetcodeHelpsMeLearn", spaces=[8,13,15]
- 输出
- "Leetcode Helps Me Learn"
- 输入
- s="AlgoMoocOJ", spaces=[4,8]
- 输出
- "Algo Mooc OJ"
最优解:为什么这么做
一句话答案:LeetCode 2109 向字符串添加空格:spaces 已严格递增,双指针一趟扫——i 走原串、j 追下一个插入点,i 撞上 spaces[j] 就先补空格再放字符,时间 O(n)。
空格该插在哪,插完长什么样
给一个下标从 0 开始的字符串 s,再给一个下标数组 spaces,里面的下标从小到大排、彼此不重复。spaces 里每个下标处的字符之前,都要插进一个空格,返回插好的新串。题面例子 s 是 AlgoMoocOJ、spaces 是 [4,8],在下标 4 的 M 前、下标 8 的 O 前各补一个空格,得到 Algo Mooc OJ。
遇一个空格就切一次串,账怎么算
照字面来:每碰到一个 spaces 下标,就在原串那里切开、插进一个空格。spaces 有 m 个下标就切 m 次,而字符串不可变,每切一次都要复制整条串,长 n 的串复制一趟就是 n 次搬运。切 m 次、每次搬 n 个字符,总代价是 O(n×m),串一长就拖。
spaces 升序这条件,能省下什么
既然 spaces 从小到大排好,下一个要插空格的位置永远在当前位置右边、绝不回头。那就不必反复回原串切来切去,只要从左到右扫一遍 s,攥一个 j 指着 spaces 里下一个待插入的下标:扫到的字符下标要么还没够到 spaces[j],要么正好等于它。两个各扫一条数据的指针,i 走 s、j 走 spaces,把插空格和放字符对齐。
一趟扫下来,每个字符先做哪件事
备一个可变容器(能往后追加、不用重建的列表或 StringBuilder)当答案。i 从 0 扫到串尾,每落到字符 c 先问:j 没走完 spaces、且 i 等于 spaces[j]?成立就先塞一个空格、j 前移一格;不管补没补,都无条件把 c 追加进去。i 走完,碎片拼成串就是答案。顺序不能反,先补空格再放字符。
AlgoMoocOJ、spaces=[4,8] 逐格过每步
s 是 AlgoMoocOJ,下标 0 到 9,spaces 是 [4,8],j 从 0 起。i 从 0 走到 3 都不等于 spaces[0],A、l、g、o 挨个进容器,攒成 Algo。
i 到 4 命中 spaces[0]:塞空格、j 到 1、放 M,成 Algo M。i 走 5、6、7 都不命中,o、o、c 追加成 Algo Mooc。i 到 8 命中 spaces[1]:塞空格、j 到 2、放 O,得 Algo Mooc O。i 到 9 时 spaces 已用尽,守卫不成立,跳过补空格直接放 J,成 Algo Mooc OJ。
取 spaces[j] 前不看 j,串一扫完就崩
取 spaces[j] 之前得先确认 j 没走出 spaces:用完后 j 等于它的长度,再读 spaces[j] 就摸到数组外面,越界会报错或读到脏值,守卫必须挡在取值前。空格落点也松不得,题目要在触发字符之前插,塞到后面整条串就错位。攒答案别用加号在循环里硬拼不可变字符串,每加一次都重建整串、代价滚回 O(n×m);切片重建整串是同一个坑,一遍扫描追加才线性。
扫描只碰每个字符一次,i 单遍走、j 只增不减,前移总次数不超过 spaces 长度,时间 O(n);答案串比原串多 m 个空格,长度 n 加 m 量级,空间 O(n),辅助只用两个下标是 O(1)。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:i 扫字符,j 追下一个要插空格的位置,i 撞上 spaces[j] 就先补一个空格再把字符追进去。整趟只扫一遍,又快又稳。下面从头开始拼。
- 4双指针待命布局摆好。上面这条是原串 AlgoMoocOJ,每个格子下面是它的下标。右边面板是 spaces 数组,现在 j 指着第 0 行、值是 4,意思是下一个要在它前面补空格的位置是下标 4。下面这行 ans 是我们要拼的答案,现在还空着。指针 i 从 0 出发。
- 5比较 i 与 spaces[j]指针 i 走到 0,和 spaces[0] 也就是 4 比一比,0 不等于 4,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 6追加当前字符不管刚才插没插空格,当前字符 A 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「A」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 7比较 i 与 spaces[j]指针 i 走到 1,和 spaces[0] 也就是 4 比一比,1 不等于 4,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 8追加当前字符不管刚才插没插空格,当前字符 l 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Al」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 9比较 i 与 spaces[j]指针 i 走到 2,和 spaces[0] 也就是 4 比一比,2 不等于 4,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 10追加当前字符不管刚才插没插空格,当前字符 g 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Alg」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 11比较 i 与 spaces[j]指针 i 走到 3,和 spaces[0] 也就是 4 比一比,3 不等于 4,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 12追加当前字符不管刚才插没插空格,当前字符 o 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 13比较 i 与 spaces[j]指针 i 走到 4,拿它和 spaces[0] 也就是 4 比一比,正好相等,命中了!这说明当前字符 M 之前该补一个空格。先别急着放字符,下一步先把空格补上。
- 14插空格 · j 前移命中了就先补空格。往 ans 末尾放一个空格,你看答案串在字符 M 之前多了一个空格。补完把 j 往后挪一格,从 0 变成 1,面板高亮跟着移到下一个待插入的位置(下标 8)。空格补好了,接着才轮到字符。
- 15追加当前字符不管刚才插没插空格,当前字符 M 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo M」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 16比较 i 与 spaces[j]指针 i 走到 5,和 spaces[1] 也就是 8 比一比,5 不等于 8,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 17追加当前字符不管刚才插没插空格,当前字符 o 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo Mo」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 18比较 i 与 spaces[j]指针 i 走到 6,和 spaces[1] 也就是 8 比一比,6 不等于 8,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 19追加当前字符不管刚才插没插空格,当前字符 o 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo Moo」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 20比较 i 与 spaces[j]指针 i 走到 7,和 spaces[1] 也就是 8 比一比,7 不等于 8,说明这个位置前面不用加空格。那就跳过补空格这步,直接去追加字符。
- 21追加当前字符不管刚才插没插空格,当前字符 c 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo Mooc」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 22比较 i 与 spaces[j]指针 i 走到 8,拿它和 spaces[1] 也就是 8 比一比,正好相等,命中了!这说明当前字符 O 之前该补一个空格。先别急着放字符,下一步先把空格补上。
- 23插空格 · j 前移命中了就先补空格。往 ans 末尾放一个空格,你看答案串在字符 O 之前多了一个空格。补完把 j 往后挪一格,从 1 变成 2,这下 spaces 里两个下标都用过了、j 已用完,后面不再有待插入的位置,面板高亮也随之撤掉。空格补好了,接着才轮到字符。
- 24追加当前字符不管刚才插没插空格,当前字符 O 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo Mooc O」。这一格处理完,指针 i 往右挪一步,去看下一个字符。
- 25比较 i 与 spaces[j]指针 i 走到 9,看右边面板,j 已经等于 2,spaces 两个下标都用完了。既然没有待插入的位置了,就跳过检查,直接去追加这个字符。这一步演示的就是 j 用尽后的收尾。
- 26追加当前字符不管刚才插没插空格,当前字符 J 都要放进答案。把它追加到 ans 末尾,现在答案变成了 「Algo Mooc OJ」。这一格是原串最后一个字符,处理完 i 就到达串尾之后的位置,下一步进入扫描结束、开始收束。
- 27大功告成整条串扫完了。回头看,我们只在两处补了空格:下标 4 的 M 前面、下标 8 的 O 前面,这两个触发字符标成了绿色。最后拼出来的答案就是 「Algo Mooc OJ」,和一开始说的 Algo Mooc OJ 分毫不差。全程只扫了一遍,双指针配合得干净利落。
⚠️ 容易写错的地方
✗ 错:取 spaces[j] 前不判 j 是否越界
✓ 对:先判 j 小于 spaces 长度,再比 i 等于 spaces[j]
spaces 用完后 j 会等于其长度,此时再取 spaces[j] 会越界,轻则报错重则读到脏值,守卫必须放在取值之前
✗ 错:把空格插到 spaces[j] 那个字符之后
✓ 对:空格插在该字符之前
题目要求在下标 spaces[j] 的字符之前加空格,插到后面会整体错位,输出全错
✗ 错:Java 里用 String 加号在循环里拼
✓ 对:用 StringBuilder,或 Python 用列表 join
不可变字符串每次加号都新建整串,循环里拼是 O(n 方);用可变容器攒好再一次成串才是 O(n)
✗ 错:每遇到一个 spaces 就用切片重建整串
✓ 对:一遍扫描顺序追加
反复切片拼接,每次都要复制整串,总代价是 O(n 乘 m);单遍追加只碰每个字符一次,才是线性
完整代码(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 addSpaces(self, s: str, spaces: List[int]) -> str:
ans = []
j = 0
for i, c in enumerate(s):
if j < len(spaces) and i == spaces[j]:
ans.append(' ')
j += 1
ans.append(c)
return ''.join(ans)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:
string addSpaces(string s, vector<int>& spaces) {
string ans = "";
for (int i = 0, j = 0; i < s.size(); ++i) {
if (j < spaces.size() && i == spaces[j]) {
ans += ' ';
++j;
}
ans += s[i];
}
return ans;
}
};Java
import java.util.*;
class Solution {
public String addSpaces(String s, int[] spaces) {
StringBuilder ans = new StringBuilder();
for (int i = 0, j = 0; i < s.length(); ++i) {
if (j < spaces.length && i == spaces[j]) {
ans.append(' ');
++j;
}
ans.append(s.charAt(i));
}
return ans.toString();
}
}复杂度
时间
O(n)
n 是原串长度。i 从头扫到尾只走一遍,每个位置最多补一个空格加追加一个字符,都是常数操作;j 一路只增不减,总共前移的次数等于 spaces 的长度,不超过 n。合起来随 n 线性增长
空间
O(n)
按峰值算,主要花在攒出来的答案串上:它比原串多了 spaces 长度个空格,长度是 n 加 m 量级,也就是 O(n)。除答案外只用了 i、j 两个下标,额外辅助空间是 O(1)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 向字符串添加空格 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用可变容器攒答案,直接字符串相加不行吗?+
像 Java 的 String 是不可变的,循环里用加号每加一次都会新建一整条串、把已有内容整体复制过去,n 个字符拼下来复制代价累加成 O(n×m) 甚至更糟,数据一大就超时。正确做法是用 StringBuilder 这类可变容器,或者 Python 先把碎片放进列表、最后一次 join,把总代价压回 O(n)。核心就是别在循环里反复重建整串。
取 spaces[j] 之前那个 j 没越界的判断,能省掉吗?+
不能。j 是随命中一路前移的,当 spaces 里的下标全用完,j 会等于 spaces 的长度,此时 spaces[j] 已经指到数组末尾之外,直接读会越界,轻则抛异常,重则读到内存里的脏值让结果错得没头绪。所以循环里的条件必须写成「j 还没到 spaces 长度,并且 i 等于 spaces[j]」,把 j 的范围检查放在取值之前,短路求值会在 j 用尽时直接跳过后半段比较。
空格插在触发字符前面还是后面,会差很多吗?+
差在整条串会不会错位。题目明确要求在下标 spaces[j] 的那个字符之前加空格,所以命中时的顺序是先往容器塞空格、再放字符。要是写反成先放字符再补空格,空格就落到了触发字符后头,从这个位置起后面每个字的相对位置都挪了一格,输出整体错位。记牢命中那一步的两个动作有先后:空格在前、字符在后。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 向字符串添加空格 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。