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
361 changes: 361 additions & 0 deletions arai60/hash-map/group-anagrams/answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
# 49. Group Anagrams

## STEP1

### 発想

* それぞれのwordに対して、以下を行う。
* alphabetでsortしたワードを作成。
* keyをsortしたword, valueを元々のwordを更新して追加する。
* 最後にmapのvaluesのみ取り出す。

```javascript
const groupAnagrams = function (strs) {
const sorted_word_to_anagrams = new Map()
for (const original_word of strs) {
const sorted_word = original_word.split('').sort().join()
if (sorted_word_to_anagrams.get(sorted_word) === undefined) {
sorted_word_to_anagrams.set(sorted_word, [])
}
const original_words = sorted_word_to_anagrams.get(sorted_word)
original_words.push(original_word)
sorted_word_to_anagrams.set(sorted_word, original_words)
}
const generator = sorted_word_to_anagrams.values()
let next = generator.next()
const ans = []
while (!next.done) {
ans.push(next.value)
next = generator.next()
}
return ans
};
```

## STEP2

```javascript
const groupAnagrams = function (strs) {
const sorted_word_to_anagrams = new Map()
for (const original_word of strs) {
const sorted_word = original_word.split('').sort().join()
if (sorted_word_to_anagrams.get(sorted_word) === undefined) {
sorted_word_to_anagrams.set(sorted_word, [original_word])
continue
}
const original_words = sorted_word_to_anagrams.get(sorted_word)
original_words.push(original_word)
sorted_word_to_anagrams.set(sorted_word, original_words)
}

const ans = []
const generator = sorted_word_to_anagrams.values()
let next = generator.next()
while (!next.done) {
ans.push(next.value)
next = generator.next()
}
return ans
};
```

## STEP3

```javascript
const groupAnagrams = function(strs) {
const sorted_to_anagrams = new Map()
for (const original_word of strs) {

Choose a reason for hiding this comment

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

original_word, soretd_wordは名前としてごちゃっとしすぎかなと感じます。
変数名が長いので、パッと見た時にロジックを追うよりも、変数名の識別に脳の容量が割かれる感じです。
original_word $\rightarrow$ s or word
soretd_word $\rightarrow$ sorted or t
とかが好きです。

コメント集のこの部分が参考になると思います。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.fcs3httrll4l

Copy link
Owner Author

@syoshida20 syoshida20 May 10, 2025

Choose a reason for hiding this comment

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

ありがとうございます。

パッと見た時にロジックを追うよりも、変数名の識別に脳の容量が割かれる感じです。

こちらの感覚を僕は持てていませんでした。参考になります。

コメント集を見て、短くても正しく伝わる方法を模索しようと思います。

const sorted_word = original_word.split('').sort().join()
if (!sorted_to_anagrams.has(sorted_word)) {
sorted_to_anagrams.set(sorted_word, [original_word])
continue
}
const original_words = sorted_to_anagrams.get(sorted_word)
original_words.push(original_word)
sorted_to_anagrams.set(sorted_word, original_words)
Copy link

Choose a reason for hiding this comment

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

JavaScript 知らないんですが、この set っているんですか?

Copy link
Owner Author

@syoshida20 syoshida20 May 10, 2025

Choose a reason for hiding this comment

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

質問の意図を汲み取れているのか怪しいので、質問をさせてください。

この set っているんですか?

こちらは、どういう意味でしょうか?

Copy link

Choose a reason for hiding this comment

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

こういうことです。

const m = new Map();
m.set(1, []);
const a = m.get(1);
a.push("hello");
console.log(m.get(1));

Copy link
Owner Author

Choose a reason for hiding this comment

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

意図が理解できました!

コードを試したところ、mapのvalueが正しく置き換わっていることを確認しました。

> const m = new Map();
> m.set(1, []);
> const a = m.get(1);
> a.push("hello");
> console.log(m.get(1));
[ 'hello' ]

また、Mapのget関数のドキュメントを参照したところ、MapのValueがObjectの場合には、更新可能という記載を見つけました。 調べる習慣/量が足りていないので、増やそうと思います。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get

If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map object.

Copy link
Owner Author

@syoshida20 syoshida20 May 11, 2025

Choose a reason for hiding this comment

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

ObjectにArrayが含まれていることも確認しました!

Objectの一覧: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
Primitive Value (Objectでないデータ型)の一覧 : https://developer.mozilla.org/en-US/docs/Glossary/Primitive

}
const ans = new Array()
Copy link

@chanseok-lim chanseok-lim May 6, 2025

Choose a reason for hiding this comment

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

もしpythonだったらconst ans = Array()return ansの直前で改行を入れたくなります。javaにおける改行ってどんな感じなんでしょうか?

また ansという変数名は「問題への答え」という気持ちが前に出過ぎていて、システムに組み込み込まれているとしたら変だなという感じです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

いくつかスタイルガイドを見てみたのですが、改行に関する記載は見つかりませんでした。

const ans = Array()の前の改行に関しては、どちらものケースもあるかなと思います。
return前に改行を入れるのはあまり見ないイメージがあります。

変数名についてもシステムに組み込むことを意識し、resultという変数を使おうと思います!

Copy link

Choose a reason for hiding this comment

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

改行の入れ方はわりと自由です。慣れるにつれてだんだん改行が減ってくる傾向があるかなとは思います。

const generator = sorted_to_anagrams.values()
let next = generator.next()
while (!next.done) {
ans.push(next.value)
next = generator.next()
}
return ans
};
```

* wordごとに、アルファベットの出現回数をカウントする方法

```javascript
const groupAnagrams = function(strs) {
const valid_inputs = 'abcdefghijklmnopqrstuvwxyz'
const key_to_anagrams = {}
for (const word of strs) {
const char_count = new Array(26).fill(0)
for (const ch of word) {
if (!valid_inputs.includes(ch)) {
throw new Error("invalid input")
}
const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0)
char_count[idx]++
}
const key = arrayToVarLenQuantity(char_count)
if (key_to_anagrams[key] === undefined) {
key_to_anagrams[key] = [word]
continue
}
key_to_anagrams[key].push(word)
}
return Object.values(key_to_anagrams)
};

const arrayToVarLenQuantity = function(array) {
for (let i = 0; i < array.length; i++) {
// 問題の入力で、strs[i].length <= 100のため。
if (array[i] > 256) {
throw new Error("invalid input")
}
if (array[i] < 16) {
array[i] = '0x0' + array[i].toString(16)
continue
}
array[i] = '0x' + array[i].toString(16)
}
return array.join('')
}
```

## 動かないコードの例

* (誤り)

```javascript
const groupAnagrams = function(strs) {
const sorted_words = strs.map((word) => word.split('').sort().join(''))
const indices = [...Array(strs.length).keys()]

indices.sort((idx_a, idx_b) => sorted_words[idx_a] - sorted_words[idx_b])
const ans = []
let current_sorted_word = sorted_words[indices[0]]
let anagrams = [strs[indices[0]]]
for (let i = 1; i < indices.length; i++) {
const original_word = strs[indices[i]]
const sorted_word = sorted_words[indices[i]]
if (sorted_word === current_sorted_word) {
anagrams.push(original_word)
continue
}
ans.push(anagrams)
current_sorted_word = sorted_word
anagrams = [original_word]
}
return ans
};
```

* 誤りは2点で、
* stringの引き算をしてしまっており、NaNとなりソートが正しく機能しない。
* For文の中でansにanagramsを追加するのが、不一致が発生したタイミングなので、
最後のanagramsを追加ができていない。

* (正しい)

```javascript
const groupAnagrams = function(strs) {
const sorted_words = strs.map((word) => word.split('').sort().join(''))
const indices = [...Array(strs.length).keys()]

// UPDATED.
indices.sort((idx_a, idx_b) => sorted_words[idx_a].localeCompare(sorted_words[idx_b]))
const ans = []
let current_sorted_word = sorted_words[indices[0]]
let anagrams = [strs[indices[0]]]
for (let i = 1; i < indices.length; i++) {
const original_word = strs[indices[i]]
const sorted_word = sorted_words[indices[i]]
if (sorted_word === current_sorted_word) {
anagrams.push(original_word)
continue
}
ans.push(anagrams)
current_sorted_word = sorted_word
anagrams = [original_word]
}
// UPDATED.
ans.push(anagrams)
return ans
};
```

## 感想

* Mapとobjectを使った方法があるが、それぞれの特徴について理解ができていなかった。調べたところ、Mapの方が優れているらしい。
* 元々は、Objectがmapとして使われていた。
* Mapは、以下の3点でobjectよりも優れている。
* 1. 挿入した順序で、keyを操作(iterate)できること。
* 2. sizeは、objectの場合自前で実装しないといけないが、mapの場合size関数を使える。
* 3. Objectのkeyは、stringのdatatypeのみだが、mapのkeyは、Integerなどどのような型でも大丈夫なこと。
* 参考 : https://stackoverflow.com/a/18541990


* Javascriptで取り組んでいるため、C++の言語仕様の議論についていけていない。
* どこまで、C++の仕組みや議論について理解を進めるのが良いのだろうか?
* 参考: https://github.com/Ryotaro25/leetcode_first60/pull/13/files#r1636916877

### コメント集を読んで

* Javascriptでは配列のnegative indexがないが、pythonではnegative indexが有効なため、
不正なinputが入ってきた際に誤判定されることがある。

* 呼び出し側/関数側で、何を前提とエラーを検知しようとしないか、何のエラーを検知するかを意識する。

### 他の人のPRを読んで

* Pythonのdefaultdictのkeyについて、何を入力としてOKかを調べようとしたが、ドキュメントを探しても見つからず調べ方を教えて欲しいです。
Copy link

@chanseok-lim chanseok-lim May 6, 2025

Choose a reason for hiding this comment

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

defaultdictのkeyもdictのkeyと同じです。

default は組み込みの dict クラスのサブクラスです。これはメソッドをひとつオーバーライドし、書き込み可能なインスタンス変数をひとつ追加しています。それ以外の機能は dict クラスと同じです
https://docs.python.org/ja/3.13/library/collections.html#collections.defaultdict:~:text=defaultdict%20%E3%81%AF%E7...

辞書のキーには、 ほぼ どんな値も使うことができます。 キーとして使えないのは、 hashable (ハッシュ可能) でない値、すなわちリストや辞書のようなミュータブルな型 (内包する値ではなくオブジェクト自体が同一であるかによって比較が行われるような型)です。https://docs.python.org/ja/3/library/stdtypes.html#dict:~:text=%E8%BE%9E%E6%9B...

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます!

tupleが辞書のキーにできると言っていた箇所が原文を確認し、理解できました。

* 参考: https://github.com/olsen-blue/Arai60/pull/12/files#r1915836160

* olsen-blue
* PR: https://github.com/olsen-blue/Arai60/pull/12/
* Javascriptにはない、defaultdictを使うと、mapの初期化のコードが不要となる。

* Ryotaro25
* PR: https://github.com/Ryotaro25/leetcode_first60/pull/13/
* 赤黒木のコードの理解はSKIPし、Arai60をやり終えてから取り組むこととする。

* hayashi-ay
* PR: https://github.com/hayashi-ay/leetcode/pull/19/
* defaultdictのkeyに、tupleを使えることが知らなかった。

## その他の解法

### `*1` MapではなくObjectを用いる方法

```javascript
const groupAnagrams = function(strs) {
const sorted_to_anagrams = {}
for (const original_word of strs) {
const sorted_word = original_word.split('').sort().join()
if (sorted_to_anagrams[sorted_word] === undefined) {
sorted_to_anagrams[sorted_word] = [original_word]
continue
}
sorted_to_anagrams[sorted_word].push(original_word)
}

return Object.values(sorted_to_anagrams)
};
```
### `*2` AlgoExpertにあった回答
* 発想は、インデックスを保持した状態で、配列をAnagarmごとにソートする。
これだと、Mapが不要になる。
しかし、時間計算量は、他のN * W log W に比べて、N log N
* Javascriptで、rangeを作る方法はこちらのStackoverflowを参照した。
参考: https://stackoverflow.com/questions/3895478/does-javascript-have-a-method-like-range-to-generate-a-range-within-the-supp

```javascript
const groupAnagrams = function(strs) {
const sorted_words = strs.map((word) => word.split('').sort().join(''))
const indices = [...Array(strs.length).keys()]

indices.sort((idx_a, idx_b) => sorted_words[idx_a].localeCompare(sorted_words[idx_b]))
const ans = []
let current_sorted_word = sorted_words[indices[0]]
let anagrams = [strs[indices[0]]]
for (let i = 1; i < indices.length; i++) {
const original_word = strs[indices[i]]
const sorted_word = sorted_words[indices[i]]
if (sorted_word === current_sorted_word) {
anagrams.push(original_word)
continue
}
ans.push(anagrams)
current_sorted_word = sorted_word
anagrams = [original_word]
}
ans.push(anagrams)
return ans
};
```

### `*3` 文字数をカウントする方法もある

```javascript
const groupAnagrams = function(strs) {
const valid_inputs = 'abcdefghijklmnopqrstuvwxyz'
const key_to_anagrams = {}
for (const word of strs) {
const char_count = new Array(26).fill(0)
for (const ch of word) {
if (!valid_inputs.includes(ch)) {
throw new Error("invalid input")
}
const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0)
++char_count[idx]
}
const key = JSON.stringify(char_count)
if (key_to_anagrams[key] === undefined) {
key_to_anagrams[key] = [word]
continue
}
key_to_anagrams[key].push(word)
}
return Object.values(key_to_anagrams)
};
```

### `*4` 文字列のシリアライズ / エスケープシーケンス / 可変長数値表現で表現する。

* 文字列のシリアライズ

```javascript
const key = JSON.stringify(char_count)
```

* エスケープシーケンス

```javascript
const key = char_count.join("\t")
```

* 可変長数値表現 (Variable Length Quantity)

```javascript
const groupAnagrams = function(strs) {
const valid_inputs = 'abcdefghijklmnopqrstuvwxyz'
const key_to_anagrams = {}
for (const word of strs) {
const char_count = new Array(26).fill(0)
for (const ch of word) {
if (!valid_inputs.includes(ch)) {
throw new Error("invalid input")
}
const idx = ch.charCodeAt(0) - 'a'.charCodeAt(0)
++char_count[idx]
}
const key = arrayToVarLenQuantity(char_count)
console.log(key)
if (key_to_anagrams[key] === undefined) {
key_to_anagrams[key] = [word]
continue
}
key_to_anagrams[key].push(word)
}
return Object.values(key_to_anagrams)
};

const arrayToVarLenQuantity = function(array) {
for (var i = 0; i < array.length; i++) {
// 問題の入力で、strs[i].length <= 100のため。
if (array[i] > 256) {
throw new Error("invalid input")
}
if (array[i] < 16) {
array[i] = "0x0" + array[i].toString(16)
continue
}
array[i] = "0x" + array[i].toString(16)
}
return array.join('')
}
```