Skip to content

[Invidam] Week 06 Solutions #122

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

Merged
merged 2 commits into from
Jun 11, 2024
Merged
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
105 changes: 105 additions & 0 deletions container-with-most-water/invidam.go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Intuition
높이가 양 끝 라인에 의해 결정된다는 것을 응용하여 투포인터 해결법을 고안했다.

# Approach
1. 높이(`h`)를 0 ~ 최대까지 순회한다.
2. 높이를 초과하는 선까지 양 끝(`l`, `r`)을 조절한다.
3. 조절한 양 끝을 최댓값 갱신에 이용한다.

# Complexity
- Time complexity: $O(n)$
- 배열의 크기 `n`에 대하여, 반복문이 배열의 인덱스를 모두 순회하면 종료되기에 (`l < r`이 깨질 때) 이에 따라 시간복잡도가 결정된다.
- Space complexity: $O(n), inline$
- 배열의 크기 `n`에 대하여, 입력값으로 배열을 받으니 inline인 n이 소요된다.

# Code
## Two Pointer
```go
func maxArea(height []int) int {
l, r := 0, len(height)-1
var maxArea int
for l < r {
minH := min(height[l], height[r])
maxArea = max(minH*(r-l), maxArea)

if minH == height[l] {
l++
} else {
r--
}
}

return maxArea
}

```
: 원래는 deque를 이용해 해결했는데, 솔루션의 투포인터를 이용한 것과 동일한 방식이어서 깔끔한 후자로 수정했다.

## Deque
```go
type Deque struct {
Nodes []int
}

func NewDeque(arr []int) Deque {
return Deque{Nodes: arr}
}

func (dq *Deque) PushFront(node int) {
dq.Nodes = append([]int{node}, dq.Nodes...)
}

func (dq *Deque) PushBack(node int) {
dq.Nodes = append(dq.Nodes, node)
}
func (dq *Deque) Front() int {
return dq.Nodes[0]
}

func (dq *Deque) Back() int {
return dq.Nodes[len(dq.Nodes)-1]
}

func (dq *Deque) PopFront() int {
ret := dq.Front()

dq.Nodes = dq.Nodes[1:]

return ret
}

func (dq *Deque) PopBack() int {
ret := dq.Back()

dq.Nodes = dq.Nodes[0 : len(dq.Nodes)-1]

return ret
}

func (dq *Deque) Size() int {
return len(dq.Nodes)
}

func maxArea(height []int) int {
dq := NewDeque(height)

var max int
for h := 0; dq.Size() > 1; h++ {
for dq.Size() != 0 && dq.Front() < h {
dq.PopFront()
}
for dq.Size() != 0 && dq.Back() < h {
dq.PopBack()
}

area := h * (dq.Size() - 1)

if area > max {
max = area
}
}

return max
}

```
44 changes: 44 additions & 0 deletions find-minimum-in-rotated-sorted-array/invidam.go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Intuition
전형적인 이분탐색 문제였다.

# Approach
1. `nums[0]보다 작은지 여부`를 검사하도록 설계했다.
2. `lo`는 항상 F이며, `hi`는 항상 T이다.
2. 배열의 원소들에 대해 FFFFFFFTTTTTT가 되는데, 이 때 `T`가 처음 등장하는 지점을 찾는다.
3. 등장하지 않는 경우 (`nums[0]`이 최소인 경우 = 회전이 없는 경우)는 hi가 배열 범위 밖이므로, 인덱스 0을 반환하게 했다.

(문제 해결 이후 타 문제 재활용을 위해 인덱스를 찾도록 리팩터링했다.)
# Complexity
- Time complexity: $O(log(n))$
- 배열의 길이 `n`에 대하여, 범위를 반으로 줄여가며 이분 탐색하므로 `log(n)`이 발생한다.

- Space complexity: $O(n), inline$
- 배열의 길이 `n`에 대하여, 입력(`nums`)의 비용이 존재한다.
Copy link
Contributor

Choose a reason for hiding this comment

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

이 문제도 입력값에 대해서는 고려하지 않아도 될 것 같습니다.


# Code
```go
func findMinIdx(nums []int) int {
lo, hi := -1, len(nums)
for lo+1 < hi {
mid := (lo + hi) / 2

if nums[mid] < nums[0] {
hi = mid
} else {
lo = mid
}
}
if hi == len(nums) {
return 0
}
return hi
}

func findMin(nums []int) int {
return nums[findMinIdx(nums)]
}

```

# 여담
이분탐색 문제는 타 솔루션을 참고하지 않는 편이다. 왜냐하면 `lo`, `hi`, 종료 조건, 반환 값 설정이 비슷해보이지만 엄청 다른 의미이기에 큰 도움이 안된다고 생각한다.
46 changes: 46 additions & 0 deletions longest-repeating-character-replacement/invidam.go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Intuition
문자열 내에 영문자들만 등장한다는 것에서 DP, 그리디가 아니라 빈도수 계산 + 투포인터임을 알게되었다.

# Approach
1. A-Z에 대해 순회한다.
2. 포함하는 배열이 커지며(`j++`) 순회하는 문자가 몇 번 연속해서 등장하는지 계산한다. (`have`)
3. 연속하지 않은 문자가 등장하더라도 `k`번은 봐준다.
4. `k`번이상이라 봐줄 수 없는 경우 (`have+k < j-i+1`인 경우) 포함하는 배열을 줄인다. (`i++`)

# Complexity
- Time complexity: $O(n)$
- 순회하는 문자들은 상수 개수(26)이므로 무시한다.
- 문자열의 길이 `n`에 대하여, 이를 순회하는 비용이 든다. (`j<len(s)`)

- Space complexity: $O(n) inline$
- 문자열의 길이 `n`에 대하여, 입력으로 받은 `s`만큼 비용이 든다.

# Code
```go
func characterReplacement(s string, k int) int {
var maxLen int
for ch := 'A'; ch <= 'Z'; ch++ {
i, j := 0, 0
var have int
for j < len(s) {
if ch == rune(s[j]) {
have++
}
for have+k < j-i+1 {
if ch == rune(s[i]) {
have--
}
i++
}
maxLen = max(maxLen, j-i+1)
j++
}
}
return maxLen
}

```
# 여담
- 가장 많이 등장한 문자를 검색하는 솔루션을 확인했다. 동일 문제를 다르게 접근한다는 게 신기했다.
- 비교해보자면, A~Z중 한 문자가 등장하는 것만 계산한다는 본인의 풀이가 더욱 직관적이라는 생각이 들었다.
- 다만, 코드로 나타냈을 때는 덜 직관적이었다...;
39 changes: 39 additions & 0 deletions longest-substring-without-repeating-characters/invidam.go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Intuition
DP, 그리디 등을 시도하려다가 오류를 발견했다. 화이트보드에 예제 문제들을 손으로 풀어보니 직관적으로 떠올랐다.
# Approach
1. 똑같은 음식을 먹지 않는 지렁이를 생각하자.
2. 다음 음식을 이미 먹었다면, 먹었던 걸 뱉는다. (`l++`)
3. 다음 음식을 먹지 않았다면, 먹는다. (`r++`)
4. 지렁이의 길이를 최댓값 갱신에 활용한다.
# Complexity
- Time complexity: $O(n)$
- 문자열의 길이 `n`에 대하여, 이를 순회하는 비용이 소모된다.
- Space complexity: $O(n)$
- 문자열의 길이 `n`에 대하여, 등장 여부를 저장하는 자료구조(`contains`)는 최대 `n`개 만큼을 저장할 수 있으므로 `n`이다.
<!-- Add your space complexity here, e.g. $$O(n)$$ -->

# Code
```go
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 {
return 0
}
contains := make(map[uint8]bool)

l, r := 0, 0
maxLen := 1

for r < len(s) {
for contains[s[r]] {
contains[s[l]] = false
l++
}
contains[s[r]] = true
maxLen = max(maxLen, r-l+1)
r++
}

return maxLen
}

```
74 changes: 74 additions & 0 deletions search-in-rotated-sorted-array/invidam.go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Intuition
이분 탐색인 게 직관적으로 보였다.

# Approach
1. 찾으려는 값(`target`)이 최솟값의 인덱스(`minIdx`) 이전/이후인지 판단한다. (`target < nums[0] || minIdx == 0`)
2. 판단한 범위(이전, 이후)에 대해서만 배열을 잘라 `lowerBound`를 실행한다.
3. 결과를 통해 타겟의 인덱스(`targetIdx`)를 획득한다.
4. 예외처리(범위 벗어난 경우, `lowerBound`은 맞으나 일치하진 않는 경우)엔 `-`을 아닌 경우는 인덱스를 반환한다.

# Complexity
- Time complexity: $O(log(n))$
- 배열의 길이 `n`에 대하여, 범위를 반으로 줄여가며 이분 탐색하므로 `log(n)`이 발생한다.
- 두 이분탐색 함수를 사용하긴 하지만, 합연산이므로 복잡도에는 영향이 없다.
- Space complexity: $O(n), inline$
- 배열의 길이 `n`에 대하여, 입력(`nums`)의 비용이 존재한다.
Copy link
Contributor

Choose a reason for hiding this comment

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

입력값에 대해서는 고려하지 않아도 되지 않을까 싶네요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

다음부턴 의견 반영해보겠습니다!


# Code
```go
func findMinIdx(nums []int) int {
lo, hi := -1, len(nums)
for lo+1 < hi {
mid := (lo + hi) / 2

if nums[mid] < nums[0] {
hi = mid
} else {
lo = mid
}
}
if hi == len(nums) {
return 0
}
return hi
}

func lowerBound(nums []int, target int) int {
Copy link
Contributor

@bky373 bky373 Jun 9, 2024

Choose a reason for hiding this comment

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

(사소) 사용하신 이분탐색은 결국 lo+1 < hi 조건을 만족할 때만 반복문을 종료할 수 있는데
중간에 nums[mid] == target 일 때 반복문을 조기 종료시키는 다른 방식보다 연산이 더 길어질 것 같습니다~
현재 코드에서 조기 종료 조건을 추가해보시는 건 어떠신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

생각해보지 못했는데 그것도 좋은 아이디어인 것 같네요!

저도 자세히 아는 건 아니지만, 매번 불필요하게 nums[mid] == target을 검사하는게 더욱 오래걸릴 수도 있다고 들어서 비교를 해봐야 정확히 알 것 같아요!

Copy link
Contributor

@bky373 bky373 Jun 9, 2024

Choose a reason for hiding this comment

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

저도 갑자기 궁금해져서 gpt 에 질문 올려봤습니다 (재미로 봐주세요 ㅋㅋ)
그냥 선호하시는 스타일 대로(혹은 면접관이 좋아할 만한 스타일 대로?) 작성하시는 게 나을 것 같습니다 ㅋㅋ

nums[mid] == target 동등 비교 검사가 없는 코드(비담님 코드)와 있는 코드(제안드린 코드) 두 개 코드 보여주고 비교해달라 했습니다.

image image

lo, hi := -1, len(nums)
for lo+1 < hi {
mid := (lo + hi) / 2

if nums[mid] < target {
lo = mid
} else {
hi = mid
}
}
return hi
}

func search(nums []int, target int) int {
minIdx := findMinIdx(nums)
var targetIdx int
if target < nums[0] || minIdx == 0 {
targetIdx = lowerBound(nums[minIdx:], target) + minIdx
} else {
targetIdx = lowerBound(nums[0:minIdx], target)
}

if len(nums) <= targetIdx || target != nums[targetIdx] {
return -1
}
return targetIdx

}

```
# I learned
`whitespace around operator`(연산자 근처에서 공백 사용)에 대한 golang의 해석이 일반적인 언어들과 다른 걸 알게되었다.
- 일반언어: `lo + 1 < hi`
- GoLang: `lo+1 < hi`

즉, 모든 연산자 근처에 공백을 추가하는 것이 아니라 낮은 우선순위의 연산자에 대해서만 추가한다.

참고: https://news.ycombinator.com/item?id=10796427