2025-12-23:用特殊操作处理字符串Ⅱ。用go语言,给定一个只包含小写字母和三种特殊符号(‘*‘、‘#‘、‘%‘)的字符串 s,以及一个整数 k。 按 s

19 阅读8分钟

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"。

解释:

is[i]操作当前 result
0'a'添加 'a'"a"
1'#'复制 result"aa"
2'b'添加 'b'"aab"
3'%'反转 result"baa"
4'*'删除最后一个字符"ba"

最终的 result 是 "ba"。下标为 k = 1 的字符是 'a'。

题目来自力扣3614。

📝 分步处理过程

  1. 初始状态

    • 字符串 s = "a#b%*"
    • 整数 k = 1
    • 用于模拟最终结果字符串长度的变量 sz 初始化为 0
    • 最终需要返回 result 字符串中索引为 k 的字符
  2. 第一次正向遍历(计算最终字符串长度 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,函数会直接返回 '.'

  3. 第二次反向遍历(定位第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' 正是最终 resultk 位置的字符。但此时 k=1 不等于 sz=2
      • 既然不相等,说明 'b' 不是我们要找的字符,我们继续向前寻找。k 值保持不变。
    • 处理字符 '#' (索引1): 这个操作在正向处理时是复制字符串(长度翻倍)。反向推理时,字符串长度需要减半,sz 变为 2 / 2 = 1
      • 这里有一个关键逻辑:翻倍后的字符串可以看作由两个原始字符串拼接而成(假设叫 PartA 和 PartB)。我们需要确定 k 位于哪个部分。
      • 如果 k 的值大于等于当前 sz(即 PartA 的长度),说明目标字符在 PartB 中。那么我们需要将 k 减去 sz,从而在 PartB 中重新定位,然后继续向前追溯。在这个例子中,k=1sz=1,满足 k >= sz。所以,k 被更新为 k - sz = 1 - 1 = 0。这意味着我们要在 PartB 对应的原始字符串中找第0个字符。
      • 如果 k 小于 sz,说明目标字符在 PartA 中,k 值不变,继续追溯。
    • 处理字符 '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;
}

在这里插入图片描述