1. 题目背景与需求解析
- Increasing Decreasing String(上升下降字符串)是LeetCode上一道考察字符串操作的经典题目。题目要求我们按照特定规则重新排列给定的字符串:先从小到大选取字符,再从大到小选取字符,如此交替进行,直到用完所有字符。
这道题出现在LeetCode的字符串分类中,难度标记为简单,但实际考察点非常全面。它不仅能检验开发者对基础数据结构的掌握程度,还能考察对循环控制和边界条件的处理能力。我在第一次遇到这个问题时,本以为10分钟就能解决,结果花了半小时调试各种边界情况——这也是为什么我决定写下这篇详细解析。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题描述与示例分析
2.1 官方题目描述
给定一个字符串s,你需要按照以下步骤重新构造字符串:
- 从s中选出最小的字符,将其接在结果字符串后面
- 从s中剩余字符中选出次小的字符,接在结果字符串后面
- 重复步骤1-2,直到无法继续选择
- 从s中剩余字符中选出最大的字符,接在结果字符串后面
- 从s中剩余字符中选出次大的字符,接在结果字符串后面
- 重复步骤4-5,直到无法继续选择
- 重复步骤1-6,直到用完所有字符
2.2 示例说明
以输入"aaaabbbbcccc"为例:
第一轮上升选择:a(0), a(1), a(2), a(3) → 结果"aaaa",剩余"bbbbcccc"
第一轮下降选择:c(7), c(6), c(5), c(4) → 结果"aaaacccc",剩余"bbbb"
第二轮上升选择:b(4), b(5), b(6), b(7) → 最终结果"aaaaccccbbbb"
这个例子清晰地展示了算法的执行过程。但实际编码时会遇到更复杂的情况,比如:
- 字符全部相同的情况(如"zzzzz")
- 字符完全乱序的情况(如"leetcode")
- 包含大小写混合的情况(题目说明只含小写字母)
3. 核心算法设计与实现
3.1 直观解法:计数排序思路
最直接的解决方案是使用计数排序的思想。因为题目说明字符串只包含小写字母,我们可以创建一个大小为26的数组来统计每个字符出现的次数。
python复制def sortString(s: str) -> str:
count = [0] * 26
for ch in s:
count[ord(ch) - ord('a')] += 1
result = []
while len(result) < len(s):
# 上升阶段
for i in range(26):
if count[i] > 0:
result.append(chr(i + ord('a')))
count[i] -= 1
# 下降阶段
for i in range(25, -1, -1):
if count[i] > 0:
result.append(chr(i + ord('a')))
count[i] -= 1
return ''.join(result)
这个实现的时间复杂度是O(n),空间复杂度是O(1)(因为计数数组大小固定)。它巧妙地利用了字母表有限的特性,避免了复杂的排序操作。
3.2 优化点与边界处理
在实际编码中,有几个关键点需要注意:
- 循环终止条件:while循环的条件是len(result) < len(s),这比检查count数组是否全零更直观
- 字符转换:使用ord()和chr()函数进行ASCII码转换时,要确保索引计算正确
- 空输入处理:虽然题目保证输入非空,但生产代码应该考虑这一情况
- 性能优化:可以记录非零字符的最小和最大范围,减少无效遍历
4. 不同语言实现对比
4.1 C++实现
cpp复制string sortString(string s) {
vector<int> count(26, 0);
for(char c : s) count[c-'a']++;
string res;
while(res.size() < s.size()) {
for(int i=0; i<26; ++i) {
if(count[i]-- > 0) res += 'a'+i;
}
for(int i=25; i>=0; --i) {
if(count[i]-- > 0) res += 'a'+i;
}
}
return res;
}
C++版本注意点:
- vector初始化简洁
- 利用了后置递减运算符的返回值特性
- 字符串拼接效率较低,实际中可以预分配空间
4.2 Java实现
java复制public String sortString(String s) {
int[] count = new int[26];
for(char c : s.toCharArray()) count[c-'a']++;
StringBuilder sb = new StringBuilder();
while(sb.length() < s.length()) {
for(int i=0; i<26; i++) {
if(count[i] > 0) {
sb.append((char)(i+'a'));
count[i]--;
}
}
for(int i=25; i>=0; i--) {
if(count[i] > 0) {
sb.append((char)(i+'a'));
count[i]--;
}
}
}
return sb.toString();
}
Java版本特点:
- 使用StringBuilder提升字符串拼接效率
- 显式的条件判断和递减操作更易读
- 类型转换需要显式处理
5. 常见错误与调试技巧
5.1 典型错误案例
- 索引越界:忘记字符到索引的转换,直接使用字符值作为索引
- 循环条件错误:错误地使用count数组的和作为循环条件,导致提前退出
- 递减时机不当:在append之前递减count,可能跳过有效字符
- 大小写混淆:虽然题目说明是小写字母,但测试时可能误用大写
5.2 调试建议
- 打印中间状态:在每次循环后打印count数组和结果字符串
- 使用简单测试用例:如"a", "ab", "ba", "aaa"等边界情况
- 单元测试:编写多个测试案例验证不同场景
- 可视化跟踪:在纸上手动模拟算法执行过程
6. 算法复杂度与优化空间
6.1 时间复杂度分析
最优解法的时间复杂度是O(n),其中n是字符串长度。这是因为:
- 统计字符频率需要O(n)时间
- 重构字符串时,每个字符恰好被处理一次
- 虽然有两层循环,但内层循环次数固定(26次)
6.2 可能的优化方向
- 减少无效遍历:记录当前非零字符的最小和最大索引,缩小扫描范围
- 批量处理:当某个字符剩余数量很多时,可以一次性添加多个
- 位运算优化:使用位掩码跟踪哪些字符还有剩余
优化后的Python实现示例:
python复制def sortString(s: str) -> str:
count = [0] * 26
for ch in s:
count[ord(ch) - ord('a')] += 1
result = []
remaining = len(s)
while remaining > 0:
# 上升阶段
for i in range(26):
if count[i] > 0:
result.append(chr(i + ord('a')))
count[i] -= 1
remaining -= 1
# 下降阶段
for i in range(25, -1, -1):
if count[i] > 0:
result.append(chr(i + ord('a')))
count[i] -= 1
remaining -= 1
return ''.join(result)
这个版本添加了remaining变量,可以提前退出循环,在某些情况下能减少几次不必要的遍历。
7. 相似题目与扩展思考
7.1 LeetCode相似题目推荐
- 451. Sort Characters By Frequency:按字符频率排序
- 791. Custom Sort String:自定义字符排序规则
- 1054. Distant Barcodes:使相邻元素不同的排列
- 767. Reorganize String:重新组织字符串使相邻字符不同
7.2 问题变种思考
- 如果字符串包含大小写字母,如何处理?
- 如果字符集很大(如Unicode),如何优化?
- 如果排序规则更复杂(如多个上升下降阶段交替),如何设计算法?
- 如果要求原地修改字符串(不使用额外空间),是否可行?
对于第一个变种,解决方案是扩展计数数组大小,并调整字符到索引的映射方式。例如:
python复制def sortStringMixedCase(s: str) -> str:
count = [0] * 52 # 26小写 + 26大写
for ch in s:
if ch.islower():
count[ord(ch) - ord('a')] += 1
else:
count[26 + ord(ch) - ord('A')] += 1
result = []
remaining = len(s)
while remaining > 0:
# 上升阶段
for i in range(52):
if count[i] > 0:
result.append(chr(i + ord('a')) if i < 26 else chr(i - 26 + ord('A')))
count[i] -= 1
remaining -= 1
# 下降阶段
for i in range(51, -1, -1):
if count[i] > 0:
result.append(chr(i + ord('a')) if i < 26 else chr(i - 26 + ord('A')))
count[i] -= 1
remaining -= 1
return ''.join(result)
这个扩展版本可以处理大小写混合的情况,展示了如何调整基础算法来解决更复杂的问题。
