本文概览:本文讲解划分字母区间的核心思路:先记录每个字符最后出现位置,再用"最远边界"贪心切割,i == curMaxEnd 时切一刀,O(n) 完成
一、题目

二、题目分析
1. 题目要求
给定一个字符串 s,将其划分为尽可能多 的片段,使得每个字母最多出现在一个片段中。返回每个片段的长度列表。
例子 :s = "ababcbacadefegdehijhklij"
a b a b c b a c a | d e f e g d e | h i j h k l i j
0 1 2 3 4 5 6 7 8 9 ... 15 16 ... 23
第1段(9) 第2段(7) 第3段(8)
输出:[9, 7, 8]
切割规则:
- 每个字母只能出现在一个片段里(不能跨片段)
- 片段数要尽可能多(每段尽可能短)
比如 "ababcbacadefegde", "hijhklij" 这样切成 2 段是错的------第 1 段太长,还能再切。
2. 怎么切才能切得尽可能多?
这是一个切割问题,目标是切出尽可能多的片段。
先想切割规则:每个字母只能出现在一个片段里。这意味着什么?
- 如果字母
a出现在位置 0 和位置 8,那么位置 0 到位置 8 必须属于同一个片段(不能把a拆开) - 如果字母
b出现在位置 1 和位置 5,那么位置 1 到位置 5 必须属于同一个片段 - 以此类推...
关键观察:每个字母的"首次出现"到"最后一次出现"之间,必须是一个完整的片段。
怎么切才能切得尽可能多?
从前往后遍历,如果已经遍历过的所有字符 在后面都不会再出现,就可以在这里切一刀。这样切出来的段最短,片段数最多。
但这里有两个难点:
难点一:怎么知道某个字符后面还有没有?
难点二 :怎么同时判断多个字符后面都不再出现?(遍历到位置 i 时,前面可能已经出现过 a、b、c 等多个字符,怎么知道它们后面都没了?)
三、思路概览
java
public List<Integer> partitionLabels(String s) {
// 1. 先统计每个字符最后一次出现的位置
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) {
last[s.charAt(i) - 'a'] = i;
}
// 2. 遍历字符串,curMaxEnd 为当前范围内最后一个字符的最后一次出现位置
int curMaxEnd = last[s.charAt(0) - 'a'];
int start = 0;
List<Integer> res = new ArrayList<>();
for (int i = 0; i < s.length(); i++) {
// 不断扩展当前片段的右边界
curMaxEnd = Math.max(curMaxEnd, last[s.charAt(i) - 'a']);
// 当遍历到的位置等于当前片段的最远边界时,说明找到了一个完整片段
if (i == curMaxEnd) {
res.add(i - start + 1);
start = i + 1;
}
}
return res;
}
思路简要说明:
- 预处理 last 数组 :
last[c]记录字符 c 的最后一次出现位置 - curMaxEnd :当前段的最远边界,初始为
last[s.charAt(0)] - 遍历字符串 :每到一个位置 i,用
last[s.charAt(i)]刷新curMaxEnd - i == curMaxEnd 时 :当前段内所有字符的最后出现都 ≤ i,可以切一刀,记录长度
i - start + 1,start更新为i + 1 - 时间复杂度 O(n):两遍遍历,第一遍统计 last,第二遍切割
四、思路详解
第一步:解决难点一------怎么知道字符后面还有没有?
预处理 :先遍历一遍字符串,记录每个字符最后一次出现的位置。
s = "ababcbacadefegdehijhklij"
last['a'] = 8 ← a 最后出现在索引 8
last['b'] = 5 ← b 最后出现在索引 5
last['c'] = 7 ← c 最后出现在索引 7
last['d'] = 14 ← d 最后出现在索引 14
...
这样遍历到位置 i 时,last[s.charAt(i)] 就是字符 si 的最后一次出现。如果 i == last[s.charAt(i)],说明这个字符后面不会再出现了。
last 数组把"后面还有没有"这个判断,从 O(n) 扫一遍降到了 O(1) 查表。
第二步:解决难点二------怎么同时判断多个字符?
遍历到位置 i 时,前面可能已经出现过 a、b、c 等多个字符。即使 a 的最后一次出现是 8,b 的最后一次出现是 5,c 的最后一次出现是 7,我们怎么知道这三个字符在位置 8 之后都不会再出现?
贪心思路 :维护一个当前段的最远边界 curMaxEnd ------它是当前段内所有字符的最后一次出现位置的最大值。
遍历到 i=0, 字符 a: curMaxEnd = last['a'] = 8
遍历到 i=1, 字符 b: curMaxEnd = max(8, last['b']) = max(8, 5) = 8
遍历到 i=4, 字符 c: curMaxEnd = max(8, last['c']) = max(8, 7) = 8
curMaxEnd = max(8, 5, 7) = 8 。只要 i < 8,说明当前段内至少有一个字符 (这里是 a)在位置 8 还会出现,这段还没结束。当 i == 8 时,说明当前段内所有字符的最后出现位置都 ≤ 8,后面不会再出现了------可以切一刀。
curMaxEnd 的本质 :它是当前段内所有字符的最后出现位置的最大值 。只要 i < curMaxEnd,段内至少有一个字符在 curMaxEnd 还会出现;当 i == curMaxEnd,段内所有字符的最后出现都 ≤ i,后面没了。
这和跳跃游戏的思路一模一样:跳跃游戏维护"最远可达距离",这题维护"最远边界",都是用一个变量代表多个状态的聚合信息。
第三步:为什么贪心是对的?
贪心策略 :每次 i == curMaxEnd 就切一刀,切出来的段尽可能短。
为什么这样能保证片段数最多?
因为切割条件是"当前段内所有字符后面都不再出现"------这是最短的合法段。如果推迟切割(比如等到 i > curMaxEnd 再切),段会变长,片段数会变少。
为什么这样能保证合法?
因为 curMaxEnd 是当前段内所有字符的最后出现位置的最大值。当 i == curMaxEnd 时,段内所有字符的最后出现都 ≤ i,后面不会再出现了------满足"每个字母最多出现在一个片段中"。
完整执行过程
以 s = "ababcbacadefegdehijhklij" 为例(只展示关键节点):
预处理 last 数组:
last['a'] = 8, last['b'] = 5, last['c'] = 7
last['d'] = 14, last['e'] = 15, last['f'] = 11
last['g'] = 13, last['h'] = 19, last['i'] = 22
last['j'] = 23, last['k'] = 20, last['l'] = 21
初始:curMaxEnd = last['a'] = 8, start = 0
i=0, a: curMaxEnd = max(8, 8) = 8
i=1, b: curMaxEnd = max(8, 5) = 8
i=2, a: curMaxEnd = max(8, 8) = 8
i=3, b: curMaxEnd = max(8, 5) = 8
i=4, c: curMaxEnd = max(8, 7) = 8
i=5, b: curMaxEnd = max(8, 5) = 8
i=6, a: curMaxEnd = max(8, 8) = 8
i=7, c: curMaxEnd = max(8, 7) = 8
i=8, a: curMaxEnd = max(8, 8) = 8, i==8 → 切割,长度 9,start=9
i=9, d: curMaxEnd = max(8, 14) = 14
i=10, e: curMaxEnd = max(14, 15) = 15
i=11, f: curMaxEnd = max(15, 11) = 15
i=12, e: curMaxEnd = max(15, 15) = 15
i=13, g: curMaxEnd = max(15, 13) = 15
i=14, d: curMaxEnd = max(15, 14) = 15
i=15, e: curMaxEnd = max(15, 15) = 15, i==15 → 切割,长度 7,start=16
i=16, h: curMaxEnd = max(15, 19) = 19
i=17, i: curMaxEnd = max(19, 22) = 22
i=18, j: curMaxEnd = max(22, 23) = 23
i=19, h: curMaxEnd = max(23, 19) = 23
i=20, k: curMaxEnd = max(23, 20) = 23
i=21, l: curMaxEnd = max(23, 21) = 23
i=22, i: curMaxEnd = max(23, 22) = 23
i=23, j: curMaxEnd = max(23, 23) = 23, i==23 → 切割,长度 8,start=24
返回 [9, 7, 8] ✓
关键观察:
- 第 1 段:i 从 0 到 8,curMaxEnd 一直是 8(a 的最后出现),i==8 时切割
- 第 2 段:i 从 9 到 15,curMaxEnd 从 14(d)刷新到 15(e),i==15 时切割
- 第 3 段:i 从 16 到 23,curMaxEnd 从 19(h)刷新到 22(i)再到 23(j),i==23 时切割
复杂度分析
时间复杂度 O(n):
- 第一遍遍历:统计 last 数组,O(n)
- 第二遍遍历:切割字符串,O(n)
- 总共 O(n)
空间复杂度 O(1):last 数组固定 26 个位置,常数空间。
五、总结
划分字母区间的核心思路:
- 题目要求:切割字符串,每个字母最多出现在一个片段,片段数尽可能多
- 切割条件:当前段内所有字符在后面都不再出现
- 预处理 last 数组:记录每个字符的最后一次出现位置,O(1) 判断"后面还有没有"
- curMaxEnd 贪心 :维护当前段的最远边界(段内所有字符最后出现位置的最大值),
i == curMaxEnd时切一刀 - 和跳跃游戏的类比:都是用一个变量(最远可达 / 最远边界)代表多个状态的聚合信息
贪心思想在本题的体现 :每次 i == curMaxEnd 就切,切出来的段是最短的合法段,片段数自然最多。
套路总结:遇到"切割 / 分段"类问题,先想想能不能用"最远边界"贪心------维护一个变量代表当前段的右边界,遍历到边界就切一刀。跳跃游戏、划分字母区间、合并区间都是这个套路。