diff --git a/3sum/JisooPyo.kt b/3sum/JisooPyo.kt new file mode 100644 index 000000000..08cdf6825 --- /dev/null +++ b/3sum/JisooPyo.kt @@ -0,0 +1,154 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +/** + * Leetcode + * 15. 3Sum + * Medium + */ +class `3Sum` { + /** + * 3중 for문으로 풀어봤는데 시간 초과가 되더라고요(당연) + * 사실 잘 모르겠어서 Topic과 힌트를 살짝 봤는데 투 포인터가 있길래 이걸 이용해서 풀어보기로 했습니다! + * + * Runtime: 72 ms(Beats: 60.54 %) + * Time Complexity: O(n^2) + * + * Memory: 56.28 MB(Beats: 49.51 %) + * Space Complexity: O(n^2) + */ + fun threeSum(nums: IntArray): List> { + val answer = mutableListOf>() + // 배열 정렬 - 중복된 경우를 제거하고 효율적으로 탐색하기 위하여 + nums.sort() + + // nums[i]의 이전 값을 의미합니다. + var prev = Integer.MIN_VALUE + for (i in nums.indices) { + // 이전 값과 동일한 값이라면 스킵하여 중복된 경우를 제거합니다. + if (nums[i] == prev) { + continue + } + + // 투 포인터 알고리즘을 이용하여 다 더하여 0이 되는 경우의 수를 찾습니다. + var left = i + 1 + var right = nums.size - 1 + while (left < right) { + + if (nums[i] + nums[left] + nums[right] > 0) { // 합이 0보다 크다면 right를 줄입니다. + // if 내에 있는 while문들은 중복 경우의 수를 피하기 위함입니다. + while (0 <= right - 1 && nums[right - 1] == nums[right]) { + right-- + } + right-- + } else if (nums[i] + nums[left] + nums[right] < 0) { // 합이 0보다 적다면 left를 높입니다. + while (left + 1 <= nums.size - 1 && nums[left] == nums[left + 1]) { + left++ + } + left++ + } else { // 합이 0이라면 경우의 수를 추가합니다. + answer.add(listOf(nums[i], nums[left], nums[right])) + while (left + 1 <= nums.size - 1 && nums[left] == nums[left + 1]) { + left++ + } + left++ + while (0 <= right - 1 && nums[right - 1] == nums[right]) { + right-- + } + right-- + } + } + prev = nums[i] + } + return answer + } + + /** + * 시간 복잡도나 공간 복잡도가 개선되진 않았지만 가독성 측면에서 개선해본 버전입니다. + * Runtime: 66 ms(Beats: 65.59 %) + * Time Complexity: O(n^2) + * + * Memory: 56.64 MB(Beats: 43.12 %) + * Space Complexity: O(n^2) + */ + fun threeSum2(nums: IntArray): List> { + val answer = mutableListOf>() + nums.sort() + + // 첫 세 수의 합이 0보다 크거나, 마지막 세 수의 합이 0보다 작으면 불가능 + val lastIndex = nums.size - 1 + if (nums[0] + nums[1] + nums[2] > 0 || + nums[lastIndex] + nums[lastIndex - 1] + nums[lastIndex - 2] < 0 + ) { + return emptyList() + } + + var prev = nums[0] - 1 + for (i in nums.indices) { + // 조기 종료 조건 추가 + if (nums[i] > 0) { + break + } + if (nums[i] == prev) { + continue + } + var left = i + 1 + var right = nums.size - 1 + while (left < right) { + // 중복 로직 제거 및 sum 변수화 + val sum = nums[i] + nums[left] + nums[right] + when { + sum > 0 -> right = skipDuplicates(nums, right, false) + sum < 0 -> left = skipDuplicates(nums, left, true) + else -> { + answer.add(listOf(nums[i], nums[left], nums[right])) + left = skipDuplicates(nums, left, true) + right = skipDuplicates(nums, right, false) + } + } + } + prev = nums[i] + } + return answer + } + + private fun skipDuplicates(nums: IntArray, index: Int, isLeft: Boolean): Int { + var current = index + return if (isLeft) { + while (current + 1 < nums.size && nums[current] == nums[current + 1]) current++ + current + 1 + } else { + while (0 <= current - 1 && nums[current - 1] == nums[current]) current-- + current - 1 + } + } + + @Test + fun test() { + threeSum(intArrayOf(-1, 0, 1, 2, -1, -4)) shouldBe listOf( + listOf(-1, -1, 2), + listOf(-1, 0, 1) + ) + threeSum(intArrayOf(0, 1, 1)) shouldBe emptyList() + threeSum(intArrayOf(0, 0, 0)) shouldBe listOf( + listOf(0, 0, 0) + ) + threeSum(intArrayOf(-2, 0, 0, 2, 2)) shouldBe listOf( + listOf(-2, 0, 2) + ) + threeSum2(intArrayOf(-1, 0, 1, 2, -1, -4)) shouldBe listOf( + listOf(-1, -1, 2), + listOf(-1, 0, 1) + ) + threeSum2(intArrayOf(0, 1, 1)) shouldBe emptyList() + threeSum2(intArrayOf(0, 0, 0)) shouldBe listOf( + listOf(0, 0, 0) + ) + threeSum2(intArrayOf(-2, 0, 0, 2, 2)) shouldBe listOf( + listOf(-2, 0, 2) + ) + } + +} diff --git a/climbing-stairs/JisooPyo.kt b/climbing-stairs/JisooPyo.kt new file mode 100644 index 000000000..0ff6816e3 --- /dev/null +++ b/climbing-stairs/JisooPyo.kt @@ -0,0 +1,68 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +/** + * Leetcode + * 70. Climbing Stairs + * Easy + * + * 사용된 알고리즘: Dynamic Programming + * n개의 계단을 오르는 방법 = n-1개의 계단을 오르는 방법 수 + n-2개의 계단을 오르는 방법 + */ +class ClimbingStairs { + /** + * Runtime: 0 ms(Beats: 100.00 %) + * Time Complexity: O(n) + * - 배열 순회 + * + * Memory: 33.94 MB(Beats: 18.06 %) + * Space Complexity: O(n) + * - n+1 크기의 배열 사용 + */ + fun climbStairs1(n: Int): Int { + if (n == 1) return 1 + if (n == 2) return 2 + + val arr = IntArray(n + 1) + arr[1] = 1 + arr[2] = 2 + for (i in 3..n) { + arr[i] = arr[i - 1] + arr[i - 2] + } + return arr[n] + } + + /** + * 배열을 쓰지 않고 변수를 사용하여 공간 복잡도를 개선한 버전입니다. + * Runtime: 0 ms(Beats: 100.00 %) + * Time Complexity: O(n) + * - n번 순회 + * + * Memory: 34.06 MB(Beats: 15.90 %) + * Space Complexity: O(1) + * - 사용되는 추가 공간이 입력 크기와 무관하게 일정함 + */ + fun climbStairs2(n: Int): Int { + if (n == 1 || n == 2) return n + var firstCase = 1 + var secondCase = 2 + var totalSteps = 0 + + for (steps in 3..n) { + totalSteps = firstCase + secondCase + firstCase = secondCase + secondCase = totalSteps + } + return totalSteps + } + + @Test + fun test() { + climbStairs1(2) shouldBe 2 + climbStairs1(3) shouldBe 3 + climbStairs2(2) shouldBe 2 + climbStairs2(3) shouldBe 3 + } +} diff --git a/construct-binary-tree-from-preorder-and-inorder-traversal/JisooPyo.kt b/construct-binary-tree-from-preorder-and-inorder-traversal/JisooPyo.kt new file mode 100644 index 000000000..c8c22c066 --- /dev/null +++ b/construct-binary-tree-from-preorder-and-inorder-traversal/JisooPyo.kt @@ -0,0 +1,237 @@ +package leetcode_study + +import org.junit.jupiter.api.Test + +/** + * Leetcode + * 105. Construct Binary Tree from Preorder and Inorder Traversal + * Medium + * + * preorder, inorder, postorder는 트리 탐색 방식 + * preorder(전위): root -> left -> right 순으로 탐색. + * inorder(중위): left -> root -> right 순으로 탐색. + * postorder(후위): left -> right -> root 순으로 탐색. + */ +class ConstructBinaryTreeFromPreorderAndInorderTraversal { + /** + * Runtime: 38 ms(Beats: 25.41 %) + * Time Complexity: O(n^2) + * - 매 재귀 호출마다 for문이 진행 + * + * Memory: 86.98 MB(Beats: 9.77 %) + * Space Complexity: O(n) + * - sliceArray() 메서드로 새로운 배열 생성 + */ + fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? { + val root = TreeNode(preorder[0]) + if (preorder.size == 1) { + return root + } + var rootIndex = 0 + for (i in inorder.indices) { + if (inorder[i] == root.`val`) { + rootIndex = i + break + } + } + if (rootIndex != 0) { + root.left = buildTree(preorder.sliceArray(1..rootIndex), inorder.sliceArray(0 until rootIndex)) + } + if (rootIndex != inorder.lastIndex) { + root.right = buildTree( + preorder.sliceArray(rootIndex + 1..preorder.lastIndex), + inorder.sliceArray(rootIndex + 1..inorder.lastIndex) + ) + } + return root + } + + /** + * sliceArray로 배열을 만들어 내지 않고, index를 전달하여 공간 복잡도를 O(h)로 감소시킴 + * - O(h): 재귀 호출 스택에 필요한 공간 + * Runtime: 24 ms(Beats: 55.17 %) + * Time Complexity: O(n^2) + * - 모든 노드에 대해서 O(n)의 검색 + * + * Memory: 39.72 MB(Beats: 40.18 %) + * Space Complexity: O(h) + * - 균형 잡힌 트리의 경우: O(log n) + * - 편향 트리의 경우: O(n) + */ + fun buildTree2(preorder: IntArray, inorder: IntArray): TreeNode? { + return buildTreeWithIndex( + preorder, inorder, + preStart = 0, preEnd = preorder.lastIndex, + inStart = 0, inEnd = inorder.lastIndex + ) + } + + private fun buildTreeWithIndex( + preorder: IntArray, inorder: IntArray, + preStart: Int, preEnd: Int, + inStart: Int, inEnd: Int + ): TreeNode? { + // 유효하지 않은 범위인 경우 + if (preStart > preEnd || inStart > inEnd) { + return null + } + + // 루트 노드 생성 + val root = TreeNode(preorder[preStart]) + + // 단일 노드인 경우 + if (preStart == preEnd) { + return root + } + + // 중위 순회에서 루트의 위치 찾기 + var rootIndex = inStart + while (inorder[rootIndex] != root.`val`) { + rootIndex++ + } + + // 왼쪽 서브트리의 크기 + val leftSubtreeSize = rootIndex - inStart + + // 왼쪽 서브트리 구성 + root.left = buildTreeWithIndex( + preorder, inorder, + preStart = preStart + 1, + preEnd = preStart + leftSubtreeSize, + inStart = inStart, + inEnd = rootIndex - 1 + ) + + // 오른쪽 서브트리 구성 + root.right = buildTreeWithIndex( + preorder, inorder, + preStart = preStart + leftSubtreeSize + 1, + preEnd = preEnd, + inStart = rootIndex + 1, + inEnd = inEnd + ) + + return root + } + + /** + * 매 번 루트 값을 찾는 선형 검색을 O(1)로 만들기 위해 HashMap 사용 + * Runtime: 21 ms(Beats: 57.14 %) + * Time Complexity: O(n) + * + * Memory: 38.62 MB(Beats: 56.62 %) + * Space Complexity: O(n) + * - HashMap + */ + private lateinit var inorderMap: MutableMap + + fun buildTree3(preorder: IntArray, inorder: IntArray): TreeNode? { + // 중위 순회 값들의 인덱스를 HashMap에 저장 + inorderMap = mutableMapOf() + for (i in inorder.indices) { + inorderMap[inorder[i]] = i + } + + return buildTreeWithIndex2( + preorder, inorder, + preStart = 0, preEnd = preorder.lastIndex, + inStart = 0, inEnd = inorder.lastIndex + ) + } + + private fun buildTreeWithIndex2( + preorder: IntArray, inorder: IntArray, + preStart: Int, preEnd: Int, + inStart: Int, inEnd: Int + ): TreeNode? { + // 유효하지 않은 범위인 경우 + if (preStart > preEnd || inStart > inEnd) { + return null + } + + // 루트 노드 생성 + val root = TreeNode(preorder[preStart]) + + // 단일 노드인 경우 + if (preStart == preEnd) { + return root + } + + // HashMap을 사용하여 O(1)로 루트의 위치 찾기 + val rootIndex = inorderMap[root.`val`]!! + + // 왼쪽 서브트리의 크기 + val leftSubtreeSize = rootIndex - inStart + + // 왼쪽 서브트리 구성 + root.left = buildTreeWithIndex2( + preorder, inorder, + preStart = preStart + 1, + preEnd = preStart + leftSubtreeSize, + inStart = inStart, + inEnd = rootIndex - 1 + ) + + // 오른쪽 서브트리 구성 + root.right = buildTreeWithIndex2( + preorder, inorder, + preStart = preStart + leftSubtreeSize + 1, + preEnd = preEnd, + inStart = rootIndex + 1, + inEnd = inEnd + ) + + return root + } + + class TreeNode(var `val`: Int) { + var left: TreeNode? = null + var right: TreeNode? = null + } + + @Test + fun test() { + printTree(buildTree(intArrayOf(3, 9, 20, 15, 7), intArrayOf(9, 3, 15, 20, 7))!!) + } + + // preorder로 출력 + fun printTree(root: TreeNode) { + println(root.`val`) + if (root.left != null) { + printTree(root.left!!) + } + if (root.right != null) { + printTree(root.right!!) + } + } +} +/** + * 기타 + * + * Runtime이 1ms인 풀이. 아름다워서 공유합니당.. + * + * private var preIdx = 0 + * private var inIdx = 0 + * + * fun buildTree4(preorder: IntArray, inorder: IntArray): TreeNode? { + * return dfs(preorder, inorder, Int.MAX_VALUE) + * } + * + * fun dfs(preorder: IntArray, inorder: IntArray, limit: Int): TreeNode? { + * if (preIdx >= preorder.size) { + * return null + * } + * if (inorder[inIdx] == limit) { + * inIdx++ + * return null + * } + * + * val root = TreeNode(preorder[preIdx]) + * preIdx++ + * + * root.left = dfs(preorder, inorder, root.`val`) + * root.right = dfs(preorder, inorder, limit) + * + * return root + * } + */ diff --git a/decode-ways/JisooPyo.kt b/decode-ways/JisooPyo.kt new file mode 100644 index 000000000..896e38afd --- /dev/null +++ b/decode-ways/JisooPyo.kt @@ -0,0 +1,111 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +/** + * Leetcode + * 91. Decode Ways + * Medium + * + * idea: + * Dynamic Programming + * 숫자는 최소 한 자리에서 최대 두자리까지 해석될 수 있는 여지가 있다. + * 따라서 "1111"이라는 문자열이 가질 수 있는 Decoding 경우의 수는 다음 두 가지가 될 수 있다. + * - "111"이 가지는 Decoding 경우의수 + "1" + * - "11"이 가지는 Decoding 경우의 수 + "11" + * Note. + * "0"이 가지는 Decoding 경우의 수는 없다는 것. + * "1"부터 "26"까지만 디코딩 가능한 것. + */ +class DecodeWays { + /** + * Runtime: 17 ms(Beats: 11.98 %) + * Time Complexity: O(n) + * + * Memory: 38.81 MB(Beats: 6.08 %) + * Space Complexity: O(n) + * - s의 길이만큼의 DP 배열(dp) 필요 + */ + fun numDecodings(s: String): Int { + val dp = IntArray(s.length) + + // 초기항: dp[0] + dp[0] = if (s[0] == '0') 0 else 1 + if (s.length == 1) return dp[0] + + // 초기항: dp[1] + val oneDigit = dp[0] * (if (s[1] == '0') 0 else 1) + val twoDigitNumber = (s[0] - '0') * 10 + (s[1] - '0') + val twoDigit = if (twoDigitNumber < 10 || 26 < twoDigitNumber) 0 else 1 + dp[1] = oneDigit + twoDigit + + if (s.length == 2) return dp[1] + + // 나머지 항 계산 + for (i in 2 until s.length) { + // 한 자리 숫자로 해석하는 경우 + val oneDigit = dp[i - 1] * (if (s[i] == '0') 0 else 1) + + // 두 자리 숫자로 해석하는 경우 + val twoDigitNumber = (s[i - 1] - '0') * 10 + (s[i] - '0') + val twoDigit = dp[i - 2] * (if (twoDigitNumber < 10 || 26 < twoDigitNumber) 0 else 1) + + dp[i] = oneDigit + twoDigit + } + + return dp[dp.lastIndex] + } + + /** + * early return, 범위 연산자, 메서드를 활용하여 가독성 개선하기 + */ + fun numDecodings2(s: String): Int { + if (s[0] == '0') return 0 + if (s.length == 1) return 1 + + val dp = IntArray(s.length) + // 초기항: dp[0] + dp[0] = 1 + + // 초기항: dp[1] + val oneDigit = dp[0] * (if (s[1] == '0') 0 else 1) + val twoDigitNumber = (s[0] - '0') * 10 + (s[1] - '0') + val twoDigit = if (twoDigitNumber in 10..26) 1 else 0 + dp[1] = oneDigit + twoDigit + + if (s.length == 2) return dp[1] + + // 나머지 항 계산 + for (i in 2 until s.length) { + // 한 자리 숫자로 해석하는 경우 + if (s[i] != '0') { + dp[i] += dp[i - 1] + } + + // 두 자리 숫자로 해석하는 경우 + val twoDigitNumber = (s[i - 1] - '0') * 10 + (s[i] - '0') + if (twoDigitNumber in 10..26) { + dp[i] += dp[i - 2] + } + } + + return dp.last() + } + + @Test + fun test() { + numDecodings("12") shouldBe 2 + numDecodings("226") shouldBe 3 + numDecodings("06") shouldBe 0 + numDecodings("2101") shouldBe 1 + } +} +/** + * 개선할 점: + * 1) 공간복잡도 O(1)로 개선 + * 배열을 만들지 않고 "전의 경우의 수"와 "전전의 경우의 수"를 변수화하면 공간 복잡도가 O(1)로 감소될 수 있습니다. + * 2) 연산 수 줄이기 + * 다른 빠른 풀이(1ms)를 보니 두 자리 숫자로 해석하는 경우에 숫자로 변환하지 않고 character만 비교해서 + * (s[i] == '1' || s[i] == '2' && s[i + 1] < '7') 10 <= x <= 26 인지 확인함. 이 경우 더 적은 연산을 수행. + */ diff --git a/valid-anagram/JisooPyo.kt b/valid-anagram/JisooPyo.kt new file mode 100644 index 000000000..6c3a9e50f --- /dev/null +++ b/valid-anagram/JisooPyo.kt @@ -0,0 +1,77 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +/** + * Leetcode + * 242. Valid Anagram + * Easy + */ +class ValidAnagram { + /** + * Runtime: 24 ms(Beats: 52.77 %) + * Time Complexity: O(n) + * - n: 문자열의 길이 + * + * Memory: 38.32 MB(Beats: 31.34 %) + * Space Complexity: O(1) + * - 해시맵의 크기가 알파벳 개수로 제한됨 + */ + fun isAnagram(s: String, t: String): Boolean { + if (s.length != t.length) return false + val map = hashMapOf() + for (i in s.indices) { + map[s[i]] = map.getOrDefault(s[i], 0) + 1 + } + for (i in t.indices) { + if (map[t[i]] == null || map[t[i]] == 0) { + return false + } + map[t[i]] = map.get(t[i])!! - 1 + } + return true + } + + /** + * 해시맵 대신 배열을 이용한 풀이 + * Runtime: 3 ms(Beats: 99.89 %) + * Time Complexity: O(n) + * + * Memory: 37.25 MB(Beats: 80.30 %) + * Space Complexity: O(1) + */ + fun isAnagram2(s: String, t: String): Boolean { + if (s.length != t.length) return false + val array = IntArray(26) + for (i in s.indices) { + array[s[i] - 'a']++ + } + for (i in t.indices) { + array[t[i] - 'a']-- + } + for (num in array) { + if (num != 0) { + return false + } + } + return true + } + + @Test + fun test() { + isAnagram("anagram", "nagaram") shouldBe true + isAnagram("rat", "car") shouldBe false + isAnagram2("anagram", "nagaram") shouldBe true + isAnagram2("rat", "car") shouldBe false + } +} + +/** + * 개선할 여지 1. + * 찾아보니 IntArray.all 이라는 메서드가 있어서 array.all { it == 0 } 을 사용했어도 괜찮았을 것 같아요! + * 모든 요소가 주어진 조건을 만족하는지 검사하는 메서드라고 합니다! + * + * 개선할 여지 2. + * s와 t의 문자열이 같음을 검사했으므로 첫 번째 for문에서 array[t[i] - 'a']-- 를 같이 진행해주었어도 괜찮았을 것 같아요! + */