-
Notifications
You must be signed in to change notification settings - Fork 0
3 https://leetcode.com/problems/longest-substring-without-repeating-characters/description/ #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e48ba04
3d93ed9
f0a5fe3
94f63f6
44b904d
45dfc38
e0a0709
85b83a9
6dc2258
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # time complexity: O(n) nの項の最善は最悪の 1/2倍 | ||
| # space complexity: O(n) nの項の最善は最悪の 1/n倍 | ||
|
|
||
| class Solution: | ||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| longest_length = 0 | ||
| window_chars = set() | ||
| end = 0 # end finally becomes len(s). | ||
| for begin in range(len(s)): | ||
| while end < len(s) and s[end] not in window_chars: | ||
| window_chars.add(s[end]) | ||
| end += 1 | ||
| longest_length = max(end - begin, longest_length) | ||
| window_chars.remove(s[begin]) | ||
| return longest_length | ||
|
|
||
| # 変更点 | ||
| """ | ||
| 変数名: | ||
| window_chars : 「見たものすべて」を予想させるseen_charsから変更 | ||
| begin, end : left, rightから変更 | ||
| window_begin, window_endも検討したが、簡潔性を選んだ。 | ||
| start, stop も検討したが、substring_stop としないと落ち着かなかったので避けた。(自分の慣れの問題かもしれない) | ||
| end が、exclusiveかつ最終的にlen(s)になることをコメントに明示したが、C++では含意されているので読み手によっては冗長と感じるかも。 | ||
|
|
||
| 制御: | ||
| beginのループをforループに変更: | ||
| 変更前は `while begin < len(s): begin += 1` だった。自明な書き換え | ||
| 内側の end の while ループはインデックスの制限の条件に加えて追加の条件があり、rangeに書き換えると制御にフラグを導入することになり複雑になったのでwhileループのまま。 | ||
|
|
||
| コメント: | ||
| 最善と最悪の定数倍はビッグO記法を使わずに記述 | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| # 問題 | ||
| 題意:与えられた文字列にたいして、異なる文字だけからなる部分文字列の最大の長さはいくらか。 | ||
|
|
||
| # 全探索 step1 | ||
| すべての区間に対して文字の出現回数を調べ、同じ文字が出現しない区間の長さを最大長と比較し更新していく。 | ||
|
|
||
| 計算量: | ||
| - 時間 O(n^3) 二重ループの中で配列の走査を2回呼んでいる O(n^2 * (n + n))。TLE | ||
| - 空間 O(n^3) O(n)のdictをn^2個構築している | ||
| - 入力の文字列長をnとして、substringの文字数の総和は、1文字がn個、2文字がn-1個、3文字がn-2個、、、n文字が1個。長い文字列ほど少ないので O(n^3) は上限ではない | ||
|
|
||
| ```py | ||
| from collections import defaultdict | ||
|
|
||
| class Solution: | ||
| def count_chars(self, s:str): | ||
| char_count_map = defaultdict(int) | ||
| for char in s: | ||
| char_count_map[char] += 1 | ||
| return char_count_map | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ご存知かも知れませんが、一応貼っておきます。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ありがとうございます。 以下、ドキュメントと実装を読んで気づいたことのメモです。 Counter クラスは key, value の value として負の数を許している。
Counter.most_common() は内部で heaqp.nlargest() を呼んでいる。
|
||
|
|
||
| def nums_appear_once(self, nums: List[int]): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この関数使ってないです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. コピペのミスで判定関数が消失 & インデントが崩れていました。ご指摘ありがとうございます。PRを出す前に確認漏れがないようにします。 |
||
| for num in nums: | ||
| if num > 1: | ||
| return False | ||
| return True | ||
|
|
||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| max_substring_length = 0 | ||
| i = 0 | ||
| while i <= len(s) - 1: | ||
| j = i | ||
| while j <= len(s) - 1: | ||
| char_count_map = self.count_chars(s[i : j+1]) | ||
| current_substring_length = 0 | ||
| current_substring_length = j - i + 1 | ||
| max_substring_length = max(max_substring_length, current_substring_length) | ||
| j += 1 | ||
| i += 1 | ||
| return max_substring_length | ||
| # 考えるのに5分、全体の実装に8分、閉区間で書き直し&関数名で悩み、42分で動くものが完成 (大きい入力ではTLE) | ||
| ``` | ||
| コードに対する感覚: | ||
| - 制御:二重ループの中身がごちゃごちゃしている。辞書を作って、それをさらに走査するのは、やりたいことに対して大げさに感じる。 | ||
| - 命名:nums_appear_once()が変。booleanを返すので疑問形になっていてほしい。 | ||
| - 制御・初期値:閉区間で書くためにwhileを使ったせいでインデックスの更新が散らばってしまっている。 | ||
|
|
||
|
|
||
| # 全探索 step2 | ||
| ```py | ||
| class Solution: | ||
| def is_valid_substring(self, s: str): | ||
| char_count_map = defaultdict(int) | ||
| for char in s: | ||
| char_count_map[char] += 1 | ||
| if char_count_map[char] > 1: | ||
| return False | ||
| return True | ||
|
|
||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| max_substring_length = 0 | ||
| for i in range(len(s)): | ||
| for j in range(i, len(s)): | ||
| ## 説明変数 current_substring_lengthを使用 | ||
| current_substring_length = 0 | ||
| if self.is_valid_substring(s[i : j+1]): | ||
| current_substring_length = j - i + 1 | ||
| max_substring_length = max(max_substring_length, | ||
| current_substring_length) | ||
|
|
||
| ## 上記ブロックの異なる表現 | ||
| if self.is_valid_substring(s[i : j+1]): | ||
| max_substring_length = max(j - i + 1, max_substring_length) # `j - i + 1` is the length of s[i:j+1] | ||
| return max_substring_length | ||
| ``` | ||
| コードに対する感覚: | ||
| - 説明変数として current_substring_lengthを使っているが、max()の中で直接 `j-i-1`を使って、コメントで補うほうがスッキリする。 | ||
| - 印象として全探索step1より整理されているように感じる。 | ||
|
|
||
|
|
||
| # setを使う解法 step1 | ||
| 注目している部分文字列が含む文字をsetで記憶 | ||
|
|
||
| 計算量: | ||
| - 時間 O(n) left, rightともにsの末尾近くまで進むとき最悪でO(2n)、すべての文字列が異なるとき最善。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 定数倍は普通O記法の中に書かないかもしれません。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 定数倍を気にするシチュエーションでは、多項式中の最も強い項以外でも定数倍を気にする必要がありそうなので、O記法を避けるようにします。 |
||
| - 空間 O(n) すべての文字列が異なるとき最悪、すべての文字が同じ時最善。 | ||
|
|
||
| ```py | ||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| max_substring_length = 0 | ||
| seen_chars = set() | ||
| left = 0 | ||
| right = 0 | ||
| while left < len(s): | ||
| while right < len(s) and s[right] not in seen_chars: | ||
| seen_chars.add(s[right]) | ||
| right += 1 | ||
| max_substring_length = max(right - left, max_substring_length) | ||
| seen_chars.remove(s[left]) | ||
| left += 1 | ||
| return max_substring_length | ||
| ``` | ||
| アプローチ: | ||
| - 全探索ではスライスが更新されるごとに辞書を作り直していたが、right, leftのインクリメントにもとなって端の移動分だけ更新すると「現時点のsubstringに含まれている文字がなにか」の情報を使い回せる。 | ||
|
|
||
| コードに対する感覚: | ||
| - 参考にした回答にはfor文でrightをインクリメントする回答が多かった。leftに対するwhile文で一番外側の制御を書くのは自分以外には読みにくいかもしれない。 | ||
| - 2重ループっぽい見た目なので O(n^2)っぽい印象を与えるかもしれない | ||
| - 「重複要素を見つけた直後にleftを1だけインクリメントする」というのは、ぱっと見では、leftを進め足りないと思ってしまう。 | ||
| - 重複が見つかったときのループの動き:`while right < ~`のループに入らずに、seen_charsがs[right]と重複するs[left]を吐き出すまで、leftをインクリメントし続ける。 | ||
| - お尻を一歩引きつけるごとに頭を出しては引っ込める尺取り虫 | ||
|
|
||
| 変数名の改善: | ||
| - left, rightだと、`right - left`の順序で何度も間違える。 | ||
| - leftの代替として、substring_start_index, substring_head_index, first_char_indexなど。 | ||
|
|
||
| # setを使う解法 step2 | ||
|
|
||
| やったこと: | ||
| - 変数名の変更 `max_substring_leng` -> `max_length` | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| seen_chars = set() | ||
| max_length = 0 | ||
| left = 0 | ||
| right = 0 | ||
| while left < len(s): | ||
| while right < len(s) and s[right] not in seen_chars: | ||
| seen_chars.add(s[right]) | ||
| right += 1 | ||
| max_length = max(right - left, max_length) | ||
| seen_chars.remove(s[left]) | ||
| left += 1 | ||
| return max_length | ||
| ``` | ||
| コードに対する感覚: | ||
| - `max_length`ではなく、問題文の表現を使った `longest_substring_length` のほうがよさそう。 | ||
| - 抽象的には、既存の言葉づかいと一貫性した表現にすること。 | ||
| - max_lengthは略しすぎになる恐れがありそう。関数名を脳内に保持しておかなければならないので、関数が長くなったらsubstringを省略しないほうがよいと思われる。 | ||
| - 関数名を忘れた読み手には、スライスを使っている箇所から max_length は、substring の長さを言っていると予想させることになる。 | ||
| - 関数が長くなることに備えるならばsubstringをはじめから省略しないことを選択する | ||
| - `max_X = max(current_X, max_X)` では、`max()` の引数は、字面が長いほうを後ろに置くほうが見やすそう。 | ||
|
|
||
| # set step3 | ||
| ```py | ||
| class Solution: | ||
| def lengthOfLongestSubstring(self, s: str) -> int: | ||
| # 4'15 -> 5'25 -> 4'01 | ||
| longest_length = 0 | ||
| seen_chars = set() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sliding windowなので、window_charsなどと表現することもできます。seen_charsだと今まで見た全ての文字みたいな印象ですが、実際には[left, right)に含まれる文字ですよね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. window_chars は全く思いつきませんでした。とても明確でわかりやすいと感じたので、類似の問題では window_x を使えないかまず考えるようにします。 |
||
| base_index = 0 | ||
| right = 0 | ||
| while base_index < len(s): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. base_indexが+1ずつ動いているので、for loopの方がわかりやすいです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 自明な書き換えのはずだと思うのですが気づけませんでした。rangeで書き直してみます。 |
||
| while right < len(s) and s[right] not in seen_chars: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このrightがexclusiveなのは変数名から自明ではないですよね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whileからforに書き直せばこちらも明示できると思いました。インデックスが動く範囲を読りやすい変数名、制御構造にすることを意識します。 |
||
| seen_chars.add(s[right]) | ||
| right += 1 | ||
| longest_length = max(right - base_index, longest_length) | ||
| seen_chars.remove(s[base_index]) | ||
| base_index += 1 | ||
| return longest_length | ||
| ``` | ||
|
|
||
| コードに対する感覚: | ||
| - base_indexは良い名前ではないと感じる。substring_start_indexがしっくりくる。 | ||
|
|
||
| # 参考 | ||
| - https://github.com/thonda28/leetcode/pull/6/files#diff-6b92fcf16b9cd27d1d2d7ff3daeb4a7f48fb0a4fd64c5c68c2a58ae7af4a1837 | ||
| - dictの命名は `x_to_y` もよさそう | ||
| - inclusive, exclusiveを含意している変数名がある | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://en.cppreference.com/w/cpp/container/vector/end https://en.wikipedia.org/wiki/Upper_and_lower_bounds
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inclusiveと下限、exclusiveと上界がごっちゃになっていました。 |
||
| - 閉区間:start, end, first, last, begin, finish | ||
| - 開区間:lower_bound, upper_bound, limit | ||
| - forでrightを進めていく実装。重複したら、被った文字の次の文字にleftをセットする。 | ||
| - すでに出現した文字に対してそのインデックスを覚えておく。 | ||
| - `longest_lengt = max(right, char_to_index[right] + 1)` で max() はleftを逆戻りさせないために必要。"abba"のように重複が挟まれていると、間の重複を飛び越してしまう。 | ||
| - こちらも `for right in s:` で文字を走査し、重複のたびにleftを更新する実装 https://github.com/shining-ai/leetcode/pull/48/files#diff-70cb8db03b4879ea63d98354edceeb583ded7f2ca60a22c5d6ad2d53fa853ef5 | ||
|
|
||
| # 調べ物 | ||
| 辞書関連: | ||
| - valueの初期値を設定する方法 | ||
| 1. 書く. `if x not in my_dict: my_dict[x] = default_value` で初期化する | ||
| 2. `my_dict[key] = setdefault(value, default_value)` を使う。 https://docs.python.org/3/library/stdtypes.html#dict.setdefault | ||
| 3. collectonsモジュールの`defaultdict(<型>)`を使う https://docs.python.org/3/library/collections.html#defaultdict-objects | ||
| - このときに指定する型を `default_factory` と呼ぶ。setdefaultより早いらしい。 | ||
| - 実装 | ||
| - dictの実装。8000行あって怯む。https://github.com/python/cpython/blob/main/Objects/dictobject.c | ||
| - 概略の解説 https://stackoverflow.com/questions/327311/how-are-pythons-built-in-dictionaries-implemented/44509302#44509302 | ||
| - 3.6以前と以後で大きく変わっている。 | ||
| - 3.7から仕様として挿入順を保つようになった | ||
| - ハッシュの衝突はオープンアドレス法で処理される https://en.wikipedia.org/wiki/Hash_table#Open_addressing | ||
| - 要素の数が増えるとスロット数が拡張される | ||
| - 衝突を防ぐために要素数より大きい領域が確保されている。 | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
あ、これは O(n^3) ですね。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
一般項 a_n = k*n - (k-1)*k の数列のk=1からnまでの和で、一番強い項が n^3ですね。雰囲気で見積もっていました。
辞書のメモリ使用に関連して、hashの衝突を避けるために要素数に応じて余分に領域を確保している、も聞かれることがあるのかなと思いました。