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
212 changes: 212 additions & 0 deletions 387_first-unique-character-in-a-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# 387. First Unique Character in a String

## 1st

### ①

素直に文字ごとの出現頻度を辞書にして、2パス目で左から1回しか出ていない文字を確認するようにした。

defaultdictの代わりにCounter()を使っても良い。

所要時間: 4:00

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

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_frequency = defaultdict(int)
for c in s:
char_to_frequency[c] += 1
for i, c in enumerate(s):
if char_to_frequency[c] == 1:
return i
return -1
```

### ②

Counter版

所要時間: 0:41

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

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_frequency = Counter(s)
for i, c in enumerate(s):
if char_to_frequency[c] == 1:
return i
return -1
```

### ③

辞書を作らないのであれば、左右から検索して最初に見つかった位置が一致していればunique, という探し方もできる。

所要時間: 1:33

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

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
for c in s:
left_index = s.find(c)
if left_index == s.rfind(c):
return left_index
return -1
```

## 2nd

### 参考

- https://discord.com/channels/1084280443945353267/1198621745565937764/1244101017507991613
- https://discord.com/channels/1084280443945353267/1227073733844406343/1233831347051565137

議論にあったdefaultdictを書いてみる

継承

```py
class DefaultDict(dict):
def __init__(self, default_factory=None):
self.default_factory = default_factory

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

質問ぽい感じになるのですが、継承する時ってsuper().init()を書かなくて良いのでしょうか。
そこまで神経質にならなくてもいいのでしょうか。

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.

あまり自信ないんですが、考えてみると書いた方がいい気がしました。下のような動きになるので。

$ ipython
Python 3.11.3 (main, Jun  5 2023, 00:49:13) [Clang 14.0.3 (clang-1403.0.22.14.1)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: class DefaultDict(dict):
   ...:   def __init__(self, default_factory=None):
   ...:     self.default_factory = default_factory
   ...:
   ...:   def __missing__(self, key):
   ...:     if self.default_factory is None:
   ...:       raise KeyError
   ...:     self[key] = self.default_factory()
   ...:     return self[key]
   ...:

In [2]: d = DefaultDict(list, a=10)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[2], line 1
----> 1 d = DefaultDict(list, a=10)

TypeError: DefaultDict.__init__() got an unexpected keyword argument 'a'

In [3]: from collections import defaultdict

In [4]: d2 = defaultdict(list, a=10) # 例外は起こらない

こんな感じに書けばいいかもですね。↓

In [5]: class DefaultDict(dict):
   ...:   def __init__(self, default_factory=None, *args, **kwargs):
   ...:     super().__init__(*args, **kwargs)
   ...:     self.default_factory = default_factory
   ...:
   ...:   def __missing__(self, key):
   ...:     if self.default_factory is None:
   ...:       raise KeyError
   ...:     self[key] = self.default_factory()
   ...:     return self[key]
   ...:

In [6]: d3 = DefaultDict(list, a=10) # 例外は起こらない

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

なるほど、おそらくdictクラスの方でなんかしらの初期化変数が実はあるんですね。
書き方も参考になりました。

def __missing__(self, key):
if self.default_factory is None:
raise KeyError
self[key] = self.default_factory()
return self[key]
```

委譲。こんな感じか? `del d['foo']` とかはできるようにしていない

```py
class DefaultDict:
def __init__(self, default_factory=None):
self.d = {}
self.default_factory = default_factory

def __missing__(self, key):
if self.default_factory is None:
raise KeyError
self.d[key] = self.default_factory()
return self.d[key]

def __getitem__(self, key):
if not key in self:
return self.__missing__(key)
return self.d[key]

def __setitem__(self, key, value):
self.d[key] = value

def __contains__(self, key):
return key in self.d
```

- https://discord.com/channels/1084280443945353267/1200089668901937312/1209879956436291645
- https://github.com/hayashi-ay/leetcode/pull/28/files

1-passの解法が面白かった。自分も参考に書いてみる。

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
unique_occurrence_indexes = {} # 2回以上出現した場合はlen(s)が入る
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OrderedDictというのもあるので見ておくといいと思います。
https://docs.python.org/ja/3/library/collections.html#collections.OrderedDict

OrderedDictとsetを組み合わせて解けます。

for i, c in enumerate(s):
if c in unique_occurrence_indexes:
unique_occurrence_indexes[c] = len(s)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2回以上出現した際の値のマーカーとしてはlen(s)以上の値なら何でもよいのかと思いますが、特定の数値だとその数値に何か意味があるのかなと思ってしまいそうだなと感じました。min(a,b)の単位元がinfなので個人的にはinfにするのがわかりやすいと思いました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

変数を grue や bleen のようにしない
https://discord.com/channels/1084280443945353267/1192736784354918470/1194530857902419978

first_zero を自然言語で表現するとなんですかね。
「はじめの0が出てくるまでは、i か i + 1 を指していて、0が出てくると、そこで止まり、swap する関係上、そこから先は常にはじめの0を指している。」
https://discord.com/channels/1084280443945353267/1192736784354918470/1199760331824713758

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

これ、この変数を一言で説明するとなんですかね。
unique_occurrence_indexes は、「文字をキーとし、バリューは、はじめに出てきたインデックスであるが、2回以上出現した場合には len(s) が入っている dict」ですよね。

それだったら「文字とそのはじめの出現位置の dict」「2回以上出現した文字の set」の二変数を持ったほうが素直ですよね。

自然言語で説明すると複雑な変数は分けましょう。私は、grue や bleen みたいなものだと思っています。
https://en.wikipedia.org/wiki/New_riddle_of_induction

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.

2回以上出現した際の値のマーカーとしてはlen(s)以上の値なら何でもよいのかと思いますが、特定の数値だとその数値に何か意味があるのかなと思ってしまいそうだなと感じました。min(a,b)の単位元がinfなので個人的にはinfにするのがわかりやすいと思いました。

indexを入れる辞書にfloatが入る気持ち悪さを優先してlen(s)を選択した感じでした。単位元がinfだから自然というのはなるほどと思いました

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

問題に対してオーバーキルではありますが、意味が分かりやすい変数だと、unordered_map<char, vector<int>> char_to_indicesunordered_map<char, int> first_indexunordered_map<char, int> char_countなどといった選択肢がありそうですね。

continue
unique_occurrence_indexes[c] = i
candidate = min(iter(unique_occurrence_indexes.values()))
if candidate == len(s):
return -1
return candidate
```

## 3rd

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_frequency = defaultdict(int)
for c in s:
char_to_frequency[c] += 1
for i, c in enumerate(s):
if char_to_frequency[c] == 1:
return i
return -1
```

## 4th

微妙な感じ。

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_first_occurence_index = {}
seen = set()
for i, c in enumerate(s):
if c in seen:
if c in char_to_first_occurence_index:
del char_to_first_occurence_index[c]
continue
char_to_first_occurence_index[c] = i
seen.add(c)
if len(char_to_first_occurence_index) == 0:
return -1
return min(char_to_first_occurence_index.values())
```

[こちら](https://github.com/hayashi-ay/leetcode/pull/28/files#diff-5ec7c3c87171edf4d61e9eb79fd926cafa27caf068da7474222897c8e9e7ab96R92)を参考に、書き直し。

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_first_occurence_index = {}
duplicated_chars = set()
for i, c in enumerate(s):
if c in duplicated_chars:
continue
if c in char_to_first_occurence_index:
del char_to_first_occurence_index[c]
duplicated_chars.add(c)
continue
char_to_first_occurence_index[c] = i
if not char_to_first_occurence_index:
return -1
return min(char_to_first_occurence_index.values())
```


上で参考にしたコードはdictが挿入順に順序付けられることを利用しているが、あえてそれを保証しない3.7以前のPythonを気にするならこう書くだろうか (気にする場面は少なくなっているだろうが)。

```py
class Solution:
def firstUniqChar(self, s: str) -> int:
char_to_first_occurence_index = OrderedDict()
duplicated_chars = set()
for i, c in enumerate(s):
if c in duplicated_chars:
continue
if c in char_to_first_occurence_index:
del char_to_first_occurence_index[c]
duplicated_chars.add(c)
continue
char_to_first_occurence_index[c] = i
if not char_to_first_occurence_index:
return -1
return char_to_first_occurence_index.popitem(last=False)[1]
```