스크린샷 2019-10-30 오후 11 32 53

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HashMaraton {


    public String solution(String[] participant, String[] completion) {

        Arrays.sort(participant);
        Arrays.sort(completion);


        int i = 0;
        for (i = 0; i < completion.length; i++) {
            if(!participant[i].equals(completion[i])){
               return participant[i];
            }
        }

        return participant[i];
    }


    public static void main(String[] args) {

        HashMaraton maraton = new HashMaraton();

        String[] participant = {"leo", "kiki", "kiki", "eden"};
        String[] completion = {"eden", "kiki"};

        System.out.println(maraton.solution(participant, completion));


    }
}

이 문제는 해시를 이용하여 푸는 문제입니다. 위의 코드는 해시를 사용하지 않고 정렬 후 단순히 participant와 completion 배열의 값을 비교하여
completion에 없는 참가자를 리턴하면 됩니다.

하지만 문제의 의도는 해시로 완주하지 못한 참가자의 이름을 리턴하라고 나왔기 때문에 아래코드와 같이 해시 맵을 이용해서 문제를 풀어보았습니다.

public String solution(String[] participant, String[] completion){        

        String answer = "";

        Map<String, Integer> map = new HashMap<>();

        for (String player : participant){

            if(map.containsKey(player)){
                map.put(player, map.get(player) + 1);
                continue;
            }
            map.put(player, 1);

        }

        for (String player : completion){
            if (map.containsKey(player)){
                map.put(player, map.get(player) - 1);
            }
        }


        Set<String> keySet = map.keySet();

        for (String key : keySet){

            if(map.get(key) != 0){
                answer = key;
            }
        }

        return answer;
 }

참조: 아래 코드는 8버전부터 JAVA에서 제공해주는 HashMap 메소드입니다. 중복체크를 할때 유용하게 사용 할 수 있습니다.
Map에서 원하는 key 값이 존재하지 않아도 기본적으로 value를 가지고 싶을때 사용하는 메소드입니다.

 for (String player : participant) map.put(player, map.getOrDefault(player, 0) + 1);

문제 설명

비밀지도

네오는 평소 프로도가 비상금을 숨겨놓는 장소를 알려줄 비밀지도를 손에 넣었다. 그런데 이 비밀지도는 숫자로 암호화되어 있어 위치를 확인하기 위해서는 암호를 해독해야 한다. 다행히 지도 암호를 해독할 방법을 적어놓은 메모도 함께 발견했다.
지도는 한 변의 길이가 n인 정사각형 배열 형태로, 각 칸은 공백(" ) 또는벽(#") 두 종류로 이루어져 있다.
전체 지도는 두 장의 지도를 겹쳐서 얻을 수 있다. 각각 지도 1과 지도 2라고 하자. 지도 1 또는 지도 2 중 어느 하나라도 벽인 부분은 전체 지도에서도 벽이다. 지도 1과 지도 2에서 모두 공백인 부분은 전체 지도에서도 공백이다.
지도 1과 지도 2는 각각 정수 배열로 암호화되어 있다.
암호화된 배열은 지도의 각 가로줄에서 벽 부분을 1, 공백 부분을 0으로 부호화했을 때 얻어지는 이진수에 해당하는 값의 배열이다.

secret8

네오가 프로도의 비상금을 손에 넣을 수 있도록, 비밀지도의 암호를 해독하는 작업을 도와줄 프로그램을 작성하라.

입력 형식
입력으로 지도의 한 변 크기 n 과 2개의 정수 배열 arr1, arr2가 들어온다.
1 ≦ n ≦ 16
arr1, arr2는 길이 n인 정수 배열로 주어진다.
정수 배열의 각 원소 x를 이진수로 변환했을 때의 길이는 n 이하이다. 즉, 0 ≦ x ≦ 2n - 1을 만족한다.
출력 형식
원래의 비밀지도를 해독하여 '#', 공백으로 구성된 문자열 배열로 출력하라.

스크린샷 2019-10-29 오후 11 25 54

2018년도 카카오 블라인드 공채 문제 중에서 가장 적중률이 높은 비밀지도 문제에 대해서 리뷰하겠습니다. 사실 그림과 설명만 보면 복잡해보이지만 해당 문제의 의도는 이진연산의 개념을 묻는 문제로 그 중에서 or 연산을 사용할줄 알면 쉽게 풀 수 있는 문제였습니다.

처음에 or연산이라는것을 코드를 작성할때 쓸일이 거의 없어서 가물가물했지만... 찾아보니 값1 | 값2 으로 손 쉽게 이진 or연산이 가능합니다. 하지만 연산만으로는 정답을 맞추기가 힘듭니다. n의 값의 해당하는 자리수를 맞춰줘야기 때문에 고려해야할 부분이 조금 있습니다. 저같은 경우에는 계속 테스트 케이스가 1~2개씩 틀려서 대체 어디서 틀린걸까 많은 고민 끝에 좋은 풀이 방법이 있어서 소개해드릴려고 합니다.

참고로 값1 & 값2는 and 연산을 의미합니다.

public class FindHiddenMap {

    public static void main(String[] args) {

        int n = 5;
        int[] arr1 = {9, 20, 28, 18, 11};
        int[] arr2 = {30, 1, 21, 17, 28};

        String[] result = new String[n];

        for (int i = 0; i < n; i++) {
            int arr = arr1[i] | arr2[i];
            String resultString = "";
            int target = 1;
            //n 자리수를 맞추기 위해서 포문을 추가
            for (int j = 0; j < n; j++) {
                resultString = ((arr & target) > 0 ? "#" : " ") + resultString;
                target = target << 1;  //좌측 Shift 연산 : target 변수 값 1을 1비트씩 좌측으로 이동시킵니다.

            }

            result[i] = resultString;
            System.out.println(result[i]);
        }
    }
}

위의 코드에서는 Left Shift 연산을 이용하여 n의 값에 해당하는 자리수를 맞추는 방식입니다. 사실 Shift 연산도 많이 쓸일이 없었지만 이번 카카오 문제를 풀면서 공부하게 되었습니다.

위의 코드 target 값을 1로 대입을 하고 n의 값만큼 내부적으로 for문을 수행하여 이진 or 연산의 결과 값과 다시 and 연산을 수행한 후에 target을 1비트씩 Left Shift 연산을 수행하고 있습니다.

int target = 1;

target = target << 1;  // 1비트씩 좌측으로 이동합니다.

현재 target 값이 1이기 때문에 실제로 루프를 돌때마다 아래와 같이 1비트씩 이동하는 것을 알 수 있습니다.

n = 3일때

j = 0일때
0000 0000 0000 0000 0000 0000 0000 0001

j = 1일때
0000 0000 0000 0000 0000 0000 0000 0010

j = 2일때
0000 0000 0000 0000 0000 0000 0000 0100

두번째 방법은 Shift 연산을 사용하지 않아도 쉽게 풀 수 있는 방법입니다.

public class FindHiddenMap {

    public static void main(String[] args) {

        int n = 5;
        int[] arr1 = {9, 20, 28, 18, 11};
        int[] arr2 = {30, 1, 21, 17, 28};

        String[] result = new String[n];

        for (int i = 0; i < n; i++) {
            result[i] = Integer.toBinaryString(arr1[i] | arr2[i]);
        }

        for (int i = 0; i < n; i++) {
            result[i] = String.format("%" + n + "s", result[i]);
            result[i] = result[i].replaceAll("1", "#");
            result[i] = result[i].replaceAll("0", " ");
        }

        for (String c : result){
            System.out.println(c);
        }
    }
}

위의 두번째 풀이 방식은 제가 푼 풀이는 아니고 프로그래머스에서 다른사람 풀이에서 가장 깔끔하게 짜서 참고하였습니다.

result[i] = Integer.toBinaryString(arr1[i] | arr2[i]);

Integer 클래스의 toBinaryString 메소드는 or 연산의 결과 값을 바이너리 표현 방식으로 바꿔주는 Util 메소드 입니다.

이후 배열 길이만큼 루프를 타면서 String.format을 이용하여 해당 값의 자리수를 n 값만큼 맞춘 후에 replace 값으로 "1" -> "#", "0" -> " "으로 변경하면 됩니다.

'알고리즘' 카테고리의 다른 글

해시- 전화번호 목록  (0) 2019.11.05
해시 - 완주하지 못한 마라톤 선수  (0) 2019.10.31
Hackerrank- EqualizeTheArray  (0) 2019.10.28
완전탐색 - 카펫  (0) 2019.10.25
[카카오 2020 공채] 문자열 압축  (0) 2019.10.09

오랜만에 해커랭크 Easy 문제를 풀어보면서...금방 풀줄 알았지만... 2시간정도 걸렸습니다.
사실 난이도가 Easy임에도 불구하고... 너무 많은 생각을 많이했습니다. 이걸 풀때즘에 저의 문제해결 능력이 너무 나쁘다는걸 깨달았습니다. 저 자신에 대한 프로그래머로써의 자질도 의심하게 되더군요....

사실 이 문제를 해결하기 위해서 어떤 자료구조를 써야할지.. 많은 고민을 했습니다.
제가 생각한 전체적인 흐름은 컬렉션 타입인 List 객체를 사용하여 중복없는 원소 값들을 저장하고 그 저장된 원소값들과 기존의 정수형 타입의 배열과 비교를 통해서 각각의 중복 값의 개수를 저장하는 배열을 얻습니다. 이후 배열 속에서 중복 개수가 가장 많은 배열의 index를 이용하여 처음 주어진 배열에서 최소한의 삭제 횟수를 구하도록 비즈니스 로직을 구현하는 것입니다.

이걸 푼 다음에 다른 분들의 풀이방법을 보고나서... 정말 쉽게 풀수있는 방법이 많았습니다.

스크린샷 2019-10-27 오후 10 01 21

package hackerrank;

import java.util.*;

public class EqualizeTheAraay {

    private static final Scanner sc = new Scanner(System.in);

    public static int equalizeArray(int[] arr) {

        int duplicate = findDuplicate(arr);
        int deleteCount = 0;

        // 중복 개수가 가장 많은 값과 비교하여 일치하지 값을 삭제합니다.
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] != duplicate)
                deleteCount++;
        }
        return deleteCount;
    }

    // 중복 개수가 가장 많은 값을 찾기
    static int findDuplicate(int[] arr) {

        int duplicateValue = 0;
        List<Integer> list = new ArrayList<>();

        //스트림 객체의 필터 메소드를 이용하여 중복 없는 원소들을 가진 정수형 타입의 배열을 리턴합니다.
        int[] noRedundant = Arrays.stream(arr).distinct().toArray();

        int[] count = new int[noRedundant.length];

        for (int i = 0; i < noRedundant.length; i++){
            int redundant = 0;
            for (int j = 0; j < arr.length; j++) {
                if(noRedundant[i] == arr[j]){
                    redundant++;
                }
            }
            count[i] = redundant;
        }

        duplicateValue = nooRedundant[getMax(count)];

        return duplicateValue;
    }


    // 중복 개수가 들어있는 배열에서 중복개수가 가장 많은 원소의 인덱스를 리턴.
    static int getMax(int[] count){

        int max = count[0];
        int index = 0;

        for (int i = 0; i < count.length; i++) {
            if(max <= count[i]){
                max = count[i];
                index = i;
            }
        }
        return index;
    }


    public static void main(String[] args) {

        int n = sc.nextInt();
        sc.skip("(\r\n|[\n\r\u2028\u2029\u0085])?");

        int[] arr = new int[n];

        String[] arrItems = sc.nextLine().split(" ");

        for (int i = 0; i < n; i++) {
            int arrItem = Integer.parseInt(arrItems[i]);
            arr[i] = arrItem;
        }

        int result = equalizeArray(arr);
        System.out.println(result);

        sc.close();
    }
}

같은 원소만 가진 배열

위의 문제를 해석해보면 정수타입의 배열이 주어질때 칼이라는 아이는 이 배열안에 있는 모든 원소들이 같아질때까지 배열의 크기를 줄기를 원한다는 게 이 문제의 핵심입니다.

그렇기 위해서는 배열안의 엘리먼트 중에서 중복 개수가 가장많은 값을 제외하고 나머지 원소들의 삭제 횟수를 구하는게 핵심입니다.
한마디로 중복 개수가 제외한 값을 제외하고 나머지 원소들의 삭제를 몇건이나 했는지 출력하라는 문제입니다.

완전탐색 알고리즘

카펫

스크린샷 2019-10-24 오후 11 18 36

해당 문제는 완전탐색 알고리즘의 문제로.. 사실 완전 탐색에 대해서 공부를 한적이 없어서 처음에 어떻게 접근할까 고민하다가 이틀정도 고민 끝에 해답을 찾을 수 있었습니다. 난이도는 그렇게 어려운 편이 아니라고 하지만.. 역시 수포자한테는 힘들었던 문제였습니다.... 풀이를 봐도 전혀 이해를 못하다가 결국 그림을 그리면서 패턴을 찾아서 풀었습니다.

위의 문제에서 접근법을 찾을 수 있는 힌트는 가로의 길이가 세로의 길이보다 크거나 같다는 전제 조건을 캐치하는것과 갈색 벽돌이 빨간색 벽돌을 둘러쌓야하는 것을 이해하였다면 접근방법을 이해하는데 시간이 단축 될 것 입니다.

먼저, 갈색 벽돌의 개수는 카펫의 위, 아래 가로 길이가 같고, 세로 길이 역시 좌, 우가 같기 때문에 (2 * y) + (2 * x) - 4라는 방정식으로 갈색 벽돌의 개수를 구할 수 있습니다. 여기서 - 4는 가로, 세로 길이가 아무리 변해도 카펫의 꼭지점의 개수는 4개로 변하지 않는 불변의 값을 가집니다.

그리고 빨간색 벽돌의 세로 길이는 갈색 벽돌의 세로 길이보다 -2만큼 이고, 가로 길이 역시 갈색 벽돌 -2 정도이기 때문에 (y - 2) * (x - 2) 식을 도출할 수 가 있습니다.

갈색 벽돌과 빨간색 벽돌의 개수를 이용하여 카펫의 가로, 세로의 길이를 구할 수 있었습니다.

JAVA 코드로 다음과 같이 작성 할 수 있습니다.

import java.util.Arrays;

public class Carpet {

    public int[] solution(int brown, int red){

        if(brown < 8 ||brown >5000 && red < 1 || red >2000000) {
            throw new IllegalStateException();
        }

        int[] answer = new int[2];
        // 빨간색 벽돌은 갈색벽돌에 둘러 쌓여 있기 때문에 세로의 길이는 무조건 3이상부터 시작한다.
        // 가로 길이도 마찬가지로 갈색 벽돌이 빨간색 벽돌을 둘러쌓기 위해서는 3이상이여 됩니다.
        A:for (int y = 3; y < 5000; y++) {
            for (int x = y; x < 5000; x++) {
                if(isSameAsBrown(y, x, brown)){
                    if(isSameAsRed(y, x, red)){
                        answer[0] = x;
                        answer[1] = y;

                        break A;
                    }
                }

            }
        }
        return answer;
    }

    // 세로의 길이 : y, 가로의 길이 : x, 갈색 벽돌 개수 : brown 을 argument로 받아서 가로 세로의 길이를 구하기 위한 로직
    public static boolean isSameAsBrown(int y, int x, int brown){

        //위 아래로 가로 길이는 같고, 좌 우 가로 길이는 같기 때문에 갈색 벽돌의 개수는 아래의 식으로 구할 수 있습니다.
        //가로 세로의 길이에 따라서 변하지 않는 점은 결국 꼭지점은 항상 4개이기 때문에 -4를 해주어야 합니다.
       if((((y * 2) + (x * 2)) - 4) == brown){

            return true;
        }

        return false;
    }

    // 마찬가지로 빨산색 벽돌의 수를 가지고 가로 세로의 길이를 구합니다.
    public static boolean isSameAsRed(int y, int x, int red){
        //가로 -2 * 세로 - 2를 하면 빨간색 벽돌의 넓이가 나옵니다.
        if(((y - 2) * (x - 2)) == red){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        Carpet carpet = new Carpet();
        System.out.println(Arrays.toString(carpet.solution(10,2)));
        System.out.println(Arrays.toString(carpet.solution(8,1)));
        System.out.println(Arrays.toString(carpet.solution(24,24)));
    }
}
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;

import static org.junit.Assert.*;

@RunWith(JUnit4.class)
public class CarpetTest {

    @Test
    public void 완전탐색_카펫_테스트() {
        //given
        Carpet carpet = new Carpet();

        //when
        int[] tesCase1 = carpet.solution(8, 1);
        int[] tesCase2 = carpet.solution(10, 2);
        int[] tesCase3 = carpet.solution(24, 24);

        //then
        assertEquals("[3, 3]", Arrays.toString(tesCase1));
        assertEquals("[4, 3]", Arrays.toString(tesCase2));
        assertEquals("[8, 6]", Arrays.toString(tesCase3));
    }
}

위의 소스코드에서 이중 포문에서 첫번째 포문을 A로 alias를 설정하여 break문으로 해당 포문을 한번에 빠져나갈 수 있는 문법입니다.

A:for (int y = 3; y < 5000; y++) {
    // 비즈니스로직 구현부
} 

처음 써보는건데 유용해서 한번 써봤습니다.

어제 알고리즘 스터디를 진행하면서 카카오 2020 신입 공채로 나온 문제를 풀기로 하였습니다.
카카오 난이도 중에서 가장 정답률이 높았던 문제여서 가벼운 마음으로 도전하였지만... 역시 저에겐 알고리즘이란 거대한 벽은 넘기가 힘들었습니다. 차라리 Hackerrank에서 영어로 된 난이도 중정도 되는 문제가 나은거 같기도하고.... 어쨌든 멍청한 저의 머리로 하루내내 생각하면서 짜보았습니다.

문제 설명

데이터 처리 전문가가 되고 싶은 어피치는 문자열을 압축하는 방법에 대해 공부를 하고 있습니다. 최근에 대량의 데이터 처리를 위한 간단한 비손실 압축 방법에 대해 공부를 하고 있는데, 문자열에서 같은 값이 연속해서 나타나는 것을 그 문자의 개수와 반복되는 값으로 표현하여 더 짧은 문자열로 줄여서 표현하는 알고리즘을 공부하고 있습니다.
간단한 예로 aabbaccc의 경우 2a2ba3c(문자가 반복되지 않아 한번만 나타난 경우 1은 생략함)와 같이 표현할 수 있는데, 이러한 방식은 반복되는 문자가 적은 경우 압축률이 낮다는 단점이 있습니다. 예를 들면, abcabcdede와 같은 문자열은 전혀 압축되지 않습니다. 어피치는 이러한 단점을 해결하기 위해 문자열을 1개 이상의 단위로 잘라서 압축하여 더 짧은 문자열로 표현할 수 있는지 방법을 찾아보려고 합니다.

예를 들어, ababcdcdababcdcd의 경우 문자를 1개 단위로 자르면 전혀 압축되지 않지만, 2개 단위로 잘라서 압축한다면 2ab2cd2ab2cd로 표현할 수 있습니다. 다른 방법으로 8개 단위로 잘라서 압축한다면 2ababcdcd로 표현할 수 있으며, 이때가 가장 짧게 압축하여 표현할 수 있는 방법입니다.

다른 예로, abcabcdede와 같은 경우, 문자를 2개 단위로 잘라서 압축하면 abcabc2de가 되지만, 3개 단위로 자른다면 2abcdede가 되어 3개 단위가 가장 짧은 압축 방법이 됩니다. 이때 3개 단위로 자르고 마지막에 남는 문자열은 그대로 붙여주면 됩니다.

압축할 문자열 s가 매개변수로 주어질 때, 위에 설명한 방법으로 1개 이상 단위로 문자열을 잘라 압축하여 표현한 문자열 중 가장 짧은 것의 길이를 return 하도록 solution 함수를 완성해주세요.

 

 

 

스크린샷 2019-10-09 오전 2 44 42

 

스크린샷 2019-10-09 오전 2 44 54

 

 


## @DataJpaTest

@DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드합니다. DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능합니다. 그리고 가장 좋은점은.. 무려 내장형 데이터베이스를 사용하여 실제 데이터베이스를 사용하지 않고 테스트 데이터베이스로 테스트할 수 있는.. 개꿀같은 장점이 있습니다.

@DataJpaTest는 기본적으로 @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성합니다. 만약 최적하한 별도의 데이터소스를 사용하여 테스트하고 싶다면 기본 설정된 데이터소스를 사용하지 않도록 아래와 같이 설정해도 됩니다.


```java
package kakao;

public class ConvertRightBracket {

    // 올바른 괄호인지 판단을 하는 메소드
    public boolean isRightBracket(String p) {

        int temp = 0;

        for (int i = 0; i < p.length(); i++) {

            if (p.charAt(i) == '(') temp++;
            if (p.charAt(i) == ')') temp--;
            if (temp < 0) return false;

            //String a = "(()())()";
        }
        return true;
    }


    public String convertBracket(String p) {

        //문자열 길이가 0 이거나 ""(빈문자열)이면 "" 리턴
        if (p.equals("") || p.length() == 0)
            return "";

        String u = balancedBracket(p);
        String v = p.substring(u.length());


        StringBuilder sb = new StringBuilder();

        if (isRightBracket(u)) {
            sb.append(u);
            sb.append(convertBracket(v));
            return sb.toString();
        } else {
            sb.append("(");
            sb.append(convertBracket(v));
            sb.append(")");
            sb.append(removeAndReverse(u));
            return sb.toString();
        }
    }


    public String balancedBracket(String w) {

        int temp = 0;

        for (int i = 0; i < w.length(); i++) {

            if (w.charAt(i) == '(') temp++;
            if (w.charAt(i) == ')') temp--;

            if (i >= 1 && temp == 0) return w.substring(0, i + 1);
        }

        return "";
    }

    // 괄호 문자열 맨 앞 뒤 제거 후 남은 문자열 역순으로 나열하기
    public String removeAndReverse(String u) {
        return reverse(u.substring(1, u.length() - 1));
    }


    public String reverse(String u){

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < u.length(); i++) {

            if(u.charAt(i) == '(')
                sb.append(")");

            if(u.charAt(i) == ')')
                sb.append("(");

        }

        return sb.toString();
    }


    public String soluton(String p) {

        if (isRightBracket(p)) return p;
        else return convertBracket(p);

    }

    public static void main(String[] args) {

        ConvertRightBracket c = new ConvertRightBracket();
        String p = "()))((()";
        System.out.println(c.soluton("()))((()"));
        System.out.println(c.soluton(")("));
        System.out.println(c.soluton("()))((()"));
    }
}

 

이 문제는 푸는것보다 문제를 이해하는데 더 많은 어려움을 겪었습니다.  정말 문제설명에서 나온것처럼 시키는대로 잘하면 풀수 있는 문제였는데.. 왜 이렇게 오래걸렸는지 OTL.... 이 문제를 다룬 다른 블로그를 보면 정말 훌륭하게 객체지향적으로 코드를 작성하신 분들이 많습니다. 그걸 보고 나중에 저도 코드를 작성할때 참조 하겠습니다.

 

이상 카카오 2020 문자열 압축.. 리뷰였습니다.

'알고리즘' 카테고리의 다른 글

해시 - 완주하지 못한 마라톤 선수  (0) 2019.10.31
2018년 카카오 블라인드 공채 문제1 - 비밀지도  (0) 2019.10.29
Hackerrank- EqualizeTheArray  (0) 2019.10.28
완전탐색 - 카펫  (0) 2019.10.25
스택과 큐  (0) 2019.10.05

최근에 자료구조와 함께 배우는 알고리즘 입문(자바편) 책을보고 예전에 대학교 시절에 대충 배웠던 자료구조에 대해서 자세하게 공부할 기회가 생겨서 리뷰를 하게 되었습니다.

확실히 책은 한번 읽는것보다 여러번 읽어보니 그때는 왜 이런걸 못봤을까…하는 생각이 듭니다.
스택과 큐에 대해서 간단하게 코드 리뷰와 이러한 자료구조를 어디에서 사용되는지도 다시 한번 알게된 계기가 되었습니다.

스택(Stack)

스택은 데이터를 일시적으로 저장하기 위해 사용하는 자료구조로, 선입선출(가장 나중에 넣은 데이터를 가장 먼저 꺼내는) 방식입니다. 음… 이걸 읽고 생각나는게… 아침에 출근을 할때 지하철을 타게되면 분명 가장먼저 탔는데 내릴때는 가장 늦게내리는 제 모습이 생각나는군요…묘하게 비슷합니다.

스택의 구조

스크린샷 2019-10-05 오전 1 17 27

맨 처음에 스택이라는 배열에는 Data 1, Data 2, Data 3이 들어왔고, 그 다음으로 Data 4가 들어왔습니다. 이때 스택안으로 자료를 넣는 작업을 Push라고 합니다. 그리고 해당 데이터를 꺼내는 작업을 Pop이라고 합니다. 이때 알아두어야 할 점은 내가 원하는 데이터를 꺼낼수 있는게 아니라 스택에서 가장 후입(늦게 들어온 데이터)된 데이터를 꺼낸다는 점입니다.

스택은 사용자가 구현한 Java 프로그램에서 메서드를 호출하고 실행할 때 프로그램 내부에서는 스택을 사용합니다. 다음 아래와 같이 main메서드를 포함한 총 4개의 메서드로 이루어진 코드로 스택을 살펴보겠습니다.

스택 사용의 예제

void x(){
    System.out.println("x를 호출하였습니다.")
}
void y(){
    System.out.println("y를 호출하였습니다.")
}

void z(){
    x();
    y();
    System.out.println("z를 호출하였습니다.")
}

int main(){
    z();
}

먼저 사용자가 작성한 자바소스 코드는 javac 컴파일러라는 녀석에 의해서 기계어로 번역되고 만약 문법적인 오류가 없다면 해당 소스코드를 class 파일로 만든 후 클래스 로더라는 녀석이 JVM이 관리하고 있는 각각의 메모리 영역에 로딩을 하게 됩니다. 그리고 JVM은 프로그램 흐름이 시작하는 main 메서드를 찾게됩니다.

위의 코드에서 main 메서드를 가장 먼저 찾아서 스택에 Push를 수행합니다. main 메서드는 스코프 안에 정의되어 있는 z 메서드를 호출합니다. 마찬가지로 z메서드에서 x 메서드를 호출하게 됩니다.
그러면 스택에서 main -> z -> x 순으로 Push가 수행되고 x 메서드가 실행종료 되면 다시 z 메서드로 돌아오는데 이때 스택에서는 x 메서드를 Pop하는 작업을 수행하게 됩니다.

z 메서드는 y 메서드를 호출하고, 스택에서는 Push를 수행하여 main -> z -> y 순으로 쌓여 있습니다. y 메서드가 실행종료되면, 다시 z 메서드로 돌아오고 더 이상 실행할 문장이 없이 종료가 되면 스택 영역에서 Pop 작업이 수행됩니다. 그러면 스택 안에서는 더 이상 main 메서드 밖에 남지 않게 됩니다. main 메서드도 실행할 문장이 존재하지 않다면 스택에서 Pop이 이루어지고 프로그램이 종료가 됩니다.

이처럼 스택은 가장 나중에 저장된 데이터가 가장 먼저 인출되는 방식으로 동작합니다.

아래 소스코드는 스택에서 사용할 수 있는 메소드를 책을보고 구현해본 코드입니다.

public class IntStack {

    int max; //스택 용량 총 배열의 크기를 말함
    int ptr; //스택에 쌓여있는 데이터 수를 나타낸다.
    int[] stk; //스택 본체를 의미한다.

    //실행 시 예외: 스택이 비어 있을때 
    public class EmptyIntStackException extends RuntimeException {
        public EmptyIntStackException() {
        }
    }

    //실행 시 예외: 스택이 가득 차 있을때 
    public class OverflowIntStackException extends RuntimeException {
        public OverflowIntStackException() {
        }
    }

    //생성자를 통해서 IntStack 클래스의 맴버변수들을 초기화 수행
    public IntStack(int capacity) {

        this.ptr = 0;
        this.max = capacity;
        try {
            this.stk = new int[max];
        } catch (OutOfMemoryError e) {
            max = 0;
        }
    }
    //스택영역에 데이터를 Push를 수행하는 메서드
    public int push(int x) throws OverflowIntStackException {
        if (ptr >= max) //데이터의 개수가 스택 용량과 크거나 같으면 오버플로우 예외 발생
            throw new OverflowIntStackException();

        return stk[ptr++] = x;
    }

    //스택영역에 데이터를 Pop을 수행하는 메서드 
    public int pop() throws EmptyIntStackException {
        if (ptr <= 0) //데이터의 개수가 0보다 작거나 같으면 EmptyIntStack 예외 발생
            throw new EmptyIntStackException();
        return stk[--ptr];
    }
    //peek은 엿보다라는 뜻으로 스택영역에서 가장 늦게 들어온 데이터를 읽어서 출력하는 메서드
    public int peek() throws EmptyIntStackException {
        if (ptr <= 0) { //마찬가지로 스택이 비어있으면 EmptyIntStack 예외 발생
            throw new EmptyIntStackException();
        }
        return stk[ptr - 1];
    }

    //특정 데이터의 index를 가져오는 메서드
    public int indexOf(int x) {
        for (int i = ptr - 1; i >= 0; i--)
            if (stk[i] == x)
                return i;

        return -1; //해당 데이터가 존재하지 않으면 -1을 리턴
    }

    //스택에 있는 데이터를 비우는 메서드
    public void clear() {
        ptr = 0;
    }
    //스택의 용량을 확인하는 메서드
    public int capacity() {
        return max;
    }
    //스택의 데이터 개수를 확인하는 메서드
    public int size() {
        return ptr;
    }
    //dump 메서드는 가장 먼저 들어온 데이터부터 숱차적으로 읽어서 출력하는 메서드
    public void dump() {
        if (ptr <= 0)
            System.out.println("스택이 비어 있습니다.");
        else {
            for (int i = 0; i < ptr; i++)
                System.out.print(stk[i] + " ");
            System.out.println();
        }
    }
}

큐(Queue)

마지막으로 살펴볼 큐는 스택과 마찬가지로 데이터를 일시적으로 쌓아 놓은 자료구조입니다. 하지만 가장 구별되는 특징은 가장 먼저 넣은 데이터를 가장 먼저 꺼내는 선입선출인 점이 스택과 다릅니다.
예를들어서 대형마트 같은곳에서 계산을 할때에는 대기열 맨앞에 줄을서는 고객이 먼져 계산하고 나가는게 대표적이네요. 어떻게보면 공정하다고 할 수 있는 자료구조라고 생각됩니다…

큐의 구조

스크린샷 2019-10-05 오전 1 17 41

큐에 데이터를 넣는 작업을 인큐(enqueue)라 하고, 데이터를 꺼내는 작업을 디큐(dequeue)라고 합니다.
또 데이터를 꺼내는 쪽을 프런트(front)라 하고, 데이터를 넣는 쪽을 리어(rear)라고 합니다.

위의 그림에서 보듯이 큐는 스택과 마찬가지로 배열을 사용하여 구현할 수 있습니다.
현재 1번 그림은 배열의 프런트부터 5개(a,b,c,d,e)의 데이터가 들어가 있는 모습입니다. 배열의 이름을 que라고 할 경우 que[0]부터 que[4]까지의 데이터가 저장됩니다.

2번 그림은 a를 디큐합니다. 디큐를 하면 프런트의 값이 +1 증가하하게 됩니다.
3번 그림도 b를 디큐하고, 프런트 값이 1 증가하였습니다. 인큐는 시간복잡도가 O(1)이지만 디큐를 하게되면 모든 요소를 하나씩 앞쪽으로 옮겨야 합니다. 이러한 문제때문에 처리의 복잡도는 O(n)이 되며 데이터를 꺼낼 때마다 이런 처리를 하게되면 효율이 떨어지는 단점이 있습니다.

이러한 문제때문에 배열 요소를 앞쪽으로 옮기지 않는 큐를 구현해 보았습니다.,
이러한 큐를 링 버퍼(ring buffer)라고 합니다. 링 버퍼는 그림처럼 배열의 처음과 끝이 연결되었다고 보는 자료구조입니다.

스크린샷 2019-10-05 오전 1 17 49

프런트: 맨 처음 요소의 인덱스
리어: 맨 끝 요소의 인덱스

3개의 데이터(A,B,C)가 차례대로 que[0],que[1],que[2]에 저장 됩니다. 프런트의 값은 0이고 리어 값은 3입니다. 그리고 D라는 데이터를 인큐하면 리어 값이 1만큼 증가합니다.

큐가 완전히 비어있는 상황일 경우 프런트, 리어는 0값을 가지고 있습니다. C라는 데이터를 디큐하게 되면 프런트의 값이 1증가하는것을 볼 수가 있습니다. 이렇게 큐를 구현하면 프런트와 리어 값을 업데이트하며 인큐와 디큐를 수행하기 때문에 앞에서 발생한 요소 이동 문제를 해결할 수 있습니다. 물론 처리복잡도 또한 O(1)입니다.

큐도 마찬가지로 소스코드로 구현해보았습니다.

링 버퍼 구현

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;
        this.max = capacity;
        try {
            que = new int[max]; //인자 값으로 큐 본체용 배열을 생성함
        } catch (OutOfMemoryError e) {  //생성할 수 없음
            max = 0;
        }
    }
    //인큐 발생시에 rear 값을 1만큼 증가시킨다. 
    public int enque(int x) throws OverflowIntQueueException {
        if (num >= max)
            throw new OverflowIntQueueException();

        que[rear++] = x;
        num++;
        if (rear == max) //배열의 마지막 인덱스에 데이터가 존재하면 rear 값을 0으로 변경
            rear = 0;
        return x;
    }

    //디큐가 발생하면 프런트 값이 1만큼 증가한다.
    public int deque() throws EmptyIntQueueException {
        if (num <= 0) //큐가 비어있을때는 예외발생
            throw new EmptyIntQueueException();
        int x = que[front++];
        num--;
        if (front == max) //계속해서 디큐가 발생하여 프런트 값이 큐의 끝에 달하면 0으로 변경
            front = 0;
        return x;
    }

    //마찬가지로 큐의 프런트 부분을 읽고 출력하는 메서드
    public int peek() throws EmptyIntQueueException {
        if (num <= 0)
            throw new EmptyIntQueueException();
        return que[front];
    }

    //해당 데이터가 큐의 몇번째 인덱스에 들어있는지 출력하는 메서드
    public int indextOf(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++) {
                int idx = (i + front) % max;
                System.out.print(que[idx] + " ");
            }
            System.out.println();
        }
    }
}

링버퍼는 프런트와 리어부분만 업데이트를 해주기 때문에 인큐가 발생해도 모든 요소들을 옮기지 않아도 되고,만약 n개인 배열에 계속 데이터가 입력되면 가장 최근에 들어온 데이터 n개만 저장하고 오래된 데이터를 버리는 방식입니다. 이러한 링 버퍼를 어디서 사용할까 생각을 해보면서… 스마트 뱅킹을 사용할때 고객들이 최근 카드 결제내역 10건등을 조회하는데 링 버퍼를 이용하면 유용하지 않을까 생각을 해보았습니다.

오늘은 이렇게 자료구조를 대표하는 스택과 큐에 대해서 공부를 하면서 대학교때 대강대강 넘어가면서 보지 못했던 부분을 다시 보게되어서 보람이 있었습니다.

다음번에도 자료구조와 관련해서 다양한 알고리즘에 대해서 리뷰를 진행하겠습니다.

+ Recent posts