Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Priority Queue / Heap

Builds on Top-K Heap and Hash Map, which you haven’t finished yet.

P2MediumBuilt on Top-K Heap~5 min speedrun

Greedy Scheduling with a Heap (Reorganize String / Task Scheduler)

Built on
Top-K HeapA cooldown buffer holds the item just used until it may be used again.
The problem

Reorganize String (LC 767)(opens leetcode.com)

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.

Example 1
Input:s = "aab"
Output:"aba"
Putting b between the two as keeps equal letters apart.

Most Frequent First

character counts ➔ max-heap of frequencies ➔ cooldown buffer
Spot it when: Items must be arranged so equal ones aren't adjacent (or are k apart), and you want to know whether that is possible and build one arrangement.
Most Frequent First
Python
1import heapq
2from collections import Counter
3 
4def 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
Invariant: 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 0 · prev_ch none

prev_cnt 0 · prev_ch none

prev_cnt -3 · prev_ch a

prev_cnt -3 · prev_ch a

prev_cnt -1 · prev_ch b

prev_cnt -1 · prev_ch b

prev_cnt -2 · prev_ch a

prev_cnt -2 · prev_ch a

prev_cnt -1 · prev_ch c

prev_cnt -1 · prev_ch c

prev_cnt -1 · prev_ch a

prev_cnt -1 · prev_ch a

prev_cnt 0 · prev_ch b

prev_cnt 0 · prev_ch b

prev_cnt 0 · prev_ch a

prev_cnt 0 · prev_ch a

prev_cnt 0 · prev_ch c

prev_cnt 0 · prev_ch c · answer "abacabac"

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.

  • placed the character just appended to result
  • held the character kept out of max_heap for one turn
  • pushed the 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.

Pop a(-4) from max_heap and append a to result.

Nothing was held, so nothing is pushed back. Hold a(-3), the character just placed.

Pop b(-2) and append b to result. The held a has 3 left and would win the pop, but it is out of max_heap.

Push a(-3) back into max_heap, because it still has copies left. Hold b(-1), the character just placed.

Pop a(-3) from max_heap and append a to result.

Push b(-1) back into max_heap, because it still has copies left. Hold a(-2), the character just placed.

Pop c(-2) and append c to result. The held a has 2 left and would win the pop, but it is out of max_heap.

Push a(-2) back into max_heap, because it still has copies left. Hold c(-1), the character just placed.

Pop a(-2) from max_heap and append a to result.

Push c(-1) back into max_heap, because it still has copies left. Hold a(-1), the character just placed.

Pop b(-1) and append b to result. The held a has 1 left and would win the pop, but it is out of max_heap.

Push a(-1) back into max_heap, because it still has copies left. Hold b(0), the character just placed.

Pop a(-1) from max_heap and append a to result.

b has no copies left, so it is not pushed back. Hold a(0), the character just placed.

Pop c(-1) from max_heap and append c to result.

a has no copies left, so it is not pushed back. Hold c(0), the character just placed.

Done: max_heap is empty and result has all 8 characters, so return "abacabac".

max_heap
0a(-4)
1b(-2)
2c(-2)
3
4
5
6
7
held
none
result
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning res_str without checking its length

    Mistake
    return res_str
    Fix
    return res_str if len(res_str) == len(s) else ""

    What happens: The heap can run empty while one character still waits in prev_ch. Then res_str is shorter than s, 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 ''
  2. 2

    Gotcha 2: Pushing the held character back before the next pop

    Mistake
    if prev_cnt < 0:
        heapq.heappush(max_heap, (prev_cnt, prev_ch))
    cnt, ch = heapq.heappop(max_heap)
    Fix
    cnt, ch = heapq.heappop(max_heap)
    result.append(ch)
    
    if prev_cnt < 0:
        heapq.heappush(max_heap, (prev_cnt, prev_ch))

    What happens: The held character is the one placed last. If it goes back into the heap before the pop, it can be the most frequent one again. The pop then picks it right away. Two equal characters end up next to each other.

    Example: s = "aab"
    Mistake: returns 'aab'Fix: returns 'aba'
  3. 3

    Gotcha 3: Writing cnt - 1 for the count that is left

    Mistake
    prev_cnt, prev_ch = cnt - 1, ch
    Fix
    prev_cnt, prev_ch = cnt + 1, ch

    What happens: The counts are stored negated, so 2 copies left is -2. Using one copy must move the count toward 0, which is cnt + 1. cnt - 1 moves it away from 0. The count never reaches 0, so the character goes back into the heap forever.

    Example: s = "aab"
    Mistake: the loop never endsFix: returns 'aba'
  4. 4

    Gotcha 4: Building the heap from counts that are not negated

    Mistake
    max_heap = [(cnt, ch) for ch, cnt in counts.items()]
    Fix
    max_heap = [(-cnt, ch) for ch, cnt in counts.items()]

    What happens: heapq is a min-heap, so positive counts put the least frequent character on top. The counts also stay positive, so prev_cnt < 0 is never True. A used character never goes back into the heap. res_str gets each character once, is shorter than s, and the result is "".

    Example: s = "aab"
    Mistake: returns ''Fix: returns 'aba'
Not this one if…
  • 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 m satisfies m <= (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 N is len(s) and A is the number of distinct characters. The heap never holds more than A entries, and each of the N passes does one pop and at most one push. For lowercase letters, log A is 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 k entries, remove the oldest one and push it back into the heap if its count is still below 0. If the heap is empty before result is complete, return "". With k = 2 the 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. If m is larger, return "". This takes O(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 k apart.
  • You must say whether such an arrangement exists, and also build one.
  • Only the counts of the items matter, not their original order. A Counter plus 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

  1. Counter(s) gives one count per character. max_heap holds (-cnt, ch), so the most frequent character is on top. prev_cnt, prev_ch starts as 0, "".
  2. 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 to result.
  3. If prev_cnt < 0, the previous character still has copies left. Push it back now. It has waited exactly one turn.
  4. Set prev_cnt, prev_ch = cnt + 1, ch. The new count is closer to 0, because the counts are negated.
  5. result never has two equal neighbours. The new last character is prev_ch, and the next pop comes from a heap that does not contain it.
  6. When max_heap is empty, compare len(res_str) with len(s). If a character is still waiting in prev_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 - 1 for the count that is left. Counts are negated, so use cnt + 1. With cnt - 1, the count never reaches 0 and the loop does not end.
  • Counts that are not negated. heapq is a min-heap, so the least frequent character comes first. The check prev_cnt < 0 is also never True.
  • No length check at the end. For "aaab" the loop returns "aba", which is shorter than s. The problem asks for "".
  • Pushing the held character without the prev_cnt < 0 test. A character with no copies left returns to the heap. The loop then keeps popping and pushing, and it does not end.

Practice

Canonical LeetCode:Reorganize String (LC 767)Rearrange String k Distance Apart (LC 358)Distant Barcodes (LC 1054)Longest Happy String (LC 1405)Task Scheduler (LC 621)
Theory Context•Priority Queue / Heap

Builds on Top-K Heap and Hash Map, which you haven’t finished yet.

P2MediumBuilt on Top-K Heap~5 min speedrun

Greedy Scheduling with a Heap (Reorganize String / Task Scheduler)

Built on
Top-K HeapA cooldown buffer holds the item just used until it may be used again.
The problem

Reorganize String (LC 767)(opens leetcode.com)

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.

Example 1
Input:s = "aab"
Output:"aba"
Putting b between the two as keeps equal letters apart.

Most Frequent First

character counts ➔ max-heap of frequencies ➔ cooldown buffer
Spot it when: Items must be arranged so equal ones aren't adjacent (or are k apart), and you want to know whether that is possible and build one arrangement.
Most Frequent First
Python
1import heapq
2from collections import Counter
3 
4def 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
Invariant: 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 0 · prev_ch none

prev_cnt 0 · prev_ch none

prev_cnt -3 · prev_ch a

prev_cnt -3 · prev_ch a

prev_cnt -1 · prev_ch b

prev_cnt -1 · prev_ch b

prev_cnt -2 · prev_ch a

prev_cnt -2 · prev_ch a

prev_cnt -1 · prev_ch c

prev_cnt -1 · prev_ch c

prev_cnt -1 · prev_ch a

prev_cnt -1 · prev_ch a

prev_cnt 0 · prev_ch b

prev_cnt 0 · prev_ch b

prev_cnt 0 · prev_ch a

prev_cnt 0 · prev_ch a

prev_cnt 0 · prev_ch c

prev_cnt 0 · prev_ch c · answer "abacabac"

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.

  • placed the character just appended to result
  • held the character kept out of max_heap for one turn
  • pushed the 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.

Pop a(-4) from max_heap and append a to result.

Nothing was held, so nothing is pushed back. Hold a(-3), the character just placed.

Pop b(-2) and append b to result. The held a has 3 left and would win the pop, but it is out of max_heap.

Push a(-3) back into max_heap, because it still has copies left. Hold b(-1), the character just placed.

Pop a(-3) from max_heap and append a to result.

Push b(-1) back into max_heap, because it still has copies left. Hold a(-2), the character just placed.

Pop c(-2) and append c to result. The held a has 2 left and would win the pop, but it is out of max_heap.

Push a(-2) back into max_heap, because it still has copies left. Hold c(-1), the character just placed.

Pop a(-2) from max_heap and append a to result.

Push c(-1) back into max_heap, because it still has copies left. Hold a(-1), the character just placed.

Pop b(-1) and append b to result. The held a has 1 left and would win the pop, but it is out of max_heap.

Push a(-1) back into max_heap, because it still has copies left. Hold b(0), the character just placed.

Pop a(-1) from max_heap and append a to result.

b has no copies left, so it is not pushed back. Hold a(0), the character just placed.

Pop c(-1) from max_heap and append c to result.

a has no copies left, so it is not pushed back. Hold c(0), the character just placed.

Done: max_heap is empty and result has all 8 characters, so return "abacabac".

max_heap
0a(-4)
1b(-2)
2c(-2)
3
4
5
6
7
held
none
result
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Returning res_str without checking its length

    Mistake
    return res_str
    Fix
    return res_str if len(res_str) == len(s) else ""

    What happens: The heap can run empty while one character still waits in prev_ch. Then res_str is shorter than s, 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 ''
  2. 2

    Gotcha 2: Pushing the held character back before the next pop

    Mistake
    if prev_cnt < 0:
        heapq.heappush(max_heap, (prev_cnt, prev_ch))
    cnt, ch = heapq.heappop(max_heap)
    Fix
    cnt, ch = heapq.heappop(max_heap)
    result.append(ch)
    
    if prev_cnt < 0:
        heapq.heappush(max_heap, (prev_cnt, prev_ch))

    What happens: The held character is the one placed last. If it goes back into the heap before the pop, it can be the most frequent one again. The pop then picks it right away. Two equal characters end up next to each other.

    Example: s = "aab"
    Mistake: returns 'aab'Fix: returns 'aba'
  3. 3

    Gotcha 3: Writing cnt - 1 for the count that is left

    Mistake
    prev_cnt, prev_ch = cnt - 1, ch
    Fix
    prev_cnt, prev_ch = cnt + 1, ch

    What happens: The counts are stored negated, so 2 copies left is -2. Using one copy must move the count toward 0, which is cnt + 1. cnt - 1 moves it away from 0. The count never reaches 0, so the character goes back into the heap forever.

    Example: s = "aab"
    Mistake: the loop never endsFix: returns 'aba'
  4. 4

    Gotcha 4: Building the heap from counts that are not negated

    Mistake
    max_heap = [(cnt, ch) for ch, cnt in counts.items()]
    Fix
    max_heap = [(-cnt, ch) for ch, cnt in counts.items()]

    What happens: heapq is a min-heap, so positive counts put the least frequent character on top. The counts also stay positive, so prev_cnt < 0 is never True. A used character never goes back into the heap. res_str gets each character once, is shorter than s, and the result is "".

    Example: s = "aab"
    Mistake: returns ''Fix: returns 'aba'
Not this one if…
  • 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 m satisfies m <= (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 N is len(s) and A is the number of distinct characters. The heap never holds more than A entries, and each of the N passes does one pop and at most one push. For lowercase letters, log A is 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 k entries, remove the oldest one and push it back into the heap if its count is still below 0. If the heap is empty before result is complete, return "". With k = 2 the 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. If m is larger, return "". This takes O(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 k apart.
  • You must say whether such an arrangement exists, and also build one.
  • Only the counts of the items matter, not their original order. A Counter plus 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

  1. Counter(s) gives one count per character. max_heap holds (-cnt, ch), so the most frequent character is on top. prev_cnt, prev_ch starts as 0, "".
  2. 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 to result.
  3. If prev_cnt < 0, the previous character still has copies left. Push it back now. It has waited exactly one turn.
  4. Set prev_cnt, prev_ch = cnt + 1, ch. The new count is closer to 0, because the counts are negated.
  5. result never has two equal neighbours. The new last character is prev_ch, and the next pop comes from a heap that does not contain it.
  6. When max_heap is empty, compare len(res_str) with len(s). If a character is still waiting in prev_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 - 1 for the count that is left. Counts are negated, so use cnt + 1. With cnt - 1, the count never reaches 0 and the loop does not end.
  • Counts that are not negated. heapq is a min-heap, so the least frequent character comes first. The check prev_cnt < 0 is also never True.
  • No length check at the end. For "aaab" the loop returns "aba", which is shorter than s. The problem asks for "".
  • Pushing the held character without the prev_cnt < 0 test. A character with no copies left returns to the heap. The loop then keeps popping and pushing, and it does not end.

Practice

Canonical LeetCode:Reorganize String (LC 767)Rearrange String k Distance Apart (LC 358)Distant Barcodes (LC 1054)Longest Happy String (LC 1405)Task Scheduler (LC 621)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•