1. 题目解析与需求拆解
1370题"上升下降字符串"是一道典型的字符串处理题目,题目要求我们按照特定规则重新排列字符串。具体规则如下:
- 从字符串中选出最小的字符,加到结果字符串中
- 再从剩余字符中选出次小的字符,加到结果字符串中
- 重复此过程直到没有字符剩余
- 然后从剩余字符中选出最大的字符,加到结果字符串中
- 接着选出次大的字符
- 重复此过程直到没有字符剩余
- 重复步骤1-6直到所有字符都被使用
这个规则可以简称为"先升序后降序"的交替选取策略。举个例子,对于输入字符串"aaaabbbbcccc",输出应该是"abccbaabccba"。
1.1 核心问题分析
这道题的核心在于理解并实现这个特殊的排序规则。我们需要关注几个关键点:
- 如何高效地获取当前最小/最大字符
- 如何处理字符的重复出现
- 如何实现升序和降序的交替选取
- 如何判断所有字符是否已被使用
提示:这类字符串重排问题通常需要考虑字符频率统计,这道题也不例外。使用哈希表记录字符出现次数是常见的解决方案。
2. 算法设计与实现思路
2.1 基础解法:桶排序思想
最直观的解法是使用桶排序的思想:
- 创建一个大小为26的数组(对应26个字母)来统计每个字符的出现次数
- 按照题目要求的顺序遍历这个数组:
- 先从左到右(升序)选取字符
- 再从右到左(降序)选取字符
- 每次选取一个字符后,减少其计数
- 重复这个过程直到所有字符计数为0
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)
2.2 时间复杂度分析
这个算法的时间复杂度是O(n),其中n是字符串的长度。因为:
- 统计字符频率:O(n)
- 构建结果字符串:外层循环最多执行n/26次(每次至少处理26个字符),内层是固定26次循环,所以也是O(n)
空间复杂度是O(1),因为我们只使用了固定大小的数组(26个元素)。
3. 优化与变种解法
3.1 使用优先队列的解法
虽然桶排序解法已经很高效,但我们也可以使用优先队列(堆)来实现:
python复制import heapq
def sortString(s: str) -> str:
heap = []
for ch in s:
heapq.heappush(heap, ord(ch))
result = []
direction = 1 # 1表示升序,-1表示降序
temp = []
while heap:
if direction == 1:
curr = heapq.heappop(heap)
if not result or curr != ord(result[-1]):
result.append(chr(curr))
else:
temp.append(curr)
else:
# 降序需要特殊处理
pass # 实际实现会更复杂
if not heap:
heap = temp
temp = []
direction *= -1
return ''.join(result)
注意:这个解法在实际实现上会比桶排序复杂,特别是处理降序部分时。因此在实际应用中,桶排序解法更为推荐。
3.2 递归解法
我们也可以考虑递归实现,虽然这不是最优解,但有助于理解问题:
python复制def sortString(s: str) -> str:
def helper(s, result, ascending):
if not s:
return result
unique_chars = sorted(set(s), reverse=not ascending)
new_result = result + ''.join(unique_chars)
remaining = []
count = {ch: s.count(ch) for ch in set(s)}
for ch in s:
if count[ch] > 1 or ch not in unique_chars:
remaining.append(ch)
count[ch] -= 1
return helper(remaining, new_result, not ascending)
return helper(s, "", True)
这个递归解法虽然直观,但效率较低,因为每次都要重新计算字符频率和排序。
4. 边界条件与测试用例
4.1 常见测试用例
好的测试应该覆盖以下情况:
- 常规情况:"aaaabbbbcccc" → "abccbaabccba"
- 单个字符:"z" → "z"
- 所有字符相同:"aaaaa" → "aaaaa"
- 已经有序的字符串:"abc" → "abc"
- 逆序字符串:"cba" → "abc"
- 空字符串:"" → ""
- 大小写混合(假设题目只要求小写):"aAbBcC" → "abcCBA"(如果题目允许大写)
4.2 特殊边界处理
在实际编码中需要注意:
- 空字符串输入
- 字符串长度为1的情况
- 所有字符相同的情况
- 字符频率为1的情况
- 非常大的输入(测试算法效率)
5. 实际编码中的技巧与陷阱
5.1 性能优化技巧
-
提前终止循环:可以在每次升序/降序遍历后检查是否所有字符都已用完,避免不必要的遍历。
python复制total = len(s) while len(result) < total: # ...原有逻辑... if len(result) == total: break -
减少字符转换:可以一直使用ASCII码操作,最后一次性转换为字符串。
-
并行处理:对于超大字符串,可以考虑并行处理不同字符区段(虽然这道题可能不需要)。
5.2 常见错误与调试
-
无限循环:忘记更新字符计数或终止条件,导致循环无法结束。
调试技巧:在循环中加入打印语句,输出当前字符计数和结果字符串长度。
-
顺序错误:升序和降序的逻辑写反了。
检查方法:用简单测试用例如"abc"验证输出顺序。
-
重复字符处理不当:没有正确处理相同字符连续出现的情况。
验证方法:测试所有字符相同的输入。
-
边界条件遗漏:没有处理空字符串或单字符字符串。
防御性编程:在函数开始处添加边界条件检查。
6. 算法应用与扩展思考
6.1 实际应用场景
这种字符串重排算法可以应用于:
- 数据加密:创建特定的排列模式
- 测试用例生成:验证排序算法的稳定性
- 游戏开发:特殊动画效果的字符序列生成
- 数据压缩:特定模式的字符串可能更易压缩
6.2 类似题目推荐
-
自定义排序规则:
-
- Largest Number
-
- Longest Word in Dictionary through Deleting
-
-
字符串重构:
-
- Reorganize String
-
- Distant Barcodes
-
-
频率统计类:
-
- Sort Characters By Frequency
-
- Top K Frequent Elements
-
6.3 进阶挑战
尝试解决以下变种问题:
- 如果字符串包含大写字母和小写字母,如何处理?
- 如果排序规则改为"先降序后升序",如何修改代码?
- 如果每次选取要求严格大于/小于前一个字符(不能等于),如何实现?
- 如果字符串包含Unicode字符(而不仅仅是小写字母),算法应该如何调整?
7. 不同语言实现对比
7.1 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();
}
7.2 C++实现
cpp复制string sortString(string s) {
int count[26] = {0};
for (char c : s) {
count[c - 'a']++;
}
string result;
while (result.size() < s.size()) {
// 升序
for (int i = 0; i < 26; ++i) {
if (count[i] > 0) {
result += 'a' + i;
count[i]--;
}
}
// 降序
for (int i = 25; i >= 0; --i) {
if (count[i] > 0) {
result += 'a' + i;
count[i]--;
}
}
}
return result;
}
7.3 JavaScript实现
javascript复制function sortString(s) {
const count = new Array(26).fill(0);
for (const c of s) {
count[c.charCodeAt() - 'a'.charCodeAt()]++;
}
let result = [];
while (result.length < s.length) {
// 升序
for (let i = 0; i < 26; i++) {
if (count[i] > 0) {
result.push(String.fromCharCode(i + 'a'.charCodeAt()));
count[i]--;
}
}
// 降序
for (let i = 25; i >= 0; i--) {
if (count[i] > 0) {
result.push(String.fromCharCode(i + 'a'.charCodeAt()));
count[i]--;
}
}
}
return result.join('');
}
8. 个人解题心得
在实际解决这个问题时,我有以下几点体会:
-
理解题意是关键:最初我误解了题目要求,以为只需要简单的升序降序交替,忽略了每次都要从剩余字符中选取最小/最大的要求。仔细阅读题目描述和示例非常重要。
-
可视化帮助大:在纸上画出字符选取的过程,特别是对于示例"aaaabbbbcccc",手动模拟输出"abccbaabccba"的过程,大大加深了我对算法的理解。
-
从暴力法开始:先实现一个直观但可能低效的解法(如递归解法),再逐步优化,这种方法往往比直接尝试最优解更有效。
-
测试驱动开发:编写多个测试用例,特别是边界情况,在实现过程中不断验证,可以避免很多错误。
-
空间换时间:使用额外的计数数组(桶排序思想)虽然增加了少量空间复杂度,但显著提高了时间效率,这是算法设计中常见的权衡。
这道题看似简单,但真正高效且正确地实现它,需要考虑很多细节。在实际面试中,除了写出代码,还需要清楚地解释算法选择和复杂度分析,这也是我平时练习的重点。