# 20. Valid Parentheses
# Easy
# Topics
# Companies
# Hint
# Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
# An input string is valid if:
# Open brackets must be closed by the same type of brackets.
# Open brackets must be closed in the correct order.
# Every close bracket has a corresponding open bracket of the same type.誤答1回目:ユースケース漏れ ・入力文字列とそこから想定される正しい文字列を比較して、一致していれば入力値が適切、不一致ならば入力値が不適切とする。 ・先に開き括弧が来て、後に閉じ括弧が来る。→閉じ括弧が先に来るときは、false ・開き括弧の文字列とセットの閉じ括弧が来る。 →開き括弧に対する閉じ括弧のハッシュマップを作成して、開き括弧が入力されていることをチェックした後に次の文字列があるときは、対応する閉じ括弧を入力するようにする。
class Solution:
def isValid(self, s: str) -> bool:
# ({)} is true?
# If you don’t care about the order in which the brackets are closed, as in the example above, you can simply run a loop.
hashmap = {"(":")","{":"}","[":"]"}
openS = []
closeS = []
for i in range(len(s)) :
if s[i] == "(" or s[i] == "{" or s[i] == "[":
openS.append(s[i])
closeS.append(hashmap[s[i]])
else:
# 組み合わせとして先に閉じ括弧が来るときはfalse
if closeS == []: return false
openS.append(closeS.pop())
return s == ''.join(openS)Input
s ="["
Use Testcase
Output
true
Expected
false考慮漏れ ・開き括弧だけが入力文字列として存在する場合でもTrueになる。
誤答2回目:ユースケース漏れ 追加仕様 ・開き括弧だけ単独で存在していて次の文字列がない場合、適切な文字列扱いとなるので、模範文字列が奇数の場合はfalseとする。
class Solution:
def isValid(self, s: str) -> bool:
# ({)} is true?
# If you don’t care about the order in which the brackets are closed, as in the example above, you can simply run a loop.
hashmap = {"(":")","{":"}","[":"]"}
openS = []
closeS = []
for i in range(len(s)) :
if s[i] == "(" or s[i] == "{" or s[i] == "[":
openS.append(s[i])
closeS.append(hashmap[s[i]])
else:
# 組み合わせとして先に閉じ括弧が来るときはfalse
if closeS == []: return False
openS.append(closeS.pop())
if len(openS) % 2 == 1 :
return False
return s == ''.join(openS)Input
s ="(("
Use Testcase
Output
true
Expected
false考慮漏れ:偶数文字数であっても開き括弧だけが連続して単独に存在するケースがある。
正答1回目: ・奇数文字列かつ開き括弧に対応する閉じ括弧を文字列に使っていない場合falseとすることで、 開き括弧だけの入力文字列を文字数にかかわらずfalseにできる。
class Solution:
def isValid(self, s: str) -> bool:
# ({)} is true?
# If you don’t care about the order in which the brackets are closed, as in the example above, you can simply run a loop.
hashmap = {"(":")","{":"}","[":"]"}
openS = []
closeS = []
for i in range(len(s)) :
if s[i] == "(" or s[i] == "{" or s[i] == "[":
openS.append(s[i])
closeS.append(hashmap[s[i]])
else:
# 組み合わせとして先に閉じ括弧が来るときはfalse
if closeS == []: return False
openS.append(closeS.pop())
if len(openS) % 2 == 1 or len(closeS) != 0 :
return False
return s == ''.join(openS)コードを見やすくする ・変数名が指示する内容を表すように修正
class Solution:
def isValid(self, s: str) -> bool:
# ({)} is true?
# If you don’t care about the order in which the brackets are closed, as in the example above, you can simply run a loop.
brackets = {"(":")","{":"}","[":"]"}
ideal_strings = []
corresponding_close_brackets = []
for bracket in range(len(s)) :
if s[bracket] == "(" or s[bracket] == "{" or s[bracket] == "[":
ideal_strings.append(s[bracket]) # 開き括弧を記録する
corresponding_close_brackets.append(brackets[s[bracket]]) # 閉じ括弧
else:
# 組み合わせとして先に閉じ括弧が来るときはfalse
if corresponding_close_brackets == []: return False
ideal_strings.append(corresponding_close_brackets.pop()) # 閉じ括弧に来るべき括弧の種類を考える
if len(ideal_strings) % 2 == 1 or len(corresponding_close_brackets) != 0 :
return False
return s == ''.join(ideal_strings)fuga-98さんレビュー ・corresponding_close_brackets→open_to_closeなどとキーとバリューのわかる名前のほうがよさそう。
・括弧の対応において全体の式が有効ならば、部分的に取り出した括弧の式も有効 →開き括弧に同タイプの閉じ括弧が対応していない場合はFalseというのは、自前の回答と同じ考え ・スタックをうまく使う。自前の回答はリストを2つ使っている。その分計算量が大きくなる。
・模範回答が0msで自前の回答が1msで模範回答の方が速かった。(この計測は偶然の可能性あり・・・)
原因は主に2つ。
・時間計算量:処理の最後に理想的な文字列と実際の文字列を比較している
・空間計算量:リストを2つ使用および1つのリストのメモリ占有がそもそも大きいと
詳細は以下。
Fumintonさんレビュー ・実行時間はサーバーの空き状況で大小変わるらしくそこまで気にしなくて良いのでは? ・自前の環境で計測する方法
両方のプログラムを分析し、時間・空間計算量を比較してみましょう。
時間計算量
模範回答(0ms):
文字列sを1回だけ走査(O(n))
スタック操作(push/pop)は定数時間(O(1))
マッピング検索も定数時間(O(1))
全体:O(n)
自前の回答(1ms):
文字列sを1回だけ走査(O(n))
ideal_stringsとcorresponding_close_bracketsへの追加/削除操作(O(1))
最後にs == ''.join(openS)で文字列結合と比較(O(n))
全体:O(n)
時間計算量的には両方ともO(n)ですが、自前の回答の最後に''.join(openS)という操作があり、これは追加のO(n)時間を要します。
空間計算量
模範回答:
スタック:最悪の場合O(n)(全ての文字が開き括弧の場合)
マッピング辞書:定数サイズO(1)
全体:O(n)
自前の回答:
ideal_strings:最悪の場合O(n)
corresponding_close_brackets:最悪の場合O(n)
全体:O(n)
ただし、自前の回答は2つのリストを使用するため、実質的に2倍のメモリを使用します。
速度の差の理由
余分な操作: 自前の回答は''.join(openS)で文字列結合を行い、元の文字列と比較しています。これは不要なO(n)の操作です。
2つのリストの使用: 自前の回答はideal_stringsとcorresponding_close_bracketsという2つのリストを管理しています。これに対し、模範回答はスタック1つだけです。
アルゴリズムの効率: 模範回答は必要最小限の操作だけを行い、閉じ括弧が来たときにすぐに検証します。自前の回答は閉じ括弧が来たときに対応する開き括弧をideal_stringsに追加し、最後に全体を検証します。
コードの問題点: 自前の回答にはopenSという変数が参照されていますが、定義されていません。おそらくideal_stringsの間違いです。また、({)}のようなケースをTrue扱いするコメントがありますが、実際のコードの結果はFalseになります。
まとめ
模範回答の方が速いのは主に以下の理由によります:
より少ないメモリ使用量(スタック1つのみ)
余分な文字列操作の欠如(join不要)
より効率的なアルゴリズム設計(閉じ括弧が来たときに即検証)
自前の回答は同じ計算量を持ちますが、定数倍の違いが実行時間の差として現れています。また、コード内のエラーや余分な操作も実行時間に影響しています。odaさんレビュー ・手作業でできることを機械に指示するようにコードを書く。
模範解答を見る
class Solution(object):
def isValid(self, s: str) -> bool:
# The stack to keep track of opening brackets.
stack = []
# Hash map for keeping track of mappings. This keeps the code very clean.
# Also makes adding more types of parenthesis easier
mapping = {")": "(", "}": "{", "]": "["}
# For every bracket in the expression.
for char in s:
# If the character is an closing bracket
if char in mapping:
# Pop the topmost element from the stack, if it is non empty
# Otherwise assign a dummy value of '#' to the top_element variable
top_element = stack.pop() if stack else "#"
# The mapping for the opening bracket in our hash and the top
# element of the stack don't match, return False
if mapping[char] != top_element:
return False
else:
# We have an opening bracket, simply push it onto the stack.
stack.append(char)
# In the end, if the stack is empty, then we have a valid expression.
# The stack won't be empty for cases like ((()
return not stack模範回答を再現する ・頭の中でコードを表面的に理解(解説とコードの対応を暗記)、記憶したら、何も見ずに、その記憶のコードを咀嚼して深く理解する(解説の内容を腹落ち) ・実際に書いてみてチェックする
class Solution:
def isValid(self, s:str) -> bool:
stack = []
corresponding_open_brackets={")":"(","}":"{","]":"["}
for char in s:
if char in corresponding_open_brackets:
expected_char = stack.pop() if stack else "#"
if expected_char != corresponding_open_brackets[char]:
return False
else:
stack.append(char)
return not stackFuminitonさんレビュー ・stackの命名は改善の余地あり
・自前の回答で変数名の修正を行なったのでここでは省略する。
・レビューで別観点の修正があれば行う。
class Solution:
def isValid(self, s:str) -> bool:
open_brackets_stack = []
clese_to_open={")":"(","}":"{","]":"["}
for char in s:
if char in clese_to_open:
expected_char = open_brackets_stack.pop() if open_brackets_stack else "#"
if expected_char != clese_to_open[char]:
return False
else:
open_brackets_stack.append(char)
return not open_brackets_stack手作業 ・閉じ括弧か開き括弧か見る。開き括弧なら、メモする、閉じ括弧ならメモがあるならそのメモの最新の開きカッコを取り出して、同じ種類の閉じ括弧と、今の閉じ括弧をみくらべて、一致していることを確認。
・自前で正答まで辿り着いたことと、1回目で3分で正解を再現できたため省略する。
・時間を測って再現する。
1回目 00:09:52
class Solution:
def isValid(self,s: str) -> bool:
open_brackets_stack = []
close_to_open = {")":"(","}":"{","]":"["}
for char in s:
if char in close_to_open:
expected_close = open_brackets_stack.pop() if open_brackets_stack else "#"
if expected_close != close_to_open[char]:
return False
else:
open_brackets_stack.append(char)
return not open_brackets_stack2回目 00:08:00
class Solution:
def isValid(self, s: str) -> bool:
close_to_open = {")":"(", "}":"{", "]":"["}
open_brackets_stack = []
for char in s:
if char in close_to_open:
expected_bracket = open_brackets_stack.pop() if open_brackets_stack else "#"
if expected_bracket != close_to_open[char]:
return False
else:
open_brackets_stack.append(char)
return not open_brackets_stack