728x90

1. Static이란??

Static 키워드를 선언하게 되면 사용할 때만 메모리에 할당하고, 사용 안 할 때는 제거되는 일반 선언들과 달리 프로그램이 종료될 때 까지 메모리에 할당되어 있는 것을 의미합니다.

 

그렇기 때문에 우리가 Static으로 선언한 것들은 따로 생성하지 않고 가져다 사용할 수 있는 것입니다.

 

그렇다면 우리가 흔히 사용하는 Static 변수와 Static 메소드들은 어떤 작동원리를 가지고, 어떤 차이점이 존재하는지 알아보도록 하겠습니다.


2. Static 변수

Static 변수는 클래스 수준에서 선언되며, 인스턴스와 관계없이 모든 객체가 동일한 메모리 공간을 공유합니다. Static 변수는 클래스가 메모리에 로드될 때 한 번만 초기화되며, 프로그램이 종료될 때까지 메모리에 유지됩니다.

Static 변수의 특징

1. 공유 메모리
Static 변수는 클래스 로더가 클래스를 로드할 때 메모리에 할당되며, 해당 클래스의 모든 인스턴스가 이 변수를 공유합니다. 따라서 인스턴스마다 별도의 값을 가지지 않고, 모든 인스턴스가 동일한 값을 참조합니다.

 

2. 클래스 이름으로 접근 가능
Static 변수는 객체를 생성하지 않고, 클래스 이름을 통해 직접 접근할 수 있습니다.

 

3. 메모리 효율성
여러 객체에서 공통으로 사용하는 데이터를 Static 변수로 선언하면, 각 객체가 별도의 메모리를 할당받지 않으므로 메모리 효율성을 높일 수 있습니다.

 

4. 초기화 시점
Static 변수는 프로그램 실행 시점에 클래스가 메모리에 로드될 때 초기화됩니다. 한 번만 초기화되며, 이후 변경된 값은 프로그램 종료 시점까지 유지됩니다.


Static 변수 사용 사례

1. 공유 데이터 관리
객체생성과 관계없이 데이터를 공유하고 싶다면 Static변수를 사용하여 관리할 수 있습니다.

 

2. 상수 선언

변하지 않는 상수 값을 정의할 때 Static 변수를 final 키워드와 함께 사용하여 메모리 낭비를 줄일 수 있습니다.

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
}


3. 유틸리티 클래스에서 공용 변수 사용
특정 설정 값이나 공용 데이터를 유틸리티 클래스에 Static 변수로 선언해 다른 클래스에서 쉽게 접근하도록 합니다.

public final class StandardCharsets {
    private StandardCharsets() {
        throw new AssertionError("No java.nio.charset.StandardCharsets instances for you!");
    }
    public static final Charset US_ASCII = sun.nio.cs.US_ASCII.INSTANCE;
    public static final Charset ISO_8859_1 = sun.nio.cs.ISO_8859_1.INSTANCE;
    public static final Charset UTF_8 = sun.nio.cs.UTF_8.INSTANCE;
    public static final Charset UTF_16BE = new sun.nio.cs.UTF_16BE();
    public static final Charset UTF_16LE = new sun.nio.cs.UTF_16LE();
    public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
}

3. Static 메소드

Static 메소드는 객체를 생성하지 않고도 호출할 수 있는 클래스 수준의 메소드입니다. Static 변수와는 다르게 메소드는 로직을 정의하며, Static 키워드로 선언된 메소드는 클래스의 모든 객체가 동일한 동작을 공유합니다.

Static 메소드의 특징

1. 객체 없이 호출 가능
Static 메소드는 클래스 이름으로 호출할 수 있습니다.

 

2. Static 변수와 연동 가능
Static 메소드는 같은 클래스에 선언된 Static 변수에 직접 접근할 수 있습니다.

 

3. 인스턴스 멤버 사용 불가
Static 메소드 내에서는 클래스의 인스턴스 변수나 인스턴스 메소드를 직접 사용할 수 없습니다. 이는 Static 메소드가 클래스 수준에서 동작하기 때문입니다. 인스턴스 멤버를 사용하려면 해당 메소드에서 객체를 생성하거나 참조를 전달받아야 합니다.

 

4. 상속과 재정의 제한
Static 메소드는 상속받은 클래스에서 오버라이드(재정의)할 수 없습니다. 하지만 동일한 이름으로 정의해 숨길 수는 있습니다.


Static 메소드 사용 사례

1. 유틸리티 메소드 제공
자주 사용되는 공용 로직들을 Static 메소드로 정의해 간편하게 호출할 수 있습니다.

 

2. 팩토리 메소드 구현
특정 객체를 생성하는 로직을 Static 메소드로 제공할 수 있습니다.

 

3. 프로그램의 진입점
Java 프로그램은 main 메소드를 Static으로 선언하여, 객체를 생성하지 않고 프로그램의 실행을 시작합니다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

728x90
728x90

I/O 스트림이란?

I/O 스트림은 Java에서 데이터를 입력(Input)하거나 출력(Output)할 때 사용하는 추상화된 모델입니다.

  • InputStream: 데이터를 읽어오는 데 사용.
  • OutputStream: 데이터를 외부로 쓰는 데 사용.

특징

  • 데이터의 흐름을 Stream으로 간주.
  • Byte 단위 또는 Character 단위로 처리.
  • 데이터 소스: 파일, 네트워크 소켓, 메모리 등.

💡 "Java의 I/O는 Stream 기반이다. 데이터를 한 번에 처리하지 않고, 스트림으로 데이터를 흘려보내면서 효율적으로 작업한다."


InputStream과 OutputStream의 기본 구조

InputStream의 주요 메서드

 
int read() throws IOException  
int read(byte[] b, int off, int len) throws IOException  
void close() throws IOException

OutputStream의 주요 메서드

void write(int b) throws IOException  
void write(byte[] b, int off, int len) throws IOException  
void close() throws IOException
 

주요 I/O 클래스와 사용 사례

파일 읽기: FileInputStream

 
import java.io.FileInputStream;
import java.io.IOException;

public class FileInputExample {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("example.txt")) {
            int data;
            while ((data = fis.read()) != -1) {
                System.out.print((char) data); // 데이터를 문자로 변환해 출력
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

파일 쓰기: FileOutputStream

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("example.txt")) {
            String data = "Java I/O Stream 예제입니다.";
            fos.write(data.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

🔑 TIP: try-with-resources를 사용하면 스트림을 자동으로 닫아 메모리 누수를 방지할 수 있습니다.


Buffered 스트림과 성능 향상

BufferedInputStream 사용 예

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedInputExample {
    public static void main(String[] args) {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("example.txt"))) {
            int data;
            while ((data = bis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

왜 Buffered 스트림을 사용할까?

  • 기본 스트림은 데이터를 1바이트씩 처리.
  • Buffered 스트림은 버퍼를 사용해 데이터 블록을 한꺼번에 처리 -> 성능 향상.
728x90
728x90

레드-블랙 트리란?

레드-블랙 트리란 BTS의 일종으로, BTS의 최악의 시간복잡도 O(n)을 방지하기 위해 균형을 유지하게 하는 균형 이진 탐색트리 중 하나입니다.

FunctionSearchInsertDelete

Function Amortized Worst Case
Select O(log⁡n) O(log⁡n)
Insert O(log⁡n) O(log⁡n)
Delete O(log⁡n) O(log⁡n)

 

레드-블랙 트리의 속성

레드-블랙 트리의 노드들은 red 또는 black색을 가지며, 아래와 같은 5가지 특징을 준수하여 균형을 이룹니다.

  1. 모든 노드는 red 혹은 black이다.
  2. 루트 노드는 black이다.
  3. 모든 리프 노드( NIL )는 black이다.
  4. red 노드의 자식 노드는 항상 black이다.
  5. 모든 노드에서 리프 노드들 까지 가는 경로에 존재하는 black노드의 수는 같다. ( 검색 시작 노드 제외 )

데이터 삽입

우선 레드-블랙 트리에서 삽입하는 노드의 색은 항상 red입니다.

 

데이터 삽입 과정 중 레드 - 블랙 트리의 속성에 위배하는 경우 Rebalancing과정을 거치게 됩니다.

Rebalancing은 색변경(Recoloring)회전(Rotation) 과정으로 이루어져 있으며, 케이스에 따라 두 가지 방법을 혼합하여 적용하면 레드 - 블랙 트리의 속성을 위반하지 않게 만들 수 있습니다.

 

아래에 삽입 시 발생할 수 있는 케이스에 대해 알아보고 또한 Rebalancing과정이 어떻게 이루어질 수 있는지 알아보도록 하겠습니다.

Case1. Root 삽입

맨 처음 Root에 데이터를 삽입하는 Case입니다.

 

데이터 삽입 시 들어오는 노드의 색은 항상 red로 고정되어 있기 때문에 삽입된 노드의 색상은 red입니다.

이때, 레드-블랙 트리 2번째 속성 'root는 black이다'를 위반하기 때문에 Recoloring과정을 통해 black으로 변경하여 줍니다.


Case2. 삽입한 노드의 부모 노드가 black인 경우

데이터 삽입 시 삽입되는 노드의 색상은 red이고, 위반하는 규칙은 존재하지 않습니다.


Case3. 부모 노드가 red이고, 한쪽으로 쏠린 경우

1을 삽입하게 되면 위 사진의 첫번째와 같이 red하위에 red가 존재하는 양상의 띄게 되며 그와 동시에 4번 속성을 위반하게 됩니다.

 

그럴경우 부모노드 색을 black으로 조부모 색을 red로 변경해줍니다.

그 다음 왼쪽으로 쏠림현상이 존재하므로 5를 기준으로 오른쪽으로 회전하여 2가 root에 위치하도록 합니다.

 

수정 후 모습을 보면 4번 속성을 위반하지 않고, 2번 속성도 위반하지 않는 것을 확인 할 수 있었습니다.


Case4. 부모노드가 red이고, 한쪽으로 몰려있으며, 오른쪽으로 꺾인 경우

3을 삽입하게 되면 오른쪽으로 꺾이게 됩니다.

이때 삽입한 노드를 기준으로 좌로 회전합니다.

 

이후 root와 삽입한 노드의 색을 바꿔주고, root를 기준으로 오른쪽으로 회전합니다.


Case5. 부모노드가 red이고, 삼촌노드도 red인 경우

6을 삽입하였습니다.

우선 4번 속성을 위반하기 때문에 부모와 삼촌 노드의 색을 black으로, 조상 노드의 색을 red로 변경합니다.

 

이후 2번 속성을 위반하였기 때문에 루트 노드의 색을 black으로 변경해줍니다.

 

이때 black 하위 black인 경우가 생기게 되는데, 이는 속성을 위반하는 사항이 아니므로 괜찮습니다.


이후 많은 다양한 상황들이 나올 수 있지만, 위 케이스들을 기반으로 비교하면 레드-블랙 트리의 속성을 지키며, 작업을 이어나갈 수 있게 됩니다.


레드-블랙 트리  VS AVL 트리

레드 블랙 트리를 왜 사용하는지, 그리고 AVL 트리와의 차이는 어떤 점들이 존재하는지 알아보도록 하겠습니다.

  레드-블랙 트리 AVL 트리
삽입/삭제 속도 더 빠름 (재조정이 덜 복잡함) 느림 (더 엄격한 균형 유지)
검색 속도 약간 느림 ( 완벽한 균형 x )  더 빠름 (더 균형 잡힘)
구현 난이도 상대적으로 단순 더 빠름(더 균형 잡힘)
적합한 용도 삽입/삭제가 잦은 경우 일기 성능이 중요한 경우
사용처 TreeMap, TreeSet,... Index, Cache,...

Reference

https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

https://www.youtube.com/watch?v=2MdsebfJOyM

 

 

 

728x90
728x90

들어가기에 앞서

해당 Post는 이진 트리에 대한 기초 지식을 요구합니다.

만약 이진트리를 모른다면 이 글을 읽고 난 이후 봐주시기 바랍니다.

이진 탐색 트리 값 삽입

이진트리 중 중복 값은 허용하지 않고, 왼쪽은 부모 노드보다 작은 값을 오른쪽 자식노드는 부모노드 보다 큰 조건을 충족하는 트리를 이진 탐색 트리라고 합니다.

 

위와 같이 데이터가 삽입되며, 왼쪽은 currnet보다 작은 값, 오른쪽은 큰 값을 배치하게 됩니다.

public class BinaryTree {
    TreeNode<Integer> root;

    public void add(Integer value) {
        root = add(root, value);
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, Integer value) {
        if(current == null) {
            return new TreeNode<>(value);
        }

        if(current.getValue() < value) {
            current.setRight(add(current.getRight(), value));
        } else {
            current.setLeft(add(current.getLeft(), value));
        }

        return current;
    }
}

이진 탐색 트리의 삭제

이진 탐색 트리의 삭제는 어떤 상태를 가진 노드를 삭제하느냐로 3가지로 나눠볼 수 있습니다.

Leaf Node 삭제

가장 생각할 게 없는 단순한 삭제입니다.

 

자식 노드가 존재하지 않기 때문에 균형 이진트리가 아니라면 따로 큰 작업이 이뤄지지 않는 삭제입니다.

자식이 1개인 부모 Node 삭제

Leaf Node삭제보다 살짝 작업이 추가된 삭제라고 생각합니다.

 

자식이 1개일 경우 삭제할 노드의 부모 노드에 자식 노드의 Instance정보를 연결해 주고, 본인은 참조되지 않는 방법으로 간단하게 삭제처리할 수 있습니다.

자식이 2개인 부모 Node 삭제

자식이 2개인 경우 어떤 정책을 통해 부모 노드를 대체할 것인지를 고민해야 합니다.

  • 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
  • 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.

위와 같이 정책을 정하여 삭제를 진행할 경우 데이터의 쏠림 현상으로 인해 시간복잡도가 차이 날 수 있는데,

평균적으로 O(log n)의 시간복잡도를 가지며, 최악의 경우 O(n)의 시간복잡도를 가지게 됩니다.

 

중위 전임자 정책을 사용하여 삭제하였을 경우

public class BinaryTree {
    TreeNode<Integer> root;

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;
        // 중위 전임자(In-order Predecessor)
        if(Objects.equals(current.getValue(), value)) {
            TreeNode<Integer> predecessor = predecessor(current.getLeft());
            predecessor.setRight(current.getRight());
            if(current.getLeft() != null)
                add(predecessor, current.getLeft());
            return predecessor;
        }

        current.setLeft(remove(current.getLeft(), value));
        current.setRight(remove(current.getRight(), value));

        return current;
    }

    // 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
    private TreeNode<Integer> predecessor(TreeNode<Integer> current) {
        if(current.getRight() == null)
            return current;

        TreeNode<Integer> predecessor =  predecessor(current.getRight());
        if(Objects.equals(current.getRight().getValue(), predecessor.getValue()))
            current.setRight(null);

        return predecessor;
    }
}


중위 후속자 정책을 사용하여 삭제하였을 경우

public class BinaryTree {
    TreeNode<Integer> root;

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;

        // 중위 후속자(In-order Successor)
        if(Objects.equals(current.getValue(), value)) {
            TreeNode<Integer> successor = successor(current.getRight());
            successor.setLeft(current.getLeft());
            if(current.getRight() != null)
                add(successor, current.getRight());
            return successor;
        }

        current.setLeft(remove(current.getLeft(), value));
        current.setRight(remove(current.getRight(), value));

        return current;
    }

    // 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.
    private TreeNode<Integer> successor(TreeNode<Integer> current) {
        if(current.getLeft() == null)
            return current;

        TreeNode<Integer> successor =  successor(current.getLeft());
        if(Objects.equals(current.getLeft().getValue(), successor.getValue()))
            current.setLeft(null);
        return successor;
    }
}


최종 코드

public class BinaryTree {
    TreeNode<Integer> root;

    public BinaryTree() {
        root = null;
    }

    public void add(Integer value) {
        root = add(root, value);
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, Integer value) {
        if(current == null) {
            return new TreeNode<>(value);
        }

        if(current.getValue() < value) {
            current.setRight(add(current.getRight(), value));
        } else {
            current.setLeft(add(current.getLeft(), value));
        }

        return current;
    }

    private TreeNode<Integer> add(TreeNode<Integer> current, TreeNode<Integer> prevNode) {
        if(current == null) {
            return prevNode;
        }

        if(current.getValue() < prevNode.getValue()) {
            current.setRight(add(current.getRight(), prevNode));
        } else {
            current.setLeft(add(current.getLeft(), prevNode));
        }

        return current;
    }

    public void remove(Integer value) {
        root = remove(root, value);
    }

    private TreeNode<Integer> remove(TreeNode<Integer> current, Integer value) {
        if(current == null) return null;

        if(!Objects.equals(current.getValue(), value)) {
            current.setLeft(remove(current.getLeft(), value));
            current.setRight(remove(current.getRight(), value));
            return current;
        }

        // Leaf일 때 삭제
        if(current.getRight() == null && current.getLeft() == null)
            return null;

        if(current.getLeft() == null) return current.getRight();
        if(current.getRight() == null) return current.getLeft();

//        // 중위 전임자(In-order Predecessor)
//        TreeNode<Integer> predecessor = predecessor(current.getLeft());
//        predecessor.setRight(current.getRight());
//        if(current.getLeft() != null)
//           add(predecessor, current.getLeft());
//        return predecessor;

        // 중위 후속자(In-order Successor)
        TreeNode<Integer> successor = successor(current.getRight());
        successor.setLeft(current.getLeft());
        if(current.getRight() != null)
            add(successor, current.getRight());

        return successor;

    }

    // 중위 전임자(In-order Predecessor): 삭제할 노드의 왼쪽 서브트리에서 가장 큰 값을 가진 노드.
    private TreeNode<Integer> predecessor(TreeNode<Integer> current) {
        if(current.getRight() == null)
            return current;

        TreeNode<Integer> predecessor =  predecessor(current.getRight());
        if(Objects.equals(current.getRight().getValue(), predecessor.getValue()))
            current.setRight(null);

        return predecessor;
    }

    // 중위 후속자(In-order Successor): 삭제할 노드의 오른쪽 서브트리에서 가장 작은 값을 가진 노드.
    private TreeNode<Integer> successor(TreeNode<Integer> current) {
        if(current.getLeft() == null)
            return current;

        TreeNode<Integer> successor =  successor(current.getLeft());
        if(Objects.equals(current.getLeft().getValue(), successor.getValue()))
            current.setLeft(null);
        return successor;
    }


    // 전위 순회 (Pre-order Traversal) 출력 메서드
    public void traversePreOrder(TreeNode<Integer> node) {
        if (node != null) {
            System.out.print(" " + node.getValue());
            traversePreOrder(node.getLeft());
            traversePreOrder(node.getRight());
        }
    }

    // 중위 순회 (In-order Traversal) 출력 메서드
    public void traverseInOrder(TreeNode<Integer> node) {
        if (node != null) {
            traverseInOrder(node.getLeft());
            System.out.print(" " + node.getValue());
            traverseInOrder(node.getRight());
        }
    }

    // 후위 순회 (Post-order Traversal) 출력 메서드
    public void traversePostOrder(TreeNode node) {
        if (node != null) {
            traversePostOrder(node.getLeft());
            traversePostOrder(node.getRight());
            System.out.print(" " + node.getValue());
        }
    }

    // 레벨 순회 (Level-order Traversal) 출력 메서드
    public void traverseLevelOrder(TreeNode root) {
        if (root == null) return;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            System.out.print(" " + node.getValue());

            if (node.getLeft() != null) queue.add(node.getLeft());
            if (node.getRight() != null) queue.add(node.getRight());
        }
    }

    public static void main(String[] args) {
        BinaryTree bt = new BinaryTree();
        bt.add(5);
        bt.add(2);
        bt.add(3);
        bt.add(1);
        bt.add(0);
        bt.add(7);
        bt.add(8);
        bt.add(6);

        bt.remove(5);

        // 순회 결과 출력
        System.out.print("Pre-order traversal:");
        bt.traversePreOrder(bt.root);
        System.out.println();

        System.out.print("In-order traversal:");
        bt.traverseInOrder(bt.root);
        System.out.println();

        System.out.print("Post-order traversal:");
        bt.traversePostOrder(bt.root);
        System.out.println();

        System.out.print("Level-order traversal:");
        bt.traverseLevelOrder(bt.root);
        System.out.println();
    }
}
728x90

'알고리즘 문제풀이 > 자료구조' 카테고리의 다른 글

[Tree] 이진 트리 순회  (1) 2024.11.08
[Tree] 이진 트리 ( Binary Tree )  (0) 2024.11.07
[자료구조] 스택(Stack)  (0) 2024.10.21
728x90

 

바보같이 너무 많이 생각해서 어려웠던 문제였습니다.

 

문제자체는 단순합니다.

parent 값들을 주고 해당 값으로 생성된 node들 중 하나를 삭제하고, 이후 leaf의 갯수를 알아내면 되는 문제입니다.

 

parent와 index값을 저장을 하고,

index를 부모로 가지는 node들을 child로 저장,

삭제된 노드들의 정보를 가지게 된다면 간단하게 해결할 수 있는 문제입니다.

 

다만 주의할 점은 현재 삭제된 노드들만 가지고 있는 노드 또한 leaf라는 점입니다.

이점은 코드를 확인해보며 찾아보시기 바랍니다.

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int N = Integer.parseInt(br.readLine());

        int[] parents = new int[N];
        boolean[] isDeleted = new boolean[N];
        List<List<Integer>> graph = new ArrayList<>();

        for(int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
        }

        int root = -1;
        StringTokenizer st = new StringTokenizer(br.readLine());

        for(int i = 0; i < N; i++) {
            parents[i] = Integer.parseInt(st.nextToken());
            if(parents[i] == -1) {
                root = i;
            } else {
                graph.get(parents[i]).add(i);
            }
        }
        int deleteNode = Integer.parseInt(br.readLine());
        if(parents[deleteNode] > -1) {
            delete(graph.get(parents[deleteNode]), isDeleted, deleteNode);
        } else {
            isDeleted[root] = true;
        }
        bw.write(countLeafNode(graph, isDeleted, root) + "");
        bw.flush();
        br.close();
        bw.close();
    }

    public static void delete(List<Integer> graph, boolean[] isDeleted, int node) {
        for(int child : graph) {
            if(child == node) {
                isDeleted[child] = true;
                return;
            }
        }
    }

    public static int countLeafNode(List<List<Integer>> graph, boolean[] isDeleted, int node) {
        if(isDeleted[node]) return 0;
        if(graph.get(node).isEmpty()) return 1;
        int count = 0;

        for(int child : graph.get(node)) {
            count += countLeafNode(graph, isDeleted, child);
        }

        return count == 0 ? 1 : count;
    }
}
728x90
728x90

문제를 보면 모든 노드들은 연결이 되어있고, 모든 노드들을 돌아볼 때 가장 적은 종류의 비행기( 간선 정보 )를 타고 움직이는 수를 알아내야하는 것이 문제의 의도 입니다.

 

이때 우리는 모든 노드가 연결되어있고, 최소한의 경로로 모든 노드를 방문해야한다는 조건에서 최소신장 트리를 떠올릴 수 있어야 합니다.

 

최소신장 트리의 모든 노드를 연결하면서 가중치의 합을 구하면 되겠는데, 이때 값은 N - 1로써 가장 간단하게 구할 수 있습니다.

(N은 노드 수입니다.)

 

import java.io.*;
import java.util.*;

public class Main {
    static BufferedReader br;
    static BufferedWriter bw;
    static StringTokenizer st;
    public static void main(String[] args) throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int T = Integer.parseInt(br.readLine());

        StringBuilder sb = new StringBuilder();
        while (T-- > 0) {
            st = new StringTokenizer(br.readLine());
            int n = Integer.parseInt(st.nextToken());
            int m = Integer.parseInt(st.nextToken());
            for (int i = 0; i < m; i++) {
                br.readLine();
            }
            sb.append(n - 1).append("\n");
        }
        bw.write(sb.toString());
        bw.flush();
        br.close();
        bw.close();
    }

}
  • 시간복잡도: O(1)
  • 공간복잡도: O(1)
728x90
728x90

1. Mysql 접속

기본적으로 데이터베이스 작업을 하거나 테이블 작업을 하려면 설치한 mysql에 접속해야합니다.

 

만약 처음 설치하였다면 기본적으로 root라는 계정을 할당해주고, 해당 계정은 초기 설치할 때 설정한 비밀번호로 설정이 되게 됩니다. 

mysql -u root -p

2. Database

2-1. 생성

CREATE DATABASE 데이터베이스명;

2-2. 목록 확인

SHOW DATABASES;

2-3. 데이터베이스 선택

USE 데이터베이스명;

2-4. 데이터베이스 삭제

DROP DATABASE 데이터베이스명;

3. 사용자 및 권한 설정

3-1. 사용자 생성

CREATE USER '사용자명'@'호스트' IDENTIFIED BY '비밀번호';

호스트: 접속할 수 있는 정보로 로컬만 허용할 경우 localhost, 모두 허용할 경우 %로 지정

3-2. 권한 부여

GRANT 권한종류 ON 데이터베이스명.* TO '사용자명'@'호스트';
FLUSH PRIVILEGES;

권한 종류: ALL PRIVILEGES, SELECT, INSERT, UPDATE, DELETE, CREATE, DROP

3 - 2. 권한 확인

SHOW GRANTS FOR '사용자명'@'호스트';

3-3. 사용자 삭제

DROP USER '사용자명'@'호스트';
728x90

'CS > Database' 카테고리의 다른 글

[Index] 구축한 서버 Query분석 및 성능 측정 후 Index 구축  (2) 2024.08.08
Redis는 어떤걸까?  (0) 2024.04.08
728x90

개요

개발을 하거나 면접을 보게 되면 Spring Data Jpa관련 단골 질문으로 가장 많이 나오는 내용이라 생각합니다.

 

그럼 도대체 OSIV가 뭐길래 JPA에서 계속 말이 나오게 되는 걸까? 한번 알아보도록 하겠습니다.


OSIV란

OSIV란 Open Session In View의 약자로써 사실 JPA가 도입되면서 Open EntityManager In View로 개념이 확립되었으나 편의상 지금까지 OSIV라고 부르고 있습니다.

말 그대로 OSIV는 View까지 EntityManager를 열어두는 것을 의미합니다.

 

그럼 JPA에서 EntityManager이라 함은 무엇일까요?

바로 영속성 컨텍스트를 의미합니다.

 

영속성 컨텍스트는 요청이 들어오면 생성되고 사라지는 휘발성 콘텍스트입니다.

해당 콘텍스트는 기본적으로 요청이 들어오면 생성되고, 요청이 끝나면 사라지게끔 되어있는데, 이는 OSIV 설정이 기본으로 True로 설정되어 있어 가능한 일입니다.

 

만약 OSIV가 False로 설정되어 있다면 영속성 콘텍스트는 Transaction범위에 따라 생성되고 Transaction이 끝날 시 사라지게 됩니다.

 

이로써 OSIV가 어떤 설정인지 알았으니 해당 설정이 기능과 성능에 미치는 영향에 대해 알아보겠습니다.


OSIV가 미치는 영향

OSIV 설정은 아래 그림으로 한 번에 확인할 수 있습니다.

위와 같이 데이터를 생성, 수정, 삭제 기능들은 트랜잭션 범위 내에서 작업이 가능합니다.

다만 읽기 기능의 경우 영속성 콘텍스트 범위 내에 있을 경우 읽기가 가능한데,

OSIV는 영속성 콘텍스트를 view까지 늘려 트랜잭션이 아닌 요청의 생존 범위까지 늘리는 것을 의미합니다.

 

즉 생성, 수정, 삭제는 OSIV의 목적은 아니고, 읽기 범위를 늘리는 것이 주목적이라고 보시면 됩니다.

 

그럼 왜! 읽기 범위를 늘리는 것인가?

이는 Lazy Loading과 밀접한 관련이 있습니다.

 

Lazy Loading이란 지연 읽기 기능인데, 처음 데이터베이스에서 조회하는 것이 아닌 추후 해당 데이터를 사용할 때 조회하는 기능입니다.

 

해당 기능을 사용할 때, 영속성 콘텍스트의 생존범위 내에서 로딩하는지가 중요한데 이를 어디까지 허용할 것인지를 조절할 수 있다고 보시면 좋을 것 같습니다.

 

그럼 해당 기능이 왜 성능 향상에 기여할 수 있는가? 영속성 콘텍스트를 유지하는 것 또한 많은 자원을 소모하게 됩니다.

이를 View까지 유지하지 않고 트랜잭션 범위에 맞춰 종료하게 되면 OSIV가 TRUE인 상태보다 좀 더 효율적으로 자원을 사용할 수 있게 됩니다.

 

해당 옵션은 위에서 말했듯 default True인 옵션입니다.

만약 애플리케이션 성능 향상에 있어 관심이 많다면 테스트해보시길 바랍니다.


Reference

https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.jpa-and-spring-data.open-entity-manager-in-view

 

SQL Databases :: Spring Boot

Spring’s JdbcTemplate and NamedParameterJdbcTemplate classes are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.ste

docs.spring.io

 

 

728x90

+ Recent posts