-
Notifications
You must be signed in to change notification settings - Fork 126
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
[Flynn] Week 10 #533
[Flynn] Week 10 #533
Changes from all commits
77bebde
79f42eb
6541b17
2a6021e
92fb01e
8c9dd17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
풀이 | ||
- 각 course를 node로 생각하고, prerequisite 관계에 있는 node를 간선으로 이어주면 단방향 간선으로 연결된 node 집합의 graph를 떠올릴 수 있습니다 | ||
- 이 문제는 위에서 설명한 graph에 loop가 있냐 없느냐를 판단하는 문제입니다 | ||
- 함수의 재귀호출 및 백트래킹을 이용해서 풀이할 수 있습니다 | ||
|
||
Big O | ||
- N: 과목 수 (node의 개수) | ||
- M: 배열 prerequisites의 길이 (간선의 개수) | ||
- Time compleixty: O(N + M) | ||
- prereqMap을 초기화 -> O(M) | ||
- 함수의 재귀호출을 통해 우리는 각 node를 최대 한번씩 조회합니다 -> O(N) | ||
- Space complexity: O(N + M) | ||
- prereqMap -> O(M) | ||
- checkingSet, checkedSet -> O(N) | ||
*/ | ||
|
||
func canFinish(numCourses int, prerequisites [][]int) bool { | ||
// 주어진 prerequisites 배열로 `a_i: b_0, b_1, ...` 형태의 맵을 짭니다 | ||
prereqMap := make(map[int][]int, numCourses) | ||
for _, pair := range prerequisites { | ||
prereqMap[pair[0]] = append(prereqMap[pair[0]], pair[1]) | ||
} | ||
|
||
// checkingSet으로 현재 탐색하고 있는 구간에 loop가 생겼는지 여부를 판단하고, checkedSet으로 이미 탐색한 node인지를 판단합니다 | ||
checkingSet, checkedSet := make(map[int]bool, 0), make(map[int]bool, 0) | ||
|
||
// 특정 과목 c를 듣기 위한 선행 과목들을 탐색했을 때 loop가 생기는지 여부를 판단하는 함수입니다 | ||
// (Go 언어 특성상 L:20-21 처럼 함수를 선언합니다 (함수 내부에서 함수를 선언할 땐 익명 함수를 사용 + 해당 함수를 재귀호출하기 위해서 선언과 초기화를 분리)) | ||
var checkLoop func(int) bool // loop가 있다면 true | ||
checkLoop = func(c int) bool { | ||
// 과목 c가 현재 탐색하고 있는 구간에 존재한다면 loop가 있다고 판단 내릴 수 있습니다 | ||
_, checkingOk := checkingSet[c] | ||
if checkingOk { | ||
return true | ||
} | ||
// 과목 c가 이미 탐색이 완료된 과목이라면 과목 c를 지나는 하위 구간에는 loop가 없다고 판단할 수 있습니다 | ||
_, checkedOk := checkedSet[c] | ||
if checkedOk { | ||
return false | ||
} | ||
// 과목 c를 checkingSet에 추가합니다 | ||
// 만약 하위 구간에서 과목 c를 다시 만난다면 loop가 있다고 판단할 수 있습니다 | ||
checkingSet[c] = true | ||
// 각 선행과목 별로 하위구간을 만들어 탐색을 진행합니다 | ||
// 하위구간 중 하나라도 loop가 발생하면 현재 구간에는 loop가 있다고 판단할 수 있습니다 | ||
for _, prereq := range prereqMap[c] { | ||
if checkLoop(prereq) { | ||
return true | ||
} | ||
} | ||
// 만약 loop가 발견되지 않았다면 checkedSet에 과목 c를 추가함으로써 과목 c를 지나는 구간이 안전하다고 표시합니다 | ||
checkedSet[c] = true | ||
// checkingSet에서 과목 c를 지워줍니다 | ||
delete(checkingSet, c) | ||
return false | ||
} | ||
|
||
for i := 0; i < numCourses; i++ { | ||
if checkLoop(i) { | ||
return false | ||
} | ||
} | ||
return true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
/* | ||
풀이 1 | ||
- 함수의 재귀호출을 이용해서 풀이할 수 있습니다 | ||
|
||
Big O | ||
- N: 노드의 개수 | ||
- H: 트리의 높이 | ||
- Time complexity: O(N) | ||
- Space complexity: O(H) (logN <= H <= N) | ||
- 재귀 호출 스택의 최대 깊이는 트리의 높이에 비례하여 증가합니다 | ||
*/ | ||
|
||
/** | ||
* Definition for a binary tree node. | ||
* type TreeNode struct { | ||
* Val int | ||
* Left *TreeNode | ||
* Right *TreeNode | ||
* } | ||
*/ | ||
func invertTree(root *TreeNode) *TreeNode { | ||
if root == nil { | ||
return root | ||
} | ||
|
||
tmp := invertTree(root.Right) | ||
root.Right = invertTree(root.Left) | ||
root.Left = tmp | ||
|
||
return root | ||
} | ||
|
||
/* | ||
풀이 2 | ||
- 큐와 반복문을 이용하여 풀이할 수 있습니다 | ||
|
||
Big O | ||
- N: 노드의 개수 | ||
- Time complexity: O(N) | ||
- Space complexity: O(N) | ||
- 큐의 최대 크기는 N / 2 를 넘지 않습니다 | ||
큐의 최대 크기는 트리의 모든 층 중에서 가장 폭이 큰 층의 노드 수와 같습니다 | ||
높이가 H인 트리의 최대 폭은 1. balanced tree일 때 2. 맨 아랫 층의 폭이고 이 때의 폭 W는 2^(H-1) 입니다 | ||
높이가 H인 balanced tree의 노드 개수는 2^H - 1 = N 이므로 아래 관계가 성립합니다 | ||
N/2 = (2^H - 1) / 2 = 2^(H-1) - 1/2 >= 2^(H-1) = W | ||
따라서 공간 복잡도는 O(N/2) = O(N) 입니다 | ||
*/ | ||
|
||
/** | ||
* Definition for a binary tree node. | ||
* type TreeNode struct { | ||
* Val int | ||
* Left *TreeNode | ||
* Right *TreeNode | ||
* } | ||
*/ | ||
func invertTree(root *TreeNode) *TreeNode { | ||
queue := make([]*TreeNode, 0) | ||
queue = append(queue, root) | ||
|
||
for len(queue) > 0 { | ||
node := queue[0] | ||
queue = queue[1:] | ||
|
||
if node == nil { | ||
continue | ||
} | ||
|
||
tmp := node.Left | ||
node.Left = node.Right | ||
node.Right = tmp | ||
|
||
queue = append(queue, node.Left, node.Right) | ||
} | ||
|
||
return root | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
풀이 1 | ||
- memo 배열을 이용하여 n-1번째 인덱스부터 0번째 인덱스 방향으로 탐색하여 풀이할 수 있습니다 | ||
memo[i] = i번째 인덱스에서 출발했을 때 마지막 인덱스에 도달할 수 있는지 여부 | ||
|
||
Big O | ||
- N: 주어진 배열 nums의 길이 | ||
- Time complexity: O(N) | ||
- Space complexity: O(N) | ||
- 풀이 2를 이용하면 O(1)으로 최적화할 수 있습니다 | ||
*/ | ||
|
||
func canJump(nums []int) bool { | ||
n := len(nums) | ||
|
||
if nums[0] == 0 && n > 1 { | ||
return false | ||
} | ||
|
||
memo := make([]bool, n) | ||
memo[n-1] = true | ||
|
||
for i := n - 2; i >= 0; i-- { | ||
for j := 1; j <= nums[i]; j++ { | ||
if i+j >= n { | ||
break | ||
} | ||
if memo[i+j] { | ||
memo[i] = true | ||
break | ||
} | ||
} | ||
} | ||
|
||
return memo[0] | ||
} | ||
|
||
/* | ||
풀이 | ||
- 풀이 1을 잘 관찰하면 memo배열의 모든 값을 가지고 있을 필요가 없다는 걸 알 수 있습니다 | ||
memo 배열 대신에, 문제의 조건대로 마지막 인덱스까지 갈 수 있는 가장 좌측의 인덱스만 기록합니다 (leftmost) | ||
|
||
Big O | ||
- N: 주어진 배열 nums의 길이 | ||
- Time complexity: O(N) | ||
- Space complexity: O(1) | ||
*/ | ||
|
||
func canJump(nums []int) bool { | ||
n := len(nums) | ||
|
||
if nums[0] == 0 && n > 1 { | ||
return false | ||
} | ||
|
||
leftmost := n - 1 | ||
|
||
for i := n - 2; i >= 0; i-- { | ||
if i+nums[i] >= leftmost { | ||
leftmost = i | ||
} | ||
} | ||
|
||
return leftmost == 0 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
풀이 1 | ||
- lists를 순회하면서 순서대로 링크드리스트 두 개를 짝 지어 병합하는 방식으로 풀이할 수 있습니다 | ||
|
||
Big O | ||
- K: 배열 lists의 길이 | ||
- N: 모든 링크드리스트의 노드 개수의 합 | ||
- n_i: i번 인덱스 링크드리스트의 노드의 개수 | ||
- Time complexity: O(KN) | ||
- K-1 번의 병합을 진행합니다 | ||
- i번째 병합 때, 병합하는 두 링크드리스트는 각각 𝚺(n_(i-1)), n_i입니다 | ||
이 때 𝚺(n_(i-1))의 상한을 고려한다면 두 링크드리스트의 병합에 걸리는 시간복잡도는 O(N)입니다 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제 시간복잡도 산출결과가 달라서 한참 보다가 알았네요 |
||
- O((K-1)N) = O(KN) | ||
- 풀이 2로 시간복잡도를 O((logK)N)으로 최적화할 수 있습니다 | ||
- Space complexity: O(1) | ||
- res, dummy, curr 등의 추가적인 포인터를 생성하긴 하지만 기존에 주어져 있던 ListNode의 Next만 조작하므로 K, N과 상관 없이 공간복잡도는 상수값을 가집니다 | ||
*/ | ||
|
||
/** | ||
* Definition for singly-linked list. | ||
* type ListNode struct { | ||
* Val int | ||
* Next *ListNode | ||
* } | ||
*/ | ||
func mergeKLists(lists []*ListNode) *ListNode { | ||
n := len(lists) | ||
|
||
if n == 0 { | ||
return nil | ||
} | ||
|
||
res := lists[0] | ||
for i := 1; i < n; i++ { | ||
res = mergeTwoLists(res, lists[i]) | ||
} | ||
return res | ||
} | ||
|
||
func mergeTwoLists(first *ListNode, second *ListNode) *ListNode { | ||
dummy := &ListNode{} | ||
curr := dummy | ||
|
||
for first != nil && second != nil { | ||
if first.Val < second.Val { | ||
curr.Next = first | ||
first = first.Next | ||
} else { | ||
curr.Next = second | ||
second = second.Next | ||
} | ||
curr = curr.Next | ||
} | ||
|
||
if first != nil { | ||
curr.Next = first | ||
} | ||
if second != nil { | ||
curr.Next = second | ||
} | ||
|
||
return dummy.Next | ||
} | ||
|
||
|
||
/* | ||
풀이 2 | ||
- Divide and Conquer 방식으로 시간복잡도를 최적화할 수 있습니다 | ||
- 하지만 공간복잡도 측면에서는 trade-off가 있습니다 | ||
|
||
Big O | ||
- K: 배열 lists의 길이 | ||
- N: 모든 링크드리스트의 노드 개수의 합 | ||
- Time complexity: O((logK)N) | ||
- lists를 반으로 쪼개 가면서 재귀호출을 진행하므로 재귀호출은 logK 레벨에 걸쳐 이루어집니다 -> O(logK) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 단순히 절반씩 나누는것만으로도 낭비되는 연산량을 꽤 줄일 수 있네요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 저도 이 기회에 분할 정복의 효용에 대해 다시 보게 되었습니다.. ㅎㅎㅎㅎ |
||
- 각 계층마다 우리는 모든 노드를 최대 한 번씩 조회합니다 -> O(N) | ||
- Space complexity: O(logK) | ||
- 풀이 1과 비슷하지만 재귀호출 스택을 고려해야 합니다 | ||
*/ | ||
|
||
/** | ||
* Definition for singly-linked list. | ||
* type ListNode struct { | ||
* Val int | ||
* Next *ListNode | ||
* } | ||
*/ | ||
func mergeKLists(lists []*ListNode) *ListNode { | ||
n := len(lists) | ||
|
||
if n == 0 { | ||
return nil | ||
} | ||
if n == 1 { | ||
return lists[0] | ||
} | ||
|
||
left := mergeKLists(lists[:n/2]) | ||
right := mergeKLists(lists[n/2:]) | ||
|
||
return mergeTwoLists(left, right) | ||
} | ||
|
||
func mergeTwoLists(first *ListNode, second *ListNode) *ListNode { | ||
dummy := &ListNode{} | ||
curr := dummy | ||
|
||
for first != nil && second != nil { | ||
if first.Val < second.Val { | ||
curr.Next = first | ||
first = first.Next | ||
} else { | ||
curr.Next = second | ||
second = second.Next | ||
} | ||
curr = curr.Next | ||
} | ||
|
||
if first != nil { | ||
curr.Next = first | ||
} | ||
if second != nil { | ||
curr.Next = second | ||
} | ||
|
||
return dummy.Next | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
풀이 | ||
- 이진탐색을 두 번 사용하여 풀이할 수 있습니다 | ||
첫번째, pivot index를 찾습니다 | ||
두번째, target을 찾습니다 | ||
|
||
Big O | ||
- N: 주어진 배열 nums의 길이 | ||
- Time complexity: O(logN) | ||
- 각 이진탐색이 모두 O(logN)의 시간 복잡도를 가집니다 | ||
- Space complexity: O(1) | ||
*/ | ||
|
||
func search(nums []int, target int) int { | ||
n := len(nums) | ||
|
||
lo, hi := 0, n | ||
for lo < hi { | ||
mid := lo + (hi-lo)/2 | ||
if nums[mid] > nums[n-1] { | ||
lo = mid + 1 | ||
} else { | ||
hi = mid | ||
} | ||
} | ||
pivot := lo | ||
if pivot == n { | ||
pivot = 0 | ||
} | ||
|
||
lo, hi = pivot, pivot+n | ||
for lo < hi { | ||
mid := lo + (hi-lo)/2 | ||
normalizedMid := mid | ||
if normalizedMid >= n { | ||
normalizedMid = mid - n | ||
} | ||
if nums[normalizedMid] <= target { | ||
lo = mid + 1 | ||
} else { | ||
hi = mid | ||
} | ||
} | ||
|
||
if lo > n { | ||
lo -= n | ||
} | ||
if lo-1 < 0 || nums[lo-1] != target { | ||
return -1 | ||
} | ||
return lo - 1 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 시간복잡도가 O(N^2)이지 않을까 생각이 드네요
2중 for문에서 두번째 루프의 경우 nums[i] 가 n - j - 1이라면 순회 횟수를 무시할수 없을것같습니다
혹시 플린님 의견은 어떠실까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
희찬님 의견이 맞다고 생각합니다
시간 복잡도 수정해두겠습니다!
풀이 1도 시간복잡도 O(N)으로 최적화가 가능한데, 수정해서 맞춰놓겠습니다감사합니다 ㅎㅎ