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

C++ 이진 검색 트리에서 목표값에 가장 가까운 k개의 값 찾기


문제 설명

이진 검색 트리(BST)와 하나의 목표값(target)이 주어졌을 때, 트리에 있는 값 중 target에 가장 가까운 k개의 값을 찾아야 합니다. 이때 target은 부동소수점 실수라는 점에 유의해야 하며, k는 항상 유효한 값(k ≤ 전체 노드 수)이라고 가정할 수 있습니다.

예를 들어 아래와 같은 트리가 주어지고,

C++ 이진 검색 트리에서 목표값에 가장 가까운 k개의 값 찾기

target = 3.714286, k = 2라면 출력은 [4, 3]이 됩니다.

알고리즘 접근 방식

이 문제는 두 개의 스택을 활용하면 효율적으로 해결할 수 있습니다. 하나는 target보다 작은 값들을 관리하는 smaller 스택, 다른 하나는 target보다 크거나 같은 값들을 관리하는 larger 스택입니다. 각 단계마다 두 스택의 최상단(top) 값 중 target에 더 가까운 것을 결과에 추가하고, 선택된 노드의 서브트리를 계속 탐색하여 다음 후보를 준비합니다.

구체적인 풀이 단계는 다음과 같습니다.

  • pushSmaller() 함수를 정의합니다. 이 함수는 노드(node), 스택(st), 목표값(target)을 매개변수로 받습니다.

    • node가 NULL이 아닌 동안 다음을 반복합니다.

      • node의 값이 target보다 작으면:

        • node를 st에 삽입(push)

        • node를 node의 오른쪽 자식으로 갱신

      • 그렇지 않으면:

        • node를 node의 왼쪽 자식으로 갱신

  • pushLarger() 함수를 정의합니다. 마찬가지로 노드(node), 스택(st), 목표값(target)을 받습니다.

    • node가 NULL이 아닌 동안 다음을 반복합니다.

      • node의 값이 target보다 크거나 같으면:

        • node를 st에 삽입(push)

        • node를 node의 왼쪽 자식으로 갱신

      • 그렇지 않으면:

        • node를 node의 오른쪽 자식으로 갱신

  • 메인 메서드에서는 다음을 수행합니다.

    • 결과를 담을 배열 ret을 선언합니다.

    • 스택 smaller와 larger를 각각 선언합니다.

    • pushLarger(root, larger, target)을 호출합니다.

    • pushSmaller(root, smaller, target)을 호출합니다.

    • k번 반복하면서(매 회차마다 k를 1씩 감소) 다음을 수행합니다.

      • smaller가 비어 있지 않고, (larger가 비어 있거나 |target − smaller.top()->val| < |target − larger.top()->val|)인 경우:

        • curr = smaller.top()으로 설정한 뒤 smaller에서 제거(pop)

        • curr의 값을 ret의 끝에 추가

        • pushSmaller(curr->left, smaller, target)을 호출

      • 그렇지 않으면:

        • curr = larger.top()으로 설정한 뒤 larger에서 제거(pop)

        • curr의 값을 ret의 끝에 추가

        • pushLarger(curr->right, larger, target)을 호출

  • ret을 반환합니다.

작동 원리

pushSmaller는 target 미만의 후보들을, pushLarger는 target 이상의 후보들을 중위 순회(in-order) 순서대로 스택에 쌓습니다. 덕분에 smaller 스택의 top은 항상 target보다 작은 값 중 가장 큰 값(전임자, predecessor)이 되고, larger 스택의 top은 target 이상인 값 중 가장 작은 값(후속자, successor)이 됩니다. 따라서 매 단계마다 이 두 값만 비교하면 target에 가장 가까운 값을 빠르게 골라낼 수 있습니다.

C++ 구현 예제

아래 구현을 통해 더 잘 이해해 보겠습니다.

#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<auto> v){
    cout << "[";
    for(int i = 0; i<v.size(); i++){
        cout << v[i] << ", ";
    }
    cout << "]"<<endl;
}
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:
    vector<int> closestKValues(TreeNode* root, double target, int k) {
        vector<int> ret;
        stack<TreeNode*> smaller;
        stack<TreeNode*> larger;
        pushLarger(root, larger, target);
        pushSmaller(root, smaller, target);
        while (k--) {
            if (!smaller.empty() && (larger.empty() || (abs(target - smaller.top()->val) < abs(target - larger.top()->val)))) {
                TreeNode* curr = smaller.top();
                smaller.pop();
                ret.push_back(curr->val);
                pushSmaller(curr->left, smaller, target);
            }
            else {
                TreeNode* curr = larger.top();
                larger.pop();
                ret.push_back(curr->val);
                pushLarger(curr->right, larger, target);
            }
        }
        return ret;
    }
    void pushSmaller(TreeNode* node, stack <TreeNode*>& st, double target){
        while (node) {
            if (node->val < target) {
                st.push(node);
                node = node->right;
            }
            else {
                node = node->left;
            }
        }
    }
    void pushLarger(TreeNode* node, stack <TreeNode*>& st, double target){
        while (node) {
            if (node->val >= target) {
                st.push(node);
                node = node->left;
            }
            else
                node = node->right;
        }
    }
};
main(){
    Solution ob;
    vector<int> v = {4,2,5,1,3};
    TreeNode *root = make_tree(v);
    print_vector(ob.closestKValues(root, 3.7142, 2));
}

실행 결과

입력

{4,2,5,1,3}, 3.7142, 2

출력

[4, 3]

복잡도 분석

균형 잡힌 BST에서는 초기 스택 구성에 O(log n)이 소요되고, 이후 k개의 값을 하나씩 추출할 때마다 다음 전임자/후속자를 찾는 데 O(log n)이 걸리므로 전체 시간 복잡도는 약 O(k log n)입니다. 트리가 한쪽으로 치우친 최악의 경우에는 O(n)까지 증가할 수 있습니다. 공간 복잡도는 스택에 저장되는 노드 수에 비례하여 O(n)입니다.