Hand of Straights: Greedy Grouping with Frequency Counting
Hand of Straights (LeetCode 846) asks whether you can rearrange a hand of cards into groups of consecutive values. It is the same problem as LeetCode 1296: Divide Array in Sets of Consecutive Numbers. If you can solve one, you can solve both.
The trick is greedy: always start each group from the smallest card that has not been used yet. If at any point a consecutive card is missing, return False.
The problem
You are given an integer array hand and an integer groupSize. Return True if you can rearrange the cards into groups where each group contains groupSize consecutive cards.
Example: hand = [1, 2, 3, 6, 2, 3, 4, 7, 8], groupSize = 3
The output is True because the cards can be divided into [1,2,3], [2,3,4], and [6,7,8].
Why greedy works here
Think about the smallest card in the hand. It has to belong to some group, and it can only be the starting card of that group (there is no smaller card to pair it with as a continuation). So the group starting at the smallest card is forced.
Once you form that group and remove those cards, the same logic applies to the next smallest remaining card. At every step, the smallest available card must start a new group. If you cannot complete the group because a consecutive card is missing, the answer is False.
This greedy choice is always correct because no other assignment of the smallest card would work.
The greedy insight is simple: the smallest available card has no choice. It must be the start of a new group. If the required consecutive cards do not exist, the answer is immediately False.
The brute force alternative
You could try all possible ways to partition the cards into groups. But the number of partitions grows factorially, and even a smart backtracking approach would be far too slow. The greedy method avoids this entirely because the choice at each step is forced.
The greedy solution (sorted + Counter)
from collections import Counter
def isNStraightHand(hand: list[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
count = Counter(hand)
for card in sorted(count):
while count[card] > 0:
for i in range(card, card + groupSize):
if count[i] <= 0:
return False
count[i] -= 1
return True
Here is how each piece works:
- Quick check: if the total number of cards is not divisible by
groupSize, it is impossible. ReturnFalseimmediately. - Count frequencies: use a
Counterto track how many of each card value you have. - Iterate in sorted order: for each card value (smallest first), while there are remaining copies, try to form a group starting at that card.
- Form the group: check that all
groupSizeconsecutive values exist. Decrement each count. If any count is 0 or negative when you need it, returnFalse. - All groups formed: if you make it through the loop, return
True.
Walking through the algorithm
Let's trace through hand = [1, 2, 3, 6, 2, 3, 4, 7, 8] with groupSize = 3.
Step 1: Sort and count frequencies
Build a frequency map from the hand. Cards sorted: 1, 2, 3, 4, 6, 7, 8.
Step 2: Start from smallest card (1). Form group [1, 2, 3].
Decrement counts for 1, 2, 3. Card 1 is now exhausted. Group [1,2,3] formed.
Step 3: Next smallest available (2). Form group [2, 3, 4].
Decrement counts for 2, 3, 4. All three exhausted. Group [2,3,4] formed.
Step 4: Next smallest available (6). Form group [6, 7, 8].
Decrement counts for 6, 7, 8. All exhausted. Group [6,7,8] formed.
Step 5: All cards used. Return True.
Every count is 0. We successfully divided all 9 cards into 3 groups of 3 consecutive values.
Each step shows the frequency map and highlights which cards are being consumed. The greedy approach always picks the smallest available card and tries to extend it into a full consecutive group.
Alternative: min-heap approach
Instead of sorting all unique keys upfront, you can use a min-heap to always find the smallest available card in O(log n) time. This also validates that cards are consumed in the right order.
from collections import Counter
import heapq
def isNStraightHand(hand: list[int], groupSize: int) -> bool:
if len(hand) % groupSize != 0:
return False
count = Counter(hand)
min_heap = list(count.keys())
heapq.heapify(min_heap)
while min_heap:
start = min_heap[0]
for i in range(start, start + groupSize):
if count[i] <= 0:
return False
count[i] -= 1
if count[i] == 0:
if i != min_heap[0]:
return False
heapq.heappop(min_heap)
return True
The key detail here: when a card count drops to 0, it must be the current heap minimum. If card i is exhausted but it is not the smallest in the heap, that means a smaller card still has remaining copies that cannot form a complete group. That is an immediate False.
Both approaches have the same time complexity, but the heap version catches invalid states earlier.
Complexity analysis
| Metric | Value |
|---|---|
| Time | O(n log n) for sorting the unique card values, plus O(n) to process all cards |
| Space | O(n) for the frequency map |
The dominant cost is sorting. Once sorted, each card is decremented exactly once across all groups, so the inner work is O(n) total.
Building blocks
This problem combines two reusable patterns that appear across many LeetCode problems.
1. Greedy with frequency counting
Count occurrences, then make locally optimal choices based on what is available. This same pattern shows up in Task Scheduler (use the most frequent task first) and Top K Frequent Elements (count, then select).
2. Sorted iteration over unique keys
Process elements in sorted order to enforce a specific grouping or ordering constraint. Sorting the unique values (not the full array) keeps things efficient when there are many duplicates.
The combination of frequency counting and sorted iteration is a powerful toolkit. Any time you need to greedily assign elements to groups, sequences, or slots, think about counting first and processing in order second.
Edge cases
Before submitting, make sure your solution handles these scenarios:
- Hand not divisible by groupSize:
hand = [1, 2, 3],groupSize = 2. The length is 3, which is not divisible by 2. ReturnFalsebefore doing any work. - Duplicate cards:
hand = [1, 1, 2, 2, 3, 3],groupSize = 3. Two copies of 1, 2, 3 means two groups[1,2,3]and[1,2,3]. Thewhile count[card] > 0loop handles this naturally. - Gaps in the sequence:
hand = [1, 2, 4, 5, 6, 7],groupSize = 3. Starting from 1, you need 1, 2, 3 but 3 is missing. ReturnFalse. - Single group:
hand = [5, 6, 7],groupSize = 3. One group that works. ReturnTrue. - groupSize equals 1: every card forms its own group. Always
True(as long as the input is valid).
From understanding to recall
The greedy logic here is clean: count, sort, and greedily build groups from the smallest card up. But in an interview, the details matter. Do you remember to check divisibility first? Do you handle the while count[card] > 0 loop correctly for duplicates? Do you decrement before checking or after?
These small details are where interviews trip people up. Reading the solution once gives you understanding, but it does not give you recall. Spaced repetition drills close that gap. You write the solution from scratch, then do it again a few days later, then a week later. After a few rounds, the pattern is automatic.
Related posts
- Group Anagrams - Another hash map grouping problem where you classify elements by a computed key
- Top K Frequent Elements - Frequency counting as the foundation for a selection algorithm
- Task Scheduler - Greedy approach driven by frequency counts
Hand of Straights is a clean example of how frequency counting plus sorted greedy iteration solves what initially looks like a complex partitioning problem. Learn the building blocks, drill them independently, and the full solution writes itself.