题目要求:
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 或 ["ab", "ab", "cc"] 的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
解题思路
要解决这个问题,核心思路是贪心算法:
-
先记录字符串中每个字符最后一次出现的下标,这是关键 —— 因为同一个字符必须只在一个片段里,所以片段的结束位置至少要到这个字符最后出现的位置。
-
遍历字符串,动态维护当前片段的最远结束下标:
- 每遍历一个字符,更新当前片段的最远结束下标为「当前最远下标」和「该字符最后下标」的最大值。
- 当遍历到当前片段的最远结束下标时,说明这个片段可以分割,记录长度,然后开始新的片段。
代码解释:
-
记录字符最后出现位置:
用长度为 26 的数组lastIndex存储每个小写字母最后一次出现的下标,空间复杂度 O (1)(固定大小数组)。 -
贪心遍历分割:
- start:当前片段的起始下标。
- end:当前片段能到达的最远下标(由片段内所有字符的最后出现位置决定)。
- 当遍历到i == end时,说明当前片段内所有字符都不会出现在后续位置,完成分割,记录长度。
复杂度:
- 时间复杂度:O (n),仅遍历字符串两次(一次记录最后位置,一次分割)。
- 空间复杂度:O (1),除结果列表外,仅使用固定大小的辅助数组。
总结
- 核心是先记录每个字符最后出现的位置,再用贪心策略动态确定片段边界。
- 遍历到当前片段最远结束下标时分割,保证同一字母只在一个片段,且分割片段最多。
- 时间复杂度 O (n),效率最优,适合处理长字符串。
Java代码实现如下
import java.util.ArrayList;
import java.util.List;public class PartitionLabels {public List<Integer> partitionLabels(String s) {// 1. 记录每个字符最后一次出现的索引int[] lastIndex = new int[26]; // 小写字母,用26长度数组即可int n = s.length();for (int i = 0; i < n; i++) {char ch = s.charAt(i);lastIndex[ch - 'a'] = i; // 字符转数组下标:ch-'a'}List<Integer> result = new ArrayList<>();int start = 0; // 片段起始下标int end = 0; // 片段最远结束下标// 2. 遍历字符串,贪心分割for (int i = 0; i < n; i++) {char ch = s.charAt(i);// 更新当前片段的最远结束位置end = Math.max(end, lastIndex[ch - 'a']);// 遍历到当前片段的最远位置,分割成功if (i == end) {result.add(end - start + 1); // 记录片段长度start = i + 1; // 更新下一个片段的起始位置}}return result;}// 测试示例public static void main(String[] args) {PartitionLabels solution = new PartitionLabels();// 测试用例1:"ababcc" → 输出 [4,2]System.out.println(solution.partitionLabels("ababcc"));// 测试用例2:"ababcbacabcdefegdehijhklij" → 输出 [9,7,8]System.out.println(solution.partitionLabels("ababcbacabcdefegdehijhklij"));}
}
解题思路:
-
由于同一个字母最多出现在一个片段中,因此我们要确保这个片段能覆盖到当前字符的最远位置,我们其实不需要特别地去找字符最后一次出现的索引,我们只需要在遍历的过程中,每次都在循环体中记录当前字符的位置,那么当遍历到同一个字符最后一次出现的位置时,就会自然地更新为字符最后一次出现的位置
-
由于英文小写字母总共有26个,因此我们只需要创建一个长度为26的整型数组,通过关系式ch - 'a'可以得到当前字符与英文字母a的差值,这个差值代表了字符对应的数组索引,此时英文字母就可以用数组的下标唯一表示,例如ch[0]对应字母a。
// 1. 记录每个字符最后一次出现的索引int[] lastIndex = new int[26]; // 小写字母,用26长度数组即可int n = s.length(); //字符串的长度是调用函数length(),数组的长度不是函数因此不需要加括号,直接arr.lengthfor (int i = 0; i < n; i++) {char ch = s.charAt(i);lastIndex[ch - 'a'] = i; // 字符转数组下标:ch-'a'}
- 由于需要返回一个表示每个字符串片段的长度的列表。我们创建了一个集合用来记录每个片段的长度,为了帮助我们更好地分割字符串并且记录片段的长度,我们创建了一对双指针start和end,分别对应片段的起始索引和结束索引
List<Integer> result = new ArrayList<>();int start = 0; // 片段起始下标int end = 0; // 片段最远结束下标
- 第一个for循环是为了记录每个字符最后一次出现的索引,第二个for循环目的是为了分割字符串
那么如何有了上述工具,我们该如何贪心分割呢?其实就是将当前遍历到的字符最后一次出现的索引与当前片段的最远位置索引进行比较,更新为最大的那个,确保当前片段覆盖了片段中每个字符最后出现的位置。
for(int i = 0;i<n;i++){
char ch = s.charAt(i);
end = Math.max(end,lastIndex[ch-'a')];
- 当循环遍历到当前片段的最远位置后(i==end),我们就可以对该片段进行分割了,结合前面引入的双指针,用公式end-start+1,就得到了当前片段的长度,将这个长度加入到结果集合result中,同时更新start的位置为i+1,实现片段的分割
if(i==end){result.add(end - start + 1);start = i+1;}
}
完成第一个片段的分割后,start来到了第二个片段的起始索引位置(i+1),循环继续往后遍历字符串,不断更新下一个片段的最远位置(end = Math.max(end,lastIndex(ch-'a'))),直到到达片段的最远位置(i==end),用(end - start + 1)记录当前片段的长度,将长度加到结果集合中,result.add(end - start + 1),再更新start为下一个片段的起始索引(start = i + 1)
这就是字符串片段划分的贪心策略解法
