Skip to content

Two Sum #12

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
146 changes: 146 additions & 0 deletions twoSum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Two Sum

## Step1

### 考えたこと

全部の数字の和を要素として持っといてそれと一致するものを出力すれば良いのでは
j は i < j <= len(nums) にしないと同じ和を2回計算することになる
i は len(nums) - 1 にしないと i = len(nums)で j が定義できなくなる

```python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
sum_to_indices = {}

for i in range(len(nums) - 1):
for j in range(i + 1,len(nums)):
sum_nums = nums[i] + nums[j]
sum_to_indices[sum_nums] = [i,j]

return sum_to_indices[target]
```

## Step2

### Hash Mapとは

ハッシュ関数を用いてキーを一意に管理し、そのキーに対応するvalueを保持する。pythonではdictがそれ
- 方法
- キーからハッシュ値を計算する
- ハッシュ値を元にバケット(Hash Map内の配列)のどこに格納するかを決める
- 値を格納・取得する
- 格納:バケットにキーとバリューのペアを保存
- 取得:キーからハッシュ値を計算し、対応するバケットからバリューを取り出す
- 特徴
- 速い:検索・追加・削除がO(1)
- 衝突
- 違うキーで同じハッシュ値が計算されてしまうこと
- 対処法
- チェイニング:同じハッシュ値の要素をまとめて保存する(連結リストなどを使用)
- オープンアドレッシング:空いてるバケットを探して再確認する
- pythonのdictはこれを用いている(二次探査)
- リハッシュ:バケットが要素で満たされてくると自動的にバケットのサイズを増やして再配置を行う
- 衝突を軽減する
- リハッシュの際は一時的にO(n)かかる

### 参考にしたもの

https://github.com/katsukii/leetcode/pull/2/files/7023788fe5ed525606712b0c5f4fd025d5f03f21#diff-e834754f59f018414ec6917d9f04e6be9d730c536df3f52d9181d7aa3539b651

java読んだことなくて難しい。
書かれているものをpythonで書くと以下のようになる
```python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
num_to_index = {}
for i in range(len(nums)):
complement = target - nums[i]
if complement in num_to_index:
return [num_to_index[complement], i]
num_to_index[nums[i]] = i

raise ValueError("No two sum pair found.")
```

targetから引いた余りがnum_to_indexの中にないか探す
numsの中から探せばいいじゃんと思ったが、numsは配列なのでもう一周しないと見つからない
- 計算量
- 時間計算量
- O(n)
- 空間計算量
- 最悪O(n)

https://github.com/Hurukawa2121/leetcode/pull/11

C++も読んだことない
解法は同じ
赤黒木 std::mapというものも使えるらしい
Copy link

Choose a reason for hiding this comment

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

C++ の企画上は、 std::map を赤黒木で実装しなければならないとは書いておらず、計算量のみが規定されています。ただし、赤黒木で実装されることが多いようです。

https://timsong-cpp.github.io/cppwp/n4950/map.access

Complexity: Logarithmic.

https://en.cppreference.com/w/cpp/container/map

Search, removal, and insertion operations have logarithmic complexity. Maps are usually implemented as Red–black trees.


### 赤黒木

自己平衡二分探索木の一種
- 性質
- 各ノードが赤もしくは黒である
- ルートノードは常に黒
- 全ての葉ノードは黒
- 赤ノードの子は黒
- 任意のノードからその子孫の葉ノードに至るまでの黒ノードの数は同じ
- 利点
- 挿入、削除、検索がO(logn)

https://github.com/t0hsumi/leetcode/pull/11

入力をソートして両端から調べていく方法もある
left、rightが行きすぎて戻ることがあるからHashmapのものより少しだけ遅そうに見える
Copy link

Choose a reason for hiding this comment

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

いろんなパターンの実装をされていて素晴らしいと思いました。

left、rightが行きすぎて戻ることがある

僕の理解のために教えて欲しいのですが、「行きすぎる」ってどういうパターンでしょうか。
コードを見ると、whileループの中で、

  • leftは0から増えるだけ
  • 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.

ありがとうございます。よく考えたら行き過ぎることはなかったです。

[a, b, c, d, e]とかで target = b + d の時 a + e < target < c + e だと c まで行きすぎてから戻るのかなとなんとなく思ってたのですが、 b + e > b + d が必ず成立するのでそんなことにはならなかったです。

- 計算量 n = len(nums)
- 時間計算量
- ソート O(nlogn)
- 左右からの探索 O(n)
- 空間計算量
- num_index_pairs O(n)

```python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
num_index_pairs = []

Choose a reason for hiding this comment

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

パフォーマンスの違いはほとんどないと思いますが、sorted 関数の key 引数を利用すれば、リストを新たに生成せずとも処理が可能です

num_index_pairs = sorted(enumerate(nums), key=lambda x: x[1])

for i, num in enumerate(nums):
num_index_pairs.append((num, i))
num_index_pairs.sort()

left = 0
right = len(nums) - 1
while left < right:
two_sum = num_index_pairs[left][0] + num_index_pairs[right][0]
if two_sum == target:
return [num_index_pairs[left][1], num_index_pairs[right][1]]

if two_sum > target:
right -= 1
else:
left += 1

raise RuntimeError(
f"No pair found in twoSum(): nums = {nums}, target = {target}"
)
```

for i in range(len(nums))と for i, _ in enumerate(nums) はどっちが早いんだろう
https://stackoverflow.com/questions/11990105/rangelenlist-or-enumeratelist
rangeの方がちょい早いらしい?enumerateはtupleを出力してるからか。
rangeの方が絶対にわかりやすくはある。
Copy link

Choose a reason for hiding this comment

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

Python がそもそもあんまり速くないので、このあたりの速度はあまり気にしなくていいでしょう。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほど。
細かい関数の差異は気にせず、計算量だけ気にしていれば十分ということでしょうか。
ありがとうございます。

Copy link

Choose a reason for hiding this comment

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

より正確に書くと、計算量を求め、データサイズを代入し、おおよその計算ステップ数を求め、言語ごとの処理速度を考慮し、許容される実行時間内に実行可能な計算ステップ数以内に十分収まっていれば、ひとまず大丈夫だと思います。速度を求めるなら、言語を変えることも一つの手だと思います。


## Step3

ハッシュマップで書いた

```python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
num_to_index = {}
for i in range(len(nums)):
complement = target - nums[i]
if complement in num_to_index:
return [num_to_index[complement], i]
num_to_index[nums[i]] = i
```