트리는 계층 구조를 표현하는 기본적인 자료 구조이다. 노드와 간선으로 구성되며, 최상위 노드를 루트 노드라고 부른다. 트리는 파일 시스템, 조직도, XML 문서 구조 등 다양한 분야에서 계층적 데이터를 표현하는 데 널리 사용된다.
이진 탐색 트리는 트리의 특별한 형태로, 각 노드가 최대 두 개의 자식 노드를 가지는 이진 트리에 추가적인 정렬 속성을 부여한 것이다. 이 속성 덕분에 데이터의 효율적인 탐색, 삽입, 삭제가 가능해진다. BST는 정렬된 데이터를 유지하면서도 배열보다 유연한 구조를 제공한다.
트리와 BST는 알고리즘과 소프트웨어 공학의 핵심 개념으로, 데이터베이스 인덱싱, 심볼 테이블, 우선순위 큐 등 여러 고급 자료 구조와 알고리즘의 기초를 이룬다. 특히 BST의 변형인 균형 이진 탐색 트리는 성능 저하를 방지하는 데 중요한 역할을 한다.
트리는 노드와 간선으로 구성된 계층적 자료 구조이다. 하나의 루트 노드에서 시작하여 각 노드는 0개 이상의 자식 노드를 가질 수 있으며, 사이클이 존재하지 않는다는 특징이 있다. 이는 그래프의 특수한 형태로 볼 수 있다. 트리는 데이터를 계층적으로 표현하고 저장하는 데 널리 사용된다.
트리를 설명하는 주요 용어는 다음과 같다.
용어 | 설명 |
|---|---|
트리의 최상위 노드로, 부모 노드가 없다. | |
특정 노드와 직접 연결된 상위 노드이다. | |
특정 노드와 직접 연결된 하위 노드이다. | |
자식 노드가 없는 단말 노드이다. | |
두 노드를 연결하는 선이다. | |
루트 노드에서 특정 노드까지의 경로에 있는 간선의 수이다. | |
트리에서 가장 깊은 노드의 깊이이다. | |
한 노드가 가진 자식 노드의 수이다. |
트리는 구조와 제약 조건에 따라 여러 종류로 나뉜다. 가장 기본적인 분류는 이진 트리와 이진 탐색 트리이다. 이진 트리는 각 노드의 자식 노드가 최대 두 개인 트리이다. 이진 탐색 트리는 이진 트리의 일종으로, 각 노드의 왼쪽 서브트리에는 해당 노드의 값보다 작은 값들만, 오른쪽 서브트리에는 큰 값들만 존재하도록 구성된 트리이다. 이 외에도 균형 트리, 포화 이진 트리, 완전 이진 트리 등 다양한 특성을 가진 트리가 존재한다.
트리는 그래프의 일종으로, 노드와 간선으로 구성된 비선형 자료 구조이다. 트리는 계층적 관계를 표현하는 데 사용되며, 최상위에 하나의 루트 노드가 존재한다. 루트 노드를 제외한 모든 노드는 정확히 하나의 부모 노드를 가지며, 여러 개의 자식 노드를 가질 수 있다. 자식이 없는 노드를 리프 노드 또는 단말 노드라고 부른다.
트리에서 사용되는 주요 용어는 다음과 같다.
용어 | 설명 |
|---|---|
트리를 구성하는 기본 요소. 데이터와 다른 노드에 대한 링크를 포함한다. | |
두 노드를 연결하는 선. 부모-자식 관계를 나타낸다. | |
트리의 최상위 노드. 부모 노드가 존재하지 않는다. | |
특정 노드와 직접 연결된 상위 노드. | |
특정 노드와 직접 연결된 하위 노드. | |
자식 노드가 하나도 없는 노드. 트리의 끝단에 위치한다. | |
리프 노드가 아닌, 적어도 하나의 자식을 가진 노드. | |
루트 노드에서 특정 노드까지의 경로에 있는 간선의 수. | |
트리에서 가장 깊은 노드의 깊이. 트리의 전체적인 레벨 수를 의미한다. | |
한 노드가 가지고 있는 자식 노드의 수. | |
트리 내의 한 노드와 그 모든 자손 노드들로 구성된 부분 트리. |
트리의 구조는 재귀적으로 정의된다. 즉, 하나의 노드와 그 자식 노드들의 서브트리들의 집합으로 볼 수 있다. 이 재귀적 특성은 트리를 다루는 많은 알고리즘의 기초가 된다.
트리는 계층적 구조를 가지며, 각 노드가 0개 이상의 자식 노드를 가질 수 있는 비순환 그래프이다. 트리의 종류는 노드의 최대 자식 수, 균형 상태, 용도 등에 따라 다양하게 분류된다.
자식 노드의 최대 개수에 따른 분류가 가장 일반적이다. 각 노드가 최대 두 개의 자식 노드(왼쪽 자식과 오른쪽 자식)를 가지는 트리를 이진 트리라고 한다. 각 노드가 임의의 개수의 자식 노드를 가질 수 있는 트리는 다진 트리 또는 M-ary 트리라고 부른다. 특히 모든 노드가 0개 또는 2개의 자식 노드를 가지는 이진 트리를 완전 이진 트리라고 하며, 마지막 레벨을 제외한 모든 레벨의 노드가 채워져 있는 트리를 포화 이진 트리라고 한다.
트리의 균형 상태에 따른 분류도 중요하다. 모든 리프 노드의 깊이 차이가 1 이내인 트리를 균형 트리라고 한다. 반면, 노드들이 한쪽 방향으로 치우쳐 연결 리스트와 유사한 형태를 띠는 트리는 편향 트리라고 부른다. 편향 트리는 연산 효율이 크게 저하된다는 단점이 있다. 주요 균형 트리의 종류는 다음과 같다.
트리 종류 | 설명 | 주요 특징 |
|---|---|---|
최초로 발명된 균형 이진 탐색 트리 | 각 노드의 왼쪽/오른쪽 서브트리 높이 차이가 1을 넘지 않음 | |
널리 사용되는 균형 이진 탐색 트리 | 노드에 색깔(레드/블랙) 속성을 부여하여 균형 유지 | |
데이터베이스와 파일 시스템에 주로 사용 | 하나의 노드가 여러 키와 자식을 가질 수 있는 다진 균형 트리 |
용도에 따라서는 이진 탐색 트리처럼 탐색을 효율적으로 하기 위한 트리, 힙처럼 우선순위 큐 구현을 위한 트리, 구간 트리처럼 구간 질의를 처리하기 위한 트리 등으로 구분된다. 또한 표현식의 연산 순서를 나타내는 표현식 트리, 파일 시스템의 디렉토리 구조를 표현하는 트리 등 다양한 특수 목적의 트리가 존재한다.
이진 탐색 트리는 이진 트리의 일종으로, 특정한 순서 속성을 만족하는 자료 구조이다. 모든 노드에 대해, 해당 노드의 왼쪽 서브트리에 있는 모든 노드의 키 값은 현재 노드의 키 값보다 작고, 오른쪽 서브트리에 있는 모든 노드의 키 값은 현재 노드의 키 값보다 크다는 규칙을 따른다. 이 규칙은 트리의 모든 노드에 대해 재귀적으로 적용된다.
이 핵심 속성 덕분에 이진 탐색과 유사한 방식으로 데이터를 효율적으로 탐색할 수 있다. 트리의 루트 노드부터 시작하여, 찾고자 하는 값이 현재 노드의 값보다 작으면 왼쪽 서브트리로, 크면 오른쪽 서브트리로 이동하는 과정을 반복한다. 이 구조는 정렬된 데이터를 저장하고 검색하는 데 매우 적합하다.
BST의 주요 장점은 평균적인 경우 연결 리스트나 배열에 비해 빠른 탐색, 삽입, 삭제 연산을 제공한다는 점이다. 또한, 중위 순회를 수행하면 저장된 데이터를 정렬된 순서로 쉽게 얻을 수 있다. 그러나 이 장점은 트리가 균형을 유지할 때 최대화된다. 트리가 한쪽으로 치우치는 편향 트리가 되면 연산의 효율성이 선형 시간 복잡도로 떨어질 수 있다[1].
이진 탐색 트리의 핵심 속성은 모든 노드에 대해 다음 세 가지 조건을 만족하는 것이다.
첫째, 각 노드는 하나의 키 값을 가진다. 둘째, 특정 노드의 왼쪽 서브트리에 있는 모든 노드의 키 값은 그 노드의 키 값보다 작아야 한다. 셋째, 특정 노드의 오른쪽 서브트리에 있는 모든 노드의 키 값은 그 노드의 키 값보다 커야 한다. 이 속성은 재귀적으로 모든 서브트리에 적용되며, 트리의 구조적 순서를 정의한다.
이 속성 덕분에 이진 탐색과 유사한 방식으로 효율적인 탐색이 가능해진다. 루트 노드부터 시작하여 찾고자 하는 값과 현재 노드의 값을 비교한다. 찾는 값이 작으면 왼쪽 서브트리로, 크면 오른쪽 서브트리로 이동하는 과정을 반복한다. 이 과정은 값을 찾거나 더 이상 자식 노드가 없는 리프 노드에 도달할 때까지 계속된다.
속성 | 설명 |
|---|---|
순서 속성 | 왼쪽 서브트리 < 부모 노드 < 오른쪽 서브트리 |
재귀적 적용 | 모든 노드에서 그 노드를 루트로 하는 서브트리가 동일한 속성을 가짐 |
중복 키 | 일반적으로 중복된 키 값을 허용하지 않지만, 허용하는 변형도 존재한다[2]. |
이 핵심 속성은 삽입 정렬된 데이터를 효과적으로 조직화하여, 평균 시간 복잡도 측면에서 연결 리스트나 정렬되지 않은 배열보다 우수한 탐색 성능을 제공하는 기반이 된다.
이진 탐색 트리는 정렬된 데이터를 효율적으로 관리하기 위한 자료 구조로, 여러 가지 장점을 가진다. 가장 큰 장점은 평균 시간 복잡도 측면에서 효율적인 탐색, 삽입, 삭제 연산을 지원한다는 점이다. 이진 탐색의 원리를 트리 구조에 적용하여, 각 단계마다 탐색 범위를 절반 가까이 줄여 나갈 수 있다. 이로 인해 균형이 잘 잡힌 트리에서 이러한 기본 연산의 시간 복잡도는 O(log n)에 달한다[3]. 이는 연결 리스트나 정렬되지 않은 배열에서의 선형 탐색(O(n))에 비해 훨씬 빠른 성능을 의미한다.
또한, 트리의 중위 순회를 수행하면 저장된 데이터를 정렬된 순서대로 쉽게 얻을 수 있다. 이는 데이터 자체를 명시적으로 정렬할 필요 없이, 트리의 구조적 속성 덕분에 자연스럽게 정렬된 결과를 제공한다는 장점이다. 이 특성은 범위 검색이나 정렬된 데이터 스트림 처리와 같은 작업에 유용하게 활용된다.
메모리 사용 측면에서도 동적 할당이 가능한 연결 구조를 가지므로, 데이터의 삽입과 삭제가 빈번한 상황에서 배열 기반 구조보다 유연하다. 배열은 크기를 미리 예측하거나 재할당하는 오버헤드가 발생할 수 있지만, 이진 탐색 트리는 노드 단위로 메모리를 관리하여 이러한 문제를 피할 수 있다.
이진 탐색 트리의 기본 연산은 주로 세 가지로 구분된다. 각 연산은 트리의 핵심 속성인 "왼쪽 서브트리의 모든 노드 값 < 루트 노드 값 < 오른쪽 서브트리의 모든 노드 값"을 바탕으로 동작한다.
탐색(Search) 연산은 주어진 키 값을 가진 노드를 찾는다. 루트 노드에서 시작하여, 찾는 값이 현재 노드의 값보다 작으면 왼쪽 서브트리로, 크면 오른쪽 서브트리로 재귀적으로 이동한다. 값이 일치하는 노드를 찾거나 더 이상 자식 노드가 없는 리프 노드에 도달할 때까지 이 과정을 반복한다. 탐색 경로는 트리의 높이에 비례한다.
삽입(Insertion) 연산은 새로운 노드를 적절한 위치에 추가하여 트리의 속성을 유지한다. 먼저 탐색 연산과 유사한 과정으로 새 노드가 위치해야 할 빈 자리(NULL 포인터)를 찾는다. 그 후, 해당 위치의 부모 노드 포인터를 이용해 새 노드를 연결한다. 삽입 연산은 항상 새로운 노드를 리프 노드로 추가하므로, 기존 노드들의 구조를 크게 변경하지 않는다.
삭제(Deletion) 연산은 세 가지 경우로 나누어 처리된다.
1. 삭제할 노드가 자식이 없는 경우(리프 노드): 해당 노드를 단순히 제거한다.
2. 삭제할 노드가 자식을 하나만 가진 경우: 해당 노드를 제거하고, 그 자식 노드를 부모 노드에 직접 연결한다.
3. 삭제할 노드가 두 개의 자식을 모두 가진 경우: 이는 가장 복잡한 경우이다. 일반적으로 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값(중위 순회 후속자)을 찾거나, 왼쪽 서브트리에서 가장 큰 값(중위 순회 선행자)을 찾아 그 값으로 삭제할 노드를 대체한다. 그 후, 대체에 사용된 노드(후속자 또는 선행자)를 원래 위치에서 제거하는데, 이 노드는 최대 하나의 자식만을 가지므로 경우 1 또는 2에 해당하게 된다. 이 방법으로 트리의 순서 속성을 보존한다.
연산 | 설명 | 주요 처리 과정 |
|---|---|---|
탐색 | 키 값을 가진 노드 찾기 | 비교를 통해 왼쪽/오른쪽 서브트리 선택적 이동 |
삽입 | 새 노드를 올바른 위치에 추가 | 빈 리프 노드 위치 탐색 후 연결 |
삭제 | 노드를 제거하고 속성 유지 | 자식 노드 수(0, 1, 2)에 따라 다른 절차 적용 |
이진 탐색 트리에서 탐색 연산은 주어진 키 값을 가진 노드를 찾는 과정이다. 이 연산은 이진 탐색 트리의 핵심 속성인 정렬된 구조를 활용하여 효율적으로 수행된다.
탐색은 항상 루트 노드에서 시작한다. 찾고자 하는 키 값과 현재 노드의 키 값을 비교한다. 두 값이 같으면 탐색이 성공적으로 종료된다. 찾고자 하는 키 값이 현재 노드의 키 값보다 작으면, 이진 탐색 트리의 속성에 따라 해당 키는 현재 노드의 왼쪽 서브트리에만 존재할 수 있다. 따라서 탐색은 왼쪽 자식 노드로 이동하여 계속된다. 반대로, 찾고자 하는 키 값이 현재 노드의 키 값보다 크면, 탐색은 오른쪽 서브트리로 진행된다. 이 비교 및 이동 과정은 원하는 노드를 찾거나 더 이상 자식 노드가 없는 리프 노드에 도달할 때까지 재귀적으로 반복된다. 리프 노드에 도달했는데도 키가 일치하지 않으면, 트리 내에 해당 키를 가진 노드가 존재하지 않는 것이므로 탐색은 실패로 끝난다.
탐색 연산의 효율성은 트리의 높이에 직접적으로 의존한다. 최선의 경우(트리가 완전히 균형 잡혀 있을 때) 시간 복잡도는 O(log n)이다. 여기서 n은 트리의 총 노드 수이다. 그러나 최악의 경우(트리가 한쪽으로 치우친 편향 트리일 때) 시간 복잡도는 O(n)으로 퇴화되어 선형 탐색과 같아진다. 이는 이진 탐색 트리의 주요 단점 중 하나로, 이를 해결하기 위해 AVL 트리나 레드-블랙 트리 같은 균형 이진 탐색 트리가 개발되었다.
비교 조건 | 다음 탐색 방향 | 근거 |
|---|---|---|
찾는 키 == 현재 노드 키 | 탐색 종료 (성공) | 키를 찾음 |
찾는 키 < 현재 노드 키 | 왼쪽 서브트리로 이동 | 왼쪽 서브트리의 모든 키는 현재 노드 키보다 작음 |
찾는 키 > 현재 노드 키 | 오른쪽 서브트리로 이동 | 오른쪽 서브트리의 모든 키는 현재 노드 키보다 큼 |
삽입 연산은 새로운 노드를 이진 탐색 트리의 적절한 위치에 추가하여 트리의 핵심 속성을 유지하는 과정이다. 삽입은 항상 리프 노드가 될 수 있는 위치에서 이루어진다. 알고리즘은 먼저 루트 노드부터 시작하여 삽입할 값과 현재 노드의 값을 비교하며 트리를 순회한다. 삽입할 값이 현재 노드의 값보다 작으면 왼쪽 서브트리로, 크면 오른쪽 서브트리로 이동한다. 이 과정을 더 이상 이동할 수 없는 널 노드에 도달할 때까지 반복한다. 그 위치가 새로운 노드가 삽입될 자리이다.
삽입 연산의 구체적인 단계는 다음과 같다.
1. 삽입할 값 k와 함께 시작한다.
2. 트리가 비어 있으면, k를 값으로 갖는 새 노드를 생성하여 루트로 만든다.
3. 트리가 비어 있지 않으면, 현재 노드를 루트 노드로 설정한다.
4. k를 현재 노드의 값과 비교한다.
k가 현재 노드의 값보다 작으면, 현재 노드의 왼쪽 자식으로 이동한다. 만약 왼쪽 자식이 없으면, 그 위치에 새 노드를 생성하여 연결한다.
k가 현재 노드의 값보다 크면, 현재 노드의 오른쪽 자식으로 이동한다. 만약 오른쪽 자식이 없으면, 그 위치에 새 노드를 생성하여 연결한다.
k가 현재 노드의 값과 같으면, 일반적으로 중복 삽입을 허용하지 않는 BST의 정의에 따라 삽입을 중단하거나 특정 규칙(예: 왼쪽 서브트리에 삽입)에 따라 처리한다[4].
5. 자식 노드로 이동한 후, 해당 자식 노드를 새로운 '현재 노드'로 설정하고 4번 단계를 반복한다.
이 연산은 트리의 구조에 따라 성능이 달라진다. 비교적 균형 잡힌 트리에서는 평균적으로 트리의 높이에 비례하는 시간이 소요된다. 그러나 극단적으로 불균형한 트리(예: 한쪽으로 치우친 사향 트리)가 형성될 경우, 삽입 시간은 노드의 수에 선형적으로 비례할 수 있다. 이는 이진 탐색 트리의 주요 단점 중 하나로, 이를 해결하기 위해 AVL 트리나 레드-블랙 트리 같은 균형 이진 탐색 트리가 개발되었다.
이진 탐색 트리에서 노드를 삭제하는 연산은 세 가지 주요 경우로 나뉜다. 삭제할 노드가 단말 노드인 경우, 자식 노드가 하나인 경우, 그리고 자식 노드가 두 개인 경우이다.
첫 번째 경우, 삭제할 노드가 단말 노드라면 해당 노드를 단순히 제거하면 된다. 부모 노드의 해당 자식 포인터를 NULL로 설정하여 트리에서 분리한다. 두 번째 경우, 삭제할 노드가 하나의 자식(왼쪽 또는 오른쪽)만 가지고 있다면, 해당 노드를 제거하고 그 자식 노드를 부모 노드의 적절한 위치에 직접 연결한다. 이는 노드를 건너뛰고 연결하는 것과 같다.
가장 복잡한 경우는 삭제할 노드가 두 개의 자식 노드를 모두 가질 때이다. 이 경우, 삭제할 노드를 그대로 제거할 수 없다. 대신, 삭제할 노드의 중위 순회 후속자 또는 전임자를 찾아 대체하는 방법을 사용한다. 일반적으로 후속자를 사용하며, 이는 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드이다. 이 후속자 노드의 값을 삭제할 노드로 복사한 후, 원래의 후속자 노드를 삭제한다. 후속자 노드는 왼쪽 자식을 가질 수 없으므로[5], 이 삭제 작업은 앞서 설명한 첫 번째 또는 두 번째 간단한 경우에 해당하게 되어 재귀적으로 처리할 수 있다.
삭제 연산 후에도 이진 탐색 트리의 핵심 속성(왼쪽 서브트리의 모든 노드 값 < 부모 노드 값 < 오른쪽 서브트리의 모든 노드 값)이 유지되어야 한다. 위의 세 가지 방법은 모두 이 속성을 보존한다.
이진 탐색 트리의 시간 복잡도는 트리의 구조, 즉 균형 상태에 크게 의존한다. 이상적인 균형 상태에서는 각 연산의 시간 복잡도가 트리 높이에 비례하여 O(log n)이다. 여기서 n은 트리에 저장된 노드의 총 개수를 의미한다. 이는 각 단계에서 탐색 범위가 절반으로 줄어들기 때문이다.
그러나 이진 탐색 트리에 데이터가 순차적으로 삽입되는 등의 이유로 트리가 편향될 경우, 최악의 시나리오가 발생한다. 예를 들어, 키가 1, 2, 3, ... 순으로 삽입되면 트리는 연결 리스트와 유사한 형태를 띠게 된다. 이 경우 트리의 높이는 n이 되며, 탐색, 삽입, 삭제 연산 모두 최악의 경우 O(n)의 시간이 소요된다.
다음 표는 균형 상태에 따른 이진 탐색 트리의 시간 복잡도를 요약한다.
연산 | 평균 경우 (균형) | 최악 경우 (편향) |
|---|---|---|
탐색(Search) | O(log n) | O(n) |
삽입(Insertion) | O(log n) | O(n) |
삭제(Deletion) | O(log n) | O(n) |
이러한 최악의 경우를 방지하고 효율적인 O(log n)의 성능을 보장하기 위해 AVL 트리나 레드-블랙 트리와 같은 균형 이진 탐색 트리가 고안되었다. 이러한 자료구조는 삽입 및 삭제 연산 중에 추가적인 작업을 통해 트리의 균형을 자동으로 유지한다.
이진 탐색 트리는 데이터의 삽입 순서에 따라 트리의 구조가 편향될 수 있다. 이 경우 탐색, 삽입, 삭제 연산의 시간 복잡도가 최악의 경우 O(n)으로 저하될 수 있다. 이를 해결하기 위해 삽입과 삭제 시 트리의 균형을 자동으로 유지하는 균형 이진 탐색 트리가 개발되었다. 대표적인 균형 트리로는 AVL 트리와 레드-블랙 트리가 있다.
AVL 트리는 1962년 G. M. Adelson-Velsky와 Evgenii Landis가 제안한 최초의 균형 이진 탐색 트리이다. 이 트리는 모든 노드에서 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이(균형 인수)가 -1, 0, 1 중 하나가 되도록 유지한다. 균형이 깨지면 회전 연산(단일 회전, 이중 회전)을 통해 균형을 복구한다. AVL 트리는 엄격한 균형을 유지하여 탐색 성능이 매우 우수하지만, 삽입과 삭제 시 빈번한 회전 연산이 필요할 수 있다는 단점이 있다.
레드-블랙 트리는 각 노드에 레드 또는 블랙의 색상 속성을 부여하여 균형을 유지한다. 다음의 다섯 가지 규칙을 만족해야 한다[6]. AVL 트리보다 덜 엄격한 균형 조건을 가지므로 삽입과 삭제 시 필요한 회전 횟수가 상대적으로 적다. 이로 인해 많은 프로그래밍 언어의 표준 라이브러리(C++의 std::map, 자바의 TreeMap 등)에서 내부적으로 레드-블랙 트리를 구현에 사용한다.
특성 | AVL 트리 | 레드-블랙 트리 |
|---|---|---|
균형 기준 | 각 노드의 서브트리 높이 차이 | 노드의 색상과 경로 상의 블랙 노드 수 |
균형 엄격도 | 매우 엄격함 | 비교적 느슨함 |
탐색 성능 | 일반적으로 더 빠름 | AVL보다 약간 느릴 수 있음 |
삽입/삭제 성능 | 회전이 더 빈번할 수 있음 | 회전이 상대적으로 적음 |
주요 사용처 | 검색이 빈번한 데이터베이스 등 | 종합적인 성능이 요구되는 라이브러리 |
AVL 트리는 이진 탐색 트리의 한 종류로, 1962년 게오르기 아델슨-벨스키와 예브게니 란디스에 의해 발표되었다[7]. 이 트리는 삽입과 삭제 연산 후에 자동으로 균형을 맞추는 자가 균형 이진 탐색 트리의 대표적인 예이다. AVL 트리의 핵심은 모든 노드의 왼쪽 서브트리와 오른쪽 서브트리의 높이 차이(균형 인수)가 -1, 0, 1 중 하나여야 한다는 엄격한 조건이다. 이 조건을 통해 트리가 편향되는 것을 방지하고, 연산의 최악의 경우에도 효율적인 성능을 보장한다.
균형이 깨진 노드를 발견하면, AVL 트리는 회전 연산을 통해 균형을 복구한다. 회전은 네 가지 기본 유형이 존재한다.
회전 유형 | 설명 | 적용 조건 (균형 인수) |
|---|---|---|
LL 회전 | 한 번의 오른쪽 회전 | 부모: +2, 왼쪽 자식: +1 |
RR 회전 | 한 번의 왼쪽 회전 | 부모: -2, 오른쪽 자식: -1 |
LR 회전 | 왼쪽-오른쪽 회전 | 부모: +2, 왼쪽 자식: -1 |
RL 회전 | 오른쪽-왼쪽 회전 | 부모: -2, 오른쪽 자식: +1 |
균형 인수가 +2 또는 -2가 된 노드를 기준으로, 자식 노드의 균형 인수를 확인하여 위 표에 따라 적절한 회전을 수행한다. 예를 들어, LL 회전은 불균형 노드의 왼쪽 자식 노드를 위로 올리는 오른쪽 회전을 의미한다.
AVL 트리는 엄격한 균형 조건으로 인해 탐색, 삽입, 삭제 연산의 시간 복잡도가 최악의 경우에도 O(log n)을 보장한다. 이는 일반적인 이진 탐색 트리가 최악의 경우 O(n)이 될 수 있는 것에 비해 큰 장점이다. 그러나 삽입이나 삭제 시 빈번한 균형 점검과 회전이 필요하기 때문에 상대적으로 연산 오버헤드가 크다는 단점도 있다. 이러한 특성으로 AVL 트리는 검색이 빈번하고 데이터의 변경(삽입/삭제)이 비교적 적은 데이터베이스나 파일 시스템의 인덱스 구현 등에 적합하다.
레드-블랙 트리는 이진 탐색 트리의 한 종류로, 삽입, 삭제, 탐색 연산이 최악의 경우에도 O(log n) 시간 복잡도를 보장하는 균형 이진 탐색 트리이다. AVL 트리보다 덜 엄격한 균형 조건을 사용하여, 회전 횟수를 줄이고 삽입 및 삭제 연산을 상대적으로 효율적으로 수행한다.
레드-블랙 트리는 다음의 다섯 가지 규칙을 만족한다.
2. 루트 노드는 검정이다.
3. 모든 리프 노드(NIL 노드)는 검정이다.
4. 빨강 노드의 자식 노드는 모두 검정이다(빨강 노드가 연속적으로 존재할 수 없다).
5. 각 노드에서부터 그 노드의 자손 리프 노드까지의 모든 경로에는 동일한 개수의 검정 노드가 존재한다[8].
삽입 또는 삭제 연산 후 이러한 규칙이 위반되면, 노드 재색칠과 트리 회전 연산을 통해 트리의 균형을 복원한다. 삽입 시 새 노드는 기본적으로 빨강으로 삽입된 후, 주변 노드의 색상과 구조를 조정한다. 삭제 연산은 더 복잡한 경우의 수를 처리해야 한다.
레드-블랙 트리의 성능 특성을 요약하면 다음과 같다.
연산 | 평균 시간 복잡도 | 최악 시간 복잡도 |
|---|---|---|
탐색 | O(log n) | O(log n) |
삽입 | O(log n) | O(log n) |
삭제 | O(log n) | O(log n) |
이 트리는 실무에서 광범위하게 사용되며, C++ 표준 템플릿 라이브러리(STL)의 std::map과 std::set, 자바의 TreeMap과 TreeSet 등의 내부 구현에 채택되어 있다. AVL 트리에 비해 균형 조건이 완화되어 전체적인 삽입/삭제 성능이 우수한 경우가 많아, 지속적인 동적 삽입과 삭제가 빈번한 상황에 적합하다.
이진 탐색 트리는 그 구조적 특성 덕분에 다양한 컴퓨터 과학 및 소프트웨어 공학 분야에서 폭넓게 응용된다. 가장 대표적인 응용은 데이터의 효율적인 정렬과 탐색이다. BST를 중위 순회(in-order traversal)하면 저장된 데이터를 정렬된 순서로 얻을 수 있어, 동적으로 데이터가 추가되거나 삭제되는 상황에서도 정렬 상태를 유지하는 데 유용하다. 또한, 데이터베이스 시스템의 인덱스 구조나 파일 시스템의 디렉토리 계층을 구현하는 데에도 활용된다.
보다 구체적인 응용 분야는 다음과 같다.
응용 분야 | 설명 |
|---|---|
이진 힙과 함께 우선순위 큐를 구현하는 방법 중 하나로, 특히 우선순위 키의 동적 변경이 필요한 경우 유리하다. | |
많은 프로그래밍 언어의 표준 라이브러리(예: C++의 | |
B-트리, B+ 트리와 같은 다방향 탐색 트리는 BST의 개념을 확장한 것으로, 디스크 기반 데이터베이스에서 대용량 데이터에 대한 효율적인 인덱싱을 제공한다. | |
라우터에서 패킷 전송 경로를 결정하는 라우팅 테이블을 관리할 때, IP 주소의 범위 기반 검색에 적합한 변형 트리 구조가 사용된다. |
또한, 3D 그래픽스 및 게임 엔진에서 공간 분할을 위한 이진 공간 분할(BSP) 트리나, 데이터 압축에 쓰이는 허프만 코딩 트리도 이진 트리 구조를 기반으로 한다. 이러한 광범위한 응용은 BST가 제공하는 로그 시간 복잡도의 효율적인 연산과 데이터 간의 계층적 관계 표현 능력에서 비롯된다.
이진 탐색 트리의 구현은 재귀 또는 반복문을 사용하여 이루어진다. 핵심 연산인 탐색, 삽입, 삭제는 모두 트리의 루트 노드부터 시작하여 이진 탐색 원리에 따라 적절한 서브트리로 내려가며 수행된다.
다음은 C++를 사용한 간단한 이진 탐색 트리 클래스의 구현 예시이다. 이 예시는 정수 값을 저장하며, 각 노드는 Node 구조체로 표현된다.
```cpp
#include <iostream>
class BinarySearchTree {
private:
struct Node {
int data;
Node* left;
Node* right;
Node(int val) : data(val), left(nullptr), right(nullptr) {}
};
Node* root;
// 재귀적 삽입 보조 함수
Node* insertRec(Node* node, int value) {
if (node == nullptr) {
return new Node(value);
}
if (value < node->data) {
node->left = insertRec(node->left, value);
} else if (value > node->data) {
node->right = insertRec(node->right, value);
}
return node;
}
// 재귀적 탐색 보조 함수
bool searchRec(Node* node, int value) {
if (node == nullptr) return false;
if (node->data == value) return true;
if (value < node->data) return searchRec(node->left, value);
else return searchRec(node->right, value);
}
// 중위 순회 보조 함수 (오름차순 출력)
void inorderRec(Node* node) {
if (node == nullptr) return;
inorderRec(node->left);
std::cout << node->data << " ";
inorderRec(node->right);
}
// 최소값 노드 찾기 (삭제 연산에 사용)
Node* findMin(Node* node) {
while (node && node->left != nullptr) {
node = node->left;
}
return node;
}
// 재귀적 삭제 보조 함수
Node* deleteRec(Node* node, int value) {
if (node == nullptr) return node;
if (value < node->data) {
node->left = deleteRec(node->left, value);
} else if (value > node->data) {
node->right = deleteRec(node->right, value);
} else {
// 삭제할 노드를 찾은 경우
// 자식이 하나 또는 없는 경우
if (node->left == nullptr) {
Node* temp = node->right;
delete node;
return temp;
} else if (node->right == nullptr) {
Node* temp = node->left;
delete node;
return temp;
}
// 자식이 둘인 경우: 오른쪽 서브트리의 최소값 노드로 대체
Node* temp = findMin(node->right);
node->data = temp->data;
node->right = deleteRec(node->right, temp->data);
}
return node;
}
public:
BinarySearchTree() : root(nullptr) {}
void insert(int value) {
root = insertRec(root, value);
}
bool search(int value) {
return searchRec(root, value);
}
void remove(int value) {
root = deleteRec(root, value);
}
void display() {
inorderRec(root);
std::cout << std::endl;
}
};
// 사용 예시
int main() {
BinarySearchTree bst;
bst.insert(50);
bst.insert(30);
bst.insert(70);
bst.insert(20);
bst.insert(40);
std::cout << "트리 내용 (중위 순회): ";
bst.display(); // 20 30 40 50 70 출력
std::cout << "40 검색: " << (bst.search(40) ? "찾음" : "없음") << std::endl;
std::cout << "60 검색: " << (bst.search(60) ? "찾음" : "없음") << std::endl;
bst.remove(30);
std::cout << "30 삭제 후: ";
bst.display(); // 20 40 50 70 출력
return 0;
}
```
다른 프로그래밍 언어에서의 구현 방식도 유사하다. 파이썬에서는 클래스와 재귀를, 자바에서는 제네릭을 활용하여 일반화된 버전을 구현할 수 있다. 삭제 연산은 세 가지 경우(자식이 없음, 하나 있음, 둘 있음)를 정확히 처리하는 것이 핵심이다. 이 기본 구현은 균형 이진 탐색 트리가 아니므로, 입력 순서에 따라 성능이 저하될 수 있다는 점에 유의해야 한다.