Skip to content

Commit bd90354

Browse files
committed
feat(algorithms, hash table): word pattern
1 parent b0326eb commit bd90354

3 files changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Word Pattern
2+
3+
Given a `pattern` and a string `s`, find if s follows the same pattern.
4+
5+
Here follow means a full match, such that there is a bijection between a letter in `pattern` and a non-empty word in `s`.
6+
Specifically:
7+
8+
- Each letter in `pattern` maps to exactly one unique word in `s`.
9+
- Each unique word in `s` maps to exactly one letter in `pattern`.
10+
- No two letters map to the same word, and no two words map to the same letter.
11+
12+
## Examples
13+
14+
Example 1:
15+
16+
```text
17+
Input: pattern = "abba", s = "dog cat cat dog"
18+
Output: true
19+
Explanation:
20+
The bijection can be established as:
21+
22+
'a' maps to "dog".
23+
'b' maps to "cat".
24+
```
25+
26+
Example 2:
27+
28+
```text
29+
Input: pattern = "abba", s = "dog cat cat fish"
30+
31+
Output: false
32+
```
33+
34+
Example 3:
35+
36+
```text
37+
Input: pattern = "aaaa", s = "dog cat cat dog"
38+
39+
Output: false
40+
```
41+
42+
## Constraints
43+
44+
- 1 <= `pattern.lengt`h <= 300
45+
- `pattern` contains only lower-case English letters.
46+
- 1 <= `s.length` <= 3000
47+
- `s` contains only lowercase English letters and spaces ' '.
48+
- `s` does not contain any leading or trailing spaces.
49+
- All the words in `s` are separated by a single space.
50+
51+
## Topics
52+
53+
- Hash Table
54+
- String
55+
56+
## Solution(s)
57+
58+
1. [Using two hash maps](#using-two-hash-maps)
59+
2. [Using One hash map](#using-one-hash-map)
60+
61+
### Using two hash maps
62+
63+
The key insight is recognizing that we need to maintain a bidirectional mapping between pattern characters and words.
64+
Think of it like a translation dictionary that works both ways - if we know that 'a' translates to "dog", then "dog"
65+
must always translate back to 'a'.
66+
67+
Why do we need two hash tables instead of just one? Consider this scenario:
68+
69+
- If we only track that 'a' -> "dog" and 'b' -> "dog", we wouldn't catch the error that two different letters map to the
70+
same word.
71+
- Similarly, if pattern is "aa" and string is "dog cat", using only one mapping wouldn't detect that the same letter 'a'
72+
is trying to map to two different words.
73+
74+
The bidirectional check ensures both conditions of the bijection are satisfied:
75+
76+
- Forward mapping (d1): Ensures each pattern character consistently maps to the same word
77+
- Reverse mapping (d2): Ensures each word consistently maps to the same pattern character
78+
79+
As we iterate through the pattern and words simultaneously using zip, we check:
80+
81+
- If we've seen this pattern character before (a in d1), does it still map to the same word?
82+
- If we've seen this word before (b in d2), does it still map to the same pattern character?
83+
84+
If either check fails, we know the bijection is broken. If we successfully process all pairs without conflicts, the
85+
pattern matches.
86+
87+
The initial length check (len(pattern) != len(ws)) is a quick optimization - if the pattern has a different number of
88+
characters than we have words, there's no possible valid mapping.
89+
90+
### Time and Space Complexity
91+
92+
#### Time Complexity: O(m + n), where `m` is the length of the pattern string and `n` is the length of string `s`
93+
94+
- Splitting string `s` by spaces takes `O(n)` time as it needs to traverse the entire string.
95+
- The zip operation combined with the loop iterates through `min(pattern length, words length)` elements, which is at
96+
most `O(m)`iterations after the length check.
97+
- Inside each iteration, dictionary lookups and insertions (in operator and assignment) take `O(1)` average time.
98+
99+
Therefore, the overall time complexity is `O(n)` for splitting + `O(m)` for the loop = `O(m + n)`.
100+
101+
#### Space Complexity: O(m + n), where `m` is the length of the pattern string and `n` is the length of string `s`
102+
103+
- The `words` list stores all words from string `s`, which takes `O(n)` space in the worst case (when `s` contains no
104+
spaces, the entire string is one word).
105+
- Dictionary `pattern_to_word` stores at most `m` key-value pairs (pattern characters to words), requiring `O(m)` space
106+
for keys plus the space for word values.
107+
- Dictionary `word_to_pattern` stores at most `m` key-value pairs (words to pattern characters), requiring space for word
108+
keys plus `O(m)` space for character values.
109+
- The total space used by both dictionaries is proportional to the number of unique pattern characters and unique words,
110+
bounded by `O(m + n)`.
111+
112+
Therefore, the overall space complexity is `O(m + n)`.
113+
114+
### Using One Hash Map
115+
116+
The algorithm checks whether a given string of words follows the same pattern as a sequence of characters, ensuring a
117+
one-to-one correspondence (bijection) between pattern characters and words. It begins by splitting the string s into
118+
individual words and verifying that the number of characters in the pattern matches the number of words; if not, it
119+
immediately returns False. Using a hash map, the algorithm records the first index at which each pattern character and
120+
word appear by creating unique keys. As it iterates through the words, it checks that the indices associated with a
121+
character and its corresponding word remain consistent, ensuring that each character maps to exactly one word and each
122+
word maps to exactly one character. If any mismatch occurs in this mapping, return False; otherwise, return True,
123+
confirming that the string follows the specified pattern.
124+
125+
The steps of the algorithm are as follows:
126+
127+
1. Create an empty dictionary map_index to store the mapping indices for pattern characters and words.
128+
2. Use s.split() to divide the string into a list of words.
129+
3. If the number of characters in the pattern does not equal the number of words, return False immediately.
130+
4. Iterate through each character–word pair simultaneously using their indices.
131+
- For each character c and word w, form two distinct keys:
132+
- `char_key = 'char_{}'.format(c)`
133+
- `char_word = 'word_{}'.format(w)`
134+
- Assign or compare mapping indices:
135+
- If a key does not exist in map_index, assign its value as the current index i.
136+
- After the assignment, compare the stored indices for both keys.
137+
- If the indices do not match, return False (indicating inconsistent mapping).
138+
5. After processing all pairs without inconsistencies, return True, confirming that the pattern matches the word sequence
139+
correctly.
140+
141+
#### Time and Space Complexity
142+
143+
##### Time Complexity
144+
145+
The time complexity of the above solution is `O(n)` due to splitting the string and iterating through all characters and
146+
words once. Each lookup and insertion in the hash map takes `O(1)` on average, making the total linear in input size.
147+
148+
##### Space Complexity
149+
150+
The space complexity of the above solution is `O(n)` because it stores all unique pattern–word mappings and the list of
151+
words. The hash map and word list grow proportionally to the number of words in the input.
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from typing import Dict
2+
3+
4+
def word_pattern(pattern: str, s: str) -> bool:
5+
"""
6+
Check if a pattern matches a string following a bijective mapping.
7+
Each character in pattern should map to exactly one word in s, and vice versa.
8+
9+
Args:
10+
pattern: A string containing the pattern (e.g., "abba")
11+
s: A space-separated string of words (e.g., "dog cat cat dog")
12+
13+
Returns:
14+
True if the pattern matches the string, False otherwise
15+
"""
16+
# Split the string into individual words
17+
words = s.split()
18+
19+
# If lengths don't match, pattern cannot be followed
20+
if len(pattern) != len(words):
21+
return False
22+
23+
# Dictionary to map pattern characters to words
24+
pattern_to_word: Dict[str, str] = {}
25+
# Dictionary to map words to pattern characters (ensures bijection)
26+
word_to_pattern: Dict[str, str] = {}
27+
28+
# Iterate through pattern characters and corresponding words simultaneously
29+
for pattern_char, word in zip(pattern, words):
30+
# Check if pattern character already has a different word mappting
31+
if pattern_char in pattern_to_word and pattern_to_word[pattern_char] != word:
32+
return False
33+
34+
# Check if word already has a different pattern character mapping
35+
if word in word_to_pattern and word_to_pattern[word] != pattern_char:
36+
return False
37+
38+
# Establish the bidirectional mapping
39+
pattern_to_word[pattern_char] = word
40+
word_to_pattern[word] = pattern_char
41+
42+
return True
43+
44+
45+
def word_pattern_2(pattern: str, s: str) -> bool:
46+
"""
47+
Check if a pattern matches a string following a bijective mapping.
48+
Each character in pattern should map to exactly one word in s, and vice versa.
49+
50+
Args:
51+
pattern: A string containing the pattern (e.g., "abba")
52+
s: A space-separated string of words (e.g., "dog cat cat dog")
53+
54+
Returns:
55+
True if the pattern matches the string, False otherwise
56+
"""
57+
# Create a dictionary to store index mappings for both characters and words
58+
map_index: Dict[str, int] = {}
59+
60+
# Split the input string into individual words
61+
words = s.split()
62+
63+
# If the number of pattern characters and words don't match,
64+
# there can't be a one-to-one correspondence
65+
if len(pattern) != len(words):
66+
return False
67+
68+
# Iterate through each character-word pair
69+
for idx in range(len(words)):
70+
pattern_char = pattern[idx] # Current character in the pattern
71+
word = words[idx] # Corresponding word in the string
72+
73+
# Create separate keys for characters and words
74+
# This helps differentiate between them in the same dictionary
75+
char_key = f"char_{pattern_char}"
76+
char_word = f"word_{word}"
77+
78+
# If this character hasn't been seen before, store its index
79+
if char_key not in map_index:
80+
map_index[char_key] = idx
81+
82+
# If this word hasn't been seen before, store its index
83+
if char_word not in map_index:
84+
map_index[char_word] = idx
85+
86+
# If the indices for the character and word don't match,
87+
# it means they were mapped inconsistently — return False
88+
if map_index[char_key] != map_index[char_word]:
89+
return False
90+
91+
# If we finish the loop without mismatches, the pattern is valid
92+
return True
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from utils.test_utils import custom_test_name_func
4+
from algorithms.hash_table.word_pattern import word_pattern, word_pattern_2
5+
6+
WORD_PATTERN_TEST_CASES = [
7+
("abba", "dog cat cat dog", True),
8+
("abba", "dog dog dog dog", False),
9+
("abba", "dog cat cat fish", False),
10+
("aaaa", "dog cat cat dog", False),
11+
("abc", "red blue green", True),
12+
("abcba", "sun moon star moon sun", True),
13+
]
14+
15+
16+
class WordPatternTestCase(unittest.TestCase):
17+
@parameterized.expand(WORD_PATTERN_TEST_CASES, name_func=custom_test_name_func)
18+
def test_word_pattern(self, pattern: str, s: str, expected: bool):
19+
actual = word_pattern(pattern, s)
20+
self.assertEqual(expected, actual)
21+
22+
@parameterized.expand(WORD_PATTERN_TEST_CASES, name_func=custom_test_name_func)
23+
def test_word_pattern_2(self, pattern: str, s: str, expected: bool):
24+
actual = word_pattern_2(pattern, s)
25+
self.assertEqual(expected, actual)
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()

0 commit comments

Comments
 (0)