Facebook Pixel

358. Rearrange String k Distance Apart 🔒

Problem Description

You are given a string s and an integer k. Your task is to rearrange the characters in the string such that any two identical characters are separated by at least k distance from each other.

For example, if you have the character 'a' at position i, then the next 'a' cannot appear until position i + k or later.

If it's impossible to create such a rearrangement where all identical characters maintain this minimum distance requirement, you should return an empty string "".

The key challenge is to strategically place characters to ensure the distance constraint is satisfied for all characters. Characters that appear more frequently in the original string will be harder to place since they need more positions while maintaining the required spacing.

Example scenarios:

  • If s = "aabbcc" and k = 3, one valid rearrangement could be "abcabc" where each pair of identical characters is exactly 3 positions apart
  • If s = "aaabc" and k = 3, it would be impossible to arrange since we have three 'a's that each need to be at least 3 positions apart, requiring more total positions than available
  • If k = 0 or k = 1, any arrangement works since there's no effective distance requirement
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to greedily place the most frequent characters first to avoid getting stuck later. Think about it this way: if a character appears many times, it needs many "slots" with proper spacing. If we delay placing these high-frequency characters, we might run out of valid positions.

Consider a greedy strategy: always pick the character with the highest remaining count that is currently available (not in the recent k-1 positions). This ensures we're always making progress on the characters that are hardest to place.

To implement this strategy, we need two main components:

  1. A max heap to always know which character has the highest remaining count among those currently available to place
  2. A cooling queue to temporarily hold characters that were recently used and cannot be placed yet

The process works like this:

  • Pick the character with the highest count from the heap
  • Place it in the result
  • Decrease its count and put it in a "cooling" queue
  • After exactly k positions, the character becomes available again and returns to the heap

The cooling mechanism is crucial: when we place a character at position i, it cannot be used again until position i+k. By using a queue of size k, we naturally enforce this constraint - a character enters the queue after being used and only exits (becoming available again) after k other characters have been placed.

If at any point we cannot place a character (the heap is empty but we still have characters to place), it means the arrangement is impossible. This happens when the cooling queue contains characters that still need to be placed, but there are no available characters to fill the gap.

The beauty of this approach is that it automatically handles the distance constraint through the queue mechanism while ensuring we prioritize characters that are most likely to cause problems if delayed.

Solution Approach

Let's walk through the implementation step by step:

1. Character Frequency Count and Max Heap Setup

h = [(-v, c) for c, v in Counter(s).items()]
heapify(h)
  • First, we count the frequency of each character using Counter(s)
  • We create a max heap by negating the frequencies (Python's heapq is a min heap by default)
  • Each heap element is a tuple (-frequency, character)

2. Initialize Data Structures

q = deque()
ans = []
  • q is our cooling queue that holds characters temporarily unavailable
  • ans accumulates our result string

3. Main Processing Loop

while h:
    v, c = heappop(h)
    v *= -1
    ans.append(c)
    q.append((v - 1, c))
  • While we have available characters in the heap:
    • Pop the character with highest frequency
    • Convert back to positive frequency (v *= -1)
    • Add this character to our result
    • Decrease its count and add to cooling queue

4. Cooling Queue Management

if len(q) >= k:
    w, c = q.popleft()
    if w:
        heappush(h, (-w, c))
  • Once the cooling queue reaches size k, the oldest character has "cooled down"
  • We remove it from the queue
  • If it still has remaining count (w > 0), push it back to the heap with negated frequency

5. Validation and Return

return "" if len(ans) != len(s) else "".join(ans)
  • If we successfully placed all characters, len(ans) equals len(s)
  • Otherwise, some characters got stuck in the cooling queue, making arrangement impossible

Why This Works:

The algorithm ensures that after placing a character, it won't be available for selection until exactly k other characters have been placed. The max heap guarantees we always choose the most frequent available character, preventing scenarios where high-frequency characters get blocked later.

Time Complexity: O(n log m) where n is the length of string and m is the number of unique characters Space Complexity: O(m) for the heap and queue

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with s = "aabbcc" and k = 3.

Initial Setup:

  • Character frequencies: {'a': 2, 'b': 2, 'c': 2}
  • Max heap (negated): [(-2, 'a'), (-2, 'b'), (-2, 'c')]
  • Cooling queue: []
  • Result: []

Iteration 1:

  • Pop from heap: (-2, 'c') → frequency = 2
  • Add 'c' to result: ['c']
  • Add to cooling queue with decreased count: [(1, 'c')]
  • Queue size (1) < k (3), so nothing returns to heap
  • Heap: [(-2, 'a'), (-2, 'b')]

Iteration 2:

  • Pop from heap: (-2, 'b') → frequency = 2
  • Add 'b' to result: ['c', 'b']
  • Add to cooling queue: [(1, 'c'), (1, 'b')]
  • Queue size (2) < k (3), so nothing returns to heap
  • Heap: [(-2, 'a')]

Iteration 3:

  • Pop from heap: (-2, 'a') → frequency = 2
  • Add 'a' to result: ['c', 'b', 'a']
  • Add to cooling queue: [(1, 'c'), (1, 'b'), (1, 'a')]
  • Queue size (3) = k (3), so first element (1, 'c') exits queue
  • Since count = 1 > 0, push 'c' back to heap with (-1, 'c')
  • Heap: [(-1, 'c')]

Iteration 4:

  • Pop from heap: (-1, 'c') → frequency = 1
  • Add 'c' to result: ['c', 'b', 'a', 'c']
  • Add to cooling queue: [(1, 'b'), (1, 'a'), (0, 'c')]
  • Queue size (3) = k (3), so (1, 'b') exits queue
  • Push 'b' back to heap with (-1, 'b')
  • Heap: [(-1, 'b')]

Iteration 5:

  • Pop from heap: (-1, 'b') → frequency = 1
  • Add 'b' to result: ['c', 'b', 'a', 'c', 'b']
  • Add to cooling queue: [(1, 'a'), (0, 'c'), (0, 'b')]
  • Queue size (3) = k (3), so (1, 'a') exits queue
  • Push 'a' back to heap with (-1, 'a')
  • Heap: [(-1, 'a')]

Iteration 6:

  • Pop from heap: (-1, 'a') → frequency = 1
  • Add 'a' to result: ['c', 'b', 'a', 'c', 'b', 'a']
  • Add to cooling queue: [(0, 'c'), (0, 'b'), (0, 'a')]
  • Queue size (3) = k (3), so (0, 'c') exits queue
  • Count = 0, so don't push back to heap
  • Heap: [] (empty)

Final Check:

  • Heap is empty, loop ends
  • Result length (6) = original string length (6) ✓
  • Return: "cbacba"

Each identical character is exactly 3 positions apart: c's at positions 0 and 3, b's at positions 1 and 4, a's at positions 2 and 5.

Solution Implementation

1from collections import Counter, deque
2from heapq import heapify, heappop, heappush
3
4class Solution:
5    def rearrangeString(self, s: str, k: int) -> str:
6        # Edge case: if k is 0 or 1, any arrangement works
7        if k <= 1:
8            return s
9      
10        # Count frequency of each character and create max heap
11        # Using negative values to simulate max heap (since Python has min heap by default)
12        char_freq = Counter(s)
13        max_heap = [(-freq, char) for char, freq in char_freq.items()]
14        heapify(max_heap)
15      
16        # Queue to hold characters that are in cooldown period
17        cooldown_queue = deque()
18      
19        # Result list to build the rearranged string
20        result = []
21      
22        # Process characters from heap
23        while max_heap:
24            # Get the character with highest frequency
25            neg_freq, char = heappop(max_heap)
26            freq = -neg_freq
27          
28            # Add character to result
29            result.append(char)
30          
31            # Add character to cooldown queue with decremented frequency
32            cooldown_queue.append((freq - 1, char))
33          
34            # If cooldown period is over (queue has k elements), 
35            # the first element can be added back to heap
36            if len(cooldown_queue) >= k:
37                remaining_freq, cooldown_char = cooldown_queue.popleft()
38                if remaining_freq > 0:
39                    heappush(max_heap, (-remaining_freq, cooldown_char))
40      
41        # If we couldn't place all characters, return empty string
42        # This happens when the cooldown constraint cannot be satisfied
43        if len(result) != len(s):
44            return ""
45      
46        return "".join(result)
47
1class Solution {
2    public String rearrangeString(String s, int k) {
3        int length = s.length();
4      
5        // Count frequency of each character
6        int[] charFrequency = new int[26];
7        for (char ch : s.toCharArray()) {
8            charFrequency[ch - 'a']++;
9        }
10      
11        // Create max heap based on character frequency
12        // Each element is [frequency, character_index]
13        PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b) -> b[0] - a[0]);
14        for (int i = 0; i < 26; i++) {
15            if (charFrequency[i] > 0) {
16                maxHeap.offer(new int[] {charFrequency[i], i});
17            }
18        }
19      
20        // Queue to hold characters that are in cooldown period
21        Deque<int[]> cooldownQueue = new ArrayDeque<>();
22        StringBuilder result = new StringBuilder();
23      
24        // Process characters greedily
25        while (!maxHeap.isEmpty()) {
26            // Get character with highest frequency
27            int[] current = maxHeap.poll();
28            int frequency = current[0];
29            int charIndex = current[1];
30          
31            // Append character to result
32            result.append((char) ('a' + charIndex));
33          
34            // Add character to cooldown queue with decremented frequency
35            cooldownQueue.offer(new int[] {frequency - 1, charIndex});
36          
37            // If cooldown period is over (queue size >= k), 
38            // release the first character from cooldown
39            if (cooldownQueue.size() >= k) {
40                int[] released = cooldownQueue.pollFirst();
41                // If character still has remaining frequency, add back to heap
42                if (released[0] > 0) {
43                    maxHeap.offer(released);
44                }
45            }
46        }
47      
48        // Check if all characters were successfully placed
49        return result.length() == length ? result.toString() : "";
50    }
51}
52
1class Solution {
2public:
3    string rearrangeString(string s, int k) {
4        // Count frequency of each character
5        unordered_map<char, int> charFrequency;
6        for (char c : s) {
7            ++charFrequency[c];
8        }
9      
10        // Create max heap with pairs of (frequency, character)
11        // Heap will prioritize characters with higher frequency
12        priority_queue<pair<int, char>> maxHeap;
13        for (auto& [character, frequency] : charFrequency) {
14            maxHeap.push({frequency, character});
15        }
16      
17        // Queue to hold characters that are in cooldown period
18        // Each character must wait k positions before being used again
19        queue<pair<int, char>> cooldownQueue;
20        string result;
21      
22        // Greedily build the result string
23        while (!maxHeap.empty()) {
24            // Get the character with highest frequency
25            auto [frequency, character] = maxHeap.top();
26            maxHeap.pop();
27          
28            // Add character to result
29            result += character;
30          
31            // Put character in cooldown with decremented frequency
32            cooldownQueue.push({frequency - 1, character});
33          
34            // If cooldown period is over (queue has k elements),
35            // the front element can be reused
36            if (cooldownQueue.size() >= k) {
37                auto cooledPair = cooldownQueue.front();
38                cooldownQueue.pop();
39              
40                // Only add back to heap if character still has remaining frequency
41                if (cooledPair.first > 0) {
42                    maxHeap.push(cooledPair);
43                }
44            }
45        }
46      
47        // Check if we successfully rearranged all characters
48        // If not, it means rearrangement with distance k is impossible
49        return result.size() == s.size() ? result : "";
50    }
51};
52
1function rearrangeString(s: string, k: number): string {
2    // Count frequency of each character
3    const charFrequency = new Map<string, number>();
4    for (const char of s) {
5        charFrequency.set(char, (charFrequency.get(char) || 0) + 1);
6    }
7  
8    // Create max heap with pairs of [frequency, character]
9    // Heap will prioritize characters with higher frequency
10    const maxHeap: Array<[number, string]> = [];
11    for (const [character, frequency] of charFrequency.entries()) {
12        maxHeap.push([frequency, character]);
13    }
14    // Sort in descending order by frequency, then by character for consistency
15    maxHeap.sort((a, b) => b[0] - a[0] || a[1].localeCompare(b[1]));
16  
17    // Queue to hold characters that are in cooldown period
18    // Each character must wait k positions before being used again
19    const cooldownQueue: Array<[number, string]> = [];
20    let result = '';
21  
22    // Greedily build the result string
23    while (maxHeap.length > 0) {
24        // Get the character with highest frequency
25        const [frequency, character] = maxHeap.shift()!;
26      
27        // Add character to result
28        result += character;
29      
30        // Put character in cooldown with decremented frequency
31        cooldownQueue.push([frequency - 1, character]);
32      
33        // If cooldown period is over (queue has k elements),
34        // the front element can be reused
35        if (cooldownQueue.length >= k) {
36            const cooledPair = cooldownQueue.shift()!;
37          
38            // Only add back to heap if character still has remaining frequency
39            if (cooledPair[0] > 0) {
40                maxHeap.push(cooledPair);
41                // Re-sort to maintain max heap property
42                maxHeap.sort((a, b) => b[0] - a[0] || a[1].localeCompare(b[1]));
43            }
44        }
45    }
46  
47    // Check if we successfully rearranged all characters
48    // If not, it means rearrangement with distance k is impossible
49    return result.length === s.length ? result : '';
50}
51

Time and Space Complexity

Time Complexity: O(n log m) where n is the length of the input string and m is the number of unique characters (at most 26 for lowercase English letters).

  • Creating the frequency counter takes O(n) time
  • Building the initial heap from the counter takes O(m) time to create the list and O(m) time for heapify operation
  • The main while loop runs exactly n iterations (once for each character in the string)
  • In each iteration:
    • heappop() operation: O(log m)
    • Append to answer list: O(1)
    • Append to queue: O(1)
    • Conditionally heappush() operation: O(log m)
  • Total: O(n) + O(m) + O(n × log m) = O(n log m)

Since m ≤ 26 for English letters, this can also be considered O(n) in practice.

Space Complexity: O(m) or O(1) if we consider the alphabet size as constant

  • Counter storage: O(m) for storing frequency of unique characters
  • Heap storage: O(m) for at most m unique characters
  • Queue storage: O(k) but since k ≤ m, this is bounded by O(m)
  • Answer list: O(n) but this is typically not counted as it's the output
  • Total auxiliary space: O(m)

Since the number of unique characters is bounded by 26 for lowercase English letters, the space complexity can be considered O(1) constant space.

Common Pitfalls

1. Forgetting the Edge Case When k = 0

The Pitfall: Many implementations forget that when k = 0, there's no distance requirement between identical characters. The main algorithm with the cooldown queue becomes unnecessary and can even produce incorrect results due to the queue management logic.

Why It Happens:

# Without handling k = 0, this condition fails:
if len(cooldown_queue) >= k:  # When k = 0, this is always True
    # Characters immediately get popped from queue

When k = 0, characters would immediately cycle back from the cooldown queue to the heap, but the overhead and logic complexity can introduce bugs.

The Solution:

# Add this check at the beginning
if k <= 1:
    return s  # Any arrangement is valid

2. Incorrect Cooldown Queue Size Check

The Pitfall: Using > instead of >= when checking if the cooldown period has elapsed:

# WRONG - This makes characters wait one extra position
if len(cooldown_queue) > k:
    remaining_freq, cooldown_char = cooldown_queue.popleft()
  
# CORRECT - Character becomes available exactly after k positions
if len(cooldown_queue) >= k:
    remaining_freq, cooldown_char = cooldown_queue.popleft()

Why It Matters:

  • With >, a character at position i wouldn't be available until position i + k + 1
  • This unnecessarily restricts placement options and can cause valid arrangements to fail

3. Not Checking for Remaining Characters in Cooldown Queue

The Pitfall: Assuming that if the main loop completes, the arrangement is valid:

# WRONG - Missing validation
while max_heap:
    # ... process characters
return "".join(result)  # Might return partial result!

Why It Fails: When impossible arrangements occur, some characters remain stuck in the cooldown queue. Without checking, you might return a string shorter than the input.

The Solution:

# Always validate that all characters were placed
if len(result) != len(s):
    return ""
return "".join(result)

4. Heap Value Sign Confusion

The Pitfall: Forgetting to negate values when working with the max heap simulation:

# WRONG - Forgets to negate when pushing back
if remaining_freq > 0:
    heappush(max_heap, (remaining_freq, cooldown_char))  # Should be negative!

# WRONG - Forgets to convert back to positive
neg_freq, char = heappop(max_heap)
cooldown_queue.append((neg_freq - 1, char))  # Should use positive freq

The Solution: Always maintain consistency:

  • Negate when pushing to heap: heappush(max_heap, (-remaining_freq, cooldown_char))
  • Convert to positive after popping: freq = -neg_freq

5. Inefficient String Concatenation

The Pitfall: Using string concatenation instead of a list:

# INEFFICIENT - O(n²) due to string immutability
result = ""
while max_heap:
    result += char  # Creates new string each time

The Solution: Use a list and join at the end:

result = []
while max_heap:
    result.append(char)
return "".join(result)  # O(n) operation
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More