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

C++로 구현하는 이진 트리의 최대 너비 계산 알고리즘


이진 트리가 하나 주어졌을 때, 해당 트리의 최대 너비(maximum width)를 구하는 함수를 정의해야 합니다. 여기서 트리의 너비란 모든 레벨(층) 중에서 가장 넓은 레벨의 너비를 의미합니다.

이 문제에서 이진 트리는 완전 이진 트리와 동일한 구조를 기준으로 하되, 일부 노드가 null일 수 있다고 가정합니다. 한 레벨의 너비는 그 레벨의 양 끝 노드, 즉 가장 왼쪽에 있는 null이 아닌 노드와 가장 오른쪽에 있는 null이 아닌 노드 사이의 길이로 정의되며, 두 끝 노드 사이에 존재하는 null 노드들도 길이 계산에 포함됩니다.

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

C++로 구현하는 이진 트리의 최대 너비 계산 알고리즘


위 트리의 마지막 레벨은 [5, 3, null, 9]이므로, null 노드까지 포함한 길이가 4가 되어 전체 트리의 최대 너비는 4입니다.

해결 접근 방법

이 문제는 레벨 순회(BFS)와 인덱스 부여 기법을 활용하면 효율적으로 해결할 수 있습니다. 각 노드에 완전 이진 트리에서의 위치를 나타내는 인덱스를 부여하고, 같은 레벨에서 첫 번째 노드와 마지막 노드의 인덱스 차이를 이용해 너비를 계산합니다.

  • ans := 1, size := 0으로 초기화합니다.

  • (노드, 값) 쌍을 저장할 양방향 큐(deque) q를 정의합니다.

  • (root, 1)q에 삽입합니다.

  • q가 비어 있지 않은 동안 다음을 반복합니다.

    • size := q의 크기

    • (노드, 값) 쌍 curr을 정의합니다.

    • size가 1이면, (큐 맨 앞 요소의 노드, 1)을 q에 삽입한 후 맨 앞 요소를 삭제합니다. 즉, 현재 레벨에 노드가 하나뿐이라면 너비를 1로 재설정합니다.

    • size가 0이 아닌 동안 다음을 반복합니다.

      • curr := q의 맨 앞 요소, 그리고 맨 앞 요소를 삭제합니다.

      • curr 노드의 왼쪽 자식이 null이 아니라면, (왼쪽 자식, 2 × curr의 값)을 만들어 q에 삽입합니다.

      • curr 노드의 오른쪽 자식이 null이 아니라면, (오른쪽 자식, 2 × curr의 값 + 1)을 만들어 q에 삽입합니다.

      • q의 크기가 1보다 크다면, ans := max(ans, q의 마지막 요소 값 − q의 첫 번째 요소 값 + 1)로 갱신합니다.

      • size := size − 1

  • ans를 반환합니다.

핵심 아이디어는 왼쪽 자식에게는 부모 인덱스의 2배, 오른쪽 자식에게는 부모 인덱스의 2배 + 1을 부여하는 것입니다. 이렇게 하면 null 노드가 있어도 마치 완전 이진 트리처럼 각 노드의 상대적 위치를 추적할 수 있습니다.

예제 코드

아래 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){
         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;
}
class Solution {
   public:
   int widthOfBinaryTree(TreeNode* root) {
      int ans = 0;
      deque < pair <TreeNode*, int> > q;
      q.push_back({root,1});
      ans = 1;
      int size;
      while(!q.empty()){
          size = q.size();
          pair <TreeNode*, int> curr;
          if(size == 1){
             q.push_back({q.front().first, 1});
             q.pop_front();
          }
          while(size--){
             curr = q.front();
             q.pop_front();
             if(curr.first->left){
                q.push_back({curr.first->left, 2 * curr.second});
             }
             if(curr.first->right){
                q.push_back({curr.first->right, 2 * curr.second + 1});
             }
          }
          if(q.size() > 1)
             ans = max(ans, q.back().second - q.front().second + 1);
      }
      return ans;
   }
};
main(){
   vector<int> v = {1,3,2,5,3,NULL,9};
   TreeNode *root = make_tree(v);
   Solution ob;
   cout << (ob.widthOfBinaryTree(root));
}

입력

[1,3,2,5,3,null,9]

출력

4

복잡도 분석

시간 복잡도: O(N) — 모든 노드를 정확히 한 번씩 방문합니다.
공간 복잡도: O(N) — 큐에는 임의의 한 레벨에 속한 노드들이 최대로 저장됩니다.

참고로 트리의 깊이가 매우 깊어지면 인덱스 값이 지수적으로 증가하여 정수 오버플로가 발생할 수 있습니다. 실제 환경에서는 unsigned long long 타입을 사용하거나, 각 레벨의 첫 번째 노드 인덱스를 기준으로 상대적인 값을 계산하도록 정규화하는 것이 안전합니다.