こんにちは
今回の学びは
・dictに数百万単位のデータ投入は数秒のコストと心得よ
・ポインターはどっちに動かせばいいかわかりやすい状況をつくろう。ソートとか。
問題は。。。
(時間配分は5分問題理解 10分検討 10分実装 20分振り返り の一文45分構成)
まずは入出力をしっかりおさえましょう。
実装前後でこれは毎回書きましょう。timespace analysis (時間とスペースの分析)は
pseudocode(擬似コード)をかいてみますか。
。。。
実際の実装はこちら
import sys
sys.setrecursionlimit(314159265)
from collections import defaultdict
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
memo = defaultdict(list)
sz = len(nums)
for i in range(sz):
for j in range(i+1, sz):
y = nums[i] + nums[j]
memo[-y].append((i, j))
ans = set()
for i in range(sz):
x = nums[i]
if x in memo:
for candidate in memo[x]:
if i not in candidate:
indice = list((nums[candidate[0]], nums[candidate[1]], x))
indice.sort()
ans.add(tuple(indice))
return list(ans)
O(N^2)を思いつく、時間もすくないので面接菅にこれで実装してもよいか聞いて から実装しはじめる 12分で実装して さらに7分かけてデバッグしました。 最終的にはTLEでエラーとなりあした。自分の分析ではO(N^2)でこの問題なら通るとおもったのですが https://leetcode.com/submissions/detail/270830314/ のNが3000のケースでTLEです。3000*3000は9百万なのでまだいけるはずなのですが、 これはtime complexity分析を見誤りましたか?! 以下サンプルケースをくづしていく様子です
samples = [
#([-1, 0, 1, 2, -1, -4], [[-1, 0, 1], [-1, -1, 2]]),
# 0830 failed 113/331 test case
# [-4, 0, 4]がはいってきてない。。。
# そうか。。合計が被っている時にオーバーライトしちゃってるんだ。
([-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6], []),
# 0834 TLE 311 / 313 test cases passed.
# https://leetcode.com/submissions/detail/270830314/
# O(N^2)では足りないといこがわかりました。
# ではここから10minかけて、さらにO(N)とかO(NlogN)とかもしくはO(N^2)-X にならないかみていきます。
# 0837 10 min timer start till 0846
]
ここで時間が40ふんほど経過しましたので 答えを見ることにしました。 少々おまちください https://www.techseries.dev/products/tech-interview-pro/categories/1408104/posts/4821258 ソートして、ソート特性をいかして探していく方法! O(N * (n/2)) と O(NlogN) なので 結局 O(N^2)だけで少し早い!?アルゴリズムとなる。 ソートするとポインターをどっちに動かせばいいかわかりやすい ポインターはどっちに動かせばいいかわかりやすい状況をつくろう。ソートとか。 space is O(1) so , answer would be time O(N^2) let's code it up こちら新しい実装です。 やることがわかってしまえば実装は簡単でした。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
sz = len(nums)
ans = set()
nums.sort()
for i in range(sz-2):
# having 2 pointer
l = i + 1
r = sz - 1
while l < r:
local = nums[i] + nums[l] + nums[r]
#print("%s , %s, %s => %s as sum" % (i, l, r, local))
if local == 0:
ans.add((nums[i], nums[l], nums[r]))
l += 1
elif local > 0:
# should decrease in next round, so moveing r toward left r-=1
r -= 1
else:
l += 1
return list(ans)
実装後です。timespace analysis (時間とスペースの分析)は
O(N^2)で spaceはO(1)です
実際、通るソリューションが実装できたので、前の私のO(N^2)と信じていた実装の実行速度を比較しましたら、以下のように前回の実装は5倍も遅い!結果となりました。この結果からO(N^2)ではないはずだ!との仮説がたちます。
# 新しいポインターでの実装 elapsed: 0.9121770858764648 # 前のセットでの実装 elapsed: 4.646041631698608
さてどこがO(N^2)から外れる原因の箇所でしょうか?
まずは、TLEの実装をすこしづつ削ってどこがボトルネックかみてみたら。。。。
`#memo[-y].append((i, j))`
をコメントしただけで、実行速度が0.4になりもうした!!
class Solution_hashmap_way_but_TLE_in_311_outof_313:
def threeSum(self, nums: List[int]) -> List[List[int]]:
memo = defaultdict(list)
sz = len(nums)
for i in range(sz):
for j in range(i+1, sz):
y = nums[i] + nums[j]
#memo[-y].append((i, j))
ans = set()
return list(ans)
むむ!?
defaultdictが遅いのか?だってappend自体は早いでしょう!?
ということで
memo = defaultdict(list)
sz = len(nums)
for i in range(sz):
for j in range(i+1, sz):
y = nums[i] + nums[j]
memo[-y] = (i, j)
とするとまた4秒かかったよ!遅いよ〜
appendは悪くないので、defaultdictをやめてみた。
するとさらに遅くなり5.5秒もかかった。つまりdefaultdictもわるくない。
class Solution_hashmap_way_but_TLE_in_311_outof_313:
@timeit
def threeSum(self, nums: List[int]) -> List[List[int]]:
memo = {}
sz = len(nums)
for i in range(sz):
for j in range(i+1, sz):
y = nums[i] + nums[j]
if -y in memo:
memo[-y].append((i, j))
else:
memo[-y]= [(i, j)]
つまり
dictであるmemoに大量にデータを投入すること自体が悪、
なのである、space(O^2)になっているので、dictの実装的にも数回mallocとかやってると思うし、そういう要因での遅延とみた。
なので、dictに数百万単位のデータ投入は数秒のコストと心得よ
を教訓とします
つまりは私のtime complexity分析は合っていました。しかしならが大量データを扱う場合は、space complexityがtime complexityに影響するという事実を突きつけられた結果となりました。
「次のこの問題を解くばあい知っていればよい一つの事実とは?」
ソートしてポインターを当てる、合計からどっちをづらせばいいかわかるやん。
まとめ
・dictに数百万単位のデータ投入は数秒のコストと心得よ
・ポインターはどっちに動かせばいいかわかりやすい状況をつくろう。ソートとか。
以上です