2025-12-23:用特殊操作处理字符串Ⅱ。用go语言,给定一个只包含小写字母和三种特殊符号('*'、'#'、'%')的字符串 s,以及一个整数 k。
按 s 中字符的顺序依次处理,维护并更新一个初始为空的字符串 result:
-
遇到小写字母,就把它接到 result 的末尾;
-
遇到 '*',如果 result 非空则删掉末尾的一个字符;
-
遇到 '#',将当前的 result 再复制一份并拼接到末尾(相当于把 result 重复一次);
-
遇到 '%',把 result 的字符顺序颠倒。
处理完所有字符后,返回 result 中索引为 k 的字符(索引从 0 开始)。如果 k 超出了 result 的长度范围,则返回 '.'。
1 <= s.length <= 100000。
s 只包含小写英文字母和特殊字符 '*'、'#' 和 '%'。
0 <= k <= 1000000000000000。
处理 s 后得到的 result 的长度不超过 1000000000000000。
输入: s = "a#b%*", k = 1。
输出: "a"。
解释:
| i | s[i] | 操作 | 当前 result |
|---|---|---|---|
| 0 | 'a' | 添加 'a' | "a" |
| 1 | '#' | 复制 result | "aa" |
| 2 | 'b' | 添加 'b' | "aab" |
| 3 | '%' | 反转 result | "baa" |
| 4 | '*' | 删除最后一个字符 | "ba" |
最终的 result 是 "ba"。下标为 k = 1 的字符是 'a'。
题目来自力扣3614。
📝 分步处理过程
-
初始状态
- 字符串
s = "a#b%*" - 整数
k = 1 - 用于模拟最终结果字符串长度的变量
sz初始化为0 - 最终需要返回
result字符串中索引为k的字符
- 字符串
-
第一次正向遍历(计算最终字符串长度
sz) 程序首先从头到尾遍历字符串s的每个字符,根据规则更新sz。这个sz代表了如果完全模拟构建result字符串,其最终的长度。- 处理字符
'a'(索引0): 这是一个小写字母。规则是将其添加到result末尾。因此,sz增加1,变为1。 - 处理字符
'#'(索引1): 这是特殊符号#。规则是将当前result复制一份拼接到末尾(即长度翻倍)。因此,sz乘以2,变为1 * 2 = 2。 - 处理字符
'b'(索引2): 这是一个小写字母。规则是将其添加到result末尾。因此,sz增加1,变为3。 - 处理字符
'%'(索引3): 这是特殊符号%。规则是将result的字符顺序颠倒。重要的是,这个操作并不改变result的长度。因此,sz保持不变,仍为3。 - 处理字符
'*'(索引4): 这是特殊符号*。规则是如果result非空则删除末尾一个字符。此时sz为3(非空),所以sz减少1,变为2。这里使用max(sz-1, 0)是为了确保长度不会变为负数。
第一次遍历结束后,我们得到最终
result字符串的长度sz = 2。因为k = 1小于sz = 2,所以我们需要找出这个位置的字符是什么。如果k >= sz,函数会直接返回'.'。 - 处理字符
-
第二次反向遍历(定位第k个字符) 程序接着从字符串
s的末尾开始,向前(向左)遍历每个字符。这次遍历的目的是反向模拟构建过程,从而直接确定在最终result中索引k(即第1个位置,从0开始计数)上的字符,而无需真正构建出整个可能非常庞大的字符串。- 初始状态: 当前考虑的字符串范围是最终的
result(长度为sz = 2),我们关注的位置是k = 1。 - 处理字符
'*'(索引4): 这个操作在正向处理时是删除末尾字符。那么反向推理时,这个被删除的字符需要被“恢复”回来。因此,sz增加1,变为3。注意:此时我们只是知道了原来有一个字符被删了,但还没有直接定位到k。 - 处理字符
'%'(索引3): 这个操作在正向处理时是反转字符串。反向推理时,再次反转可以还原。反转操作有一个关键特性:反转后,原始索引为i的字符,在新字符串中的索引会变为sz - 1 - i。因此,为了找出在当前反转操作之前,我们关心的字符在哪个位置,需要应用这个逆变换:k被更新为sz - 1 - k。代入当前值sz=3,k=1,得到新的k = 3 - 1 - 1 = 1。 - 处理字符
'b'(索引2): 这是一个小写字母,它是正向处理时被添加到末尾的。反向推理时,我们需要判断这个字符是否就是我们正在寻找的k位置的字符。- 此时
result的长度sz需要减1,变回2(因为添加'b'是最后一步,反向时先遇到它,所以要“移除”)。 - 现在判断:当前的
k(值为1)是否等于移除字母'b'之后的字符串长度(即sz=2)?如果相等,说明这个'b'正是最终result里k位置的字符。但此时k=1不等于sz=2。 - 既然不相等,说明
'b'不是我们要找的字符,我们继续向前寻找。k值保持不变。
- 此时
- 处理字符
'#'(索引1): 这个操作在正向处理时是复制字符串(长度翻倍)。反向推理时,字符串长度需要减半,sz变为2 / 2 = 1。- 这里有一个关键逻辑:翻倍后的字符串可以看作由两个原始字符串拼接而成(假设叫 PartA 和 PartB)。我们需要确定
k位于哪个部分。 - 如果
k的值大于等于当前sz(即 PartA 的长度),说明目标字符在 PartB 中。那么我们需要将k减去sz,从而在 PartB 中重新定位,然后继续向前追溯。在这个例子中,k=1,sz=1,满足k >= sz。所以,k被更新为k - sz = 1 - 1 = 0。这意味着我们要在 PartB 对应的原始字符串中找第0个字符。 - 如果
k小于sz,说明目标字符在 PartA 中,k值不变,继续追溯。
- 这里有一个关键逻辑:翻倍后的字符串可以看作由两个原始字符串拼接而成(假设叫 PartA 和 PartB)。我们需要确定
- 处理字符
'a'(索引0): 这是一个小写字母。sz减1,变为0。- 判断
k(当前为0)是否等于新的sz(0)。相等! 这意味着,这个'a'就是我们要找的字符。因此,函数返回'a'。
- 初始状态: 当前考虑的字符串范围是最终的
⏱️ 时间复杂度分析
- 该算法对输入字符串
s进行了两次遍历。- 第一次是正向遍历,计算最终长度
sz。 - 第二次是反向遍历,定位第
k个字符。
- 第一次是正向遍历,计算最终长度
- 每次遍历都只处理每个字符一次,并且在每个字符上的操作都是常数时间(如比较、加减、乘除、条件判断)。即使有
%反转操作,也只是通过数学计算更新k的值,并不实际反转字符串。 - 因此,总的时间复杂度为 O(n),其中
n是字符串s的长度 。这与字符串最终可能产生的巨大长度无关,效率非常高。
💾 空间复杂度分析
- 算法在运行过程中只使用了固定数量的额外变量,如
sz,k, 循环索引i等。这些变量的空间占用与输入字符串s的长度n或最终结果字符串的长度无关。 - 它没有显式地构建最终的结果字符串
result,从而避免了可能高达 O(2^n) 的空间开销。 - 因此,总的额外空间复杂度为 O(1),即常数空间 。
💎 总结
这个算法的巧妙之处在于通过两次线性遍历和数学映射,避开了直接处理可能指数级增长的字符串,从而在线性时间和常数空间内高效地解决了问题。
Go完整代码如下:
package main
import (
"fmt"
)
func processStr(s string, k int64) byte {
sz := int64(0)
for _, c := range s {
if c == '*' {
sz = max(sz-1, 0)
} else if c == '#' {
sz *= 2
} else if c != '%' {
sz++
}
}
if k >= sz {
return '.'
}
for i := len(s) - 1; ; i-- {
c := s[i]
if c == '*' {
sz++
} else if c == '#' {
sz /= 2
if k >= sz {
k -= sz
}
} else if c == '%' {
k = sz - 1 - k
} else {
sz--
if k == sz {
return c
}
}
}
}
func main() {
s := "a#b%*"
k := 1
result := processStr(s, int64(k))
fmt.Println(string([]byte{result}))
}
Python完整代码如下:
# -*-coding:utf-8-*-
def process_str(s: str, k: int) -> str:
"""
处理特殊字符串并返回第k个字符
规则:
- 普通字符:长度+1
- '*':长度-1(最少为0)
- '#':长度×2
- '%':不影响长度计算
正向计算最终长度后,反向定位第k个字符
"""
# 正向计算最终长度
sz = 0
for c in s:
if c == '*':
sz = max(sz - 1, 0)
elif c == '#':
sz *= 2
elif c != '%':
sz += 1
# 如果k超出范围,返回'.'
if k >= sz:
return '.'
# 反向定位字符
# 注意:这里k是我们要找的索引位置
for c in reversed(s):
if c == '*':
sz += 1
elif c == '#':
half = sz // 2
if k >= half:
k -= half
sz = half
elif c == '%':
k = sz - 1 - k
else:
sz -= 1
if k == sz:
return c
return '.' # 正常情况下不会执行到这里
def main() -> None:
s = "a#b%*"
k = 1
result = process_str(s, k)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <iostream>
#include <string>
#include <algorithm>
#include <cstdint>
char processStr(const std::string& s, int64_t k) {
int64_t sz = 0;
// 正向计算最终长度
for (char c : s) {
if (c == '*') {
sz = std::max(sz - 1, (int64_t)0);
} else if (c == '#') {
sz *= 2;
} else if (c != '%') {
sz++;
}
}
// 如果k超出范围,返回'.'
if (k >= sz) {
return '.';
}
// 反向定位字符
for (int i = s.length() - 1; ; i--) {
char c = s[i];
if (c == '*') {
sz++;
} else if (c == '#') {
sz /= 2;
if (k >= sz) {
k -= sz;
}
} else if (c == '%') {
k = sz - 1 - k;
} else {
sz--;
if (k == sz) {
return c;
}
}
}
return '.'; // 正常情况下不会执行到这里
}
int main() {
std::string s = "a#b%*";
int64_t k = 1;
char result = processStr(s, k);
std::cout << result << std::endl;
return 0;
}