Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PYTHON_VERSION = 3.13
PROBLEM ?= gas_station
PROBLEM ?= design_add_and_search_words_data_structure
FORCE ?= 0
COMMA := ,

Expand Down
47 changes: 47 additions & 0 deletions leetcode/design_add_and_search_words_data_structure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Design Add and Search Words Data Structure

**Difficulty:** Medium
**Topics:** String, Depth-First Search, Design, Trie
**Tags:** grind

**LeetCode:** [Problem 211](https://leetcode.com/problems/design-add-and-search-words-data-structure/description/)

## Problem Description

Design a data structure that supports adding new words and finding if a string matches any previously added string.

Implement the `WordDictionary` class:

- `WordDictionary()` Initializes the object.
- `void addWord(word)` Adds `word` to the data structure, it can be matched later.
- `bool search(word)` Returns `true` if there is any string in the data structure that matches `word` or `false` otherwise. `word` may contain dots `'.'` where dots can be matched with any letter.

## Examples

### Example 1:

```
Input
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
Output
[null,null,null,null,false,true,true,true]

Explanation
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // return False
wordDictionary.search("bad"); // return True
wordDictionary.search(".ad"); // return True
wordDictionary.search("b.."); // return True
```

## Constraints

- `1 <= word.length <= 25`
- `word` in `addWord` consists of lowercase English letters.
- `word` in `search` consist of `'.'` or lowercase English letters.
- There will be at most `2` dots in `word` for `search` queries.
- At most `10^4` calls will be made to `addWord` and `search`.
Empty file.
25 changes: 25 additions & 0 deletions leetcode/design_add_and_search_words_data_structure/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Any


def run_word_dictionary(solution_class: type, operations: list[str], inputs: list[list[str]]):
wd: Any = None
results: list[bool | None] = []

for op, args in zip(operations, inputs):
if op == "WordDictionary":
wd = solution_class()
results.append(None)
elif op == "addWord":
assert wd is not None
wd.add_word(args[0])
results.append(None)
elif op == "search":
assert wd is not None
results.append(wd.search(args[0]))

return results


def assert_word_dictionary(result: list[bool | None], expected: list[bool | None]) -> bool:
assert result == expected
return True
30 changes: 30 additions & 0 deletions leetcode/design_add_and_search_words_data_structure/playground.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.3
# kernelspec:
# display_name: leetcode-py-py3.13
# language: python
# name: python3
# ---

# %%
from helpers import assert_word_dictionary, run_word_dictionary
from solution import WordDictionary

# %%
# Example test case
operations = ["WordDictionary", "addWord", "addWord", "addWord", "search", "search", "search", "search"]
inputs = [[], ["bad"], ["dad"], ["mad"], ["pad"], ["bad"], [".ad"], ["b.."]]
expected = [None, None, None, None, False, True, True, True]

# %%
result = run_word_dictionary(WordDictionary, operations, inputs)
result

# %%
assert_word_dictionary(result, expected)
37 changes: 37 additions & 0 deletions leetcode/design_add_and_search_words_data_structure/solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Any


class WordDictionary:

# Time: O(1)
# Space: O(1)
def __init__(self) -> None:
self.root: dict[str, Any] = {}

# Time: O(m) where m = len(word)
# Space: O(m) for new word
def add_word(self, word: str) -> None:
node = self.root
for char in word:
if char not in node:
node[char] = {}
node = node[char]
node["#"] = True

# Time: O(n * 26^k) where n = len(word), k = number of dots
# Space: O(n) for recursion stack
def search(self, word: str) -> bool:
def dfs(i: int, node: dict[str, Any]) -> bool:
if i == len(word):
return "#" in node

char = word[i]
if char == ".":
for key in node:
if key != "#" and dfs(i + 1, node[key]):
return True
return False
else:
return char in node and dfs(i + 1, node[char])

return dfs(0, self.root)
130 changes: 130 additions & 0 deletions leetcode/design_add_and_search_words_data_structure/test_solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import pytest

from leetcode_py import logged_test

from .helpers import assert_word_dictionary, run_word_dictionary
from .solution import WordDictionary


class TestDesignAddAndSearchWordsDataStructure:

@logged_test
@pytest.mark.parametrize(
"operations, inputs, expected",
[
(
[
"WordDictionary",
"addWord",
"addWord",
"addWord",
"search",
"search",
"search",
"search",
],
[[], ["bad"], ["dad"], ["mad"], ["pad"], ["bad"], [".ad"], ["b.."]],
[None, None, None, None, False, True, True, True],
),
(
["WordDictionary", "addWord", "search", "search", "search"],
[[], ["a"], ["a"], ["."], ["aa"]],
[None, None, True, True, False],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[[], ["at"], ["and"], ["an"], [".at"], ["an."]],
[None, None, None, False, False, True],
),
(
["WordDictionary", "addWord", "addWord", "search", "search"],
[[], ["word"], ["world"], ["word"], ["wor."]],
[None, None, None, True, True],
),
(
["WordDictionary", "addWord", "search", "search"],
[[], ["test"], ["test"], ["t..t"]],
[None, None, True, True],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[[], ["a"], ["b"], ["a"], ["."], ["c"]],
[None, None, None, True, True, False],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[[], ["abc"], ["def"], ["..."], ["a.."], ["..f"]],
[None, None, None, True, True, True],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[
[],
["programming"],
["algorithm"],
["prog......."],
["algo....."],
["........ing"],
],
[None, None, None, True, True, True],
),
(
["WordDictionary", "addWord", "addWord", "search", "search"],
[[], ["x"], ["xy"], ["."], [".."]],
[None, None, None, True, True],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[[], ["hello"], ["world"], ["hi"], ["word"], ["......"]],
[None, None, None, False, False, False],
),
(
[
"WordDictionary",
"addWord",
"addWord",
"addWord",
"search",
"search",
"search",
"search",
],
[[], ["cat"], ["car"], ["card"], ["c.."], ["ca."], ["c..d"], ["....."]],
[None, None, None, None, True, True, True, False],
),
(
[
"WordDictionary",
"addWord",
"addWord",
"addWord",
"search",
"search",
"search",
],
[
[],
["run"],
["runner"],
["running"],
["run"],
["run..."],
["run....."],
],
[None, None, None, None, True, True, False],
),
(
["WordDictionary", "addWord", "addWord", "search", "search", "search"],
[[], ["abc"], ["xyz"], ["..."], [".."], ["...."]],
[None, None, None, True, False, False],
),
],
)
def test_word_dictionary(
self,
operations: list[str],
inputs: list[list[str]],
expected: list[bool | None],
):
result = run_word_dictionary(WordDictionary, operations, inputs)
assert_word_dictionary(result, expected)
46 changes: 46 additions & 0 deletions leetcode/group_anagrams/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Group Anagrams

**Difficulty:** Medium
**Topics:** Array, Hash Table, String, Sorting
**Tags:** grind

**LeetCode:** [Problem 49](https://leetcode.com/problems/group-anagrams/description/)

## Problem Description

Given an array of strings `strs`, group the anagrams together. You can return the answer in **any order**.

An **anagram** is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

## Examples

### Example 1:

```
Input: strs = ["eat","tea","tan","ate","nat","bat"]
Output: [["bat"],["nat","tan"],["ate","eat","tea"]]
Explanation:
- There is no string in strs that can be rearranged to form "bat".
- The strings "nat" and "tan" are anagrams as they can be rearranged to form each other.
- The strings "ate", "eat", and "tea" are anagrams as they can be rearranged to form each other.
```

### Example 2:

```
Input: strs = [""]
Output: [[""]]
```

### Example 3:

```
Input: strs = ["a"]
Output: [["a"]]
```

## Constraints

- `1 <= strs.length <= 10^4`
- `0 <= strs[i].length <= 100`
- `strs[i]` consists of lowercase English letters.
Empty file.
13 changes: 13 additions & 0 deletions leetcode/group_anagrams/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def run_group_anagrams(solution_class: type, strs: list[str]):
implementation = solution_class()
return implementation.group_anagrams(strs)


def assert_group_anagrams(result: list[list[str]], expected: list[list[str]]) -> bool:
# Sort both result and expected for comparison since order doesn't matter
result_sorted = [sorted(group) for group in result]
expected_sorted = [sorted(group) for group in expected]
result_sorted.sort()
expected_sorted.sort()
assert result_sorted == expected_sorted
return True
29 changes: 29 additions & 0 deletions leetcode/group_anagrams/playground.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.3
# kernelspec:
# display_name: leetcode-py-py3.13
# language: python
# name: python3
# ---

# %%
from helpers import assert_group_anagrams, run_group_anagrams
from solution import Solution

# %%
# Example test case
strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
expected = [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]]

# %%
result = run_group_anagrams(Solution, strs)
result

# %%
assert_group_anagrams(result, expected)
25 changes: 25 additions & 0 deletions leetcode/group_anagrams/solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Solution:

# Time: O(n * k) - when k > 26 use counting O(k), when k ≤ 26 use sorting O(k log k)
# Space: O(n * k)
def group_anagrams(self, strs: list[str]) -> list[list[str]]:
groups: dict[str | tuple[int, ...], list[str]] = {}

for s in strs:
if len(s) >= 26:
# Use counting for short strings (better time)
# Time: O(k) - single pass through string + O(26) for tuple
# Space: O(26) = O(1) per key
count = [0] * 26
for c in s:
count[ord(c) - ord("a")] += 1
key: tuple[int, ...] | str = tuple(count)
else:
# Use sorting for long strings (better space)
# Time: O(k log k) - sorting dominates
# Space: O(k) per key
key: tuple[int, ...] | str = "".join(sorted(s))

groups.setdefault(key, []).append(s)

return list(groups.values())
Loading