Skip to content
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
78 changes: 78 additions & 0 deletions 49/49.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
## 何も見ずに溶いてみる

ソートを使った解法で解いた。
最初sorted(string)を直接キーにしようとしたが、ミュータブルなのでエラーが出てしまった。一度文字列にしてキーとした。

```python
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
sorted_to_strings = defaultdict(list)
for string in strs:
sorted_ = "".join(sorted(string))
sorted_to_strings[sorted_].append(string)
return list(sorted_to_strings.values())
```

## いろいろ調べてみる

https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.e5dwa7yj3tv0
https://github.com/Fuminiton/LeetCode/pull/12/files それぞれのアルファベットの出現回数をリストで管理している
https://github.com/azriel1rf/leetcode-prep/pull/4/files
https://github.com/kazukiii/leetcode/pull/13/files frozensetを使っている方法
https://github.com/hayashi-ay/leetcode/pull/19/files
https://qiita.com/amber__dev/items/542cd6389a8c423f7dae

Choose a reason for hiding this comment

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

markdownをプレビューした時に改行が入っていないようです。行末に半角スペース2つ入れると改行されて見やすくなると思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

あ、本当ですね。ありがとうございます。

ひとまず十分そうな解法を思いついたらとりあえず実装に取り掛かってしまう癖があるのはよくないかもしれない。
次の問題からは発想を一つ思いついても他の手段を発想する時間を多少取るようにしてみよう。

自分のコードでいう"".join(sorted(string))の部分をtuple(sorted(string))としている方もいた。
個人的には本当にどっちでも良い気がしたのですが、皆さんの感覚としてはどうでしょうか。

Choose a reason for hiding this comment

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

アナグラムなので str の方が自然かもと思いますが、好みだと思われます。


frozensetを使った方法は
- 英小文字以外を含むすべての文字に対応している
- sortする方法と違い1つのstringあたりの計算量が文字列長をLとしてO(L)で済み、かつ同じ文字列が何度も登場するような入力でもメモリ消費が必要以上に増加しない
という点で良いなと思ったので、こちらのコードも作ってみた。

```python
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
group_to_strings = defaultdict(list)
for s in strs:
counter = Counter(s)

Choose a reason for hiding this comment

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

何のカウンターかわかるとより読みやすいと思います。

group = frozenset(counter.items())
group_to_strings[group].append(s)
return list(group_to_strings.values())
```

しかし、このコードでsubmitするとだいぶ時間がかかることに気づいた。上のsortを用いた解放は7msぐらいでできるのに対し、こちらは50msぐらいかかる。

https://github.com/python/cpython/blob/main/Lib/collections/__init__.py#L546
ここを見る限りcounterの処理はCで書かれたネイティブコードで走ってそう?なのでそこそこ早いかなと思ったのだが、ハッシュの計算がボトルネックになっているのだろうか?
Copy link

Choose a reason for hiding this comment

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

下につけたのは適当に拾ってきたソートの速度の比較ですが、ネイティブコードにすると10-20倍くらいは簡単に変わるんですね。

C 言語と Python は50-100倍くらい違い、ただしネイティブコードでもガーベージコレクションなど Python Object の操作をしないといけないので、C 言語並までにはならないだろうくらいの推測です。

文字列のハッシュ計算のほうが(タプルなどよりも構造が単純なので)なんとなく速そうですね。

https://medium.com/@tuvo1106/how-fast-can-you-sort-9f75d27cf9c1#7d4f
Python’s built-in sorted: 0.009s
Radix sort: 0.220s
Quicksort: 0.247s
Shell sort: 0.250s
Merge sort: 0.435s
Heap sort: 0.473s

Copy link

Choose a reason for hiding this comment

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

意図がちょっとはっきりしなかったんですが、速度が 7 ms か 50 ms かと7倍くらいの話をしていますが、これは同一アルゴリズムでも最適化で変わる範囲なので、インタープリターであることに理由を求める必要はないと思います、(これくらいの差は環境にもよるので色々実験しないといえることはあまりない)ということです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど、、ありがとうございます。

というか、むしろ普通にfor文で回したほうがCounterを使うより早かった。。。(38ms程度になった)

Choose a reason for hiding this comment

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

leetcode上の実行時間は目安程度と過去にコメントされてました。正確には計測するコードを書いた方が良さそうです。メモリ消費の観点ではどうなったかも気になりました。ですが実際には多目的最適化で、比較的コードの複雑さが優先されるとのことです。
Ryotaro25/leetcode_first60#66 (comment)


```python
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
group_to_strings = defaultdict(list)
for s in strs:
counter = defaultdict(int)
for c in s:
counter[c] += 1
group = frozenset(counter.items())
group_to_strings[group].append(s)
return list(group_to_strings.values())
```

ネイティブコードで走っていればいいというものでもないのか、そもそもネイティブコードで走っていないのか・・・詳しい方ご教示ください。
Copy link

Choose a reason for hiding this comment

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

sortedもネイティブコード化されているので,コンパイル済みかどうかはあまり関係ないかもしれないです.
償却のオーダーが小さくても,定数倍が重くて計算量と実際の処理速度が逆転することはしばしばある気がします.


## 最終コード(エラーを出さずに3回書いてみる)

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