Skip to content

Conversation

@bona1122
Copy link
Collaborator

@bona1122 bona1122 commented Feb 6, 2025

[트리]

문제를 풀면서 나온 트리의 종류와 특징을 정리해봤습니다.

트리의 기본 특징

  • 순환(사이클)이 없는 그래프이다.
  • 임의의 두 노드 사이의 경로는 유일하며, 노드가 N개일 때, 간선의 개수는 항상 N-1개이다.
  • 루트노드를 제외한 모든 노드는 하나의 부모 노드를 가진다.

1. 이진트리: 트리의 각 노드가 최대 2개의 자식을 갖는 트리

루트노드를 언제 처리하는 지에 따라 순회방법이 3가지가 있다.

  • 순회 방법 3가지
전위: root -> left -> right (루트부터)
중위: left -> root -> right (루트를 중간에)
후위: left -> right -> root (루트를 마지막으로)

2. 완전이진트리: 마지막 레벨을 제외한 모든 레벨이 왼쪽부터 완전히 채워진 트리

  • 중위 순회 결과에서 중간값이 항상 루트가 되는 특징이 있다.

3. 이진검색트리

이진트리의 모든 노드에서 왼쪽 서브트리 값 < 노드의 값 < 오른쪽 서브트리의 값을 만족하는 트리

📌 푼 문제


📝 간단한 풀이 과정

트리 순회 기본

  • 이진 트리 입력되면, 전위순회/중위순회/후위순회 출력하는 문제였는데

  • 클래스를 이용해서 이진트리를 입력받는 것과

  • map을 활용해서 이진트리를 입력받는 것 두가지로 구현해봤습니다. (key를 노드값으로, value를 [left, right] 배열로 저장)

  • 클래스

// https://www.acmicpc.net/problem/1991
// 이진 트리 입력되면, 전위순회/중위순회/후위순회 출력하기

const fs = require("fs")
const path = require("path")
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : path.join(__dirname, "input.txt")
let input = fs.readFileSync(filePath).toString().split("\n")
const log = console.log

class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
  }
}

class BinaryTree {
  constructor() {
    this.root = null
  }

  findNode(value, node = this.root) {
    if (!node) return null
    if (node.value === value) return node
    return this.findNode(value, node.left) || this.findNode(value, node.right)
  }

  insert(value, left, right) {
    if (!this.root) {
      this.root = new Node(value)
      if (left !== ".") this.root.left = new Node(left)
      if (right !== ".") this.root.right = new Node(right)
    } else {
      const node = this.findNode(value)
      if (left !== ".") node.left = new Node(left)
      if (right !== ".") node.right = new Node(right)
    }
  }
}

const N = Number(input[0])
const tree = new BinaryTree()
let result = ""

for (let i = 1; i <= N; i++) {
  const [node, left, right] = input[i].split(" ")
  tree.insert(node, left, right)
}

// 전위 순회
function preorder(node) {
  if (!node) return
  result += node.value
  preorder(node.left)
  preorder(node.right)
}

// 중위 순회
function inorder(node) {
  if (!node) return
  inorder(node.left)
  result += node.value
  inorder(node.right)
}

// 후위 순회
function postorder(node) {
  if (!node) return
  postorder(node.left)
  postorder(node.right)
  result += node.value
}

preorder(tree.root)
result += "\n"
inorder(tree.root)
result += "\n"
postorder(tree.root)

log(result)
  • map
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

const N = Number(input[0])
const tree = new Map()

// 트리 구성
for (let i = 1; i <= N; i++) {
  const [node, left, right] = input[i].split(" ")
  tree.set(node, [left, right])
}

let result = ""

// 전위 순회
function preorder(node) {
  if (node === ".") return
  result += node // root
  preorder(tree.get(node)[0]) // left 하위 트리
  preorder(tree.get(node)[1]) // right 하위 트리
}

// 중위 순회
function inorder(node) {
  if (node === ".") return
  inorder(tree.get(node)[0]) // left 하위 트리
  result += node // root
  inorder(tree.get(node)[1]) // right 하위 트리
}

// 후위 순회
function postorder(node) {
  if (node === ".") return
  postorder(tree.get(node)[0]) // left 하위 트리
  postorder(tree.get(node)[1]) // right 하위 트리
  result += node // root
}

// 결과 출력
preorder("A")
result += "\n"
inorder("A")
result += "\n"
postorder("A")

log(result)

완전 이진 트리

  • 완전 이진 트리가 각 레벨이 왼쪽부터 순차적으로 채워지는 특성 때문에, 중위 순회 결과에서 중간값이 항상 루트가 되는 것을 이용했습니다.
  • 재귀적으로 배열을 반으로 나누어서 왼쪽/오른쪽 서브트리를 처리하도록 구현했습니다.
// https://www.acmicpc.net/problem/9934
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// 상근이는 중위순회로 이진 트리를 순회한 결과를 가지고 있다.
// 각 레벨에 있는 빌딩 번호 구하기
const k = +input[0]
const arr = input[1].split(" ").map(Number)
let result = Array.from({ length: k }, () => [])

const dfs = (arr, depth) => {
  if (arr.length < 1) return

  const mid = Math.floor(arr.length / 2)
  result[depth].push(arr[mid])

  dfs(arr.slice(0, mid), depth + 1)
  dfs(arr.slice(mid + 1), depth + 1)
}

dfs(arr, 0)
result.forEach((row) => log(row.join(" ")))

트리의 부모 찾기

  • 입력을 통해 그래프를 먼저 구성하고, 루트 노드부터 BFS로 탐색하면서 부모-자식 관계를 기록했습니다.
// https://www.acmicpc.net/problem/11725

// 루트없는 트리 주어지면(트리루트 1), 2번노드부터 각 노드의 부모를 구하기
// => 그래프 정보 저장하고, bfs로 탐색하면서 부모노드 찾기
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

const n = +input[0]
const graph = Array.from({ length: n + 1 }, () => [])
for (let i = 1; i < n; i++) {
  let [v1, v2] = input[i].split(" ").map(Number)
  graph[v1].push(v2)
  graph[v2].push(v1)
}

const visited = new Array(n + 1).fill(false)
const parent = new Array(n + 1).fill(0)

const queue = [1]
visited[1] = true

while (queue.length) {
  let item = queue.shift()

  for (let v of graph[item]) {
    if (visited[v]) continue
    visited[v] = true
    queue.push(v)
    parent[v] = item
  }
}
log(result.slice(2).join("\n"))

트리

  • BFS로 그래프를 탐색할 때, 삭제될 노드의 경로는 제외하고 수행하였고
  • 어떤 노드가 리프노드인지 판단하기 위해서는 해당노드와 연결된 노드가 없는지 확인해서 카운트 했습니다.
// https://www.acmicpc.net/problem/1068
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// 노드 제거시, 노드와 노드의 모든 자손이 트리에서 제거됨
// 노드를 지운 후, 트리의 리프노드 개수 구하기
// => 부모노드 정보를 통해 그래프를 구성하고, bfs로 탐색하며 리프노드 개수 카운트

const n = +input[0]
const parents = input[1].split(" ").map(Number)
const remove = +input[2]
const graph = Array.from({ length: n }, () => [])
let root = -1

for (let i = 0; i < n; i++) {
  const parent = parents[i]
  if (parent === -1) {
    root = i
  } else {
    graph[parent].push(i)
  }
}

let result = 0
const queue = remove === root ? [] : [root]
const visited = new Array(n).fill(false)
visited[root] = true

while (queue.length) {
  let item = queue.shift()
  let isLeaf = true

  for (let v of graph[item]) {
    if (v !== remove) {
      isLeaf = false
      if (!visited[v]) {
        visited[v] = true
        queue.push(v)
      }
    }
  }

  if (isLeaf) result++
}

log(result)

이진탐색트리

  • 전위순회결과에서 첫번째 값이 루트가 되는 특징을 이용해서
  • 오른쪽 서브트리와 왼쪽 서브트리를 나누고
  • 재귀적으로 서브트리들을 처리하면서 후위순회결과를 출력했습니다.
// https://www.acmicpc.net/problem/5639
// 이진 검색트리의 전위 순회 경과가 주어지면, 후위순회 결과 구하기
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

const arr = input.map(Number)

const postorder = (root, end) => {
  if (root > end) return

  let right = end + 1 // 오른쪽 서브트리(루트보다 큰 요소들) 시작점 구하기
  for (let i = root + 1; i <= end; i++) {
    if (arr[i] > arr[root]) {
      right = i
      break
    }
  }

  // 후위 순위로 출력
  postorder(root + 1, right - 1)
  postorder(right, end)
  log(arr[root])
}

postorder(0, arr.length - 1)

트리와 쿼리

  • DFS로 재귀적으로 서브트리크기를 계산했습니다.
// https://www.acmicpc.net/problem/15681
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// 가중치, 방향 없는 트리
// 정점 U를 루트로 하는 서브트리에 속한 정점 수 출력하기

// 방법 1: 문제에서 주어진 힌트 처럼, 자식 배열을 활용
const [N, R, Q] = input[0].split(" ").map(Number)

const graph = Array.from({ length: N + 1 }, () => [])
const query = Array(Q)
const children = Array.from({ length: N + 1 }, () => [])
const size = Array(N + 1).fill(1)

for (let i = 1; i <= N - 1; i++) {
  let [u, v] = input[i].split(" ").map(Number)
  graph[u].push(v)
  graph[v].push(u)
}
let idx = 0
for (let i = N; i < N + Q; i++) {
  query[idx++] = +input[i]
}

const makeTree = (cur, parent) => {
  for (let next of graph[cur]) {
    if (next !== parent) {
      children[cur].push(next)
      makeTree(next, cur)
    }
  }
}

// 부모자식관계를 파악하기 위한 두 가지 방법
// 1. 명시적으로 배열에 저장 -> makeTree 선행 필요
const countSubtreeNodes = (cur) => {
  for (let next of children[cur]) {
    countSubtreeNodes(next)
    size[cur] += size[next]
  }
}
// 2. visited배열로 부모자식 파악
const visited = Array(N + 1).fill(false)
const countSubtreeNodes2 = (node) => {
  if (visited[node]) return size[node]
  visited[node] = true // 방문 표시로 부모-자식 관계 파악

  for (const next of graph[node]) {
    if (!visited[next]) {
      // 방문하지 않은 노드는 자식
      size[node] += countSubtreeNodes2(next)
    }
  }
  return size[node]
}
/*
 makeTree(R, 0)
 countSubtreeNodes(R)
 log(children)
*/
countSubtreeNodes2(R)

for (let q of query) log(size[q])

트리의 순회

  • 후위순회의 마지막 값이 루트인 것을 이용해서 루트값을 찾고
  • 찾은 루트값을 통해 중위순회에서 왼쪽/오른쪽 서브트리 를 찾아서
  • 재귀적으로 전위순회를 출력하도록 하였습니다.
  • 추가적으로, 문제를 푸는 과정에서, 처음에는 루트의 인덱스를 찾을 때 매번 indexOf를 사용했더니, 시간초과가 발생하여 map을 활용했습니다.
// https://www.acmicpc.net/problem/2263
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// n개의 정점존재, 이진트리의 inorder, postorder 주어지면, preorder 구하기
const n = +input[0]
const inorder = input[1].split(" ").map(Number)
const postorder = input[2].split(" ").map(Number)
let result = ""

// 매번 findIndex하니, 시간초과가 남. => inorder 값의 인덱스를 미리 Map에 저장해두기
const inorderMap = new Map()
inorder.forEach((val, idx) => inorderMap.set(val, idx))

// postorder로 루트 알아내고,
// 그 정보를 통해 inorder에서 왼/오 서브트리 알아내기
// iStart, iEnd: inorder 배열의 범위
// pStart, pEnd: postorder 배열의 범위
const preorder = (iStart, iEnd, pStart, pEnd) => {
  if (iStart > iEnd || pStart > pEnd) return

  const root = postorder[pEnd]
  const rootIdx = inorderMap.get(root)
  result += `${root} `

  const leftSize = rootIdx - iStart // 왼쪽 서브트리 크기

  // 왼쪽 서브트리 처리
  preorder(iStart, rootIdx - 1, pStart, pStart + leftSize - 1)

  // 오른쪽 서브트리 처리
  preorder(rootIdx + 1, iEnd, pStart + leftSize, pEnd - 1)
}

preorder(0, n - 1, 0, n - 1)
log(result.trim())

@bona1122 bona1122 self-assigned this Feb 6, 2025
Copy link
Collaborator

@Moonjonghoo Moonjonghoo left a comment

Choose a reason for hiding this comment

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

트리에대해서 작성해주시고, 이진트리, 이진탐색트리등의 개념과 해당트리의 순회에대해서 잘정리해주셔서 저도 공부가됬습니다. 한주간 고생하셧어요!

@JooKangsan JooKangsan merged commit e94099c into codingTestStd:main Feb 13, 2025
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants