우선순위 큐

|

우선순위 큐

우선순위 큐

  • 우선순위 큐는 평범한 스택과 비슷한 축약 자료형이다. 그러나 각 원소들은 우선순위를 갖고 있다.
  • 우선순위 큐에서 높은 우선순위를 가진 원소는 낮은 우선순위를 가진 원소보다 먼저 처리된다
  • 우선순위 큐는 “리스트”나 “맵”과 같이 추상적인 개념이다. 마치 리스트연결 리스트배열로 구현될 수 있는 것과 같이, 우선순위 큐는 힙이나 다양한 다른 방법을 이용해 구현될 수 있다.

  • 배열, 링크드 리스트로도 구현할 수 있으나

    • 배열의 경우 삽입,삭제의 비효율성
    • 링크드 리스트의 경우 검색의 비효율성이 존재하고

    우선순위라는 최대값을 찾기에 적합한 힙(최대 힙)으로 구현한다.

  • 힙 내부는 배열로 구현한다.

우선 순위 큐는 다음 연산을 제공하여야 한다

  1. Maximum(S)
  2. ExtractMax(S)
  3. IncreaseKey(S,i,key)
  4. Insert(S,key)

Maximum(S)

  • 힙의 최대값을 리턴한다.
  • 시간복잡도는 O(1)
//pseudocode
Maximum(S)
  return S[0];
  

ExtractMax(S)

  • 힙에서 최대값을 리턴하고 힙에서 제거한다.

  • 시간복잡도는 O(1) + O(logn) (MaxHeapify) 이므로 O(logn)

//pseudocode
ExtractMax(S)
  if(S.heap_size < 1)
    error "heap underflow";
  max = S[0];
  S[0] = S[S.heap_size-1];
	S.heap_size = S.heap_size -1;
  MaxHeapify(S,0);
  return max;

IncreaseKey(S,i,k)

  • 힙 S의 인덱스 i의 키 값을 k로 증가시킨다.

  • 시간복잡도는 O(1) + O(logn) 이므로 O(logn)

//pseudocode
IncreaseKey(S,i,key)
  if(key < S[i])
    error "새로운 키가 현재 키보다 작다";
  S[i] = key;
  while(i > 1 and S[Parent(i)] < S[i])
    S[i] <-> S[Parent(i)];
    i = Parent(i);

Insert(S,key)

  • 힙에 새로운 키 값을 추가한다.

  • 시간복잡도는 IncreaseKey()를 이용하므로 O(logn)

//pseudocode
Insert(S,key)
  S.heap_size = S.heap_size + 1;
  IncreaseKey(S,S.heap_size,key);

자바로 우선순위 큐 구현

  • 자바에 존재하는 PriorityQueue를 상속받지 않고 Heap 클래스를 만들어 구현해보았다.

  • 테스트 코드

class PriorityQueueTest {

    private PriorityQueue priorityQueue;
    private int[] input;

    @BeforeEach
    public void setUp(){
        input = new int[]{15,13,9,5,12,8,7,4,0,6,2,1};
        priorityQueue = new PriorityQueue(input);
    }

    @Test
    public void testGetMaximum() throws Exception {
        assertEquals(priorityQueue.getMaximum(), 15);
    }

    @Test
    public void testExtractMax() throws Exception {
        int max = priorityQueue.extractMax();
        assertEquals(max, 15);
        assertEquals(priorityQueue.getMaximum(),13);
        assertEquals(priorityQueue.size(),11);
    }

    @Test
    public void testAdd() throws Exception {
        priorityQueue.insert(30);
        assertEquals(priorityQueue.getMaximum(), 30);
        priorityQueue.insert(45);
        assertEquals(priorityQueue.getMaximum(),45);
    }

}
  • 알고리즘

import java.util.Arrays;

/**
 * have a higher priority if value is bigger
 */
public class PriorityQueue {

    private Heap heap;

    public PriorityQueue(int[] src){
        heap = new Heap(src);
    }

    public int size() {
        return heap.size();
    }

    public int extractMax() throws Exception {
        if(heap.size() < 1)
            throw new Exception("heap underflow");

        int max = heap.get(0);
        heap.set(0,heap.get(getLastIndex()));
        heap.decrementSize();
        MaxHeapify(0);
        return max;
    }

    public int getMaximum() throws Exception {
        return heap.get(0);
    }

    private int getLastIndex(){
        return heap.size()-1;
    }

    public void increaseKey(int idx, int key) throws Exception {
        if(idx >= heap.size())
            throw new IndexOutOfBoundsException();

        if(key < heap.get(idx))
            throw new Exception("new key is smaller than exist value");

        heap.set(idx,key);
        while(idx > 0 && heap.get(heap.getParentIndex(idx)) < heap.get(idx)){
            swap(idx, heap.getParentIndex(idx));
            idx = heap.getParentIndex(idx);
        }
    }

    public void insert(int key) throws Exception {
        heap.checkCapacity();
        heap.incrementSize();
        increaseKey(heap.size()-1, key);
    }

    private void MaxHeapify(int idx) {

        if(idx < 0 || idx >= heap.size)
            throw new IndexOutOfBoundsException();

        int leftIndex = heap.getLeftIndex(idx);
        int rightIndex = heap.getRightIndex(idx);
        int size = heap.size();
        int largest = -1;
        int tmp = Integer.MIN_VALUE;

        // compare with leftNode
        if(leftIndex < size && heap.get(leftIndex) > heap.get(idx))
            largest = leftIndex;
        else
            largest = idx;
        // compare with rightNode
        if(rightIndex < size && heap.get(rightIndex) > heap.get(largest))
            largest = rightIndex;
        // swap if parentNode is bigger than child.
        if(largest != idx){
            swap(idx, largest);
            //reculsive call
            MaxHeapify(largest);
        }
    }

    private void swap(int from, int to) {
        int tmp;
        tmp = heap.get(from);
        heap.set(from,heap.get(to));
        heap.set(to,tmp);
    }

  
		// MaxHeap 클래스
    public class Heap {
        int[] array = {};
        int size;

        public Heap(int[] src){
            array = src;
            size = array.length;
        }

        public Heap(){
            array = new int[10];
            size = array.length;
        }

        public int getLeftIndex(int idx){
            return 2*idx+1;
        }

        public int getRightIndex(int idx){
            return 2*idx+2;
        }

        public int getParentIndex(int idx){return idx/2;}

        // heap's size
        public int size(){
            return size;
        }

        // array's size
        public int length(){
            return array.length;
        }

        public void incrementSize(){ size++; }

        public void decrementSize(){
            size--;
        }

        public int get(int idx) {
            return array[idx];
        }

        // if heap's size is bigger than array's length, grow array's size;
        private void checkCapacity() {
            int oldCapacity = length();
            if(size >= oldCapacity){
                int newCapacity = oldCapacity + 10;
                array = Arrays.copyOf(array, newCapacity);
            }
        }

        public int[] getHeap(){
            return array;
        }

        public boolean isValid(int idx){
            return size-1 >= idx ? true : false;
        }

        public void set(int idx, int value) {
            if(isValid(idx)){
                array[idx] = value;
                return;
            }
            throw new IndexOutOfBoundsException();
        }
    }
}

ref. wiki

ref. Introduction to Algorithms

20191212_TIL

|

12/12 목요일

  1. 알고리즘

    • 우선순위 큐(Maximum, ExtractMax, Insert, IncreaseKey)를 Heap 클래스와 함께 구현하였다. - 리팩토링을 하긴 했지만 가독성이 떨어지는 것 같다. 시간이 지나고 다시 리팩토링을 해봐야 할 것 같다.
    • 내일을 위해 퀵 정렬을 먼저 읽어보았다.
  2. 네트워크

    • 1장 Story03~04를 복습하고 정리하였다
    • 학습(1시간), 정리(0.5시간)
  3. 내일 할 일

    • 퀵정렬
    • 네트워크 2장 01~02 읽기
  4. 소감

    • 알고리즘을 매일 학습하고 구현하다보니 시간복잡도와 분할정복, 재귀에 대한 개념이 점점 익숙해지는 것같다.
    • 다음 자료구조를 구현할 때는 TDD 방식으로 해봐야겠다

힙정렬

|

힙정렬

힙정렬이란?

  • 힙이라는 자료구조를 이용한다.

힙이란 ?

  • (heap)은 최댓값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 고안된 완전이진트리[1]를 기본으로 한 자료구조(tree-based structure)로서 다음과 같은 힙 속성(property)을 만족한다.

    • A가 B의 부모노드(parent node) 이면, A의 키(key)값과 B의 키값 사이에는 대소관계가 성립한다.
    • Key[A] > Key[B] : 최대 힙
    • Key[A] < Key[B] : 최소 힙

힙은 다음과 같이 1차원 배열로도 표현이 가능하다.

어떤 노드의 인덱스를 index,

왼쪽 자식노드의 인덱스를 left_index,

오른쪽 자식노드의 인덱스를 right_index로 선언하면 다음과 같은 관계를 지닌다.

// 인덱스가 0부터 시작하는 경우
Parent(i) = i/2;

left_index(i) = 2i+1;

right_index(i) = 2i+2;

힙 정렬 알고리즘은 최대힙을 사용하며 3가지 연산이 필요하다.

  1. Max-Heapify
  2. Build Max-Heap
  3. HeapSort

Max-Heapify

  • 최대 힙 특성을 유지하기 위한 연산
  • Max-Heapify를 호출할때 left_index(i), right_index(i)를 루트로 하는 이진트리는 최대 힙이라 가정한다.
  • 시간복잡도는 높이에 비례하므로 O(logn)이다 시간복잡도 증명

// pseudocode
MaxHeapify(A,i)
	l = left_index(i);
  r = right_index(i);
  if(l <= A.heap_size && A[l] > A[i])
    largest = l;
  else largest = i;
  if(r <= A.heap_size && A[r] > A[largest])
    largest = r;
  if(largest != i)
    exchange A[i] with A[largest]
    MaxHeapify(A,largest);


Build Max-Heap

  • 임의의 배열을 최대 힙으로 구성하는 연산
// pseudocode
BuildMaxHeap(A)
  A.heap.size = A.length
  for i=A.length/2 downto 1
    MaxHeapify(A,i)
  • i=length/2부터 시작하는 이유는 length/2+1부터는 리프 노드이므로 MaxHeapify가 필요 없기 때문이다

  • 시간복잡도는 O(logn) * n/2 이므로 O(nlogn)이나, 이것은 루트 노드를 기준으로 러프하게 계산한 것이므로

    좀 더 정확하게 계산하면 O(n)이다.

    참고자료1,참고자료2

HeapSort

  • 힙 정렬은 다음과 같은 방법으로 수행된다

    1. 주어진 데이터를 힙으로 만든다.
    2. 힙에서 최대값(루트 노드)을 가장 마지막 값과 바꾼다.
    3. 힙의 크기가 1 줄어든 것으로 간주한다. 즉, 마지막 값은 힙의 일부가 아닌것으로 간주한다.
    4. 루트 노드에 대해서 HEAPIFY(1)한다.
    5. 2~4번을 반복한다.
  • 시간복잡도는 O(n) + O(nlogn) = O(nlogn)

HeapSort(A)
  BuildMaxHeap(A) // O(n)
  for i= A.length downto 2 // O(nlogn)
    exchange A[1] with A[i]
    A.heap.size = A.heap.size - 1
    MaxHeapify(A,1)

Java로 힙정렬 구현

  • 테스트 코드
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;

class HeapSortTest {

    @Test
    public void test() {
        int[] input = new int[]{5, 13, 2, 25, 7, 17, 20, 8, 4};
        HeapSort.Heap heap = new HeapSort.Heap(input);
        int[] answer = Arrays.copyOf(input, input.length);
        Arrays.sort(answer);

        HeapSort.heapSort(heap);
        assertArrayEquals(heap.getHeap(), answer);
    }
}
  • 알고리즘

public class HeapSort {

    // 최대힙의 0번째 값(최대값)을 가장 뒤로 보내 정렬한다.
    public static void heapSort(Heap heap){
        buildMaxHeap(heap);
        int tmp = Integer.MIN_VALUE;
        for(int i = heap.length()-1; i>=0; i--){
            swap(heap,i,0);
            heap.decrementSize();
            maxHeapify(heap,0);
        }
    }

    private static void buildMaxHeap(Heap heap){
        for(int i = heap.length()/2-1; i>=0; i--)
            maxHeapify(heap,i);
    }

    private static void maxHeapify(Heap heap, int idx){
        int leftIndex = getLeftIndex(idx);
        int rightIndex = getRightIndex(idx);
        int size = heap.size();
        int largest = -1;

        // 왼쪽 노드와 비교
        if(leftIndex < size && heap.get(leftIndex) > heap.get(idx))
            largest = leftIndex;
        else
            largest = idx;
        // 오른쪽 노드와 비교
        if(rightIndex < size && heap.get(rightIndex) > heap.get(largest))
            largest = rightIndex;
        // 부모 노드보다 자식노드가 큰 경우 교환
        if(largest != idx){
            swap(heap, idx, largest);
            //재귀호출
            maxHeapify(heap,largest);
        }
    }

    private static void swap(Heap heap, int to, int from) {
        int tmp = heap.get(to);
        heap.set(to, heap.get(from));
        heap.set(from, tmp);
    }

    private static int getLeftIndex(int idx){
        return 2*idx+1;
    }

    private static int getRightIndex(int idx){
        return 2*idx+2;
    }

    public static class Heap {
        int[] heap;
        int size;

        public Heap(int[] src){
            heap = src;
            size = heap.length;
        }

        public int size(){
            return size;
        }

        public int length(){
            return heap.length;
        }

        public void decrementSize(){
            size--;
        }

        public int get(int idx){
            return heap[idx];
        }

        public int[] getHeap(){
            return heap;
        }

        public void set(int idx, int value){
            heap[idx] = value;
        }
    }

}

[1]

  • 완전이진트리(complete binary tree)
    • 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 모든 노드는 가능한 한 가장 왼쪽에 있다. 마지막 레벨 h 에서 1부터 2h-1 개의 노드를 가질 수 있다.
  • 포화이진트리(perfect binary tree)
    • 모든 내부 노드가 두 개의 자식 노드를 가지며 모든 잎 노드가 동일한 깊이 또는 레벨을 갖는다

img

ref. 이진트리 이미지

ref. Introduction to Algorithm

20191211_TIL

|

12/11 수요일

  1. 알고리즘

    • 힙 자료구조와 힙정렬을 하기 위한 연산들 (maxHeapify, buildMaxHeap, HeapSort)를

      학습하였고 포스팅하였다.

    • 학습(2.5시간), 포스팅(2시간), 자바로 구현(0.5시간)

    • 포스팅 하는 시간을 좀 더 단축시켜 봐야겠다.

  2. 네트워크

    • 1장 Story01~02를 복습하고 정리하였다
    • 학습(1시간), 정리(0.5시간)
  3. 내일 할 일

    • 퀵정렬
    • 네트워크 1장 Story03~04

Spring 3요소

|

Spring의 3요소

1. IoC Container

Inversion of Control

  • 의존 관계 주입이라고도 하며, 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는게 아니라 주입받아 사용하는 방법을 말한다.

스프링 IoC 컨테이너

  • BeanFactory
  • 애플리케이션 컴포넌트의 중앙 저장소
  • 빈 설정 소스로부터 빈 정의를 읽어들이고, 빈을 구성하고 제공한다.

container magic

Bean

  • 스프링 IoC 컨테이너가 관리하는 객체
  • 스프링이 의존성 관리를 해준다
  • Scope
    • Singleton(default)
    • Prototype : 매번 다른 객체를 생성한다
  • 라이프사이클 인터페이스를 가진다

ApplicationContext

Configuration Metadata

  • Xml 기반 Config
  • Java 기반 Config

  • 최근에는 Java Config를 많이 사용하는 추세

2. AOP

**AOP 구현방법 **

  • 컴파일
    • A.java —(AOP) — > A.class (AspectJ)
  • 바이트코드 조작
    • A.java -> A.class —(AOP) —> 메모리(AspectJ)
  • 프록시 패턴
    • 스프링 AOP
      • @Transactional 애노테이션을 사용하면 해당 인터페이스 또는 클래스의 메서드 앞뒤에 부가적인 코드를 추가한 프록시 객체를 생성하여 빈으로 대신 등록하고 스프링이 호출한다

AOP 적용예제

@LogExecutionTime으로 메서드 처리 시간 로깅하기

@LogExecutionTime 애노테이션 (어디에 적용할지 표시 해두는 용도)

@Target(ElementType.METHOD) // 애노테이션 적용 대상
@Retention(RetiontionPolicy.RUNTIME) // 애노테이션을 유지 기간
public @interface LogExecutionTime{
}

실제 Aspect (@LogExecutionTime 애노테이션 달린 곳에 적용)

@Component
@Aspect
public class LogAspect{
  
   Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable{
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object proceed = joinPoint.proceed();

        stopWatch.stop();
        logger.info(stopWatch.prettyPrint());

        return proceed;
    }
  
}


3. PSA

  • @GetMapping, @PostMapping과 같은 애노테이션을 사용하면 HttpServlet 클래스를 상속받은 Servlet을 구현한 클래스를 작성할 필요가 없다. 네티 기반 webflex도 코드의 변경없이 사용가능하다.
  • 이처럼 추상화 계층을 이용하여 개발하면 구현체가 무엇이든 신경쓸 필요가 없다. 스프링이 알아서 처리하여 주기 때문이다
  • @Transactional 애노테이션도 마찬가지이다. JPA, Hibernate, JDBC를 사용하든 스프링이 알아서 처리해준다.

ref. spring.io

ref. 스프링 프레임워크 핵심 기술