diff --git a/climbing-stairs/jdalma.kt b/climbing-stairs/jdalma.kt new file mode 100644 index 00000000..83de8593 --- /dev/null +++ b/climbing-stairs/jdalma.kt @@ -0,0 +1,32 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `climbing-stairs` { + + /** + * 1. bottom-up 방식으로 가능한 경우의 수를 누적한다. + * TC: O(n), SC: O(n) + */ + fun climbStairs(n: Int): Int { + val dp = IntArray(n + 1).apply { + this[1] = 1 + if (n >= 2) this[2] = 2 + } + + for (num in 3 .. n) { + dp[num] = dp[num - 1] + dp[num - 2] + } + + return dp[n] + } + + @Test + fun `입력받은 목푯값에 1과 2만 더하여 도달할 수 있는 경우의 수를 반환한다`() { + climbStairs(1) shouldBe 1 + climbStairs(2) shouldBe 2 + climbStairs(3) shouldBe 3 + climbStairs(4) shouldBe 5 + } +} diff --git a/coin-change/jdalma.kt b/coin-change/jdalma.kt new file mode 100644 index 00000000..2ff7e706 --- /dev/null +++ b/coin-change/jdalma.kt @@ -0,0 +1,122 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import java.util.ArrayDeque +import kotlin.math.min + +class `coin-change` { + + fun coinChange(coins: IntArray, amount: Int): Int { + return topDown(coins, amount) + } + + /** + * 1. 모든 코인을 반복적으로 사용하면서 0원에 도달하면 그 때의 step 중 최솟값을 반환한다. + * TC: O(coins^amount), SC: O(amount) + */ + private fun bruteForce(coins: IntArray, amount: Int): Int { + fun dfs(coins: IntArray, remain: Int, step: Int): Int = + if (remain < 0) Int.MAX_VALUE + else if (remain == 0) step + else coins.map { dfs(coins, remain - it, step + 1) }.min() + + val result = dfs(coins, amount, 0) + return if (result == Int.MAX_VALUE) -1 else result + } + + /** + * 2. 모든 코인들을 종류별로 누적하면서 가장 빨리 목푯값에 도달하는 코인의 수를 반환한다. + * TC: O(amount * coins), SC: O(amount) + */ + private fun bfs(coins: IntArray, amount: Int): Int { + if (amount == 0) return 0 + + val visited = BooleanArray(amount + 1) { false } + val queue = ArrayDeque().apply { + this.offer(0) + } + + var coinCount = 0 + while (queue.isNotEmpty()) { + var size = queue.size + while (size-- > 0) { + val sum = queue.poll() + if (sum == amount) return coinCount + else if (sum < amount && !visited[sum] ) { + visited[sum] = true + for (coin in coins) { + queue.offer(sum + coin) + } + } + } + coinCount++ + } + return -1 + } + + /** + * 3. 반복되는 연산을 DP를 활용하여 bottom-up으로 해결 + * 1원부터 목푯값까지 각 가치를 구성하기 위한 최소 코인을 누적하면서 목푯값을 구성하는 최소 코인 개수를 구하는 것이다. + * TC: O(amount*coins), SC: O(amount) + */ + private fun bottomUp(coins: IntArray, amount: Int): Int { + val dp = IntArray(amount + 1) { 10000 + 1 }.apply { + this[0] = 0 + } + + for (target in 1 .. amount) { + coins.forEach { coin -> + if (coin <= target) { + dp[target] = min(dp[target], dp[target - coin] + 1) + } + } + } + + return if (dp[amount] == 10001) -1 else dp[amount] + } + + /** + * 4. 목표금액부터 코인만큼 차감하여 0원에 도달하면 백트래킹으로 DP 배열을 갱신한다 + * TC: O(amount * coins), SC: O(amount) + */ + private fun topDown(coins: IntArray, amount: Int): Int { + fun recursive(coins: IntArray, remain: Int, dp: IntArray): Int { + if (remain == 0) return 0 + else if (remain < 0) return -1 + else if (dp[remain] != 10001) return dp[remain] + + var minCoins = 10001 + coins.forEach { coin -> + val result = recursive(coins, remain - coin, dp) + if (result in 0 until minCoins) { + minCoins = result + 1 + } + } + dp[remain] = if (minCoins == 10001) -1 else minCoins + return dp[remain] + } + + if (amount < 1) return 0 + return recursive(coins, amount, IntArray(amount + 1) { 10000 + 1 }) + } + + @Test + fun `코인의 종류와 목표값을 입력하면 목푯값을 구성하는 코인의 최소 개수를 반환한다`() { + coinChange(intArrayOf(1,2,5), 11) shouldBe 3 + coinChange(intArrayOf(1,3,5), 15) shouldBe 3 + coinChange(intArrayOf(5,3,1), 15) shouldBe 3 + coinChange(intArrayOf(1,2), 4) shouldBe 2 + coinChange(intArrayOf(2,5,10,1), 27) shouldBe 4 + } + + @Test + fun `코인의 종류로 목표값을 완성할 수 없다면 -1을 반환한다`() { + coinChange(intArrayOf(2), 3) shouldBe -1 + } + + @Test + fun `목푯값이 0이라면 0을 반환한다`() { + coinChange(intArrayOf(1,2,3), 0) shouldBe 0 + } +} diff --git a/combination-sum/jdalma.kt b/combination-sum/jdalma.kt new file mode 100644 index 00000000..e15afcb5 --- /dev/null +++ b/combination-sum/jdalma.kt @@ -0,0 +1,46 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `combination-sum` { + + /** + * 후보자를 중복으로 사용할 수 있기에 0부터 후보자들을 누적하면서 target보다 크면 탈출하는 방식이다. + * 만약 target과 동일하다면 누적할 때 사용된 후보자들을 numbers에 저장해뒀기에 결과에 복사한다. + * 시간복잡도: O(n^t), 공간복잡도: O(t) + */ + fun combinationSum(candidates: IntArray, target: Int): List> { + + fun backtracking(candidates: IntArray, target: Int, result: MutableList>, numbers: MutableList, start: Int, total: Int) { + if (total > target) return + else if (total == target) { + result.add(numbers.toList()) + } else { + (start until candidates.size).forEach { + numbers.add(candidates[it]) + backtracking(candidates, target, result, numbers, it, total + candidates[it]) + numbers.removeLast() + } + } + } + + val result = mutableListOf>() + backtracking(candidates, target, result, mutableListOf(), 0, 0) + return result + } + + @Test + fun `입력받은 정수 리스트를 사용하여 목푯값을 만들어낼 수 있는 모든 경우를 리스트로 반환한다`() { + combinationSum(intArrayOf(2,3,6,7), 7) shouldBe listOf( + listOf(2,2,3), + listOf(7) + ) + combinationSum(intArrayOf(2,3,5), 8) shouldBe listOf( + listOf(2,2,2,2), + listOf(2,3,3), + listOf(3,5) + ) + combinationSum(intArrayOf(2), 1) shouldBe listOf() + } +} diff --git a/product-of-array-except-self/jdalma.kt b/product-of-array-except-self/jdalma.kt new file mode 100644 index 00000000..0f182486 --- /dev/null +++ b/product-of-array-except-self/jdalma.kt @@ -0,0 +1,38 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `product-of-array-except-self` { + + fun productExceptSelf(nums: IntArray): IntArray { + return usingPrefixSum(nums) + } + + /** + * 각 인덱스의 전,후를 누적곱을 합하여 이전 연산 결과를 재활용한다. + * 시간복잡도: O(n), 공간복잡도: O(n) + */ + private fun usingPrefixSum(nums: IntArray): IntArray { + val toRight = Array(nums.size) { 1 } + val toLeft = Array(nums.size) { 1 } + + (0 until nums.size - 1).forEach { + toRight[it + 1] = toRight[it] * nums[it] + } + (nums.size - 1 downTo 1).forEach { + toLeft[it - 1] = toLeft[it] * nums[it] + } + + return nums.indices + .map { toRight[it] * toLeft[it] } + .toIntArray() + } + + @Test + fun `입력받은 배열을 순회하며 자기 자신을 제외한 나머지 원소들의 곱한 값을 배열로 반환한다`() { + productExceptSelf(intArrayOf(2,3,4,5)) shouldBe intArrayOf(60,40,30,24) + productExceptSelf(intArrayOf(1,2,3,4)) shouldBe intArrayOf(24,12,8,6) + productExceptSelf(intArrayOf(-1,1,0,-3,3)) shouldBe intArrayOf(0,0,9,0,0) + } +} diff --git a/two-sum/jdalma.kt b/two-sum/jdalma.kt new file mode 100644 index 00000000..c60e992d --- /dev/null +++ b/two-sum/jdalma.kt @@ -0,0 +1,56 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `two-sum` { + + fun twoSum(nums: IntArray, target: Int): IntArray { + return usingMapOptimized(nums, target) + } + + /** + * 1. map을 활용 + * TC: O(n), SC: O(n) + */ + private fun usingMap(nums: IntArray, target: Int): IntArray { + val map = nums.withIndex().associate { it.value to it.index } + + nums.forEachIndexed { i, e -> + val diff: Int = target - e + if (map.containsKey(diff) && map[diff] != i) { + return map[diff]?.let { + intArrayOf(it, i) + } ?: intArrayOf() + } + } + return intArrayOf() + } + + /** + * 2. map에 모든 값을 초기화할 필요가 없기에, nums를 순회하며 확인한다. + * TC: O(n), SC: O(n) + */ + private fun usingMapOptimized(nums: IntArray, target: Int): IntArray { + val map = mutableMapOf() + + for (index in nums.indices) { + val diff = target - nums[index] + if (map.containsKey(diff)) { + return map[diff]?.let { + intArrayOf(it, index) + } ?: intArrayOf() + } + map[nums[index]] = index + } + + return intArrayOf() + } + + @Test + fun `정수 배열과 목푯값을 입력받아 목푯값을 만들 수 있는 정수 배열의 원소들 중 두 개의 원소의 인덱스를 반환한다`() { + twoSum(intArrayOf(2,7,11,15), 9) shouldBe intArrayOf(0,1) + twoSum(intArrayOf(3,2,4), 6) shouldBe intArrayOf(1,2) + twoSum(intArrayOf(3,3), 6) shouldBe intArrayOf(0,1) + } +}