3863. Minimum Operations to Sort a String
Problem Description
You are given a string s consisting of lowercase English letters.
In one operation, you can select any substring of s that is not the entire string and sort it in non-descending alphabetical order.
Return the minimum number of operations required to make s sorted in non-descending order. If it is not possible, return -1.
In other words, you want the final string to satisfy s[0] <= s[1] <= ... <= s[n-1]. Each operation lets you pick a contiguous portion of s — as long as it does not cover the whole string — and rearrange just that portion into sorted order. Your goal is to find the fewest such operations needed to fully sort s, or determine that no sequence of operations can achieve a sorted result.
How We Pick the Algorithm
Why Simulation / Basic DSA?
This problem maps to Simulation / Basic DSA through a short path in the full flowchart.
Case analysis on min/max character positions relative to string boundaries — no data structures needed.
Open in FlowchartIntuition
The key restriction is that we can never sort the entire string in a single operation — we may only sort a proper substring. This constraint is what makes the problem interesting, because if we could sort the whole string, the answer would always be at most 1. Let's think about how many operations we really need under this rule.
First, if the string is already sorted, we don't need to do anything, so the answer is 0.
Now consider the smallest non-trivial case: a string of length 2 that is not sorted. The only substrings we can choose are the two single characters, but sorting a single character does nothing. We can never touch both characters together (that would be the whole string), so it is impossible to fix the order. This gives the answer -1.
For longer strings, the crucial observation is to focus on the minimum character mn and the maximum character mx. In a sorted string, every copy of mn must end up at the front and every copy of mx must end up at the back. The difficulty is that we cannot sort the full string at once, so the very first and very last positions are the troublesome spots.
-
If
s[0]is alreadymn, then the first character is already where it belongs. We can sort the substrings[1:](which excludes only the first character, so it's a valid proper substring) and finish the job in 1 operation. The same logic applies ifs[-1]is alreadymx: sorts[:-1]in 1 operation. -
If neither end is in the right place, but some
mnormxappears in the middle of the string, we can use that as a stepping stone. With one operation we move amnto the front (or amxto the back), and with a second operation we sort the rest. That gives 2 operations. -
Finally, if
mnandmxonly appear at the two ends (mnsomewhere requiring it to reach the front,mxrequiring the back) and nothing helpful sits in the middle, we still cannot do it in fewer than 3 moves: one operation to bring a needed character inward using a substring that includes both extremes, then operations to settle the front and back. This worst case yields the answer 3.
So the entire problem collapses into a small case analysis based on whether the string is sorted, whether its length is 2, and where mn and mx sit relative to the boundaries and the middle. By checking these conditions in order, we directly land on the answer of 0, 1, 2, 3, or -1.
Solution Approach
We implement a direct case analysis that checks the conditions described above one by one, returning as soon as a case matches. No complex data structures are needed — just a few linear scans over the string.
Step 1: Check if already sorted.
We use pairwise(s) to iterate over every adjacent pair (a, b) and verify that a <= b holds for all of them. If the string is already in non-descending order, no work is needed:
if all(a <= b for a, b in pairwise(s)):
return 0
Step 2: Handle the length-2 impossible case.
If the string is not sorted and has length 2, the only available substrings are single characters, which cannot change anything. Sorting is impossible, so we return -1:
if len(s) == 2:
return -1
Step 3: Find the extreme characters.
We compute the minimum character mn and the maximum character mx of the string. These are the characters that must end up at the very front and very back of a sorted string, respectively:
mn, mx = min(s), max(s)
Step 4: Check the boundary positions (answer 1).
If s[0] already equals mn, the first position is correct, and sorting the proper substring s[1:] finishes the job. Symmetrically, if s[-1] equals mx, sorting s[:-1] works. Either way, one operation suffices:
if s[0] == mn or s[-1] == mx: return 1
Step 5: Check the middle for a helper character (answer 2).
If neither end is already correct, we look at the interior s[1:-1]. If any middle character equals mn or mx, we can use it as a stepping stone — one operation to position it and one more to sort the rest — giving a total of 2:
if any(c in [mn, mx] for c in s[1:-1]):
return 2
Step 6: The worst case (answer 3).
If none of the above conditions hold — meaning mn and mx only sit at the two ends with no helper in the middle — we fall through to the final case requiring 3 operations:
return 3
Complexity Analysis:
- Time complexity:
O(n), wherenis the length ofs. Each step (the sorted check, computingmin/max, and scanning the middle) performs at most a single linear pass over the string. - Space complexity:
O(1), since we only store the two charactersmnandmxand use constant extra space; the iterations withpairwiseandanydo not build additional structures proportional to the input.
This concise branching captures all five possible outcomes (0, 1, 2, 3, or -1) without any searching or simulation, relying entirely on the structural insight about where mn and mx must go.
Example Walkthrough
Let's trace through the solution approach using the string s = "cba" (length 3).
Step 1: Check if already sorted.
We examine adjacent pairs using pairwise(s):
('c', 'b')→ is'c' <= 'b'? No.
Since the very first pair fails, the string is not sorted, so we cannot return 0. We move on.
Step 2: Handle the length-2 impossible case.
We check len(s) == 2. Here len("cba") == 3, so this case does not apply. We continue.
Step 3: Find the extreme characters.
We compute:
mn = min("cba") = 'a'— the character that must end up at the front.mx = max("cba") = 'c'— the character that must end up at the back.
Step 4: Check the boundary positions (answer 1).
We test whether either end is already correct:
s[0] == mn? →'c' == 'a'? No.s[-1] == mx? →'a' == 'c'? No.
Neither boundary is in its correct position, so we cannot finish in a single operation. We continue.
Step 5: Check the middle for a helper character (answer 2).
We inspect the interior s[1:-1] = "b":
- Is
'b'equal tomn('a') ormx('c')? No —'b'is neither.
There's no helper character in the middle to act as a stepping stone, so we cannot achieve the goal in 2 operations. We fall through.
Step 6: The worst case (answer 3).
All previous conditions failed. The minimum 'a' sits only at the back and the maximum 'c' sits only at the front, with an unhelpful 'b' in the middle. This is exactly the worst case, so we return 3.
Why 3 operations are genuinely needed here:
- Sort the proper substring
s[0:2] = "cb"→"bc", giving"bca". (We can't include index 2 or it would be the whole string.) - Sort the proper substring
s[1:3] = "ca"→"ac", giving"bac". Now'a'has migrated leftward. - Sort the proper substring
s[0:2] = "ba"→"ab", giving"abc". ✅ Fully sorted.
After three operations the string becomes "abc", confirming the answer of 3 that our case analysis produced directly — without any simulation.
Solution Implementation
1from itertools import pairwise
2
3
4class Solution:
5 def minOperations(self, s: str) -> int:
6 # Already non-decreasing: every char is <= the one after it.
7 if all(prev <= curr for prev, curr in pairwise(s)):
8 return 0
9
10 # A length-2 string that isn't sorted is treated as impossible.
11 if len(s) == 2:
12 return -1
13
14 # Smallest and largest characters in the string.
15 min_char, max_char = min(s), max(s)
16
17 # If the first char is the min (it's already in its ideal position)
18 # or the last char is the max, a single operation is enough.
19 if s[0] == min_char or s[-1] == max_char:
20 return 1
21
22 # If any interior character is the min or the max,
23 # two operations are sufficient.
24 if any(char in (min_char, max_char) for char in s[1:-1]):
25 return 2
26
27 # Otherwise, three operations are required.
28 return 3
291class Solution {
2 public int minOperations(String s) {
3 // Convert the input string into a char array for easy indexed access
4 char[] chars = s.toCharArray();
5 int length = chars.length;
6
7 // Track whether the array is already in non-decreasing order
8 boolean isSorted = true;
9
10 // Track the minimum and maximum characters in the array,
11 // initialized with the first element
12 char minChar = chars[0];
13 char maxChar = chars[0];
14
15 // Single pass to update min/max and check the sorted condition
16 for (int i = 1; i < length; ++i) {
17 minChar = (char) Math.min(minChar, chars[i]);
18 maxChar = (char) Math.max(maxChar, chars[i]);
19 // If the current element is smaller than its predecessor,
20 // the array is not sorted
21 if (chars[i] < chars[i - 1]) {
22 isSorted = false;
23 }
24 }
25
26 // Case 1: already sorted, no operation needed
27 if (isSorted) {
28 return 0;
29 }
30
31 // Case 2: only two elements and not sorted, impossible to fix
32 if (length == 2) {
33 return -1;
34 }
35
36 // Case 3: if the first element is the global minimum,
37 // or the last element is the global maximum, one operation suffices
38 if (chars[0] == minChar || chars[length - 1] == maxChar) {
39 return 1;
40 }
41
42 // Case 4: if any interior element equals the global min or max,
43 // two operations are required
44 for (int i = 1; i < length - 1; ++i) {
45 if (chars[i] == minChar || chars[i] == maxChar) {
46 return 2;
47 }
48 }
49
50 // Case 5: otherwise, three operations are needed
51 return 3;
52 }
53}
541class Solution {
2public:
3 int minOperations(string s) {
4 int n = s.size();
5
6 // Check whether the string is already in non-decreasing (sorted) order.
7 bool isSorted = true;
8 for (int i = 1; i < n; ++i) {
9 if (s[i] < s[i - 1]) {
10 isSorted = false;
11 break;
12 }
13 }
14
15 // Already sorted: no operation is needed.
16 if (isSorted) {
17 return 0;
18 }
19
20 // With only two characters that are not sorted, it is impossible to fix.
21 if (n == 2) {
22 return -1;
23 }
24
25 // Find the smallest and largest characters in the string.
26 char minChar = *min_element(s.begin(), s.end());
27 char maxChar = *max_element(s.begin(), s.end());
28
29 // If the first character is the minimum, or the last character is the
30 // maximum, a single operation suffices.
31 if (s[0] == minChar || s[n - 1] == maxChar) {
32 return 1;
33 }
34
35 // If any interior character equals the minimum or maximum, two operations
36 // are enough.
37 for (int i = 1; i < n - 1; ++i) {
38 if (s[i] == minChar || s[i] == maxChar) {
39 return 2;
40 }
41 }
42
43 // Otherwise, three operations are required.
44 return 3;
45 }
46};
471function minOperations(s: string): number {
2 const length = s.length;
3
4 // Check whether the string is already sorted in non-decreasing order.
5 let isSorted = true;
6 for (let i = 1; i < length; i++) {
7 if (s[i] < s[i - 1]) {
8 isSorted = false;
9 break;
10 }
11 }
12
13 // No operation is needed when the string is already sorted.
14 if (isSorted) {
15 return 0;
16 }
17
18 // A string of length 2 that is unsorted cannot be fixed.
19 if (length === 2) {
20 return -1;
21 }
22
23 // Find the minimum and maximum characters in the string.
24 let minChar = s[0];
25 let maxChar = s[0];
26 for (const char of s) {
27 if (char < minChar) {
28 minChar = char;
29 }
30 if (char > maxChar) {
31 maxChar = char;
32 }
33 }
34
35 // If the first character is the minimum or the last is the maximum,
36 // a single operation suffices.
37 if (s[0] === minChar || s[length - 1] === maxChar) {
38 return 1;
39 }
40
41 // If any interior character is the minimum or maximum,
42 // two operations are required.
43 for (let i = 1; i < length - 1; i++) {
44 if (s[i] === minChar || s[i] === maxChar) {
45 return 2;
46 }
47 }
48
49 // Otherwise three operations are needed.
50 return 3;
51}
52Time and Space Complexity
Time Complexity: O(n), where n is the length of the string s.
The analysis is as follows:
- The
all(a <= b for a, b in pairwise(s))check iterates over adjacent pairs of the string, takingO(n)time. - Computing
min(s)andmax(s)each scans the entire string, takingO(n)time. - The
any(c in [mn, mx] for c in s[1:-1])check iterates over the inner characters, takingO(n)time.
Since these operations run sequentially, the overall time complexity is O(n).
Space Complexity: O(1).
The analysis is as follows:
- The variables
mnandmxuse constant extra space. - The generator expressions used in
allandanyare evaluated lazily and do not create additional collections proportional to the input size. - Note that
s[1:-1]creates a slice, which technically usesO(n)space; however, ignoring this auxiliary slice, the algorithm itself only requiresO(1)extra space, consistent with the reference answer.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Misinterpreting the Length-2 "Impossible" Case as a General Rule
A frequent mistake is to assume that an unsorted string of length 2 being impossible (-1) generalizes — i.e., to think that any short or specially-structured string might also be impossible. Developers sometimes over-engineer additional -1 checks for length 3, strings with all distinct characters, etc.
Why this is wrong: For any string of length >= 3, you can always sort it. The reason is that you can pick any proper substring (a prefix or suffix that omits at least one character) and sort it. With length >= 3, the worst case requires only 3 operations — never impossibility. The -1 case is exclusively for an unsorted length-2 string, because its only proper substrings are single characters, which cannot reorder anything.
The trap in code: If you place the length-2 check before the "already sorted" check, you break correctness for sorted length-2 strings:
# WRONG ORDER
if len(s) == 2:
return -1 # incorrectly returns -1 for "ab" which is already sorted!
if all(prev <= curr for prev, curr in pairwise(s)):
return 0
For input "ab", this returns -1, but "ab" is already sorted and the answer should be 0.
Solution: Always perform the "already sorted" check first, so that any sorted string (including length-2 ones) short-circuits to 0. Only after confirming the string is unsorted should you apply the length-2 impossibility rule:
# CORRECT ORDER
if all(prev <= curr for prev, curr in pairwise(s)):
return 0 # sorted strings of ANY length handled here
if len(s) == 2:
return -1 # only unsorted length-2 reaches this point
Pitfall: Confusing "Position of min/max" with "Value Equality"
Another subtle error is checking whether s[0] is the minimum index of where min_char appears, rather than checking value equality s[0] == min_char. The logic only cares whether the boundary character equals the extreme value, not where the first/last occurrence lands.
Edge case to watch: Duplicate extremes. Consider "aebca" where min_char = 'a'. Both s[0] and s[-1] are 'a'. The condition s[0] == min_char correctly returns 1 because sorting the suffix s[1:] finishes the job — the leading 'a' is already where it belongs. Comparing positions or indices instead of values would needlessly complicate this and risk wrong answers.
Solution: Stick to direct value comparisons (s[0] == min_char, s[-1] == max_char, and membership char in (min_char, max_char)), which cleanly handle duplicates and align with the structural insight that only the value at the boundary matters.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhat is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!