Skip to content

20. Valid Parentheses #14

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 4 commits 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
181 changes: 181 additions & 0 deletions 0020_Valid_Parentheses/solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
## Problem
https://leetcode.com/problems/valid-parentheses/

## Step 1
5分程度答えを見ずに考えて、手が止まるまでやってみる。
何も思いつかなければ、答えを見て解く。ただし、コードを書くときは答えを見ないこと。
動かないコードも記録する。
正解したら一旦OK。思考過程もメモする。

### Approach
* 文字列を一つずつ走査。Stackに開きカッコためていき、閉じカッコが出現したらStackと突合し、適切でなければfalseを返す
* 文字列が適切な並び順の場合、対応する開カッコがStackの一番上にあるはず。そうでなければfalse
* もしくは、閉じカッコが来ているのにStackが空の場合はfalseを返す
* 走査完了後、Stackが空であればtrue,そうでなければfalse

```java
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
Copy link

Choose a reason for hiding this comment

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

念のため知識面を確認させてください。 LinkedList を使用する場合と比べ、 ArrayDeque はどのような利点がありますか?

Copy link
Owner Author

Choose a reason for hiding this comment

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

正直、stackの実装にはArrayDequeを使うという形で機械的に考えていたので、LinkedListで実装できるという発想すらなかったですね。。

ご質問に対して調べずに回答すると、ArrayDequeの方がLinkedListよりメモリ効率が良いと思います。LinkedListは各要素が隣のノードの参照を持つため1要素のメモリ消費が大きいのではないかと思います。

Copy link

Choose a reason for hiding this comment

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

ありがとうございます。メモリ効率についてはその通りなのですが、 ArrayDeque より高速であるという点のほうが重要だと思います。詳しくは Javadoc をご覧ください。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/ArrayDeque.html
疑問に思ったときに公式ドキュメントを読む癖をつけることをお勧めいたします。

Copy link
Owner Author

Choose a reason for hiding this comment

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

公式ドキュメントに明記してあるのですね。読む癖をつけようと思います。

for (char c : s.toCharArray()) {
if (c == '(' ||c == '[' ||c == '{') {
stack.push(c);
}
if (c == ')' ||c == ']' ||c == '}') {
if (stack.isEmpty()) return false;
if (c == ')' && stack.pop() != '(') return false;
if (c == ']' && stack.pop() != '[') return false;
if (c == '}' && stack.pop() != '{') return false;
}
}
return stack.isEmpty();
}
}
```

## Step 2
他の方が描いたコードを見て、参考にしてコードを書き直してみる。
参考にしたコードのリンクは貼っておく。
読みやすいことを意識する。
他の解法も考えみる。

### 参考にしたPR
* https://github.com/hajimeito1108/arai60/pull/4
* https://github.com/SanakoMeine/leetcode/pull/6
* https://github.com/SanakoMeine/leetcode/pull/7
* https://github.com/olsen-blue/Arai60/pull/6
* https://github.com/canisterism/leetcode/pull/7



### 解法1. カッコを定数化した方法
* 処理の流れはStep1と同様。カッコを定数化したことにより条件分岐の数を減らせている

#### 所感
* メソッドの行数は減ったものの、Step 1とどちらを選択すべきかは悩ましい
* 個人的な感覚としては定数とメソッドとの間の目線の移動量が増えたことによって結果的に認知不可を上げてしまっている気がする
* 何らかの事情でinputデータが変更になる可能性がある場合を考慮すると、こちらの方が管理しやすそうではある

```java
class Solution {
private static final Set<Character> OPEN_BRACKETS = Set.of(
'(', '[', '{'
);

private static final Map<Character, Character> BRACKET_CLOSE_OPEN = Map.of(
')', '(',
']', '[',
'}', '{'
);

public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();

for (char c : s.toCharArray()) {
if (OPEN_BRACKETS.contains(c)) {
stack.push(c);
Copy link

Choose a reason for hiding this comment

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

私は個人的にはここで continue と書いてしまうのが好みですね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。たしかに後続の判定必要ないのでその方がいいですね。

continue;
}
if (!BRACKET_CLOSE_OPEN.containsKey(c)) {
continue;
}
if (stack.isEmpty()) {
return false;
}
if (BRACKET_CLOSE_OPEN.get(c) != stack.pop()) {
return false;
}

throw new IllegalArgumentException("Invalid character in input: " + c);
}
return stack.isEmpty();
}
}
```

#### 参考
カッコペアのMap定数を、{key: 開く, val: 閉じる} の順に持たせた場合の書き方

```java
if (BRACKET_OPEN_CLOSE.containsKey(c)) {
stack.push(c);
}
else if (stack.isEmpty())
{
return false;
}
else if (BRACKET_OPEN_CLOSE.get(stack.getLast()) == c)
{
stack.pop();
}
else
{
return false;
}
```

* レビューの中で以下のようなコメントをいただいた
> LinkedList を使用する場合と比べ、 ArrayDeque はどのような利点がありますか?
* LinkedListは隣のノードの参照を持つため1要素のメモリ消費が大きいという認識であったが、それに加えてArrayDequeの方が高速であるということは意識できていなかった
* ArrayDequeは内部的には動的配列を使っており、メモリ上で要素が連続して配置される。そのためキャッシュヒット率が高くアクセスが早い
* このあたりは公式ドキュメントを読む癖をつけたい
* https://docs.oracle.com/javase/8/docs/api/java/util/ArrayDeque.html


### 解法2. 番兵(Sentinel) を利用する方法
* 番兵をStackの底に置いておくことで、空かどうかの事前確認が不要になるという方法

```java
class Solution {
private static final Set<Character> OPEN_BRACKETS = Set.of(
'(', '[', '{'
);

private static final Map<Character, Character> BRACKET_CLOSE_OPEN = Map.of(
')', '(',
']', '[',
'}', '{'
);

public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
stack.push('*'); // Add the sentinel

for (char c : s.toCharArray()) {
if (OPEN_BRACKETS.contains(c)) {
stack.push(c);
}
if (BRACKET_CLOSE_OPEN.containsKey(c)) {
if (BRACKET_CLOSE_OPEN.get(c) != stack.pop()) {
return false;
}
}
}
return stack.size() == 1; // Check whether only the sentinel remains
}
}
```

## Step 3
今度は、時間を測りながら、もう一回書く。
アクセプトされたら消すを3回連続できたら問題はOK。

```java
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' ||c == '[' ||c == '{') {
stack.push(c);
}
if (c == ')' ||c == ']' ||c == '}') {
if (stack.isEmpty()) return false;
if (c == ')' && stack.pop() != '(') return false;
if (c == ']' && stack.pop() != '[') return false;
if (c == '}' && stack.pop() != '{') return false;
}
}
return stack.isEmpty();
}
}
```