Skip to content

Commit 1254903

Browse files
authored
Merge pull request #98 from BrianLusina/feat/group-anagrams
feat(strings, anagrams): group anagrams
2 parents e84daec + b1c9167 commit 1254903

File tree

8 files changed

+178
-1
lines changed

8 files changed

+178
-1
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,8 @@
676676

677677
## Pystrings
678678
* Anagram
679+
* Group Anagrams
680+
* [Test Group Anagrams](https://github.com/BrianLusina/PythonSnips/blob/master/pystrings/anagram/group_anagrams/test_group_anagrams.py)
679681
* [Test Anagram](https://github.com/BrianLusina/PythonSnips/blob/master/pystrings/anagram/test_anagram.py)
680682
* Balanced Paren
681683
* [Test Balanced Paren](https://github.com/BrianLusina/PythonSnips/blob/master/pystrings/balanced_paren/test_balanced_paren.py)

poetry.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ readme = "README.md"
88

99
[tool.poetry.dependencies]
1010
python = "^3.11"
11+
parameterized = "^0.9.0"
1112

1213
[tool.poetry.group.dev.dependencies]
1314
ruff = "^0.3.5"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Group Anagrams
2+
3+
Given a list of words or phrases, group the words that are anagrams of each other. An anagram is a word or phrase formed
4+
from another word by rearranging its letters.
5+
6+
Constraints:
7+
8+
Let `strs` be the list of strings given as input to find the anagrams.
9+
10+
- 1 <= `strs.length` <=10^3
11+
- 0 <= `strs[i].length` <= 100
12+
- `strs[i]` consists of lowercase English letters
13+
14+
> Note the order in which the output is displayed doesn't matter
15+
16+
## Examples
17+
18+
![Example one](images/group_anagrams_example_one.png)
19+
![Example two](images/group_anagrams_example_two.png)
20+
21+
22+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from typing import List, Dict, Tuple
2+
from collections import defaultdict
3+
4+
5+
def group_anagrams_naive(strs: List[str]) -> List[List[str]]:
6+
"""
7+
Groups a list of strings by their anagrams.
8+
9+
Parameters:
10+
strs (List[str]): A list of strings
11+
12+
Returns:
13+
List[List[str]]: A list of lists, where each inner list contains strings that are anagrams of each other
14+
"""
15+
word_map: Dict[str, List[str]] = defaultdict(list)
16+
# traversing the list of strings takes O(n) time where n is the number of strings in this list
17+
for word in strs:
18+
# Note that the sorting here takes O(nlog(n)) time were n is the number of characters in this string
19+
key = "".join(sorted(word.lower()))
20+
word_map[key].append(word)
21+
22+
return list(word_map.values())
23+
24+
def group_anagrams(strs: List[str]) -> List[List[str]]:
25+
"""
26+
Groups a list of strings by their anagrams.
27+
28+
This uses A better approach than sorting can be used to solve this problem. This solution involves computing the
29+
frequency of each letter in every string. This will help reduce the time complexity of the given problem. We’ll just
30+
compute the frequency of every string and store the strings in their respective list in a hash map.
31+
32+
We see that all members of each set are characterized by the same frequency of each letter. This means that the
33+
frequency of each letter in the words belonging to the same group is equal. In the set [["speed", "spede"]], the
34+
frequency of the characters s, p, e, and d are the same in each word.
35+
36+
Let’s see how we can implement the above algorithm:
37+
38+
- For each string, compute a 26-element list. Each element in this list represents the frequency of an English letter
39+
in the corresponding string. This frequency count will be represented as a tuple. For example, "abbccc" will be
40+
represented as (1, 2, 3, 0, 0, ..., 0). This mapping will generate identical lists for strings that are anagrams.
41+
42+
- Use this list as a key to insert the strings into a hash map. All anagrams will be mapped to the same key in this
43+
hash map.
44+
45+
- While traversing each string, we generate its 26-element list and check if this list is present as a key in the
46+
hash map. If it does, we'll append the string to the array corresponding to that key. Otherwise, we'll add the new
47+
key-value pair to the hash map.
48+
49+
- Return the values of the hash map in a two-dimensional array, since each value will be an individual set of
50+
anagrams.
51+
52+
Parameters:
53+
strs (List[str]): A list of strings
54+
55+
Returns:
56+
List[List[str]]: A list of lists, where each inner list contains strings that are anagrams of each other
57+
"""
58+
word_map: Dict[Tuple[int,...], List[str]] = defaultdict(list)
59+
# traversing the list of strings takes O(n) time where n is the number of strings in this list
60+
for word in strs:
61+
count = [0] * 26
62+
for char in word.lower():
63+
index = ord(char) - ord('a')
64+
count[index] += 1
65+
66+
key = tuple(count)
67+
word_map[key].append(word)
68+
69+
return list(word_map.values())
25.2 KB
Loading
21.5 KB
Loading
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from . import group_anagrams, group_anagrams_naive
4+
5+
6+
class GroupAnagramsTestCase(unittest.TestCase):
7+
@parameterized.expand([
8+
("test_1", ["eat", "beat", "neat", "tea"], [["eat", "tea"], ["beat"], ["neat"]]),
9+
("test_2", ["duel", "dule", "speed", "spede", "deul", "cars"], [["duel", "dule", "deul"], ["speed", "spede"], ["cars"]]),
10+
("test_3", ["eat","tea","tan","ate","nat","bat"], [["eat","tea","ate"],["tan","nat"],["bat"]]),
11+
("test_4", ["word","sword","drow","rowd","iced","dice"], [["word","drow","rowd"],["sword"],["iced","dice"]]),
12+
("test_5", ["eat","drink","sleep","repeat"], [["eat"],["drink"],["sleep"],["repeat"]]),
13+
("test_6", ["hello","ohlle","dark"], [["hello","ohlle"],["dark"]])
14+
])
15+
def test1(self, _, strs, expected):
16+
actual = group_anagrams(strs)
17+
self.assertEqual(actual, expected)
18+
19+
@parameterized.expand([
20+
("test_1", ["eat", "beat", "neat", "tea"], [["eat", "tea"], ["beat"], ["neat"]]),
21+
("test_2", ["duel", "dule", "speed", "spede", "deul", "cars"], [["duel", "dule", "deul"], ["speed", "spede"], ["cars"]]),
22+
("test_3", ["eat","tea","tan","ate","nat","bat"], [["eat","tea","ate"],["tan","nat"],["bat"]]),
23+
("test_4", ["word","sword","drow","rowd","iced","dice"], [["word","drow","rowd"],["sword"],["iced","dice"]]),
24+
("test_5", ["eat","drink","sleep","repeat"], [["eat"],["drink"],["sleep"],["repeat"]]),
25+
("test_6", ["hello","ohlle","dark"], [["hello","ohlle"],["dark"]])
26+
])
27+
def test2(self, _, strs, expected):
28+
actual = group_anagrams_naive(strs)
29+
self.assertEqual(actual, expected)
30+
31+
def test_1(self):
32+
strs = ["eat", "beat", "neat", "tea"]
33+
expected = [["eat", "tea"], ["beat"], ["neat"]]
34+
actual = group_anagrams(strs)
35+
self.assertEqual(expected, actual)
36+
37+
def test_2(self):
38+
strs = ["duel", "dule", "speed", "spede", "deul", "cars"]
39+
expected = [["duel", "dule", "deul"], ["speed", "spede"], ["cars"]]
40+
actual = group_anagrams(strs)
41+
self.assertEqual(expected, actual)
42+
43+
def test_3(self):
44+
strs = ["eat","tea","tan","ate","nat","bat"]
45+
expected = [["eat","tea","ate"],["tan","nat"],["bat"]]
46+
actual = group_anagrams(strs)
47+
self.assertEqual(expected, actual)
48+
49+
def test_4(self):
50+
strs = ["word","sword","drow","rowd","iced","dice"]
51+
expected = [["word","drow","rowd"],["sword"],["iced","dice"]]
52+
actual = group_anagrams(strs)
53+
self.assertEqual(expected, actual)
54+
55+
def test_5(self):
56+
strs = ["eat","drink","sleep","repeat"]
57+
expected = [["eat"],["drink"],["sleep"],["repeat"]]
58+
actual = group_anagrams(strs)
59+
self.assertEqual(expected, actual)
60+
61+
def test_6(self):
62+
strs = ["hello","ohlle","dark"]
63+
expected = [["hello","ohlle"],["dark"]]
64+
actual = group_anagrams(strs)
65+
self.assertEqual(expected, actual)
66+
67+
68+
if __name__ == '__main__':
69+
unittest.main()

0 commit comments

Comments
 (0)