Skip to content

Add 49. Group Anagrams.md #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
177 changes: 177 additions & 0 deletions 49. Group Anagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# step 1
考えたこと
- アナグラムとなる文字列同士で共通しているもの->文字の出現回数
- 出現回数->それに該当する文字列の集合のマッピングを作りたい
- 出現回数の表現
- 単純に数え上げる, nを文字列の長さとしてO(n)
- アナグラムなら、sortしたら同じ文字列になる、O(n log(n)) or O(n^2)くらいか
- sortを10^4個の文字列でやりたくない
- 出現回数を数えてマッピングを作る。
```python3
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
def count_char_frequency(string: str) -> Tuple[int, ...]:
char_frequency = [0] * 26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alphabet_count = 26
char_frequency = [0] * alphabet_count

としてマジックナンバーを避けると良いと思いました

for char in string:
index = ord(char) - ord('a')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.python.org/3/library/functions.html#ord
文字コードを一回確認しておいてください。
たとえば、C++ だと、ASCII とは限らないのでアルファベットが連続しているとは限りません。
https://discord.com/channels/1084280443945353267/1301587996298182696/1309561565187538964
(コードの正誤ではなく)確認したい気持ちがあるかどうかを大事にしてください。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asciiでa-zが連続していることは知っていましたが、連続していない文字コードがあることは一切想定していなかったです。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python ord は Unicode のコードポイントが返ってくるので連続しているとしていいんです。つまり、結果的には同じコードになるんですが、しかし「Python ord は Unicode のコードポイントだからアルファベット部分は ASCII と同じ、だから連続しているとしてよい」と思って書いたほうがいいでしょう。
C / C++ だと、この保証はないのですが、しかし、そういう文字コードは稀なので「意図的にサポートを切り捨てた」と思っていればいいです。

char_frequency[index] += 1
return tuple(char_frequency)

frequency_to_strings = {}
for string in strs:
char_frequency = count_char_frequency(string)
if char_frequency not in frequency_to_strings:
frequency_to_strings[char_frequency] = []
frequency_to_strings[char_frequency].append(string)
return [strings for strings in frequency_to_strings.values()]
```
n=strs.length, mをstrsの中にある文字列の中で最大の長さとすると、
- time complexity: O(nm)
- space complexity: O(n)

`count_char_frequency()`の返り値の型で、流石に26個intを並べるのもどうかと思って`...`にした。
dictのkeyとなるものがhashableになる様に注意した。

sortするパターンも実装した
```python
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
sorted_string_to_anagrams = {}
for string in strs:
sorted_characters = sorted(string)
sorted_string = ''.join(sorted_characters)
if sorted_string not in sorted_string_to_anagrams:
sorted_string_to_anagrams[sorted_string] = []
sorted_string_to_anagrams[sorted_string].append(string)
return [anagrams for anagrams in sorted_string_to_anagrams.values()]
```
n=strs.length, mをstrsの中にある文字列の中で最大の長さとすると、
- time complexity: O(n m log(m))
- space complexity: O(n * m)

# step 2
文字出現回数のカウントにはCounterを使っても良かった。
上の二つはdefaultdictを使っても良かった。

一つ目の解法のほうがパフォーマンスは良いと思っていたがleetcodeのruntimeをみると、一つ目のほうが二つ目に比べ倍程度時間がかかっていた。

- https://github.com/katataku/leetcode/pull/11/files
- `list(dict.values())`としていた
- `sorted_string = ''.join(sorted(string))`としていた。これくらいなら、`sorted(string)`を別変数でおかなくても十分読めるなと感じた
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一応、str(sorted(string)) が ''.join(sorted(string)) とは異なるものであるという認識があるかの確認はしておきます。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

違いについて説明せよと言われてもできなそうなので、調べました。

文字列をsortしたリストを引数にした場合、どちらも返す値は変わらないので今回はどっちでもいいのかなぁくらいの感想でした。

sorted(string)

str(sorted(string))

''.join(sorted(string))

- https://github.com/colorbox/leetcode/pull/26/files
- sortする、数えるどちらも行っていた。
- 数える場合、英語小文字以外が来た場合の対処をどうするか
- 特殊な値を返す
- 例外処理
- 英語小文字以外無視
- プログラムを落とす

自分の実装は落ちるものだった。この問題では、特殊な値として適するものが思い当たらないので、例外処理を入れるか無視するかが穏当な気がした。
- https://github.com/tarinaihitori/leetcode/pull/12/files
- [pythonのhashable](https://docs.python.org/3/glossary.html#term-hashable)
- tupleとstrのhashの計算
- [tuple](https://github.com/python/cpython/blob/260843df1bd8a28596b9a377d8266e2547f7eedc/Objects/tupleobject.c#L318):tuple内の全ての要素に対してhashを計算し、tuple自体のhashを求める
- str
- https://github.com/python/cpython/blob/47c5a0f307cff3ed477528536e8de095c0752efa/Python/pyhash.c#L149
- https://github.com/python/cpython/blob/2513593303b306cd8273682811d26600651c60e4/Objects/unicodeobject.c#L11691

hashがsetされていればそれを返し、されていなければ計算する。
- [joinがメモリ割り当てを一回で済ませている](https://github.com/python/cpython/blob/main/Objects/stringlib/join.h#L109-L110)
- tupleも一回で済ませているようにみえた
- https://github.com/python/cpython/blob/47c5a0f307cff3ed477528536e8de095c0752efa/Objects/tupleobject.c#L68
- https://github.com/python/cpython/blob/47c5a0f307cff3ed477528536e8de095c0752efa/Objects/tupleobject.c#L374

```python
from collections import defaultdict


class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
sorted_string_to_anagrams = defaultdict(list)
for s in strs:
sorted_string = str(sorted(s))
sorted_string_to_anagrams[sorted_string].append(s)
return list(sorted_string_to_anagrams.values())
```
この様に書くと、lowercase以外が入ってきてもうごく。

```python
from collections import defaultdict, Counter
import string


class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
lowercase_frequency_to_anagrams = defaultdict(list)
for s in strs:
frequency = Counter(s)
lowercase_frequency = tuple(
frequency[lowercase] for lowercase in string.ascii_lowercase
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感想: こういう書き方もあるんですね

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

リストの内包表記の部品ですよ。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あー、上は誤りですね。ジェネレータ式は文法上内包表記とは別物でした。
https://docs.python.org/3/reference/expressions.html#generator-expressions
https://peps.python.org/pep-0289/

lowercase_frequency_to_anagrams[lowercase_frequency].append(s)
return list(lowercase_frequency_to_anagrams.values())
```
上の様に書くと、lowercase以外は無視される。
関数内でしか使わない変数にしては`lowercase_frequency_to_anagrams`は長い気がする。

# step 3
```python3
from collections import defaultdict


class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
sorted_string_to_anagrams = defaultdict(list)
for s in strs:
sorted_string = str(sorted(s))
sorted_string_to_anagrams[sorted_string].append(s)
return list(sorted_string_to_anagrams.values())
```

# step 4
コメントまとめ
- https://docs.python.org/3/library/functions.html#ord
> Python ord は Unicode のコードポイントが返ってくるので連続しているとしていいんです。つまり、結果的には同じコードになるんですが、しかし「Python ord は Unicode のコードポイントだからアルファベット部分は ASCII と同じ、だから連続しているとしてよい」と思って書いたほうがいいでしょう。
C / C++ だと、この保証はないのですが、しかし、そういう文字コードは稀なので「意図的にサポートを切り捨てた」と思っていればいいです。
- 切り捨てたものにも自覚的になるべき。というか、意識して切り捨てたのか、たまたま切り捨てた様にみえるのかは一緒に仕事したい基準に大きく関わってきそう。
- step 1で英語小文字の数をマジックナンバーで置いてしまっている。


あるキーに対応する値は、一つのanagramのグループだと考え、dictの末尾をanagramsからanagramに変更した。


sorted stringをkeyにする
```python
from collections import defaultdict


class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
sorted_string_to_anagram = defaultdict(list)
for s in strs:
sorted_string = str(sorted(s))
sorted_string_to_anagram[sorted_string].append(s)
return list(sorted_string_to_anagram.values())
```

英語小文字の出現回数を数える
```python
from collections import Counter
import string

class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
def count_lowercase_frequency(s: str) -> Tuple[int, ...]:
char_to_count = Counter(s)
return tuple(
char_to_count[lowercase] for lowercase in string.ascii_lowercase
)

frequency_to_anagram = {}
for s in strs:
lowercase_frequency = count_lowercase_frequency(s)
if lowercase_frequency not in frequency_to_anagram:
frequency_to_anagram[lowercase_frequency] = []
frequency_to_anagram[lowercase_frequency].append(s)
return list(frequency_to_anagram.values())
```