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

C++로 이진 트리에서 중복 서브트리 찾는 방법 완벽 가이드


이진 트리(binary tree)가 주어졌을 때, 모든 중복 서브트리(duplicate subtrees)를 찾아야 합니다. 여기서 각 종류의 중복 서브트리마다 그중 하나의 루트 노드만 반환하면 됩니다.

예를 들어 다음과 같은 트리가 있다고 가정해 보겠습니다.

C++로 이진 트리에서 중복 서브트리 찾는 방법 완벽 가이드

이 트리에서 찾을 수 있는 중복 서브트리는 다음과 같습니다.

C++로 이진 트리에서 중복 서브트리 찾는 방법 완벽 가이드

해결 접근 방식

이 문제의 핵심 아이디어는 각 서브트리를 고유한 문자열로 직렬화(serialize)하는 것입니다. 동일한 구조와 값을 가진 서브트리는 반드시 동일한 문자열 표현을 갖게 되므로, 해시 맵을 이용해 등장 횟수를 세면 중복을 쉽게 판별할 수 있습니다.

구체적인 알고리즘은 다음과 같습니다.

  • 결과를 저장할 배열 ret과 서브트리 문자열의 등장 횟수를 기록할 맵(map) m을 생성합니다.
  • 노드를 입력으로 받는 재귀 함수 solve()를 정의합니다. 이 함수는 다음과 같이 동작합니다.
  • 노드가 null이면 "-1"을 반환합니다. (빈 자식 노드 구분용)
  • 현재 노드의 값을 문자열로 변환한 뒤, 구분자 "#"을 붙입니다.
  • 왼쪽 자식과 오른쪽 자식에 대해 재귀적으로 solve()를 호출하여 각각 left, right에 저장합니다.
  • 문자열 x에 left와 right를 구분자와 함께 이어 붙여 현재 서브트리의 고유한 직렬화 표현을 만듭니다.
  • m에서 해당 문자열의 카운트를 1 증가시킵니다.
  • 카운트가 정확히 2가 되는 순간(즉, 두 번째로 발견되었을 때) 해당 노드를 ret에 삽입합니다. 이렇게 하면 같은 서브트리가 세 번 이상 나타나도 중복 추가를 방지할 수 있습니다.
  • 직렬화된 문자열 x를 상위 호출로 반환합니다.

마지막으로 메인 함수에서 solve(root)를 호출하고 ret을 반환하면 됩니다.

시간 복잡도

모든 노드를 한 번씩 방문하며 각 단계에서 문자열 연결이 발생하므로, 전체 시간 복잡도는 O(n²)입니다. 공간 복잡도 역시 직렬화 문자열을 저장하기 위해 O(n²)까지 증가할 수 있습니다.

C++ 구현 예제

#include <bits/stdc++.h>
using namespace std;
class TreeNode{
   public:
      int val;
      TreeNode *left, *right;
      TreeNode(int data){
         val = data;
         left = 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){
         if(val != NULL)
            temp->left = new TreeNode(val);
            else
               temp->left = new TreeNode(0);
               return;
          } else {
               q.push(temp->left);
            }
            if(!temp->right){
               if(val != NULL)
                  temp->right = new TreeNode(val);
               else
                  temp->right = new TreeNode(0);
                  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;
         }
         void tree_level_trav(TreeNode*root){
            if (root == NULL) return;
            cout << "[";
            queue<TreeNode *> q;
            TreeNode *curr;
            q.push(root);
            q.push(NULL);
            while (q.size() > 1) {
               curr = q.front();
               q.pop();
               if (curr == NULL){
                  q.push(NULL);
             } else {
             if(curr->left)
             q.push(curr->left);
             if(curr->right)
             q.push(curr->right);
             if(curr->val == 0 || curr == NULL){
                       cout << "null" << ", ";
             } else {
                       cout << curr->val << ", ";
             }
           }
     }
   cout << "]"<<endl;
}
class Solution {
public:
  vector <TreeNode*> ret;
   map <string, int> m;
   string solve(TreeNode* node){
      if(!node || node->val == 0){
         return "-1";
      }
      string x = to_string(node->val);
      x += "#";
      string left = solve(node->left);
      string right = solve(node->right);
      x = x + "#" + left + "#" + right;
      m[x]++;
      if(m[x] == 2){
         ret.push_back(node);
      }
      return x;
   }
   vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
      ret.clear();
      m.clear();
      solve(root);
      return ret;
   }
};
main(){
   vector<int> v = {1,2,3,4,NULL,2,4,NULL,NULL,NULL,NULL,4};
   Solution ob;
   TreeNode *root = make_tree(v);
   vector<TreeNode*> trees = ob.findDuplicateSubtrees(root);
   for(TreeNode *t : trees){
      tree_level_trav(t);
   }
}

실행 결과

입력

[1,2,3,4,null,2,4,null,null,null,null,4]

출력

[4]
[2, 4]

출력 결과를 보면 값 4를 가진 리프 노드 서브트리 하나와, 루트가 2이고 오른쪽 자식으로 4를 가지는 서브트리 하나, 총 두 개의 중복 서브트리가 올바르게 검출된 것을 확인할 수 있습니다.