Generated by Codex with GPT-5
Quick facts
- Difficulty:
MEDIUM - Problem: Reorganize String
- Main tags:
Hash Table,String,Greedy,Heap,Counting
What the problem is really asking
The input is a string s. The task is to rearrange its characters so that no
two adjacent characters are equal. If no such rearrangement exists, return the
empty string. Any valid rearrangement is accepted.
The important point is that the exact order does not matter. This is not a sorting problem. It is a spacing problem: the most frequent character needs enough other characters between its copies.
For example, aab can become aba. But aaab cannot be fixed, because three
copies of a would need at least two non-a characters to separate them, and
there is only one.
The key feasibility rule
Let n be the string length and let max_count be the count of the most common
character.
The string can be reorganized only if:
max_count <= (n + 1) // 2
This is the whole impossibility test.
Why? The most frequent character needs to occupy separated slots. In a string of
length n, the largest number of non-adjacent positions available to one
character is the number of even-indexed slots, which is (n + 1) // 2.
If a character appears more often than that, at least two of its copies must be adjacent. If every character fits within that bound, a greedy construction can always place characters without creating adjacent duplicates.
Greedy construction with a max heap
The practical strategy is to always place the character with the highest remaining count, but never allow the same character to be used twice in a row.
A max heap gives direct access to the character that is currently most urgent to place. Python only has a min heap, so the code stores negative counts.
The clean trick is to hold back the character just used for one turn. That character is not pushed back into the heap immediately, which prevents the next step from choosing it again. After a different character is placed, the held character can return to the heap if it still has remaining copies.
That gives this rhythm:
- Pop the most frequent available character.
- Append it to the answer.
- Decrease its remaining count.
- Push back the previously used character, if it still has copies left.
- Hold the current character out until the next step.
Because the impossible case was already ruled out, this process can consume all characters and produce a valid string.
How to derive this in an interview
Start with the failure case. Ask what makes the problem impossible. The only
way to fail is for one character to appear so often that the rest of the string
cannot separate its copies. That leads directly to the max_count <= ceil(n / 2)
rule.
After that, focus on construction. Since the most frequent remaining character is always the one most likely to cause trouble later, place it early. But because using the same character twice in a row is forbidden, temporarily remove the character just placed from the candidate pool. A heap plus one held-back character expresses exactly that idea.
This is a good interview answer because it explains both halves of the problem: why an answer may not exist, and why the greedy construction is safe when it does.
Python solution
from collections import Counter
from heapq import heapify, heappop, heappush
class Solution:
def reorganizeString(self, s: str) -> str:
character_counts = Counter(s)
if not self._is_reorganization_possible(character_counts, len(s)):
return ""
max_heap = self._build_max_heap(character_counts)
result_chars: list[str] = []
held_count = 0
held_char = ""
while max_heap:
current_count, current_char = heappop(max_heap)
result_chars.append(current_char)
# Counts are negative in the heap, so adding 1 means "use one copy."
current_count += 1
# The previously used character is safe to offer again only after
# we have placed a different character.
if held_count < 0:
heappush(max_heap, (held_count, held_char))
held_count = current_count
held_char = current_char
return "".join(result_chars)
@staticmethod
def _is_reorganization_possible(
character_counts: Counter,
string_length: int,
) -> bool:
max_allowed_count = (string_length + 1) // 2
return all(
count <= max_allowed_count
for count in character_counts.values()
)
@staticmethod
def _build_max_heap(character_counts: Counter) -> list[tuple[int, str]]:
max_heap = [
(-count, character)
for character, count in character_counts.items()
]
heapify(max_heap)
return max_heapTime complexity is O(n log k), where n is the string length and k is the
number of distinct characters. Space complexity is O(n + k) for the output
and heap. If the alphabet is fixed, such as lowercase English letters, the heap
factor is effectively constant and the runtime is linear in n.
Interview follow-ups
Can this be solved without a heap?
Yes. If the alphabet is small or fixed, count every character, sort characters
by descending frequency, and fill the answer array at every other index: 0,
2, 4, and so on. When the end is reached, continue with odd indices: 1,
3, 5, and so on.
This works because the feasibility rule guarantees the most frequent character
fits into the separated even slots. Placing high-frequency characters first
prevents them from being forced together later. The resulting complexity is
O(n + k log k) if the distinct characters are sorted, or O(n + k) with a
fixed-size counting array.
What if the interviewer asks for the lexicographically smallest valid answer?
The heap solution returns any valid answer, not necessarily the smallest one. For the lexicographically smallest valid string, build the answer from left to right and try candidate characters in sorted order. A candidate can be committed only if it is different from the previous output character and the remaining multiset can still be completed.
The feasibility check after choosing a candidate is similar to the original
rule, but it must account for the previous character. If the previous character
still has remaining copies, it has fewer legal slots because the next position
cannot use it. With a fixed alphabet, scanning all counts after each tentative
choice is simple and keeps the code understandable. The tradeoff is higher
runtime: O(n * k^2) in the most direct version, or better with additional
bookkeeping.
How would this change if identical characters had to be at least d positions apart?
This becomes the common “rearrange string k distance apart” variant. The same
max-heap idea still applies, but instead of holding the previous character for
one turn, keep used characters in a cooldown queue for d turns.
At each step, pop the highest-count available character from the heap, append
it, decrement its count, and put it into the cooldown queue with the step when
it becomes available again. Once a cooled character is far enough away, push it
back into the heap. If the heap becomes empty before the answer reaches length
n, the reorganization is impossible. The runtime remains O(n log k), and
the extra queue uses O(k) space.
How can you prove the greedy heap method is safe?
The impossibility rule handles the only structural blocker: one character being too frequent to separate. Once that is ruled out, the most urgent remaining character should be placed as soon as it is legal, because delaying it only makes future spacing harder.
The held-back character prevents the only local violation, which is repeating the character just placed. Every other character in the heap is legal for the next position. Choosing the highest-count legal character keeps the remaining counts balanced, so no character can suddenly exceed the available separated slots unless it already violated the feasibility rule earlier.
What if only a yes-or-no answer is needed?
Then no construction is necessary. Count the characters and return whether the
largest count is at most (n + 1) // 2.
That works because the bound is both necessary and sufficient for this exact
problem. The check takes O(n) time and O(k) space, and it is often the best
first observation to state before writing the full rearrangement algorithm.