Facebook Pixel

290. Word Pattern

EasyHash TableString
Leetcode Link

Problem Description

You are given a pattern string and a string s. You need to determine if s follows the same pattern.

For s to follow the pattern, there must be a bijection (one-to-one correspondence) between each letter in pattern and each word in s. This means:

  • Each letter in pattern must map to exactly one unique word in s. For example, if letter 'a' maps to word "dog", then every occurrence of 'a' must correspond to "dog".

  • Each unique word in s must map to exactly one letter in pattern. For example, if word "dog" maps to letter 'a', then every occurrence of "dog" must correspond to 'a'.

  • No two different letters can map to the same word, and no two different words can map to the same letter.

The string s contains words separated by spaces, and each word is non-empty.

Example:

  • If pattern = "abba" and s = "dog cat cat dog", this would return true because:

    • 'a' maps to "dog"
    • 'b' maps to "cat"
    • The pattern abba corresponds to dog cat cat dog
  • If pattern = "abba" and s = "dog cat cat fish", this would return false because:

    • 'a' would need to map to both "dog" and "fish", which violates the bijection rule

The solution uses two hash tables to track the bidirectional mapping between pattern characters and words, ensuring that the bijection property is maintained throughout the traversal.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need to maintain a bidirectional mapping between pattern characters and words. Think of it like a translation dictionary that works both ways - if we know that 'a' translates to "dog", then "dog" must always translate back to 'a'.

Why do we need two hash tables instead of just one? Consider this scenario:

  • If we only track that 'a' -> "dog" and 'b' -> "dog", we wouldn't catch the error that two different letters map to the same word.
  • Similarly, if pattern is "aa" and string is "dog cat", using only one mapping wouldn't detect that the same letter 'a' is trying to map to two different words.

The bidirectional check ensures both conditions of the bijection are satisfied:

  1. Forward mapping (d1): Ensures each pattern character consistently maps to the same word
  2. Reverse mapping (d2): Ensures each word consistently maps to the same pattern character

As we iterate through the pattern and words simultaneously using zip, we check:

  • If we've seen this pattern character before (a in d1), does it still map to the same word?
  • If we've seen this word before (b in d2), does it still map to the same pattern character?

If either check fails, we know the bijection is broken. If we successfully process all pairs without conflicts, the pattern matches.

The initial length check (len(pattern) != len(ws)) is a quick optimization - if the pattern has a different number of characters than we have words, there's no possible valid mapping.

Solution Approach

The implementation uses two hash tables to maintain the bidirectional mapping between pattern characters and words.

Step 1: Split and Length Check

ws = s.split()
if len(pattern) != len(ws):
    return False

First, we split the string s by spaces to get a list of words ws. If the number of pattern characters doesn't match the number of words, we immediately return False since a valid bijection is impossible.

Step 2: Initialize Two Hash Tables

d1 = {}  # Maps pattern character -> word
d2 = {}  # Maps word -> pattern character
  • d1 tracks the mapping from each character in the pattern to its corresponding word
  • d2 tracks the reverse mapping from each word to its corresponding pattern character

Step 3: Iterate and Validate Mappings

for a, b in zip(pattern, ws):
    if (a in d1 and d1[a] != b) or (b in d2 and d2[b] != a):
        return False
    d1[a] = b
    d2[b] = a

For each pair (a, b) where a is a pattern character and b is a word:

  1. Check for conflicts:
    • If a already exists in d1 but maps to a different word than b, the pattern is violated
    • If b already exists in d2 but maps to a different character than a, the pattern is violated
  2. Update mappings: If no conflicts are found, we establish or confirm the mappings:
    • d1[a] = b: Character a maps to word b
    • d2[b] = a: Word b maps to character a

Step 4: Return Success If we complete the iteration without finding any conflicts, return True.

Time Complexity: O(n + m) where n is the length of the pattern and m is the length of string s (for splitting)

Space Complexity: O(n) for storing the mappings in both hash tables, where at most we store n unique mappings

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with pattern = "abb" and s = "cat dog dog".

Step 1: Split and Length Check

  • Split s into words: ws = ["cat", "dog", "dog"]
  • Check lengths: len(pattern) = 3, len(ws) = 3 ✓ (equal, continue)

Step 2: Initialize Hash Tables

  • d1 = {} (pattern char → word)
  • d2 = {} (word → pattern char)

Step 3: Process Each Pair

Iteration 1: a = 'a', b = "cat"

  • Check: Is 'a' in d1? No. Is "cat" in d2? No.
  • No conflicts found, update mappings:
    • d1 = {'a': "cat"}
    • d2 = {"cat": 'a'}

Iteration 2: a = 'b', b = "dog"

  • Check: Is 'b' in d1? No. Is "dog" in d2? No.
  • No conflicts found, update mappings:
    • d1 = {'a': "cat", 'b': "dog"}
    • d2 = {"cat": 'a', "dog": 'b'}

Iteration 3: a = 'b', b = "dog"

  • Check:
    • Is 'b' in d1? Yes. Does d1['b'] == "dog"? Yes ✓
    • Is "dog" in d2? Yes. Does d2["dog"] == 'b'? Yes ✓
  • No conflicts found, mappings remain:
    • d1 = {'a': "cat", 'b': "dog"}
    • d2 = {"cat": 'a', "dog": 'b'}

Step 4: Return Result All pairs processed without conflicts → Return True

Counter-example: If we had pattern = "abb" and s = "cat dog cat":

  • At iteration 3: a = 'b', b = "cat"
  • Check: 'b' is in d1 and d1['b'] = "dog", but b = "cat"
  • Conflict detected ("dog" != "cat") → Return False

Solution Implementation

1class Solution:
2    def wordPattern(self, pattern: str, s: str) -> bool:
3        """
4        Check if a pattern matches a string following a bijective mapping.
5        Each character in pattern should map to exactly one word in s, and vice versa.
6      
7        Args:
8            pattern: A string containing the pattern (e.g., "abba")
9            s: A space-separated string of words (e.g., "dog cat cat dog")
10      
11        Returns:
12            True if the pattern matches the string, False otherwise
13        """
14        # Split the string into individual words
15        words = s.split()
16      
17        # If lengths don't match, pattern cannot be followed
18        if len(pattern) != len(words):
19            return False
20      
21        # Dictionary to map pattern characters to words
22        pattern_to_word = {}
23        # Dictionary to map words to pattern characters (ensures bijection)
24        word_to_pattern = {}
25      
26        # Iterate through pattern characters and corresponding words simultaneously
27        for pattern_char, word in zip(pattern, words):
28            # Check if pattern character already has a different word mapping
29            if pattern_char in pattern_to_word and pattern_to_word[pattern_char] != word:
30                return False
31          
32            # Check if word already has a different pattern character mapping
33            if word in word_to_pattern and word_to_pattern[word] != pattern_char:
34                return False
35          
36            # Establish the bidirectional mapping
37            pattern_to_word[pattern_char] = word
38            word_to_pattern[word] = pattern_char
39      
40        return True
41
1class Solution {
2    public boolean wordPattern(String pattern, String s) {
3        // Split the string into words by spaces
4        String[] words = s.split(" ");
5      
6        // Check if pattern length matches the number of words
7        if (pattern.length() != words.length) {
8            return false;
9        }
10      
11        // Map to store pattern character to word mapping
12        Map<Character, String> patternToWord = new HashMap<>();
13        // Map to store word to pattern character mapping (for bijection check)
14        Map<String, Character> wordToPattern = new HashMap<>();
15      
16        // Iterate through each pattern character and corresponding word
17        for (int i = 0; i < words.length; i++) {
18            char patternChar = pattern.charAt(i);
19            String word = words[i];
20          
21            // Check if the current mapping is consistent with existing mappings
22            // If patternChar exists in map, it should map to the current word
23            // If word exists in map, it should map to the current patternChar
24            if (!patternToWord.getOrDefault(patternChar, word).equals(word) || 
25                wordToPattern.getOrDefault(word, patternChar) != patternChar) {
26                return false;
27            }
28          
29            // Update both mappings
30            patternToWord.put(patternChar, word);
31            wordToPattern.put(word, patternChar);
32        }
33      
34        return true;
35    }
36}
37
1class Solution {
2public:
3    bool wordPattern(string pattern, string s) {
4        // Parse the string into individual words
5        istringstream stringStream(s);
6        vector<string> words;
7        string word;
8        while (stringStream >> word) {
9            words.push_back(word);
10        }
11      
12        // Check if pattern length matches number of words
13        if (pattern.size() != words.size()) {
14            return false;
15        }
16      
17        // Create bidirectional mappings
18        unordered_map<char, string> charToWord;  // Maps pattern character to word
19        unordered_map<string, char> wordToChar;  // Maps word to pattern character
20      
21        // Iterate through pattern and words simultaneously
22        for (int i = 0; i < words.size(); ++i) {
23            char currentChar = pattern[i];
24            string currentWord = words[i];
25          
26            // Check if existing mappings are consistent
27            // If char already mapped to different word, or word already mapped to different char
28            if ((charToWord.count(currentChar) && charToWord[currentChar] != currentWord) || 
29                (wordToChar.count(currentWord) && wordToChar[currentWord] != currentChar)) {
30                return false;
31            }
32          
33            // Create/update the bidirectional mapping
34            charToWord[currentChar] = currentWord;
35            wordToChar[currentWord] = currentChar;
36        }
37      
38        return true;
39    }
40};
41
1/**
2 * Checks if a string follows the same pattern as a given pattern string.
3 * Each character in pattern should map to exactly one word in the string,
4 * and each word should map to exactly one character (bijective mapping).
5 * 
6 * @param pattern - The pattern string consisting of lowercase letters
7 * @param s - The input string containing words separated by spaces
8 * @returns true if the string follows the pattern, false otherwise
9 */
10function wordPattern(pattern: string, s: string): boolean {
11    // Split the input string into individual words
12    const words: string[] = s.split(' ');
13  
14    // Early return if pattern length doesn't match word count
15    if (pattern.length !== words.length) {
16        return false;
17    }
18  
19    // Map to store pattern character to word mappings
20    const patternToWordMap = new Map<string, string>();
21  
22    // Map to store word to pattern character mappings (ensures bijection)
23    const wordToPatternMap = new Map<string, string>();
24  
25    // Iterate through each character-word pair
26    for (let i = 0; i < pattern.length; i++) {
27        const currentPatternChar: string = pattern[i];
28        const currentWord: string = words[i];
29      
30        // Check if pattern character already has a different word mapping
31        if (patternToWordMap.has(currentPatternChar) && 
32            patternToWordMap.get(currentPatternChar) !== currentWord) {
33            return false;
34        }
35      
36        // Check if word already has a different pattern character mapping
37        if (wordToPatternMap.has(currentWord) && 
38            wordToPatternMap.get(currentWord) !== currentPatternChar) {
39            return false;
40        }
41      
42        // Establish the bidirectional mapping
43        patternToWordMap.set(currentPatternChar, currentWord);
44        wordToPatternMap.set(currentWord, currentPatternChar);
45    }
46  
47    return true;
48}
49

Time and Space Complexity

Time Complexity: O(m + n), where m is the length of the pattern string and n is the length of string s.

  • Splitting string s by spaces takes O(n) time as it needs to traverse the entire string.
  • The zip operation combined with the loop iterates through min(pattern length, words length) elements, which is at most O(m) iterations after the length check.
  • Inside each iteration, dictionary lookups and insertions (in operator and assignment) take O(1) average time.
  • Therefore, the overall time complexity is O(n) for splitting + O(m) for the loop = O(m + n).

Space Complexity: O(m + n), where m is the length of the pattern string and n is the length of string s.

  • The ws list stores all words from string s, which takes O(n) space in the worst case (when s contains no spaces, the entire string is one word).
  • Dictionary d1 stores at most m key-value pairs (pattern characters to words), requiring O(m) space for keys plus the space for word values.
  • Dictionary d2 stores at most m key-value pairs (words to pattern characters), requiring space for word keys plus O(m) space for character values.
  • The total space used by both dictionaries is proportional to the number of unique pattern characters and unique words, bounded by O(m + n).
  • Therefore, the overall space complexity is O(m + n).

Common Pitfalls

Pitfall: Using Only One Hash Table

A frequent mistake is using only a single hash table to map pattern characters to words, without checking the reverse mapping. This approach fails to ensure a proper bijection.

Incorrect Implementation:

def wordPattern(self, pattern: str, s: str) -> bool:
    words = s.split()
    if len(pattern) != len(words):
        return False
  
    pattern_to_word = {}
  
    for pattern_char, word in zip(pattern, words):
        if pattern_char in pattern_to_word:
            if pattern_to_word[pattern_char] != word:
                return False
        else:
            pattern_to_word[pattern_char] = word
  
    return True

Why This Fails: This implementation only checks if each pattern character maps consistently to the same word, but doesn't verify that different pattern characters map to different words.

Failing Test Case:

  • Input: pattern = "abba", s = "dog dog dog dog"
  • The incorrect code returns True (both 'a' and 'b' map to "dog")
  • Expected output: False (violates bijection - two different letters cannot map to the same word)

Solution: Always use two hash tables to maintain bidirectional mapping:

  1. One mapping from pattern characters to words (pattern_to_word)
  2. One mapping from words to pattern characters (word_to_pattern)

This ensures that:

  • No two different pattern characters map to the same word
  • No two different words map to the same pattern character

The correct implementation checks both directions of the mapping, guaranteeing a true one-to-one correspondence between pattern characters and words.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More