버블 정렬 (Bubble Sort)


버블 정렬 (Bubble Sort)

  • 가장 단순한 정렬 알고리즘
  • 인접한 요소와 정렬 기준(오름차순, 내림차순)대로 교환
  • 안정 정렬 (stable sort)
     : 정렬 후에도 같은 값들 끼리의 순서가 보장됨을 의미
  • 제자리 정렬 (in-place sort)
     : 데이터에 필요한 공간을 제외하고 추가적인 공간이 필요없음을 의미
  • 정렬한 데이터의 개수가 적을때는 나쁘지 않은 속도를 보임

버블 정렬 방법 (오름차순 기준)

  • 첫 번째 사이클이 끝나면 맨 마지막 값이 정렬되고, 두 번째 사이클이 끝나면 뒤에서 두 번째의 값이 정렬된다. 이같은 과정을 계속 반복하면 최종적으로 정렬이 된다. 

 

버블 정렬의 성능

  • 시간복잡도
    최악의 경우 : O(n^2)
    최선의 경우 : O(n)

  • 최악의 경우 : 모든 데이터가 역순인 경우 (5, 4, 3, 2, 1)
    5를 정렬하기 위해서는 n-1번 비교 및 교환해야하고, 4를 정렬하기 위해서는 n-2번 비교 및 교환해야 한다.
    즉, 5를 정렬할 때 4번, 4를 정렬할 때 3번, 3을 정렬할 때 2번, 1과 2를 정렬할 때 1번의 비교 및 교환을 거치게 되므로 비교 횟수는 4 + 3 + 2 + 1회가 된다. 
    따라서, 데이터가 n개일 때 총 비교횟수는 (n-1) + (n-2) + (n-3) + ... + 2 + 1 이 되므로 등차수열의 합에 따라 1/2n(n-1)이 되며 빅-오는 최고차항을 가지기 때문에 O(n^2)이 된다.

  • 최선의 경우 : 모든 데이터가 이미 정렬된 경우 (1, 2, 3, 4, 5)
    한 번도 교환이 이루어지지 않았다면 이미 정렬된 상태이므로 n번 비교 후 반환되어 O(n) 이 된다.

기본 구현

  • 기본 int형 배열 정렬 구현
    [BubbleBasic.java]
    package Sort.Bubble;
    
    /**
     * 단순히 int형 배열만 오름차순 또는 내림차순으로 정렬하는 경우
     */
    public class BubbleBasic {
        private boolean isDescending = false;
    
        /**
         * @param arr int형 배열
         * @param isDescending 내림차순 및 오름차순 선택
         */
        public static void sort(int[] arr, boolean isDescending) {
            // 이미 정렬이 되어있는지 확인하기 위한 변수
            boolean isAlreadySorted = true;
            int n = arr.length;
    
            // 정렬할 값을 선택하는 반복문
            for(int i=0; i<n-1; i++) {
                // 정렬할 값과 나머지 값들을 비교하기 위한 반복문
                // 이미 정렬이 완료된 데이터는 비교대상에서 제외해야 하므로 n-i-1
                for(int j=0; j<n-i-1; j++) {
                    // 정렬되지 않았다면
                    if (!isSortTwoElements(arr[j], arr[j + 1], isDescending)) {
                        // 데이터를 교환한다.
                        swap(arr, j, j + 1);
                        // 스왑을 했다면 초기 배열 상태가 정렬되지 않았다는 것이므로 false로 변경
                        isAlreadySorted = false;
                    }
                }
    
                // 한 번도 교환이 발생하지 않았다면 이미 정렬된 데이터이므로 반복문을 빠져나온다.
                if (isAlreadySorted) {
                    break;
                }
            }
        }
    
        /**
         *
         * @param arr 데이터 배열
         * @param idx1 교환할 인덱스
         * @param idx2 교환할 인덱스
         */
        private static void swap(int[] arr, int idx1, int idx2) {
            int tmp = arr[idx1];
            arr[idx1] = arr[idx2];
            arr[idx2] = tmp;
        }
    
        /**
         *
         * @param cmp1 비교할 값
         * @param cmp2 비교할 값
         * @param isDescending 내림차순 및 오름차순 선택
         * @return 내림차순의 경우 cmp1이 작을 경우 false, 그렇지 않으면 true
         *         오름차순의 경우 cmp1이 작을 경우 true, 그렇지 않으면 false
         */
        private static boolean isSortTwoElements(int cmp1, int cmp2, boolean isDescending) {
            // 내림차순일 경우
            if (isDescending) {
                if (cmp1 < cmp2) {
                    return false;
                }
                else {
                    return true;
                }
            }
            // 오름차순일 경우
            else {
                if (cmp1 < cmp2) {
                    return true;
                }
                else {
                    return false;
                }
            }
        }
    }​
  • BubbleBasic.java 테스트 및 실행시간 측정
    package Sort.Bubble;
    
    public class BubbleBasicMain {
        private static final int MAX_COUNT = 10;
    
        public static void main(String[] args) {
            int[] arr = new int[MAX_COUNT];
            for(int i=0; i<MAX_COUNT; i++) {
                // 0 ~ MAX_COUNT 범위내의 난수를 생성
                arr[i] = (int)(Math.random() * MAX_COUNT);
            }
    
            // 배열은 참조형 이므로 arr을 넘길 경우 arr값이 변경되기 때문에 복사본을 생성한 후 테스트
            int[] ascArrTest = arr.clone();
            System.out.println("정렬 전 데이터");
            for(int i : ascArrTest) {
                System.out.println(i);
            }
            System.out.println();
    
            BubbleBasic.sort(ascArrTest, false);
            System.out.println("오름차순 정렬 후 데이터");
            for(int i : ascArrTest) {
                System.out.println(i);
            }
            System.out.println();
    
            int[] descArrTest = arr.clone();
            BubbleBasic.sort(descArrTest, true);
            System.out.println("내림차순 정렬 후 데이터");
            for(int i : descArrTest) {
                System.out.println(i);
            }
    
            // 시간복잡도 측정 (오름차순 기준)
            long start = 0;
            long end = 0;
    
            int[] bestTimeComplexityArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
            start = System.nanoTime();
            BubbleBasic.sort(bestTimeComplexityArray, false);
            end = System.nanoTime();
            System.out.println("최선의 경우 오름차순 시간 : " + (end - start) + "ns");
    
            int[] worstTimeComplexityArray = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
            start = System.nanoTime();
            BubbleBasic.sort(worstTimeComplexityArray, false);
            end = System.nanoTime();
            System.out.println("최악의 경우 오름차순 시간 : " + (end - start) + "ns");
        }
    }​


  • 결과
    정렬 전 데이터
    2
    6
    1
    7
    7
    6
    0
    6
    1
    0

    오름차순 정렬 후 데이터
    0
    0
    1
    1
    2
    6
    6
    6
    7
    7

    내림차순 정렬 후 데이터
    7
    7
    6
    6
    6
    2
    1
    1
    0
    0
    최선의 경우 오름차순 시간 : 3805ns
    최악의 경우 오름차순 시간 : 18532ns

클래스 비교를 위한 구현

  • 클래스의 특정 값을 이용한 정렬 코드 구현
    [BubbleAdvanced.java]
    package Sort.Bubble;
    
    import java.util.Comparator;
    
    public class BubbleAdvanced {
        public static <T> void sort(T[] arr) {
            Comparable<? super T> comp;
            boolean isAlreadySorted = true;
            int n = arr.length;
    
            for(int i=0; i<n-1; i++) {
                for(int j=0; j<n-i-1; j++) {
                    comp = (Comparable<? super T>)arr[j];
                    if (comp.compareTo(arr[j + 1]) > 0) {
                        swap(arr, j, j + 1);
                        isAlreadySorted = false;
                    }
                }
    
                if (isAlreadySorted){
                    break;
                }
            }
        }
    
        /**
         *
         * @param arr 데이터 배열
         * @param idx1 교환할 인덱스
         * @param idx2 교환할 인덱스
         */
        private static <T> void swap(T[] arr, int idx1, int idx2) {
            T tmp = arr[idx1];
            arr[idx1] = arr[idx2];
            arr[idx2] = tmp;
        }
    
        public static <T> void sort(T[] arr, Comparator<? super T> comparator) {
            boolean isAlreadySorted = true;
            int n = arr.length;
    
            for(int i=0; i<n-1; i++) {
                for(int j=0; j<n-i-1; j++) {
                    if (comparator.compare(arr[j], arr[j + 1]) > 0) {
                        swap(arr, j, j + 1);
                        isAlreadySorted = false;
                    }
                }
    
                if (isAlreadySorted){
                    break;
                }
            }
        }
    }​
  • BubbleAdvanced 테스트
    package Sort.Bubble;
    
    import java.util.Comparator;
    
    public class BubbleAdvancedMain {
        private static final int MAX_COUNT = 10;
    
        public static void main(String[] args) {
            Integer[] arr = new Integer[MAX_COUNT];
            for (int i = 0; i < MAX_COUNT; i++) {
                // 0 ~ MAX_COUNT 범위내의 난수를 생성
                arr[i] = (int) (Math.random() * MAX_COUNT);
            }
    
            // 배열은 참조형 이므로 arr을 넘길 경우 arr값이 변경되기 때문에 복사본을 생성한 후 테스트
            Integer[] ascArrTest = arr.clone();
            System.out.println("정렬 전 데이터");
            for (int i : ascArrTest) {
                System.out.println(i);
            }
            System.out.println();
    
            BubbleAdvanced.sort(ascArrTest);
            System.out.println("오름차순 정렬 후 데이터");
            for (int i : ascArrTest) {
                System.out.println(i);
            }
            System.out.println();
    
            Integer[] descArrTest = arr.clone();
            System.out.println("내림차순 정렬 후 데이터");
            BubbleAdvanced.sort(descArrTest, new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    if (o1 < o2) {
                        return 1;
                    } else if (o1 == o2) {
                        return 0;
                    } else {
                        return -1;
                    }
                }
            });
            for (int i : descArrTest) {
                System.out.println(i);
            }
            System.out.println();
    
            // 클래스의 특정값을 이용한 정렬 가능
            class Student {
                String name;
                int age;
    
                public Student(String name, int age) {
                    this.name = name;
                    this.age = age;
                }
    
                @Override
                public String toString() {
                    return "name='" + name + '\'' +
                            ", age=" + age;
                }
            }
    
            System.out.println("클래스 정렬 전 데이터");
            Student[] students = new Student[5];
            students[0] = new Student("아이언맨", 11);
            students[1] = new Student("헐크", 4);
            students[2] = new Student("토르", 15);
            students[3] = new Student("블랙위도우", 13);
            students[4] = new Student("캡틴", 4);
            for(Student student : students) {
                System.out.println(student.toString());
            }
            System.out.println();
    
            System.out.println("클래스 정렬 후 데이터");
            BubbleAdvanced.sort(students, new Comparator<Student>() {
                @Override
                public int compare(Student o1, Student o2) {
                    if (o1.age < o2.age) {
                        return -1;
                    }
                    else if (o1.age == o2.age) {
                        return 0;
                    }
                    else {
                        return 1;
                    }
                }
            });
    
            for(Student student : students) {
                System.out.println(student.toString());
            }
        }
    }​
  • 결과
    정렬 전 데이터
    7
    0
    0
    9
    4
    4
    6
    0
    5
    0

    오름차순 정렬 후 데이터
    0
    0
    0
    0
    4
    4
    5
    6
    7
    9

    내림차순 정렬 후 데이터
    9
    7
    6
    5
    4
    4
    0
    0
    0
    0

    클래스 정렬 전 데이터
    name='아이언맨', age=11
    name='헐크', age=4
    name='토르', age=15
    name='블랙위도우', age=13
    name='캡틴', age=4

    클래스 정렬 후 데이터
    name='헐크', age=4
    name='캡틴', age=4
    name='아이언맨', age=11
    name='블랙위도우', age=13
    name='토르', age=15

 

깊이 우선 탐색


깊이 우선 탐색

  • DFS (Depth-First Search)
  • 깊이 우선 탐색 시 끝없이 깊어지는 것을 방지하기 위해 깊이 제한 적용 (조건을 이용)
  • 목표 노드에 도달하지 못하면 부모노드로 되돌아감 (부모노드로 되돌아가는 것을 백트래킹(backtracking)이라고 함)
  • 하나의 정점에서 아래쪽으로 깊게 탐색 후 더이상 탐색이 불가능하면 되돌아가 인접한 다른 노드를 탐색
  • 시간 복잡도 : O(V + E)
    V : 정점의 개수 (위 사진에서 A, B, C, D, E, F, G)
    E : 간선의 개수 (각 정점을 잇는 선)
  • 공간 복잡도 : O(V)
    방문 여부를 확인하기 위한 visited 배열 추가 필요

  • 방문 여부를 확인하기 위한 visited 배열 필요성
    방문 여부를 체크하지 않으면 두 번 탐색할 수 있음. (ex. D에서 부모노드로 돌아갔을 때 또 비교할 수 있음)
    싸이클(무한루프) 방지를 위해 필요

  • 재귀 및 스택으로 구현 가능
  • 장점
    1) 지나온 노드들의 경로만 알고 있으면 되기 때문에 공간을 비교적 적게 차지함
    2) 목표노드가 깊을수록 빨리 해를 구할 수 있음

  • 단점
    1) 해가 없는 경로에 빠질 수 있음 (특정 깊이까지만 탐색하고 발견하지 못하면 다시 돌아가 다른 경로로 이동하는 방법으로 해결가능)
    2) 최단경로가 된다는 보장이 없음 (검색 노드가 발견되면 탐색을 종료하기 때문)

  • 사용예제 (작성 예정)
    1) 미로에서 출구를 찾기 위한 경로 탐색

순열


순열

  • 하나의 집합을 다른 순서로 뒤섞는 것
  • n개의 집합에서 순열의 총 개수는 n!
    ex) n = 5, 5x4x3x2x1 = 순열의 총 개수
  • nPr 수식 (P = Permutation 약자)
    3P2 = 3! / (3-2)!
            = 6 / 1
            = 6   (1, 2) / (1, 3) / (2, 1) / (2, 3) / (3, 1) / (3, 2) 총 6개
  • 재귀로 구현 가능
    자주 사용되는 swap, visited 방법으로 구현
  • 완전탐색 문제에서도 자주 나올 수 있음

 

swap을 이용한 순열

  • 배열의 값을 직접 교환해서 구함
  • Depth를 이용한 풀이
  • Depth와 r의 값이 같으면 모두 다 뽑았다는 것이므로 결과값 출력
  • 순열 함수가 종료됐을 때 배열의 값을 다시 원상복구 해주어야 함
    (그 다음 수와 다시 순열을 계산해야 하므로)

구현
주석으로 설명했으나, 제일 빠른건 직접 디버깅하면서 확인해보면 된다.

public static void swapPermutation(int[] arr, int depth, int n, int r) {
    // depth와 r이 같으면 다 뽑았다는 것이므로
    if (depth == r){
        for(int i=0; i<r; i++){
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        return;
    }
    else {
        /*
            1, 2, 3 세 개의 숫자를 순열로 구한다고 했을 때,
            재귀의 특성 상
            depth가 0일 경우 1XX, 2XX, 3XX을 구해야 함
            depth가 1일 경우 12X, 13X 를 구해야 함
            depth가 2일 경우 123, 132 를 구해야 함
            depth == r에서 return 되면서 배열을 원상복구 시킨 후
            depth가 0일 경우 2XX, 3XX 를 구하는 과정을 반복
         */
        for(int i=depth; i<n; i++){
            swap(arr, i, depth);
            swapPermutation(arr, depth + 1, n, r);
            swap(arr, i, depth);
        }
    }
}

public static void swap(int[] arr, int i, int j){
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

 

visited을 이용한 순열

  • 방문했는지 확인할 수 있는 visited 배열과, 결과를 저장할 수 있는 result 배열이 필요함
  • Depth를 이용한 풀이
  • Depth와 r의 값이 같으면 모두 다 뽑았다는 것이므로 결과값 출력
  • visited를 통해 result에 데이터 저장 시 중복값이 들어가지 않도록 함

구현

public static void visitedPermutation(int[] arr, boolean[] visited, int[] result, int depth, int n, int r) {
    if (depth == r){
        for(int i : result){
            System.out.print(i + " ");
        }
        System.out.println();
        return;
    }
    else {
        for(int i=0; i<n; i++){
            if (!visited[i]){
                visited[i] = true;
                result[depth] = arr[i];
                visitedPermutation(arr, visited, result, depth + 1, n, r);
                visited[i] = false;
            }
        }
    }
}

 

결과비교

결과를 살펴보면 아래와 같다.

swap을 이용한 순열과 visited를 이용한 순열의 데이터를 비교해보면, 

swap에서는 3, 2, 1 / 3, 1, 2 순으로 출력이 되는데 visited에서는 3, 1, 2 / 3, 2, 1 순으로 출력이 된다.

따라서 swap을 이용해서 순열을 구했을 때는 순서대로 순열이 구해지지 않는다는 차이점을 발견할 수 있다. 

그리디(Greedy) 알고리즘 (탐욕 알고리즘)

참고 사이트 :

1. https://www.geeksforgeeks.org/greedy-algorithms/

2. https://ko.wikipedia.org/wiki/%ED%83%90%EC%9A%95_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98


그리디 알고리즘

  • 매 순간마다 최적의 값을 찾음
  • 매 순간의 최적값을 모두 더한것이 전체의 최적값이길 바람
  • 결과값이 항상 최적값이라고 보장할 수 없음
  • 탐욕스러운 선택 조건, 최적 부분 구조 조건이 만족해야만 최적해를 구할 수 있음
    탐욕스러운 선택 조건 : 앞서 선택한 것이 이후의 선택에 영향을 미치지 않음
    최적 부분 구조 조건 : 문제에 대한 최적해가 부분 문제에 대해서도 최적해가 됨
  • 조건이 만족하지 않으면 최적해를 구할 수 없지만, 근사 알고리즘으로 사용가능하며(근사값) 계산속도가 빨라 실용적으로 사용 가능
  • 그리디가 적용된 알고리즘 종류
    1) 크루스칼 알고리즘
    2) 프림 알고리즘
    3) 다익스트라 알고리즘
    4) 허프만 코딩 등...

 

 

예시) 활동 선택 문제

시작 및 종료 시간이 주어지며, 한 사람이 한 번에 하나의 활동만 수행할 수 있다고 가정했을 때 한 사람이 할 수 있는 최대 활동의 개수 구하기



시작시간 : [1, 3, 5, 7, 9, 13]
종료시간 : [4, 5, 7, 9, 12, 15]

최대 활동 개수 : 5개 [1~4, 5~7, 7~9, 9~12, 13~15]

활동 선택 문제 풀이

package Greedy;

public class ActivitySelectionProblem {
    public static void main(String[] args) {
        int[] start = {1, 3, 5, 7, 9, 13};
        int[] finish = {4, 5, 7, 9, 12, 15};
        // 활동 개수
        int n = start.length;
        int i=0;
        int count=0;

        // 다음 활동 시작 시간이 이전 활동의 끝나는 시간보다 클 경우를 구해야 하므로 j=1
        for(int j=1; j<n; j++){
            if (start[j] >= finish[i]){
                System.out.println(String.format("%d~%d", start[i], finish[i]));
                i = j;
                count++;
            }
        }
        // 마지막 시간은 반복문에서 제외됨
        if (start[i] >= finish[i-1]){
            System.out.println(String.format("%d~%d", start[i], finish[i]));
            count+=1;
        }

        System.out.println("최대 활동 개수 : " + count);
    }
}

 

작성예정...

오른쪽으로 가는것을 기준으로 잡고 풀었다. 좌, 우 이동만 가지고 얘기하자면, 현재 위치를 기준으로 왼쪽/오른쪽 거리를 계산했을 때, 한 번이라도 왼쪽 거리가 짧을 경우 name의 끝에서부터 빼면서 계산했다. 다른 사람의 풀이를 보면 엄청 쉽게 구현하던데 신기하다...

 

탐욕법은 매 순간마다 최적의 값을 찾는다. 탐욕법은 탐욕스러운 선택 조건과 최적 부분 구조 조건을 가지는 특징이 있는데, 탐욕스러운 선택 조건은 앞에서 내가 선택한 것이 이후의 선택에 어떠한 영향을 미치지 않는다는 조건이고 최적 부분 구조 조건은 문제에 대한 최적해가 부분 문제에 대해서도 최적해여야 한다는 특징이 있다. 

 

이 문제에서는 인덱스 0 위치에서 왼쪽으로 갈 경우 문자열의 끝에 갈 수 있지만 문자열의 끝에서 오른쪽으로 갈 경우 인덱스 0위치로 갈 수 없다. 그래서 아래와 같은 결과가 나오게 된다.

ABAAAABAAAAAAAAAAAAB => 최적해 (13) / 실제 계산한 해 (19)

탐욕법 자체가 이미 지나온 길을 되돌아갈 수 없다고 하면 탐욕법이 맞는거 같기도하고.. 아리송하다.

 

public int solution(String name) {
    int answer = 0;
    int notA_Count = 0;
    int curCount = 0;
    int lastIndex = 0;
    char curChar = 'A';
    boolean leftSmallerThanRight = false;

    // A가 아닌 개수 및 A가 아닌 마지막 인덱스 구하기
    for(int i=0; i<name.length(); i++){
        if (name.charAt(i) != 'A'){
            lastIndex = i;
            notA_Count++;
        }
    }

    // 오른쪽으로 갈 수 있는 최대 구하기
    // 인덱스 0을 기준으로 왼쪽으로 갔을 때 몇번째 값이 A가 아닌지 확인 (0의 위치에서 왼쪽으로 갈 경우 거리값이 +1이 되어야 함)
    int leftCount = name.length() - lastIndex;
    int rightCount = 0;
    int curPos = 0;
    int upDownCount = 0;
    for(int i=0; i<name.length(); i++){
        curChar = name.charAt(i);
        if (curChar == 'A'){
            continue;
        }

        // i 위치에서 현재 위치를 빼면 오른쪽으로 몇 번 가야 A가 아닌 문자가 나오는지 알 수 있음
        rightCount = i - curPos;

        // A가 아닌 문자를 만나기 위해 오른쪽으로 간 횟수와 왼쪽으로 간 횟수를 비교
        // 왼쪽으로 간 횟수가 더 적을 경우 빠져나와 맨 오른쪽에서 왼쪽으로 진행하기 위해 빠져나옴
        if (rightCount > leftCount + curPos){
            leftSmallerThanRight = true;
            break;
        }
        else{
            answer += rightCount;
        }

        // 알파벳 구하기
        upDownCount = (curChar - 'A' > 'Z' - curChar + 1)? 'Z' - curChar + 1 : curChar - 'A';
        answer += upDownCount;

        // 현재 위치를 i번째로 옮김
        curPos = i;
        rightCount = 0;
        curCount++;
    }

    // 현재 위치에서 왼쪽으로 가는게 더 빠른 경우가 있을 때
    // 현재 위치부터 왼쪽으로 구해야 하니까 leftCount는 curPos 값으로 변경
    leftCount = curPos;
    if (leftSmallerThanRight){
        for(int i=name.length() - 1; i>=0; i--){
            if (notA_Count == curCount){
                break;
            }

            curChar = name.charAt(i);
            // A면 한 칸 이동
            if (curChar == 'A'){
                leftCount++;
                continue;
            }
            leftCount++;

            upDownCount = (curChar - 'A' > 'Z' - curChar + 1)? 'Z' - curChar + 1 : curChar - 'A';
            answer += upDownCount;
            answer += leftCount;
            leftCount = 0;
            curCount++;
        }
    }

    return answer;
}

 

 

프로그래머스 Level 2의 전화번호 목록을 풀었다. Hash를 이용하라는데, 아무리봐도 안될 것 같아 그냥 이중 for문으로 작성했다.

import java.util.*;

class Solution {
    public boolean solution(String[] phone_book) {
        Arrays.sort(phone_book);
        
        for(int i=0; i<phone_book.length; i++){
            for(int j=i + 1; j<phone_book.length; j++){
                if (phone_book[j].indexOf(phone_book[i]) == 0){
                    return false;
                }
                else{
                    break;
                }
            }
        }

        return true;
    }
}

Arrays.sort 를 한 이유는 String을 정렬하면 사전순으로 정렬되기 때문이다. 생각해보니 for문 한 번으로도 될 것 같다. 위와 같이 풀었을 때의 테스트 결과이다.

다른 사람들의 풀이를 보니까 나처럼 푼 사람도 있었고 문제에서 얘기한 HashMap을 사용한 사람도 있었다. 아래는 HashMap을 사용한 사람의 코드이다. 

public boolean solution(String[] phone_book) {
        boolean answer = true;
        HashMap<String,Integer> h =new HashMap<>();
        for(String s : phone_book) {
            h.put(s, h.getOrDefault(s,0)+1);
        }

        for(String s : phone_book) {
            for(int j=1; j<s.length(); j++) {
                if(h.containsKey(s.substring(0,j))) {
                   answer = false; 
                   break;
                }              
            }
        }    



        return answer;
    }

phone_book 배열에서 문자열을 가져와 하나씩 잘라가면서 HashMap에 잘라진 문자열이 키로 존재하는지 확인하고 있다면 false를 반환하는 코드이다. 아래는 위 코드로 실행했을 때의 테스트 결과이다.

확실히 글자수가 적거나 개수가 적을 경우에는 HashMap을 이용하면 상당히 빠른 것 같다. 그런데 굳이 HashMap을 사용할 필요가 있을까 싶은 문제였다..

이 문제를 푸는데 시간이 좀 걸렸다. 

문제는 위와 같은데 단순히 10진수를 124 나라에서 사용하는 숫자로 바꾸는 문제이다. 1,2,4 세 개만 사용한다 했으니 10진수를 3진법으로 변환해봤다. 

10진법 3진법 10진법 3진법
1 1 11 102
2 2 12 110
3 10 13 111
4 11 14 112
5 12 15 120
6 20 16 121
7 21 17 122
8 22 18 200
9 100 19 201
10 101 20 202

 

그리고 3진법과 124나라의 숫자를 비교해봤다.

3진법 124나라 3진법 124나라
1 1 102 42
2 2 110 44
10 4 111 111
11 11 112 112
12 12 120 114
20 14 121 121
21 21 122 122
22 22 200 124
100 24 201 141
101 41 202 142

10진수 6을 3진법으로 변환 시 20이 되는데 이 20는 결국 124 나라의 14라는 숫자가 되어야 한다. 3진법에 의해 10진수 6이 3진수 20이 되었으니 결국 3진법으로 표현하면 13이 될 수 있다는 뜻이다. (3진수 13은 결국 3진수 20이 되므로) 이렇게 되면 3진수 20에서 2는 1이 되어야 하므로 -1 해주고 0은 3으로 바꿔야 한다. 하지만 124 나라에서는 1, 2, 4만 사용하기 때문에 결국 0은 4로 바꿔줘야 124 나라의 숫자가 된다. 

 

풀이순서

 1. 전달받은 숫자를 3으로 나눈다.

 2. 나머지가 0일 경우 4로 치환하고 몫을 -1 하고 나머지 값을 추가한다.

   2-1. 나머지가 0이 아닐 경우 그대로 값을 추가한다.

 3. 1번과 2번을 반복해서 숫자가 0이 될 때까지 반복한다. 

 

public String solution(int n) {
    StringBuilder sb = new StringBuilder();
    int rest = 0;

    while(n > 0){
        rest = n % 3;
        n /= 3;
        if (rest == 0){
            n -= 1;
            rest = 4;
        }

        sb.insert(0, rest);
    }

    return sb.toString();
}

 

 

 

해당 문제를 푸는데 좀 많은 시간이 소요되었다. 먼저 문제를 살펴보면 아래와 같다.

 

가로 길이가 Wcm, 세로 길이가 Hcm인 직사각형 종이가 있습니다. 종이에는 가로, 세로 방향과 평행하게 격자 형태로 선이 그어져 있으며, 모든 격자칸은 1cm x 1cm 크기입니다. 이 종이를 격자 선을 따라 1cm × 1cm의 정사각형으로 잘라 사용할 예정이었는데, 누군가가 이 종이를 대각선 꼭지점 2개를 잇는 방향으로 잘라 놓았습니다. 그러므로 현재 직사각형 종이는 크기가 같은 직각삼각형 2개로 나누어진 상태입니다. 새로운 종이를 구할 수 없는 상태이기 때문에, 이 종이에서 원래 종이의 가로, 세로 방향과 평행하게 1cm × 1cm로 잘라 사용할 수 있는 만큼만 사용하기로 하였습니다.
가로의 길이 W와 세로의 길이 H가 주어질 때, 사용할 수 있는 정사각형의 개수를 구하는 solution 함수를 완성해 주세요.

제한사항
  • W, H : 1억 이하의 자연수

즉, 8 x 12에서 흰색칸을 모두 지운 80이라는 값이 반환이 되어야 한다.

먼저 아래처럼 패턴을 나누었다. 패턴을 나누는 기준은 연속되지 않은 흰색 칸을 기준으로 했다.

그럼 패턴이 총 4개가 나오게 된다. 패턴의 길이는 가로 2, 세로 3인데, 이 값들은 가로와 세로를 각각 4로 나눈값이 된다. 하나 더 예시로 작성해 보았다. 

사용 못하는 칸이 모두 연속되어 있으므로 패턴이 1개이고 패턴의 길이는 가로 4, 세로 3이 된다. 패턴의 길이와 총 길이가 같으므로 패턴의 가로와 세로는 각각 1로 나눈 값과 동일하다. 이렇게 5x3, 6x4 등등의 길이의 종이를 위와 같은 방법으로 구해보았다. 

 

그 결과, 각 패턴의 길이는 종이의 가로, 세로값을 최대공약수로 나눈 값이 되며 패턴의 개수는 최대공약수의 값과 동일하다는 결론이 나왔다. 다시 위에 문제로 돌아가 패턴을 살펴보면,

패턴의 가로는 2, 세로는 3이 되는데 여기서 사용할 수 없는 칸은 총 4칸이다. 이 값 또한 가로와 세로를 더한 후 -1을 해주면 된다. 그 이유는 아래와 같다.

그림에서 볼 수 있듯이 3번을 위로, 4번을 왼쪽으로 붙이게 되면 패턴의 가로 + 세로 - 1 이 되기 때문에 사용하지 못하는 개수는 총 4개이다. 위에서 4x3 패턴의 예시도 동일하게 아래 그림처럼 가능하다. 

3, 5번을 왼쪽으로 붙이고 4, 6번을 위로 붙이게 되면 4 + 3 - 1 만큼의 개수를 사용하지 못하게 된다. 

위 내용을 코드로 작성하여 제출했다.

https://github.com/banjjak2/Programmers/blob/main/Level2/%EB%A9%80%EC%A9%A1%ED%95%9C_%EC%82%AC%EA%B0%81%ED%98%95.java

class Solution {
    public long solution(int w, int h) {
        long answer = 0;
        long totalCount = (long)w * h;
        // 패턴 총 개수
        int patternCount = gcd(w, h);

        // 1패턴당 가로 개수
        int widthPerPattern = w / patternCount;
        // 1패턴당 세로 개수
        int heightPerPattern = h / patternCount;
        // 1패턴당 못쓰는 개수
        int unusedCountPerPattern = widthPerPattern + heightPerPattern - 1;

        // 총개수 - (1패턴당 못쓰는 개수 * 패턴 총 개수);
        answer = totalCount - (unusedCountPerPattern * patternCount);
        return answer;
    }

    // 최대공약수 구하기
    public int gcd(int n, int m){
        // 나머지가 0이면
        if (m == 0){
            // 나눈값 반환
            return n;
        }

        // 나눈 수, 나머지 값
        return gcd(m, n % m);
    }
}

+ Recent posts