diff --git a/longest-consecutive-sequence/jdalma.kt b/longest-consecutive-sequence/jdalma.kt new file mode 100644 index 000000000..1604387f3 --- /dev/null +++ b/longest-consecutive-sequence/jdalma.kt @@ -0,0 +1,113 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import kotlin.math.max + +class `longest-consecutive-sequence` { + fun longestConsecutive(nums: IntArray): Int { + if (nums.isEmpty()) return 0 + return usingUnionFind(nums) + } + + /** + * 1. 배열을 정렬하여 순서대로 순회하며 연속 수열 길이를 확인한다. + * TC: O(n * log(n)), SC: O(1) + */ + private fun usingSort(nums: IntArray): Int { + nums.sort() + + var (length, maxLength) = 1 to 0 + for (index in 0 until nums.size - 1) { + if (nums[index] == nums[index + 1]) { + continue + } else if (nums[index] + 1 == nums[index + 1]) { + length++ + } else { + maxLength = max(length, maxLength) + length = 1 + } + } + return max(length, maxLength) + } + + /** + * 2. Set의 자료구조를 활용하여 가장 작은 값부터 while문을 통한 최대 증가 값을 반환한다. + * TC: O(n), SC: O(n) + */ + private fun usingSet(nums: IntArray): Int { + val numberSet = nums.toSet() + var maxLength = 0 + + for (number in nums) { + if (numberSet.contains(number - 1)) { + continue + } + var length = 1 + while (numberSet.contains(number + length)) { + length++ + } + maxLength = max(maxLength, length) + } + + return maxLength + } + + /** + * 3. Union-Find + * TC: O(n), SC: O(n) + */ + private fun usingUnionFind(nums: IntArray): Int { + val nodes = mutableMapOf() + val dsu = DSU(nums.size) + + for ((i,n) in nums.withIndex()) { + if (n in nodes) continue + + nodes[n - 1]?.let { dsu.union(i, it) } + nodes[n + 1]?.let { dsu.union(i, it) } + + nodes[n] = i + } + + return dsu.maxLength() + } + + @Test + fun `입력받은 정수 배열의 최대 연속 수열 길이를 반환한다`() { + longestConsecutive(intArrayOf()) shouldBe 0 + longestConsecutive(intArrayOf(100,4,200,1,3,2)) shouldBe 4 + longestConsecutive(intArrayOf(11,23,12,13,14,21)) shouldBe 4 + longestConsecutive(intArrayOf(0,3,7,2,5,8,4,6,0,1)) shouldBe 9 + longestConsecutive(intArrayOf(11,64,43,12,13,10,9,8,7)) shouldBe 7 + } +} + +class DSU(val n: Int) { + private val parent = IntArray(n) { it } + private val size = IntArray(n) { 1 } + + private fun find(x: Int): Int { + if (parent[x] != x) + parent[x] = find(parent[x]) + return parent[x] + } + + fun union(x: Int, y: Int) { + val root = find(x) + val child = find(y) + if(root != child) { + parent[child] = root + size[root] += size[child] + } + } + + fun maxLength(): Int { + var res = 0 + for (i in parent.indices) { + if (parent[i] == i) + res = maxOf(res, size[i]) + } + return res + } +} diff --git a/maximum-product-subarray/jdalma.kt b/maximum-product-subarray/jdalma.kt new file mode 100644 index 000000000..1613b4636 --- /dev/null +++ b/maximum-product-subarray/jdalma.kt @@ -0,0 +1,56 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import kotlin.math.max +import kotlin.math.min + +class `maximum-product-subarray` { + + fun maxProduct(nums: IntArray): Int { + return usingOptimizedDP(nums) + } + + /** + * 현재의 값, 이전 위치의 최대 누적곱, 이전 위치의 최소 누적곱 이 세 개를 비교하여 한 번의 순회로 최대 값을 반환한다. + * 음수와 음수가 곱해져 최대 값이 도출될 수 있기에 DP 배열을 두 개 생성한다. + * TC: O(n), SC: O(n) + */ + private fun usingDP(nums: IntArray): Int { + val (min, max) = IntArray(nums.size) { 11 }.apply { this[0] = nums[0] } to IntArray(nums.size) { -11 }.apply { this[0] = nums[0] } + var result = nums[0] + for (index in 1 until nums.size) { + max[index] = max(max(nums[index], nums[index] * max[index - 1]), nums[index] * min[index - 1]) + min[index] = min(min(nums[index], nums[index] * max[index - 1]), nums[index] * min[index - 1]) + result = max(max(min[index], max[index]), result) + } + + return result + } + + /** + * DP 배열이 입력받는 정수의 배열만큼 생성할 필요가 없고, 이전 값과 현재 값만 기억하면 되므로 공간복잡도가 개선되었다. + * TC: O(n), SC: O(1) + */ + private fun usingOptimizedDP(nums: IntArray): Int { + var (min, max) = nums[0] to nums[0] + var result = nums[0] + for (index in 1 until nums.size) { + val (tmpMin, tmpMax) = min to max + max = max(max(nums[index], nums[index] * tmpMax), nums[index] * tmpMin) + min = min(min(nums[index], nums[index] * tmpMax), nums[index] * tmpMin) + result = max(max(min, max), result) + } + + return result + } + + @Test + fun `입력받은 정수 배열의 가장 큰 곱을 반환한다`() { + maxProduct(intArrayOf(2,3,-2,4)) shouldBe 6 + maxProduct(intArrayOf(-2,0,-1)) shouldBe 0 + maxProduct(intArrayOf(-10)) shouldBe -10 + maxProduct(intArrayOf(-2,3,-4)) shouldBe 24 + maxProduct(intArrayOf(-4,-3,-2)) shouldBe 12 + } +} diff --git a/missing-number/jdalma.kt b/missing-number/jdalma.kt new file mode 100644 index 000000000..532000c8d --- /dev/null +++ b/missing-number/jdalma.kt @@ -0,0 +1,41 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `missing-number` { + + fun missingNumber(nums: IntArray): Int { + return usingSum(nums) + } + + /** + * 1. 배열을 추가로 생성하여 존재하는 정수의 인덱스에 true를 저장하여, false인 인덱스를 반환한다. + * TC: O(n), SC: O(n) + */ + private fun usingIndex(nums: IntArray): Int { + val existed = BooleanArray(nums.size + 1) + nums.forEach { existed[it] = true } + + existed.forEachIndexed { i, e -> + if (!e) return i + } + return -1 + } + + /** + * 2. 0부터 정수 배열의 사이즈만큼 정수를 합산하여 기대하는 합산과 뺀 결과를 반환한다. + * TC: O(n), SC: O(1) + */ + private fun usingSum(nums: IntArray): Int { + val size = nums.size + return nums.fold((size + 1) * size / 2) { acc , i -> acc - i } + } + + @Test + fun `입력받은 정수 배열에서 비어있는 정수를 반환한다`() { + missingNumber(intArrayOf(3,2,1)) shouldBe 0 + missingNumber(intArrayOf(3,0,1)) shouldBe 2 + missingNumber(intArrayOf(9,6,4,2,3,5,7,0,1)) shouldBe 8 + } +} diff --git a/valid-palindrome/jdalma.kt b/valid-palindrome/jdalma.kt new file mode 100644 index 000000000..2f582a023 --- /dev/null +++ b/valid-palindrome/jdalma.kt @@ -0,0 +1,44 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test + +class `valid-palindrome` { + + /** + * 입력받은 문자열의 양 끝부터 투 포인터를 이용하여 비교한다. + * TC: O(n), SC: O(1) + */ + fun isPalindrome(s: String): Boolean { + var (left, right) = 0 to s.length - 1 + while (left < right) { + while (left < right && !isAlphabetOrDigit(s[left])) { + left++ + } + while (left < right && !isAlphabetOrDigit(s[right])) { + right-- + } + if (s[left].lowercaseChar() != s[right].lowercaseChar()) { + return false + } + left++ + right-- + } + return true + } + + private fun isAlphabetOrDigit(c: Char): Boolean = c.isDigit() || (c in 'a' .. 'z') || (c in 'A' .. 'Z') + + @Test + fun `입력받은 문자열의 영숫자가 팰린드롬 여부를 반환한다`() { + isPalindrome("A man, a plan, a canal: Panama") shouldBe true + isPalindrome("race a car") shouldBe false + isPalindrome("ㅁaㄹㅁb듐노+_c$#&$%#b*&@!!@a$") shouldBe true + } + + @Test + fun `입력받은 문자열이 공백만 존재한다면 참을 반환한다`() { + isPalindrome(" ") shouldBe true + isPalindrome(" ") shouldBe true + } +} diff --git a/word-search/jdalma.kt b/word-search/jdalma.kt new file mode 100644 index 000000000..3b24745c5 --- /dev/null +++ b/word-search/jdalma.kt @@ -0,0 +1,95 @@ +package leetcode_study + +import io.kotest.matchers.shouldBe +import leetcode_study.`word-search`.Position.Companion.MOVES +import org.junit.jupiter.api.Test + +class `word-search` { + + /** + * 격자에 존재하는 문자를 사용하여 word 를 만들 수 있는지 확인하기 위해 DFS를 통한 visited 배열 백트래킹을 사용하여 해결 + * TC: O(너비 * 높이 * 4^word), SC: O(너비 * 높이 * 4^word) + */ + fun exist(board: Array, word: String): Boolean { + return usingBacktracking(board, word) + } + + private fun usingBacktracking(board: Array, word: String): Boolean { + fun dfs(board: Array, visited: Array, word: String, position: Position, index: Int): Boolean { + if (index == word.length) return true + for (move in MOVES) { + val next = position + move + if (next.isNotOutOfIndexed(board) && !visited[next.x][next.y] && board[next.x][next.y] == word[index]) { + visited[next.x][next.y] = true + if (dfs(board, visited, word, next, index + 1)) return true + visited[next.x][next.y] = false + } + } + return false + } + + val visited = Array(board.size) { + BooleanArray(board[0].size) + } + + for (x in board.indices) { + for (y in board[x].indices) { + visited[x][y] = true + if (board[x][y] == word[0] && dfs(board, visited, word, Position(x,y), 1)) { + return true + } + visited[x][y] = false + } + } + return false + } + + @Test + fun `문자로 구성된 2차원 배열에서 word 문자열 존재 유무를 반환한다`() { + exist(arrayOf( + charArrayOf('A','B','C','E'), + charArrayOf('S','F','C','S'), + charArrayOf('A','D','E','E') + ), "ABCCED") shouldBe true + exist(arrayOf( + charArrayOf('A','B','C','E'), + charArrayOf('S','F','C','S'), + charArrayOf('A','D','E','E') + ), "SEE") shouldBe true + exist(arrayOf( + charArrayOf('A','B','C','E'), + charArrayOf('S','F','C','S'), + charArrayOf('A','D','E','E') + ), "SES") shouldBe false + exist(arrayOf( + charArrayOf('A','B','C','E'), + charArrayOf('S','F','E','S'), + charArrayOf('A','D','E','E') + ), "ABCESEEEFS") shouldBe true + exist(arrayOf( + charArrayOf('C','A','A'), + charArrayOf('A','A','A'), + charArrayOf('B','C','D') + ), "AAB") shouldBe true + } + + data class Position( + val x: Int, + val y: Int + ) { + + operator fun plus(other: Position) = Position(this.x + other.x, this.y + other.y) + + fun isNotOutOfIndexed(board: Array) = + this.x < board.size && this.x >= 0 && this.y < board[0].size && this.y >= 0 + + companion object { + val MOVES: List = listOf( + Position(-1, 0), + Position(0, 1), + Position(1, 0), + Position(0, -1), + ) + } + } +}