diff --git a/0242.Valid-Anagram/memo.md b/0242.Valid-Anagram/memo.md new file mode 100644 index 0000000..1e4181c --- /dev/null +++ b/0242.Valid-Anagram/memo.md @@ -0,0 +1,99 @@ +# 242. Valid Anagram + + +## step1 + +ソートして比較する方法をはじめに思いつく: `step1.py` + +時間計算量がO(nlog n)となってしまう。これを最初に実装したのは反省点。 + +## step2 + +https://github.com/naoto-iwase/leetcode/pull/65#discussion_r2569195545 + +https://github.com/huyfififi/coding-challenges/pull/7 + +https://github.com/ryosuketc/leetcode_grind75/pull/7 + +https://github.com/eito2002/LeetCode/pull/2 + +https://github.com/TaisukeFujise/leetcode_tafujise/pull/2 + + + +Counter の __eq__ +https://github.com/python/cpython/blob/main/Lib/collections/__init__.py#L797-L801 + +dictの == (C) + +https://github.com/python/cpython/blob/main/Objects/dictobject.c#L4586-L4594 + +いずれも要素が全て一致していたら等しいと判定される + + +Follow-up(Unicode): 文字種が増えるので 辞書/`Counter`で対応。 + +--- + +https://github.com/rihib/leetcode/pull/5 + +あまり理解できていないがLLMにまとめさせた + +## Unicode・サロゲート・結合・グラフェーム + +**242 の Follow-up やレビューに続く話題として**、「コードポイント/UTF-8/UTF-16 とサロゲート」「結合文字・シーケンス絵文字」「なぜ `len` や `rune` 分割だけでは足りないか」「グラフェームクラスタとは何か・国旗シーケンスの癖」「バイト数・コードポイント・ユーザー単位のどれを数えるか」などを **言語非依存で整理しつつ、Go と Python の違い**まで載せたメモ。 + +### Go と `rune` + +- Go の文字列は内部で **UTF-8**。**`rune` は Unicode コードポイントを表す型(`int32` の別名)**。概要は [Strings, bytes, runes and characters in Go](https://go.dev/blog/strings)。 +- とはいえ Unicode には **サロゲートペア・結合文字列・合字(複数コードポイントで一つの見かけ)** があるため、**`rune` に分割しただけでは「正しいユーザー単位の文字処理」とは限らない**(rihib PR の「か゛/が」の議論と同型)。 +- **英語圏でも絵文字**が普通に使われるので、**BMP 外コードポイント=事実上サロゲートが絡む UTF-16 世界**の話は日本語だけの問題ではない。 + +### Python と `str`(Python 3) + +- **`str` は「Unicode コードポイントの列」として扱われる**。ソースコードもデフォルトで **UTF-8**([PEP 3120](https://peps.python.org/pep-3120/))。全体像は公式の [Unicode HOWTO](https://docs.python.org/3/howto/unicode.html) がわかりやすい。 +- **`len(str)`・インデックス・`for c in s`** は原則として **1 コードポイント=1 要素**(Python 3 の `str` には **Java のような UTF-16 の「サロゲートペア」表現は表に出てこない**。補助平面の文字も **`len("😝") == 1`** のように数えられる)。 +- それでも **`len` が「人間の文字数」と一致しない**典型は **結合文字・シーケンス絵文字**(例: `len("か\u3099")` は 2、`☝🏻` や ZWJ 連結は複数コードポイントのまま)。 +- **`bytes` と `str` の区別**: ファイル・ネットワークはバイト列。**`str.encode("utf-8")` / `bytes.decode("utf-8")`** で相互変換。UTF-8 は **可変長バイト列**だが、一度 **`str` にしたあとはコードポイント列**として処理するイメージ。 +- **正規化**: 標準ライブラリ **`unicodedata.normalize("NFC", s)`**(および NFD など)で、結合順の違いをある程度揃えられる(Go の `unicode/norm` に相当する用途)。 +- **グラフェームクラスタ単位**: 標準ライブラリだけでは **ユーザー見かけの 1 文字**までまとめて切る API はない。**サードパーティ(例: PyPI の `grapheme`)や ICU 経由**が現実的(naoto PR の「NFC → graphemes → Counter」のパターン)。 +- **まとめ**: Python は **`str` がコードポイント単位で素直**なぶん、LeetCode の ASCII 制約外では **`Counter(str)` だけでは足りないケース**は Go の `rune` 分割と同型で起きる。**要件に応じて正規化+グラフェーム分割**を検討。 + +### Unicode とサロゲートの歴史 + +- 当初は **16 bit で全世界の文字を収める**構想だったが拡張要求で破綻し、**コード空間を U+10FFFF まで拡張**。既存の **16 bit 前提(Java `char`、旧 Windows API など)を壊さないため**、**高位・低位の 2 つの 16 bit 単位の組**で補助平面を表す **サロゲートペア**が定義された。 +- **コードポイント**= Unicode が文字に割り当てた番号(0x0000〜0x10FFFF)。それを **16 bit 単位で並べたのが UTF-16**、**8 bit で可変長なのが UTF-8**。 +- **UTF-16 では「ユニット数 ≠ コードポイント数」**(例: 😝 はコードポイント 1 だが UTF-16 では長さ 2)。**絵文字は BMP 外に多く**、UTF-16 / `char` 16 bit モデルではサロゲートを意識しないと破綻する。 + +### 結合・シーケンス絵文字・正規化 + +- **シーケンス絵文字**: 複数コードポイントを組み合わせて一つの絵文字にするタイプが増えている(肌色修飾・ZWJ 連結など)。 +- **結合そのものは日本語特有ではない**。例: 「が」を **U+304C 一個**で書くか、**「か」(U+304B) + 結合用濁点 (U+3099)** で書くか。**NFC / NFD** など正規化で表現が揃ったり、OS によってファイル名にどちらを優先するか等が違いトラブルになる、という話まで触れられる。 + +### コードポイント数は「文字数」になりにくい + +- ☝🏻 は複数コードポイント、長いキス絵文字はさらに多い、など。**コードポイント=人間の文字数**とみなすと簡単に裏切られる。 + +### グラフェームクラスタ(書記素クラスタに相当) + +- Unicode が「だいたいユーザーが思う 1 文字」に近い区切りとして用意したのが **グラフェームクラスタ**(日本語では書記素クラスタと説明されることが多い)。 +- **😝・☝🏻・複合キス絵文字などを「1 単位」として数えたい**なら、これが実用上わかりやすい。**エディタのカーソル位置**とも対応しやすい。 +- **計算は Unicode データに依存**するため、**自力実装は現実的でなく API(例: ICU)や言語/ライブラリのセグメンテーション機能を使う**のが普通。 +- **注意**: **Unicode/ICU のバージョン差**で、同じ文字列でも **クラスタ数が変わる**ことがある。 +- **リガチャ**: 「fi」結合などフォント側の字形。**`!=` を `≠` に見せるフォント**のように、**コードポイントだけでは「文字数」も字形も決まらない**。グラフェームでもリガチャまではカバーしない前提の議論になりやすい。 +- **結論に近い整理**: 要件ごとに指標を選ぶ。**ストレージならエンコード後バイト数**、**ポリシー制限ならコードポイントかグラフェームのどちらかを明示**、**画面幅なら実測**など。**銀の弾丸はない**。 + +### グラフェーム境界と「途中からの判定」 + +- 境界は通常、近傍の文字属性で決まるが、**国旗シーケンス(Regional Indicator の並び)は例外**。ランダムな位置が境界かどうかだけ見ても判定できず、**先頭から走査して RI を数える**必要がありうる、という話(最悪全体スキャン)。 + +### UTF-8(デコードのイメージ) + +- **可変長**。先頭バイトから **続きを何バイト読むか**がわかる。「が」の **コードポイント列を単純にバイト/コードポイント単位で反転**すると濁点位置が狂う、など → **グラフェーム単位でバラしてから反転**という話につながる。 + +### Go での参照 + +- 正規化: [`golang.org/x/text/unicode/norm`](https://pkg.go.dev/golang.org/x/text/unicode/norm)(NFC/NFD など。**グラフェーム分割そのものは別 API/パッケージが担当することが多い**ので、実装時は「正規化」と「ユーザー単位の分割」を分けて調べるとよい)。 + + + diff --git a/0242.Valid-Anagram/step1.py b/0242.Valid-Anagram/step1.py new file mode 100644 index 0000000..0411cf2 --- /dev/null +++ b/0242.Valid-Anagram/step1.py @@ -0,0 +1,3 @@ +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + return sorted(s) == sorted(t) diff --git a/0242.Valid-Anagram/step2.py b/0242.Valid-Anagram/step2.py new file mode 100644 index 0000000..2204a9a --- /dev/null +++ b/0242.Valid-Anagram/step2.py @@ -0,0 +1,53 @@ +# 現れる文字が英数字であることを利用したカウント +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + + count_s = [0] * 26 + count_t = [0] * 26 + base = ord("a") + for c in s: + count_s[ord(c) - base] += 1 + for c in t: + count_t[ord(c) - base] += 1 + return count_s == count_t + + +# 現れる文字が英数字であることを利用したカウントその2 +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + + cnt = [0] * 26 + base = ord("a") + for i in range(len(s)): + cnt[ord(s[i]) - base] += 1 + cnt[ord(t[i]) - base] -= 1 + return all(x == 0 for x in cnt) + + +# dictを利用したカウント +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + + char_to_delta: dict[str, int] = {} + for c in s: + char_to_delta[c] = char_to_delta.get(c, 0) + 1 + for c in t: + char_to_delta[c] = char_to_delta.get(c, 0) - 1 + if char_to_delta[c] < 0: + return False + return True + + +# Counterを利用 +import collections + + +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + return collections.Counter(s) == collections.Counter(t)