1370. Increasing Decreasing String
Problem Description
You need to reorder a given string s
using a specific alternating pattern algorithm.
The algorithm works by repeatedly cycling through two phases:
Phase 1 - Ascending Order:
- Find and remove the smallest character from the remaining string, append it to the result
- Find and remove the smallest character that is greater than the last appended character, append it to the result
- Keep repeating step 2 until no valid character can be found
Phase 2 - Descending Order: 4. Find and remove the largest character from the remaining string, append it to the result 5. Find and remove the largest character that is smaller than the last appended character, append it to the result 6. Keep repeating step 5 until no valid character can be found
Continue alternating between these two phases until all characters from the original string have been used.
Example walkthrough:
If s = "aaaabbbbcccc"
, the process would be:
- Phase 1: Pick 'a', then 'b' (> 'a'), then 'c' (> 'b')
- Phase 2: Pick 'c', then 'b' (< 'c'), then 'a' (< 'b')
- Repeat until all characters are used
- Result:
"abccba..."
Important notes:
- When a character appears multiple times, you can choose any occurrence
- The process continues until every character from the original string has been placed in the result
- The result string will have the same length as the input string
The solution uses a counting approach where it tracks the frequency of each character, then alternates between iterating through characters in alphabetical order [a-z]
and reverse alphabetical order [z-a]
, picking available characters according to the algorithm's rules.
Intuition
The key insight is recognizing that this alternating pattern of ascending and descending selection can be simplified into a repetitive pattern over the alphabet.
When we carefully analyze the algorithm, we notice that:
- In the ascending phase, we're essentially picking characters in alphabetical order
a, b, c, ...
as long as they exist - In the descending phase, we're picking characters in reverse alphabetical order
z, y, x, ...
as long as they exist
The crucial observation is that we don't need to track "what was the last character appended" because:
- When going ascending, if we iterate through
a
toz
in order and only pick available characters, we automatically satisfy the "greater than last appended" condition - When going descending, if we iterate through
z
toa
in order and only pick available characters, we automatically satisfy the "smaller than last appended" condition
This leads us to think about the problem differently: instead of complex tracking and comparisons, we can simply:
- Count the frequency of each character
- Create a pattern that alternates between
[a-z]
and[z-a]
- Keep cycling through this pattern, picking each character if it's still available (count > 0)
The pattern abc...xyz
followed by zyx...cba
naturally enforces the algorithm's rules. By concatenating these two sequences and repeatedly iterating through them, we ensure that we alternate between ascending and descending phases while using all available characters.
This transforms a seemingly complex state-tracking problem into a simple counting and iteration problem, where we just need to cycle through the pattern "abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba"
and pick available characters until we've used them all.
Solution Approach
The implementation follows a counting and simulation approach using these key components:
1. Character Frequency Counting:
First, we use a Counter
(hash table) to count the occurrences of each character in the input string s
. This allows us to track how many of each character we still need to place.
cnt = Counter(s)
2. Creating the Pattern: We construct a pattern string that represents one complete cycle of the algorithm - ascending followed by descending through the alphabet:
cs = ascii_lowercase + ascii_lowercase[::-1] # This creates: "abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba"
The ascii_lowercase
contains all lowercase letters a-z
, and ascii_lowercase[::-1]
reverses it to get z-a
.
3. Simulation Process: We iterate through this pattern repeatedly until we've placed all characters:
ans = []
while len(ans) < len(s):
for c in cs:
if cnt[c]:
ans.append(c)
cnt[c] -= 1
For each character c
in our pattern:
- Check if we still have that character available (
cnt[c] > 0
) - If yes, append it to our answer and decrement its count
- If no, skip to the next character in the pattern
4. Result Construction:
Once we've placed all characters (when len(ans) == len(s)
), we join the list into a final string:
return "".join(ans)
Why This Works:
- The pattern naturally enforces the ascending-descending alternation
- By iterating through
a-z
first, we automatically pick the smallest available character, then the next smallest that's greater, and so on - By iterating through
z-a
next, we pick the largest available, then the next largest that's smaller, and so on - The continuous cycling through this pattern ensures we follow the algorithm's rules while using all characters
Time Complexity: O(n)
where n
is the length of the input string, as we process each character exactly once.
Space Complexity: O(1)
for the counter (at most 26 entries) plus O(n)
for the answer string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with s = "aabbcc"
:
Step 1: Count character frequencies
cnt = {'a': 2, 'b': 2, 'c': 2}
Step 2: Create the pattern
- Pattern =
"abcdefghijklmnopqrstuvwxyzzyxwvutsrqponmlkjihgfedcba"
Step 3: Iterate through the pattern and build result
First pass through pattern:
- Check 'a':
cnt['a'] = 2 > 0
→ append 'a',cnt['a'] = 1
, result ="a"
- Check 'b':
cnt['b'] = 2 > 0
→ append 'b',cnt['b'] = 1
, result ="ab"
- Check 'c':
cnt['c'] = 2 > 0
→ append 'c',cnt['c'] = 1
, result ="abc"
- Check 'd' through 'z': all have count 0, skip
- Now descending phase:
- Check 'z' through 'd': all have count 0, skip
- Check 'c':
cnt['c'] = 1 > 0
→ append 'c',cnt['c'] = 0
, result ="abcc"
- Check 'b':
cnt['b'] = 1 > 0
→ append 'b',cnt['b'] = 0
, result ="abccb"
- Check 'a':
cnt['a'] = 1 > 0
→ append 'a',cnt['a'] = 0
, result ="abccba"
Step 4: Check completion
len(result) = 6 = len(s)
, so we're done!
Final Result: "abccba"
Notice how the pattern naturally creates the alternating ascending-descending behavior:
- First we picked in order: a → b → c (ascending)
- Then we picked in reverse: c → b → a (descending)
- Each character was used exactly as many times as it appeared in the original string
Solution Implementation
1from collections import Counter
2from string import ascii_lowercase
3
4class Solution:
5 def sortString(self, s: str) -> str:
6 # Count frequency of each character in the input string
7 char_count = Counter(s)
8
9 # Create a pattern: a-z followed by z-a for alternating ascending/descending order
10 pattern = ascii_lowercase + ascii_lowercase[::-1]
11
12 # Result list to build the output string
13 result = []
14
15 # Continue until we've used all characters from the original string
16 while len(result) < len(s):
17 # Iterate through the pattern (a-z, then z-a)
18 for char in pattern:
19 # If this character is still available
20 if char_count[char] > 0:
21 # Add it to the result
22 result.append(char)
23 # Decrease its count
24 char_count[char] -= 1
25
26 # Join the result list into a string and return
27 return "".join(result)
28
1class Solution {
2 public String sortString(String s) {
3 // Create frequency array to count occurrences of each character (a-z)
4 int[] charFrequency = new int[26];
5 int stringLength = s.length();
6
7 // Count the frequency of each character in the input string
8 for (int i = 0; i < stringLength; i++) {
9 charFrequency[s.charAt(i) - 'a']++;
10 }
11
12 // Build the result string using StringBuilder for efficiency
13 StringBuilder result = new StringBuilder();
14
15 // Continue building until we've used all characters
16 while (result.length() < stringLength) {
17 // Step 1: Append smallest to largest characters (ascending order)
18 for (int i = 0; i < 26; i++) {
19 if (charFrequency[i] > 0) {
20 // Append the character and decrement its count
21 result.append((char) ('a' + i));
22 charFrequency[i]--;
23 }
24 }
25
26 // Step 2: Append largest to smallest characters (descending order)
27 for (int i = 25; i >= 0; i--) {
28 if (charFrequency[i] > 0) {
29 // Append the character and decrement its count
30 result.append((char) ('a' + i));
31 charFrequency[i]--;
32 }
33 }
34 }
35
36 // Convert StringBuilder to String and return
37 return result.toString();
38 }
39}
40
1class Solution {
2public:
3 string sortString(string s) {
4 // Array to store the frequency of each character (a-z)
5 int charFrequency[26]{};
6
7 // Count the frequency of each character in the input string
8 for (char& c : s) {
9 ++charFrequency[c - 'a'];
10 }
11
12 string result;
13
14 // Continue building the result string until all characters are used
15 while (result.size() < s.size()) {
16 // Step 1: Pick the smallest character and append to result (ascending order)
17 for (int i = 0; i < 26; ++i) {
18 if (charFrequency[i] > 0) {
19 result += static_cast<char>(i + 'a');
20 --charFrequency[i];
21 }
22 }
23
24 // Step 2: Pick the largest character and append to result (descending order)
25 for (int i = 25; i >= 0; --i) {
26 if (charFrequency[i] > 0) {
27 result += static_cast<char>(i + 'a');
28 --charFrequency[i];
29 }
30 }
31 }
32
33 return result;
34 }
35};
36
1/**
2 * Sorts a string by alternating between ascending and descending order of characters.
3 * First adds all characters in ascending order (a to z), then in descending order (z to a),
4 * and repeats until all characters are used.
5 *
6 * @param s - The input string to be sorted
7 * @returns The sorted string with alternating ascending/descending pattern
8 */
9function sortString(s: string): string {
10 // Create an array to count frequency of each letter (26 letters in alphabet)
11 const characterCount: number[] = Array(26).fill(0);
12
13 // Count the frequency of each character in the input string
14 for (const character of s) {
15 // Calculate index by subtracting ASCII value of 'a' from current character
16 const index: number = character.charCodeAt(0) - 'a'.charCodeAt(0);
17 characterCount[index]++;
18 }
19
20 // Array to build the result string
21 const result: string[] = [];
22
23 // Continue until all characters from the original string are processed
24 while (result.length < s.length) {
25 // First pass: Add characters in ascending order (a to z)
26 for (let i = 0; i < 26; i++) {
27 if (characterCount[i] > 0) {
28 // Convert index back to character and add to result
29 const character: string = String.fromCharCode(i + 'a'.charCodeAt(0));
30 result.push(character);
31 characterCount[i]--;
32 }
33 }
34
35 // Second pass: Add characters in descending order (z to a)
36 for (let i = 25; i >= 0; i--) {
37 if (characterCount[i] > 0) {
38 // Convert index back to character and add to result
39 const character: string = String.fromCharCode(i + 'a'.charCodeAt(0));
40 result.push(character);
41 characterCount[i]--;
42 }
43 }
44 }
45
46 // Join the array of characters into a single string and return
47 return result.join('');
48}
49
Time and Space Complexity
Time Complexity: O(n × |Σ|)
where n
is the length of the string s
and |Σ| = 26
(the size of the lowercase alphabet).
The algorithm uses a while loop that continues until we've processed all n
characters from the original string. In each iteration of the while loop, we iterate through the concatenated string cs
which contains all 26 lowercase letters twice (forward and backward), giving us 2 × 26 = 52
iterations. However, this is still O(|Σ|)
since 52 is a constant multiple of 26.
The while loop executes until len(ans) == len(s)
, meaning we add exactly n
characters total. In the worst case, we might need to go through multiple complete passes of the alphabet to collect all characters. The maximum number of passes needed would be when all characters are the same (requiring n/1
passes through that single character's position in cs
), or when characters are distributed such that we need n/26
passes to collect all occurrences.
Therefore, the total time complexity is O(n × |Σ|)
or O(26n)
which simplifies to O(n)
when treating the alphabet size as a constant.
Space Complexity: O(|Σ|)
where |Σ| = 26
.
The space is used for:
cnt
: A Counter object storing at most 26 unique characters, usingO(|Σ|)
spacecs
: A string containing 52 characters (26 letters forward + 26 letters backward), which isO(|Σ|)
spaceans
: A list that grows to sizen
, but this is the output and typically not counted in auxiliary space complexity
The auxiliary space complexity is therefore O(|Σ|)
or O(26)
, which is O(1)
when treating the alphabet size as a constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Algorithm's Continuous Nature
The Pitfall: Many developers initially think that each phase must find ALL possible characters in that direction before switching phases. For example, they might try to extract all ascending characters possible in Phase 1, then all descending characters in Phase 2, leading to incorrect implementations like:
# INCORRECT approach
def sortString(self, s: str) -> str:
char_count = Counter(s)
result = []
while char_count:
# Try to get ALL ascending characters
for c in ascii_lowercase:
while char_count[c] > 0: # Wrong! Takes all occurrences at once
result.append(c)
char_count[c] -= 1
# Then get ALL descending characters
for c in ascii_lowercase[::-1]:
while char_count[c] > 0:
result.append(c)
char_count[c] -= 1
return "".join(result)
This would produce "aaaabbbbcccc" instead of "abccba..." for input "aaaabbbbcccc".
The Solution: The algorithm picks ONE character at a time from each available position in the pattern. The correct approach uses a single pass through the pattern for each cycle:
# CORRECT approach for char in pattern: # pattern = "abc...xyz" + "zyx...cba" if char_count[char] > 0: result.append(char) char_count[char] -= 1 # Only take ONE occurrence
2. Incorrect Pattern Construction
The Pitfall: Creating the wrong alternating pattern or forgetting to include both directions:
# WRONG: Only ascending
pattern = ascii_lowercase # Just "abcdefg..."
# WRONG: Separate loops instead of combined pattern
while len(result) < len(s):
for c in ascii_lowercase: # First loop
if char_count[c]:
result.append(c)
char_count[c] -= 1
for c in ascii_lowercase[::-1]: # Second loop
if char_count[c]:
result.append(c)
char_count[c] -= 1
The separate loops approach fails because it processes the entire alphabet twice per iteration rather than treating the ascending and descending as a single continuous pattern.
The Solution: Concatenate both directions into a single pattern that represents one complete cycle:
pattern = ascii_lowercase + ascii_lowercase[::-1] # Creates: "abcdefg...xyzzyxw...cba"
3. Early Termination Issues
The Pitfall: Breaking out of loops prematurely or not continuing until all characters are used:
# WRONG: Stops after one cycle through the pattern for char in pattern: if char_count[char] > 0: result.append(char) char_count[char] -= 1 return "".join(result) # Missing characters!
The Solution: Use a while loop that continues until all characters from the original string are placed:
while len(result) < len(s): # Keep going until we've used all characters
for char in pattern:
if char_count[char] > 0:
result.append(char)
char_count[char] -= 1
The key insight is that the pattern needs to be traversed multiple times to use all character occurrences, especially when characters appear more than twice in the input string.
Which technique can we use to find the middle of a linked list?
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 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
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 represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!