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
174 changes: 174 additions & 0 deletions group-anagrams/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
## 問題

[Group Anagrams - LeetCode](https://leetcode.com/problems/group-anagrams/description/)

- 入力
- `strs`: 文字列配列
- 長さは1以上10^4以下
- 要素の文字列の長さは0以上100以下
- 要素の文字列は英語小文字
- 出力
- `strs` の要素をアナグラムのグループごとにまとめた配列
- 順序は任意

## 解法

### 文字列要素をソートして map のキーとし、スライスに追加していく

- 時間計算量
- ソートの時間計算量は O(N log N): N は文字列要素の長さで最大値10^2
- これを各要素ごとに行う O(M): M は文字列要素の数で最大10^4
- ざっくり O(MN log N)
- 空間計算量
- 文字列のコピーが必要: O(MN)
- map のキーでO(M)、値でO(MN)
- ざっくりO(MN)
- 最大で10^4*10^2 = 1MB程度
- map でざっくり2倍の2MBくらい

### 文字列の登場回数を map のキーとし、スライスに追加していく

- 英語小文字という問題設定上 `byte` として処理する
- `rune` を使う必要がある場合は使えない
- ソートのほうが汎用的
- 時間計算量
- 各文字列について各文字ごとにカウント
- O(MN)
- 空間計算量
- 文字要素の長さは最大100だから各文字の登場回数は `byte` に収まる
- 長さ26の固定長配列を最大10^4個用意する
- 260KB
- go の map はざっくり2倍で520KB
- 大丈夫そう

## Step1

### ソート版

```go
func groupAnagrams(strs []string) [][]string {
groups := make(map[string][]string, len(strs))

for _, str := range strs {
strBytes := []byte(str)
slices.Sort(strBytes)
groups[string(strBytes)] = append(groups[string(strBytes)], str)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Go に詳しくないため、理解が間違っていたら申し訳ないです。
ここは破壊的な操作に思われるため、もし入力を変更しなくない場合は、非破壊での操作を検討するのも良いのかなと思いました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。

string から []byte へ変換後の Sort していることについてコメントいただいたと考えました。


Go の仕様では string は変更不可能なリソースとなっています。
The Go Programming Language Specification - The Go Programming Language

[]byte(str)[]byte へ型変換すると、新たにメモリが確保されコピーされます。
そのため、上記コードでは入力データは破壊されないようになっています。
The Go Programming Language Specification - The Go Programming Language


改めて公式ドキュメントを確認する機会をいただき、ありがとうございましたmm
今後ともよろしくお願いいたします。

}

anagramGroups := make([][]string, 0, len(groups))
for _, anagrams := range groups {
anagramGroups = append(anagramGroups, anagrams)
}

return anagramGroups
}
```

### 配列版

```go
func groupAnagrams(strs []string) [][]string {
groups := make(map[[26]byte][]string, len(strs))

for _, str := range strs {
var counts [26]byte
for i := 0; i < len(str); i++ {
counts[str[i]-'a']++
}
Comment on lines +73 to +77
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

leetcode 上ではアルファベットが入力として与えられているため問題になりませんが、アルファベット以外の記号がきた場合には、インデックスがマイナスになる場合が考えられます。これを避けるために、アルファベット以外が来たときは、エラーを投げる、continue する、などの対処法を考えても良さそうです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。

そうですね、実務で想定外の入力を考慮する場合はエラーを返すようにします。
要件次第ですが、下記のようにラベル付き continue を使って、可能な限り処理をすることも選択肢に入れておきます。

OuterLoop:
    for _, str := range strs {
        var counts [26]byte
        for i := 0; i < len(str); i++ {
            if str[i] < 'a' || 'z' < str[i] {
                continue OuterLoop
            }
            counts[str[i]-'a']++
        } 

        groups[counts] = append(groups[counts], str)
    }


groups[counts] = append(groups[counts], str)
}

anagramGroups := make([][]string, len(groups))
i := 0
for _, anagrams := range groups {
anagramGroups[i] = anagrams
i++
}

return anagramGroups
}
```

## Step2

### ソート版

```go
func groupAnagrams(strs []string) [][]string {
keyToAnagramGroup := make(map[string][]string, len(strs))

for _, str := range strs {
key := generateKey(str)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この関数の括りだし、可読性が上がっていいですね。
左辺と右辺とで情報が増えてないので、例えばこんな命名はどうでしょうか。

Suggested change
key := generateKey(str)
key := sortedString(str)

これだと「ソートした文字列をキーにするよ」が分かりやすいのではないでしょうか。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
同じような流れになったので、インターフェース的な発想で抽象度の高い名前になってしまっていました。
状況に応じて適切な命名ができるよう注意します。

keyToAnagramGroup[key] = append(keyToAnagramGroup[key], str)
}

return slices.Collect(maps.Values(keyToAnagramGroup))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こんな便利なメソッドを用意してくれてるんですね。
https://pkg.go.dev/slices#Collect

Goって基本的には「愚直に書け」って言ってくるので感心しました。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ここ最近で追加されました。
まだあまり使い慣れてないのですが、こういうところで慣れていこうと思っています。

}

func generateKey(s string) string {
sBytes := []byte(s)
slices.Sort(sBytes)
return string(sBytes)
}
```

### 配列版

```go
func groupAnagrams(strs []string) [][]string {
keyToAnagramGroup := make(map[[26]byte][]string, len(strs))

for _, str := range strs {
key := generateKey(str)
keyToAnagramGroup[key] = append(keyToAnagramGroup[key], str)
}

return slices.Collect(maps.Values(keyToAnagramGroup))
}

func generateKey(s string) [26]byte {
var counts [26]byte
for _, b := range []byte(s) {
counts[b-'a']++
}

return counts
}
```

- [slices package - slices - Go Packages](https://pkg.go.dev/slices#Collect)
- [maps package - maps - Go Packages](https://pkg.go.dev/maps#Values)
- 英語小文字を前提としているコード

### レビューを依頼する方のPR

- [49. Group Anagrams by mamo3gr · Pull Request #12 · mamo3gr/arai60](https://github.com/mamo3gr/arai60/pull/12)
- 単語の数を `N` 、単語の長さを `W` とおくとわかりやすい
- [49_group_anagrams by Hiroto-Iizuka · Pull Request #12 · Hiroto-Iizuka/coding_practice](https://github.com/Hiroto-Iizuka/coding_practice/pull/12)
- [49. Group Anagrams by TakayaShirai · Pull Request #12 · TakayaShirai/leetcode_practice](https://github.com/TakayaShirai/leetcode_practice/pull/12)
- Dart
- [49. Group Anagram by dxxsxsxkx · Pull Request #12 · dxxsxsxkx/leetcode](https://github.com/dxxsxsxkx/leetcode/pull/12)
- ハッシュを使われていた
- [49. Group Anagrams by aki235 · Pull Request #12 · aki235/Arai60](https://github.com/aki235/Arai60/pull/12)

## Step3

```go
func groupAnagrams(strs []string) [][]string {
keyToAnagramGroup := make(map[string][]string, len(strs))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nits]
名前をなるべく短くするGoに従うなら Anagram は削っても良さそうに思いました。Group と言われたら、この関数内では AnagramGroup であることは想像が付くので。

Suggested change
keyToAnagramGroup := make(map[string][]string, len(strs))
keyToGroup := make(map[string][]string, len(strs))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

そうですね、「伝わるか?」ということを考えてなるべく短い名前を採用するようにします


for _, s := range strs {
key := generateKey(s)
keyToAnagramGroup[key] = append(keyToAnagramGroup[key], s)
}

return slices.Collect(maps.Values(keyToAnagramGroup))
}

func generateKey(s string) string {
sBytes := []byte(s)
slices.Sort(sBytes)
return string(sBytes)
}
```