자바 코딩테스트 강좌, 배열과 리스트

1. 서론

프로그래밍 입문자 및 개발자 지망생들에게 배열과 리스트는 기본적인 자료구조입니다. 이 두 가지 자료구조를 이해하는 것은 다양한 코딩테스트 문제에서 뛰어난 성능을 발휘하기 위해 매우 중요합니다. 이 글에서는 자바를 사용하여 배열과 리스트를 활용한 알고리즘 문제를 하나 풀어보겠습니다.

2. 문제 설명

문제: 주어진 정수 배열에서 중복된 요소를 제거하고, 남은 요소들을 정렬하여 반환하는 메소드를 작성하시오. 결과 배열은 오름차순으로 정렬되어야 하며, 중복 값은 제거되어야 합니다.

문제 요약

  • 입력: 정수 배열
  • 출력: 중복을 제거한 후 오름차순으로 정렬된 배열

3. 예제

입력: [3, 1, 2, 3, 4, 2, 1]
출력: [1, 2, 3, 4]

4. 접근 방식

이 문제를 해결하기 위해 우리는 다음과 같은 단계를 따를 것입니다:

  • 단계 1: 입력 배열에서 중복된 요소를 제거합니다.
  • 단계 2: 남은 요소들을 정렬합니다.
  • 단계 3: 최종 결과를 반환합니다.

5. 코드 구현

이제 위의 접근 방식을 바탕으로 자바 코드를 작성해 보겠습니다.

        
import java.util.Arrays;
import java.util.HashSet;

public class RemoveDuplicatesAndSort {

    public static int[] removeDuplicatesAndSort(int[] arr) {
        // HashSet을 사용하여 중복 제거
        HashSet set = new HashSet<>();
        for (int num : arr) {
            set.add(num);
        }

        // 중복 제거한 요소를 배열로 변환
        int[] uniqueArray = new int[set.size()];
        int index = 0;
        for (int num : set) {
            uniqueArray[index++] = num;
        }

        // 배열 정렬
        Arrays.sort(uniqueArray);
        return uniqueArray;
    }

    public static void main(String[] args) {
        int[] input = {3, 1, 2, 3, 4, 2, 1};
        int[] result = removeDuplicatesAndSort(input);
        System.out.println(Arrays.toString(result)); // [1, 2, 3, 4]
    }
}
        
    

코드 설명

위 코드는 removeDuplicatesAndSort라는 메소드를 정의하고 있습니다. 이 메소드는 입력 배열에서 중복된 요소를 제거하고 정렬된 배열을 반환합니다.

  • 우선 HashSet을 사용하여 중복된 정수들을 간단히 제거합니다.
  • 그리고 HashSet의 내용을 새로운 배열로 복사합니다.
  • 마지막으로 Arrays.sort를 사용하여 배열을 정렬합니다.

6. 복잡도 분석

이 알고리즘의 시간 복잡도는 다음과 같습니다:

  • 중복 제거: O(n), 여기서 n은 입력 배열의 크기입니다.
  • 정렬: O(m log m), 여기서 m은 중복 제거된 배열의 크기입니다.

따라서 전체 시간 복잡도는 O(n + m log m)입니다.

7. 결론

이번 강좌에서는 자바를 사용하여 배열과 리스트를 활용한 중복 제거 및 정렬 알고리즘을 구현해 보았습니다. 각 단계를 통해 기본적인 자료구조의 작동 방식을 이해하고, 실력을 키워나가는 방법을 배웠습니다. 앞으로도 다양한 알고리즘 문제를 해결하면서 더 많은 경험을 쌓아가길 바랍니다.

참고 문헌

  • 자료구조 및 알고리즘 관련 서적
  • Java 공식 문서

8. 추가 연습 문제

다음과 같은 문제를 추가로 시도해 보세요.

  • 정렬된 배열이 주어졌을 때, 중복된 값을 제거하고 새로운 배열을 반환하는 메소드를 구현하시오.

코딩 연습을 통해 알고리즘 이해도를 높이고, 실력을 쌓으세요!

자바 코딩테스트 강좌, 미로 탐색하기

코딩 인터뷰 및 알고리즘 테스트는 여러 후보자들에게 흔히 있는 도전 과제입니다. 그 중 하나인 미로 탐색 문제는 매우 인기 있는 알고리즘 문제로, BFS(너비 우선 탐색)와 DFS(깊이 우선 탐색)와 같은 탐색 알고리즘을 배우기에 적합합니다. 본 글에서는 자바를 사용하여 미로를 탐색하는 문제를 살펴보고, 이를 해결하기 위한 단계별 접근 방법을 설명하겠습니다.

문제 설명

주어진 2차원 배열에서, 0은 이동할 수 있는 공간을, 1은 벽을 나타냅니다. 시작 지점에서 도착 지점까지의 경로를 찾는 것입니다. 경로가 존재하면 그 경로를 출력하고, 존재하지 않으면 “경로가 없습니다.”라고 출력합니다.

문제 예시

    입력:
    [
        [0, 1, 0, 0, 0],
        [0, 1, 0, 1, 0],
        [0, 0, 0, 1, 0],
        [0, 1, 0, 0, 0],
        [0, 0, 0, 1, 0]
    ]
    시작 지점 (0, 0)
    도착 지점 (4, 4)

    출력:
    (0, 0) -> (1, 0) -> (2, 0) -> (2, 1) -> (2, 2) -> (3, 2) -> (4, 2) -> (4, 3) -> (4, 4)
    

해결 과정

1. 문제 이해하기

우선적으로 문제를 정확하게 이해해야 합니다. 미로를 표현하는 2차원 배열에서, 시작 지점에서 출발해 도착 지점까지 가는 경로를 찾아야 하며, 벽(1)을 지나서는 안 됩니다. 위의 예시에서는 시작 지점에서 도착 지점까지 어떤 경로를 통해 갈 수 있는지를 알아내야 합니다.

2. 탐색 알고리즘 선택하기

미로 탐색은 BFS 또는 DFS로 해결할 수 있습니다. BFS는 최단 경로를 찾는 데 유리하며, DFS는 경로를 깊게 탐색하는 데 유리합니다. 여기서는 BFS를 이용한 해결 방법을 선택하겠습니다.

3. 알고리즘 설계

BFS 알고리즘을 사용하여 다음과 같은 절차로 경로를 탐색할 것입니다:

  1. 시작 지점에서 이웃한 지점을 큐에 추가합니다.
  2. 큐에서 지점을 하나씩 꺼내면서, 해당 지점이 도착 지점인지 확인합니다.
  3. 도착 지점이 아닌 경우, 그 지점의 이웃한 지점들을 큐에 추가합니다.
  4. 모든 경로를 탐색해도 도착 지점에 도달하지 못했다면, “경로가 없습니다.”라고 출력합니다.

4. 자바 코드 구현하기

    import java.util.LinkedList;
import java.util.Queue;

public class MazeSolver {
    static class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    static int[] dx = {-1, 1, 0, 0};
    static int[] dy = {0, 0, -1, 1};

    public static void main(String[] args) {
        int[][] maze = {
            {0, 1, 0, 0, 0},
            {0, 1, 0, 1, 0},
            {0, 0, 0, 1, 0},
            {0, 1, 0, 0, 0},
            {0, 0, 0, 1, 0}
        };
        
        findPath(maze, new Point(0, 0), new Point(4, 4));
    }

    static void findPath(int[][] maze, Point start, Point end) {
        int n = maze.length;
        int m = maze[0].length;
        boolean[][] visited = new boolean[n][m];
        Point[][] previous = new Point[n][m];

        Queue queue = new LinkedList<>();
        queue.offer(start);
        visited[start.x][start.y] = true;

        while (!queue.isEmpty()) {
            Point current = queue.poll();
        
            if (current.x == end.x && current.y == end.y) {
                printPath(previous, start, end);
                return;
            }

            for (int i = 0; i < 4; i++) {
                int nx = current.x + dx[i];
                int ny = current.y + dy[i];

                if (nx >= 0 && nx < n && ny >= 0 && ny < m && !visited[nx][ny] && maze[nx][ny] == 0) {
                    visited[nx][ny] = true;
                    previous[nx][ny] = current;
                    queue.offer(new Point(nx, ny));
                }
            }
        }

        System.out.println("경로가 없습니다.");
    }

    static void printPath(Point[][] previous, Point start, Point end) {
        StringBuilder path = new StringBuilder();
        for (Point at = end; at != null; at = previous[at.x][at.y]) {
            path.insert(0, String.format("-> (%d, %d) ", at.x, at.y));
        }
        System.out.println(path.substring(4)); // 처음의 "-> " 제거
    }
}

    

5. 코드 설명

위의 코드에서는 BFS 알고리즘을 구현하였습니다. Point라는 내부 클래스를 사용해 (x, y) 좌표를 정의하여 큐와 경로 추적에 활용합니다. 큐에는 현재 위치의 이웃 좌표를 추가하며, 방문한 지점은 visited 배열에서 체크하여 중복 탐색을 방지합니다. 도착 지점에 도달하면, printPath 메서드가 호출되어 경로를 출력합니다.

6. 코드 실행 결과

    (0, 0) -> (1, 0) -> (2, 0) -> (2, 1) -> (2, 2) -> (3, 2) -> (4, 2) -> (4, 3) -> (4, 4)
    

마무리

이제 미로 탐색 문제를 해결하는 방법에 대해 배우셨습니다. 이 알고리즘은 다양한 문제에 적용할 수 있는 유용한 기술입니다. 문제를 해결하기 위해 탐색 알고리즘의 기초를 이해하고, 이를 자바로 구현하는 과정은 매우 유익한 경험이 될 것입니다. 여러분의 코딩 테스트 준비에 도움이 되길 바랍니다!

자바 코딩테스트 강좌, 물의 양 구하기

알고리즘 문제는 코딩 테스트에서 매우 중요합니다. 특히 자바를 사용하여 문제를 해결하는 능력은
많은 기업에서 요구하는 스킬 중 하나입니다. 본 글에서는 “물의 양 구하기”라는 주제를 가지고
자세히 설명하겠습니다. 이 문제를 통해 알고리즘 해법을 찾고, 자바로 구현하는 과정을 단계별로
살펴보겠습니다.

문제 설명

여러분은 한 주어지는 높이의 배열을 가지고 있습니다. 각 인덱스는 블록을 나타내며, 인덱스의 값은
블록의 높이를 의미합니다. 이 배열에서 주어진 비에 의해 저장될 수 있는 물의 양을 계산하는
프로그램을 작성해야 합니다.

예를 들어, 주어진 배열이 [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]이라면, 배열에서 저장되는
물의 양을 구해야 합니다.

문제 풀이 접근 방식

이 문제를 해결하기 위한 접근 방식은 여러 가지가 있지만, 가장 직관적인 방법은 두 포인터
접근법
을 사용하는 것입니다. 두 포인터를 사용하여 배열의 양쪽 끝에서부터 중앙으로
이동하면서 각 인덱스에 저장될 수 있는 물의 양을 계산할 수 있습니다.

1단계: 문제 이해하기

인덱스마다 저장되는 물의 양은 해당 인덱스의 왼쪽과 오른쪽에서 가장 높은 블록의 높이에 의해
결정됩니다. 따라서 저장된 물의 양은
min(왼쪽 최대 높이, 오른쪽 최대 높이) - 현재 블록의 높이입니다. 만약 이 값이
음수라면 물이 저장되지 않으므로 0으로 설정할 수 있습니다.

2단계: 알고리즘 설계

알고리즘을 설계하기 위해 다음과 같은 단계를 설정합니다:

  1. 배열의 길이를 구하여 사용자에게 예외 처리합니다.
  2. 양쪽 끝에서 시작하여 두 포인터를 설정합니다.
  3. 각 포인터에서 최대 높이를 추적합니다.
  4. 두 포인터가 만날 때까지 반복합니다.
  5. 각 단계에서 저장된 물의 양을 계산합니다.

3단계: 코드 구현

이제 위의 설계를 바탕으로 자바 코드를 구현해 보겠습니다.

public class WaterTrapping {
    public static int calculateWater(int[] height) {
        if (height == null || height.length == 0) {
            return 0;
        }

        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        int totalWater = 0;

        while (left < right) {
            if (height[left] < height[right]) {
                if (height[left] >= leftMax) {
                    leftMax = height[left];
                } else {
                    totalWater += leftMax - height[left];
                }
                left++;
            } else {
                if (height[right] >= rightMax) {
                    rightMax = height[right];
                } else {
                    totalWater += rightMax - height[right];
                }
                right--;
            }
        }

        return totalWater;
    }

    public static void main(String[] args) {
        int[] height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
        System.out.println("저장된 물의 양: " + calculateWater(height) + " 단위");
    }
}
        

4단계: 코드 설명

위의 코드에서 핵심은 while (left < right) 루프입니다. 이 반복문은 두 포인터가
서로 교차할 때까지 계속해서 반복됩니다.

  • 왼쪽 포인터가 오른쪽 포인터보다 작은 경우: 왼쪽의 최대 높이를 업데이트하거나,
    현재 값이 낮으면 저장할 수 있는 물의 양을 계산합니다.
  • 오른쪽 포인터가 왼쪽 포인터보다 작은 경우: 오른쪽의 최대 높이를 업데이트하거나,
    저장할 수 있는 물의 양을 계산합니다.

이렇게 하여 두 포인터가 만날 때까지 반복하면서 저장된 물의 양을 합산하게 됩니다.

결론

오늘은 “물의 양 구하기” 문제를 통해 코딩 테스트에서 자주 출제되는 알고리즘을 학습했습니다.
두 포인터를 사용하는 알고리즘은 효율적인 공간 활용과 시간 복잡도를 제공하기 때문에,
실제 코딩 테스트에서 많이 사용되므로 충분히 연습하시길 바랍니다.

다양한 추가적인 예제와 함께 복잡한 테스트 케이스를 통해 더욱 깊이 있는 학습을 이어가시길
바랍니다. 이 문제를 통해 알고리즘적 사고가 깊어지기를 희망합니다.

자바 코딩테스트 강좌, 문자열 찾기

문제 설명

주어진 문자열에서 특정 패턴의 문자열이 몇 번 등장하는지를 찾는 문제입니다.
예를 들어, 문자열 "banana"에서 패턴 "ana"가 몇 번 나타나는지를 구하는 것입니다.

입력

  • 문자열 text: 검색할 전체 문자열 (1 ≤ |text| ≤ 100,000)
  • 문자열 pattern: 찾을 문자열 (1 ≤ |pattern| ≤ 50)

출력

문자열 text에서 pattern이 나타나는 총 횟수를 반환합니다.

예제

입력

text = "banana"
pattern = "ana"

출력

2

문제 풀이 과정

이 문제를 해결하기 위해서는 문자열 검색 알고리즘을 사용해야 합니다.
가장 간단한 방법은 문자열을 한 글자씩 순회하며 패턴을 비교하는 방법이지만,
이는 효율적이지 않아 대규모 데이터에서 시간 복잡도가 O(n*m)으로 비효율적입니다.

효율적인 해결책: KMP 알고리즘

문자열 검색 문제를 효율적으로 해결할 수 있는 방법 중 하나는 KMP(Knuth-Morris-Pratt) 알고리즘을 사용하는 것입니다.
이 알고리즘은 다음과 같은 두 부분으로 구성되어 있습니다:

  1. 패턴을 기반으로 한 ‘LPS(Longest Prefix which is also Suffix)’ 배열을 생성합니다.
  2. 텍스트를 순회하며 패턴을 비교하면서 LPS 배열을 활용합니다.

LPS 배열 생성

LPS 배열은 패턴 내에서 접두사와 접미사가 얼마나 일치하는지를 나타냅니다. 이를 통해 패턴을 재사용하면서 검색할 수 있습니다.
예를 들어, 패턴이 "ABAB"이라면 LPS 배열은 [0, 0, 1, 2]입니다.

알고리즘 구현

이제 위 과정을 바탕으로 문자열 찾기 알고리즘을 자바로 구현해보겠습니다.

public class KMP {
    public static int[] computeLPS(String pattern) {
        int[] lps = new int[pattern.length()];
        int length = 0; 
        int i = 1;
        
        while (i < pattern.length()) {
            if (pattern.charAt(i) == pattern.charAt(length)) {
                length++;
                lps[i] = length;
                i++;
            } else {
                if (length != 0) {
                    length = lps[length - 1];
                } else {
                    lps[i] = 0;
                    i++;
                }
            }
        }
        return lps;
    }

    public static int KMPsearch(String text, String pattern) {
        int[] lps = computeLPS(pattern);
        int i = 0; 
        int j = 0; 
        int count = 0;

        while (i < text.length()) {
            if (pattern.charAt(j) == text.charAt(i)) {
                i++;
                j++;

                if (j == pattern.length()) {
                    count++;
                    j = lps[j - 1];
                }
            } else {
                if (j != 0) {
                    j = lps[j - 1];
                } else {
                    i++;
                }
            }
        }
        return count;
    }

    public static void main(String[] args) {
        String text = "banana";
        String pattern = "ana";
        int result = KMPsearch(text, pattern);
        System.out.println("Substring Count: " + result);
    }
}

코드 설명

위 코드는 KMP 알고리즘을 구현한 것으로 두 개의 주요 함수가 있습니다.
computeLPS 함수는 패턴에 대한 LPS 배열을 생성하고, KMPsearch 함수는 텍스트에서 패턴을 검색하며 일치하는 개수를 세는 역할을 합니다.

복잡도 분석

KMP 알고리즘의 시간 복잡도는 O(n + m)입니다. 여기서 n은 텍스트의 길이, m은 패턴의 길이입니다.
이는 패턴이 텍스트 내에서 이동하고 재사용되기 때문에 가능한 효율적인 방법입니다.

마무리

오늘은 문자열 검색 문제를 해결하기 위한 KMP 알고리즘에 대해 살펴보았습니다.
이 알고리즘을 사용하면 다양한 문자열 검색 문제를 효율적으로 해결할 수 있습니다.
문제를 접하면서 다양한 알고리즘을 시도해 보시고 실력을 쌓아보세요!

자바 코딩테스트 강좌, 리프 노드의 개수 구하기

현대의 소프트웨어 개발에서 알고리즘과 데이터 구조는 매우 중요한 부분을 차지합니다. 특히, 코딩테스트에서 자주 등장하는 문제 유형 중 하나가 트리 관련 문제입니다. 이 글에서는 리프 노드(Leaf Node)의 개수를 구하는 문제를 다룰 것입니다.

문제 정의

주어진 이진트리의 리프 노드의 개수를 구하는 문제입니다. 리프 노드는 자식이 없는 노드를 의미하며, 이진트리의 특성을 감안할 때, 리프 노드는 최대 2개의 자식을 가질 수 있습니다. 이 문제를 통해 재귀적 사고와 트리 탐색 알고리즘을 익히는 데 도움이 될 것입니다.

문제 설명

        // 이진 트리 노드 정의
        class TreeNode {
            int val;
            TreeNode left;
            TreeNode right;
            TreeNode(int x) { val = x; }
        }
        
        // 리프 노드의 개수를 구하는 메소드
        int countLeafNodes(TreeNode root);
    

입력: 이진트리의 루트 노드(root).
출력: 리프 노드의 개수.

알고리즘 접근 방식

리프 노드를 찾기 위한 가장 일반적인 방법은 트리를 탐색하여 자식 노드가 없는 노드를 카운트하는 것입니다. 이 문제는 재귀적 접근을 통해 쉽게 해결할 수 있습니다.

재귀적 접근

재귀적으로 트리를 탐색하면서 각 노드에 대해 다음을 수행합니다:

  1. 현재 노드가 null이면, 0을 반환합니다.
  2. 현재 노드가 리프 노드인지 확인합니다. (왼쪽, 오른쪽 자식이 모두 null이라면)
  3. 리프 노드라면 1을 반환하고, 아니라면 왼쪽 서브트리와 오른쪽 서브트리에서 리프 노드의 개수를 각각 구하고 합칩니다.

구현

        public class Main {
            public static void main(String[] args) {
                TreeNode root = new TreeNode(1);
                root.left = new TreeNode(2);
                root.right = new TreeNode(3);
                root.left.left = new TreeNode(4);
                root.left.right = new TreeNode(5);
                root.right.right = new TreeNode(6);

                System.out.println("리프 노드의 개수: " + countLeafNodes(root));
            }

            public static int countLeafNodes(TreeNode root) {
                if (root == null) {
                    return 0;
                }
                if (root.left == null && root.right == null) {
                    return 1;
                }
                return countLeafNodes(root.left) + countLeafNodes(root.right);
            }
        }
    

예시

아래와 같이 이진 트리가 구성되어 있다고 가정해보겠습니다:

                1
              /   \
             2     3
            / \     \
           4   5     6
    

위 트리에서 리프 노드는 4, 5, 6이며, 따라서 리프 노드의 개수는 3입니다.

테스트 및 검증

구현한 메소드를 테스트하기 위해 여러 다양한 이진트리를 만들고 리프 노드의 개수를 검증합니다. 예를 들어, 노드가 1개인 트리, 왼쪽 / 오른쪽 편향 트리, 빈 트리 등을 만들어 테스트합니다.

테스트 케이스

        // 단일 노드 트리
        TreeNode singleNode = new TreeNode(1);
        assert countLeafNodes(singleNode) == 1;

        // 빈 트리
        assert countLeafNodes(null) == 0;

        // 왼쪽 편향 트리
        TreeNode leftSkewedTree = new TreeNode(1);
        leftSkewedTree.left = new TreeNode(2);
        leftSkewedTree.left.left = new TreeNode(3);
        assert countLeafNodes(leftSkewedTree) == 1;

        // 오른쪽 편향 트리
        TreeNode rightSkewedTree = new TreeNode(1);
        rightSkewedTree.right = new TreeNode(2);
        rightSkewedTree.right.right = new TreeNode(3);
        assert countLeafNodes(rightSkewedTree) == 1;
    

복잡도 분석

시간 복잡도는 O(N)입니다. 모든 노드를 한번씩 방문해야 하므로, N은 노드의 수입니다. 공간 복잡도는 O(h)로, 여기서 h는 트리의 높이입니다. 최악의 경우(편향 트리) h는 N과 같을 수 있습니다.

결론

이 글에서 우리는 리프 노드의 개수를 구하는 알고리즘 문제를 다뤘습니다. 재귀적 사고를 통해 트리를 탐색하고, 기본적인 이진트리 개념을 이해하는 데 도움이 되었기를 바랍니다. 알고리즘 문제를 해결하는 과정에서, 항상 문제를 어떻게 나누고 해결할지를 고려해야 합니다. 다양한 트리 구조에 대해 연습해보는 것도 중요합니다.