1. 问题背景与理解
这道增减字符串匹配问题(LeetCode 942)乍看简单,实则蕴含了巧妙的算法设计思想。题目给定一个仅由'I'(Increase)和'D'(Decrease)组成的字符串s,要求构造一个排列perm,使得对于每个字符:
- 遇到'I'时,相邻数字递增
- 遇到'D'时,相邻数字递减
关键在于理解排列的定义:必须使用0到n(字符串长度)的所有整数且不重复。例如对于"IDID",我们需要用0,1,2,3,4构造满足条件的排列。
提示:这类构造型问题通常不需要复杂的数据结构,重点在于发现数字分配的规律性。
2. 贪心算法设计思路
2.1 核心观察
通过分析示例可以发现一个关键模式:
- 遇到'I'时选择当前可用数字中的最小值,为后续可能的'D'保留更大的数字
- 遇到'D'时选择当前可用数字中的最大值,为后续可能的'I'保留更小的数字
这种"极端选择"策略正是贪心算法的典型特征——局部最优选择导致全局最优解。
2.2 双指针实现方案
我们维护两个指针:
low:初始为0,表示当前可用最小数字high:初始为n,表示当前可用最大数字
遍历字符串时:
- 遇到'I' → 放入
low,然后low++ - 遇到'D' → 放入
high,然后high-- - 最后剩余一个数字(此时
low == high)放入末尾
这种设计保证了:
- 数字范围严格在[0, n]之间
- 每个数字只使用一次
- 时间复杂度O(n),空间复杂度O(1)
3. 代码实现详解
3.1 Java版本实现
java复制class Solution {
public int[] diStringMatch(String s) {
int n = s.length();
int[] res = new int[n + 1];
int low = 0, high = n;
for (int i = 0; i < n; i++) {
if (s.charAt(i) == 'I') {
res[i] = low++;
} else {
res[i] = high--;
}
}
res[n] = low; // 此时low == high
return res;
}
}
关键实现细节:
- 结果数组长度为n+1
- 循环只处理前n个字符
- 最后一位无需判断,直接填入剩余数字
3.2 边界情况处理
特殊输入需要考虑:
- 全'I'字符串:如"III" → [0,1,2,3]
- 全'D'字符串:如"DDD" → [3,2,1,0]
- 单字符字符串:如"D" → [1,0]
- 空字符串:返回[0]
代码中已经天然处理了这些情况,不需要额外判断。
4. 算法正确性证明
4.1 数学归纳法验证
对于任意位置i:
- 如果s[i]='I':
- 选择当前最小low
- 后续数字≥low+1,满足perm[i] < perm[i+1]
- 如果s[i]='D':
- 选择当前最大high
- 后续数字≤high-1,满足perm[i] > perm[i+1]
4.2 排列性质保证
由于我们始终在[0,n]范围内选择数字,且:
- 每次选择后移动指针
- 最终low==high时填入最后一个数字
这保证了所有数字唯一且恰好覆盖0到n
5. 复杂度分析与优化
5.1 时间复杂度
- 单次字符串遍历:O(n)
- 数组填充:O(n)
- 总体:O(n)
这是最优时间复杂度,因为必须检查每个字符。
5.2 空间复杂度
- 结果数组:O(n)(不可避免)
- 额外变量:O(1)
- 总体:O(1)额外空间
6. 测试用例设计
全面的测试应该包含:
java复制public class Test {
public static void main(String[] args) {
testCase("IDID", new int[]{0,4,1,3,2});
testCase("III", new int[]{0,1,2,3});
testCase("DDI", new int[]{3,2,0,1});
testCase("I", new int[]{0,1});
testCase("D", new int[]{1,0});
testCase("", new int[]{0}); // 边界case
}
static void testCase(String s, int[] expected) {
Solution sol = new Solution();
int[] actual = sol.diStringMatch(s);
System.out.println(s + " => " + Arrays.toString(actual));
System.out.println("验证: " + (Arrays.equals(actual, expected) ? "通过" : "失败"));
}
}
7. 常见问题与解决
7.1 为什么贪心策略有效?
贪心有效的核心在于:
- 极值选择为后续操作保留最大灵活性
- 数字范围的单调性保证总能找到解
7.2 如何处理非法输入?
实际工程中需要增加校验:
java复制if (s == null) throw new IllegalArgumentException();
for (char c : s.toCharArray()) {
if (c != 'I' && c != 'D') {
throw new IllegalArgumentException("只能包含I和D");
}
}
7.3 其他解法对比
虽然本题贪心是最优解,但也可以考虑:
- 回溯法:枚举所有排列,验证条件 → 时间复杂度O(n!)不可行
- 动态规划:难以设计状态转移方程
8. 实际应用场景
这类问题在以下场景有应用价值:
- 密码生成规则
- 数据序列化/反序列化
- 游戏关卡难度设计
- 时间序列模式匹配
9. 算法扩展思考
如果问题变更为:
- 允许数字重复 → 解法会更简单(但不符合排列定义)
- 使用特定范围的数字 → 调整初始low/high即可
- 需要所有可能解而不仅是一个 → 需要回溯算法
10. 编码技巧总结
- 双指针技巧:维护可用的数字范围
- 贪心选择:极端值选择保证后续灵活性
- 边界处理:注意数组长度是n+1
- 测试验证:必须包含全I、全D、混合、空串等case
通过这道题可以深刻理解贪心算法的设计思路——通过局部最优的累积达到全局最优。在实际编码中,这种"极值选择+范围维护"的模式也适用于许多其他问题场景。