DSA Guide
Strings

Keyboard Row

Filter words typeable on a single keyboard row by mapping each letter to its row number

EasyHash Map Lookup
ArrayHash TableString
Problem #500

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) → mixed

Intuition

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

Checking "Dad" → all letters must share a row
Scanning
d
a
d
letter → row
KeyValue
d2
a2
row2
Lowercase 'D' to 'd'. Its row is 2 — that becomes the required row for the whole word.
1 / 4
One map lookup per character, with an early exit on the first disagreement.

Approach

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 result

A 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

On this page