Skip to content
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

[forest000014] Week 6 #907

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
115 changes: 115 additions & 0 deletions container-with-most-water/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
solution 1. brute force
Time Complexity: O(n^2)
Space Complexity: O(1)
모든 선분 i에 대해, 가장 많은 물을 담을 수 있는 반대쪽 선분 j를 찾는다.


solution 2. PQ
Time Complexity: O(nlogn)
- 정렬 : O(nlogn)
- i번째 선분에 대해 (자신보다 크거나 같은) 가장 멀리있는 선분 탐색(O(logn)) * n = O(nlogn)
Space Complexity: O(n)

(사고의 흐름을 적기 위해 편의상 반말로 적었습니다... ^^)
brute force 에서 불필요한 탐색을 줄여보자.
일단 어떤 선분 i가 주어졌다고 가정하자. i를 한쪽 벽으로 해서 가장 많은 물을 담는 방법을 찾으려면, 반드시 나머지 모든 선분을 탐색해야만 할까?

(1) 자신보다 작은 선분은 탐색하지 않는다.

자신보다 큰 선분만을 탐색해도 충분하다.
왜냐하면, 설령 자신보다 더 작은 선분 중에 정답이 있었다고 하더라도, 그 선분을 기준으로 탐색할 때 정답에 해당하는 쌍을 확인하게 될 것이기 때문이다.

(2) 자신보다 크거나 같은 선분만을 탐색 대상으로 삼는다면, 가장 멀리있는 선분만 확인하면 된다.

탐색 대상이 자신보다 크거나 같은 선분들이라면, 어차피 담을 수 있는 물의 높이는 자신의 높이로 일정하다.
따라서, 멀리있는 선분일수록 더 많은 물을 담게 된다.
즉, "자신보다 크거나 같으면서", "가장 멀리 있는(오른쪽이든 왼쪽이든)" 선분만을 후보로 삼아 확인하면 충분하다.
(그 외의 나머지, 즉 자신보다 크거나 같으면서, 가장 멀리있지 않은 나머지 선분들은, 자연스럽게 탐색하지 않을 수 있다.)

정리하자면, 주어진 선분 i에 대해서, 단 2번(오른쪽, 왼쪽)의 탐색만 하면 충분하다.

(3) 내림차순 정렬과 2개의 PQ를 아래처럼 활용하면, 위 탐색을 O(logn) 시간에 수행할 수 있다.
PQ는 각각 x 좌표 기준 max heap(가장 오른쪽의 선분을 찾기 위함), x 좌표 기준 min heap(가장 왼쪽의 선분을 찾기 위함)을 사용한다.
선분들을 길이 내림차순으로 정렬해놓고, 하나씩 순회하면서 (say, i번째 선분), 아래 과정을 반복한다.
- 자신보다 크거나 같으면서 가장 오른쪽으로 멀리 있는 선분의 위치를 찾아서(= 현재 PQ(max heap)의 root), 최대 물의 양을 계산한다.
- 자신보다 크거나 같으면서 가장 왼쪽으로 멀리 있는 선분의 위치를 찾아서(= 현재 PQ(min heap)의 root), 최대 물의 양을 계산한다.
- i번째 선분을 PQ 2개에 각각 넣는다.


solution 3. two pointers
(AlgoDale 풀이를 참고함)

Time Complexity: O(n)
Space Complexity: O(1)

2 포인터를 활용하면, PQ도 없이 시간 복잡도를 O(n)으로 줄일 수 있었다.
단순히 "큰 쪽을 줄이기보다는, 작은 쪽을 줄이는 게 유리하겠지" 정도의 greedy한 논리는 충분하지 않은 것 같고, 더 명확한 근거가 있을 것 같은데 시간 관계상 고민해보지는 못했다.

To-Do : 풀이가 대강 이해는 되었지만, 이게 왜 되는지, 엄밀하게 이해하기 위해 PQ를 사용했던 논리를 좀 더 발전시켜볼 필요가 있다.

*/
class Solution {
public int maxArea(int[] height) {
int i = 0, j = height.length - 1;

int ans = 0;
while (i < j) {
int area = (j - i) * Math.min(height[i], height[j]);
if (area > ans) {
ans = area;
}

if (height[i] <= height[j]) {
i++;
} else {
j--;
}
}

return ans;
}

////// 아래는 solution 2
private static class Tuple {
private int x;
private int h;

Tuple(int x, int h) {
this.x = x;
this.h = h;
}
}

public int maxArea2(int[] height) {
List<Tuple> tuples = IntStream.range(0, height.length)
.mapToObj(i -> new Tuple(i, height[i]))
.collect(Collectors.toList());
Collections.sort(tuples, (a, b) -> b.h - a.h);

PriorityQueue<Tuple> minPq = new PriorityQueue<>((a, b) -> a.x - b.x);
PriorityQueue<Tuple> maxPq = new PriorityQueue<>((a, b) -> b.x - a.x);

int ans = 0;
for (int i = 0; i < height.length; i++) {
minPq.add(tuples.get(i));
maxPq.add(tuples.get(i));

Tuple curr = tuples.get(i);

Tuple left = minPq.peek();
int leftArea = (curr.x - left.x) * curr.h;
if (leftArea > ans) {
ans = leftArea;
}

Tuple right = maxPq.peek();
int rightArea = (right.x - curr.x) * curr.h;
if (rightArea > ans) {
ans = rightArea;
}
}

return ans;
}
}
78 changes: 78 additions & 0 deletions design-add-and-search-words-data-structure/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Time Complexity:
- add: O(w)
- search: O(26^2 * w) = O(w)
Space Complexity: O(w)

Trie를 활용하되, '.'의 탐색이 필요한 경우에는 for문을 사용한다.

*/
class WordDictionary {
class Node {
public char ch;
public boolean ends;
public Map<Character, Node> children;

Node() {
this.children = new HashMap<>();
}

Node(char ch) {
this.ch = ch;
this.children = new HashMap<>();
}
}

Node root;

public WordDictionary() {
this.root = new Node();
}

public void addWord(String word) {
Node curr = this.root;

for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
if (!curr.children.containsKey(ch)) {
curr.children.put(ch, new Node(ch));
}

curr = curr.children.get(ch);
}

curr.ends = true;
}

public boolean search(String word) {
return searchChar(word, 0, this.root);
}

private boolean searchChar(String word, int idx, Node curr) {
if (curr == null) {
return false;
} else if (idx == word.length()) {
return curr.ends;
}

char ch = word.charAt(idx);

if (ch == '.') {
for (Character key : curr.children.keySet()) {
if (searchChar(word, idx + 1, curr.children.get(key))) {
return true;
}
}
return false;
} else {
return searchChar(word, idx + 1, curr.children.get(ch));
}
}
}

/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary obj = new WordDictionary();
* obj.addWord(word);
* boolean param_2 = obj.search(word);
*/
85 changes: 85 additions & 0 deletions longest-increasing-subsequence/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
Time Complexity: O(nlogn)
Space Complexity: O(nlogn)
*/
class Solution {

int[] tree;
int L = 1;

public int lengthOfLIS(int[] nums) {
init(nums);
ArrayList<Tuple> tuples = new ArrayList<>();

for (int i = 0; i < nums.length; i++) {
tuples.add(new Tuple(i, nums[i]));
}

Collections.sort(tuples, (a, b) -> {
if (a.val == b.val) {
return b.ref - a.ref; // 2순위 : ref 내림차순
} else {
return a.val - b.val; // 1순위 : val 오름차순
}
});

int ans = 0;
for (int i = 0; i < nums.length; i++) {
int curr = getMax(0, tuples.get(i).ref - 1) + 1;
ans = Math.max(ans, curr);
insert(tuples.get(i).ref, curr);
}

return ans;
}

public class Tuple {
public int ref;
public int val;

public Tuple(int ref, int val) {
this.ref = ref;
this.val = val;
}
}

public void init(int[] nums) {
while (L < nums.length) {
L *= 2;
}

tree = new int[L * 2];

for (int i = 1; i < L * 2; i++) {
tree[i] = 0;
}
}

public void insert(int idx, int v) {
int i = idx + L;
tree[i] = v;
i /= 2;
while (i >= 1) {
tree[i] = Math.max(tree[i * 2], tree[i * 2 + 1]);
i /= 2;
}
}

public int getMax(int l, int r) {
int i = l + L;
int j = r + L;
int ret = 0;
while (i <= j) {
if (i % 2 == 1) {
ret = Math.max(ret, tree[i++]);
}
if (j % 2 == 0) {
ret = Math.max(ret, tree[j--]);
}
i /= 2;
j /= 2;
}

return ret;
}
}
35 changes: 35 additions & 0 deletions spiral-matrix/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Time Complexity: O(m * n)
Space Complexity: O(1)

현재 위치를 r, c라는 변수를 사용해서 나타내고, 이 r, c를 직접 제어하는 방식으로 행렬을 순회한다.
*/
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> ans = new ArrayList<>();

int m = matrix.length, n = matrix[0].length;
int r = 0, c = 0;
int[] dr = {0, 1, 0, -1};
int[] dc = {1, 0, -1, 0};
int d = 0;
final int VISITED = -999;

while (ans.size() < m * n) {
ans.add(matrix[r][c]);
matrix[r][c] = VISITED;

int nr = r + dr[d];
int nc = c + dc[d];
if (nr < 0 || nr >= m || nc < 0 || nc >= n || matrix[nr][nc] == VISITED) {
d = (d + 1) % 4;
nr = r + dr[d];
nc = c + dc[d];
}
r = nr;
c = nc;
}

return ans;
}
}
37 changes: 37 additions & 0 deletions valid-parentheses/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Time Complexity: O(n)
Space Complexity: O(n)

유효한 문자열이라면, 인접한 열고 닫는 괄호 쌍을 하나씩 캔슬시켰을 때, 빈 문자열만 남게 된다.
매치되는 쌍이 서로 떨어져있을 수 있기 때문에, 그 안의 유효한 쌍들을 미리 모두 캔슬시킨 뒤에 판단해야 매칭 여부를 쉽게 판단할 수 있는데, 이 과정을 스택을 이용해 구현할 수 있다.
*/
class Solution {
public boolean isValid(String s) {
Stack<Character> st = new Stack<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

안녕하세요! 제가 자바를 거의 써보지 않아서 리뷰가 정확하지 않을 수 있는 점 양해 부탁드립니다 :) 꼭 모두 고치지 않으셔도 되고, 참고만 해주세요!

혹시 ArrayDeque를 사용하면 Stack보다 성능이 더 나아질까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ArrayDeque은 잘 몰랐는데, 말씀해주신 덕분에 좀 찾아보았습니다.

시간 복잡도 측면에서는 ArrayDeque이나 Stack 모두 push/pop이 O(1)이고, 공간 복잡도도 O(n)으로 동일한 것 같습니다.
(capacity가 가득 찼을 때 특별한 설정이 없으면 둘 다 2배씩 resize하기 때문에, push()의 시간 복잡도가 amortized O(1)이 된다고 이해했습니다.)

다만, Stack은 Vector를 상속받았는데, Vector는 동기화 때문에 주요 메소드들의 속도가 좀 더 느리다고 하네요. 다음 번에 Stack 쓸 일이 있을 땐, ArrayDeque을 써봐야겠습니다 👍


if (s.length() % 2 == 1) {
return false;
}

for (char ch : s.toCharArray()) {
if (ch == '(' || ch == '{' || ch == '[') {
Copy link
Contributor

Choose a reason for hiding this comment

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

각 pair들을 담은 Map을 사용하면 조건문을 많이 줄일 수 있을 것 같습니다.

st.push(ch);
} else {
if (st.empty())
return false;
if (ch == ')') {
if (st.peek() != '(')
return false;
} else if (ch == '}') {
if (st.peek() != '{')
return false;
} else {
if (st.peek() != '[')
return false;
}
st.pop();
}
}
return st.empty();
}
}
Loading