본문 바로가기

카테고리 없음

Do it! 알고리즘 입문 자바 - Week3

이번에는 스택과 큐에 대해 학습할 예정이다.
2학년 때 이미 배웠던 주제라 큰 어려움은 없겠지만,
저번에 소개했던 제네릭 기법으로 코드를 작성해보는 것이 주된 목표이다.

 

스택: 데이터를 일시적으로 저장하기 위한 자료구조 (LIFO - Last In First Out)
         후입선출의 특징을 가진다.

  • push 스택에 넣는 작업 / pop 스택에서 꺼내는 작업
  • top 푸시와 팝을 하는 곳 (꼭대기) / bottom 가장 아랫부분

보통 프로그램이 실행될 때 메서드의 호출과 실행은 스택을 이용한다.
시스템 프로그래밍 교과목에서도 프로시져에 관련한 것은 스택을 이용했던 부분이 있다.

void x(){...}

void y(){...}

void z(){
	x();	
    y();
}

int main(){
	z();
}

 

이런 메서드들이 있을 때

 

 

메소드들이 스택에서 push되거나 pop되는 과정이 일어난다. 실제로는 스택 프레임 이런 게 더 있지만,
이번에는 따로 다루지 않는다.

 

  • 구현
    • 생성자: 스택 본체의 개별 요소는 바닥부터 stk[0]. stk[1],...,stk[max-1]이 된다.
      생성 시 스택은 비어있으므로 ptr은 0으로 한다.

    • push: 스택에 푸시하는 메서드 / 가득 찬 경우 OverflowIntStackException을 던진다.
      부등호를 이용하여 스택 본체 배열의 상한과 하한을 벗어나서 접근하는 것을 막는다.
    • 저장하고 스택 포인터를 1 증가시킨다. 후위연산자 사용!

    • pop: 스택의 top에서 제거하고 값을 반환하는 메서드 / 스택이 비어있는 경우 EmptyIntStackException을 던진다.
    • 스택 포인터를 1 감소시키고 값을 반환한다. 전위연산자 사용!

    • top / peek: 스택의 꼭대기에 있는 원소를 반환하는 메서드 / 비어있는 경우 예외 처리
    • ptr-1 인덱스의 요소를 반환한다.

    • indexOf: 스택 본체 배열에 x와 같은 값의 데이터가 포함되어 있는지, 있다면 어디에 있는지
      조사하는 메서드입니다.
    • 꼭대기에서 바닥으로 / 인덱스가 큰 곳에서 작은 곳으로 검색을 수행한다. => 먼저 제거되는 데이터를 찾기 위해서

    • 나머지: clear / capacity / size / IsEmpty / IsFull
import java.util.Scanner;

public class IntStack {
    private int max; //스택 용량
    private int ptr; //스택 포인터
    private int[] stk; //스택 본체

    public class EmptyStackException extends RuntimeException{
        public EmptyStackException(){}
    }

    public class OverflowException extends RuntimeException{
        public OverflowException(){}
    }

    public IntStack(int capacity){
        ptr = 0;
        max = capacity;
        try{
            stk = new int[max];
        }catch(OutOfMemoryError e){
            max = 0;
        }
    }

    public int push(int x) throws OverflowException{
        if(ptr >= max){
            throw new  OverflowException();
        }
        return stk[ptr++] = x; //푸시한 값을 반환
    }

    public int pop() throws EmptyStackException {
        if (ptr <= 0) {
            throw new EmptyStackException();
        }
        return stk[--ptr];
    }

    public int peek() throws EmptyStackException{
        if(ptr<=0){
            throw new EmptyStackException();
        }
        return stk[ptr-1];
    }

    public int indexOf(int x){
        for(int i = ptr - 1; i >= 0; i--){
            if(stk[i] == x){
                return i; //인덱스를 반환
            }
        }
        return -1;
    }

    public void clear(){
        ptr = 0;
    }

    public int capacity(){
        return max;
    }

    public int size(){
        return ptr;
    }

    public boolean isEmpty(){
        return ptr <= 0;
    }

    public boolean isFull(){
        return ptr >= max;
    }

    public void dump(){
        if(ptr<=0){
            System.out.println("스택이 비어있습니다.");
        }
        else{
            for(int i = 0; i<ptr;i++){
                System.out.println(stk[i]+" ");
            }
            System.out.println();
        }
    }
}

class IntStackTester{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        IntStack s = new IntStack(64);

        while(true){
            System.out.println("현재 데이터 수 : " + s.size() + "/" + s.capacity());
            System.out.print("1push  2pop  3peek  " +
                    "4dump  0end : ");

            int menu = sc. nextInt();
            if(menu==0){
                break;
            }

            int x;
            switch(menu){
                case 1:
                    System.out.print("데이터 : ");
                    x = sc.nextInt();
                    try{
                        s.push(x);
                    }catch(IntStack.OverflowException e){
                        System.out.println("스택이 가득 찼습니다.");
                    }
                    break;

                case 2:
                    try{
                        x = s.pop();
                        System.out.println("팝한 데이터는 " + x +"입니다.");
                    }catch(IntStack.EmptyStackException e){
                        System.out.println("스택이 비어있습니다.");
                    }
                    break;

                case 3:
                    try{
                        x = s.peek();
                        System.out.println("피크한 데이터는 "+x+"입니다.");
                    }catch(IntStack.EmptyStackException e){
                        System.out.println("스택이 비어있습니다.");
                    }
                    break;

                case 4:
                    s.dump();
                    break;
            }
        }
    }
}

 

IntStack을 이용하는 프로그램까지 작성해보니 전반적인 감이 잡힌다.

 

 

큐: 스택과 마찬가지로 데이터를 임시로 쌓아 놓은 자료구조, FIFO(First In First Out)
     선입선출을 따른다.

  • enqueue: 큐에 넣는 작업 / rear: 데이터를 넣는 쪽
  • dequeue: 큐에서 데이터를 꺼내는 작업 / front: 데이터를 꺼내는 쪽, 인덱스가 0이다!

 

  • 구현
    • enqueue: rear의 다음 요소에 데이터를 저장, O(1)시간
    • dequeue: 데이터를 꺼낸 후 두 번째 이후의 요소를 모두 맨 앞으로 옮긴다, O(n)시간
    • 데이터를 꺼낼 때마다 이런 처리를 하면 효율이 떨어진다.
  • 링 버퍼로 큐 만들기
    • 배열 요소를 앞쪽으로 옮기지 않는 큐 구현
    • 링 버퍼(ring buffer)라는 자료구조를 이용한다.
    • front와 rear를 요소를 식별하기 위한 변수로 사용한다.

front: 맨 처음 요소의 인덱스

rear: 맨 끝 요소의 하나 뒤의 인덱스

 

이런 식으로 구현하면 변수 값만을 업데이트하여 enqueue와 dequeue를 수행하기 때문에
요소 이동 문제를 해결하고 시간을 O(1)에 수행할 수 있다.

 

 

  • 커스텀 예외 처리
    • 스택을 구현할 때도 작성했던 코드이다.
    • 직접 IntQueue클래스에서만 사용하는 예외를 작성하여 분명하게 확인할 수 있게 한다.
      큐가 비거나 꽉 채워져있을 때 직접 만든 예외를 던지는 것이다.
  • num
    • front와 rear로 큐가 꽉 찼는지 알 수 없다.
    • 비어있는 경우 front = rear = 0 / front = rear인 경우도 있다.
    • 따라서 num과 max값을 비교하여 큐의 사용 가능 유무를 확인한다.
  • enqueue
    • enque후에 rear가 max를 넘어 배열에 없는 값을 가리키게 되는 문제가 발생한다. (배열의 끝)
    • 따라서, rear가 +1 되었을 때 max와 같아지는 것을 방지하도록 인큐를 구현해야 한다.
    • max와 같아질 경우 rear를 배열의 처음 인덱스인 0으로 변경한다.
  • dequeue
    • enqueue와 마찬가지로 인덱스 초과 문제가 발생한다. (배열의 끝)
    • front와 max가 같아질 경우 front를 배열의 처음 인덱스인 0으로 변경한다.
  • indexOf
    • 큐의 시작은 front이므로 front의 인덱스를 계산해야 한다.
    • (i + front) % max 로 계산한다.

 

import java.util.Scanner;

public class IntQueue {
    private int max;
    private int front;
    private int rear;
    private int num;
    private int[] que;

    public class EmptyIntQueueException extends RuntimeException { //커스텀 예외 처리 작성
        public EmptyIntQueueException() {}
    }

    public class OverflowIntQueueException extends RuntimeException {
        public OverflowIntQueueException() {}
    }

    public IntQueue(int capacity){
        num = front = rear = 0;
        max = capacity;
        try{
            que = new int[max];
        }catch(OutOfMemoryError e){
            max = 0;
        }
    }

    public int enque(int x) throws OverflowIntQueueException {
        if(num >= max){
            throw new OverflowIntQueueException();
        }
        que[rear++] = x;
        num++;
        if(rear == max){
            rear = 0;
        }
        return x;
    }

    public int deque() throws EmptyIntQueueException {
        if(num <= 0){
            throw new EmptyIntQueueException();
        }
        int x = que[front++];
        num--;
        if(front == max){
            front = 0;
        }
        return x;
    }

    public int peek() throws EmptyIntQueueException {
        if(num <= 0){
            throw new EmptyIntQueueException();
        }
        return que[front];
    }

    public int indexOf(int x){
        for(int i = 0; i< num; i++){
            int idx = (i + front) % max;
            if(que[idx] == x){
                return idx;
            }
        }
        return -1;
    }

    public void clear(){
        num = front = rear = 0;
    }

    public int capacity(){
        return max;
    }

    public int size(){
        return num;
    }

    public boolean isEmpty(){
        return num <= 0;
    }

    public boolean isFull(){
        return num >= max;
    }

    public void dump(){
        if(num <= 0){
            System.out.println("큐가 비어있습니다.");
        }
        else{
            for(int i = 0; i < num; i++){
                System.out.print(que[(i + front) % max] + " ");
            }
            System.out.println();
        }
    }
}

class IntQueueTestrer{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        IntQueue q = new IntQueue(64);

        while(true){
            System.out.println("현재 데이터 수 : " + q.size() + " / "
            + q.capacity());
            System.out.print("(1)인큐 (2)디큐 (3)피크 " + "(4)덤프 (0)종료 : ");

            int menu = sc.nextInt();
            if(menu == 0){
                break;
            }

            int x;

            switch(menu){
                case 1:
                    System.out.print("데이터 : ");
                    x = sc.nextInt();
                    try{
                        q.enque(x);
                    }catch(IntQueue.OverflowIntQueueException e){
                        System.out.println("큐가 가득 찼습니다.");
                    }
                    break;

                case 2:
                    try{
                        x = q.deque();
                        System.out.println("디큐한 데이터는 " + x + "입니다.");
                    }catch (IntQueue.EmptyIntQueueException e){
                        System.out.println("큐가 비어있습니다.");
                    }
                    break;

                case 3:
                    try{
                        x = q.peek();
                        System.out.println("피크한 데이터는 " + x +"입니다.");
                    }catch (IntQueue.EmptyIntQueueException e){
                        System.out.println("큐가 비어있습니다.");
                    }
                    break;

                case 4:
                    q.dump();
                    break;
            }
        }
    }
}

 

 

  • 링 버퍼 활용
    • "오래된 데이터를 버리는 용도"로 활용할 수 있다.
    • 배열 a[cnt++ % capacity] 이런 식으로 인덱스를 조절하여 n개의 최신 데이터를 유지할 수 있다.
import java.util.Scanner;

class LastNElements{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        final int  N = 10;
        int [] a = new int[N];
        int cnt = 0;
        int retry;

        System.out.println("정수를 입력하세요. ");

        do{
            System.out.printf("%d번째 정수 : ", cnt + 1);
            a[cnt++ % N] = sc.nextInt();

            System.out.printf("계속 할까요? (예.1 / 아니오.0) : ");
            retry = sc.nextInt();
        }while(retry == 1);

        int i = cnt - N;
        if(i < 0){
            i = 0;
        }
        for( ; i < cnt; i++){
            System.out.printf("%2d번째의 정수=%d\n", i+1, a[i % N]);
        }
    }
}

 

 


 

지금까지 스택과 큐에 관련된 내용들을 점검하고

구현상에 문제가 되는 부분들을 어떻게 처리하는지 학습했다.
연습문제로 변형된 스택이나 큐의 형태를 연습하고 제네릭 기법으로 작성하는 법을 공부할 계획이다.
다음 주 내용은 재귀 알고리즘이다!