diff --git a/49. Group Anagrams.md b/49. Group Anagrams.md new file mode 100644 index 0000000..b6f03b7 --- /dev/null +++ b/49. Group Anagrams.md @@ -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 + for char in string: + index = ord(char) - ord('a') + 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)`を別変数でおかなくても十分読めるなと感じた +- 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 + ) + 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()) +``` \ No newline at end of file