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

[정현준] 10주차 #538

Merged
merged 3 commits into from
Oct 19, 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
79 changes: 79 additions & 0 deletions course-schedule/jdalma.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package leetcode_study

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class `course-schedule` {

/**
* TC: O(node + edge), SC: O(node + edge)
*/
fun canFinish(numCourses: Int, prerequisites: Array<IntArray>): Boolean {
if (prerequisites.isEmpty()) return true

return usingTopologySort(numCourses, prerequisites)
}

private fun usingTopologySort(numCourses: Int, prerequisites: Array<IntArray>): Boolean {
val adj = List(numCourses) { mutableListOf<Int>() }
val degree = IntArray(numCourses)
for (e in prerequisites) {
val (course, pre) = e[0] to e[1]
adj[pre].add(course)
degree[course]++
}

val queue = ArrayDeque<Int>().apply {
Copy link
Contributor

Choose a reason for hiding this comment

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

while문을 순회하며 로직을 순차적으로 진행하기 위해 queue특성을 활용한걸로 이해가 되요. 그런의미에서 dequeue를 사용하신 이유가 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

말씀하신대로 단순히 Queue의 특성을 이용하고 싶어서 사용하였는데 내부 구현의 차이점을 확실히 이해하고 있진 않지만 ArrayDeque가 LinkedList보다 빠르다는 말이 있어서 Queue를 사용할 때는 항상 ArrayDeque를 사용했습니다 ㅎㅎ
한번 구현을 확인해 볼 필요도 있겠네요 ㅎㅎ

이 클래스는 스택으로 사용할 경우 Stack 보다 빠르고, 큐로 사용할 경우 LinkedList 보다 빠를 가능성이 높습니다.
ArrayDeque

degree.forEachIndexed { index, i ->
if (i == 0) {
this.add(index)
}
}
}

var answer = 0
while (queue.isNotEmpty()) {
val now = queue.removeFirst()
answer++

queue.addAll(adj[now].filter { --degree[it] == 0 })
}

return answer == numCourses
}

@Test
fun `코스의 개수와 코스 간 의존성을 전달하면 코스를 완료할 수 있는지 여부를 반환한다`() {
canFinish(5,
arrayOf(
intArrayOf(0,1),
intArrayOf(0,2),
intArrayOf(1,3),
intArrayOf(1,4),
intArrayOf(3,4)
)
) shouldBe true
canFinish(5,
arrayOf(
intArrayOf(1,4),
intArrayOf(2,4),
intArrayOf(3,1),
intArrayOf(3,2)
)
) shouldBe true
canFinish(2, arrayOf(intArrayOf(1, 0))) shouldBe true
canFinish(2, arrayOf(intArrayOf(1, 0), intArrayOf(0, 1))) shouldBe false
canFinish(20,
arrayOf(
intArrayOf(0,10),
intArrayOf(3,18),
intArrayOf(5,5),
intArrayOf(6,11),
intArrayOf(11,14),
intArrayOf(13,1),
intArrayOf(15,1),
intArrayOf(17,4)
)
) shouldBe false
}
}
58 changes: 58 additions & 0 deletions invert-binary-tree/jdalma.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package leetcode_study

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class `invert-binary-tree` {

fun invertTree(root: TreeNode?): TreeNode? {
if (root == null) return null

return usingStack(root)
}

/**
* TC: O(n), SC: O(n)
*/
private fun usingDFS(node: TreeNode?): TreeNode? {
Copy link
Contributor

Choose a reason for hiding this comment

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

저는 inorder traversal 로 풀었는데 문제의 핵심인 왼쪽과 오른쪽을 바꾼다를 잘 해석하신거같아 좋은 풀이같아요 👍

if (node == null) return null

val (left, right) = node.left to node.right
node.left = usingDFS(right)
node.right = usingDFS(left)

return node
}

/**
* TC: O(n), SC: O(n)
*/
private fun usingStack(node: TreeNode): TreeNode {
val stack= ArrayDeque<TreeNode>().apply {
this.add(node)
}

while (stack.isNotEmpty()) {
val now = stack.removeLast()
val tmp = now.left
now.left = now.right
now.right = tmp

now.left?.let { stack.add(it) }
now.right?.let { stack.add(it) }
}
return node
}

@Test
fun `전달된 노드의 하위 노드들의 반전된 값을 반환한다`() {
val actual = TreeNode.of(4,2,7,1,3,6,9)
val expect = TreeNode.of(4,7,2,9,6,3,1)
invertTree(actual) shouldBe expect

val actual1 = TreeNode.of(1,2)
val expect1 = TreeNode.of(1,null,2)

invertTree(actual1) shouldBe expect1
}
}
54 changes: 54 additions & 0 deletions jump-game/jdalma.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package leetcode_study

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import kotlin.math.max

class `jump-game` {

fun canJump(nums: IntArray): Boolean {
return usingGreedy(nums)
}

/**
* TC: O(n), SC: O(1)
*/
private fun usingGreedy(nums: IntArray): Boolean {
var reachable = 0
for (index in nums.indices) {
if (index > reachable) return false
reachable = max(reachable, index + nums[index])
}

return true
}

/**
* TC: O(n^2), SC: O(n)
*/
private fun usingMemoization(nums: IntArray): Boolean {
val memo = IntArray(nums.size) { -1 }
fun dfs(now: Int): Boolean {
if (now >= nums.size - 1) {
return true
} else if (memo[now] != -1) {
return memo[now] != 0
}
for (next in 1 .. nums[now]) {
if (dfs(now + next)) {
memo[now] = 1
return true
}
}
memo[now] = 0
return false
}
return dfs(0)
}

@Test
fun `첫 번째 인덱스에서 마지막 인덱스에 도달할 수 있는지 여부를 반환한다`() {
canJump(intArrayOf(2,3,1,1,4)) shouldBe true
canJump(intArrayOf(3,2,1,0,4)) shouldBe false
}
}
130 changes: 130 additions & 0 deletions merge-k-sorted-lists/jdalma.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package leetcode_study

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import java.lang.RuntimeException

class `merge-k-sorted-lists` {

data class ListNode(var `val`: Int) {
var next: ListNode? = null

companion object {
fun of(vararg `val`: Int): ListNode {
val dummy = ListNode(-1)
var prev = dummy
for (v in `val`) {
prev.next = ListNode(v)
prev = prev.next ?: throw RuntimeException()
}
return dummy.next ?: throw RuntimeException()
}
}
}

fun mergeKLists(lists: Array<ListNode?>): ListNode? {
return if (lists.isEmpty()) null
else if (lists.size == 1) lists.first()
else mergeDivideAndConquer(lists)
}

/**
* TC: O(lists.size * ListNode.size), SC: O(1)
*/
private fun usingBruteForce(lists: Array<ListNode?>): ListNode? {
val dummy = ListNode(-1)
var prev = dummy

while (true) {
var minNode: ListNode? = null
var minIndex = -1

for (index in lists.indices) {
val curr = lists[index] ?: continue
if (minNode == null || curr.`val` < minNode.`val`) {
minNode = curr
minIndex = index
}
}
prev.next = minNode ?: break
prev = prev.next ?: throw RuntimeException()

lists[minIndex] = minNode.next
}

return dummy.next
}

/**
* TC: O(lists.size * ListNode.size), SC: O(1)
*/
private fun mergeLists(lists: Array<ListNode?>): ListNode? {
fun merge(node1: ListNode?, node2: ListNode?): ListNode? {
val dummy = ListNode(-1)
var prev = dummy
var (n1, n2) = node1 to node2
while (n1 != null && n2 != null) {
if (n1.`val` < n2.`val`) {
prev.next = n1
n1 = n1.next
} else {
prev.next = n2
n2 = n2.next
}
prev.next?.let { prev = it }
}
prev.next = n1 ?: n2
return dummy.next
}
for (index in 1 until lists.size) {
lists[0] = merge(lists[0], lists[index])
}
return lists[0]
}

/**
* TC: O(lists.size * ListNode.size), SC: O(lists.size)
Copy link
Contributor

Choose a reason for hiding this comment

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

분할정복 방식에서 시간 복잡도는 O( log(lists.size) * listNode.size))가 걸리지 않을까요?
공간복잡도도 O( log(lists.size) )가 아닐까 싶습니다!

Copy link
Member Author

@jdalma jdalma Oct 19, 2024

Choose a reason for hiding this comment

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

흠 말씀해주셔서 다시 생각해보니 SC는 콜 스택 깊이가 절반으로 줄어들어서 log로 볼 수 있을 것 같네요
TC는 mergeLists풀이와 merge()를 호출하는 방법만 달라지고 노드 비교 횟수는 동일하다고 생각되어서 log가 아닌 것 같다고 생각했습니다 ㅎㅎ

*/
private fun mergeDivideAndConquer(lists: Array<ListNode?>): ListNode? {
fun merge(node1: ListNode?, node2: ListNode?): ListNode? {
val dummy = ListNode(-1)
var prev = dummy
var (n1, n2) = node1 to node2
while (n1 != null && n2 != null) {
if (n1.`val` < n2.`val`) {
prev.next = n1
n1 = n1.next
} else {
prev.next = n2
n2 = n2.next
}
prev.next?.let { prev = it }
}
prev.next = n1 ?: n2
return dummy.next
}

fun divideAndConquer(lists: Array<ListNode?>, s: Int, e: Int): ListNode? {
if (s > e) return null
else if (s == e) return lists[s]

val mid = (s + e) / 2
val left = divideAndConquer(lists, s, mid)
val right = divideAndConquer(lists, mid + 1, e)
return merge(left, right)
}

return divideAndConquer(lists, 0, lists.size - 1)
}

@Test
fun `전달받은 노드들을 정렬하고 병합된 결과를 반환한다`() {
mergeKLists(
arrayOf(
ListNode.of(1,4,5),
ListNode.of(1,3,4),
ListNode.of(2,6)
)
) shouldBe ListNode.of(1,1,2,3,4,4,5,6)
}
}
50 changes: 50 additions & 0 deletions search-in-rotated-sorted-array/jdalma.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package leetcode_study

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class `search-in-rotated-sorted-array` {

/**
* O(log n)만큼의 시간만으로 해결해야 한다.
* nums 배열은 정렬되어있지만 순환된 배열이므로 target 을 찾기 위한 일반적인 이분 탐색으로는 해결할 수 없다.
* TC: O(log n), SC: O(1)
*/
fun search(nums: IntArray, target: Int): Int {
var (low, high) = 0 to nums.size - 1

while (low + 1 < high) {
val mid = (low + high) / 2

if (nums[mid] == target) {
return mid
}
if (nums[low] <= nums[mid]) {
if (target in nums[low] .. nums[mid]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

코틀린을 모르는 입장에서 이부분이 순회처럼 보이는데 단순 비교라서 O(1)라는게 신기하네요

Copy link
Member Author

@jdalma jdalma Oct 19, 2024

Choose a reason for hiding this comment

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

저도 순회하는 것 처럼 보이길래 자바로 디컴파일해보니 조건문으로 적용되더라구요 ㅎㅎ

 int var10000;
 int var10002;
 if (nums[low] <= nums[mid]) {
    var10000 = nums[low];
    var10002 = nums[mid];
    if (var10000 <= target) {
       if (var10002 >= target) {
          high = mid;
          continue;
       }
    }

    low = mid;
 } else {
    var10000 = nums[mid];
    var10002 = nums[high];
    if (var10000 <= target) {
       if (var10002 >= target) {
          low = mid;
          continue;
       }
    }

    high = mid;
 }

high = mid
} else {
low = mid
}
} else {
if (target in nums[mid] .. nums[high]) {
low = mid
} else {
high = mid
}
}
}
return when (target) {
nums[low] -> low
nums[high] -> high
else -> -1
}
}

@Test
fun `배열에서 타겟의 인덱스를 반환한다`() {
search(intArrayOf(4,5,6,7,0,1,2), 0) shouldBe 4
search(intArrayOf(4,5,6,7,0,1,2), 3) shouldBe -1
search(intArrayOf(2,3,4,5,6,0,1), 1) shouldBe 6
search(intArrayOf(1,2,3,4,5,6,7), 6) shouldBe 5
}
}