Skip to main content

Command Palette

Search for a command to run...

DFS와 BFS, 그리고 Preorder / Inorder / Postorder의 진짜 관계

Published
4 min read

트리나 그래프를 공부하다 보면 항상 함께 등장하는 개념이 있다.

  • DFS / BFS

  • Preorder / Inorder / Postorder

이 둘은 자주 묶여 설명되지만, 실제로는 역할과 레벨이 다르다.
이번 글에서는 다음 질문들에 답하는 흐름으로 정리해본다.

  • DFS는 왜 순회 방식이 3가지로 나뉘는가?

  • 순회 코드는 같은데 visit 위치만 다른 건가?

  • BFS에는 왜 preorder / postorder 같은 개념이 없는가?

  • 실무에서는 DFS보다 BFS를 언제 더 많이 쓰는가?


1. DFS와 BFS는 “탐색 전략”이다

먼저 가장 큰 개념부터 분리해야 한다.

  • 깊이를 우선으로 탐색

  • 한 경로를 끝까지 내려간 후 되돌아옴

  • 재귀 또는 스택 기반

  • 너비(레벨)를 우선으로 탐색

  • 같은 깊이의 노드를 먼저 모두 방문

  • 큐 기반

DFS / BFS는 “어떻게 이동하느냐”에 대한 전략이다.


2. Preorder / Inorder / Postorder는 DFS의 변형이 아니다

많은 사람들이 이렇게 오해한다.

“DFS에는 preorder, inorder, postorder가 있다”

정확히 말하면 틀린 표현이다.

Preorder / Inorder / Postorder는
DFS를 ‘어떻게 관측하느냐’에 대한 규칙이다.


3. DFS는 구조적으로 3개의 “의미 있는 순간”을 가진다

재귀 DFS를 생각해보면, 모든 노드는 반드시 다음 세 시점을 거친다.

dfs(node)
 ├─ (1) node에 진입
 ├─ dfs(left)
 ├─ (2) 왼쪽 처리 완료
 ├─ dfs(right)
 └─ (3) node에서 나감

이 세 시점은 재귀 구조상 필연적이다.

시점순회 방식
진입 시Preorder
중간Inorder
종료 시Postorder

👉 DFS가 3가지 순회로 나뉘는 이유는
노드를 처리할 수 있는 논리적 시점이 3곳뿐이기 때문이다.


4. “순회 코드는 같은데 visit 위치만 다른가?”

결론부터 말하면 정확히 맞다.

DFS의 이동 구조는 항상 동일하다.

function dfs(node) {
  if (!node) return;

  // A
  dfs(node.left);
  // B
  dfs(node.right);
  // C
}

node.val어디에서 사용하느냐에 따라 결과만 달라진다.

// Preorder
visit(node); // A

// Inorder
visit(node); // B

// Postorder
visit(node); // C

즉,

DFS는 하나이고,
pre/in/post는 관측 시점의 차이다.


5. 하나의 DFS로 3가지 순회를 동시에 얻을 수 있다

이 사실이 위 설명을 가장 명확히 증명한다.

function dfs(node) {
  if (!node) return;

  pre.push(node.val);     // enter
  dfs(node.left);
  in.push(node.val);      // between
  dfs(node.right);
  post.push(node.val);    // exit
}
  • DFS는 단 한 번 실행

  • 결과는 preorder / inorder / postorder 세 가지

👉 서로 다른 알고리즘이 아니다.


6. BFS에는 왜 visit 위치를 바꿀 수 없을까?

이제 BFS를 보자.

while (queue.length > 0) {
  const node = queue.shift();
  visit(node);
  queue.push(node.children);
}

BFS의 핵심 특징은 이것이다.

  • 노드는 큐에서 꺼내지는 순간만 의미가 있다

  • 재귀 호출 ❌

  • enter / exit ❌

  • 머무는 시간 ❌

DFS와 달리 BFS에는:

  • “들어올 때”

  • “중간”

  • “나갈 때”

라는 개념이 존재하지 않는다.

👉 그래서 BFS에는 preorder / postorder가 생길 수 없다.


7. BFS는 왜 Level Order 하나뿐인가?

BFS의 정의 자체가 이것이기 때문이다.

“현재 레벨의 모든 노드를 처리한 후 다음 레벨로 이동한다”

이 성질은:

  • visit을 dequeue 시점에 할 때만 유지된다

  • 위치를 바꾸면 BFS의 의미가 깨진다

그래서 BFS 결과는 항상:

  • Level Order Traversal

8. TypeScript로 보는 BFS 표준 구현

트리 BFS

function bfs(root: TreeNode | null): number[] {
  if (!root) return [];

  const result: number[] = [];
  const queue: TreeNode[] = [root];

  while (queue.length > 0) {
    const node = queue.shift()!;
    result.push(node.val);

    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }

  return result;
}

레벨 단위 BFS

function bfsByLevel(root: TreeNode | null): number[][] {
  if (!root) return [];

  const result: number[][] = [];
  const queue: TreeNode[] = [root];

  while (queue.length > 0) {
    const size = queue.length;
    const level: number[] = [];

    for (let i = 0; i < size; i++) {
      const node = queue.shift()!;
      level.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(level);
  }

  return result;
}

9. 실무에서는 왜 DFS보다 BFS를 더 많이 쓰는가?

1️⃣ 최단 거리 / 최소 단계

  • 권한 관계

  • 조직도

  • 추천 시스템 n-hop

👉 “최소”가 들어가면 BFS


2️⃣ 단계적 확산 로직

  • 알림 전파

  • 피드 전파

  • 영향도 계산

👉 BFS는 wave처럼 퍼진다


3️⃣ UI / UX 중심 데이터

  • 댓글 트리

  • 파일 탐색기

  • 트리 UI

👉 사용자 인지는 깊이보다 레벨


4️⃣ 안정성

  • DFS: stack overflow 위험

  • BFS: 반복문 + 큐 → 안전


10. 그럼 DFS는 언제 쓰나?

DFS는 다음 상황에서 강력하다.

  • 모든 경우의 수 탐색

  • 백트래킹

  • 구조 분석

  • 사이클 탐지

  • enter / exit 시간이 필요한 알고리즘

👉 DFS는 “구조”,
👉 BFS는 “거리와 단계”


11. 핵심 요약

  • DFS / BFS는 탐색 전략이다

  • Pre/In/Post는 DFS의 관측 규칙이다

  • DFS는 구조적으로 3개의 방문 시점을 가진다

  • BFS는 visit 위치를 바꿀 수 없다

  • 실무에서는 “단계·최소·안정성” 때문에 BFS가 더 자주 쓰인다


마무리

DFS와 BFS는 단순한 알고리즘 문제가 아니라
데이터를 어떻게 이해하고 처리할 것인가에 대한 관점의 차이다.

이 차이를 정확히 이해하면:

  • 알고리즘 문제 풀이

  • 실무 설계

  • 코드 리뷰

모두에서 판단이 훨씬 명확해진다.


🧾 작성 참고

이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.

More from this blog

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

npm SemVer 실무 가이드

이 글은 npm의 Semantic Versioning(SemVer) 을 처음 접하는 개발자부터, 실무에서 이미 사용하고 있지만 헷갈리는 포인트가 많은 개발자까지를 대상으로 한다. 단순한 규칙 나열이 아니라, 실제 업무에서 어떻게 쓰이고, 어디서 사고가 나며, 어떤 관습이 굳어졌는지를 중심으로 정리했다. 1. SemVer란 무엇인가? SemVer(Semantic Versioning)는 버전 번호에 의미를 부여하는 규칙이다. 기본 형식은 다음과 ...

Jan 7, 20263 min read

Dev note

31 posts