Skip to content

Commit f52334c

Browse files
authored
Merge pull request DaleStudy#533 from obzva/main
[Flynn] Week 10
2 parents 66cd1dd + 8c9dd17 commit f52334c

File tree

5 files changed

+386
-0
lines changed

5 files changed

+386
-0
lines changed

course-schedule/flynn.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
풀이
3+
- 각 course를 node로 생각하고, prerequisite 관계에 있는 node를 간선으로 이어주면 단방향 간선으로 연결된 node 집합의 graph를 떠올릴 수 있습니다
4+
- 이 문제는 위에서 설명한 graph에 loop가 있냐 없느냐를 판단하는 문제입니다
5+
- 함수의 재귀호출 및 백트래킹을 이용해서 풀이할 수 있습니다
6+
7+
Big O
8+
- N: 과목 수 (node의 개수)
9+
- M: 배열 prerequisites의 길이 (간선의 개수)
10+
- Time compleixty: O(N + M)
11+
- prereqMap을 초기화 -> O(M)
12+
- 함수의 재귀호출을 통해 우리는 각 node를 최대 한번씩 조회합니다 -> O(N)
13+
- Space complexity: O(N + M)
14+
- prereqMap -> O(M)
15+
- checkingSet, checkedSet -> O(N)
16+
*/
17+
18+
func canFinish(numCourses int, prerequisites [][]int) bool {
19+
// 주어진 prerequisites 배열로 `a_i: b_0, b_1, ...` 형태의 맵을 짭니다
20+
prereqMap := make(map[int][]int, numCourses)
21+
for _, pair := range prerequisites {
22+
prereqMap[pair[0]] = append(prereqMap[pair[0]], pair[1])
23+
}
24+
25+
// checkingSet으로 현재 탐색하고 있는 구간에 loop가 생겼는지 여부를 판단하고, checkedSet으로 이미 탐색한 node인지를 판단합니다
26+
checkingSet, checkedSet := make(map[int]bool, 0), make(map[int]bool, 0)
27+
28+
// 특정 과목 c를 듣기 위한 선행 과목들을 탐색했을 때 loop가 생기는지 여부를 판단하는 함수입니다
29+
// (Go 언어 특성상 L:20-21 처럼 함수를 선언합니다 (함수 내부에서 함수를 선언할 땐 익명 함수를 사용 + 해당 함수를 재귀호출하기 위해서 선언과 초기화를 분리))
30+
var checkLoop func(int) bool // loop가 있다면 true
31+
checkLoop = func(c int) bool {
32+
// 과목 c가 현재 탐색하고 있는 구간에 존재한다면 loop가 있다고 판단 내릴 수 있습니다
33+
_, checkingOk := checkingSet[c]
34+
if checkingOk {
35+
return true
36+
}
37+
// 과목 c가 이미 탐색이 완료된 과목이라면 과목 c를 지나는 하위 구간에는 loop가 없다고 판단할 수 있습니다
38+
_, checkedOk := checkedSet[c]
39+
if checkedOk {
40+
return false
41+
}
42+
// 과목 c를 checkingSet에 추가합니다
43+
// 만약 하위 구간에서 과목 c를 다시 만난다면 loop가 있다고 판단할 수 있습니다
44+
checkingSet[c] = true
45+
// 각 선행과목 별로 하위구간을 만들어 탐색을 진행합니다
46+
// 하위구간 중 하나라도 loop가 발생하면 현재 구간에는 loop가 있다고 판단할 수 있습니다
47+
for _, prereq := range prereqMap[c] {
48+
if checkLoop(prereq) {
49+
return true
50+
}
51+
}
52+
// 만약 loop가 발견되지 않았다면 checkedSet에 과목 c를 추가함으로써 과목 c를 지나는 구간이 안전하다고 표시합니다
53+
checkedSet[c] = true
54+
// checkingSet에서 과목 c를 지워줍니다
55+
delete(checkingSet, c)
56+
return false
57+
}
58+
59+
for i := 0; i < numCourses; i++ {
60+
if checkLoop(i) {
61+
return false
62+
}
63+
}
64+
return true
65+
}

invert-binary-tree/flynn.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
풀이 1
3+
- 함수의 재귀호출을 이용해서 풀이할 수 있습니다
4+
5+
Big O
6+
- N: 노드의 개수
7+
- H: 트리의 높이
8+
- Time complexity: O(N)
9+
- Space complexity: O(H) (logN <= H <= N)
10+
- 재귀 호출 스택의 최대 깊이는 트리의 높이에 비례하여 증가합니다
11+
*/
12+
13+
/**
14+
* Definition for a binary tree node.
15+
* type TreeNode struct {
16+
* Val int
17+
* Left *TreeNode
18+
* Right *TreeNode
19+
* }
20+
*/
21+
func invertTree(root *TreeNode) *TreeNode {
22+
if root == nil {
23+
return root
24+
}
25+
26+
tmp := invertTree(root.Right)
27+
root.Right = invertTree(root.Left)
28+
root.Left = tmp
29+
30+
return root
31+
}
32+
33+
/*
34+
풀이 2
35+
- 큐와 반복문을 이용하여 풀이할 수 있습니다
36+
37+
Big O
38+
- N: 노드의 개수
39+
- Time complexity: O(N)
40+
- Space complexity: O(N)
41+
- 큐의 최대 크기는 N / 2 를 넘지 않습니다
42+
큐의 최대 크기는 트리의 모든 층 중에서 가장 폭이 큰 층의 노드 수와 같습니다
43+
높이가 H인 트리의 최대 폭은 1. balanced tree일 때 2. 맨 아랫 층의 폭이고 이 때의 폭 W는 2^(H-1) 입니다
44+
높이가 H인 balanced tree의 노드 개수는 2^H - 1 = N 이므로 아래 관계가 성립합니다
45+
N/2 = (2^H - 1) / 2 = 2^(H-1) - 1/2 >= 2^(H-1) = W
46+
따라서 공간 복잡도는 O(N/2) = O(N) 입니다
47+
*/
48+
49+
/**
50+
* Definition for a binary tree node.
51+
* type TreeNode struct {
52+
* Val int
53+
* Left *TreeNode
54+
* Right *TreeNode
55+
* }
56+
*/
57+
func invertTree(root *TreeNode) *TreeNode {
58+
queue := make([]*TreeNode, 0)
59+
queue = append(queue, root)
60+
61+
for len(queue) > 0 {
62+
node := queue[0]
63+
queue = queue[1:]
64+
65+
if node == nil {
66+
continue
67+
}
68+
69+
tmp := node.Left
70+
node.Left = node.Right
71+
node.Right = tmp
72+
73+
queue = append(queue, node.Left, node.Right)
74+
}
75+
76+
return root
77+
}

jump-game/flynn.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
풀이 1
3+
- memo 배열을 이용하여 n-1번째 인덱스부터 0번째 인덱스 방향으로 탐색하여 풀이할 수 있습니다
4+
memo[i] = i번째 인덱스에서 출발했을 때 마지막 인덱스에 도달할 수 있는지 여부
5+
6+
Big O
7+
- N: 주어진 배열 nums의 길이
8+
- Time complexity: O(N)
9+
- Space complexity: O(N)
10+
- 풀이 2를 이용하면 O(1)으로 최적화할 수 있습니다
11+
*/
12+
13+
func canJump(nums []int) bool {
14+
n := len(nums)
15+
16+
if nums[0] == 0 && n > 1 {
17+
return false
18+
}
19+
20+
memo := make([]bool, n)
21+
memo[n-1] = true
22+
23+
for i := n - 2; i >= 0; i-- {
24+
for j := 1; j <= nums[i]; j++ {
25+
if i+j >= n {
26+
break
27+
}
28+
if memo[i+j] {
29+
memo[i] = true
30+
break
31+
}
32+
}
33+
}
34+
35+
return memo[0]
36+
}
37+
38+
/*
39+
풀이
40+
- 풀이 1을 잘 관찰하면 memo배열의 모든 값을 가지고 있을 필요가 없다는 걸 알 수 있습니다
41+
memo 배열 대신에, 문제의 조건대로 마지막 인덱스까지 갈 수 있는 가장 좌측의 인덱스만 기록합니다 (leftmost)
42+
43+
Big O
44+
- N: 주어진 배열 nums의 길이
45+
- Time complexity: O(N)
46+
- Space complexity: O(1)
47+
*/
48+
49+
func canJump(nums []int) bool {
50+
n := len(nums)
51+
52+
if nums[0] == 0 && n > 1 {
53+
return false
54+
}
55+
56+
leftmost := n - 1
57+
58+
for i := n - 2; i >= 0; i-- {
59+
if i+nums[i] >= leftmost {
60+
leftmost = i
61+
}
62+
}
63+
64+
return leftmost == 0
65+
}

merge-k-sorted-lists/flynn.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
풀이 1
3+
- lists를 순회하면서 순서대로 링크드리스트 두 개를 짝 지어 병합하는 방식으로 풀이할 수 있습니다
4+
5+
Big O
6+
- K: 배열 lists의 길이
7+
- N: 모든 링크드리스트의 노드 개수의 합
8+
- n_i: i번 인덱스 링크드리스트의 노드의 개수
9+
- Time complexity: O(KN)
10+
- K-1 번의 병합을 진행합니다
11+
- i번째 병합 때, 병합하는 두 링크드리스트는 각각 𝚺(n_(i-1)), n_i입니다
12+
이 때 𝚺(n_(i-1))의 상한을 고려한다면 두 링크드리스트의 병합에 걸리는 시간복잡도는 O(N)입니다
13+
- O((K-1)N) = O(KN)
14+
- 풀이 2로 시간복잡도를 O((logK)N)으로 최적화할 수 있습니다
15+
- Space complexity: O(1)
16+
- res, dummy, curr 등의 추가적인 포인터를 생성하긴 하지만 기존에 주어져 있던 ListNode의 Next만 조작하므로 K, N과 상관 없이 공간복잡도는 상수값을 가집니다
17+
*/
18+
19+
/**
20+
* Definition for singly-linked list.
21+
* type ListNode struct {
22+
* Val int
23+
* Next *ListNode
24+
* }
25+
*/
26+
func mergeKLists(lists []*ListNode) *ListNode {
27+
n := len(lists)
28+
29+
if n == 0 {
30+
return nil
31+
}
32+
33+
res := lists[0]
34+
for i := 1; i < n; i++ {
35+
res = mergeTwoLists(res, lists[i])
36+
}
37+
return res
38+
}
39+
40+
func mergeTwoLists(first *ListNode, second *ListNode) *ListNode {
41+
dummy := &ListNode{}
42+
curr := dummy
43+
44+
for first != nil && second != nil {
45+
if first.Val < second.Val {
46+
curr.Next = first
47+
first = first.Next
48+
} else {
49+
curr.Next = second
50+
second = second.Next
51+
}
52+
curr = curr.Next
53+
}
54+
55+
if first != nil {
56+
curr.Next = first
57+
}
58+
if second != nil {
59+
curr.Next = second
60+
}
61+
62+
return dummy.Next
63+
}
64+
65+
66+
/*
67+
풀이 2
68+
- Divide and Conquer 방식으로 시간복잡도를 최적화할 수 있습니다
69+
- 하지만 공간복잡도 측면에서는 trade-off가 있습니다
70+
71+
Big O
72+
- K: 배열 lists의 길이
73+
- N: 모든 링크드리스트의 노드 개수의 합
74+
- Time complexity: O((logK)N)
75+
- lists를 반으로 쪼개 가면서 재귀호출을 진행하므로 재귀호출은 logK 레벨에 걸쳐 이루어집니다 -> O(logK)
76+
- 각 계층마다 우리는 모든 노드를 최대 한 번씩 조회합니다 -> O(N)
77+
- Space complexity: O(logK)
78+
- 풀이 1과 비슷하지만 재귀호출 스택을 고려해야 합니다
79+
*/
80+
81+
/**
82+
* Definition for singly-linked list.
83+
* type ListNode struct {
84+
* Val int
85+
* Next *ListNode
86+
* }
87+
*/
88+
func mergeKLists(lists []*ListNode) *ListNode {
89+
n := len(lists)
90+
91+
if n == 0 {
92+
return nil
93+
}
94+
if n == 1 {
95+
return lists[0]
96+
}
97+
98+
left := mergeKLists(lists[:n/2])
99+
right := mergeKLists(lists[n/2:])
100+
101+
return mergeTwoLists(left, right)
102+
}
103+
104+
func mergeTwoLists(first *ListNode, second *ListNode) *ListNode {
105+
dummy := &ListNode{}
106+
curr := dummy
107+
108+
for first != nil && second != nil {
109+
if first.Val < second.Val {
110+
curr.Next = first
111+
first = first.Next
112+
} else {
113+
curr.Next = second
114+
second = second.Next
115+
}
116+
curr = curr.Next
117+
}
118+
119+
if first != nil {
120+
curr.Next = first
121+
}
122+
if second != nil {
123+
curr.Next = second
124+
}
125+
126+
return dummy.Next
127+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
풀이
3+
- 이진탐색을 두 번 사용하여 풀이할 수 있습니다
4+
첫번째, pivot index를 찾습니다
5+
두번째, target을 찾습니다
6+
7+
Big O
8+
- N: 주어진 배열 nums의 길이
9+
- Time complexity: O(logN)
10+
- 각 이진탐색이 모두 O(logN)의 시간 복잡도를 가집니다
11+
- Space complexity: O(1)
12+
*/
13+
14+
func search(nums []int, target int) int {
15+
n := len(nums)
16+
17+
lo, hi := 0, n
18+
for lo < hi {
19+
mid := lo + (hi-lo)/2
20+
if nums[mid] > nums[n-1] {
21+
lo = mid + 1
22+
} else {
23+
hi = mid
24+
}
25+
}
26+
pivot := lo
27+
if pivot == n {
28+
pivot = 0
29+
}
30+
31+
lo, hi = pivot, pivot+n
32+
for lo < hi {
33+
mid := lo + (hi-lo)/2
34+
normalizedMid := mid
35+
if normalizedMid >= n {
36+
normalizedMid = mid - n
37+
}
38+
if nums[normalizedMid] <= target {
39+
lo = mid + 1
40+
} else {
41+
hi = mid
42+
}
43+
}
44+
45+
if lo > n {
46+
lo -= n
47+
}
48+
if lo-1 < 0 || nums[lo-1] != target {
49+
return -1
50+
}
51+
return lo - 1
52+
}

0 commit comments

Comments
 (0)