icon

안동민 개발노트

3장 : 자료구조

연결리스트, 트리, 이진트리 순회

자료구조 학습 절입니다.

3장 2절까지는 배열을 바탕으로 한 자료구조를 배웠습니다.

배열 → 스택
배열 → 큐
배열 → 원형 큐

이번 절에서는 배열과 다른 방식으로 데이터를 저장하는 연결리스트를 배웁니다. 그리고 자료구조의 핵심 중 하나인 트리이진트리 순회까지 갑니다.

이번 절의 핵심은 다음과 같습니다.

연결리스트 = 노드들을 포인터로 연결한 구조
트리 = 부모-자식 관계를 가진 계층 구조
이진트리 = 자식이 최대 2개인 트리
순회 = 트리의 모든 노드를 정해진 순서로 방문하는 것

이 내용은 자료구조 출제범위의 연결 리스트, 노드, 포인터, 단순 연결 리스트, 노드 삽입·삭제, 동적 연결 스택과 큐, 원형 연결 리스트, 이중 연결 리스트, 트리, 이진 트리, 이진 트리 순회와 직접 연결됩니다. 또 포인터와 구조체는 C프로그래밍의 포인터, 포인터와 배열, 구조체, 구조체 포인터와 연결됩니다.


이번 절의 큰 그림

이번 절에서 배울 흐름은 다음과 같습니다.

배열의 한계
→ 연결리스트의 필요성
→ 노드와 포인터
→ 단순 연결리스트
→ 삽입과 삭제
→ 원형 연결리스트
→ 이중 연결리스트
→ 트리의 기본 개념
→ 이진트리
→ 전위순회, 중위순회, 후위순회

자료구조에서 연결리스트와 트리는 매우 중요합니다.

왜냐하면 뒤에 나올 그래프, 해싱, 운영체제의 파일 구조, 메모리 관리, 객체지향 자료구조 라이브러리까지 다 연결되기 때문입니다.


연결리스트 기본

배열의 장점과 한계

먼저 배열을 다시 살펴보겠습니다.

int arr[5] = {10, 20, 30, 40, 50};

배열은 메모리에 연속적으로 저장됩니다.

arr[0] arr[1] arr[2] arr[3] arr[4]
 10     20     30     40     50

배열의 가장 큰 장점은 이것입니다.

인덱스로 바로 접근 가능
arr[3]

이렇게 하면 바로 네 번째 원소에 접근합니다.

시간 복잡도는
O(1)

그런데 배열에는 단점도 있습니다.

배열의 단점

단점설명
크기 고정처음 정한 크기를 바꾸기 어렵습니다
중간 삽입 불편뒤 원소들을 밀어야 합니다
중간 삭제 불편뒤 원소들을 앞으로 당겨야 합니다
연속 공간 필요큰 배열은 연속된 메모리 공간이 필요합니다

예를 들어 다음 배열이 있다고 가정하겠습니다.

[10, 20, 30, 40, 50]

20과 30 사이에 25를 넣고 싶으면?

[10, 20, 25, 30, 40, 50]

30, 40, 50을 뒤로 밀어야 합니다.

데이터가 많으면 많이 밀어야 하므로 중간 삽입은 보통
O(n)

입니다.

이런 문제를 해결하기 위해 나온 구조가 연결리스트입니다.


연결리스트란?

연결리스트는 데이터를 노드 단위로 저장하고, 각 노드가 다음 노드를 가리키는 방식의 자료구조입니다.

배열은 메모리에 연속적으로 저장됩니다.

10 20 30 40

연결리스트는 메모리 여기저기에 있는 노드들을 포인터로 연결합니다.

[10 | 다음주소] → [20 | 다음주소] → [30 | 다음주소] → NULL

정의: 연결리스트는 노드들이 포인터, 즉 링크로 연결된 선형 자료구조입니다.

자료구조 범위에서도 연결 리스트 단원은 노드, 포인터, 링크, 노드 생성, 노드 삽입, 노드 삭제를 핵심으로 다룹니다.


노드란?

연결리스트의 기본 단위는 노드입니다.

노드는 보통 두 부분으로 이루어집니다.

데이터 + 링크
그림으로 보면
[ data | link ]
부분의미
data실제 저장할 값
link다음 노드의 주소

예를 들어 값 10을 가진 노드는 이렇게 이해하면 됩니다.

[ 10 | 다음 노드 주소 ]

C언어에서는 보통 구조체와 포인터로 노드를 만듭니다.

struct Node {
    int data;
    struct Node *next;
};

여기서는 다음과 같습니다.

코드의미
int data실제 데이터
struct Node *next다음 노드의 주소

2장 4절에서 배운 구조체와 포인터가 여기서 바로 쓰입니다.

구조체 = data와 next를 하나로 묶음
포인터 = 다음 노드의 주소 저장

head 포인터

연결리스트에서는 첫 번째 노드를 가리키는 포인터가 필요합니다.

그 포인터를 보통 head라고 합니다.

head → [10 | next] → [20 | next] → [30 | NULL]

head는 리스트의 시작점을 알려줍니다.

만약 head가 없으면 첫 번째 노드가 어디 있는지 알 수 없습니다.

head = 리스트의 입구

마지막 노드의 next는 보통 NULL입니다.

NULL = 다음 노드가 없음
마지막 노드의 next = NULL

입니다.


배열과 연결리스트 비교

배열과 연결리스트는 둘 다 여러 데이터를 저장하는 선형 자료구조입니다. 하지만 저장 방식이 완전히 다릅니다.

구분배열연결리스트
저장 방식연속된 메모리노드를 포인터로 연결
접근인덱스로 바로 접근head부터 순서대로 따라감
임의 접근빠름, O(1)느림, O(n)
중간 삽입원소 이동 필요포인터만 바꾸면 가능
중간 삭제원소 이동 필요포인터만 바꾸면 가능
메모리크기 고정 경향필요할 때 동적 생성 가능
구현 난이도쉬움포인터 때문에 어려움

핵심은 이것입니다.

배열 = 찾기는 빠르지만 중간 삽입·삭제가 불편
연결리스트 = 찾기는 느리지만 삽입·삭제가 유리

단, 연결리스트도 “삽입할 위치의 노드를 이미 알고 있다면” 삽입·삭제가 빠릅니다. 그 위치를 찾기 위해 처음부터 따라가야 하면 탐색 비용은 O(n)이 듭니다.


단순 연결리스트 연산

단순 연결리스트

가장 기본적인 연결리스트는 단순 연결리스트입니다.

단순 연결리스트는 각 노드가 다음 노드만 가리킵니다.

head → [10 | next] → [20 | next] → [30 | NULL]
특징
한 방향으로만 이동 가능

즉, 10에서 20으로 갈 수 있고, 20에서 30으로 갈 수 있습니다.

하지만 30에서 20으로 바로 돌아갈 수는 없습니다.

왜냐하면 각 노드는 다음 노드의 주소만 알고 있기 때문입니다.


단순 연결리스트의 C 구조

C 코드로는 보통 이렇게 만듭니다.

typedef struct Node {
    int data;
    struct Node *next;
} Node;

이제 노드 변수를 만들 수 있습니다.

Node *head = NULL;
처음에는 리스트가 비어 있으므로
head = NULL

입니다.

노드를 동적으로 만들 때는 malloc을 씁니다.

Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = 10;
newNode->next = NULL;

여기서 ->는 구조체 포인터가 멤버에 접근할 때 쓰는 연산자입니다.

newNode->data = (*newNode).data

C프로그래밍 범위에도 구조체 포인터와 구조체 멤버 참조가 포함되어 있습니다.


동적 메모리 할당 맛보기

연결리스트는 보통 필요한 만큼 노드를 만듭니다.

이때 사용하는 것이 동적 메모리 할당입니다.

malloc
free

malloc

메모리를 새로 할당합니다.

Node *p = (Node *)malloc(sizeof(Node));
Node 하나를 저장할 공간을 메모리에 만들고,
그 주소를 p에 저장합니다.

free

사용이 끝난 메모리를 반환합니다.

free(p);
p가 가리키던 메모리를 다시 사용 가능하게 돌려줍니다.

연결리스트는 배열처럼 크기를 미리 크게 잡지 않아도 되고, 필요할 때 노드를 만들 수 있습니다. 그래서 동적 자료구조라고 부릅니다.


단순 연결리스트 순회

연결리스트의 모든 값을 출력하려면 처음부터 끝까지 따라가야 합니다.

head → 첫 번째 노드 → 두 번째 노드 → 세 번째 노드 → NULL
C 코드
Node *p = head;

while (p != NULL) {
    printf("%d ", p->data);
    p = p->next;
}
동작
p가 현재 노드를 가리킵니다.
p->data를 출력합니다.
p = p->next로 다음 노드로 이동합니다.
NULL을 만나면 끝납니다.

이 과정은 노드 개수만큼 반복됩니다.

시간 복잡도
O(n)

맨 앞에 노드 삽입

현재 리스트가 이렇게 있다고 가정하겠습니다.

head → [20] → [30] → NULL

여기에 10을 맨 앞에 넣고 싶습니다.

새 노드
[10]

삽입 순서가 중요합니다.

1단계: 새 노드의 next가 기존 head를 가리키게 합니다

[10] → [20] → [30] → NULL

2단계: head가 새 노드를 가리키게 합니다

head → [10] → [20] → [30] → NULL
C 코드
newNode->next = head;
head = newNode;

이 두 줄이 핵심입니다.

새 노드를 기존 첫 노드 앞에 연결
head를 새 노드로 변경

맨 앞 삽입은 head만 알면 됩니다.

시간 복잡도
O(1)

맨 뒤에 노드 삽입

현재 리스트
head → [10] → [20] → [30] → NULL

맨 뒤에 40을 넣고 싶습니다.

새 노드
[40 | NULL]

먼저 마지막 노드를 찾아야 합니다.

마지막 노드는 next == NULL인 노드입니다.

[30 | NULL]

그 다음 30의 next가 새 노드를 가리키게 합니다.

head → [10] → [20] → [30] → [40] → NULL
C 코드 흐름
Node *p = head;

while (p->next != NULL) {
    p = p->next;
}

p->next = newNode;

맨 뒤 삽입은 마지막 노드를 찾기 위해 처음부터 따라가야 할 수 있습니다.

시간 복잡도
O(n)

단, tail 포인터를 따로 두면 맨 뒤 삽입을 O(1)로 만들 수 있습니다.


중간 삽입

현재 리스트
head → [10] → [20] → [30] → NULL

20과 30 사이에 25를 넣고 싶습니다.

새 노드
[25]

20 노드를 prev라고 가정하겠습니다.

prev → [20]

삽입 순서는 중요합니다.

올바른 순서

newNode->next = prev->next;
prev->next = newNode;
그림
newNode->next = 30
20->next = newNode
결과
head → [10] → [20] → [25] → [30] → NULL

주의해야 합니다.

만약 순서를 반대로 하면?

prev->next = newNode;
newNode->next = prev->next;

이렇게 하면 30으로 가는 연결을 잃어버릴 수 있습니다.

그래서 연결리스트 삽입에서는 순서가 중요합니다.


노드 삭제

현재 리스트
head → [10] → [20] → [30] → NULL

20을 삭제하고 싶습니다.

삭제하려면 10의 next가 30을 가리키게 하면 됩니다.

head → [10] → [30] → NULL

20은 더 이상 리스트에 연결되어 있지 않습니다.

C 코드 흐름
prev->next = target->next;
free(target);

여기서는 다음과 같습니다.

변수의미
prev삭제할 노드의 이전 노드
target삭제할 노드
핵심
이전 노드가 삭제할 노드의 다음 노드를 가리키게 합니다.
삭제할 노드는 free로 반환합니다.

첫 노드 삭제

첫 번째 노드를 삭제할 때는 조금 다릅니다.

현재 리스트
head → [10] → [20] → [30] → NULL

10을 삭제하려면 head를 20으로 바꿔야 합니다.

Node *target = head;
head = head->next;
free(target);
결과
head → [20] → [30] → NULL

첫 노드 삭제에서는 이전 노드가 없습니다.

그래서 head를 직접 바꾸는 것이 중요합니다.


연결리스트 삽입·삭제의 핵심

연결리스트에서 삽입과 삭제는 결국 포인터 연결을 바꾸는 것입니다.

삽입

새 노드를 기존 노드 사이에 끼워 넣습니다.
핵심 코드
newNode->next = prev->next;
prev->next = newNode;

삭제

삭제할 노드를 건너뛰도록 연결합니다.
핵심 코드
prev->next = target->next;
free(target);

기억해 둡니다.

연결리스트는 데이터를 밀지 않습니다.
포인터를 바꿉니다.

이것이 배열과 가장 큰 차입니다.


연결리스트 시간 복잡도

연산시간 복잡도설명
첫 노드 삽입O(1)head만 바꾸면 됨
첫 노드 삭제O(1)head만 바꾸면 됨
특정 값 탐색O(n)처음부터 따라가야 함
특정 위치 접근O(n)인덱스로 바로 못 감
위치를 알고 있는 삽입O(1)포인터만 바꿈
위치를 알고 있는 삭제O(1)포인터만 바꿈
마지막 삽입O(n), tail 있으면 O(1)마지막 노드 탐색 필요
배열과 다시 비교하면
배열: 접근 O(1), 중간 삽입·삭제 O(n)
연결리스트: 접근 O(n), 연결 위치를 알면 삽입·삭제 O(1)

연결리스트 변형

원형 연결리스트

원형 연결리스트는 마지막 노드가 NULL을 가리키는 대신 첫 번째 노드를 가리키는 구조입니다.

단순 연결리스트
head → [10] → [20] → [30] → NULL
원형 연결리스트
head → [10] → [20] → [30]
        ↑                 ↓
        └─────────────────┘
마지막 노드의 next = head

특징은 다음과 같습니다.

특징설명
끝이 없음마지막 다음이 첫 번째
순환 구조계속 돌 수 있음
응용원형 큐, 순환 작업, 라운드 로빈

운영체제의 라운드 로빈 스케줄링처럼 순서대로 돌아가며 처리하는 구조와 잘 맞습니다. 운영체제 범위에도 라운드 로빈 스케줄링이 포함되어 있습니다.

주의할 점
while (p != NULL)

같은 조건으로 순회하면 끝나지 않을 수 있습니다.

원형 연결리스트는 종료 조건을 따로 잘 잡아야 합니다.


이중 연결리스트

단순 연결리스트는 다음 노드만 알 수 있습니다.

[prev 없음 | data | next]

하지만 이중 연결리스트는 이전 노드와 다음 노드를 모두 가리킵니다.

[prev | data | next]
그림
NULL ← [10] ⇄ [20] ⇄ [30] → NULL
노드 구조
typedef struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
} DNode;
이중 연결리스트의 장점
앞으로도 이동 가능
뒤로도 이동 가능
단점
포인터가 하나 더 필요하므로 메모리를 더 씁니다.
삽입·삭제 시 prev와 next를 모두 조정해야 합니다.

자료구조 범위에도 이중 연결 리스트의 필요성, 정의, 노드 삭제, 노드 삽입이 포함되어 있습니다.


연결리스트의 응용

연결리스트는 여러 자료구조의 기반이 됩니다.

응용설명
연결 스택노드를 push/pop
연결 큐노드를 enqueue/dequeue
다항식 표현항을 노드로 저장
희소 행렬 표현0이 아닌 원소만 노드로 저장
그래프 인접 리스트각 정점의 이웃을 리스트로 저장
메모리 가용 공간 관리빈 공간을 연결리스트로 관리

자료구조 범위에도 연결 리스트 단원 안에 동적 연결 스택과 큐, 비사용 기억 공간, 희소 행렬의 연결 리스트 표현, 다항식의 연결 리스트 표현, 원형 연결 리스트, 이중 연결 리스트, 일반 리스트가 포함되어 있습니다.

지금은 전부 깊게 들어가지는 않습니다. 핵심은 이것입니다.

연결리스트 = 포인터로 노드를 연결하는 기본 구조

이것을 알아야 뒤의 트리와 그래프가 쉬워집니다.


트리 기본

트리란?

이제 트리로 넘어갑니다.

트리는 부모와 자식 관계를 가진 계층적 자료구조입니다.

예를 들어 가족 관계를 생각해 보겠습니다.

할아버지
 ├─ 아버지
 │   ├─ 나
 │   └─ 동생
 └─ 삼촌

컴퓨터 폴더 구조도 트리입니다.

C드라이브
 ├─ Program Files
 ├─ Users
 │   ├─ Kim
 │   └─ Lee
 └─ Windows

회사 조직도도 트리입니다.

대표
 ├─ 개발팀
 ├─ 영업팀
 └─ 인사팀

정의: 트리는 노드들이 부모-자식 관계로 연결된 비선형 자료구조입니다.

자료구조 범위와 이산수학 범위 모두 트리의 개념과 이진트리 표현을 다룹니다.


트리 용어

트리에서는 용어가 중요합니다.

다음 트리를 살펴보겠습니다.

        A
      /   \
     B     C
    / \     \
   D   E     F

루트

가장 위에 있는 노드입니다.

A = 루트

부모

어떤 노드 바로 위의 노드입니다.

B의 부모 = A
D의 부모 = B

자식

어떤 노드 바로 아래의 노드입니다.

A의 자식 = B, C
B의 자식 = D, E

형제

부모가 같은 노드입니다.

B와 C는 형제
D와 E는 형제

단말노드, 리프 노드

자식이 없는 노드입니다.

D, E, F = 단말노드

내부노드

자식이 있는 노드입니다.

A, B, C = 내부노드

트리의 차수, 레벨, 높이

차수

노드의 차수는 자식 수입니다.

A의 차수 = 2
B의 차수 = 2
C의 차수 = 1
D의 차수 = 0

트리의 차수는 노드 차수 중 가장 큰 값입니다.

위 트리에서 최대 자식 수는 2다.

트리의 차수 = 2

레벨

루트에서부터의 층입니다.

보통 루트를 레벨 0 또는 레벨 1로 잡습니다. 시험에서는 문제에서 기준을 확인해야 합니다.

레벨 0 기준이면
A = 레벨 0
B, C = 레벨 1
D, E, F = 레벨 2
레벨 1 기준이면
A = 레벨 1
B, C = 레벨 2
D, E, F = 레벨 3

높이

트리의 높이는 루트에서 가장 깊은 노드까지의 길입니다.

기준에 따라 간선 수로 세기도 하고, 레벨 수로 세기도 합니다. 시험에서 기준이 중요합니다.

처음에는 이렇게 이해하면 됩니다.

높이 = 트리가 몇 층까지 내려가는가

트리의 특징

트리는 그래프의 한 종류로 볼 수 있지만, 중요한 제한이 있습니다.

트리의 특징
1. 사이클이 없습니다.
2. 하나의 루트가 있습니다.
3. 부모-자식 계층 구조입니다.
4. 노드가 n개이면 간선은 n-1개입니다.

특히 이 공식은 중요합니다.

트리의 노드 수가 n개이면 간선 수는 n - 1개

예를 들어 노드가 6개인 트리는 간선이 5개입니다.

A, B, C, D, E, F = 6개
간선 = 5개

이진트리

이진트리란?

이진트리는 각 노드가 최대 2개의 자식을 가질 수 있는 트리입니다.

자식 수가 0개, 1개, 2개까지 가능
3개는 불가능
        A
      /   \
     B     C
    / \     \
   D   E     F

이 트리는 이진트리입니다.

각 노드의 자식이 최대 2개이기 때문입니다.

이진트리에서는 자식을 보통 이렇게 부릅니다.

왼쪽 자식
오른쪽 자식

즉, 자식의 위치가 중요합니다.


일반 트리와 이진트리 차이

일반 트리
노드가 자식을 여러 개 가질 수 있습니다.
        A
   /    |    \
  B     C     D

A의 자식이 3개입니다. 이것은 일반 트리는 가능하지만 이진트리는 아닙니다.

이진트리
자식이 최대 2개
        A
      /   \
     B     C
구분일반 트리이진트리
자식 수제한이 없을 수 있음최대 2개
자식 위치순서가 덜 중요할 수 있음왼쪽/오른쪽 구분
표현다양함배열 또는 링크로 표현하기 좋음
순회여러 방식전위·중위·후위가 중요

포화 이진트리

포화 이진트리는 모든 레벨이 꽉 찬 이진트리입니다.

        A
      /   \
     B     C
    / \   / \
   D   E F   G
특징
모든 내부 노드가 자식 2개를 가집니다.
모든 단말 노드가 같은 레벨에 있습니다.
높이가 h인 포화 이진트리의 노드 수는 기준에 따라 다르지만, 레벨을 0부터 h까지 본다면
노드 수 = 2^(h+1) - 1
레벨 0: 1개
레벨 1: 2개
레벨 2: 4개
총 1 + 2 + 4 = 7개

완전 이진트리

완전 이진트리는 마지막 레벨을 제외한 레벨은 모두 꽉 차 있고, 마지막 레벨은 왼쪽부터 채워진 이진트리입니다.

        A
      /   \
     B     C
    / \   /
   D   E F

마지막 레벨에서 왼쪽부터 D, E, F가 채워져 있습니다.

이것은 완전 이진트리입니다.

하지만 이런 구조는 완전 이진트리가 아닙니다.

        A
      /   \
     B     C
      \     \
       E     F

마지막 레벨이 왼쪽부터 채워져 있지 않기 때문입니다.

자료구조 예시문제에서도 “트리의 최대 레벨이 k일 때, 레벨 k-1까지는 모든 자식노드가 채워지고, 레벨 k에서는 왼쪽부터 오른쪽으로 채워진 이진트리”를 묻고, 정답은 완전 이진트리입니다.


포화 이진트리와 완전 이진트리 비교

구분포화 이진트리완전 이진트리
영어Full Binary Tree 또는 Perfect Binary Tree 문맥Complete Binary Tree
조건모든 레벨이 꽉 참마지막 전까지 꽉 참, 마지막은 왼쪽부터
마지막 레벨꽉 차야 함덜 차도 됨
1, 3, 7, 15개 노드힙에서 많이 사용
짧게 외우면
포화 = 전부 꽉 참
완전 = 마지막만 왼쪽부터 채워짐

이진트리의 배열 표현

완전 이진트리는 배열로 표현하기 좋습니다.

인덱스를 1부터 쓴다고 하면
부모 i
왼쪽 자식 = 2i
오른쪽 자식 = 2i + 1
부모 = i / 2
        A(1)
      /      \
   B(2)      C(3)
   / \       /
D(4) E(5)  F(6)

배열은 다음과 같습니다.

인덱스
1A
2B
3C
4D
5E
6F
공식
B의 인덱스 = 2
B의 왼쪽 자식 = 4 → D
B의 오른쪽 자식 = 5 → E
C의 왼쪽 자식 = 6 → F

배열 표현은 힙에서 많이 씁니다. 힙은 나중에 정렬과 우선순위 큐에서 중요합니다.


이진트리의 링크 표현

일반적인 이진트리는 포인터로 표현하기도 합니다.

노드 구조
typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

각 노드는 세 부분을 가집니다.

왼쪽 자식 주소 + 데이터 + 오른쪽 자식 주소
그림
[ left | data | right ]
        A
      /   \
     B     C

A 노드는 B의 주소와 C의 주소를 가지고 있습니다.

A.left = B
A.right = C
B와 C가 자식이 없다면
B.left = NULL
B.right = NULL
C.left = NULL
C.right = NULL

트리도 결국 구조체와 포인터로 만듭니다.


이진트리 순회

이진트리 순회란?

순회는 트리의 모든 노드를 한 번씩 방문하는 것입니다.

배열은 순서대로 보면 됩니다.

arr[0], arr[1], arr[2], ...

하지만 트리는 가지가 있습니다.

        A
      /   \
     B     C
    / \   / \
   D   E F   G

그래서 어떤 순서로 방문할지 정해야 합니다.

이진트리의 대표 순회는 세 가지입니다.

전위순회
중위순회
후위순회

자료구조 범위에도 이진 트리 순회로 중위 순회, 후위 순회, 전위 순회가 포함되어 있습니다.


순회의 기준: 루트의 위치

전위, 중위, 후위는 “루트 노드를 언제 방문하느냐”로 구분합니다.

순회순서루트 위치
전위순회루트 → 왼쪽 → 오른쪽
중위순회왼쪽 → 루트 → 오른쪽가운데
후위순회왼쪽 → 오른쪽 → 루트
짧게 외우면
전위 = 루트 먼저
중위 = 루트 가운데
후위 = 루트 나중

영어로는은 다음과 같습니다.

한글영어
전위순회Preorder
중위순회Inorder
후위순회Postorder

예제 트리

이 트리로 순회를 연습하자.

        A
      /   \
     B     C
    / \   / \
   D   E F   G
구조
A의 왼쪽 = B
A의 오른쪽 = C
B의 왼쪽 = D
B의 오른쪽 = E
C의 왼쪽 = F
C의 오른쪽 = G

이제 전위, 중위, 후위를 각각 해 보겠습니다.


전위순회

전위순회 순서
루트 → 왼쪽 → 오른쪽
현재 노드를 먼저 방문합니다.
그다음 왼쪽 서브트리.
그다음 오른쪽 서브트리.
트리
        A
      /   \
     B     C
    / \   / \
   D   E F   G

전위순회 과정

  1. 루트 A 방문
A
  1. 왼쪽 서브트리 B로 이동 B도 루트처럼 먼저 방문
A B
  1. B의 왼쪽 D 방문
A B D
  1. B의 오른쪽 E 방문
A B D E
  1. 오른쪽 서브트리 C 방문
A B D E C
  1. C의 왼쪽 F 방문
A B D E C F
  1. C의 오른쪽 G 방문
A B D E C F G
정답
A B D E C F G
전위순회
ABDECFG

중위순회

중위순회 순서
왼쪽 → 루트 → 오른쪽
왼쪽 서브트리를 먼저 방문합니다.
그다음 현재 노드.
그다음 오른쪽 서브트리.
트리
        A
      /   \
     B     C
    / \   / \
   D   E F   G

중위순회 과정

A를 기준으로 보면
왼쪽 B 서브트리 → A → 오른쪽 C 서브트리

먼저 B 서브트리를 중위순회합니다.

B 기준
D → B → E

그다음 A 방문.

그다음 C 서브트리를 중위순회합니다.

C 기준
F → C → G
합치면
D B E A F C G
정답
DBEAFCG

후위순회

후위순회 순서
왼쪽 → 오른쪽 → 루트
왼쪽 서브트리를 먼저 방문합니다.
오른쪽 서브트리를 방문합니다.
마지막에 현재 노드를 방문합니다.
트리
        A
      /   \
     B     C
    / \   / \
   D   E F   G

후위순회 과정

A 기준
왼쪽 B 서브트리 → 오른쪽 C 서브트리 → A
B 서브트리를 후위순회하면
D → E → B
C 서브트리를 후위순회하면
F → G → C

마지막에 A.

합치면
D E B F G C A
정답
DEBFGCA

순회 결과 비교

같은 트리
        A
      /   \
     B     C
    / \   / \
   D   E F   G

순회 결과는 다음과 같습니다.

순회순서
전위순회A B D E C F G
중위순회D B E A F C G
후위순회D E B F G C A
짧게
전위: ABDECFG
중위: DBEAFCG
후위: DEBFGCA

순회를 쉽게 푸는 방법

트리 순회 문제를 풀 때는 큰 트리를 한 번에 보지 말고, 작은 서브트리로 나눠라.

        A
      /   \
     B     C
전위
A B C
중위
B A C
후위
B C A

여기에 B와 C 아래의 자식들을 같은 규칙으로 넣으면 됩니다.

순회는 재귀적으로 생각하면 쉽습니다.

전위(node):
    node 방문
    왼쪽 전위
    오른쪽 전위

중위(node):
    왼쪽 중위
    node 방문
    오른쪽 중위

후위(node):
    왼쪽 후위
    오른쪽 후위
    node 방문

2장 3절의 재귀와 여기서 다시 연결됩니다.


순회 C 코드

이진트리 노드 구조
typedef struct TreeNode {
    char data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

전위순회

void preorder(TreeNode *root) {
    if (root != NULL) {
        printf("%c ", root->data);
        preorder(root->left);
        preorder(root->right);
    }
}
순서
루트 출력
왼쪽
오른쪽

중위순회

void inorder(TreeNode *root) {
    if (root != NULL) {
        inorder(root->left);
        printf("%c ", root->data);
        inorder(root->right);
    }
}
순서
왼쪽
루트 출력
오른쪽

후위순회

void postorder(TreeNode *root) {
    if (root != NULL) {
        postorder(root->left);
        postorder(root->right);
        printf("%c ", root->data);
    }
}
순서
왼쪽
오른쪽
루트 출력

재귀 함수 구조와 완전히 연결됩니다.


시험 예시형 트리 순회

자료구조 예시문제에도 이진 트리의 후위순회 결과를 묻는 문제가 나옵니다.

트리 그림 문제는 보통 이렇게 풀이할 수 있습니다.

1. 루트를 찾습니다.
2. 왼쪽 서브트리를 먼저 봅니다.
3. 오른쪽 서브트리를 봅니다.
4. 후위순회라면 루트는 마지막에 씁니다.
후위순회는 항상
왼쪽 → 오른쪽 → 루트

입니다.

예를 들어
        A
      /   \
     B     C
    /     /
   D     E

후위순회는 다음과 같습니다.

  1. 왼쪽 B 서브트리
D B
  1. 오른쪽 C 서브트리
E C
  1. 루트 A
A
결과
D B E C A
DBECA

시험에서는 루트를 마지막에 써야 한다는 점을 자주 놓칩니다.


전위·중위·후위 빠른 구분법

순회 문제에서 가장 먼저 할 일은 루트가 어디에 나오는지 보는 것입니다.

순회루트 위치
전위맨 앞
중위왼쪽과 오른쪽 사이
후위맨 뒤

예를 들어 어떤 트리의 순회 결과가 다음과 같다고 가정하겠습니다.

전위: A B D E C

전위는 루트가 맨 앞입니다.

루트 = A
후위 결과가
D E B C A

후위는 루트가 맨 뒤입니다.

루트 = A

나중에 전위+중위 또는 중위+후위 결과를 보고 트리를 복원하는 문제도 이런 원리로 풉니다.


트리 응용

이진 탐색 트리 맛보기

이진트리의 응용 중 하나가 이진 탐색 트리입니다.

이진 탐색 트리는 데이터를 빠르게 찾기 위한 트리입니다.

규칙
왼쪽 서브트리의 값 < 루트 값 < 오른쪽 서브트리의 값
        50
      /    \
    30      70
   /  \    /  \
 20   40  60   80

50보다 작은 값은 왼쪽에 있습니다.

30, 20, 40

50보다 큰 값은 오른쪽에 있습니다.

70, 60, 80

이 구조를 잘 유지하면 탐색이 빠릅니다.

자료구조 범위에도 이진 탐색 트리의 탐색, 삽입, 삭제, 높이가 포함되어 있습니다.


이진 탐색 트리와 중위순회

이진 탐색 트리에서 중위순회를 하면 값이 정렬된 순서로 나옵니다.

트리
        50
      /    \
    30      70
   /  \    /  \
 20   40  60   80
중위순회는
왼쪽 → 루트 → 오른쪽
결과
20 30 40 50 60 70 80

정렬된 순서입니다.

이것은 매우 중요합니다.

이진 탐색 트리 + 중위순회 = 오름차순 출력

수식 트리와 순회

트리는 수식을 표현하는 데도 쓰입니다.

예를 들어 수식
A + B * C

수식 트리는 이렇게 만들 수 있습니다.

      +
     / \
    A   *
       / \
      B   C

이 트리를 순회하면 표기법이 나옵니다.

전위순회

+ A * B C

전위표기식입니다.

중위순회

A + B * C

중위표기식입니다.

후위순회

A B C * +

후위표기식입니다.

3장 2절에서 배운 수식 표기법이 트리 순회와 연결됩니다.

전위순회 = 전위표기식
중위순회 = 중위표기식
후위순회 = 후위표기식

연결리스트와 트리의 연결

연결리스트와 트리는 둘 다 포인터 기반 구조입니다.

연결리스트 노드
struct Node {
    int data;
    struct Node *next;
};
트리 노드
struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
};

차이는 포인터 개수와 의미입니다.

구조포인터의미
단순 연결리스트next 1개다음 노드
이중 연결리스트prev, next 2개이전, 다음 노드
이진트리left, right 2개왼쪽 자식, 오른쪽 자식
연결리스트 = 한 줄로 연결
트리 = 가지처럼 연결

자주 혼동되는 출제 포인트

혼동 포인트 1. 배열과 연결리스트 차이

배열 = 연속 저장, 인덱스 접근 빠름
연결리스트 = 포인터 연결, 삽입·삭제 유리

혼동 포인트 2. 연결리스트는 인덱스로 바로 못 갑니다

세 번째 노드를 보려면 head부터 따라가야 합니다.

그래서 특정 위치 접근은 O(n)입니다.


혼동 포인트 3. 마지막 노드의 next는 NULL

단순 연결리스트에서 마지막 노드는 보통
next = NULL

입니다.

원형 연결리스트에서는
마지막 노드의 next = head

입니다.


혼동 포인트 4. 삽입 순서가 중요합니다

중간 삽입은 보통
newNode->next = prev->next;
prev->next = newNode;

순서를 반대로 하면 기존 연결을 잃어버릴 수 있습니다.


혼동 포인트 5. 구조체 포인터는 ->

p->data
(*p).data

와 같은 뜻입니다.


혼동 포인트 6. 트리의 루트는 가장 위 노드

루트 = 부모가 없는 노드

혼동 포인트 7. 단말노드는 자식이 없는 노드

leaf node = 자식 0개

혼동 포인트 8. 완전 이진트리는 마지막 레벨이 왼쪽부터 채워집니다

포화 = 전부 꽉 참
완전 = 마지막 레벨만 덜 찰 수 있고 왼쪽부터 채움

혼동 포인트 9. 순회에서 루트 위치를 기억해야 합니다

전위 = 루트 먼저
중위 = 루트 가운데
후위 = 루트 마지막

혼동 포인트 10. 후위순회는 루트를 마지막에 씁니다

트리 문제에서 후위순회 결과를 구할 때 루트를 먼저 쓰면 틀립니다.

후위 = 왼쪽 → 오른쪽 → 루트

이번 절의 핵심 정리

연결리스트

노드들을 포인터로 연결한 선형 자료구조

노드

데이터 + 링크
C 구조
struct Node {
    int data;
    struct Node *next;
};
리스트의 첫 번째 노드를 가리키는 포인터

단순 연결리스트

각 노드가 다음 노드만 가리킴

원형 연결리스트

마지막 노드가 첫 번째 노드를 가리킴

이중 연결리스트

이전 노드와 다음 노드를 모두 가리킴

트리

부모-자식 관계를 가진 계층적 자료구조

이진트리

각 노드의 자식이 최대 2개인 트리

완전 이진트리

마지막 레벨을 제외하고 꽉 차 있으며, 마지막 레벨은 왼쪽부터 채워진 트리

전위순회

루트 → 왼쪽 → 오른쪽

중위순회

왼쪽 → 루트 → 오른쪽

후위순회

왼쪽 → 오른쪽 → 루트

핵심 한 문장

이번 절의 핵심을 한 문장으로 정리하면 다음과 같습니다.

연결리스트는 노드를 포인터로 연결해 삽입과 삭제를 유연하게 만드는 구조이고, 트리는 부모-자식 관계를 표현하는 계층 구조이며, 이진트리 순회는 루트를 방문하는 위치에 따라 전위·중위·후위로 나뉩니다.
더 짧게
연결리스트 = 포인터로 연결
트리 = 계층 구조
전위 = 루트 먼저
중위 = 루트 가운데
후위 = 루트 마지막

확인 문제

문제 1

연결리스트의 기본 단위는?

① 배열 ② 노드 ③ 큐 ④ 힙


문제 2

단순 연결리스트의 노드는 보통 무엇으로 구성되는가?

① 데이터와 링크 ② 인덱스와 배열 크기 ③ front와 rear ④ top과 bottom


문제 3

단순 연결리스트에서 마지막 노드의 next는 보통 무엇을 가리키는가?

① head ② NULL ③ 이전 노드 ④ 배열의 첫 칸


문제 4

연결리스트의 장점으로 알맞은 것은?

① 인덱스로 항상 O(1) 접근 가능합니다 ② 중간 삽입·삭제 시 원소들을 모두 밀 필요가 없습니다 ③ 반드시 연속된 메모리 공간이 필요합니다 ④ 배열보다 항상 탐색이 빠릅니다


문제 5

구조체 포인터 p의 멤버 data에 접근하는 올바른 표현은?

p.datap->datap:datap#data


문제 6

원형 연결리스트의 특징으로 알맞은 것은?

① 마지막 노드가 NULL만 가리킵니다 ② 마지막 노드가 첫 번째 노드를 가리킵니다 ③ 모든 노드가 두 개의 부모를 가집니다 ④ 노드가 반드시 정렬되어 있습니다


문제 7

이중 연결리스트의 노드가 가지는 포인터로 알맞은 것은?

top, rearleft, rightprev, nextfront, back


문제 8

트리에서 가장 위에 있는 노드를 무엇이라고 하는가?

① 루트 ② 단말노드 ③ 형제노드 ④ 간선


문제 9

자식이 없는 노드를 무엇이라고 하는가?

① 루트노드 ② 단말노드 ③ 부모노드 ④ 내부노드


문제 10

이진트리의 특징으로 알맞은 것은?

① 모든 노드가 반드시 자식 3개를 가집니다 ② 각 노드가 최대 2개의 자식을 가집니다 ③ 배열로 절대 표현할 수 없습니다 ④ 루트가 반드시 2개 이상입니다


문제 11

다음 설명에 해당하는 이진트리는?

마지막 레벨을 제외한 모든 레벨이 채워져 있고,
마지막 레벨은 왼쪽부터 채워진 이진트리

① 포화 이진트리 ② 완전 이진트리 ③ 일반 리스트 ④ 원형 트리


문제 12

전위순회의 순서로 알맞은 것은?

① 왼쪽 → 루트 → 오른쪽 ② 왼쪽 → 오른쪽 → 루트 ③ 루트 → 왼쪽 → 오른쪽 ④ 오른쪽 → 루트 → 왼쪽


문제 13

중위순회의 순서로 알맞은 것은?

① 루트 → 왼쪽 → 오른쪽 ② 왼쪽 → 루트 → 오른쪽 ③ 왼쪽 → 오른쪽 → 루트 ④ 오른쪽 → 왼쪽 → 루트


문제 14

후위순회의 순서로 알맞은 것은?

① 루트 → 왼쪽 → 오른쪽 ② 왼쪽 → 루트 → 오른쪽 ③ 왼쪽 → 오른쪽 → 루트 ④ 루트 → 오른쪽 → 왼쪽


문제 15

다음 이진트리의 전위순회 결과는?

        A
      /   \
     B     C
    / \   / \
   D   E F   G

DBEAFCGABDECFGDEBFGCAABCDEFG


문제 16

위 이진트리의 중위순회 결과는?

DBEAFCGABDECFGDEBFGCAABCDEFG


문제 17

위 이진트리의 후위순회 결과는?

DBEAFCGABDECFGDEBFGCAABCDEFG


정답과 해설은 절별 확인문제 정답해설에서 확인합니다.

다음 3장 4절은 그래프, DFS/BFS, 정렬, 탐색, 해싱입니다. 3장 3절의 트리가 “부모-자식 관계”였다면, 3장 4절의 그래프는 더 일반적인 “연결 관계”를 다룹니다.

목차

이번 절의 큰 그림
연결리스트 기본
배열의 장점과 한계
배열의 단점
연결리스트란?
노드란?
head 포인터
배열과 연결리스트 비교
단순 연결리스트 연산
단순 연결리스트
단순 연결리스트의 C 구조
동적 메모리 할당 맛보기
malloc
free
단순 연결리스트 순회
맨 앞에 노드 삽입
1단계: 새 노드의 next가 기존 head를 가리키게 합니다
2단계: head가 새 노드를 가리키게 합니다
맨 뒤에 노드 삽입
중간 삽입
올바른 순서
노드 삭제
첫 노드 삭제
연결리스트 삽입·삭제의 핵심
삽입
삭제
연결리스트 시간 복잡도
연결리스트 변형
원형 연결리스트
이중 연결리스트
연결리스트의 응용
트리 기본
트리란?
트리 용어
루트
부모
자식
형제
단말노드, 리프 노드
내부노드
트리의 차수, 레벨, 높이
차수
레벨
높이
트리의 특징
이진트리
이진트리란?
일반 트리와 이진트리 차이
포화 이진트리
완전 이진트리
포화 이진트리와 완전 이진트리 비교
이진트리의 배열 표현
이진트리의 링크 표현
이진트리 순회
이진트리 순회란?
순회의 기준: 루트의 위치
예제 트리
전위순회
전위순회 과정
중위순회
중위순회 과정
후위순회
후위순회 과정
순회 결과 비교
순회를 쉽게 푸는 방법
순회 C 코드
전위순회
중위순회
후위순회
시험 예시형 트리 순회
전위·중위·후위 빠른 구분법
트리 응용
이진 탐색 트리 맛보기
이진 탐색 트리와 중위순회
수식 트리와 순회
전위순회
중위순회
후위순회
연결리스트와 트리의 연결
자주 혼동되는 출제 포인트
혼동 포인트 1. 배열과 연결리스트 차이
혼동 포인트 2. 연결리스트는 인덱스로 바로 못 갑니다
혼동 포인트 3. 마지막 노드의 next는 NULL
혼동 포인트 4. 삽입 순서가 중요합니다
혼동 포인트 5. 구조체 포인터는 ->
혼동 포인트 6. 트리의 루트는 가장 위 노드
혼동 포인트 7. 단말노드는 자식이 없는 노드
혼동 포인트 8. 완전 이진트리는 마지막 레벨이 왼쪽부터 채워집니다
혼동 포인트 9. 순회에서 루트 위치를 기억해야 합니다
혼동 포인트 10. 후위순회는 루트를 마지막에 씁니다
이번 절의 핵심 정리
연결리스트
노드
head
단순 연결리스트
원형 연결리스트
이중 연결리스트
트리
이진트리
완전 이진트리
전위순회
중위순회
후위순회
핵심 한 문장
확인 문제
문제 1
문제 2
문제 3
문제 4
문제 5
문제 6
문제 7
문제 8
문제 9
문제 10
문제 11
문제 12
문제 13
문제 14
문제 15
문제 16
문제 17