290. Word Pattern
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 ins
. 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 inpattern
. 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"
ands = "dog cat cat dog"
, this would returntrue
because:'a'
maps to"dog"
'b'
maps to"cat"
- The pattern
abba
corresponds todog cat cat dog
-
If
pattern = "abba"
ands = "dog cat cat fish"
, this would returnfalse
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.
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:
- Forward mapping (
d1
): Ensures each pattern character consistently maps to the same word - 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 wordd2
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:
- Check for conflicts:
- If
a
already exists ind1
but maps to a different word thanb
, the pattern is violated - If
b
already exists ind2
but maps to a different character thana
, the pattern is violated
- If
- Update mappings: If no conflicts are found, we establish or confirm the mappings:
d1[a] = b
: Charactera
maps to wordb
d2[b] = a
: Wordb
maps to charactera
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 EvaluatorExample 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'
ind1
? No. Is"cat"
ind2
? No. - No conflicts found, update mappings:
d1 = {'a': "cat"}
d2 = {"cat": 'a'}
Iteration 2: a = 'b'
, b = "dog"
- Check: Is
'b'
ind1
? No. Is"dog"
ind2
? 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'
ind1
? Yes. Doesd1['b'] == "dog"
? Yes ✓ - Is
"dog"
ind2
? Yes. Doesd2["dog"] == 'b'
? Yes ✓
- Is
- 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 ind1
andd1['b'] = "dog"
, butb = "cat"
- Conflict detected (
"dog" != "cat"
) → ReturnFalse
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 takesO(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 mostO(m)
iterations after the length check. - Inside each iteration, dictionary lookups and insertions (
in
operator and assignment) takeO(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 strings
, which takesO(n)
space in the worst case (whens
contains no spaces, the entire string is one word). - Dictionary
d1
stores at mostm
key-value pairs (pattern characters to words), requiringO(m)
space for keys plus the space for word values. - Dictionary
d2
stores at mostm
key-value pairs (words to pattern characters), requiring space for word keys plusO(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:
- One mapping from pattern characters to words (
pattern_to_word
) - 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.
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!