https://school.programmers.co.kr/learn/courses/30/lessons/12909

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

Stack 을 활용할 수 있는 문제였다.
stack이 비어있는 것을 체크하고
')' 가 들어올때 '(' 가 없으면 break를 걸어
false 를 return 했다.

import java.util.*;

class Solution {
    boolean solution(String s) {
        boolean answer = true;

        Stack<Character> stack = new Stack<>();
        
        for(int i = 0; i < s.length(); i++){
            char ch = s.charAt(i);
            
            if(ch == '(') {
                stack.push('(');
            }
            else if(ch == ')'){
                if(!stack.isEmpty()){
                    if(stack.peek() == '('){
                        stack.pop();
                    }
                }
                else {
                    answer = false;
                    break;
                }                
            }
        }
        
        if(!stack.isEmpty())
            answer = false;
        
        return answer;
    }
}

https://school.programmers.co.kr/learn/courses/30/lessons/12951

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

문자열에서 첫번째 문자는 항상 대문자
두번째 문자는 항상 소문자
마지막은 공백 추가
이 로직 대로 StringBuilder 에 append 해주었고,

시간을 생각보다 소비했던건 테스트케이스 8번에서 "3people unFollowed me " 이런식으로
입력값 s 문자열에도 마지막 공백이 추가되어서 이런 경우를 따로 if문 처리해서 따로 sb.append(" ")
처리를 해주어야 했다.

class Solution {
    public String solution(String s) {
        String[] arr = s.split(" ");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            if (arr[i].length() == 0) {
                sb.append(" ");
            } else {
                sb = sb.append(arr[i].substring(0, 1).toUpperCase());
                sb = sb.append(arr[i].substring(1, arr[i].length()).toLowerCase());
                sb = sb.append(" ");
            }
        }

        sb.delete(sb.length() - 1, sb.length());

        if (s.substring(s.length() - 1, s.length()).equals(" ")) {
            sb.append(" ");
        }
        
        return sb.toString();
    }
}

코드 설명

문제 포인트는
1. 3개의 벽을 세울 수 있는 모든 경우의 수를 구해서 새롭게 복사한 COPY_MAP을 각각 만드는 것
여기서 세울 수 있는 경우의 수가 엄청 많은데 이를 하나하나 다 BFS를 돌려봐야 한다.
2. 1번에서 새운 MAP 에서 BFS 돌려서 안전거리의 영역을 카운트해서 그 안전거리의 최대값을 출력

DFS와 BFS 를 동시에 연습할수 있는 아주 좋은 문제였다.
 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {

    static int N, M;
    static int[][] map;
    static int[] dy = {1, 0, -1, 0};
    static int[] dx = {0, 1, 0, -1};
    static int ans = Integer.MIN_VALUE;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        map = new int[N][M];

        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < M; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        DFS(0);

        System.out.println(ans);
    }

    public static void DFS(int cnt) {
        if (cnt == 3) {
            BFS();
            return;
        }

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (map[i][j] == 0) {
                    map[i][j] = 1;
                    DFS(cnt + 1);
                    map[i][j] = 0;
                }
            }
        }
    }

    public static void BFS() {
        // 벽을 새로 새울떄마다 배열 초기화
        int[][] copy_map = new int[N][M];

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                copy_map[i][j] = map[i][j];
            }
        }
        Queue<Node> qu = new LinkedList<>();

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (copy_map[i][j] == 2) {
                    qu.add(new Node(i, j));
                }
            }
        }

        while (!qu.isEmpty()) {
            Node curNode = qu.poll();

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

                if (ny < 0 || nx < 0 || N <= ny || M <= nx) continue;

                if (copy_map[ny][nx] == 0) {
                    copy_map[ny][nx] = 2;
                    qu.add(new Node(ny, nx));
                }


            }

        }
        int cnt = 0;

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                if (copy_map[i][j] == 0) {
                    cnt++;
                }
            }
        }
        ans = Math.max(cnt, ans);
    }


    public static class Node {
        int y;
        int x;

        public Node(int y, int x) {
            this.y = y;
            this.x = x;
        }
    }

}

https://www.acmicpc.net/problem/14500

 

14500번: 테트로미노

폴리오미노란 크기가 1×1인 정사각형을 여러 개 이어서 붙인 도형이며, 다음과 같은 조건을 만족해야 한다. 정사각형은 서로 겹치면 안 된다. 도형은 모두 연결되어 있어야 한다. 정사각형의 변

www.acmicpc.net

 

코드 설명

테트리스의 모양처럼 생긴 블록들이 알고보면 DFS의 방향대로 탐색할때의 모양이라는 것을 알아낼 수 있었다.
DFS를 visited배열을 true 해주면서 depth가 4일때까지 돌리고 빠져나오면서 visited 배열을 false 해주는 백트래킹 방식으로 구현하였다.

여기서 핵심인 부분은 'ㅏ,ㅗ,ㅜ,ㅓ' 에 대한 블록을 어떻게 처리해줄까였는데
나는 cnt == 2 일때 arr[ny][nx] 의 값을 더해준 뒤 다른 ny,nx가 아닌 다른 방향으로 DFS를 한번 더 돌려서
sum 값을 계산하였다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static int N, M;
    static int[][] arr;
    static boolean[][] visited;
    static int[] dy = {1, -1, 0, 0};
    static int[] dx = {0, 0, 1, -1};
    static int ans = Integer.MIN_VALUE;

    public static void main(String[] agrs) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        arr = new int[N][M];
        visited = new boolean[N][M];

        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < M; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < M; j++) {
                visited[i][j] = true;
                DFS(i, j, 0, 0);
                visited[i][j] = false;
            }
        }
        System.out.println(ans);
    }

    public static void DFS(int y, int x, int sum, int cnt) {
        if (cnt == 4) {
            ans = Math.max(sum, ans);
            return;
        }

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

            if (ny < 0 || nx < 0 || N <= ny || M <= nx)
                continue;

            if (visited[ny][nx])
                continue;

            if (cnt == 2) {
                visited[ny][nx] = true;
                DFS(y, x, sum + arr[ny][nx], cnt + 1);
                visited[ny][nx] = false;
            }

            visited[ny][nx] = true;
            DFS(ny, nx, sum + arr[ny][nx], cnt + 1);
            visited[ny][nx] = false;
        }

    }
}

https://www.acmicpc.net/problem/15686

 

15686번: 치킨 배달

크기가 N×N인 도시가 있다. 도시는 1×1크기의 칸으로 나누어져 있다. 도시의 각 칸은 빈 칸, 치킨집, 집 중 하나이다. 도시의 칸은 (r, c)와 같은 형태로 나타내고, r행 c열 또는 위에서부터 r번째 칸

www.acmicpc.net

코드 설명

BFS로 모든 경로를 다 찾으려고 하면 당연히 시간초과가 나버린다.
집에서 치킨집의 조합의 경우의 수 만큼 거리를 백트래킹으로 구하여 그것의 최소값을 구하면 된다.
(이 아이디어를 생각해내는데 꽤 오래걸렸다..)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;

public class Main {
    static int N, M;
    static int[][] arr;
    static boolean[] visited;
    static ArrayList<Node> chickenList = new ArrayList<>();
    static ArrayList<Node> houseList = new ArrayList<>();
    static int answer = Integer.MAX_VALUE;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        arr = new int[N + 1][N + 1];


        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());

            for (int j = 0; j < N; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());

                if (arr[i][j] == 1)
                    houseList.add(new Node(i, j));

                if (arr[i][j] == 2)
                    chickenList.add(new Node(i, j));
            }
        }

        visited = new boolean[chickenList.size()];

        DFS(0, 0);
        System.out.println(answer);
    }

    public static void DFS(int idx, int cnt) {
        if (cnt == M) {

            int res = 0;

            for (int i = 0; i < houseList.size(); i++) {
                int sum = Integer.MAX_VALUE;
                for (int j = 0; j < chickenList.size(); j++) {
                    if (visited[j]) {
                        // 각 치킨집에서 집까지의 거리의 최소값
                        int distance = Math.abs(chickenList.get(j).y - houseList.get(i).y) + Math.abs(chickenList.get(j).x - houseList.get(i).x);
                        sum = Math.min(sum, distance);
                    }
                }
                res += sum;
            }
            answer = Math.min(answer, res);
            return;
        }

        for (int i = idx; i < chickenList.size(); i++) {
            visited[i] = true;
            DFS(i + 1, cnt + 1);
            visited[i] = false;
        }
    }

    public static class Node {
        int x;
        int y;

        public Node(int y, int x) {
            this.x = x;
            this.y = y;

        }
    }
}

https://www.acmicpc.net/problem/3190

 

코드 설명

이중 for문에서 빼낼려고 이상한 짓 하다가 시간을 많이 잡아먹었던 문제였다.

 

3190번: 뱀

'Dummy' 라는 도스게임이 있다. 이 게임에는 뱀이 나와서 기어다니는데, 사과를 먹으면 뱀 길이가 늘어난다. 뱀이 이리저리 기어다니다가 벽 또는 자기자신의 몸과 부딪히면 게임이 끝난다. 게임

www.acmicpc.net

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

    static Deque<int[]> qu = new LinkedList<>();
    static HashMap<Integer, String> map = new HashMap<>();
    static boolean[][] apple;
    static int[] dy = {0, 1, 0, -1};
    static int[] dx = {1, 0, -1, 0};
    static int N, K, L;
    static int direction = 0;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;

        N = Integer.parseInt(br.readLine());
        K = Integer.parseInt(br.readLine());

        apple = new boolean[101][101];

        // 사과 맵
        for (int i = 0; i < K; i++) {
            st = new StringTokenizer(br.readLine());

            int y = Integer.parseInt(st.nextToken());
            int x = Integer.parseInt(st.nextToken());

            apple[y][x] = true;
        }

        L = Integer.parseInt(br.readLine());

        //
        for (int i = 0; i < L; i++) {
            st = new StringTokenizer(br.readLine());

            int X = Integer.parseInt(st.nextToken());
            String C = st.nextToken();
            map.put(X, C);
        }

        // 이중 포문 break를 사용하지 않기 위한?
        solve();
    }

    private static void solve() {
        int time = 0;

        int curY = 1;
        int curX = 1;

        qu.add(new int[]{1, 1});

        while (true) {
            int ny = curY + dy[direction];
            int nx = curX + dx[direction];

            // 맵 범위에 있는지, 몸통이랑 부딪혔는지 확인
            if (GameoverCheck(ny, nx))
                break;

            time++;

            qu.add(new int[]{ny, nx});

            // 사과가 있으면
            if (apple[ny][nx])
                apple[ny][nx] = false;
                // 사과가 없으면
            else {
                qu.poll();
            }

            if (map.containsKey(time)) {

                if (map.get(time).equals("D")) {
                    direction++;
                    if(direction == 4)
                        direction = 0;
                } else {
                    direction--;
                    if(direction == -1)
                        direction = 3;
                }
            }

            curY = ny;
            curX = nx;
        }
        System.out.println(time + 1);
    }

    private static boolean GameoverCheck(int ny, int nx) {
        boolean flag = false;

        // 범위에 벗어나면
        if (ny < 1 || nx < 1 || N < ny || N < nx)
            flag = true;

        // 몸통에 부딪히면
        for (int[] arr : qu) {

            if (arr[0] == ny && arr[1] == nx) {

                flag = true;
                break;
            }
        }

        return flag;
    }
}

https://school.programmers.co.kr/learn/courses/30/lessons/12927

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

우선순위큐의 개념만 이해하고 있다면 어렵지 않은 문제였었다. 

더보기

Priority Queue

PriorityQueue란 우선순위 큐로써 일반적인 큐의 구조 FIFO(First In First Out)를 가지면서, 데이터가 들어온 순서대로 데이터가 나가는 것이 아닌 우선순위를 먼저 결정하고 그 우선순위가 높은 데이터가 먼저 나가는 자료구조이다.

PriorityQueue를 사용하기 위해선 우선순위 큐에 저장할 객체는 필수적으로 Comparable Interface를 구현해야한다.

Comparable Interface를 구현하면 compareTo method를 override 하게 되고 해당 객체에서 처리할 우선순위 조건을 리턴해주면 PriorityQueue 가 알아서 우선순위가 높은 객체를 추출 해준다.

PriorityQueue는 Heap을 이용하여 구현하는 것이 일반적이다.

데이터를 삽입할 때 우선순위를 기준으로 최대 힙 혹은 최소 힙을 구성하고 데이터를 꺼낼 때 루트 노드를 얻어낸 뒤 루트 노드를 삭제할 때는 빈 루트 노드 위치에 맨 마지막 노드를 삽입한 후 아래로 내려가면서 적절한 자리를 찾아 옮기는 방식으로 진행된다.
최대 값이 우선순위인 큐 = 최대 힙, 최소 값이 우선순위인 큐 = 최소 힙

남은 작업량들의 제곱의 합이 야근 지수기 때문에 가장 높은 야근 지수를 N 시간 만큼 -1 해주면 된다.
우선순위큐는 내부적으로 Queue에서 우선순위가 높은 데이터가 먼저 나가기 때문에 poll 할때마다 가장 높은 데이터가 가장 높은 우선순위로 옮겨진다. 이러한 특징을 이용하여 풀면 아래 코드와 같이 구현할 수 있다.

전체 코드

import java.util.Comparator;
import java.util.PriorityQueue;

class Solution {

    public long solution(int n, int[] works) {
        long answer = 0;

        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder());
        for (int work : works) {
            pq.add(work);
        }

        while (n > 0) {
            int max = pq.poll();
            pq.add(--max);
            n--;

            if(pq.peek() == 0 && n > 0) break;
        }

        for (Integer num : pq) {
            answer += Math.pow(num, 2);
        }
        
        if(n != 0) answer = 0;

        return answer;
    }
}

https://school.programmers.co.kr/learn/courses/30/lessons/43163

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

DFS를 최소한으로 돌아서 target 이랑 같아지는 경우를 확인하면 된다.

문자열에서 하나의 문자를 제외하고 나머지 문자열은 일치 해야하므로
k 에 문자열 -1이 저장되는 경우

        int k = 0;
        for (int j = 0; j < target.length(); j++) {
            if (begin.charAt(j) == words[i].charAt(j)) {
                k++;
            }
        }

방문 체크를 해주고 cnt + 1 을 해주면서 다시 DFS 한단계 더 깊게 들어가서 target이랑 일치하는 문자열이
나올때까지 확인해주면 된다.

            // 하나 빼고 다 일치하면
            if (k == target.length() - 1) {
                visited[i] = true;
                DFS(words[i], target, words, cnt + 1);
                visited[i] = false;
            }

 

전체 코드

class Solution {

    static int answer = 0;
    static boolean visited[];

    public int solution(String begin, String target, String[] words) {

        int words_cnt = words.length;
        visited = new boolean[words.length];


        DFS(begin, target, words, 0);


        return answer;
    }

    public void DFS(String begin, String target, String[] words, int cnt) {
        if (begin.equals(target)){
            answer = cnt;
            return;
        }

        for (int i = 0; i < words.length; i++) {
            // 방문했으면 continue;
            if (visited[i])
                continue;

            int k = 0;
            for (int j = 0; j < target.length(); j++) {
                if (begin.charAt(j) == words[i].charAt(j)) {
                    k++;
                }
            }

            // 하나 빼고 다 일치하면
            if (k == target.length() - 1) {
                visited[i] = true;
                DFS(words[i], target, words, cnt + 1);
                visited[i] = false;
            }
        }
    }
}

 

| 프로그래머스 불량 사용자

https://school.programmers.co.kr/learn/courses/30/lessons/64064

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

코드 설명

불량사용자의 조합이 여러가지 나올 수 있기 때문에 중복을 허용하지 않는 set 을 사용하여 불량사용자 조건에 해당되는 user들을 set에 모두 중복, 순서에 상관없이 add 해주는 방법으로 문제를 구현하였다.

유저 아이디(str1)가 불량사용자의 아이디(str2)에 해당이 되는지 검증하여 true/false 로 나타내는 함수이다.
첫번째 if문에서는 길이를 비교하여 길이가 맞지 않으면 false를 return 해주었고, 그아래 코드에선
각 문자를 비교하여 * 이 아니거나 문자열이 일치하지 않는 경우 false를 return 해주었다.

    public boolean check(String str1, String str2) {
        boolean flag = true;

        if (str1.length() != str2.length()) {
            flag = false;
            return flag;
        }

        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i) && str2.charAt(i) != '*') {
                flag = false;
                return flag;
            }
        }

        return flag;
    }

여기서 백트래킹 방법으로 HashSet에 다가 해당 되는 userId를 계속 add 해주었다.
유저 수 만큼 for문을 돌려서 해당 유저가 불량 사용자 조건에 해당 되는지를 체크하여 해당된다고 하면 set에 add를 해주었다.

여기서 헷갈렸던 포인트는 
DFS(new HashSet<> (set), depth + 1); // 객체 자체를 매개변수로 넘김
DFS(set, depth + 1); // 객체 주소를 매개변수로 넘김

이거에 따라 테스트케이스 3번이 실패하냐 성공하냐로 나뉘었다는 점이었다.
질문하기 게시판에 있는 댓글을 보고 힌트를 얻어 깊은 복사, 얕은 복사 개념 때문이었다는 것을 알게되었고
오늘도 공부할게 정말 많고 갈길이 멀다는 것을 느꼈다.

https://studywithus.tistory.com/116

 

[JAVA] 깊은 복사(Deep Copy) vs 얕은 복사(Shallow Copy)

안녕하세요! 이번에 정리할 내용은 자바에서의 깊은 복사와 얕은 복사 입니다. 깊은 복사와 얕은 복사라는 개념은 평소에 접한적이 꽤 있었습니다. 하지만 오늘 알고리즘 문제를 풀면서 아무런

studywithus.tistory.com

또한 HashSet<HashSet<String>> result_set = HashS

    public void DFS(HashSet<String> set, int depth) {
        if (depth == bannedid.length) {
            result_set.add(new HashSet<>(set));
            
            return;
        }

        for (int i = 0; i < userid.length; i++) {
            if (set.contains(userid[i]))
                continue;

            if (check(userid[i], bannedid[depth])) {
                set.add(userid[i]);
                
                DFS(new HashSet<> (set), depth + 1);
                // DFS(set, depth + 1);
                set.remove(userid[i]);
            }
        }
    }

HashSet 안에 HashSet 을 이렇게 선언해준 이유는
불량사용자들의 조합을 Set으로 저장했을 때, 입출력예시 3번 처럼
["frodo", "fradi", "crodo", "abc123", "frodoc"]
["fr*d*", "*rodo", "******", "******"]
아래 불량유저 조건에 해당되는 조합이
[crodo, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [fradi, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [fradi, abc123, frodo, frodoc] 
[crodo, abc123, frodo, frodoc] [crodo, fradi, abc123, frodoc] [fradi, abc123, frodo, frodoc] 
이런 식으로 5개 나오지만 1,2번과 3,4번은 각각 중복되는 하나의 조합이기 때문에 
HashSet에 넣어서 중복을 제거해준 것이다.

HashSet<HashSet<String>> result_set = new HashSet<>();

 

전체코드

import java.util.*;

class Solution {

    static String[] userid;
    static String[] bannedid;
    static int answer = 0;
    
     HashSet<HashSet<String>> result_set = new HashSet<>();

    public int solution(String[] user_id, String[] banned_id) {

        userid = user_id;
        bannedid = banned_id;

        DFS(new HashSet<String>(), 0);

        return result_set.size();
    }

    public void DFS(HashSet<String> set, int depth) {
        if (depth == bannedid.length) {
            result_set.add(new HashSet<>(set));
            
            return;
        }

        for (int i = 0; i < userid.length; i++) {
            if (set.contains(userid[i]))
                continue;

            if (check(userid[i], bannedid[depth])) {
                set.add(userid[i]);
                // new HashSet<> (set) 차이가 뭐지
                
                DFS(new HashSet<> (set), depth + 1);
                // DFS(new HashSet<> (), depth + 1);
                set.remove(userid[i]);
            }
        }
    }

    public boolean check(String str1, String str2) {
        boolean flag = true;

        if (str1.length() != str2.length()) {
            flag = false;
            return flag;
        }

        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i) && str2.charAt(i) != '*') {
                flag = false;
                return flag;
            }
        }

        return flag;
    }
}

안녕하세요! 이번에 정리할 내용은 자바에서의 깊은 복사와 얕은 복사 입니다.

깊은 복사와 얕은 복사라는 개념은 평소에 접한적이 꽤 있었습니다.

하지만 오늘 알고리즘 문제를 풀면서 아무런 의심없이(?) 다음과 같이 컬렉션 List를 얕은 복사하는 코드를 작성했었고, 이에 따라 참조하고 있는 두 리스트가 모두 값이 변경되어 생각했던 아웃풋과 다르게 나와서 약간 어리둥절한 상태였습니다. 🤔

List<String> list = new ArrayList<>();

...

List<String> temp = list; // shallow copy

 

해당 문제점은 디버깅을 통해 파악할 수 있었는데요, 기본적인 내용이지만 확실하게 정리하고 넘어가도록 하겠습니다 😃

 

깊은 복사(Deep Copy)는 '실제 값'을 새로운 메모리 공간에 복사하는 것을 의미하며,

얕은 복사(Shallow Copy)는 '주소 값'을 복사한다는 의미입니다.

 

얕은 복사의 경우 주소 값을 복사하기 때문에, 참조하고 있는 실제값은 같습니다.

예제와 설명을 통해 확인해보겠습니다.

 

 

🎯  얕은 복사(Shallow Copy)

public class CopyObject {

    private String name;
    private int age;
    
    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}



import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CopyObjectTest {

    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original; // 얕은 복사

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }
}

위 코드에서는 copy 객체에 set메소드를 통해 이름을 변경했는데,

실제 결과는 original 객체와 copy 객체 모두 값이 변경이 되었습니다.

 

CopyObject copy = original 의 코드에서 객체의 얕은 복사를 통해 '주소 값'을 변경했기 때문에

참조하고 있는 실제 값은 동일하고, 복사한 객체가 변경된다면 기존의 객체도 변경이 되는 것입니다.

 

 

위 상태에 대한 메모리 구조를 나타내면 다음과 같이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

스택이 스팸으로 보이는건 배가 고파서일까요..

original 인스턴스를 생성하면 Stack 영역에 참조값이, Heap 영역에 실제값이 저장이 됩니다.

그리고 얕은 복사를 통해 객체를 복사했기 때문에 copy 인스턴스는 original 인스턴스가 참조하고 있는

Heap 영역의 참조값을 동일하게 바라보고 있는 상태가 됩니다.

 

그 후 set 메소드를 통해 값을 변경하면 동일한 주소를 참조하고 있기 때문에 아래와 같이 됩니다.

따라서 코드로는 copy 객체의 name만 변경했지만,

동일한 주소를 참조하고 있기 때문에 original의 객체에도 영향을 끼치게 됩니다.

 

그래서 객체를 출력해보면 아래와 같이 동일한 주소가 출력이 됩니다.

CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

System.out.println(original);
System.out.println(copy);

 

 

 

 

🎯  깊은 복사(Deep Copy)

깊은 복사를 구현하는 방법은 여러가지가 있습니다.

  • Cloneable 인터페이스 구현
  • 복사 생성자
  • 복사 팩터리 등등....

 

 

◎ Cloneable 인터페이스 구현

Cloneable 인터페이스는 위와 같이 빈 껍데기의 인터페이스지만

주석을 살펴보면 Object 클래스의 clone() 메소드를 반드시 구현하라고 설명이 되어있습니다.

 

 

public class CopyObject implements Cloneable {

    private String name;
    private int age;

    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected CopyObject clone() throws CloneNotSupportedException {
        return (CopyObject) super.clone();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


    @Test
    void shallowCopy() throws CloneNotSupportedException {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original.clone();

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }

깊은 복사를 통해 테스트를 진행해보면 얕은 복사와는 달리 original 인스턴스의 값은 변경이 되지 않습니다.

 

 

※ Effective Java 13장에서는 clone 재정의는 주의해서 진행하라 라는 아이템이 있습니다.

해당 책의 내용을 대략적으로 살펴보면 다음과 같습니다.

Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다. 여기서 큰 문제점은 clone 메서드가 선언된 곳이 Cloneable이 아닌 OBject이고, 그 마저도 protected이다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메소드를 호출할 수 없다. 리플렉션을 사용하면 가능하지만, 100% 성공하는 것도 아니다. 

이러한 여러 문제점을 가진 인터페이스이지만, Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다.

 

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 구현해도 위험이 크지는 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고' 라는 것이다.

단, 배열만은 clone 메소드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

 

http://www.yes24.com/Product/Goods/65551284

 

 

 

◎ 복사 생성자, 복사 팩터리

public class CopyObject {

    private String name;
    private int age;

    public CopyObject() {
    }

    /* 복사 생성자 */
    public CopyObject(CopyObject original) {
        this.name = original.name;
        this.age = original.age;
    }

    /* 복사 팩터리 */
    public static CopyObject copy(CopyObject original) {
        CopyObject copy = new CopyObject();
        copy.name = original.name;
        copy.age = original.age;
        return copy;
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copyConstructor = new CopyObject(original);
        CopyObject copyFactory = CopyObject.copy(original);

        copyConstructor.setName("JuBal");
        copyFactory.setName("BalJu");

        System.out.println(original.getName());
        System.out.println(copyConstructor.getName());
        System.out.println(copyFactory.getName());
    }

복사 생성자와 복사 팩터리를 통해 객체를 복사하는 과정도 깊은 복사임을 알 수 있습니다.

 

 

깊은 복사를 그림으로 나타내면 다음과 같습니다.

얕은 복사와는 다르게 Heap 영역에 새로운 메모리 공간을 생성하여 실제 값을 복사합니다.

 

Collections나 Map의 경우 이미 복사 팩터리인 copy() 메소드를 구현하고 있습니다.

    /**
     * Copies all of the elements from one list into another.  After the
     * operation, the index of each copied element in the destination list
     * will be identical to its index in the source list.  The destination
     * list's size must be greater than or equal to the source list's size.
     * If it is greater, the remaining elements in the destination list are
     * unaffected. <p>
     *
     * This method runs in linear time.
     *
     * @param  <T> the class of the objects in the lists
     * @param  dest The destination list.
     * @param  src The source list.
     * @throws IndexOutOfBoundsException if the destination list is too small
     *         to contain the entire source List.
     * @throws UnsupportedOperationException if the destination list's
     *         list-iterator does not support the {@code set} operation.
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

 

+ Recent posts