Facebook Pixel

1370. Increasing Decreasing String

EasyHash TableStringCounting
Leetcode Link

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:

  1. Find and remove the smallest character from the remaining string, append it to the result
  2. Find and remove the smallest character that is greater than the last appended character, append it to the result
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. When going ascending, if we iterate through a to z in order and only pick available characters, we automatically satisfy the "greater than last appended" condition
  2. When going descending, if we iterate through z to a 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 Evaluator

Example 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, using O(|Σ|) space
  • cs: A string containing 52 characters (26 letters forward + 26 letters backward), which is O(|Σ|) space
  • ans: A list that grows to size n, 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More