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
165 changes: 165 additions & 0 deletions 3_longest-substring-without-repeating-characters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# 3. Longest Substring Without Repeating Characters

## 1st

注目している文字列に含まれる文字の集合を管理し、その集合に次の文字をいれるときに重複が出るなら重複が出なくなるまで左側を進めて集合から削除をしていく。

文字->indexの辞書じゃなくてsetで良かった...

left, rightだと閉区間っぽい気がして気になるが、左側を開区間にしたいときに伝わりやすい変数名はあるだろうか?もちろん閉区間にしてright - left + 1と区間の長さを計算してもいいのだが。

`left = -1 # exclusive` みたいにとりあえずコメントを書くか。

所要時間: 8:16

n: len(s)
- 時間計算量: O(n)
- 空間計算量: O(n)

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
char_to_index = {}
max_length = 0
left = -1
Copy link

Choose a reason for hiding this comment

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

個人的には 2nd のように left = 0 から始める方が好きですが、 left = -1 から始めても良いと思います。

for right in range(len(s)):
if s[right] in char_to_index:
while s[right] in char_to_index:
left += 1
del char_to_index[s[left]]
max_length = max(max_length, right - left)
char_to_index[s[right]] = right
return max_length
```

setで書き直した。

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
chars = set()
left = -1
max_length = 0
for right in range(len(s)):
while s[right] in chars:
left += 1
chars.remove(s[left])
max_length = max(max_length, right - left)
chars.add(s[right])
return max_length
```

## 2nd

### 参考

- https://discord.com/channels/1084280443945353267/1196472827457589338/1246368719148548117
- https://github.com/Mike0121/LeetCode/pull/21

setってremove(), clear()だけじゃなく、discard()やpop()もあるのか。[Built-in Types — Python 3.12.4 documentation](https://docs.python.org/3.12/library/stdtypes.html#set)
Copy link

Choose a reason for hiding this comment

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

ドキュメント読むの大事ですね。

discard(elem)はelemが無くてもエラーにならない、pop()は要素があるならelemを返す。

辞書にすると、管理にかかる計算コストが少し少なくなるのか。setにする方が素直だとは思うが。
Copy link

@thonda28 thonda28 Jul 20, 2024

Choose a reason for hiding this comment

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

個人的には dict の方が while 文が不要で理解しやすく感じました。left をずらしながら set から要素を削除する処理が不要な分、(書かれている通り)計算コストも少なそうです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど...setの解法の場合、setの中身が注目している部分文字列の文字集合だという点が分かりやすいので好みなのですが、人それぞれなのかもですね

Choose a reason for hiding this comment

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

setの解法の場合、setの中身が注目している部分文字列の文字集合だという点が分かりやすいので好みなのですが、人それぞれなのかもですね

そうですね、人それぞれなのかもです。個人的には、各文字の index 情報があるのに利用しないのがもったいないという感覚がありました。


```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
left = 0
char_to_index = {}
for right in range(len(s)):

Choose a reason for hiding this comment

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

合計4回 s[right] を使っているのが冗長かつ Typo のようなミスが起きやすいのでは、と感じました。Python の場合は enumerate を使うと s[right] と繰り返し書く必要がなく、可読性が高まりそうです。また enumerate を使わない場合も、わかりやすい命名の一時変数で s[right] を扱ってもよさそうです。

Suggested change
for right in range(len(s)):
for right, char in enumerate(s):

Copy link
Owner Author

Choose a reason for hiding this comment

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

enumerateや一次変数に入れるより、s[right]の方が文字列のright番目を取り出す意図がはっきりして分かりやすいかなと思って書いたんですが、うーむ好みの違いなのかもですね。
typoの起きやすさや冗長さ、 s[i] のように書くと微妙になりそうですかね?そこの感覚はあまり無かったです。

Choose a reason for hiding this comment

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

たしかに元のコードのほうが可読性が高いのかもしれません。(僕もこの問題を解いたことがあったので)読み手というより書き手側の視点で見てしまっていたかもです。とくにもっと処理が大きくなったときには char のようなものよりは s[right] の方が読みやすそう(理解しやすそう)ですね。

コードを書く側の目線で見たときに enumerate が楽だなと思ってのコメントでしたが、重要なのは読み手の視点なので上のコメントは適切でなかったかもです。

if s[right] in char_to_index and char_to_index[s[right]] >= left:
left = char_to_index[s[right]] + 1
max_length = max(max_length, right - left + 1)
char_to_index[s[right]] = right
return max_length
```

- https://discord.com/channels/1084280443945353267/1235971495696662578/1236898802703663208
- https://github.com/sakzk/leetcode/pull/3

左端をloopで回す方法。

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
chars = set()
end = 0
for begin in range(len(s)):
while end < len(s) and s[end] not in chars:
chars.add(s[end])
end += 1
max_length = max(max_length, end - begin)
if end == len(s):
break
chars.remove(s[begin])
return max_length

Choose a reason for hiding this comment

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

ここはbegin, endに変更した理由が特になければ、step1, 2のようにleft, rightの方が読みやすく感じました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ループ不変条件が [begin, end) の区間に注目してるので半開区間であることを主張したく、begin-endを使ってみました。
right使ったときは右側はinclusiveだよということを主張したつもりでした

```

全探索するならこうするだろうか。

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
for begin in range(len(s)):
for end in range(begin + 1, len(s) + 1):
if len(set(s[begin:end])) == end - begin:
max_length = max(max_length, end - begin)
return max_length
```

endの範囲がlen(s) + 1までにしなくてはいけないことに気づかず1ミス。
TLEした。

毎回setを作るのがさすがに無駄かと思い、左端ごとにsetを作るようにする。こちらは通った。

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
for begin in range(len(s)):
chars = set()
end = begin
while end < len(s) and s[end] not in chars:
chars.add(s[end])
end += 1
max_length = max(max_length, end - begin)
return max_length
```

- https://discord.com/channels/1084280443945353267/1233295449985650688/1233598424780116039
- https://github.com/Exzrgs/LeetCode/pull/2
- https://discord.com/channels/1084280443945353267/1230079550923341835/1233450465560363070
- https://github.com/thonda28/leetcode/pull/6
- https://discord.com/channels/1084280443945353267/1226536545020809226/1228387150291144775
- https://github.com/t0d4/leetcode/pull/2
- https://discord.com/channels/1084280443945353267/1225849404037009609/1229038363613331498
- https://github.com/SuperHotDogCat/coding-interview/pull/3
- https://discord.com/channels/1084280443945353267/1226508154833993788/1228744387937177650
- https://github.com/nittoco/leetcode/pull/3
- https://discord.com/channels/1084280443945353267/1201211204547383386/1228003299651879044
- https://github.com/shining-ai/leetcode/pull/48
- https://discord.com/channels/1084280443945353267/1217527351890546789/1218817308328464405
- https://github.com/cheeseNA/leetcode/pull/3
- https://discord.com/channels/1084280443945353267/1200089668901937312/1215986234812403752
- https://github.com/hayashi-ay/leetcode/pull/47


## 3rd

```py
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
chars = set()
left = -1 # exclusive
for right in range(len(s)):
while s[right] in chars:
left += 1
chars.remove(s[left])
max_length = max(max_length, right - left)
chars.add(s[right])
return max_length
```