diff --git a/course-schedule/flynn.go b/course-schedule/flynn.go new file mode 100644 index 000000000..b48da908d --- /dev/null +++ b/course-schedule/flynn.go @@ -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 +} diff --git a/invert-binary-tree/flynn.go b/invert-binary-tree/flynn.go new file mode 100644 index 000000000..4053e1a3c --- /dev/null +++ b/invert-binary-tree/flynn.go @@ -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 +} diff --git a/jump-game/flynn.go b/jump-game/flynn.go new file mode 100644 index 000000000..678d930d5 --- /dev/null +++ b/jump-game/flynn.go @@ -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 +} diff --git a/merge-k-sorted-lists/flynn.go b/merge-k-sorted-lists/flynn.go new file mode 100644 index 000000000..53682bd1f --- /dev/null +++ b/merge-k-sorted-lists/flynn.go @@ -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)입니다 + - 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) + - 각 계층마다 우리는 모든 노드를 최대 한 번씩 조회합니다 -> 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 +} diff --git a/search-in-rotated-sorted-array/flynn.go b/search-in-rotated-sorted-array/flynn.go new file mode 100644 index 000000000..4ea96dcb8 --- /dev/null +++ b/search-in-rotated-sorted-array/flynn.go @@ -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 +}