ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 트리
    알고리즘 2025. 5. 14. 21:54

     

     

     

    https://takeuforward.org/data-structure/preorder-inorder-postorder-traversals-in-one-traversal/

     

     

    https://medium.com/@Roshan-jha/a-comprehensive-guide-to-binary-tree-traversal-in-java-74c86ee23725

     

     

     

     

    이진 트리 

     

    1. maximum-depth-of-binary-tree

    문제 : https://leetcode.com/problems/maximum-depth-of-binary-tree/

     

    트리의 깊이를 찾는 문제다. 

    먼저 DFS 방식으로 풀 수 있다. 

    n : 노드의 갯수, h : 트리의 최대 깊이 

    시간 복잡도 : O(n) -> 모든 노드를 한번씩 방문 

    공강 복잡도 : O(h) -> 재귀 콜 스택에 최대 h개의 노드 

        fun maxDepth(root: TreeNode?): Int {
            if(root == null) return 0
            var left = maxDepth(root.left)
            var right = maxDepth(root.right)
            return maxOf(left, right) +1
        }

     

     

    BFS 방식으로도 풀 수 있다. 

    n : 노드의 갯수, w: 최대 넓이 

    시간 복잡도 : O(n) -> 모든 노드를 한번씩 방문 

    공강 복잡도 : O(w) -> 최악의 경우 큐에 가장 넓은 레벨의 모든 노드가 들어감 

        fun maxDepth(root: TreeNode?): Int {
            if(root == null) return 0
            val queue: Queue<TreeNode> = LinkedList() 
            queue.offer(root)
            var depth = 0
            while(queue.isNotEmpty()) {
                val size = queue.size
                depth ++
                repeat(size) {
                    val node = queue.poll()
                    node.left?.let{queue.offer(it)}
                    node.right?.let{queue.offer(it)}
                }
            }
            return depth
        }

     

     

     

    참고 ) 

    1. 완전 이진 트리 (balanced)

    • 노드 수 n = 2^h - 1 → 균형 잡힘
    • DFS 공간: O(log n)
    • BFS 공간: O(n/2) (마지막 레벨이 큐에 들어감)
    • 시간은 둘 다 O(n)
    • 공간은 트리 구조에 따라 DFS가 더 유리하거나 불리할 수 있음
    • 균형 트리라면 DFS는 더 메모리 효율적
    • BFS는 모든 레벨을 동시에 보기에 **레벨 기반 문제(예: 최소 깊이)**에 더 적합

     

    2. same-tree

    문제 : https://leetcode.com/problems/same-tree/

    두개의 트리가 같은지 물어보는 문제다.

     

    DFS 방법 

    fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean {
        if(p == null && q == null) {
            return true 
        }
        if(p == null || q== null ) {
            return false 
        }
    
        if(p.`val` != q.`val`) {
            return false 
        }
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
    
    }

     

    BFS 방법 

        fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean {
            if(p == null && q == null) {
                return true 
            }
            if(p == null || q== null ) {
                return false 
            }
    
            val pQueue:Queue<TreeNode> = LinkedList()
            val qQueue:Queue<TreeNode> = LinkedList() 
            pQueue.offer(p)
            qQueue.offer(q)
            while(pQueue.isNotEmpty() && qQueue.isNotEmpty()) {
                val pTemp = pQueue.poll()
                val qTemp = qQueue.poll()
                if(pTemp == null && qTemp == null) continue
                if(pTemp == null || qTemp == null) return false 
                if(pTemp.`val` != qTemp.`val`) return false 
                pQueue.offer(pTemp.left)
                pQueue.offer(pTemp.right)
                qQueue.offer(qTemp.left)
                qQueue.offer(qTemp.right)
            }
    
            return pQueue.isEmpty() && qQueue.isEmpty()
        }

     

     

     

     

    3. 트리 뒤집기 

    문제 : https://leetcode.com/problems/invert-binary-tree/

     

    주어진 트리의 좌우를 바꾸는 문제이다. 

     

    DFS 

        fun invertTree(root: TreeNode?): TreeNode? {
            if(root == null) {
                return null 
            }
    
            val left = invertTree(root.left)
            val right = invertTree(root.right)
            root.right = left 
            root.left = right 
            return root 
        }

     

    BFS 

    fun invertTree(root: TreeNode?): TreeNode? {
            if(root == null) return null 
            val queue: Queue<TreeNode> = LinkedList()
            queue.offer(root)
            while(queue.isNotEmpty()) {
                val node = queue.poll()
                val temp = node.left 
                node.left = node.right 
                node.right = temp 
    
                node.left?.let{queue.offer(it)}
                node.right?.let{queue.offer(it)}
            }
            return root 
        }

     

     

     

    4. 트리의 최대합 구하기 

    문제 : https://leetcode.com/problems/binary-tree-maximum-path-sum/submissions/1633699245/

     

     

    현재 노드까지의 최대 합을 구하고 갱신해서 최댓값을 구하는 방식으로 DFS로 문제를 풀수 있다. 

    BFS방식은 모든 노드를 방문해서 레벨 순서로 순회하고, 현재 노드와 자식들을 보고 지나가기 때문에, 노드 기준으로 양쪽 자식을 더해서 구성하는 판단이 불가능하다. 

    ✅ BFS로 가능한 범위

    문제 종류 DFS 적합 BFS 가능
    최대 깊이 (maxDepth)
    최소 깊이 (minDepth)
    루트→리프 경로합 (hasPathSum)
    🔥 임의의 경로에서 최대 합 (maxPathSum)
     

     

    class Solution {
    
        var maxSum = Int.MIN_VALUE
        fun maxPathSum(root: TreeNode?): Int {
            dfs(root)
            return maxSum 
        }
    
        fun dfs(root: TreeNode?): Int {
            if(root == null) {
                return 0
            }
            val left = maxOf(0, dfs(root.left))
            val right = maxOf(0, dfs(root.right))
            val temp = root.`val` + right + left
            maxSum = maxOf(maxSum, temp)
            return root.`val` + maxOf(right, left)
        }
    
    }

     

     

    5. Binary Tree Level Order Traversal 

    문제 : https://leetcode.com/problems/binary-tree-level-order-traversal/

    이진 트리의 레벨 별 리스트를 구하는 문제이다.

    BFS 방식을 이용하면 직관적으로 풀 수 있다. 

     

    BFS를 이용한 방식으로 문제를 풀었다. 

    class Solution {
        fun levelOrder(root: TreeNode?): List<List<Int>> {
            if (root == null) {
                return emptyList()
            }
            val answer = mutableListOf<MutableList<Int>>()
            val queue: Queue<Pair<TreeNode, Int>> = LinkedList()
            queue.offer(Pair(root, 0))
            while(queue.isNotEmpty()) {
                val (node, level) = queue.poll()
                if(level == answer.size) {
                    answer.add(mutableListOf())
                }
                answer[level].add(node.`val`)
                node.left?.let{queue.offer(Pair(it, level + 1))}
                node.right?.let{queue.offer(Pair(it, level + 1))}
                
            }
            return answer 
        }
    }

     

     

    DFS 방식을 사용하면 더 간단하게 풀 수 있다. 

    DFS로는 다음과 같이 풀 수 있다. 

    fun levelOrder(root: TreeNode?): List<List<Int>> {
            val result = mutableListOf<MutableList<Int>>()
            dfs(root, 0, result)
            return result 
        }
    
        fun dfs(root:TreeNode?, level:Int, result: MutableList<MutableList<Int>>) {
            if(root == null) return 
            if(result.size == level) {
                result.add(mutableListOf())
            }
            result[level].add(root.`val`)
            dfs(root.left, level+1, result)
            dfs(root.right, level+1, result)
        }

     

     

     

    6. Serialize and Deserialize Binary Tree 

    트리를 인코딩하고 디코딩 하는 문제이다. 

    BFS를 사용하면 다음과 같다. 

    class Codec() {
    	// Encodes a URL to a shortened URL.
        fun serialize(root: TreeNode?): String {
            if(root == null) return "null"
            val queue :Queue<TreeNode> = LinkedList()
            val result = mutableListOf<String>()
            queue.offer(root)
            while(queue.isNotEmpty()) {
                val node = queue.poll()
                if(node == null ) {
                    result.add("null")
                }else {
                    result.add(node.`val`.toString())
                    queue.offer(node.left)
                    queue.offer(node.right)
                }
            }
            return result.joinToString(",")
        }
    
        // Decodes your encoded data to tree.
        fun deserialize(data: String): TreeNode? {
            if(data == "null") return null         
            val values = data.split(",")
            val root = TreeNode(values[0].toInt())
            val queue: Queue<TreeNode> = LinkedList()
            queue.offer(root)
            var i = 1 
            while (queue.isNotEmpty() && i < values.size) {
                val current = queue.poll()
                if(values[i] != "null") {
                    val left = TreeNode(values[i].toInt())
                    current.left = left 
                    queue.offer(left)
                }
                i ++ 
                if(i<values.size && values[i] != "null") {
                    val right = TreeNode(values[i].toInt())
                    current.right = right 
                    queue.offer(right)
                }
                i++
            }
            return root 
        }
    }

     

    DFS로 풀면 다음과 같다. 

        fun serialize(root:TreeNode?): String {
            val sb = StringBuilder()
            fun dfs(node: TreeNode?) {
                if (node == null) {
                    sb.append("null,")
                    return 
                }
                sb.append("${node.`val`},")
                dfs(node.left)
                dfs(node.right)
            }
            dfs(root)
            return sb.toString()
        }
    
        fun deserialize(data: String): TreeNode? {
            val nodes = LinkedList(data.split(",").toList())
    
            fun dfs(): TreeNode? {
                val value = nodes.poll()
                if(value == "null") return null 
                val node = TreeNode(value.toInt())
                node.left = dfs()
                node.right = dfs()
                return node 
            }
            return dfs()
        }

     

    7. Kth Smallest Element 

    문제 : https://leetcode.com/problems/kth-smallest-element-in-a-bst/

     

    BST에서 k번째로 작은 수를 찾는 문제이다. 

     

    처음에는 BST라고 생각을 안하고 넓이 우선 탐색으로 전체 탐색하고 정렬해서 문제를 풀었다. 

        fun kthSmallest(root: TreeNode?, k: Int): Int {
            // 모든 트리를 순회하면서 정렬하기 
            if(root == null || k == 0) return 0
            val answer = mutableListOf<Int>()
            val queue: Queue<TreeNode> = LinkedList()
            queue.offer(root)
            while(queue.isNotEmpty()) {
                val node = queue.poll() 
                answer.add(node.`val`)
                node.left?.let{queue.offer(it)}
                node.right?.let{queue.offer(it)}
            }
            answer.sort()
            return answer[k-1]
        }

     

     

    하지만 해당 문제는 이렇게 풀면 안된다. 

     

    문제에서 BST로 주어졌는데, 이진 트리 탐색의 경우 다음과 같은 특징을 가진다. 

    중위 순회의 경우 순차 정렬된다. (Inorder Traversal을 수행하여 모든 키를 정렬된 순서) 

    참고 :https://yoongrammer.tistory.com/71 

     

    [자료구조] 이진 탐색 트리 (BST, Binary Search Tree)

    목차이진 탐색 트리 (BST, Binary Search Tree)이진 탐색 트리란 정렬된 이진트리로써 다음과 같은 속성을 가지고 있습니다.노드의 왼쪽 하위 트리에는 노드의 키보다 작은 키가있는 노드 만 포함됩니

    yoongrammer.tistory.com

    이를 이용해서 문제를 다시푼다. 

    H = 트리높이라고할때, 

    시간 복잡도 (H + K)이다. 

    최악의 경우 (불균형트리) -> O(N), 

    균형잡힌 트리인경우 -> O(logN + K) 

        private var count = 0 
        private var result = 0 
    
        fun kthSmallest(root: TreeNode?, k: Int): Int {
            if(root == null || k == 0) return 0
            inorder(root, k)
            return result
        }
    
        private fun inorder(node: TreeNode?, k: Int) {
            if(node == null) {
                return 
            }
            inorder(node.left, k)
            count ++ 
            if (count == k) {
                result = node.`val`
                return 
            }
            inorder(node.right, k)
        }

     

     

     

    8. Lowest common ancestor of BST 

    문제 : https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/

     

    두개의 노드가 주어지면 공통 부모 노드중에서 가장 최소 부모를 찾는 문제이다. 

    BST에서는 해당 노드보다 작으면 왼쪽, 크면 오른쪽에 위치하게 된다는 점을 활용해야한다.

    왼쪽 노드 < 현재 노드 < 오른쪽 노드 

    즉, 노드가 p, q의 공통 조상이 되려면 다음중 하나여야 한다.

     

    • p와 q가 양쪽 자식에 하나씩 있음 (즉, p < node < q 또는 q < node < p)
    • p 또는 q가 node 자신임 (자기 자신도 조상이 될 수 있음)

    시간복잡도 O(h) 

    공간복잡도 O(n) 

    - 재귀 호출 스택 사용 

    으로 문제를 풀 수 있다. 

    class Solution {
        fun lowestCommonAncestor(root: TreeNode?, p: TreeNode?, q: TreeNode?): TreeNode? {
            val value = root?.`val` ?: return null 
            val pVal = p?.`val` ?: return null 
            val qVal = q?.`val` ?: return null 
    
            return when {
                pVal < value && qVal < value -> lowestCommonAncestor(root.left, p, q)
                pVal >value && qVal > value -> lowestCommonAncestor(root.right, p, q)
                else -> root 
            }
    
            
        }
    
    }

     

     

     

     

     

    반복문으로도 해당 문제를 풀 수 있다. 

    명시적 재귀 호출이 없고 node 하나만의 참조만으로 루프를 돌아서 스택도 안 쓰고, 함수 호출도 한 번이므로 공간은 상수이다.

    반복문의 경우 공간 복잡도가 O(1)로 줄어든다 .

     

        fun lowestCommonAncestor(root: TreeNode?, p: TreeNode?, q: TreeNode?): TreeNode? {
            var node = root 
            if(q == null || p == null){
                return null 
            }
            while (node != null) {
                val value = node.`val`
                when {
                    p.`val`<value && q.`val`< value -> node = node.left 
                    p.`val`>value && q.`val`> value -> node = node.right 
                    else -> return node 
                }
            }            
            return null 
        }

     

     

     

     

     

    '알고리즘' 카테고리의 다른 글

    문자열  (0) 2025.04.17
    행렬 문제  (0) 2025.04.11
    연결리스트 문제  (0) 2025.04.10
    그래프(DFS, BFS) 문제  (0) 2025.03.11
    백준 14891) 톱니바퀴 (골드.5)  (0) 2022.08.02
Designed by Tistory.