메모리 영역에는 code, data, heap, stack이 있습니다. 코드는 실행할 코드가 저장되는 텍스트 영역, 데이터는 전역변수와 정적변수가 저장되는 영역, 스택은 함수의 호출과 관계되는 지역변수와 매개변수가 저장되는 영역, 힙 영역은 사용자가 직접 관리할 수 있는 동적 메모리 영역입니다.
데드락, 또는 교착 상태란, 둘 이상의 프로세스들이 자원을 점유한 상태에서 서로 다른 프로세스가 점유하고 있는 자원을 요구하며 무한정 기다리는 상황을 말합니다. 예를 들면 A프로세스가 1번 자원을, B프로세스가 2번 자원을 점유하고 있는 상황에서 A는 2번 자원이, B는 1번 자원이 필요할 때 생길 수 있습니다.
데드락을 해결하기 위해서는 데드락의 4가지 조건 중 하나라도 없애면 됩니다. 공유 자원 중 많은 경우가 한 프로세스만 사용할 수 있는 경우가 많기 때문에 상호 배제는 없애기 힘들기에 많은 경우 순환 대기를 막는데 초점이 맞추어져있습니다. 해결 방법은 예방, 회피, 탐지와 복구 등이 있습니다.
임계 영역이란 프로세스간의 공유 자원을 접근함에 있어 문제가 생기지 않도록 한번에 한 프로세스만이 접근하도록 해야하는 영역을 의미합니다. 임계 영역을 만들기 위해서는 세가지 조건이 필요합니다. 하나의 프로세스만이 들어갈 수 있어야 한다는 상호 배제, 임계 영역에 들어간 프로세스가 없는 상태에서 여러 프로세스가 동시에 임계 영역에 진입하려 할 때 어느 것이 들어갈지 결정해준다는 진행, 그리고 기아를 방지하기위한 한정 대기입니다.
뮤텍스는 lock을 사용해 공유 자원에 하나의 프로세스 또는 스레드만이 접근할 수 있도록 하는 것입니다. 그에 반해 세마포어는 세마포어의 카운트 갯수만큼의 프로세스 또는 스레드가 공유 자원에 접근할 수 있도록 하는 것입니다. 따라서 세마포어의 카운트를 1로 설정하면 뮤텍스로 사용이 가능합니다.
페이징 기법으로 관리되는 운영체제에서 주기억 장치에 필요한 페이지가 적재되어있지 않았을 때 어떤 페이지 프레임을 선택해 교체할지 선택하기 위해 사용하는 알고리즘입니다. FIFO, optimal 페이지 교체, LRU (Least Recently Used), LFU (Least Frequently Used), MFU (Most Frequently Used) 등이 있습니다. Optimal 페이지 교체는 실제로 활용할 수 있는 방법은 아니기에 연구 목적으로만 사용되며 LFU와 MFU는 구현 비용이 높기 때문에 실제로 쓰이지 않습니다.
컨텍스트 스위칭이란 어떤 프로세스를 실행하고 있는 중에 발생한 인터럽트로 인해 현재 실행 중인 프로세스를 중지하고 다음 우선 순위 프로세스를 실행하기 위한 과정입니다. 현재 실행중인 프로세스의 상태, 즉 PCB를 저장하고 다음 프로세스를 동작시켜 작업을 처리한 후에 이전에 저장했던 컨텍스트를 복구시킵니다.
동기는 요청과 결과가 동시에 일어난다는 의미로, 메소드를 실행시킴과 동시에 값이 반환되기 전까지는 blocking되어 있다는 것을 의미합니다. 비동기의 경우, blocking 되지 않고 이벤트 큐에 넣거나 백그라운드 스레드에게 해당 task를 위임하고 바로 다음 코드를 실행합니다.
동기 방식은 설계가 간편하고 직관적이라는 장점이 있으나 결과가 주어질때가지 대기해야한다는 단점이 있습니다. 비동기 방식은 동기보다 복잡하지만 결과가 주어질 때까지 다른 작업을 수행할 수 있기 때문에 자원을 효율적으로 사용할 수 있습니다.
OOP를 더 발전시킨 개념으로 OOP에서 비즈니스 로직을 모듈화했다면 거기서 관점을 다르게해인프라/부가 기능 측면에서 공통된 요소를 추출하는 것입니다. 예를들면 OOP가 프로그램을 계좌이체/입출금/이자계산으로 모듈화 했다면 AOP는 그 중에서 공통적으로 발생하는 부가 기능인 인증, 권한 체크, 트랜잭션 등을 분리시켜 필요한 시점에 자동으로 삽입되게 하는 것입니다.
추상클래스는 일반 클래스와 별 다를 것이 없습니다. 단지, 추상 메서드를 선언하여 상속을 통해서 자손 클래스에서 완성하도록 유도하는 클래스입니다. 그래서 미완성 설계도라고도 표현합니다. 상속을 위한 클래스이기 때문에 따로 객체를 생성할 수 없습니다.
추상클래스가 미완성 설계도라면 인터페이스는 기본 설계도라고 할 수 있습니다. 인터페이스도 추상클래스처럼 다른 클래스를 작성하는데 도움을 주는 목적으로 작성하고 인터페이스는 추상메서드만을 가지고있습니다. 클래스와 다르게 다중상속이 가능합니다.
추상 클래스와 인터페이스를 나눠서 사용하는 이유는 사용 의도와 공통 기능 재사용이라는 두가지가 있습니다. 추상 클래스는 '상속', 즉 is-a 의 관계가 되기 때문에 클래스의 구분을 추상 클래스로 해결하고, 다중 상속이 가능한 인터페이스는 has-a 관계로 생각해 클래스가 가지고있는 기능들에 대한 부분을 해결하는 용도로 쓰입니다. 또한 만약 모든 클래스가 인터페이스를 사용해서 기본 틀을 구성한다면 공통으로 필요한 기능들도 모든 클래스에서 오버라이딩 하여 재정의 해야하는 번거로움이 있습니다.
자바의 메모리 공간은 method, stack, heap, PC register, native method stack 영역으로 구분되고 데이터의 타입에 따라 적절한 영역에 할당됩니다.
Method 영역에는전역 변수, static으로 선언되는 것들을 저장하며, 클래스 정보, static 변수, 변수 정보, 메소드 정보, 런타임 상수풀등을 저장합니다.프로그램의 시작부터 종료까지메모리에 남아있으며모든 쓰레드가 공유합니다. 그렇기 때문에 이 영역에 저장되어있는 데이터들을 어디서든 사용할 수 있게 됩니다. JVM이 동작해서 클래스가 로딩될 때 할당됩니다.
런타임 상수풀: JVM은 런타임 상수풀에서 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조합니다
Heap 영역에는Object 타입의 데이터와배열을 저장합니다.가비지 컬렉터가 주기적으로 관리하는 영역이기도합니다. Compile time시 할당되며 이 또한 모든 쓰레드가 공유합니다.
Stack 영역은 메서드 호출 시마다 스택 프레임(그 메서드만을 위한 공간)의 형태로 생성됩니다. 그리고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장합니다. 만약, 지역변수이지만 Reference Type일 경우에는Heap 에 저장된 데이터의 주소값을 Stack 에 저장해서 사용하게 됩니다. 마지막으로, 메서드 수행이 끝나면 프레임별로 삭제하게되고 쓰레드 당 하나씩 존재합니다.
PC register 영역은 쓰레드가 생성될 때마다 생성되는 영역으로 프로그램 카운터, 즉 현재 스레드가 실행되는 부분의 주소와 명령을 저장하고 있는 영역입니다.
Natvie method stack 영역은 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다. JNI(Java Native Interface)를 통해 호출되는 다른 언어의 코드를 수행하기 위해 존재합니다.
Heap 영역은 young generation, tenured generation으로 나누어져있으며 해당 영역은 application에서 사용됩니다. Java 7까지는 이에 추가로 JVM이 사용하는 permanent generation영역이 존재했으나 Java 8부터는 OS가 관리하는 native 영역의 메모리의 Metaspace로 바뀌었습니다.
Young generation은 또다시eden영역과survivor영역으로 나누어져있습니다. Heap영역에 객체가 새로 생성되면 eden에 저장이 되고 eden이 어느정도 차면 참조 정도에 따라 survivor 영역의 빈 공간으로 이동됩니다.
Tenured generation에는old영역이 있으며 young generation 영역이 차게되면 참조 정도에 따라 이 영역으로 옮겨집니다. 이 과정은minor garbage collection이라고 불립니다. Old 영역의 허용치가 넘어가면 old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는major GC가 진행됩니다. Major GC가 진행되는 중에는 GC를 진행하는 쓰레드를 제외한 모든 쓰레드가 멈추는 'stop-the-world'가 발생합니다.
Heap Permanent Generation 제거: 이전에는 permanent generation이라는 이름을 가지고 초기 설정시 설정해여야했던 메모리가 metaspace로 변경되었습니다. Metaspace는 런타임시 메모리 요구 사항에 따라 다이나믹하게 자체 크기 조절이 가능하며 OS레벨에서 관리되는 native 메모리 영역에 위치합니다.
Interface에 Default, Static Methods를 추가할 수 있게됨: default method는 하위 클래스에서 재정의가 가능하지만 static method는 불가합니다.
함수형 interfaces:하나의 기능을 제공하는 단 하나의 추상 메소드를 정의하는 인터페이스가 추가되었습니다. 단, 추상 메소드 외에 디폴트 메소드나 정적 메소드는 원하는 만큼 사용할 수 있고, @FunctionalInterface와 함께 쓰이며, 만약 추상 메소드가 여러 개라면 컴파일 타임에서 에러를 잡아낼 수 있습니다. 주요 함수형 인터페이스로는 Consumer, Supplier, Function 등이 있습니다.
람다 표현식: 람다 표현식은 함수를 식으로 나타내는 것으로 함수를 변수로 나타낼 수 있다는 장점이 있습니다.이러한 람다 표현식은 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높이는 데 그 목적이 있습니다. Java 8 버전부터는 람다 표현식을 사용하여 자바에서도 함수형 프로그래밍을 할 수 있게 되었습니다.
메서드 참조: 람다식의 가독성을 높이기 위해 사용하는 방법입니다. 종류로는 정적 메서드참조, 인스턴스 메서드 참조, 특정 유형의 객체의 인스턴스 메서드 참조, 생성자 참조 등이 있으며 모두 더블콜론을 사용하여 사용됩니다.
Stream API: 스트림은 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해 주는 내부 반복자입니다. 스트림 API는 데이터를 추상화하여 다루므로, 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공합니다.
컬렉션은 데이터를 어떻게 저장/관리하고 접근하는지를 목표하고 있습니다. 이와 반대로 스트림은 데이터를 직접 접근하거나 조작하는 기능을 제공하지 않으며, 데이터를 어떻게 계산할지에 대한 목표를 가지고있습니다. 즉, 데이터의 저장/관리가 목적이라면 컬렉션을, 데이터의 계산이 목적이라면 스트림을 사용하는게 좋다고 볼 수 있습니다.
Date and time API 지원
Optional 지원: null에 대한 참조를 안전하게할 수 있게됩니다.
배열 정렬의 병렬처리: 이전의 Arrays.sort는 merge sort와 같은 순차적 실행이 되는 방식을 사용했으나 Arrays.parallelSort가 추가되며 병렬처리가 가능해졌습니다.
String은 불변하는 특성을 가지고있기 때문에 수정이 불가합니다. 따라서 수정이 발생할때마다 새로운 인스턴스가 생성되고 이전 값들은 가비지 컬렉션 발생 시 삭제됩니다. StringBuffer은 가변하는 특성을 가지고있으며 동기화를 지원하기 때문에 멀티스레드 환경에서 안전합니다. StringBuilder 또한 가변하는 특성을 가지고있으나 동기화를 지원하지 않기에 멀티스레드 환경에서 사용하기에 적합하지 않으나 단일 스레드 환경이라면 StringBuffer보다 높은 성능을 보입니다.
String은 primitive type가 아니기 때문에 heap에 저장됩니다. 그러나 java는 String 객체를 다른 객체들과 달리 heap 메모리 영역 안의 String constant pool에서 관리합니다. 만약 따옴표로 선언이 되면 이 String 상수풀에서 해당 값을 가진 String이 있는지 확인하고 있다면 그 주소값을 리턴하고, 없다면 String 상수풀에 해당 객체를 생성, 할당한 뒤 그 주소값을 리턴합니다. 이를 통해서 같은 값을 가진 String 객체가 생성되는 것을 막을 수 있습니다.
만약 new를 사용한다면 다른 객체들처럼 강제로 heap 영역에 객체를 생성합니다. 그러나 이럼에도 다른 객체와 다르게 동작하는데, 만약 new를 사용했다 하더라도 String 상수풀에 그 값이 있는지 확인하고 없다면 String 상수풀에도 객체를 생성하여 총 두개의 객체를 생성합니다.
Oracle을 이용해 프로그래머스 레벨1 문제를 모두 풀며 잊지 않기 위한 내용을 정리합니다. 문제링크는 생략합니다.
1. 상위 N개 행 조회하기
ROWNUM을 이용하는 방법.
SELECT NAME
FROM (SELECT NAME FROM ANIMAL_INS ORDER BY DATETIME)
WHERE ROWNUM <= 1;
2. NULL 처리
NVL : NULL값이 있으면 다른 NULL이 아닌 지정값으로 조회되도록 하는 함수
SELECT warehouse_id, warehouse_name, address, NVL(freezer_yn, 'N')
from food_warehouse
where address like '경기도%';
-- 코드를 입력하세요
SELECT PT_NAME, PT_NO, GEND_CD, AGE, NVL(TLNO, 'NONE')
FROM PATIENT
WHERE AGE <= 12 AND GEND_CD = 'W'
ORDER BY AGE DESC, PT_NAME;
3. 날짜처리
TO_CHAR : 날짜 데이터와 형식을 인자로 넘기면 형식에 맞추어 출력해주는 함수
SELECT COUNT(*) AS USERS
FROM USER_INFO
WHERE TO_CHAR(JOINED, 'YYYY') = '2021' AND AGE >=20 AND AGE <= 29;
-- 코드를 입력하세요
SELECT dr_name, dr_id, mcdp_cd, to_char(hire_ymd, 'yyyy-mm-dd')
from doctor
where mcdp_cd IN ('CS', 'GS')
order by hire_ymd desc, dr_name asc;
4. 기본 JOIN
아직 JOIN에 대해 깊이있게 이해를 못해서 추가적으로 공부가 필요합니다.
SELECT H.FLAVOR
FROM FIRST_HALF H
INNER JOIN ICECREAM_INFO I ON H.FLAVOR = I.FLAVOR
WHERE H.TOTAL_ORDER > 3000 AND I.INGREDIENT_TYPE = 'fruit_based';
SELECT B.TITLE,
B.BOARD_ID,
R.REPLY_ID,
R.WRITER_ID,
R.CONTENTS,
TO_CHAR(R.CREATED_DATE, 'YYYY-MM-DD') AS CREATED_DATE
FROM USED_GOODS_BOARD B
INNER JOIN USED_GOODS_REPLY R ON B.BOARD_ID = R.BOARD_ID
WHERE TO_CHAR(B.CREATED_DATE, 'YYYYMM') = '202210'
ORDER BY R.CREATED_DATE, B.TITLE;
5. 평균, 반올림
AVG는 평균을 구해주며
ROUND는 반올림한 값을 줍니다. ROUND의 두번째 인자값에 따라 반올림 위치를 지정할 수 있습니다.
두번째 인자를 적지 않는다면 0으로 인식하며 소수점 첫째짜리에서 반올림한 값을 리턴합니다.
SELECT ROUND(AVG(DAILY_FEE)) AS AVERAGE_FEE
FROM CAR_RENTAL_COMPANY_CAR
WHERE CAR_TYPE = 'SUV';
6. CASE-WHEN-THEN
주어진 테이블의 정보를 통해 새로운 컬럼을 만들고 싶을때 사용하는 것으로 이해했습니다.
조건이 여러개라면 WHEN-THEN-WHEN-THEN...형태로 이어붙이면 되는것 같습니다.
-- 코드를 입력하세요
SELECT HISTORY_ID, CAR_ID, TO_CHAR(START_DATE, 'YYYY-MM-DD') AS START_DATE, TO_CHAR(END_DATE, 'YYYY-MM-DD') AS END_DATE,
CASE WHEN(END_DATE - START_DATE + 1) >= 30 THEN '장기 대여' ELSE '단기 대여' END AS RENT_TYPE
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE TO_CHAR(START_DATE, 'YYYY-MM') = '2022-09'
ORDER BY HISTORY_ID DESC;
import java.util.ArrayList;
import java.util.List;
public class SubjectImpl implements Subject{
private final List<Observer> observerList;
public SubjectImpl(){
observerList = new ArrayList<>();
}
// Observer 추가
@Override
public void registerObserver(Observer o) {
observerList.add(o);
}
// Observer 삭제
@Override
public void removeObserver(Observer o) {
observerList.remove(o);
}
// Observer들에게 이벤트 알림
@Override
public void notifyObserver(String title) {
for (Observer o : observerList)
o.update(title);
}
}
Subject를 구현한 클래스입니다.
Observer들을 List를 이용해 관리합니다.
옵저버를 등록하고 알림을 보내는 것을 간단하게 구현했습니다.
public class youtube {
public static void main(String[] args) {
// 유튜브 채널 생성
Subject channel = new SubjectImpl();
// 구독자 생성
Observer subscriber1 = new ObserverImpl("철수");
Observer subscriber2 = new ObserverImpl("영희");
// 구독자 등록
channel.registerObserver(subscriber1);
channel.registerObserver(subscriber2);
// 영상 업로드 알림
channel.notifyObserver("웃긴 짤");
channel.notifyObserver("무서운 짤");
// 영희가 구독 취소
channel.removeObserver(subscriber2);
// 영상 업로드 알림
channel.notifyObserver("슬픈 짤");
}
}
실행 결과는 아래와 같습니다.
영희가 구독을 취소한 이후로는 영희에게 알림이 가지 않습니다.
장점
1. 옵저버 패턴을 사용하면 Subject의 상태 변경을 매번 일일히 확인하지 않고도 자동으로 감지가 가능합니다.
2. Subject의 코드를 변경하지 않아도 새 구독자 클래스를 도입이 가능해 OCP를 준수합니다.
3. 런타임 시점에서 Subject와 Observer가 구독 알림 관계를 맺을 수 있습니다.
4. Subject와 Observer의 관계를 느슨하게 유지할 수 있습니다.
단점
1. 알림의 순서를 제어할 수 없으며, 무작위 순서로 알림을 받게됩니다.
2. 무분별한 사용은 코드의 복잡도가 증가합니다.
3. 옵저버 객체 등록 이후 해지하지 않으면 메모리 누수가 발생할 수 있습니다.
MVC 패턴
정의
MVC 패턴은 사용자 인터페이스를 설계하는 데 사용되는 디자인 패턴으로, 애플리케이션을 세 가지 주요 구성 요소로 나눕니다.
1. Model: 애플리케이션의 데이터와 비즈니스 로직을 처리합니다.
2. View: 사용자 인터페이스 요소를 처리하고 모델의 데이터를 사용자에게 표시합니다.
3. Controller: 사용자 입력을 처리하고 모델과 뷰를 업데이트합니다.
옵저버 패턴은 MVC 패턴의 모델과 뷰 간의 관계를 구현하는 데 자주 사용됩니다.
모델이 옵저버 패턴의 Subject이고, 뷰가 Observer가 됩니다.
모델의 상태가 변경되면, 모델은 뷰에게 변경 사항을 알리고, 뷰는 이를 반영하여 화면을 업데이트 합니다.
이것을 통해 MVC 패턴에서 모델과 뷰 간의 느슨한 결합을 유지할 수 있습니다.
옵저버 패턴을 직접 구현하지 않아도 자바의 내장 Observer 객체가 있어 클래스를 상속하기만 하면 쉽게 이용할 수 있습니다. (java.util.Observer, java.util.Observable)
하지만 Observable는 인터페이스가 아닌 클래스입니다. Observable클래스를 상속해야 하는데, 자바에서는 단일 상속만 지원하기 때문에
Subject역할을 하는 클래스가 다른 클래스를 상속하고 있는 상태라면 내장 객체를 이용할 수 없습니다.
메서드 위임을 해주면 되지 않나 싶지만, Observable의 메서드가 protected로 선언되어 있기 때문에 결국 자식 클래스에서 호출할 수 밖에 없습니다.
위 그림처럼 X노드를 P노드와 자리를 바꾸고 트리의 형태에 따라 옮긴 후 노드간의 관계를 재설정 하도록하는 함수를 만들것입니다.
void rotate(node* x){ // 위로 올릴 노드
node* p = x -> p; // x의 부모 노드
node* b = NULL; // b노드
if(!p) return; // x가 루트라면 리턴
if (x == p->l){ // x가 p의 왼쪽 자식
p -> l = b = x -> r; // p의 왼쪽을 b노드로
x -> r = p; // x의 오른쪽 자식을 p노드로
}else{ // x가 p의 오른쪽 자식
p -> r = b = x -> l; // p의 오른쪽을 b노드로
x -> l = p; // x의 왼쪽 자식을 p노드로
}
// 부모노드 재설정
x -> p = p -> p; // x의 부모는 p의 부모
p -> p = x; // p의 부모는 x
if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p
// x가 루트노드가 아닐경우
// p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
(x -> p ? p == x -> p -> l ? x -> p -> l : x -> p -> r : tree) = x;
}
node* b 는 위 그림에서의 B노드입니다.
Splay
이제 Splay 함수를 구현할것입니다. Splay 함수는 특정 노드를 루트노드까지 올리는 작업을 하게 될것입니다.
3 - 2. 방향이 다르다면, rotate(x)를 두 번 수행합니다. (Zig-Zag step)
이걸 x가 루트가 될 때까지 반복합니다.
zig, zig-zig, zig-zeg 과정은 아래 사진과 같이 진행됩니다.
1. zig step
2. zig-zig step
x와 p가 둘 다 왼쪽 자식이거나 둘 다 오른쪽 자식일 경우
rotate(p) 후에 rotate(x)를 해줍니다.
3. zig-zag step
함수로 구현하면 다음과 같습니다.
void splay(node* x){
while(x->p){ // x가 루트가 될때까지 반복
node* p = x -> p;
node* g = p -> p;
if (g){
if ((x == p -> l) == (p == g -> l)) rotate(p); // zig-zig
else rotate(x); // zig-zag;
}
rotate(x);
}
}
이제 insert, find, delete 함수를 차례차례 보겠습니다.
Insert
삽입 방식은 일반적인 BST와 같고 마지막에 splay를 해서 루트로 올려줍니다.
void insert(int key){
node* p =tree;
node** pp; // 넣을 위치
if(!p){ // 빈 트리인 경우
node* x = new node;
tree = x;
x -> l = x -> r = x -> p = NULL;
x -> key = key;
return;
}
while(true){ // 삽입할 위치를 찾을때까지 반복
if (key == p -> key) return; // 중복값이 있을 경우 종료
if (key < p -> key){ // 현재 노드의 값보다 작을경우
if (!p -> l){ // 왼쪽이 비어있으면 왼쪽에 삽입
pp = &p->l;
break;
}
p = p->l; // 비어있지 않으면 왼쪽 자식으로 이동
}else{ // 현재 노드의 값보다 크다
if (!p->r){
pp = &p->r;
break;
}
p = p->r;
}
}
// 위에서 삽입할 위치를 찾음
node* x = new node;
*pp = x;
x -> l = x -> r = NULL;
x-> p = p;
x-> key = key;
// splay
splay(x);
}
find
find도 또한 비슷하게 진행되며
마지막에 splay를 해주어 다른 연산을 쉽게 해주도록 합니다.
bool find(int key){
node* p = tree;
if(!p) return false; // 비어있는 트리
while(p){
if (key == p -> key) break; // 찾았다
if (key < p -> key){ // 찾으려는게 현재 값보다 작음
if (!p -> l) break; // 왼쪽이 없다 -> 탐색 실패
p = p -> l; // 왼쪽으로 이동
}else{ // 찾으려는게 현재 값보다 큼
if (!p -> r) break; // 오른쪽이 없다 -> 탐색 실패
p = p -> r; // 오른쪽으로 이동
}
}
splay(p); // 마지막에 탐색한 노드를 루트로 올림
return key == p->key; // 탐색한 노드의 값이 찾는값이랑 같은지 return
}
Delete
삭제연산입니다.
void del(int key){
if (!find(key)) return; // 삭제할 값이 없으면 루트로 이동 후 종료
node* p = tree;
if (p -> l && p -> r){ // 자식이 두개
tree = p->l; // 왼쪽 자식이 새로운 루트
tree->p = NULL;
// 오른쪽 서브트리를 왼쪽 서브트리 아래에 삽입
node* x = tree;
while(x -> r) x = x -> r;
x -> r = p -> r;
p -> r -> p = x;
delete p;
return;
}
if(p->l){ //자식이 왼쪽만 있는 경우
tree = p->l;
tree->p = NULL; //왼쪽 자식이 새로운 루트
delete p; //노드 삭제
return;
}
if(p->r){ //자식이 오른쪽만 있는 경우
tree = p->r;
tree->p = NULL; //오른쪽 자식이 새로운 루트
delete p; //노드 삭제
return;
}
// 노드가 자신 하나인경우
delete p;
tree = NULL;
}
void update(node* x){
x -> cnt = 1;
if (x -> l) x->cnt += x->l->cnt;
if (x -> r) x->cnt += x->r->cnt;
}
rotate 할 때마다 cnt값이 바뀐다고 했으니 rotate의 마지막 부분에 update함수를 추가해주면 됩니다.
void rotate(node* x){ // 위로 올릴 노드
node* p = x -> p; // x의 부모 노드
node* b = NULL; // b노드
if(!p) return; // x가 루트라면 리턴
if (x == p->l){ // x가 p의 왼쪽 자식
p -> l = b = x -> r; // p의 왼쪽을 b노드로
x -> r = p; // x의 오른쪽 자식을 p노드로
}else{ // x가 p의 오른쪽 자식
p -> r = b = x -> l; // p의 오른쪽을 b노드로
x -> l = p; // x의 왼쪽 자식을 p노드로
}
// 부모노드 재설정
x -> p = p -> p; // x의 부모는 p의 부모
p -> p = x; // p의 부모는 x
if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p
// x가 루트노드가 아닐경우
// p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
(x -> p ? p == x -> p -> l ? x -> p -> l : x -> p -> r : tree) = x;
update(p), update(x);
}
roatate가 끝난 시점에서, p노드는 x노드의 자식노드이기 때문에
p노드에 대해 먼저 update후에 x노드를 update 해줘야 합니다.
이제 모든 노드의 서브트리의 크기를 cnt에 저장해 뒀으니 k-th element를 구할 수 있습니다.
void kth(int k){
node* x = tree;
while(1){
while(x->l && x->l->cnt > k) x = x->l;
if (x -> l) x -= x->l->cnt;
if(!k--)break;
x = x->r;
}
splay(x);
}
여기까지가 기본연산들에 대해 알아봤습니다.
이제 문제에 어떻게 적용할 수 있는지 알아보겠습니다.
대부분 배열에서의 쿼리 문제를 다룰 것입니다.
BBST이기 때문에 중위순회를 하면 항상 같은 순서로 탐색한다는 점을 이용합니다.
따라서 배열의 인덱스를 노드의 key로 생각해주면 배열의 원소를 스플레이트리로 관리할 수 있습니다.
간단한 구간합과 RMQ, 그리고 구간을 뒤집는 flip연산을 살펴보겠습니다.
splay연산을 통해 어떤 구간 [s, e]를 하나의 노드로 모으는 것에 집중해봅니다.
구간 [s, e]를 하나의 노드에 모으는 방법을 알아봅시다
s - 1번째 노드를 splay 해주면
s-1번째 노드가 루트가 되고 왼쪽 자식은 s - 2까지, 오른쪽 자식은 s부터의 구간에 대한 정보를 담고 있을것입니다.
그런 다음에 e + 1번 노드를 s - 1번 노드 오른쪽에 붙여주면
이렇게 될 것이고
루트의 오른쪽 자식의 왼쪽 자식에 [s, e] 구간에 대한 정보가 있을것입니다.
이것을 위해 splay 함수를 수정해서 루트로 올리는 함수에서 특정 노드를 다른 노드의 자식으로 가도록 수정합니다.
void splay(node* x, node* g = nullptr){
node* y;
while(x -> p != g){
node* p = x -> p;
if (p -> p == g){ // zig step
rotate(x);
break;
}
auto pp = p -> p;
if ((p -> l == x) == (pp -> l == p)){ // zig-zig step
rotate(p);
rotate(x);
}else{ // zig-zag step
rotate(x);
rotate(x);
}
}
if(!g) tree = x;
}
구간 [s, e] 를 모으는 gather 함수는 다음과 같습니다.
node* gather(int s, int e){ // [s, e]구간을 관리하는 노드를 리턴
kth(e + 1);
auto tmp = tree;
kth(s - 1);
splay(tmp, tree);
return tree->r->l;
}
이제 구간을 뒤집는 flip 함수를 보겠습니다.
구간을 뒤집는 연산은 왼쪽 서브트리와 오른쪽 서브트리를 swap하는 것을 재귀적으로 해주면 됩니다.
재귀적으로 처리하면 느리기 때문에 flip됐는지 여부를 lazy하게 처리해줍니다.
구조체에 변수를 추가합니다.
struct node{
// 다른 변수들
bool flip;
}
그리고 flip됐는지 여부를 전파할 함수를 만듭니다
void push(node *x){
if (!x -> flip) return;
swap(x->l, x->r);
if (x -> l) x->l->flip = !x->l->flip;
if (x->r) x->r->flip = !x->r->flip;
x->flip = false;
}
flip함수는 다음과 같이 간단하게 구현이 가능합니다.
void flip(int s, int e){
node* x = gather(s, e);
x->flip = !x->flip;
}
실제 문제에 적용하면 또 코드가 구현하기 편하도록 수정이 됩니다.
이것은 문제 풀이 카테고리에 문제를 직접 풀며 올려보겠습니다.
전체 소스
struct node{
node* l;
node* r;
node* p;
int key, cnt;
bool flip;
}*tree;
void update(node* x);
void rotate(node* x){ // 위로 올릴 노드
node* p = x -> p; // x의 부모 노드
node* b = NULL; // b노드
if(!p) return; // x가 루트라면 리턴
if (x == p->l){ // x가 p의 왼쪽 자식
p -> l = b = x -> r; // p의 왼쪽을 b노드로
x -> r = p; // x의 오른쪽 자식을 p노드로
}else{ // x가 p의 오른쪽 자식
p -> r = b = x -> l; // p의 오른쪽을 b노드로
x -> l = p; // x의 왼쪽 자식을 p노드로
}
// 부모노드 재설정
x -> p = p -> p; // x의 부모는 p의 부모
p -> p = x; // p의 부모는 x
if (b) b -> p = p; // b노드가 존재한다면 b의 부모는 p
// x가 루트노드가 아닐경우
// p노드가 p노드의 부모노드의 왼쪽이었다면 x를 왼쪽으로, 오른쪽이었다면 오른쪽으로
(x -> p ? p == x -> p -> l ? x -> p -> l : x -> p -> r : tree) = x;
update(p), update(x);
}
void splay(node* x, node* g = nullptr){
node* y;
while(x -> p != g){
node* p = x -> p;
if (p -> p == g){
rotate(x);
break;
}
auto pp = p -> p;
if ((p -> l == x) == (pp -> l == p)){
rotate(p);
rotate(x);
}else{
rotate(x);
rotate(x);
}
}
if(!g) tree = x;
}
void insert(int key){
node* p =tree;
node** pp; // 넣을 위치
if(!p){ // 빈 트리인 경우
node* x = new node;
tree = x;
x -> l = x -> r = x -> p = NULL;
x -> key = key;
return;
}
while(true){ // 삽입할 위치를 찾을때까지 반복
if (key == p -> key) return; // 중복값이 있을 경우 종료
if (key < p -> key){ // 현재 노드의 값보다 작을경우
if (!p -> l){ // 왼쪽이 비어있으면 왼쪽에 삽입
pp = &p->l;
break;
}
p = p->l; // 비어있지 않으면 왼쪽 자식으로 이동
}else{ // 현재 노드의 값보다 크다
if (!p->r){
pp = &p->r;
break;
}
p = p->r;
}
}
// 위에서 삽입할 위치를 찾음
node* x = new node;
*pp = x;
x -> l = x -> r = NULL;
x-> p = p;
x-> key = key;
// splay
splay(x);
}
bool find(int key){
node* p = tree;
if(!p) return false; // 비어있는 트리
while(p){
if (key == p -> key) break; // 찾았다
if (key < p -> key){
if (!p -> l) break;
p = p -> l;
}else{
if (!p -> r) break;
p = p -> r;
}
}
splay(p);
return key == p->key;
}
void del(int key){
if (!find(key)) return; // 삭제할 값이 없으면 루트로 이동 후 종료
node* p = tree;
if (p -> l && p -> r){ // 자식이 두개
tree = p->l; // 왼쪽 자식이 새로운 루트
tree->p = NULL;
// 오른쪽 서브트리를 왼쪽 서브트리 아래에 삽입
node* x = tree;
while(x -> r) x = x -> r;
x -> r = p -> r;
p -> r -> p = x;
delete p;
return;
}
if(p->l){ //자식이 왼쪽만 있는 경우
tree = p->l;
tree->p = NULL; //왼쪽 자식이 새로운 루트
delete p; //노드 삭제
return;
}
if(p->r){ //자식이 오른쪽만 있는 경우
tree = p->r;
tree->p = NULL; //오른쪽 자식이 새로운 루트
delete p; //노드 삭제
return;
}
// 노드가 자신 하나인경우
delete p;
tree = NULL;
}
void update(node* x){
x -> cnt = 1;
if (x -> l) x->cnt += x->l->cnt;
if (x -> r) x->cnt += x->r->cnt;
}
void kth(int k){
node* x = tree;
while(1){
while(x->l && x->l->cnt > k) x = x->l;
if (x -> l) x -= x->l->cnt;
if(!k--)break;
x = x->r;
}
splay(x);
}
node* gather(int s, int e){ // [s, e]구간을 관리하는 노드를 리턴
kth(e + 1);
auto tmp = tree;
kth(s - 1);
splay(tmp, tree);
return tree->r->l;
}
void push(node *x){
if (!x -> flip) return;
swap(x->l, x->r);
if (x -> l) x->l->flip = !x->l->flip;
if (x->r) x->r->flip = !x->r->flip;
x->flip = false;
}
void flip(int s, int e){
node* x = gather(s, e);
x->flip = !x->flip;
}
문제 조건에 따라 낮동안에 정상체온이 되는 사람들의 그룹은 반드시 직사각형 형태가 됩니다.
밤에는 K명이 저체온증이 되는데 만들어지는 직사각형들 중 가로 또는 세로의 길이가 K보다 작거나 같다면 그 변에 있는 모든 사람이 저체온증에 걸리게 하면 낮동안에 정상체온이 될 방법이 없으므로 그 직사각형 그룹은 모두 저체온증이 됩니다.
따라서 bfs 한 번으로 정상체온 그룹을 만들어주고,
그룹을 만날때마다 가로, 세로 사이즈를 구해서 모두 K보다 크다면 직사각형의 크기만큼을 정답에 추가해주면 됩니다.
총 시간복잡도는 O(NM)입니다.
코드(python)
import sys
from collections import deque
input = sys.stdin.readline
def in_range(a, b):
return 0 <= a < n and 0 <= b < m
n, m, k = map(int, input().split())
board = [list(input().strip()) for _ in range(n)]
q = deque()
visited = [[False] * m for _ in range(n)]
dxy = [(1, 0), (0, 1), (-1, 0), (0, -1)]
for i in range(n):
for j in range(m):
if board[i][j] == 'O':
q.append((i, j))
visited[i][j] = True
while q:
x, y = q.popleft()
board[x][y] = 'O'
for dx, dy in dxy:
nx, ny = x + dx, y + dy
if not in_range(nx, ny):
continue
if visited[nx][ny]:
continue
if board[nx][ny] == '.':
cnt = 0
for ddx, ddy in dxy:
nnx, nny = nx + ddx, ny + ddy
if not in_range(nnx, nny):
continue
if board[nnx][nny] == 'O':
cnt += 1
if cnt >= 2:
q.append((nx, ny))
visited[nx][ny] = True
visited = [[False] * m for _ in range(n)]
ans = 0
for i in range(n):
for j in range(m):
if board[i][j] == 'O' and not visited[i][j]:
garo = 0
saero = 0
x = i
y = j
while x < n and board[x][j] == 'O':
x += 1
saero += 1
while y < m and board[i][y] == 'O':
y += 1
garo += 1
q = deque()
q.append((i, j))
visited[i][j] = True
while q:
x, y = q.popleft()
for dx, dy in dxy:
nx, ny = x + dx, y + dy
if not in_range(nx, ny):
continue
if visited[nx][ny]:
continue
if board[nx][ny] != 'O':
continue
q.append((nx, ny))
visited[nx][ny] = True
if garo > k and saero > k:
ans += garo * saero
print(ans)
가로세로 체크하며 이미 방문한 그룹을 다시 방문하지 않기 위해 BFS를 한 번 더 했는데 굳이 이렇게 하지 않고 더 효율적으로 하는 방법이 있습니다. 저는 비효율적으로 구현한것 같습니다.