Keyboard Row
Filter words typeable on a single keyboard row by mapping each letter to its row number
Problem Statement
Given an array of words, return those that can be typed using letters from only one row of an American QWERTY keyboard.
Row 1: q w e r t y u i o p
Row 2: a s d f g h j k l
Row 3: z x c v b n m
Input: ["Hello", "Alaska", "Dad", "Peace"]
Output: ["Alaska", "Dad"]
Why: "Alaska" uses only a, l, s, k → all row 2
"Dad" uses only d, a → all row 2
"Hello" uses h (row 2) and e (row 1) → mixedIntuition
The naive reading suggests checking each word against each of three sets and seeing whether any set contains everything. That works, but there is a cleaner formulation.
The key insight
Build one map from letter to row number, then a word is valid exactly when all its letters map to the same row number.
Look up the row of the first letter, then verify every other letter agrees. One pass per word, no set operations.
Case is irrelevant to which key you press, so lowercase everything before looking up. That is the detail the "Dad" example is there to test.
Real-world analogy
Checking whether a shopping list can be filled from a single aisle. You do not compare the list against every aisle's inventory — you look up the aisle number of the first item, then confirm each remaining item has that same number.
Watch it run
Approach
Build the letter-to-row map once
Iterate the three row strings, recording each letter's row index.
Verify every remaining letter has the same row
Lowercase before every lookup.
Solution
def find_words(words: list[str]) -> list[str]:
"""Words typeable using letters from a single keyboard row."""
rows = ["qwertyuiop", "asdfghjkl", "zxcvbnm"]
# letter -> which row it lives on
row_of = {letter: index for index, row in enumerate(rows) for letter in row}
result: list[str] = []
for word in words:
lowered = word.lower()
first_row = row_of[lowered[0]]
if all(row_of[char] == first_row for char in lowered):
result.append(word) # append the ORIGINAL, preserving its case
return resultA set-based alternative, which reads well and is equally correct:
def find_words_sets(words: list[str]) -> list[str]:
rows = [set("qwertyuiop"), set("asdfghjkl"), set("zxcvbnm")]
return [
word
for word in words
if any(set(word.lower()) <= row for row in rows)
]<= on sets is the subset test — "every letter of the word lives in this row".
Return the original words, not the lowercased ones
"Dad" must come back as "Dad", not "dad". Lowercasing is only for the lookup — append the original string to the result.
Complexity Analysis
Time Complexity
O(n × L)
Space Complexity
O(1)
Where n is the number of words and L is the average word length.
- Time — building the map touches 26 letters (constant), and each word is scanned once with an early exit on the first mismatch.
- Space — the map holds exactly 26 entries regardless of input, so it does not grow — effectively
O(1). The output list is the only input-dependent allocation.
Edge Cases
Related Problems
- Shortest Completing Word — filtering words by a character-based condition
- Unique Email Addresses — normalising strings before comparing them
- Valid Anagram — character-level string testing