LeetCode Go · 字符串(4) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 30 题 · 点击题目展开,先独立思考,再查看解法。
2609. 最长平衡子字符串 ​
题目、示例与约束
给你一个仅由 0 和 1 组成的二进制字符串 s 。
如果子字符串中 所有的 0 都在 1 之前 且其中 0 的数量等于 1 的数量,则认为 s 的这个子字符串是平衡子字符串。请注意,空子字符串也视作平衡子字符串。
返回 s 中最长的平衡子字符串长度。
子字符串是字符串中的一个连续字符序列。
示例 1:
输入:s = "01000111" 输出:6 解释:最长的平衡子字符串是 "000111" ,长度为 6 。
示例 2:
输入:s = "00111"
输出:4
解释:最长的平衡子字符串是 "0011" ,长度为 4 。
示例 3:
输入:s = "111" 输出:0 解释:除了空子字符串之外不存在其他平衡子字符串,所以答案为 0 。
提示:
1 <= s.length <= 50'0' <= s[i] <= '1'
方法一:暴力枚举 · Go 实现
思考
平衡子串形态固定为若干 后接同样多的 。 ,枚举全部 子串再 检查,总时间 可通过。
对每个区间统计 的个数,并保证所有 都出现在第一个 之前,且 的个数恰为长度一半。
注意到数据范围很小,因此,我们可以枚举所有的子串 ,检查其是否为平衡子串,如果是,则更新答案。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func findTheLongestBalancedSubstring(s string) (ans int) {
n := len(s)
check := func(i, j int) bool {
cnt := 0
for k := i; k <= j; k++ {
if s[k] == '1' {
cnt++
} else if cnt > 0 {
return false
}
}
return cnt*2 == j-i+1
}
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if check(i, j) {
ans = max(ans, j-i+1)
}
}
}
return
}
方法二:枚举优化 · Go 实现
思考
方法一立方扫区间。合法串只能由一段连续 紧接一段连续 构成,因此只需维护当前这两段的长度。
遇 时若已有 则开启新段;遇 则用 更新答案。一次遍历即可。
我们用变量 和 分别记录当前连续的 和 的个数。
遍历字符串 ,对于当前字符 :
- 如果当前字符为
'0',我们判断此时 是否大于 ,是则将 和 重置为 ,接下来将 加 。 - 如果当前字符为
'1',则将 加 ,并更新答案为 。
遍历结束后,即可得到最长的平衡子串的长度。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func findTheLongestBalancedSubstring(s string) (ans int) {
zero, one := 0, 0
for _, c := range s {
if c == '0' {
if one > 0 {
zero, one = 0, 0
}
zero++
} else {
one++
ans = max(ans, 2*min(zero, one))
}
}
return
}
2678. 老人的数目 ​
题目、示例与约束
给你一个下标从 0 开始的字符串数组 details 。details 中每个元素都是一位乘客的信息,信息用长度为 15 的字符串表示,表示方式如下:
- 前十个字符是乘客的手机号码。
- 接下来的一个字符是乘客的性别。
- 接下来两个字符是乘客的年龄。
- 最后两个字符是乘客的座位号。
请你返回乘客中年龄 严格大于 60 岁 的人数。
示例 1:
输入:details = ["7868190130M7522","5303914400F9211","9273338290F4010"] 输出:2 解释:下标为 0 ,1 和 2 的乘客年龄分别为 75 ,92 和 40 。所以有 2 人年龄大于 60 岁。
示例 2:
输入:details = ["1313579440F2036","2921522980M5644"] 输出:0 解释:没有乘客的年龄大于 60 岁。
提示:
1 <= details.length <= 100details[i].length == 15details[i]中的数字只包含'0'到'9'。details[i][10]是'M','F'或者'O'之一。- 所有乘客的手机号码和座位号互不相同。
方法一:遍历计数 · Go 实现
思考
每条记录定长,年龄固定占第 、 位。抽出这两位转整数后与 比较即可计数,不必解析其余字段。
我们可以遍历 details 中的每个字符串 ,并将 的第 和第 个字符(下标为 , )转换为整数,判断是否大于 ,如果是则将答案加一。
遍历结束后,返回答案即可。
时间复杂度 ,其中 是 details 的长度。空间复杂度 。
func countSeniors(details []string) (ans int) {
for _, x := range details {
age, _ := strconv.Atoi(x[11:13])
if age > 60 {
ans++
}
}
return
}
2710. 移除字符串中的尾随零 ​
题目、示例与约束
给你一个用字符串表示的正整数 num ,请你以字符串形式返回不含尾随零的整数 num 。
示例 1:
输入:num = "51230100" 输出:"512301" 解释:整数 "51230100" 有 2 个尾随零,移除并返回整数 "512301" 。
示例 2:
输入:num = "123" 输出:"123" 解释:整数 "123" 不含尾随零,返回整数 "123" 。
提示:
1 <= num.length <= 1000num仅由数字0到9组成num不含前导零
方法一:遍历 · Go 实现
思考
只需去掉十进制表示末尾的 ,中间与开头的 都要保留。从左扫描无法在见到末尾前判断哪些 该删。
从右剥掉连续的 即得结果,对应一次 。
我们可以从后往前遍历字符串,遇到第一个不是 0 的字符时停止遍历,然后返回从头开始到这个字符的子串。
时间复杂度 ,其中 是字符串的长度。忽略答案字符串的空间消耗,空间复杂度 。
func removeTrailingZeros(num string) string {
i := len(num) - 1
for num[i] == '0' {
i--
}
return num[:i+1]
}
2788. 按分隔符拆分字符串 ​
题目、示例与约束
给你一个字符串数组 words 和一个字符 separator ,请你按 separator 拆分 words 中的每个字符串。
返回一个由拆分后的新字符串组成的字符串数组,不包括空字符串 。
注意
separator用于决定拆分发生的位置,但它不包含在结果字符串中。- 拆分可能形成两个以上的字符串。
- 结果字符串必须保持初始相同的先后顺序。
示例 1:
输入:words = ["one.two.three","four.five","six"], separator = "." 输出:["one","two","three","four","five","six"] 解释:在本示例中,我们进行下述拆分: "one.two.three" 拆分为 "one", "two", "three" "four.five" 拆分为 "four", "five" "six" 拆分为 "six" 因此,结果数组为 ["one","two","three","four","five","six"] 。
示例 2:
输入:words = ["$easy$","$problem$"], separator = "$" 输出:["easy","problem"] 解释:在本示例中,我们进行下述拆分: "$easy$" 拆分为 "easy"(不包括空字符串) "$problem$" 拆分为 "problem"(不包括空字符串) 因此,结果数组为 ["easy","problem"] 。
示例 3:
输入:words = ["|||"], separator = "|" 输出:[] 解释:在本示例中,"|||" 的拆分结果将只包含一些空字符串,所以我们返回一个空数组 [] 。
提示:
1 <= words.length <= 1001 <= words[i].length <= 20words[i]中的字符要么是小写英文字母,要么就是字符串".,|$#@"中的字符(不包括引号)separator是字符串".,|$#@"中的某个字符(不包括引号)
方法一:模拟 · Go 实现
思考
用给定分隔符拆分每个字符串,丢掉空段。手写扫描找分隔符与调用 后过滤空串等价,后者更短。
对 中每个串 ,只保留非空片段。
我们遍历字符串数组 ,对于每个字符串 ,我们使用 separator 作为分隔符进行拆分,如果拆分后的字符串不为空,则将其加入答案数组。
时间复杂度 ,空间复杂度 ,其中 是字符串数组 的长度,而 是字符串数组 中字符串的最大长度。
func splitWordsBySeparator(words []string, separator byte) (ans []string) {
for _, w := range words {
for _, s := range strings.Split(w, string(separator)) {
if s != "" {
ans = append(ans, s)
}
}
}
return
}
2810. 故障键盘 ​
题目、示例与约束
你的笔记本键盘存在故障,每当你在上面输入字符 'i' 时,它会反转你所写的字符串。而输入其他字符则可以正常工作。
给你一个下标从 0 开始的字符串 s ,请你用故障键盘依次输入每个字符。
返回最终笔记本屏幕上输出的字符串。
示例 1:
输入:s = "string" 输出:"rtsng" 解释: 输入第 1 个字符后,屏幕上的文本是:"s" 。 输入第 2 个字符后,屏幕上的文本是:"st" 。 输入第 3 个字符后,屏幕上的文本是:"str" 。 因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "rts" 。 输入第 5 个字符后,屏幕上的文本是:"rtsn" 。 输入第 6 个字符后,屏幕上的文本是: "rtsng" 。 因此,返回 "rtsng" 。
示例 2:
输入:s = "poiinter" 输出:"ponter" 解释: 输入第 1 个字符后,屏幕上的文本是:"p" 。 输入第 2 个字符后,屏幕上的文本是:"po" 。 因为第 3 个字符是 'i' ,屏幕上的文本被反转,变成 "op" 。 因为第 4 个字符是 'i' ,屏幕上的文本被反转,变成 "po" 。 输入第 5 个字符后,屏幕上的文本是:"pon" 。 输入第 6 个字符后,屏幕上的文本是:"pont" 。 输入第 7 个字符后,屏幕上的文本是:"ponte" 。 输入第 8 个字符后,屏幕上的文本是:"ponter" 。 因此,返回 "ponter" 。
提示:
1 <= s.length <= 100s由小写英文字母组成s[0] != 'i'
方法一:模拟 · Go 实现
思考
每次遇到字符
i便反转当前屏幕文本, 不大,直接用列表模拟追加与整段反转即可。无需引入双端队列来优化常数。
我们直接模拟键盘的输入过程,用一个字符数组 来记录屏幕上的文本,初始时 为空。
对于字符串 中的每个字符 ,如果 不是字符 ,那么我们将 加入到 的末尾;否则我们将 中的所有字符反转。
最终答案即为 中的字符组成的字符串。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
func finalString(s string) string {
t := []rune{}
for _, c := range s {
if c == 'i' {
for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {
t[i], t[j] = t[j], t[i]
}
} else {
t = append(t, c)
}
}
return string(t)
}
2828. 判别首字母缩略词 ​
题目、示例与约束
给你一个字符串数组 words 和一个字符串 s ,请你判断 s 是不是 words 的 首字母缩略词 。
如果可以按顺序串联 words 中每个字符串的第一个字符形成字符串 s ,则认为 s 是 words 的首字母缩略词。例如,"ab" 可以由 ["apple", "banana"] 形成,但是无法从 ["bear", "aardvark"] 形成。
如果 s 是 words 的首字母缩略词,返回 true ;否则,返回 false 。
示例 1:
输入:words = ["alice","bob","charlie"], s = "abc" 输出:true 解释:words 中 "alice"、"bob" 和 "charlie" 的第一个字符分别是 'a'、'b' 和 'c'。因此,s = "abc" 是首字母缩略词。
示例 2:
输入:words = ["an","apple"], s = "a" 输出:false 解释:words 中 "an" 和 "apple" 的第一个字符分别是 'a' 和 'a'。 串联这些字符形成的首字母缩略词是 "aa" 。 因此,s = "a" 不是首字母缩略词。
示例 3:
输入:words = ["never","gonna","give","up","on","you"], s = "ngguoy" 输出:true 解释:串联数组 words 中每个字符串的第一个字符,得到字符串 "ngguoy" 。 因此,s = "ngguoy" 是首字母缩略词。
提示:
1 <= words.length <= 1001 <= words[i].length <= 101 <= s.length <= 100words[i]和s由小写英文字母组成
方法一:模拟 · Go 实现
思考
首字母缩略词即每个单词首字符的拼接。一次遍历取出首字母并与 比较即可。
我们可以遍历数组 中的每个字符串,将其首字母拼接起来,得到一个新的字符串 ,然后判断 是否等于 即可。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func isAcronym(words []string, s string) bool {
t := []byte{}
for _, w := range words {
t = append(t, w[0])
}
return string(t) == s
}
方法二:模拟(空间优化) · Go 实现
思考
方法一额外构造了长度为 的字符串。先比较 与 的长度,再按下标逐个核对该词首字母,可将额外空间降为常数。
我们首先判断 中的字符串个数是否等于 的长度,如果不等于,那么 一定不是 的首字母缩略词,直接返回 。
然后我们遍历 的每个字符,判断其是否等于 中对应字符串的首字母,如果不等于,那么 一定不是 的首字母缩略词,直接返回 。
遍历结束后,如果没有返回 ,那么 就是 的首字母缩略词,返回 。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
func isAcronym(words []string, s string) bool {
if len(words) != len(s) {
return false
}
for i := range s {
if words[i][0] != s[i] {
return false
}
}
return true
}
2833. 距离原点最远的点 ​
题目、示例与约束
给你一个长度为 n 的字符串 moves ,该字符串仅由字符 'L'、'R' 和 '_' 组成。字符串表示你在一条原点为 0 的数轴上的若干次移动。
你的初始位置就在原点(0),第 i 次移动过程中,你可以根据对应字符选择移动方向:
- 如果
moves[i] = 'L'或moves[i] = '_',可以选择向左移动一个单位距离 - 如果
moves[i] = 'R'或moves[i] = '_',可以选择向右移动一个单位距离
返回在移动 n 次之后,可以到达的距离原点 最远 的点 到原点的距离。
示例 1:
输入:moves = "L_RL__R" 输出:3 解释:可以到达的距离原点 0 最远的点是 -3 ,移动的序列为 "LLRLLLR" 。
示例 2:
输入:moves = "_R__LL_" 输出:5 解释:可以到达的距离原点 0 最远的点是 -5 ,移动的序列为 "LRLLLLL" 。
示例 3:
输入:moves = "_______" 输出:7 解释:可以到达的距离原点 0 最远的点是 7 ,移动的序列为 "RRRRRRR" 。
提示:
1 <= moves.length == n <= 50moves仅由字符'L'、'R'和'_'组成
方法一:贪心 · Go 实现
思考
下划线可任意走向,最远距离在把全部空位加到较多的那一侧时取到。因而答案是 加上下划线个数,不必分别模拟两种定向。
遇到字符 '_' 时,我们可以选择向左或向右移动,而题目需要我们求出离原点最远的点,因此,我们可以先进行一次遍历,贪心地把所有的 '_' 都移到左边,求出此时离原点最远的点,再进行一次遍历,贪心地把所有的 '_' 都移到右边,求出此时离原点最远的点,最后取两次遍历中的最大值即可。
进一步地,我们只需要统计出字符串中 'L'、'R' 的个数之差,再加上 '_' 的个数即可。
时间复杂度 ,其中 为字符串的长度。空间复杂度 。
func furthestDistanceFromOrigin(moves string) int {
count := func(c string) int { return strings.Count(moves, c) }
return abs(count("L")-count("R")) + count("_")
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
2839. 判断通过操作能否让字符串相等 I ​
题目、示例与约束
给你两个字符串 s1 和 s2 ,两个字符串的长度都为 4 ,且只包含 小写 英文字母。
你可以对两个字符串中的 任意一个 执行以下操作 任意 次:
- 选择两个下标
i和j且满足j - i = 2,然后 交换 这个字符串中两个下标对应的字符。
如果你可以让字符串 s1 和 s2 相等,那么返回 true ,否则返回 false 。
示例 1:
输入:s1 = "abcd", s2 = "cdab" 输出:true 解释: 我们可以对 s1 执行以下操作: - 选择下标 i = 0 ,j = 2 ,得到字符串 s1 = "cbad" 。 - 选择下标 i = 1 ,j = 3 ,得到字符串 s1 = "cdab" = s2 。
示例 2:
输入:s1 = "abcd", s2 = "dacb" 输出:false 解释:无法让两个字符串相等。
提示:
s1.length == s2.length == 4s1和s2只包含小写英文字母。
方法一:计数 · Go 实现
思考
长度为 时,可交换的下标必须同奇偶,故偶数位与奇数位各自构成可任意重排的两组。分别统计两组字符频次,两组均一致则可变得相等。
我们观察题目中的操作,可以发现,如果字符串的两个下标 和 的奇偶性相同,那么它们可以通过交换改变顺序。
因此,我们可以统计两个字符串中奇数下标的字符的出现次数,以及偶数下标的字符的出现次数,如果两个字符串的统计结果相同,那么我们就可以通过操作使得两个字符串相等。
时间复杂度 ,空间复杂度 。其中 是字符串的长度,而 是字符集。
相似题目:
func canBeEqual(s1 string, s2 string) bool {
cnt := [2][26]int{}
for i := 0; i < len(s1); i++ {
cnt[i&1][s1[i]-'a']++
cnt[i&1][s2[i]-'a']--
}
for i := 0; i < 26; i++ {
if cnt[0][i] != 0 || cnt[1][i] != 0 {
return false
}
}
return true
}
2914. 使二进制字符串变美丽的最少修改次数 ​
题目、示例与约束
给你一个长度为偶数下标从 0 开始的二进制字符串 s 。
如果可以将一个字符串分割成一个或者更多满足以下条件的子字符串,那么我们称这个字符串是 美丽的 :
- 每个子字符串的长度都是 偶数 。
- 每个子字符串都 只 包含
1或 只 包含0。
你可以将 s 中任一字符改成 0 或者 1 。
请你返回让字符串 s 美丽的 最少 字符修改次数。
示例 1:
输入:s = "1001" 输出:2 解释:我们将 s[1] 改为 1 ,且将 s[3] 改为 0 ,得到字符串 "1100" 。 字符串 "1100" 是美丽的,因为我们可以将它分割成 "11|00" 。 将字符串变美丽最少需要 2 次修改。
示例 2:
输入:s = "10" 输出:1 解释:我们将 s[1] 改为 1 ,得到字符串 "11" 。 字符串 "11" 是美丽的,因为它已经是美丽的。 将字符串变美丽最少需要 1 次修改。
示例 3:
输入:s = "0000" 输出:0 解释:不需要进行任何修改,字符串 "0000" 已经是美丽字符串。
提示:
2 <= s.length <= 105s的长度为偶数。s[i]要么是'0',要么是'1'。
方法一:计数 · Go 实现
思考
美丽串须能分成若干长度为 的相等片段,且划分是固定的: 、 等互不影响。跨对修改不会减少某一对内部的代价。
因此只需检查每个奇数下标是否与前一字符相同,不同则改一次。 ,一次步长为 的扫描即可。
我们只需要遍历字符串 的所有奇数下标 ,如果当前奇数下标与前一个下标的字符不同,即 ,那么就需要修改当前字符,使得 。因此,此时答案需要加 。
遍历结束后,返回答案即可。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func minChanges(s string) (ans int) {
for i := 1; i < len(s); i += 2 {
if s[i] != s[i-1] {
ans++
}
}
return
}
2937. 使三个字符串相等 ​
题目、示例与约束
给你三个字符串 s1、s2 和 s3。 你可以根据需要对这三个字符串执行以下操作 任意次数 。
在每次操作中,你可以选择其中一个长度至少为 2 的字符串 并删除其 最右位置上 的字符。
如果存在某种方法能够使这三个字符串相等,请返回使它们相等所需的 最小 操作次数;否则,返回 -1。
示例 1:
输入:s1 = "abc",s2 = "abb",s3 = "ab" 输出:2 解释:对 s1 和 s2 进行一次操作后,可以得到三个相等的字符串。 可以证明,不可能用少于两次操作使它们相等。
示例 2:
输入:s1 = "dac",s2 = "bac",s3 = "cac" 输出:-1 解释:因为 s1 和 s2 的最左位置上的字母不相等,所以无论进行多少次操作,它们都不可能相等。因此答案是 -1 。
提示:
1 <= s1.length, s2.length, s3.length <= 100s1、s2和s3仅由小写英文字母组成。
方法一:枚举 · Go 实现
思考
每次只能删某一串的最后一个字符,最终三串相等当且仅当它们共享一段非空公共前缀,且该前缀即最终串。枚举前缀长度 :三位字符首次分叉时,前缀只能到 。
则无法得到非空相等串。否则删除次数为总长减去 。若一直相等,则取最短串长度。 ,一次对齐扫描即可。
根据题目描述,我们知道,如果删除字符后的三个字符串相等,那么它们存在一个长度大于 的公共前缀。因此,我们可以枚举公共前缀的位置 ,如果当前下标 对应的三个字符不完全相等,那么公共前缀长度为 ,此时,我们判断 是否为 ,若是,返回 ,否则返回 ,其中 为三个字符串的长度和。
时间复杂度 ,其中 为三个字符串的最小长度。空间复杂度 。
func findMinimumOperations(s1 string, s2 string, s3 string) int {
s := len(s1) + len(s2) + len(s3)
n := min(len(s1), len(s2), len(s3))
for i := range s1[:n] {
if !(s1[i] == s2[i] && s2[i] == s3[i]) {
if i == 0 {
return -1
}
return s - 3*i
}
}
return s - 3*n
}
2942. 查找包含给定字符的单词 ​
题目、示例与约束
给你一个下标从 0 开始的字符串数组 words 和一个字符 x 。
请你返回一个 下标数组 ,表示下标在数组中对应的单词包含字符 x 。
注意 ,返回的数组可以是 任意 顺序。
示例 1:
输入:words = ["leet","code"], x = "e" 输出:[0,1] 解释:"e" 在两个单词中都出现了:"leet" 和 "code" 。所以我们返回下标 0 和 1 。
示例 2:
输入:words = ["abc","bcd","aaaa","cbc"], x = "a" 输出:[0,2] 解释:"a" 在 "abc" 和 "aaaa" 中出现了,所以我们返回下标 0 和 2 。
示例 3:
输入:words = ["abc","bcd","aaaa","cbc"], x = "z" 输出:[] 解释:"z" 没有在任何单词中出现。所以我们返回空数组。
提示:
1 <= words.length <= 501 <= words[i].length <= 50x是一个小写英文字母。words[i]只包含小写英文字母。
方法一:遍历 · Go 实现
思考
返回含字符 的单词下标。 长度与串长均 ,对每个单词做一次成员判断即可。
列表推导按枚举顺序收集下标,无需额外索引结构。
我们直接遍历字符串数组 中的每一个字符串 ,如果 在 中出现,就将 加入答案数组中。
遍历结束后,返回答案数组即可。
时间复杂度 ,其中 为字符串数组 中所有字符串的长度和。忽略答案数组的空间消耗,空间复杂度 。
func findWordsContaining(words []string, x byte) (ans []int) {
for i, w := range words {
for _, c := range w {
if byte(c) == x {
ans = append(ans, i)
break
}
}
}
return
}
3019. 按键变更的次数 ​
题目、示例与约束
给你一个下标从 0 开始的字符串 s ,该字符串由用户输入。按键变更的定义是:使用与上次使用的按键不同的键。例如 s = "ab" 表示按键变更一次,而 s = "bBBb" 不存在按键变更。
返回用户输入过程中按键变更的次数。
注意:shift 或 caps lock 等修饰键不计入按键变更,也就是说,如果用户先输入字母 'a' 然后输入字母 'A' ,不算作按键变更。
示例 1:
输入:s = "aAbBcC" 输出:2 解释: 从 s[0] = 'a' 到 s[1] = 'A',不存在按键变更,因为不计入 caps lock 或 shift 。 从 s[1] = 'A' 到 s[2] = 'b',按键变更。 从 s[2] = 'b' 到 s[3] = 'B',不存在按键变更,因为不计入 caps lock 或 shift 。 从 s[3] = 'B' 到 s[4] = 'c',按键变更。 从 s[4] = 'c' 到 s[5] = 'C',不存在按键变更,因为不计入 caps lock 或 shift 。
示例 2:
输入:s = "AaAaAaaA" 输出:0 解释: 不存在按键变更,因为这个过程中只按下字母 'a' 和 'A' ,不需要进行按键变更。
提示:
1 <= s.length <= 100s仅由英文大写字母和小写字母组成。
方法一:一次遍历 · Go 实现
思考
大小写视为同一按键, 。相邻字符若忽略大小写后不同,则计一次变更。
先将整串转为小写,再对相邻对计数即可,无需模拟键盘。
我们可以遍历字符串,每次判断当前字符的小写形式是否与前一个字符的小写形式相同,如果不同则说明发生了按键变更,将答案加一即可。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func countKeyChanges(s string) (ans int) {
s = strings.ToLower(s)
for i, c := range s[1:] {
if byte(c) != s[i] {
ans++
}
}
return
}
3029. 将单词恢复初始状态所需的最短时间 I ​
题目、示例与约束
给你一个下标从 0 开始的字符串 word 和一个整数 k 。
在每一秒,你必须执行以下操作:
- 移除
word的前k个字符。 - 在
word的末尾添加k个任意字符。
注意 添加的字符不必和移除的字符相同。但是,必须在每一秒钟都执行 两种 操作。
返回将 word 恢复到其 初始 状态所需的 最短 时间(该时间必须大于零)。
示例 1:
输入:word = "abacaba", k = 3 输出:2 解释: 第 1 秒,移除 word 的前缀 "aba",并在末尾添加 "bac" 。因此,word 变为 "cababac"。 第 2 秒,移除 word 的前缀 "cab",并在末尾添加 "aba" 。因此,word 变为 "abacaba" 并恢复到始状态。 可以证明,2 秒是 word 恢复到其初始状态所需的最短时间。
示例 2:
输入:word = "abacaba", k = 4 输出:1 解释: 第 1 秒,移除 word 的前缀 "abac",并在末尾添加 "caba" 。因此,word 变为 "abacaba" 并恢复到初始状态。 可以证明,1 秒是 word 恢复到其初始状态所需的最短时间。
示例 3:
输入:word = "abcbabcd", k = 2 输出:4 解释: 每一秒,我们都移除 word 的前 2 个字符,并在 word 末尾添加相同的字符。 4 秒后,word 变为 "abcbabcd" 并恢复到初始状态。 可以证明,4 秒是 word 恢复到其初始状态所需的最短时间。
提示:
1 <= word.length <= 501 <= k <= word.lengthword仅由小写英文字母组成。
方法一:枚举 · Go 实现
思考
每次删去前 个字符并在末尾补任意字符。恢复原串意味着剩余后缀必须等于原串的等长前缀。 ,枚举操作次数即可。
第 次操作后剩余为 ,只需与 比较。若一直不等,则最多 次必能清空。
按 枚举并做字符串比较,否则返回上取整。
我们不妨假设,如果只操作一次,就能使得 word 恢复到初始状态,那么意味着 word[k:] 是 word 的前缀,即 word[k:] == word[:n-k]。
如果有多次操作,不妨设 为操作次数,那么意味着 word[k*i:] 是 word 的前缀,即 word[k*i:] == word[:n-k*i]。
因此,我们可以枚举操作次数,判断 word[k*i:] 是否是 word 的前缀,如果是,则返回 。
时间复杂度 ,空间复杂度 。其中 为 word 的长度。
func minimumTimeToInitialState(word string, k int) int {
n := len(word)
for i := k; i < n; i += k {
if word[i:] == word[:n-i] {
return i / k
}
}
return (n + k - 1) / k
}
方法二:枚举 + 字符串哈希 · Go 实现
思考
方法一每次比较长度为 的串,总代价 。本范围可过,但比较本身可以预处理成常数。
字符串哈希给出任意子串的指纹,判断前缀是否相等只需一次查询,时间降为线性。
我们也可以在方法一的基础上,利用字符串哈希来判断两个字符串是否相等。
时间复杂度 ,空间复杂度 。其中 为 word 的长度。
type Hashing struct {
p []int64
h []int64
mod int64
}
func NewHashing(word string, base int64, mod int64) *Hashing {
n := len(word)
p := make([]int64, n+1)
h := make([]int64, n+1)
p[0] = 1
for i := 1; i <= n; i++ {
p[i] = (p[i-1] * base) % mod
h[i] = (h[i-1]*base + int64(word[i-1]-'a')) % mod
}
return &Hashing{p, h, mod}
}
func (hashing *Hashing) Query(l, r int) int64 {
return (hashing.h[r] - hashing.h[l-1]*hashing.p[r-l+1]%hashing.mod + hashing.mod) % hashing.mod
}
func minimumTimeToInitialState(word string, k int) int {
hashing := NewHashing(word, 13331, 998244353)
n := len(word)
for i := k; i < n; i += k {
if hashing.Query(1, n-i) == hashing.Query(i+1, n) {
return i / k
}
}
return (n + k - 1) / k
}
3031. 将单词恢复初始状态所需的最短时间 II ​
题目、示例与约束
给你一个下标从 0 开始的字符串 word 和一个整数 k 。
在每一秒,你必须执行以下操作:
- 移除
word的前k个字符。 - 在
word的末尾添加k个任意字符。
注意 添加的字符不必和移除的字符相同。但是,必须在每一秒钟都执行 两种 操作。
返回将 word 恢复到其 初始 状态所需的 最短 时间(该时间必须大于零)。
示例 1:
输入:word = "abacaba", k = 3 输出:2 解释: 第 1 秒,移除 word 的前缀 "aba",并在末尾添加 "bac" 。因此,word 变为 "cababac"。 第 2 秒,移除 word 的前缀 "cab",并在末尾添加 "aba" 。因此,word 变为 "abacaba" 并恢复到始状态。 可以证明,2 秒是 word 恢复到其初始状态所需的最短时间。
示例 2:
输入:word = "abacaba", k = 4 输出:1 解释: 第 1 秒,移除 word 的前缀 "abac",并在末尾添加 "caba" 。因此,word 变为 "abacaba" 并恢复到初始状态。 可以证明,1 秒是 word 恢复到其初始状态所需的最短时间。
示例 3:
输入:word = "abcbabcd", k = 2 输出:4 解释: 每一秒,我们都移除 word 的前 2 个字符,并在 word 末尾添加相同的字符。 4 秒后,word 变为 "abcbabcd" 并恢复到初始状态。 可以证明,4 秒是 word 恢复到其初始状态所需的最短时间。
提示:
1 <= word.length <= 1061 <= k <= word.lengthword仅由小写英文字母组成。
方法一:枚举 + 字符串哈希 · Go 实现
思考
题意与 I 相同,但 ,逐次比较后缀与前缀会超时。
I 的第二种做法已把每次比较降为哈希查询。本范围必须采用该做法。
预处理前缀哈希后,枚举 的倍数并在 内比较 与 。
我们不妨假设,如果只操作一次,就能使得 word 恢复到初始状态,那么意味着 word[k:] 是 word 的前缀,即 word[k:] == word[:n-k]。
如果有多次操作,不妨设 为操作次数,那么意味着 word[k*i:] 是 word 的前缀,即 word[k*i:] == word[:n-k*i]。
因此,我们可以枚举操作次数,利用字符串哈希来判断两个字符串是否相等。
时间复杂度 ,空间复杂度 。其中 为 word 的长度。
type Hashing struct {
p []int64
h []int64
mod int64
}
func NewHashing(word string, base int64, mod int64) *Hashing {
n := len(word)
p := make([]int64, n+1)
h := make([]int64, n+1)
p[0] = 1
for i := 1; i <= n; i++ {
p[i] = (p[i-1] * base) % mod
h[i] = (h[i-1]*base + int64(word[i-1]-'a')) % mod
}
return &Hashing{p, h, mod}
}
func (hashing *Hashing) Query(l, r int) int64 {
return (hashing.h[r] - hashing.h[l-1]*hashing.p[r-l+1]%hashing.mod + hashing.mod) % hashing.mod
}
func minimumTimeToInitialState(word string, k int) int {
hashing := NewHashing(word, 13331, 998244353)
n := len(word)
for i := k; i < n; i += k {
if hashing.Query(1, n-i) == hashing.Query(i+1, n) {
return i / k
}
}
return (n + k - 1) / k
}
3034. 匹配模式数组的子数组数目 I ​
题目、示例与约束
给你一个下标从 0 开始长度为 n 的整数数组 nums ,和一个下标从 0 开始长度为 m 的整数数组 pattern ,pattern 数组只包含整数 -1 ,0 和 1 。
大小为 m + 1 的子数组 nums[i..j] 如果对于每个元素 pattern[k] 都满足以下条件,那么我们说这个子数组匹配模式数组 pattern :
- 如果
pattern[k] == 1,那么nums[i + k + 1] > nums[i + k] - 如果
pattern[k] == 0,那么nums[i + k + 1] == nums[i + k] - 如果
pattern[k] == -1,那么nums[i + k + 1] < nums[i + k]
请你返回匹配 pattern 的 nums 子数组的 数目 。
示例 1:
输入:nums = [1,2,3,4,5,6], pattern = [1,1] 输出:4 解释:模式 [1,1] 说明我们要找的子数组是长度为 3 且严格上升的。在数组 nums 中,子数组 [1,2,3] ,[2,3,4] ,[3,4,5] 和 [4,5,6] 都匹配这个模式。 所以 nums 中总共有 4 个子数组匹配这个模式。
示例 2:
输入:nums = [1,4,4,1,3,5,5,3], pattern = [1,0,-1] 输出:2 解释:这里,模式数组 [1,0,-1] 说明我们需要找的子数组中,第一个元素小于第二个元素,第二个元素等于第三个元素,第三个元素大于第四个元素。在 nums 中,子数组 [1,4,4,1] 和 [3,5,5,3] 都匹配这个模式。 所以 nums 中总共有 2 个子数组匹配这个模式。
提示:
2 <= n == nums.length <= 1001 <= nums[i] <= 1091 <= m == pattern.length < n-1 <= pattern[i] <= 1
方法一:枚举 · Go 实现
思考
模式描述相邻元素的升降关系, 。每个长度为 的窗口都可以直接对照模式。
把相邻比较映射为 后,窗口与模式逐位相等则计数。
枚举起点并在 内验证即可。
我们可以枚举数组 nums 的所有长度为 的子数组,然后判断是否满足模式数组 pattern,如果满足则答案加一。
时间复杂度 ,其中 和 分别是数组 nums 和 pattern 的长度。空间复杂度 。
func countMatchingSubarrays(nums []int, pattern []int) (ans int) {
f := func(a, b int) int {
if a == b {
return 0
}
if a < b {
return 1
}
return -1
}
n, m := len(nums), len(pattern)
for i := 0; i < n-m; i++ {
ok := 1
for k := 0; k < m && ok == 1; k++ {
if f(nums[i+k], nums[i+k+1]) != pattern[k] {
ok = 0
}
}
ans += ok
}
return
}
3036. 匹配模式数组的子数组数目 II ​
题目、示例与约束
给你一个下标从 0 开始长度为 n 的整数数组 nums ,和一个下标从 0 开始长度为 m 的整数数组 pattern ,pattern 数组只包含整数 -1 ,0 和 1 。
大小为 m + 1 的子数组 nums[i..j] 如果对于每个元素 pattern[k] 都满足以下条件,那么我们说这个子数组匹配模式数组 pattern :
- 如果
pattern[k] == 1,那么nums[i + k + 1] > nums[i + k] - 如果
pattern[k] == 0,那么nums[i + k + 1] == nums[i + k] - 如果
pattern[k] == -1,那么nums[i + k + 1] < nums[i + k]
请你返回匹配 pattern 的 nums 子数组的 数目 。
示例 1:
输入:nums = [1,2,3,4,5,6], pattern = [1,1] 输出:4 解释:模式 [1,1] 说明我们要找的子数组是长度为 3 且严格上升的。在数组 nums 中,子数组 [1,2,3] ,[2,3,4] ,[3,4,5] 和 [4,5,6] 都匹配这个模式。 所以 nums 中总共有 4 个子数组匹配这个模式。
示例 2:
输入:nums = [1,4,4,1,3,5,5,3], pattern = [1,0,-1] 输出:2 解释:这里,模式数组 [1,0,-1] 说明我们需要找的子数组中,第一个元素小于第二个元素,第二个元素等于第三个元素,第三个元素大于第四个元素。在 nums 中,子数组 [1,4,4,1] 和 [3,5,5,3] 都匹配这个模式。 所以 nums 中总共有 2 个子数组匹配这个模式。
提示:
2 <= n == nums.length <= 1061 <= nums[i] <= 1091 <= m == pattern.length < n-1 <= pattern[i] <= 1
方法一 · Go 实现
思考
升至 ,I 中对每个窗口验证模式会超时。
相邻关系序列长度为 ,在其上做模式匹配等价于字符串匹配,KMP 为线性。
先把 转成 序列,再对 跑 KMP,匹配次数即答案。
func countMatchingSubarrays(nums []int, pattern []int) int {
N := len(pattern)
ps := make([]int, N+1)
ps[0], ps[1] = -1, 0
for i, p := 2, 0; i <= N; i++ {
x := pattern[i-1]
for p >= 0 && pattern[p] != x {
p = ps[p]
}
p++
ps[i] = p
}
res := 0
M := len(nums)
for i, p := 1, 0; i < M; i++ {
t := nums[i] - nums[i-1]
switch {
case t > 0:
t = 1
case t < 0:
t = -1
}
for p >= 0 && pattern[p] != t {
p = ps[p]
}
if p++; p == N {
res++
p = ps[p]
}
}
return res
}
3110. 字符串的分数 ​
题目、示例与约束
给你一个字符串 s 。一个字符串的 分数 定义为相邻字符 ASCII 码差值绝对值的和。
请你返回 s 的 分数 。
示例 1:
输入:s = "hello"
输出:13
解释:
s 中字符的 ASCII 码分别为:'h' = 104 ,'e' = 101 ,'l' = 108 ,'o' = 111 。所以 s 的分数为 |104 - 101| + |101 - 108| + |108 - 108| + |108 - 111| = 3 + 7 + 0 + 3 = 13 。
示例 2:
输入:s = "zaz"
输出:50
解释:
s 中字符的 ASCII 码分别为:'z' = 122 ,'a' = 97 。所以 s 的分数为 |122 - 97| + |97 - 122| = 25 + 25 = 50 。
提示:
2 <= s.length <= 100s只包含小写英文字母。
方法一:模拟 · Go 实现
思考
分数定义为相邻字符 ASCII 差的绝对值之和,定义本身已是一遍线性扫描。
的规模下无需预处理或分段。相邻对互不依赖,直接累加即可。
因此对 的相邻字符取
ord后求绝对差并求和。时间和额外空间均为线性与常数。
我们直接遍历字符串 ,计算相邻字符的 ASCII 码差值的绝对值之和即可。
时间复杂度 ,其中 是字符串 的长度。空间复杂度 。
func scoreOfString(s string) (ans int) {
for i := 1; i < len(s); i++ {
ans += abs(int(s[i-1]) - int(s[i]))
}
return
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3114. 替换字符可以得到的最晚时间 ​
题目、示例与约束
给你一个字符串 s,表示一个 12 小时制的时间格式,其中一些数字(可能没有)被 "?" 替换。
12 小时制时间格式为 "HH:MM" ,其中 HH 的取值范围为 00 至 11,MM 的取值范围为 00 至 59。最早的时间为 00:00,最晚的时间为 11:59。
你需要将 s 中的 所有 "?" 字符替换为数字,使得结果字符串代表的时间是一个 有效 的 12 小时制时间,并且是可能的 最晚 时间。
返回结果字符串。
示例 1:
输入: s = "1?:?4"
输出: "11:54"
解释: 通过替换 "?" 字符,可以得到的最晚12小时制时间是 "11:54"。
示例 2:
输入: s = "0?:5?"
输出: "09:59"
解释: 通过替换 "?" 字符,可以得到的最晚12小时制时间是 "09:59"。
提示:
s.length == 5s[2]是字符":"- 除
s[2]外,其他字符都是数字或"?" - 输入保证在替换
"?"字符后至少存在一个介于"00:00"和"11:59"之间的时间。
方法一:枚举 · Go 实现
思考
合法时刻只有 种。从大到小枚举小时与分钟,检查与模式串在非
?位是否一致,第一次命中即最大合法时间。状态空间为常数,无需回溯或逐位推最优。按 到 的顺序保证字典序最大。
因此双重循环生成
HH:MM,用逐位比较过滤?,返回第一个匹配的串。
我们可以从大到小枚举所有的时间,其中小时 从 到 ,分钟 从 到 。对于每一个时间 ,我们检查是否满足 的每一位字符都与 的对应位置字符相等(如果 的对应位置字符不是 "?" 的话)。如果满足,那么我们就找到了答案,返回 。
时间复杂度 ,其中 , 。空间复杂度 。
func findLatestTime(s string) string {
for h := 11; ; h-- {
for m := 59; m >= 0; m-- {
t := fmt.Sprintf("%02d:%02d", h, m)
ok := true
for i := 0; i < len(s); i++ {
if s[i] != '?' && s[i] != t[i] {
ok = false
break
}
}
if ok {
return t
}
}
}
}
方法二:逐个判断 · Go 实现
思考
方法一枚举全部时刻,虽为常数但仍做了数百次格式化与比较。每位的取值范围由 12 小时制独立确定,可以直接写下。
小时十位只能是 或 ,个位在十位为 时至多为 ;分钟十位至多为 ,个位可取 。
为此按从左到右把每个
?换成仍合法的最大数字:先定 再定 ,分钟两位分别填 与 。一次扫描即可。
我们可以逐个判断 的每一位字符,如果是 "?" 的话,我们就根据前后的字符来确定这一位字符的值。具体地,我们有以下规则:
- 如果 是 "?",那么 的值应该是 "1" 或 "0",具体取决于 的值。如果 是 "?" 或者 小于 "2",那么 的值应该是 "1",否则 的值应该是 "0"。
- 如果 是 "?",那么 的值应该是 "1" 或 "9",具体取决于 的值。如果 是 "1",那么 的值应该是 "1",否则 的值应该是 "9"。
- 如果 是 "?",那么 的值应该是 "5"。
- 如果 是 "?",那么 的值应该是 "9"。
时间复杂度 ,空间复杂度 。
func findLatestTime(s string) string {
cs := []byte(s)
if cs[0] == '?' {
if cs[1] == '?' || cs[1] < '2' {
cs[0] = '1'
} else {
cs[0] = '0'
}
}
if cs[1] == '?' {
if cs[0] == '1' {
cs[1] = '1'
} else {
cs[1] = '9'
}
}
if cs[3] == '?' {
cs[3] = '5'
}
if cs[4] == '?' {
cs[4] = '9'
}
return string(cs)
}
3136. 有效单词 ​
题目、示例与约束
有效单词 需要满足以下几个条件:
- 至少 包含 3 个字符。
- 由数字 0-9 和英文大小写字母组成。(不必包含所有这类字符。)
- 至少 包含一个 元音字母 。
- 至少 包含一个 辅音字母 。
给你一个字符串 word 。如果 word 是一个有效单词,则返回 true ,否则返回 false 。
注意:
'a'、'e'、'i'、'o'、'u'及其大写形式都属于 元音字母 。- 英文中的 辅音字母 是指那些除元音字母之外的字母。
示例 1:
输入:word = "234Adas"
输出:true
解释:
这个单词满足所有条件。
示例 2:
输入:word = "b3"
输出:false
解释:
这个单词的长度少于 3 且没有包含元音字母。
示例 3:
输入:word = "a3$e"
输出:false
解释:
这个单词包含了 '$' 字符且没有包含辅音字母。
提示:
1 <= word.length <= 20word由英文大写和小写字母、数字、'@'、'#'和'$'组成。
方法一:模拟 · Go 实现
思考
合法单词需长度至少 、仅含字母数字,且同时出现元音与辅音。条件彼此独立,一次扫描即可判定。
无需自动机或多次遍历。非字母数字立即失败;字母再按元音集合分类。
先判长度,再维护 与 ,结束时二者均为真则合法。
我们首先判断字符串的长度是否小于 3,如果是则返回 false。
然后我们遍历字符串,判断每个字符是否是字母或数字,如果不是则返回 false。否则我们判断该字符是否是元音字母,如果是则将 has_vowel 置为 true,否则将 has_consonant 置为 true。
最后,如果 has_vowel 和 has_consonant 都为 true,则返回 true,否则返回 false。
时间复杂度 ,空间复杂度 。其中 为字符串的长度。
func isValid(word string) bool {
if len(word) < 3 {
return false
}
hasVowel := false
hasConsonant := false
vs := make([]bool, 26)
for _, c := range "aeiou" {
vs[c-'a'] = true
}
for _, c := range word {
if unicode.IsLetter(c) {
if vs[unicode.ToLower(c)-'a'] {
hasVowel = true
} else {
hasConsonant = true
}
} else if !unicode.IsDigit(c) {
return false
}
}
return hasVowel && hasConsonant
}
3163. 压缩字符串 III ​
题目、示例与约束
给你一个字符串 word,请你使用以下算法进行压缩:
- 从空字符串
comp开始。当word不为空 时,执行以下操作:- 移除
word的最长单字符前缀,该前缀由单一字符c重复多次组成,且该前缀长度 最多 为 9 。 - 将前缀的长度和字符
c追加到comp。
- 移除
返回字符串 comp 。
示例 1:
输入:word = "abcde"
输出:"1a1b1c1d1e"
解释:
初始时,comp = "" 。进行 5 次操作,每次操作分别选择 "a"、"b"、"c"、"d" 和 "e" 作为前缀。
对每个前缀,将 "1" 和对应的字符追加到 comp。
示例 2:
输入:word = "aaaaaaaaaaaaaabb"
输出:"9a5a2b"
解释:
初始时,comp = ""。进行 3 次操作,每次操作分别选择 "aaaaaaaaa"、"aaaaa" 和 "bb" 作为前缀。
- 对于前缀
"aaaaaaaaa",将"9"和"a"追加到comp。 - 对于前缀
"aaaaa",将"5"和"a"追加到comp。 - 对于前缀
"bb",将"2"和"b"追加到comp。
提示:
1 <= word.length <= 2 * 105word仅由小写英文字母组成。
方法一:双指针 · Go 实现
思考
压缩规则是把连续相同字符写成次数(至多 )加字符。手写下标容易在跨 时出错。
groupby已按字符分段,每段长度再按 切开即可。对每段 反复取 追加
str(x)+c。一遍线性拼接。
我们可以利用双指针,统计每个字符的连续出现次数。假如当前字符 连续出现了 次,然后我们将 划分成若干个 ,每个 最大为 ,然后将 和 拼接起来,将每个 和 拼接起来到结果中。
最后返回结果即可。
时间复杂度 ,空间复杂度 。其中 为字符串 word 的长度。
func compressedString(word string) string {
ans := []byte{}
n := len(word)
for i := 0; i < n; {
j := i + 1
for j < n && word[j] == word[i] {
j++
}
k := j - i
for k > 0 {
x := min(9, k)
ans = append(ans, byte('0'+x))
ans = append(ans, word[i])
k -= x
}
i = j
}
return string(ans)
}
3168. 候诊室中的最少椅子数 ​
题目、示例与约束
给你一个字符串 s,模拟每秒钟的事件 i:
- 如果
s[i] == 'E',表示有一位顾客进入候诊室并占用一把椅子。 - 如果
s[i] == 'L',表示有一位顾客离开候诊室,从而释放一把椅子。
返回保证每位进入候诊室的顾客都能有椅子坐的 最少 椅子数,假设候诊室最初是 空的 。
示例 1:
输入:s = "EEEEEEE"
输出:7
解释:
每秒后都有一个顾客进入候诊室,没有人离开。因此,至少需要 7 把椅子。
示例 2:
输入:s = "ELELEEL"
输出:2
解释:
假设候诊室里有 2 把椅子。下表显示了每秒钟等候室的状态。
| 秒 | 事件 | 候诊室的人数 | 可用的椅子数 |
|---|---|---|---|
| 0 | Enter | 1 | 1 |
| 1 | Leave | 0 | 2 |
| 2 | Enter | 1 | 1 |
| 3 | Leave | 0 | 2 |
| 4 | Enter | 1 | 1 |
| 5 | Enter | 2 | 0 |
| 6 | Leave | 1 | 1 |
示例 3:
输入:s = "ELEELEELLL"
输出:3
解释:
假设候诊室里有 3 把椅子。下表显示了每秒钟等候室的状态。
| 秒 | 事件 | 候诊室的人数 | 可用的椅子数 |
|---|---|---|---|
| 0 | Enter | 1 | 2 |
| 1 | Leave | 0 | 3 |
| 2 | Enter | 1 | 2 |
| 3 | Enter | 2 | 1 |
| 4 | Leave | 1 | 2 |
| 5 | Enter | 2 | 1 |
| 6 | Enter | 3 | 0 |
| 7 | Leave | 2 | 1 |
| 8 | Leave | 1 | 2 |
| 9 | Leave | 0 | 3 |
提示:
1 <= s.length <= 50s仅由字母'E'和'L'组成。s表示一个有效的进出序列。
方法一:模拟 · Go 实现
思考
E来人、L离开,椅子用完才能新买且不可搬走。模拟人数峰值即可,无需真正排队。空闲椅可立刻复用:来人时若有空椅则占用,否则总椅数加一;离开则空椅加一。
维护 与 ,扫完 后 即最少椅子数。
我们用变量 来记录当前需要的椅子数,用变量 来记录当前剩余的空椅子数。遍历字符串 ,如果当前字符是 'E',那么如果有剩余的空椅子,就直接使用一个空椅子,否则需要增加一个椅子;如果当前字符是 'L',那么剩余的空椅子数加一。
遍历结束后,返回 即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func minimumChairs(s string) int {
cnt, left := 0, 0
for _, c := range s {
if c == 'E' {
if left > 0 {
left--
} else {
cnt++
}
} else {
left++
}
}
return cnt
}
3210. 找出加密后的字符串 ​
题目、示例与约束
给你一个字符串 s 和一个整数 k。请你使用以下算法加密字符串:
- 对于字符串
s中的每个字符c,用字符串中c后面的第k个字符替换c(以循环方式)。
返回加密后的字符串。
示例 1:
输入: s = "dart", k = 3
输出: "tdar"
解释:
- 对于
i = 0,'d'后面的第 3 个字符是't'。 - 对于
i = 1,'a'后面的第 3 个字符是'd'。 - 对于
i = 2,'r'后面的第 3 个字符是'a'。 - 对于
i = 3,'t'后面的第 3 个字符是'r'。
示例 2:
输入: s = "aaa", k = 1
输出: "aaa"
解释:
由于所有字符都相同,加密后的字符串也将相同。
提示:
1 <= s.length <= 1001 <= k <= 104s仅由小写英文字母组成。
方法一:模拟 · Go 实现
思考
,按定义把每位换成其后第 个字符即可。 可达 ,直接数 步会重复绕圈,但循环移位只依赖 。
因此对每个下标 取 写入新串。一次线性构造,无需真正旋转 轮。
我们可以使用模拟的方法,对字符串的第 个字符,我们将其替换为字符串的第 个字符。
时间复杂度 ,空间复杂度 。其中 是字符串 的长度。
func getEncryptedString(s string, k int) string {
cs := []byte(s)
for i := range s {
cs[i] = s[(i+k)%len(s)]
}
return string(cs)
}
3234. 统计 1 显著的字符串的数量 ​
题目、示例与约束
给你一个二进制字符串 s。
请你统计并返回其中 1 显著 的 子字符串 的数量。
如果字符串中 1 的数量 大于或等于 0 的数量的 平方,则认为该字符串是一个 1 显著 的字符串 。
示例 1:
输入:s = "00011"
输出:5
解释:
1 显著的子字符串如下表所示。
| i | j | s[i..j] | 0 的数量 | 1 的数量 |
|---|---|---|---|---|
| 3 | 3 | 1 | 0 | 1 |
| 4 | 4 | 1 | 0 | 1 |
| 2 | 3 | 01 | 1 | 1 |
| 3 | 4 | 11 | 0 | 2 |
| 2 | 4 | 011 | 1 | 2 |
示例 2:
输入:s = "101101"
输出:16
解释:
1 不显著的子字符串如下表所示。
总共有 21 个子字符串,其中 5 个是 1 不显著字符串,因此有 16 个 1 显著子字符串。
| i | j | s[i..j] | 0 的数量 | 1 的数量 |
|---|---|---|---|---|
| 1 | 1 | 0 | 1 | 0 |
| 4 | 4 | 0 | 1 | 0 |
| 1 | 4 | 0110 | 2 | 2 |
| 0 | 4 | 10110 | 2 | 3 |
| 1 | 5 | 01101 | 2 | 3 |
提示:
1 <= s.length <= 4 * 104s仅包含字符'0'和'1'。
方法一:预处理 + 枚举 · Go 实现
思考
子串需满足 。 ,枚举两端再计数为平方。由不等式知 ,固定左端后只需跳着枚举接下来的若干个 。
预处理 为 起第一个 的位置。对每个左端 ,沿 的位置跳转并更新 ,在 足够时把右端可滑动的长度计入。每起点至多 次跳跃。
根据题目描述,显著字符串满足 ,那么 的最大值不超过 ,其中 是字符串的长度。因此我们可以枚举 的值,然后计算满足条件的子字符串数量。
我们首先预处理字符串中每个位置开始的第一个 的位置,存储在数组 中,其中 表示从位置 开始的第一个 的位置,如果不存在则为 。
接下来,我们遍历字符串的每个位置 作为子字符串的起始位置,初始化 的值为 或 (取决于当前位置是否为 )。然后我们使用一个指针 从位置 开始,逐步跳转到下一个 的位置,同时更新 的值。
对于从位置 开始,且包含 个 的子字符串,最多可以包含 个 。如果这个数量大于等于 ,则说明存在满足条件的子字符串。此时需要判断字符串从右端点 向左移动多少个位置,可以使得子字符串仍然满足条件。具体来说,右端点一共可以有 种选择方式。我们将这些数量累加到答案中。然后将指针 移动到下一个 的位置,继续枚举下一个 的值,直到 超过字符串长度为止。
时间复杂度 ,空间复杂度 ,其中 是字符串的长度。
func numberOfSubstrings(s string) int {
n := len(s)
nxt := make([]int, n+1)
nxt[n] = n
for i := n - 1; i >= 0; i-- {
nxt[i] = nxt[i+1]
if s[i] == '0' {
nxt[i] = i
}
}
ans := 0
for i := 0; i < n; i++ {
cnt0 := 0
if s[i] == '0' {
cnt0 = 1
}
j := i
for j < n && int64(cnt0*cnt0) <= int64(n) {
cnt1 := nxt[j+1] - i - cnt0
if cnt1 >= cnt0*cnt0 {
ans += min(nxt[j+1]-j, cnt1-cnt0*cnt0+1)
}
j = nxt[j+1]
cnt0++
}
}
return ans
}
3248. 矩阵中的蛇 ​
题目、示例与约束
大小为 n x n 的矩阵 grid 中有一条蛇。蛇可以朝 四个可能的方向 移动。矩阵中的每个单元格都使用位置进行标识: grid[i][j] = (i * n) + j。
蛇从单元格 0 开始,并遵循一系列命令移动。
给你一个整数 n 表示 grid 的大小,另给你一个字符串数组 commands,其中包括 "UP"、"RIGHT"、"DOWN" 和 "LEFT"。题目测评数据保证蛇在整个移动过程中将始终位于 grid 边界内。
返回执行 commands 后蛇所停留的最终单元格的位置。
示例 1:
输入:n = 2, commands = ["RIGHT","DOWN"]
输出:3
解释:
| 0 | 1 |
| 2 | 3 |
| 0 | 1 |
| 2 | 3 |
| 0 | 1 |
| 2 | 3 |
示例 2:
输入:n = 3, commands = ["DOWN","RIGHT","UP"]
输出:1
解释:
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
提示:
2 <= n <= 101 <= commands.length <= 100commands仅由"UP"、"RIGHT"、"DOWN"和"LEFT"组成。- 生成的测评数据确保蛇不会移动到矩阵的边界外。
方法一:模拟 · Go 实现
思考
在 网格按指令移动,保证不越界。 、 至多 ,按字面模拟即可。
维护坐标 ,由命令首字母改一行或一列,最后返回 。无需建盘。
我们可以用两个变量 和 来表示蛇的位置,初始时 ,然后遍历 ,根据当前的命令更新 和 的值,最后返回 即可。
时间复杂度 ,其中 是数组 的长度。空间复杂度 。
func finalPositionOfSnake(n int, commands []string) int {
x, y := 0, 0
for _, c := range commands {
switch c[0] {
case 'U':
x--
case 'D':
x++
case 'L':
y--
case 'R':
y++
}
}
return x*n + y
}
3271. 哈希分割字符串 ​
题目、示例与约束
给你一个长度为 n 的字符串 s 和一个整数 k ,n 是 k 的 倍数 。你的任务是将字符串 s 哈希为一个长度为 n / k 的新字符串 result 。
首先,将 s 分割成 n / k 个 子字符串 ,每个子字符串的长度都为 k 。然后,将 result 初始化为一个 空 字符串。
我们依次从前往后处理每一个 子字符串 :
- 一个字符的 哈希值 是它在 字母表 中的下标(也就是
'a' → 0,'b' → 1,... ,'z' → 25)。 - 将子字符串中字母的 哈希值 求和。
- 将和对 26 取余,将结果记为
hashedChar。 - 找到小写字母表中
hashedChar对应的字符。 - 将该字符添加到
result的末尾。
返回 result 。
示例 1:
输入:s = "abcd", k = 2
输出:"bf"
解释:
第一个字符串为 "ab" ,0 + 1 = 1 ,1 % 26 = 1 ,result[0] = 'b' 。
第二个字符串为: "cd" ,2 + 3 = 5 ,5 % 26 = 5 ,result[1] = 'f' 。
示例 2:
输入:s = "mxz", k = 3
输出:"i"
解释:
唯一的子字符串为 "mxz" ,12 + 23 + 25 = 60 ,60 % 26 = 8 ,result[0] = 'i' 。
提示:
1 <= k <= 100k <= s.length <= 1000s.length能被k整除。s只含有小写英文字母。
方法一:模拟 · Go 实现
思考
把 每 个字符映射成一个字母: 。 且 能被 整除,按块模拟即可。
步长 切开,块内累加码点再取模,转回字符。线性一遍。
我们可以按照题目描述的步骤模拟即可。
遍历字符串 ,每次取 个字符,计算它们的哈希值之和,记为 ,然后对 取模 ,找到对应的字符,将其添加到结果字符串的末尾。
最后返回结果字符串即可。
时间复杂度 ,其中 为字符串 的长度。忽略答案字符串的空间消耗,空间复杂度 。
func stringHash(s string, k int) string {
n := len(s)
ans := make([]byte, 0, n/k)
for i := 0; i < n; i += k {
t := 0
for j := i; j < i+k; j++ {
t += int(s[j] - 'a')
}
hashedChar := t % 26
ans = append(ans, 'a'+byte(hashedChar))
}
return string(ans)
}
3303. 第一个几乎相等子字符串的下标 ​
题目、示例与约束
给你两个字符串 s 和 pattern 。
如果一个字符串 x 修改 至多 一个字符会变成 y ,那么我们称它与 y 几乎相等 。
请你返回 s 中下标 最小 的 子字符串 ,它与 pattern 几乎相等 。如果不存在,返回 -1 。
子字符串 是字符串中的一个 非空、连续的字符序列。
示例 1:
输入:s = "abcdefg", pattern = "bcdffg"
输出:1
解释:
将子字符串 s[1..6] == "bcdefg" 中 s[4] 变为 "f" ,得到 "bcdffg" 。
示例 2:
输入:s = "ababbababa", pattern = "bacaba"
输出:4
解释:
将子字符串 s[4..9] == "bababa" 中 s[6] 变为 "c" ,得到 "bacaba" 。
示例 3:
输入:s = "abcd", pattern = "dba"
输出:-1
示例 4:
输入:s = "dde", pattern = "d"
输出:0
提示:
1 <= pattern.length < s.length <= 105s和pattern都只包含小写英文字母。
进阶:如果题目变为 至多
k 个 连续 字符可以被修改,你可以想出解法吗?方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
思路和周赛第三题一样,采用前后缀分解解决。
现在变成了两个问题:
- 第一个问题:对于每个从 开始的字符串 ,计算它能匹配 多长的前缀。
- 第二个问题:对于每个以 结尾的字符串 ,计算它能匹配 多长的后缀。
比如示例 1,,其中:
- 可以匹配 长为 的前缀。
- 可以匹配 长为 的后缀。
那么 等于 的长度减一,我们可以修改一个字母使得 与 相等。
对于第一个问题,我们可以构造字符串 ,计算其 Z 数组 。那么 与 前缀可以匹配的最长长度为 ,其中 为 的长度。
对于第二个问题,我们可以构造字符串 ,计算其 Z 数组,再反转 Z 数组,得到 。其中 表示 反转后的字符串。那么 与 后缀可以匹配的最长长度为 。
设 为 的长度, 为 的长度。
回到原问题,我们枚举 ,那么当前需要匹配的子串为 ,对应的 Z 数组元素为 和 。
如果
那么答案为 。
代码实现时,也可以枚举 ,这样上面的式子可以简化为
答案为 。
如果没有找到匹配,返回 。
具体请看 视频讲解 第四题,欢迎点赞关注~
复杂度分析
- 时间复杂度:,其中 是 的长度。
- 空间复杂度:。
思考题
题目最下面有个进阶问题(连续改至多 个),把上式中的 改成 即可。
类似的问题还有:使答案存在的最小的 是多少?
欢迎在评论区分享你的思路/代码。
相似题目
- 3036. 匹配模式数组的子数组数目 II 1895
- 3008. 找出数组中的美丽下标 II 2016
- 2223. 构造字符串的总得分和 2220
- 3031. 将单词恢复初始状态所需的最短时间 II 2278
- 3045. 统计前后缀下标对 II 2328
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与一般树(前后指针/快慢指针/DFS/BFS/直径/LCA)
import "slices"
// https://space.bilibili.com/206214
func calcZ(s string) []int {
n := len(s)
z := make([]int, n)
boxL, boxR := 0, 0 // z-box 左右边界
for i := 1; i < n; i++ {
if i <= boxR {
z[i] = min(z[i-boxL], boxR-i+1)
}
for i+z[i] < n && s[z[i]] == s[i+z[i]] {
boxL, boxR = i, i+z[i]
z[i]++
}
}
return z
}
func rev(s string) string {
t := []byte(s)
slices.Reverse(t)
return string(t)
}
func minStartingIndex(s, pattern string) int {
preZ := calcZ(pattern + s)
sufZ := calcZ(rev(pattern) + rev(s))
slices.Reverse(sufZ) // 也可以不反转,下面写 sufZ[len(sufZ)-i]
m := len(pattern)
for i := m; i <= len(s); i++ {
if preZ[i]+sufZ[i-1] >= m-1 {
return i - m
}
}
return -1
}
3324. 出现在屏幕上的字符串序列 ​
题目、示例与约束
给你一个字符串 target。
Alice 将会使用一种特殊的键盘在她的电脑上输入 target,这个键盘 只有两个 按键:
- 按键 1:在屏幕上的字符串后追加字符
'a'。 - 按键 2:将屏幕上字符串的 最后一个 字符更改为英文字母表中的 下一个 字符。例如,
'c'变为'd','z'变为'a'。
注意,最初屏幕上是一个空字符串 "",所以她 只能 按按键 1。
请你考虑按键次数 最少 的情况,按字符串出现顺序,返回 Alice 输入 target 时屏幕上出现的所有字符串列表。
示例 1:
输入: target = "abc"
输出: ["a","aa","ab","aba","abb","abc"]
解释:
Alice 按键的顺序如下:
- 按下按键 1,屏幕上的字符串变为
"a"。 - 按下按键 1,屏幕上的字符串变为
"aa"。 - 按下按键 2,屏幕上的字符串变为
"ab"。 - 按下按键 1,屏幕上的字符串变为
"aba"。 - 按下按键 2,屏幕上的字符串变为
"abb"。 - 按下按键 2,屏幕上的字符串变为
"abc"。
示例 2:
输入: target = "he"
输出: ["a","b","c","d","e","f","g","h","ha","hb","hc","hd","he"]
提示:
1 <= target.length <= 400target仅由小写英文字母组成。
方法一:模拟 · Go 实现
思考
屏幕从空串开始,每次追加一个字母或把末位沿字母表推进。 ,按目标逐位模拟即可。
对目标的每一位,从 依次追加到当前前缀,直到追加出该位为止。中间每一个串都是题目要求记录的屏幕状态。
因此不必搜索按键序列,沿 走到目标字符即得到全部中间结果。
我们可以模拟 Alice 按键的过程,从空字符串开始,每次按键后更新字符串,直到得到目标字符串。
时间复杂度 ,其中 是目标字符串的长度,而 是字符集,这里是小写字母集合,因此 。
func stringSequence(target string) (ans []string) {
for _, c := range target {
s := ""
if len(ans) > 0 {
s = ans[len(ans)-1]
}
for a := 'a'; a <= c; a++ {
t := s + string(a)
ans = append(ans, t)
}
}
return
}
3330. 找到初始输入字符串 I ​
题目、示例与约束
Alice 正在她的电脑上输入一个字符串。但是她打字技术比较笨拙,她 可能 在一个按键上按太久,导致一个字符被输入 多次 。
尽管 Alice 尽可能集中注意力,她仍然可能会犯错 至多 一次。
给你一个字符串 word ,它表示 最终 显示在 Alice 显示屏上的结果。
请你返回 Alice 一开始可能想要输入字符串的总方案数。
示例 1:
输入:word = "abbcccc"
输出:5
解释:
可能的字符串包括:"abbcccc" ,"abbccc" ,"abbcc" ,"abbc" 和 "abcccc" 。
示例 2:
输入:word = "abcd"
输出:1
解释:
唯一可能的字符串是 "abcd" 。
示例 3:
输入:word = "aaaa"
输出:4
提示:
1 <= word.length <= 100word只包含小写英文字母。
方法一:直接遍历 · Go 实现
思考
Alice 至多有一处「长按」,因此可能的原串要么是 本身,要么是某段连续相同字符少按一次。
,扫描相邻相等位置即可。每一对相邻相同字符对应一次可缩短的长按。
故答案为相邻相等对数加 (全部不缩短的那一种)。
根据题目描述,如果所有相邻字符都不相同,那么只有 1 种可能的输入字符串;如果有 1 对相邻字符相同,例如 "abbc",那么可能的输入字符串有 2 种:"abc" 和 "abbc"。
依此类推,如果有 对相邻字符相同,那么可能的输入字符串有 种。
因此,我们只需要遍历字符串,统计相邻字符相同的对数再加 1 即可。
时间复杂度 ,其中 为字符串的长度。空间复杂度 。
func possibleStringCount(word string) int {
f := 1
for i := 1; i < len(word); i++ {
if word[i] == word[i-1] {
f++
}
}
return f
}
3340. 检查平衡字符串 ​
题目、示例与约束
给你一个仅由数字 0 - 9 组成的字符串 num。如果偶数下标处的数字之和等于奇数下标处的数字之和,则认为该数字字符串是一个 平衡字符串。
如果 num 是一个 平衡字符串,则返回 true;否则,返回 false。
示例 1:
输入:num = "1234"
输出:false
解释:
- 偶数下标处的数字之和为
1 + 3 = 4,奇数下标处的数字之和为2 + 4 = 6。 - 由于 4 不等于 6,
num不是平衡字符串。
示例 2:
输入:num = "24123"
输出:true
解释:
- 偶数下标处的数字之和为
2 + 1 + 3 = 6,奇数下标处的数字之和为4 + 2 = 6。 - 由于两者相等,
num是平衡字符串。
提示:
2 <= num.length <= 100num仅由数字 0 - 9 组成。
方法一:模拟 · Go 实现
思考
平衡串要求偶数下标数字之和等于奇数下标之和。 ,一次扫描即可。
用长度为 的数组按 累加,最后比较两格是否相等。
无需真正拆出两个子序列,下标奇偶已足够。
我们可以用一个长度为 的数组 来记录偶数下标和奇数下标的数字之和,然后遍历字符串 ,根据下标的奇偶性将数字加到对应的位置上,最后判断 是否等于 即可。
时间复杂度 ,其中 为字符串 的长度。空间复杂度 。
func isBalanced(num string) bool {
f := [2]int{}
for i, c := range num {
f[i&1] += int(c - '0')
}
return f[0] == f[1]
}
3407. 子字符串匹配模式 ​
题目、示例与约束
给你一个字符串 s 和一个模式字符串 p ,其中 p 恰好 包含 一个 '*' 符号。
p 中的 '*' 符号可以被替换为零个或多个字符组成的任意字符序列。
如果 p 可以变成 s 的 子字符串,那么返回 true ,否则返回 false 。
示例 1:
输入:s = "leetcode", p = "ee*e"
输出:true
解释:
将 '*' 替换为 "tcod" ,子字符串 "eetcode" 匹配模式串。
示例 2:
输入:s = "car", p = "c*v"
输出:false
解释:
不存在匹配模式串的子字符串。
示例 3:
输入:s = "luck", p = "u*"
输出:true
解释:
子字符串 "u" ,"uc" 和 "uck" 都匹配模式串。
提示:
1 <= s.length <= 501 <= p.length <= 50s只包含小写英文字母。p只包含小写英文字母和一个'*'符号。
方法一:字符串匹配 · Go 实现
思考
中恰有一个 ,且 ,即使回溯也能通过。真正需要保证的是:星号两侧的字面量在 中按顺序出现,中间允许任意(含空)间隔。
若把 整段当成正则去匹配整串,会把「子串」误写成「全串匹配」。题目只要求 替换后是 的某个子串。
因此按 拆开得到至多两段字面量,从左到右在 中依次 ,指针只前进不回退。任一段找不到则失败。
根据题目描述,* 可以被替换为零个或多个字符组成的任意字符序列,因此我们可以将模式字符串 按照 * 分割成若干个子串,如果这些子串依次出现在字符串 中且顺序不变,则说明 可以变成 的子字符串。
因此,我们首先初始化一个指针 指向字符串 的起始位置,然后遍历模式字符串 按照 * 分割得到的每个子串 ,在字符串 中从位置 开始查找子串 ,如果找到了,则将指针 移动到子串 的末尾位置继续查找下一个子串;如果找不到,则说明模式字符串 不能变成字符串 的子字符串,返回 。如果所有子串都找到了,则返回 。
时间复杂度 ,空间复杂度 。其中 和 分别是字符串 和模式字符串 的长度。
func hasMatch(s string, p string) bool {
i := 0
for _, t := range strings.Split(p, "*") {
j := strings.Index(s[i:], t)
if j == -1 {
return false
}
i += j + len(t)
}
return true
}
继续阅读
- 实践案例
- 实践案例
- 实践案例