2026-07-02:将一个字符串排序的最小操作次数。用go语言,问题:给定只包含小写字母的字符串 s。你可以反复进行操作:每次选取 s 的一个连续子串,但子串不能覆盖整个字符串;然后把这个子串内部的字符按“从小到大不降序”重新排列。目标是通过尽可能少的这种操作,使整个字符串最终变成非降序(即从左到右字母不减)。
要求:计算实现上述目标所需的最小操作次数;如果无论怎么操作都无法做到,则输出 -1。
1 <= s.length <= 100000。
s 仅由小写英文字母组成。
输入: s = "dog"。
输出: 1。
解释:
将子字符串 "og" 排序为 "go"。
现在,s = "dgo",已按升序排列。因此,答案是 1。
题目来自力扣3863。
一、函数逻辑分步拆解(逐段流程详解)
整体逻辑是分6层条件逐层判断,从最优解(0次)到最差解(3次)依次匹配,匹配到即直接返回答案,无需后续判断。
步骤1:判断原字符串是否已经全局升序
- 将字符串转为字节切片,调用有序校验函数检查完整数组是否非降;
- 若本身有序,不需要任何操作,直接返回操作次数0。
- 示例
dog原始序列 d→o→g,o > g,不满足升序,进入下一步判断。
步骤2:特殊边界判断——字符串长度等于2
规则:长度为2时,可选子串只能是单个字符(排序单个字符无变化),不存在有效操作能调换两个字符,永远无法排序,直接返回-1。
示例dog长度为3,跳过该分支,继续判断。
步骤3:找出全局最小字符 mn、全局最大字符 mx
遍历整个字符数组,提取全部字符里ASCII最小、最大的字母:
- s="dog" 字符ASCII:d=100,o=111,g=103
- mn = d(100),mx = o(111)
步骤4:判断能否仅用1次操作完成排序(返回1)
判定条件满足其一即可:
- 字符串第一个字符就是全局最小值 mn; 含义:首字符已经是全串最小,只需要对除去首字符的后半段连续子串做一次升序排序,后半段有序后整体必然全局升序;
- 字符串最后一个字符就是全局最大值 mx; 含义:末尾字符已经是全串最大,只需要对除去末尾字符的前半段连续子串做一次升序排序,前半段有序后整体必然全局升序。
代入示例 "dog" 验证
首字符 t[0] = d,正好等于全局最小值 mn,满足第一条判定条件,直接返回操作次数1,和题目输出结果一致。
操作对应解释:选取后两位连续子串og排序为go,得到dgo全局升序,仅1次操作。
补充该分支通用场景举例
例1:s="dxb",mn=d在首位,排序[x,b]一次即可; 例2:s="bdz",mx=z在末尾,排序[b,d]一次即可。
步骤5:不满足1次操作,判断能否仅用2次操作完成排序(返回2)
前置前提:首字符不是全局最小、末尾字符不是全局最大。
遍历中间区间t[1 : n-1](去掉第一个、最后一个字符的中间所有字符):
只要中间任意一个字符等于全局最小值mn 或 全局最大值mx,就返回2次操作。
原理说明
两种场景对应两次操作方案:
- 中间存在全局最小值mn:
第一次操作:排序
[0, n-2](除最后一位),把中间的最小值挪到字符串最左侧; 第二次操作:排序[1, n-1](除第一位),将后半段全部理顺,整体有序; - 中间存在全局最大值mx:
第一次操作:排序
[1, n-1](除第一位),把中间最大值挪到字符串最右侧; 第二次操作:排序[0, n-2](除最后一位),理顺前半段,整体有序。
步骤6:仅剩最坏场景,固定需要3次操作(返回3)
前置全部条件都不满足时,唯一剩余特征:
- 第一个字符是全局最大值mx;
- 最后一个字符是全局最小值mn;
- 中间所有字符既不含全局最小mn,也不含全局最大mx。
三次操作完整逻辑
- 第一次操作:排序子串
[0, n-2](去掉末尾),将首位的全局最大值移动到倒数第二位; - 第二次操作:排序子串
[1, n-1](去掉首位),把全局最大值移到最后一位,同时把末尾的全局最小值挪到第二位; - 第三次操作:再次排序子串
[0, n-2],将第二位的全局最小值移动到第一位,整个字符串完全升序。
二、无解场景总结
仅一种情况输出-1:字符串长度严格等于2,无论字符如何排列,都无法通过规则内操作实现升序。
三、时间复杂度分析
1. 各环节耗时拆解
- slices.IsSorted:完整遍历一次字符串,O(n);
- slices.Min、slices.Max:各遍历一次字符串,合计O(n);
- 中间区间循环
t[1:n-1]:最坏遍历n-2个字符,O(n); 所有步骤为顺序串行执行,无嵌套循环。
总时间复杂度
O(n),n为字符串长度,线性复杂度,可支持题目上限1e5长度输入。
四、额外空间复杂度分析
空间占用来源
仅创建一个字节切片t := []byte(s)存储字符串副本,占用n字节;其余变量(mn、mx、循环临时变量)为常数级空间,不随字符串长度变化。
总额外空间复杂度
O(n) 补充优化说明:若不复制完整切片,仅通过索引读取原字符串字符,可优化为O(1)常数空间;当前代码实现采用切片拷贝,因此空间为线性O(n)。
Go完整代码如下:
package main
import (
"fmt"
"slices"
)
func minOperations(s string) int {
t := []byte(s)
// s 已经是升序
if slices.IsSorted(t) {
return 0
}
n := len(t)
// 长为 2,无法排序
if n == 2 {
return -1
}
mn := slices.Min(t)
mx := slices.Max(t)
// 如果 s[0] 是最小值,排序 [1,n-1] 即可
// 如果 s[n-1] 是最大值,排序 [0,n-2] 即可
if t[0] == mn || t[n-1] == mx {
return 1
}
// 如果 [1,n-2] 中有最小值,那么先排序 [0,n-2],把最小值排在最前面,然后排序 [1,n-1] 即可
// 如果 [1,n-2] 中有最大值,那么先排序 [1,n-1],把最大值排在最后面,然后排序 [0,n-2] 即可
for _, ch := range t[1 : n-1] {
if ch == mn || ch == mx {
return 2
}
}
// 现在只剩下一种情况:s[0] 是最大值,s[n-1] 是最小值,且 [1,n-2] 不含最小值和最大值
// 先排序 [0,n-2],把最大值排到 n-2
// 然后排序 [1,n-1],把最大值排在最后面,且最小值排在 1
// 最后排序 [0,n-2],把最小值排在最前面
return 3
}
func main() {
s := "dog"
result := minOperations(s)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
from typing import List
def minOperations(s: str) -> int:
t = list(s)
n = len(t)
# s 已经是升序
if all(t[i] <= t[i+1] for i in range(n-1)):
return 0
# 长为 2,无法排序
if n == 2:
return -1
mn = min(t)
mx = max(t)
# 如果 s[0] 是最小值,排序 [1,n-1] 即可
# 如果 s[n-1] 是最大值,排序 [0,n-2] 即可
if t[0] == mn or t[n-1] == mx:
return 1
# 如果 [1,n-2] 中有最小值,那么先排序 [0,n-2],把最小值排在最前面,然后排序 [1,n-1] 即可
# 如果 [1,n-2] 中有最大值,那么先排序 [1,n-1],把最大值排在最后面,然后排序 [0,n-2] 即可
for ch in t[1:n-1]:
if ch == mn or ch == mx:
return 2
# 现在只剩下一种情况:s[0] 是最大值,s[n-1] 是最小值,且 [1,n-2] 不含最小值和最大值
# 先排序 [0,n-2],把最大值排到 n-2
# 然后排序 [1,n-1],把最大值排在最后面,且最小值排在 1
# 最后排序 [0,n-2],把最小值排在最前面
return 3
def main():
s = "dog"
result = minOperations(s)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;
int minOperations(string s) {
string t = s;
int n = t.length();
// s 已经是升序
if (is_sorted(t.begin(), t.end())) {
return 0;
}
// 长为 2,无法排序
if (n == 2) {
return -1;
}
char mn = *min_element(t.begin(), t.end());
char mx = *max_element(t.begin(), t.end());
// 如果 s[0] 是最小值,排序 [1,n-1] 即可
// 如果 s[n-1] 是最大值,排序 [0,n-2] 即可
if (t[0] == mn || t[n-1] == mx) {
return 1;
}
// 如果 [1,n-2] 中有最小值,那么先排序 [0,n-2],把最小值排在最前面,然后排序 [1,n-1] 即可
// 如果 [1,n-2] 中有最大值,那么先排序 [1,n-1],把最大值排在最后面,然后排序 [0,n-2] 即可
for (int i = 1; i < n - 1; i++) {
if (t[i] == mn || t[i] == mx) {
return 2;
}
}
// 现在只剩下一种情况:s[0] 是最大值,s[n-1] 是最小值,且 [1,n-2] 不含最小值和最大值
// 先排序 [0,n-2],把最大值排到 n-2
// 然后排序 [1,n-1],把最大值排在最后面,且最小值排在 1
// 最后排序 [0,n-2],把最小值排在最前面
return 3;
}
int main() {
string s = "dog";
int result = minOperations(s);
cout << result << endl;
return 0;
}