Hand of Straights
MedPrompt
Alice has some number of cards and she wants to rearrange the cards into groups so that each group is of size groupSize, and consists of groupSize consecutive cards.
Given an integer array hand where hand[i] is the value written on the ith card and an integer groupSize, return true if she can rearrange the cards, or false otherwise.
Example 1:
Input: hand = [1,2,3,6,2,3,4,7,8], groupSize = 3
Output: true
Explanation: Alice's hand can be rearranged as [1,2,3],[2,3,4],[6,7,8]Example 2:
Input: hand = [1,2,3,4,5], groupSize = 4
Output: false
Explanation: Alice's hand can not be rearranged into groups of 4.
Constraints:
1 <= hand.length <= 1040 <= hand[i] <= 1091 <= groupSize <= hand.length
Note: This question is the same as 1296: https://leetcode.com/problems/divide-array-in-sets-of-k-consecutive-numbers/
Approaches
3 approaches with complexity analysis and trade-offs.
This approach first sorts the hand to make finding consecutive cards easier. It then iterates through the sorted hand, and for each card not yet used, it tries to form a new group of groupSize consecutive cards by scanning the rest of the array. A boolean array is used to keep track of cards that have been placed into a group.
Algorithm
- First, perform a basic check: if the total number of cards
hand.lengthis not divisible bygroupSize, it's impossible to form the required groups, so returnfalse. - Sort the
handarray in ascending order. This makes it easier to find consecutive cards. - Create a boolean array
usedof the same size ashand, initialized tofalse, to keep track of which cards have been assigned to a group. - Iterate through the sorted
handfrom left to right. For each cardhand[i]:- If the card is already used (
used[i]istrue), skip it. - If it's not used, treat it as the starting card of a new group.
- Mark
used[i]astrue. - Now, search for the next
groupSize - 1consecutive cards (hand[i] + 1,hand[i] + 2, etc.) in the rest of the array. - For each required consecutive card, perform a linear scan from the current position onwards to find an available (unused) card with the correct value.
- If a required card is found, mark it as used and continue searching for the next one in the sequence.
- If at any point a required consecutive card cannot be found, it means a valid group cannot be formed from the current starting card. Return
false.
- If the card is already used (
- If the entire
handarray is traversed and all cards are successfully grouped, returntrue.
Walkthrough
import java.util.Arrays; class Solution { public boolean isNStraightHand(int[] hand, int groupSize) { int n = hand.length; if (n % groupSize != 0) { return false; } Arrays.sort(hand); boolean[] used = new boolean[n]; for (int i = 0; i < n; i++) { if (used[i]) { continue; } // This card starts a new group int lastCard = hand[i]; used[i] = true; int cardsInGroup = 1; // Find the rest of the group int searchIdx = i + 1; while (cardsInGroup < groupSize) { int nextCardNeeded = lastCard + 1; int foundIdx = -1; // Linearly scan for the next card for (int j = searchIdx; j < n; j++) { if (!used[j] && hand[j] == nextCardNeeded) { foundIdx = j; break; } } if (foundIdx != -1) { used[foundIdx] = true; lastCard = hand[foundIdx]; cardsInGroup++; searchIdx = foundIdx + 1; // Optimization: start next search after the found card } else { // Could not find the next consecutive card return false; } } } return true; }}Complexity
Time
O(N^2). Sorting the array takes O(N log N). The main loop iterates through N elements. Inside, for each potential group start, we might scan a large portion of the array up to `groupSize` times. This results in a time complexity dominated by the nested search, which is approximately O((N/groupSize) * groupSize * N) = O(N^2).
Space
O(N), where N is the number of cards in the hand. This space is used for the `used` boolean array.
Trade-offs
Pros
Conceptually simple and easy to understand.
It's a direct simulation of the grouping process.
Cons
Highly inefficient due to the nested loops and repeated linear scanning.
Will likely result in a 'Time Limit Exceeded' error on larger inputs.
This approach improves upon the brute-force method by using a hash map to count the frequency of each card, which avoids the costly linear scans. After counting, it sorts the unique card numbers and iterates through them to form groups. For each starting card, it checks if enough consecutive cards exist and updates their counts in the map.
Algorithm
- First, check if
hand.lengthis divisible bygroupSize. If not, returnfalse. - Create a
HashMapto store the frequency of each card. Iterate through thehandarray and populate this map. For example,map.put(card, map.getOrDefault(card, 0) + 1). - Extract the unique card numbers (the keys of the map) into a
List. - Sort this list of unique keys in ascending order.
- Iterate through the
sortedKeyslist. - For each
keyin the list:- Get its frequency from the map:
count = cardCounts.get(key). - If
countis 0, it means this card has already been fully used in groups starting with smaller cards, so we cancontinueto the next key. - If
count > 0, it signifies that we must formcountnew groups starting with thiskey. - To form these groups, we check for the availability of the next
groupSize - 1consecutive cards. Loop fromi = 0togroupSize - 1:- Let
currentCard = key + i. - Check if the map has enough copies of
currentCard. IfcardCounts.getOrDefault(currentCard, 0) < count, returnfalse. - If available, 'use' these cards by decrementing their counts in the map:
cardCounts.put(currentCard, cardCounts.get(currentCard) - count).
- Let
- Get its frequency from the map:
- If the loop completes without returning
false, it means all cards were successfully grouped. Returntrue.
Walkthrough
import java.util.Collections;import java.util.HashMap;import java.util.ArrayList;import java.util.List;import java.util.Map; class Solution { public boolean isNStraightHand(int[] hand, int groupSize) { if (hand.length % groupSize != 0) { return false; } Map<Integer, Integer> cardCounts = new HashMap<>(); for (int card : hand) { cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); } List<Integer> sortedKeys = new ArrayList<>(cardCounts.keySet()); Collections.sort(sortedKeys); for (int key : sortedKeys) { int count = cardCounts.get(key); if (count > 0) { // If this card is a potential start of a new group(s) for (int i = 0; i < groupSize; i++) { int currentCard = key + i; if (cardCounts.getOrDefault(currentCard, 0) < count) { return false; } cardCounts.put(currentCard, cardCounts.get(currentCard) - count); } } } return true; }}Complexity
Time
O(N + U log U + U * groupSize), where N is the number of cards and U is the number of unique cards. Building the map is O(N). Sorting the U keys is O(U log U). Iterating through U keys and for each, checking `groupSize` cards is O(U * groupSize). In the worst case (U=N), this is O(N log N + N * groupSize).
Space
O(U), where U is the number of unique cards. In the worst case, U can be N, so O(N). This space is for the `HashMap` and the list of keys.
Trade-offs
Pros
Much more efficient than the brute-force approach by avoiding repeated scans of the array.
Handles duplicate cards efficiently through frequency counting.
Cons
The performance can degrade if
groupSizeis large, as the complexity has aU * groupSizeterm.Requires an explicit sorting step after building the map.
This is the most efficient approach. It uses a TreeMap, which is a sorted map, to store card frequencies. This combines the counting and sorting steps, allowing us to greedily form groups starting from the smallest available card without a separate sorting phase. By always processing the smallest card first, we ensure that if a solution exists, this greedy strategy will find it.
Algorithm
- As with other approaches, first check if
hand.length % groupSize != 0and returnfalseif true. - Create a
TreeMap<Integer, Integer>to store the frequency of each card. ATreeMapis a sorted map, so it will automatically keep the card numbers (keys) in ascending order. - Populate the
TreeMapby iterating through thehandarray. - Start a loop that continues as long as the
TreeMapis not empty. - Inside the loop, get the smallest card currently available. This is the greedy choice for the start of a new group. This can be done via
map.firstKey(). - Get the frequency of this starting card,
startCard. Let this becount. - This
counttells us we need to formcountgroups starting withstartCard. - Now, verify and consume the cards for these
countgroups. Iterate fromi = 0togroupSize - 1:- Let
currentCard = startCard + i. - Check if the map contains
currentCardand if its frequency is at leastcount. IfcardCounts.getOrDefault(currentCard, 0) < count, it's impossible to form the groups, so returnfalse. - If the check passes, update the frequency of
currentCardby subtractingcount:cardCounts.put(currentCard, cardCounts.get(currentCard) - count). - If the new frequency of
currentCardbecomes 0, remove it from the map entirely usingcardCounts.remove(currentCard). This is crucial for efficiency and for the loop's termination condition.
- Let
- If the
whileloop finishes (meaning the map becomes empty), all cards have been successfully arranged into groups. Returntrue.
Walkthrough
import java.util.TreeMap;import java.util.Map; class Solution { public boolean isNStraightHand(int[] hand, int groupSize) { if (hand.length % groupSize != 0) { return false; } // TreeMap stores keys in sorted order Map<Integer, Integer> cardCounts = new TreeMap<>(); for (int card : hand) { cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); } while (!cardCounts.isEmpty()) { // Get the smallest card number available int startCard = cardCounts.keySet().iterator().next(); // or ((TreeMap<Integer, Integer>) cardCounts).firstKey(); int count = cardCounts.get(startCard); // We need to form 'count' groups starting with 'startCard'. // Check if we have enough consecutive cards for all 'count' groups. for (int i = 0; i < groupSize; i++) { int currentCard = startCard + i; if (cardCounts.getOrDefault(currentCard, 0) < count) { return false; } // Use up 'count' cards of value 'currentCard' cardCounts.put(currentCard, cardCounts.get(currentCard) - count); // If the count of a card becomes zero, remove it from the map if (cardCounts.get(currentCard) == 0) { cardCounts.remove(currentCard); } } } return true; }}Complexity
Time
O(N log U), where N is the number of cards and U is the number of unique cards. Building the `TreeMap` takes O(N log U) as each of the N insertions takes O(log U) time. The processing loop consumes each card exactly once. Each consumption involves map operations (get, put, remove) which take O(log U) time. Thus, the total processing time is also bounded by O(N log U). In the worst case where U=N, the complexity is O(N log N).
Space
O(U), where U is the number of unique cards. In the worst case, U can be N, so O(N). This space is for the `TreeMap`.
Trade-offs
Pros
Most efficient and robust solution.
The greedy strategy is guaranteed to work because we always start with the smallest available card.
The
TreeMaphandles sorting implicitly, leading to cleaner code and better overall time complexity.
Cons
The underlying data structure (
TreeMap, a balanced binary search tree) is slightly more complex than aHashMap.
Solutions
Solution
class Solution {public boolean isNStraightHand(int[] hand, int groupSize) { Map<Integer, Integer> cnt = new HashMap<>(); for (int v : hand) { cnt.put(v, cnt.getOrDefault(v, 0) + 1); } Arrays.sort(hand); for (int v : hand) { if (cnt.containsKey(v)) { for (int x = v; x < v + groupSize; ++x) { if (!cnt.containsKey(x)) { return false; } cnt.put(x, cnt.get(x) - 1); if (cnt.get(x) == 0) { cnt.remove(x); } } } } return true; }}class Solution {public: bool isNStraightHand(vector<int> &hand, int groupSize) { unordered_map<int, int> cnt; for (int &v : hand) ++cnt[v]; sort(hand.begin(), hand.end()); for (int &v : hand) { if (cnt.count(v)) { for (int x = v; x < v + groupSize; ++x) { if (!cnt.count(x)) { return false; } if (--cnt[x] == 0) { cnt.erase(x); } } } } return true; }};class Solution: def isNStraightHand(self, hand: List[int], groupSize: int) -> bool: cnt = Counter(hand) for v in sorted(hand): if cnt[v]: for x in range(v, v + groupSize): if cnt[x] == 0: return False cnt[x] -= 1 if cnt[x] == 0: cnt . pop(x) return TrueVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.