Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++ 재귀로 구현하는 플립 등가 이진 트리 판별 알고리즘

문제 개요

이진 트리(binary tree)에서 뒤집기(flip) 연산이란 임의의 노드 하나를 선택하여 해당 노드의 왼쪽 자식 서브트리와 오른쪽 자식 서브트리를 서로 교환하는 것을 의미합니다.

두 이진 트리 X와 Y가 플립 등가(flip equivalent) 관계라는 것은, X에 대해 여러 번의 뒤집기 연산을 수행하여 Y를 만들어낼 수 있을 때, 그리고 오직 그 경우에만 성립합니다.

이번 글에서는 두 이진 트리가 서로 플립 등가인지 판별하는 메서드를 작성해 보겠습니다. 트리는 각각 루트 노드 root1root2로 주어집니다.

예시

다음과 같은 두 트리가 있다고 가정해 보겠습니다.

C++ 재귀로 구현하는 플립 등가 이진 트리 판별 알고리즘

위 트리에서 값이 1, 3, 5인 노드들을 차례로 뒤집으면 두 트리가 동일한 형태가 됩니다. 따라서 출력 결과는 true(1)입니다.

알고리즘 접근 방법

이 문제는 재귀적으로 해결할 수 있습니다. 핵심 아이디어는 각 노드 쌍을 비교할 때, 왼쪽·오른쪽 자식을 그대로 비교하거나 뒤집어서 비교하는 두 가지 경우를 모두 고려하는 것입니다. 단계별로 살펴보면 다음과 같습니다.

  • 두 트리 노드 t1, t2를 인자로 받는 재귀 함수 solve()를 정의합니다.
  • root1root2가 모두 null이면 true를 반환합니다. (두 트리 모두 빈 경우)
  • 둘 중 하나만 null이라면 false를 반환합니다. (구조가 다르므로)
  • 두 노드의 값이 다르면 false를 반환합니다.
  • (t1과 t2 모두 왼쪽 서브트리가 없는 경우) 또는 (t1과 t2 모두 왼쪽 서브트리가 존재하고, 두 왼쪽 자식의 값이 서로 같은 경우)에는 뒤집기 없이 비교합니다.
    solve(root1의 왼쪽, root2의 왼쪽) AND solve(root1의 오른쪽, root2의 오른쪽)
  • 그 외의 경우에는 한쪽을 뒤집어서 비교합니다.
    solve(root1의 왼쪽, root2의 오른쪽) AND solve(root1의 오른쪽, root2의 왼쪽)

C++ 구현 예제

아래 코드는 위 알고리즘을 그대로 구현한 전체 예제입니다.

#include <bits/stdc++.h>
using namespace std;

class TreeNode {
public:
    int val;
    TreeNode *left, *right;
    TreeNode(int data) {
        val = data;
        left = NULL;
        right = NULL;
    }
};

// 레벨 순회 방식으로 값을 삽입하는 헬퍼 함수
void insert(TreeNode **root, int val) {
    queue<TreeNode*> q;
    q.push(*root);
    while (q.size()) {
        TreeNode *temp = q.front();
        q.pop();
        if (!temp->left) {
            temp->left = new TreeNode(val);
            return;
        } else {
            q.push(temp->left);
        }
        if (!temp->right) {
            temp->right = new TreeNode(val);
            return;
        } else {
            q.push(temp->right);
        }
    }
}

// 벡터로부터 이진 트리 생성
TreeNode *make_tree(vector<int> v) {
    TreeNode *root = new TreeNode(v[0]);
    for (int i = 1; i < v.size(); i++) {
        insert(&root, v[i]);
    }
    return root;
}

class Solution {
public:
    bool flipEquiv(TreeNode* root1, TreeNode* root2) {
        // 둘 다 null이면 등가
        if (!root1 && !root2) return true;
        // 한쪽만 null이면 등가 아님
        else if (!root1 || !root2) return false;
        // 노드 값이 다르면 등가 아님
        else if (root1->val != root2->val) return false;
        // 왼쪽 자식 구조가 일치하면 그대로 비교
        else if ((!root1->left && !root2->left) ||
                 (root1->left && root2->left &&
                  root1->left->val == root2->left->val)) {
            return flipEquiv(root1->left, root2->left) &&
                   flipEquiv(root1->right, root2->right);
        }
        // 그렇지 않으면 뒤집어서 비교
        else {
            return flipEquiv(root1->left, root2->right) &&
                   flipEquiv(root1->right, root2->left);
        }
    }
};

int main() {
    vector<int> v = {1,2,3,4,5,6,NULL,NULL,NULL,7,8};
    TreeNode *r1 = make_tree(v);
    vector<int> v1 = {1,3,2,NULL,6,4,5,NULL,NULL,NULL,NULL,NULL,NULL,8,7};
    TreeNode *r2 = make_tree(v1);
    Solution ob;
    cout << (ob.flipEquiv(r1, r2));
}

실행 결과 확인

입력

[1,2,3,4,5,6,null,null,null,7,8]
[1,3,2,null,6,4,5,null,null,null,null,8,7]

출력

1

출력값 1true를 의미하며, 두 트리가 플립 등가임을 나타냅니다.

복잡도 분석

  • 시간 복잡도: O(N) — 각 트리의 모든 노드를 최대 한 번씩 방문합니다. N은 두 트리 중 더 작은 트리의 노드 수입니다.
  • 공간 복잡도: O(H) — 재귀 호출 스택 깊이는 트리의 높이 H에 비례합니다. 최악의 경우(편향 트리) O(N)까지 증가할 수 있습니다.

마무리

플립 등가 판별 문제는 일반적인 이진 트리 동일성 검사에서 '자식 순서를 바꿔 비교하는 분기' 하나만 추가하면 해결됩니다. 재귀적 사고방식을 연습하기에 좋은 문제이며, 실제 코딩 테스트에서도 자주 등장하는 유형이니 위 구현 과정을 직접 따라 해 보시길 권장합니다.