버블 정렬 (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);
    }
}

 

작성예정...

+ Recent posts