title: Reorganize String slug: reorganize-string difficulty: medium leetcode_id: 767 leetcode_url: https://leetcode.com/problems/reorganize-string/ categories: - strings - hash-tables - heap patterns: - greedy - heap function_signature: "def reorganize_string(s: str) -> str:" test_cases: visible: - input: { s: "aab" } expected: "aba" - input: { s: "aaab" } expected: "" hidden: - input: { s: "a" } expected: "a" - input: { s: "aa" } expected: "" - input: { s: "ab" } expected: "ab" - input: { s: "aabb" } expected: "abab" - input: { s: "aaabb" } expected: "ababa" description: | Given a string `s`, rearrange the characters of `s` so that any two adjacent characters are not the same. Return *any possible rearrangement of* `s` *or return* `""` *if not possible*. constraints: | - `1 <= s.length <= 500` - `s` consists of lowercase English letters. examples: - input: 's = "aab"' output: '"aba"' explanation: "We can rearrange 'aab' to 'aba' where no two adjacent characters are the same." - input: 's = "aaab"' output: '""' explanation: "There are 3 'a's but only 1 'b'. Even with optimal placement, we cannot avoid adjacent 'a's: 'a_a_a' needs at least 2 other characters to separate them." explanation: intuition: | Imagine you have a collection of coloured balls and need to arrange them in a line such that no two balls of the same colour are next to each other. The key insight is that this problem is all about **frequency distribution**. If any single character appears too frequently, it becomes impossible to separate all its occurrences with other characters. Specifically, if a character appears more than `(n + 1) / 2` times (where `n` is the string length), there's no valid arrangement. Think of it like seating people at a round table where enemies can't sit adjacent: if one group is too large, you simply can't place enough "buffers" between them. The greedy strategy is to **always place the most frequent character first**. By continuously choosing the character with the highest remaining count, we ensure we're using up the "problem" characters early while we still have other characters to interleave between them. approach: | We solve this using a **Max Heap (Greedy) Approach**: **Step 1: Count character frequencies** - Use a hash map (or `Counter`) to count occurrences of each character - Check if any character appears more than `(n + 1) / 2` times — if so, return `""` immediately   **Step 2: Build a max heap** - Create a max heap containing `(count, character)` pairs - Python's `heapq` is a min heap, so negate counts to simulate max heap behaviour   **Step 3: Greedily build the result** - Pop the character with the highest frequency from the heap - Append it to the result - If there's a "previous" character waiting to be re-added (with remaining count > 0), push it back onto the heap - Store the current character as "previous" so we don't use it again immediately - Repeat until the heap is empty   **Step 4: Return the result** - Join the result list into a string and return it   This greedy approach works because by always placing the most frequent character (that isn't the same as the last placed), we maximise our chances of successfully interleaving all characters. common_pitfalls: - title: Ignoring the Impossibility Check description: | Before attempting to build a solution, you must verify that a valid arrangement is even possible. If any character appears more than `(n + 1) / 2` times, it's mathematically impossible to arrange the string. For example, with `"aaab"` (length 4), the character `'a'` appears 3 times but `(4 + 1) / 2 = 2` is the maximum allowed frequency. Failing to check this upfront leads to infinite loops or incorrect results. wrong_approach: "Attempting to build without checking frequencies first" correct_approach: "Check max frequency <= (n + 1) / 2 before proceeding" - title: Using the Same Character Consecutively description: | A naive greedy approach might always pick the most frequent character, but this fails when that character was just placed. For example, with `"aabb"`, always picking the most frequent could give `"aab..."` which already has adjacent duplicates. The solution is to track the previously placed character and ensure we don't pick it again until at least one other character has been placed. wrong_approach: "Always pop the max without tracking previous" correct_approach: "Hold back the previous character for one iteration" - title: Off-by-One in Frequency Threshold description: | The threshold for impossibility is `(n + 1) / 2`, not `n / 2`. This matters for odd-length strings. For `"aab"` (length 3), we have `(3 + 1) / 2 = 2`. Character `'a'` appears exactly 2 times, which is valid: `"aba"`. Using `n / 2 = 1` would incorrectly reject valid inputs. wrong_approach: "Using n / 2 as the threshold" correct_approach: "Using (n + 1) / 2 (ceiling of n/2)" key_takeaways: - "**Frequency analysis first**: Many string rearrangement problems require checking if a solution is possible before attempting construction" - "**Greedy with heap**: When you need to repeatedly pick the 'best' remaining option, a heap provides O(log n) selection" - "**Hold-back pattern**: To avoid consecutive duplicates, temporarily exclude the just-used element from selection" - "**Related problems**: This pattern applies to Task Scheduler (LC 621), Distant Barcodes (LC 1054), and other interleaving problems" time_complexity: "O(n log k). We process each of the `n` characters, and each heap operation takes O(log k) where `k` is the number of unique characters (at most 26)." space_complexity: "O(k). We store at most `k` unique characters in the heap and hash map, where `k <= 26` for lowercase English letters." solutions: - approach_name: Max Heap (Greedy) is_optimal: true code: | import heapq from collections import Counter def reorganize_string(s: str) -> str: # Count frequency of each character counts = Counter(s) n = len(s) # Check if reorganization is possible max_count = max(counts.values()) if max_count > (n + 1) // 2: return "" # Build max heap (negate counts for max heap behaviour) heap = [(-count, char) for char, count in counts.items()] heapq.heapify(heap) result = [] prev_count, prev_char = 0, "" while heap: # Pop the most frequent character count, char = heapq.heappop(heap) result.append(char) # Push back the previous character if it has remaining count if prev_count < 0: heapq.heappush(heap, (prev_count, prev_char)) # Update previous (decrement count since we used one) prev_count, prev_char = count + 1, char return "".join(result) explanation: | **Time Complexity:** O(n log k) — We process n characters, each with O(log k) heap operations where k <= 26. **Space Complexity:** O(k) — The heap and counter store at most k unique characters. The key insight is holding back the previously used character for one iteration. This ensures we never place the same character twice in a row. By always choosing the most frequent available character, we maximise our chances of using up high-frequency characters while we have options to interleave. - approach_name: Odd-Even Index Placement is_optimal: true code: | from collections import Counter def reorganize_string(s: str) -> str: counts = Counter(s) n = len(s) # Check if reorganization is possible max_count = max(counts.values()) if max_count > (n + 1) // 2: return "" # Sort characters by frequency (descending) sorted_chars = sorted(counts.keys(), key=lambda c: -counts[c]) result = [""] * n idx = 0 for char in sorted_chars: for _ in range(counts[char]): # Place at current index result[idx] = char # Move to next even index, wrap to odd indices idx += 2 if idx >= n: idx = 1 return "".join(result) explanation: | **Time Complexity:** O(n + k log k) — Counting takes O(n), sorting k characters takes O(k log k), and placement takes O(n). **Space Complexity:** O(n) — We use an array of size n for the result. This approach places characters at alternating indices: first fill all even positions (0, 2, 4, ...), then odd positions (1, 3, 5, ...). By placing the most frequent character first, we ensure it gets spread across even indices. Since max frequency is at most `(n + 1) / 2`, we're guaranteed not to overflow into adjacent positions of the same character. - approach_name: Brute Force (Backtracking) is_optimal: false code: | from collections import Counter def reorganize_string(s: str) -> str: counts = Counter(s) def backtrack(result: list[str], prev: str) -> bool: # Base case: used all characters if len(result) == len(s): return True # Try each character for char in counts: if counts[char] > 0 and char != prev: # Choose this character counts[char] -= 1 result.append(char) # Recurse if backtrack(result, char): return True # Backtrack result.pop() counts[char] += 1 return False result: list[str] = [] if backtrack(result, ""): return "".join(result) return "" explanation: | **Time Complexity:** O(n! / (c1! * c2! * ...)) — Explores permutations with pruning, still exponential in worst case. **Space Complexity:** O(n) — Recursion depth and result array. This approach tries all valid arrangements using backtracking. While correct, it's far too slow for larger inputs. Included to illustrate that a greedy approach (choosing most frequent available) is much more efficient than exploring all possibilities.