Builds on Top-K Heap and Hash Map, which you haven’t finished yet.
Greedy Scheduling with a Heap (Reorganize String / Task Scheduler)
You're given a string s. Reorder its characters so that no two characters sitting next to each other are the same.
Return any ordering that satisfies this, or return an empty string "" if no such ordering exists.
b between the two as keeps equal letters apart.Most Frequent First
character counts ➔ max-heap of frequencies ➔ cooldown buffer| 1 | import heapq |
| 2 | from collections import Counter |
| 3 | |
| 4 | def reorganize_string(s: str) -> str: |
| 5 | """Reorganize String: rearrange so no two adjacent chars are equal. |
| 6 | Time: O(N log A) | Space: O(A) where A is alphabet size |
| 7 | """ |
| 8 | counts = Counter(s) |
| 9 | max_heap = [(-cnt, ch) for ch, cnt in counts.items()] # Negate: heapq is a min-heap |
| 10 | heapq.heapify(max_heap) |
| 11 | |
| 12 | prev_cnt, prev_ch = 0, "" # Holding buffer: last char used, kept out of the heap for one turn |
| 13 | result: list[str] = [] |
| 14 | |
| 15 | while max_heap: |
| 16 | # 1. Place the Most Frequent Available Char: |
| 17 | # The previous char is out of the heap for this turn, so the pop can never repeat the char just placed. |
| 18 | cnt, ch = heapq.heappop(max_heap) # Most frequent char that isn't the previous one |
| 19 | result.append(ch) |
| 20 | |
| 21 | # 2. Release the Char From Last Turn: |
| 22 | # It was held out for exactly one turn, so returning it now means it can never land next to itself. |
| 23 | if prev_cnt < 0: |
| 24 | heapq.heappush(max_heap, (prev_cnt, prev_ch)) # Previous char is allowed again |
| 25 | |
| 26 | # 3. Hold the Char Just Placed: |
| 27 | # Counts are negated, so using one copy moves the count toward zero, which is cnt + 1, not cnt - 1. |
| 28 | prev_cnt, prev_ch = cnt + 1, ch # One copy used (+1 because counts are negated) |
| 29 | |
| 30 | # 4. Confirm Every Char Was Placed: |
| 31 | # The heap can run out while a char still waits in prev_ch; that means fewer chars were placed than len(s), so no valid arrangement exists. |
| 32 | res_str = "".join(result) |
| 33 | return res_str if len(res_str) == len(s) else "" # Stuck early: no valid arrangement |
prev_ch is the last character appended to result and stays out of max_heap until the next character is placed, so result never has two equal neighbours.Placing the most frequent remaining character first keeps the biggest pile from being stranded at the end with nothing to separate it. The character just used waits in a one-slot holding buffer (prev_cnt, prev_ch) and goes back into the heap only after the next character is placed, so the same character never lands twice in a row. If the heap empties while a character still waits, the result is shorter than s and no valid arrangement exists.
See it run
Most Frequent First, step by step
prev_cnt 0prev_ch none
Each entry of max_heap shows the character and its negated count, for example a(-4) means 4 copies of a. The top row is the heap array, indexed from 0; the other rows are the held buffer and result.
placedthe character just appended to resultheldthe character kept out of max_heap for one turnpushedthe held character, returned to max_heap
Start: counts of "aaaabbcc" are negated and heapified into max_heap. Nothing is held yet, and result is empty.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
res_strwithout checking its lengthMistakereturn res_strFixreturn res_str if len(res_str) == len(s) else ""What happens: The heap can run empty while one character still waits in
prev_ch. Thenres_stris shorter thans, and no valid arrangement exists. The problem asks for""in that case. This happens exactly when the most frequent count is more than(len(s) + 1) // 2.Example: Reorganize String (LC 767): s = "aaab"Mistake: returns 'aba'Fix: returns ''
- Equal characters must end up together, not apart, as in Sort Characters By Frequency (LC 451): order by count with buckets, with no holding buffer and no adjacency rule. See Bucket Sort
Deep Dive
Why the most frequent character goes first, and when no answer exists
Take a string of length n, and let m be the count of its most frequent character. That character needs a different character between every two of its copies. So m copies need at least m - 1 separators. The other characters supply n - m separators. An answer exists exactly when m - 1 <= n - m, which is the same as m <= (n + 1) // 2.
Example: "aab" has m = 2 and (3 + 1) // 2 = 2, so the answer is "aba". The string "aaab" has m = 3 and (4 + 1) // 2 = 2, so no answer exists and reorganize_string returns "".
The heap makes the greedy choice safe. Each turn it pops the character with the most copies left, except the held one. The biggest pile shrinks first, so it is not left for the end of the string with nothing to separate its copies.
The card does not test m directly. It runs the loop and compares len(res_str) with len(s). When the heap is empty while prev_ch still waits, fewer characters were placed than len(s). That happens exactly when m > (n + 1) // 2. I ran 2000 random strings over 4 letters, and the loop returned a non-empty string exactly when m <= (n + 1) // 2.
One trace: the held character waits one turn
The board replays reorganize_string on s = "aaaabbcc". The counts are a:4, b:2 and c:2, so max_heap starts as [(-4, 'a'), (-2, 'b'), (-2, 'c')]. Every loop pass has two steps on the board: the pop, then the release and hold.
The first pop takes (-4, 'a'). prev_cnt is 0, so nothing is pushed back. Then prev_cnt, prev_ch = cnt + 1, ch gives (-3, 'a'). Three copies of a remain, and none of them is in max_heap.
The second pop is the marked trap step. a has 3 copies left and b has 2, so a would win the pop. It is held out, so max_heap offers only b and c. The pop takes b, and result is "ab". Only after this pop does a go back into max_heap.
Now suppose the push came before the pop. This version returns "aaabcabc" for the same string, which has three a in a row. The held character was the largest in the heap, so it was popped again at once.
The loop pops 8 times and pushes back 5 times, and result is "abacabac". The last held character is c, with count 0. A held character with count 0 is not pushed back, because prev_cnt < 0 is False.
The cost: the heap holds the alphabet, not the string
Counter(s) costs O(N). The heap has one entry per distinct character, so its size is at most A, the alphabet size. The held character is outside the heap, so the heap never holds more than A entries.
The loop runs once per character of s. Each pass has one pop and at most one push, and each of them costs O(log A). That gives O(N log A) time and O(A) space. For lowercase letters, A is 26, so log A is at most 5.
Example: for "aaaabbcc" the heap starts with 3 entries and never holds more than 3. The 8 passes make 8 pops and 5 pushes, which is 13 heap operations on a heap of at most 3 entries.
From one held slot to a wait of k turns
The holding buffer is the new idea on this card. Keep the Best K gives the heap. This card adds a place where a used item waits before it may return. The size of that place is the distance rule.
In Reorganize String (LC 767), equal characters must not be neighbours, so one slot is enough. In Rearrange String k Distance Apart (LC 358), equal characters must be at least k positions apart. Then the waiting place is a queue. A used character joins the queue, and after each placement, if the queue holds k entries, the oldest one leaves it and returns to the heap if it has copies left. With k = 2 (equal characters must not be neighbours) this is the single slot of this card.
Example: for "aabbcc" and k = 3, the queue version returns "abcabc". For "aaabc" and k = 3, it returns "". I checked this version against all permutations of random strings of up to 7 characters, for k from 1 to 4.
Task Scheduler (LC 621) uses the same wait. A task that ran waits n more intervals. The count of intervals includes the idle ones. For ["A","A","A","B","B","B"] and n = 2, a simulation with a heap and a wait queue counts 8 intervals, which is the LeetCode example answer.
Distant Barcodes (LC 1054) is the same problem as Reorganize String (LC 767). It promises that an answer exists, so the length check at the end is not needed.
60-Second Pitch
"Equal neighbours are not allowed, so the biggest pile is the danger. I count the characters and put them in a max-heap of negated counts. Each turn I pop the most frequent character and append it. I do not push it back at once, because it would be the most frequent again. I hold it in prev_ch for one turn. After the next pop, I push the held character back if it still has copies left. That way the heap never contains the character I just placed, so two equal neighbours are impossible. If the heap becomes empty while a character still waits, the string cannot be arranged, and I return an empty string. I check this by comparing the result length with len(s). The heap has at most one entry per distinct character. Time is O(N log A) and space is O(A), where A is the alphabet size. For a distance of k, I replace the single slot with a queue of length k."
Common Questions
- Why do you pop the most frequent character and not any available one?
The biggest pile is the one that can run out of separators. Placing it early leaves enough other characters to separate its copies. A string has an answer exactly when the largest count
msatisfiesm <= (n + 1) // 2, and this greedy finds an answer whenever that holds.- What are the time and space complexity of
reorganize_string? O(N log A) time and O(A) space, where
Nislen(s)andAis the number of distinct characters. The heap never holds more thanAentries, and each of theNpasses does one pop and at most one push. For lowercase letters,log Ais at most 5, so the time is close to linear.- Why hold the character for one turn and not remove it from the counts?
The character still has copies left, so it must come back. Holding it for exactly one turn keeps it away from the next pop and returns it before the pop after that. A permanent removal would lose its remaining copies.
- How do you change it for Rearrange String k Distance Apart (LC 358)?
Replace the single slot with a queue. Each used character joins the queue with its new count. After each placement, if the queue holds
kentries, remove the oldest one and push it back into the heap if its count is still below 0. If the heap is empty beforeresultis complete, return"". Withk = 2the queue is the single slot of this card.- Could you decide that no answer exists without running the loop?
Yes. Compute
m, the largest count, and compare it with(n + 1) // 2. Ifmis larger, return"". This takesO(N)and no heap. You still need the heap to build an answer when one exists. The loop gives the same decision as a side effect, through the length check.
SWE Reasoning
Spot it
- The statement says no two equal items may be next to each other, or they must be at least
kapart. - You must say whether such an arrangement exists, and also build one.
- Only the counts of the items matter, not their original order. A
Counterplus a heap fits. - The answer is an arrangement, not a total or a count. A formula on the largest count can decide if one exists, but it does not build one.
- When a counting argument says the biggest pile is too large, the statement allows an empty result.
The invariant, step by step
Counter(s)gives one count per character.max_heapholds(-cnt, ch), so the most frequent character is on top.prev_cnt, prev_chstarts as0, "".- Each pass pops the most frequent character in
max_heap. The previous character is outside the heap, so this pop cannot repeat it. Append the character toresult. - If
prev_cnt < 0, the previous character still has copies left. Push it back now. It has waited exactly one turn. - Set
prev_cnt, prev_ch = cnt + 1, ch. The new count is closer to 0, because the counts are negated. resultnever has two equal neighbours. The new last character isprev_ch, and the next pop comes from a heap that does not contain it.- When
max_heapis empty, comparelen(res_str)withlen(s). If a character is still waiting inprev_ch, the lengths differ and the answer is"".
Traps
- Push before pop. The held character can be the largest again and is popped at once. For
"aab", the result is"aab". cnt - 1for the count that is left. Counts are negated, so usecnt + 1. Withcnt - 1, the count never reaches 0 and the loop does not end.- Counts that are not negated.
heapqis a min-heap, so the least frequent character comes first. The checkprev_cnt < 0is also never True. - No length check at the end. For
"aaab"the loop returns"aba", which is shorter thans. The problem asks for"". - Pushing the held character without the
prev_cnt < 0test. A character with no copies left returns to the heap. The loop then keeps popping and pushing, and it does not end.
Practice
Builds on Top-K Heap and Hash Map, which you haven’t finished yet.
Greedy Scheduling with a Heap (Reorganize String / Task Scheduler)
You're given a string s. Reorder its characters so that no two characters sitting next to each other are the same.
Return any ordering that satisfies this, or return an empty string "" if no such ordering exists.
b between the two as keeps equal letters apart.Most Frequent First
character counts ➔ max-heap of frequencies ➔ cooldown buffer| 1 | import heapq |
| 2 | from collections import Counter |
| 3 | |
| 4 | def reorganize_string(s: str) -> str: |
| 5 | """Reorganize String: rearrange so no two adjacent chars are equal. |
| 6 | Time: O(N log A) | Space: O(A) where A is alphabet size |
| 7 | """ |
| 8 | counts = Counter(s) |
| 9 | max_heap = [(-cnt, ch) for ch, cnt in counts.items()] # Negate: heapq is a min-heap |
| 10 | heapq.heapify(max_heap) |
| 11 | |
| 12 | prev_cnt, prev_ch = 0, "" # Holding buffer: last char used, kept out of the heap for one turn |
| 13 | result: list[str] = [] |
| 14 | |
| 15 | while max_heap: |
| 16 | # 1. Place the Most Frequent Available Char: |
| 17 | # The previous char is out of the heap for this turn, so the pop can never repeat the char just placed. |
| 18 | cnt, ch = heapq.heappop(max_heap) # Most frequent char that isn't the previous one |
| 19 | result.append(ch) |
| 20 | |
| 21 | # 2. Release the Char From Last Turn: |
| 22 | # It was held out for exactly one turn, so returning it now means it can never land next to itself. |
| 23 | if prev_cnt < 0: |
| 24 | heapq.heappush(max_heap, (prev_cnt, prev_ch)) # Previous char is allowed again |
| 25 | |
| 26 | # 3. Hold the Char Just Placed: |
| 27 | # Counts are negated, so using one copy moves the count toward zero, which is cnt + 1, not cnt - 1. |
| 28 | prev_cnt, prev_ch = cnt + 1, ch # One copy used (+1 because counts are negated) |
| 29 | |
| 30 | # 4. Confirm Every Char Was Placed: |
| 31 | # The heap can run out while a char still waits in prev_ch; that means fewer chars were placed than len(s), so no valid arrangement exists. |
| 32 | res_str = "".join(result) |
| 33 | return res_str if len(res_str) == len(s) else "" # Stuck early: no valid arrangement |
prev_ch is the last character appended to result and stays out of max_heap until the next character is placed, so result never has two equal neighbours.Placing the most frequent remaining character first keeps the biggest pile from being stranded at the end with nothing to separate it. The character just used waits in a one-slot holding buffer (prev_cnt, prev_ch) and goes back into the heap only after the next character is placed, so the same character never lands twice in a row. If the heap empties while a character still waits, the result is shorter than s and no valid arrangement exists.
See it run
Most Frequent First, step by step
prev_cnt 0prev_ch none
Each entry of max_heap shows the character and its negated count, for example a(-4) means 4 copies of a. The top row is the heap array, indexed from 0; the other rows are the held buffer and result.
placedthe character just appended to resultheldthe character kept out of max_heap for one turnpushedthe held character, returned to max_heap
Start: counts of "aaaabbcc" are negated and heapified into max_heap. Nothing is held yet, and result is empty.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
res_strwithout checking its lengthMistakereturn res_strFixreturn res_str if len(res_str) == len(s) else ""What happens: The heap can run empty while one character still waits in
prev_ch. Thenres_stris shorter thans, and no valid arrangement exists. The problem asks for""in that case. This happens exactly when the most frequent count is more than(len(s) + 1) // 2.Example: Reorganize String (LC 767): s = "aaab"Mistake: returns 'aba'Fix: returns ''
- Equal characters must end up together, not apart, as in Sort Characters By Frequency (LC 451): order by count with buckets, with no holding buffer and no adjacency rule. See Bucket Sort
Deep Dive
Why the most frequent character goes first, and when no answer exists
Take a string of length n, and let m be the count of its most frequent character. That character needs a different character between every two of its copies. So m copies need at least m - 1 separators. The other characters supply n - m separators. An answer exists exactly when m - 1 <= n - m, which is the same as m <= (n + 1) // 2.
Example: "aab" has m = 2 and (3 + 1) // 2 = 2, so the answer is "aba". The string "aaab" has m = 3 and (4 + 1) // 2 = 2, so no answer exists and reorganize_string returns "".
The heap makes the greedy choice safe. Each turn it pops the character with the most copies left, except the held one. The biggest pile shrinks first, so it is not left for the end of the string with nothing to separate its copies.
The card does not test m directly. It runs the loop and compares len(res_str) with len(s). When the heap is empty while prev_ch still waits, fewer characters were placed than len(s). That happens exactly when m > (n + 1) // 2. I ran 2000 random strings over 4 letters, and the loop returned a non-empty string exactly when m <= (n + 1) // 2.
One trace: the held character waits one turn
The board replays reorganize_string on s = "aaaabbcc". The counts are a:4, b:2 and c:2, so max_heap starts as [(-4, 'a'), (-2, 'b'), (-2, 'c')]. Every loop pass has two steps on the board: the pop, then the release and hold.
The first pop takes (-4, 'a'). prev_cnt is 0, so nothing is pushed back. Then prev_cnt, prev_ch = cnt + 1, ch gives (-3, 'a'). Three copies of a remain, and none of them is in max_heap.
The second pop is the marked trap step. a has 3 copies left and b has 2, so a would win the pop. It is held out, so max_heap offers only b and c. The pop takes b, and result is "ab". Only after this pop does a go back into max_heap.
Now suppose the push came before the pop. This version returns "aaabcabc" for the same string, which has three a in a row. The held character was the largest in the heap, so it was popped again at once.
The loop pops 8 times and pushes back 5 times, and result is "abacabac". The last held character is c, with count 0. A held character with count 0 is not pushed back, because prev_cnt < 0 is False.
The cost: the heap holds the alphabet, not the string
Counter(s) costs O(N). The heap has one entry per distinct character, so its size is at most A, the alphabet size. The held character is outside the heap, so the heap never holds more than A entries.
The loop runs once per character of s. Each pass has one pop and at most one push, and each of them costs O(log A). That gives O(N log A) time and O(A) space. For lowercase letters, A is 26, so log A is at most 5.
Example: for "aaaabbcc" the heap starts with 3 entries and never holds more than 3. The 8 passes make 8 pops and 5 pushes, which is 13 heap operations on a heap of at most 3 entries.
From one held slot to a wait of k turns
The holding buffer is the new idea on this card. Keep the Best K gives the heap. This card adds a place where a used item waits before it may return. The size of that place is the distance rule.
In Reorganize String (LC 767), equal characters must not be neighbours, so one slot is enough. In Rearrange String k Distance Apart (LC 358), equal characters must be at least k positions apart. Then the waiting place is a queue. A used character joins the queue, and after each placement, if the queue holds k entries, the oldest one leaves it and returns to the heap if it has copies left. With k = 2 (equal characters must not be neighbours) this is the single slot of this card.
Example: for "aabbcc" and k = 3, the queue version returns "abcabc". For "aaabc" and k = 3, it returns "". I checked this version against all permutations of random strings of up to 7 characters, for k from 1 to 4.
Task Scheduler (LC 621) uses the same wait. A task that ran waits n more intervals. The count of intervals includes the idle ones. For ["A","A","A","B","B","B"] and n = 2, a simulation with a heap and a wait queue counts 8 intervals, which is the LeetCode example answer.
Distant Barcodes (LC 1054) is the same problem as Reorganize String (LC 767). It promises that an answer exists, so the length check at the end is not needed.
60-Second Pitch
"Equal neighbours are not allowed, so the biggest pile is the danger. I count the characters and put them in a max-heap of negated counts. Each turn I pop the most frequent character and append it. I do not push it back at once, because it would be the most frequent again. I hold it in prev_ch for one turn. After the next pop, I push the held character back if it still has copies left. That way the heap never contains the character I just placed, so two equal neighbours are impossible. If the heap becomes empty while a character still waits, the string cannot be arranged, and I return an empty string. I check this by comparing the result length with len(s). The heap has at most one entry per distinct character. Time is O(N log A) and space is O(A), where A is the alphabet size. For a distance of k, I replace the single slot with a queue of length k."
Common Questions
- Why do you pop the most frequent character and not any available one?
The biggest pile is the one that can run out of separators. Placing it early leaves enough other characters to separate its copies. A string has an answer exactly when the largest count
msatisfiesm <= (n + 1) // 2, and this greedy finds an answer whenever that holds.- What are the time and space complexity of
reorganize_string? O(N log A) time and O(A) space, where
Nislen(s)andAis the number of distinct characters. The heap never holds more thanAentries, and each of theNpasses does one pop and at most one push. For lowercase letters,log Ais at most 5, so the time is close to linear.- Why hold the character for one turn and not remove it from the counts?
The character still has copies left, so it must come back. Holding it for exactly one turn keeps it away from the next pop and returns it before the pop after that. A permanent removal would lose its remaining copies.
- How do you change it for Rearrange String k Distance Apart (LC 358)?
Replace the single slot with a queue. Each used character joins the queue with its new count. After each placement, if the queue holds
kentries, remove the oldest one and push it back into the heap if its count is still below 0. If the heap is empty beforeresultis complete, return"". Withk = 2the queue is the single slot of this card.- Could you decide that no answer exists without running the loop?
Yes. Compute
m, the largest count, and compare it with(n + 1) // 2. Ifmis larger, return"". This takesO(N)and no heap. You still need the heap to build an answer when one exists. The loop gives the same decision as a side effect, through the length check.
SWE Reasoning
Spot it
- The statement says no two equal items may be next to each other, or they must be at least
kapart. - You must say whether such an arrangement exists, and also build one.
- Only the counts of the items matter, not their original order. A
Counterplus a heap fits. - The answer is an arrangement, not a total or a count. A formula on the largest count can decide if one exists, but it does not build one.
- When a counting argument says the biggest pile is too large, the statement allows an empty result.
The invariant, step by step
Counter(s)gives one count per character.max_heapholds(-cnt, ch), so the most frequent character is on top.prev_cnt, prev_chstarts as0, "".- Each pass pops the most frequent character in
max_heap. The previous character is outside the heap, so this pop cannot repeat it. Append the character toresult. - If
prev_cnt < 0, the previous character still has copies left. Push it back now. It has waited exactly one turn. - Set
prev_cnt, prev_ch = cnt + 1, ch. The new count is closer to 0, because the counts are negated. resultnever has two equal neighbours. The new last character isprev_ch, and the next pop comes from a heap that does not contain it.- When
max_heapis empty, comparelen(res_str)withlen(s). If a character is still waiting inprev_ch, the lengths differ and the answer is"".
Traps
- Push before pop. The held character can be the largest again and is popped at once. For
"aab", the result is"aab". cnt - 1for the count that is left. Counts are negated, so usecnt + 1. Withcnt - 1, the count never reaches 0 and the loop does not end.- Counts that are not negated.
heapqis a min-heap, so the least frequent character comes first. The checkprev_cnt < 0is also never True. - No length check at the end. For
"aaab"the loop returns"aba", which is shorter thans. The problem asks for"". - Pushing the held character without the
prev_cnt < 0test. A character with no copies left returns to the heap. The loop then keeps popping and pushing, and it does not end.