코테

[프로그래머스/Java] 가장 많이 받은 선물

nkdev 2025. 10. 3. 16:19

📍 문제 풀이

* 카카오 테크의 문제 풀이 발췌

 

자료구조, 반복문, 조건문 등 프로그래밍의 기초 개념들을 적절히 활용해서 푸는 문제

N명의 이름 리스트에 인덱스를 붙이고 NxN 2차원 배열에 선물을 주고 받은 횟수를 저장하는 방법으로 요구사항을 구현할 수 있다.

이름 리스트에 인덱스를 붙일 때, 언어에 따라 해시맵(HashMap) 등의 자료구조를 활용할 수 있고, 입력 사이즈가 작기 때문에 인덱스를 찾는 내장 함수를 사용하거나 반복문을 사용해 인덱스를 찾는 코드를 직접 구현해도 됩니다.

 

이를 위해, 각 gifts의 원소에서 선물을 준 사람 A와 받은 사람 B의 이름을 분리합니다. A의 인덱스가 i, B의 인덱스가 j라면 위에서 생성한 2차원 배열의 [i행][j열]의 값을 1 늘려 A가 B에게 보낸 선물의 수를 카운팅할 수 있습니다. gifts의 모든 기록을 저장하면 A가 B에게 준 선물의 총 수는 배열 [i행][j열]의 값, A가 모두에게 준 선물의 수는 배열 [i행]의 합, A가 모두에게 받은 선물의 수는 배열 [j행]의 합으로 구할 수 있습니다.

 

위 값들을 모두 구했다면 각 사람마다 자신을 제외한 N-1명에게 선물을 몇 개 받는지 카운팅하고, 그 최댓값을 갱신해 문제의 정답을 구할 수 있습니다.

📍 코드 설계

처음에는 이렇게 설계했는데 

friends 이름(0행) 마다 received(1행), gave(2행) 배열 생성하기
gifts 순회하며 원소의 문자열 앞쪽이면 일치하는 이름의 2행을, 문자열 뒤쪽이면 일치하는 이름의 1행을 cnt++하여 receivedCnt, gaveCnt 채워넣기

선물 지수(presentIndex) = aGaveCnt - aReceivedCnt 계산하기

두 명의 친구를 선정하여 비교 시작
기록o : receivedCnt 비교해서 더 적게 받은 사람이 선물 받음
기록x || bReceivedCnt == aReceivedCnt : 선물 지수 비교해서 더 큰 사람이 선물 받음

 

문제에서 이런 입출력 예시를 괜히 주는 게 아니었다.

이 형태로 데이터를 저장한 다음, 적절하게 활용해서 정답을 도출해야 했다.

 

* 다른 사람 코드를 보고 개선할 부분을 찾아보았다

 

먼저 HashMap을 사용해 이름(String)을 인덱스(Integer)로 매핑했다.

첫 번째 정보는 이차원 배열 giftInfo로 선언했고

gifts의 각 원소값 'a b'를 통해 giftInfo[map.get(a)][map.get(b)]값을 1 늘려 a가 b에게 보낸 선물 수를 카운트했다.

선물 지수 giftFactor은 미리 구해두지 않고 giftInfo의 각 인덱스를 탐색하면서 항상 새로 구했다. 

friends<=50이라서 비효율을 감수하고 직관적이고 간단하게 구현했다.

-> 선물 지수를 저장하는 배열을 만들어서 giftInfo값 저장할 때 giftFactor[giver]++, giftFactor[receiver]--로 동시에 갱신, calcGiftFactor() 없애기

giftInfo에서 i행 j열을 확인했으면 j행 i열도 확인한 것과 같으므로 visited 처리를 통해 중복 검사를 막았다.

-> visited 대신 이중 for문 조건에서 i<j인 경우만 검사하여 중복이 발생하지 않도록 하기

그리고 나머지는 문제에서 언급한 조건을 그대로 코드로 옮겼다.

-> i행 j열과 j행 i열의 값이 다른 경우와 아닌 경우로만 나누어도 되는데 0인 경우까지 고려하고 있어 조건문이 복잡해짐

📍 관련 개념 / 아이디어

(1) 인덱스를 매핑하는 방법

결론 - 데이터가 많으면 HashMap을 사용하자!!

그리고 Java 표준 라이브러리에는 배열에서 문자열 값의 인덱스를 바로 찾아주는 내장 함수가 없어서 어차피 HashMap 써야 함

 

1) HashMap

- 맵 검색이 빠름 : O(1)

- 메모리에 HashMap을 따로 둬야 함

Map<String, Integer> nameIdx = new HashMap<>();
for (int i = 0; i < friends.length; i++) {
    nameIdx.put(friends[i], i);
}
int idx = nameIdx.get("muzi");  // 바로 O(1)에 인덱스 찾기

 

2) 반복문

- 구현이 단순, 추가 메모리 불필요

- 검색이 느림 : O(N)

int findIndex(String[] arr, String target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i].equals(target)) {
            return i;
        }
    }
    return -1; // 없으면 -1 반환
}

 

3) Java 8 Stream

- 코드가 간결

- 내부적으로 선형 탐색이라서 HashMap보다 느림 : O(N)

int idx = IntStream.range(0, friends.length)
                   .filter(i -> friends[i].equals("muzi"))
                   .findFirst()
                   .orElse(-1);

 

4) List.indexOf()

- 내부적으로 선형 탐색이라서 HashMap보다 느림 : O(N)

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> friends = Arrays.asList("muzi", "ryan", "frodo", "neo");

        // 특정 이름의 인덱스 찾기
        int idx1 = friends.indexOf("frodo");  // 2
        int idx2 = friends.indexOf("neo");    // 3
        int idx3 = friends.indexOf("apeach"); // -1 (없으면 -1 반환)

        System.out.println("frodo index: " + idx1);
        System.out.println("neo index: " + idx2);
        System.out.println("apeach index: " + idx3);
    }
}

 

 

(2) Generic Type / Raw Type

결론 - Collection은 제네릭 타입으로 선언해야 한다.

- 값을 꺼낼 때 자동으로 캐스팅 + 언박싱됨

- 형변환이 필요 없어서 편함

- 코드 가독성이 좋아짐

- 컴파일 시점에 타입이 강제되기 때문에 런타임 에러가 적음

Map map = new HashMap();  
map.put("a", 1);

Object value = map.get("a"); // 반환 타입은 Object
int x = (Integer) value;     // 직접 형변환 필요 -> 형변환 하지 않으면 런타임에 ClassCastException
Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);

int x = map.get("a"); // 자동 언박싱, 캐스팅 필요 없음

 

(3) Auto Unboxing

.intValue()는 Java 5 이하 버전에서 쓰는 방식

최근 버전에서는 자바 컴파일러가 Integer -> int으로 오토 언박싱해줌

Integer x = 10; 
int y = x;

 

(4) Java Stream

- Java 8부터 지원

- 데이터를 선언적으로 다루는 기능

- 배열, 컬렉션을 반복문 없이 작업

- Collection (List, Map, Set)은 .stream() 메서드를 기본적으로 제공함

- Primitive Type 배열은 .stream() 메서드가 제공되지 않아서 Arrays.stream(arr)을 써서 배열을 스트림으로 바꿔야 함

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 1. stream() 호출
// 2. 중간 연산 (filter, map, sorted 등)
// 3. 최종 연산 (collect, forEach, sum 등)
List<String> result = names.stream()
                           .filter(name -> name.startsWith("A"))  // A로 시작하는 것만
                           .map(String::toUpperCase)              // 대문자로 변환
                           .toList();                             // 리스트로 수집

System.out.println(result); // [ALICE]
int[] arr = {1, 2, 3, 4};

Arrays.stream(arr) //Arrays.stream()으로 감싸야 함
      .map(x -> x * x)
      .forEach(System.out::println);

 

* 참고로 char[] 배열은 Arrays.stream()을 사용할 수 없다. error: no suitable method found for stream(char[]) 이런 에러가 발생함. 자바가 Object[] 배열로 간주하려고 시도하지만, 타입이 맞지 않기 때문.

📍 정답 코드

처음 짠 코드

import java.util.*;

class Solution {
    
    static int[][] giftInfo;
    public int solution(String[] friends, String[] gifts) {
        
        StringTokenizer st;
        //주고받은 선물 정보 저장
        giftInfo = new int[friends.length][friends.length];
        //선물 지수 저장
        int[] giftFactor = new int[friends.length];

        //맵을 사용해 friends 이름을 idx로 표현 
        Map<String, Integer> nameIdx = new HashMap<>();
        for(int i=0; i<friends.length; i++){
            nameIdx.put(friends[i], i);
        }
        
        //giftInfo에 값 입력
        //gifts 배열의 'a b'를 [a의 인덱스][b의 인덱스]로 변환하여 선물 정보 저장
        for(int i=0; i<gifts.length; i++){
            
            st = new StringTokenizer(gifts[i]);
            String f1 = st.nextToken();
            String f2 = st.nextToken();
                 
            giftInfo[nameIdx.get(f1)][nameIdx.get(f2)]++;
        }
        
        //선물 지수 구하기
        for(int i=0; i<friends.length; i++){
            giftFactor[i] = calcGiftFactor(i);
        }
        
        //giftInfo의 요소를 하나씩 탐색
        int[] result = new int[friends.length];
        
        for(int i=0; i<giftInfo.length; i++){
            for(int j=i+1; j<giftInfo.length; j++){
                
                if(i==j) continue;
                
                //0이 아니고(주고 받은 기록이 있고) 주고받은 수가 다르다면
                //[a][b], [b][a] 중 더 큰 값의 행에 해당하는 사람이 선물 한 개 획득
                if((giftInfo[i][j] != 0 || giftInfo[j][i] != 0)
                                        && giftInfo[i][j] != giftInfo[j][i]){
                                        
                    int val = giftInfo[i][j] > giftInfo[j][i] ? i : j;
                    result[val]++;
                    continue;
                }
                
                
                //0이거나 (주고 받은 기록이 없거나) 주고 받은 수가 같다면
                //선물 지수가 더 큰 사람이 선물 한 개 획득
                if((giftInfo[i][j] == 0 && giftInfo[j][i] == 0)
                    || giftInfo[i][j] == giftInfo[j][i]){
                                        
                    //선물 지수가 다르다면, 선물지수 더 큰 사람이 선물 하나 획득
                    if(giftFactor[i] != giftFactor[j]){
                        int val = giftFactor[i] > giftFactor[j] ? i : j;
                        result[val]++;
                    }
                    
                }
            }
        }
        int ans = Arrays.stream(result).max().getAsInt();
        return ans;
    }
    
    //선물 지수 계산기
    //선물 지수 = k가 준 선물의 수 - k가 받은 선물의 수
    static int calcGiftFactor(int k){
        //k가 선물을 준 수 : giftInfo의 k행 값의 합
        int gaveCnt = Arrays.stream(giftInfo[k]).sum();
        //k가 선물을 받은 수 : giftInfo의 k열 값의 합
        int receivedCnt = Arrays.stream(giftInfo)
                                .mapToInt(row -> row[k])
                                .sum();

        return gaveCnt - receivedCnt;
    }
    
}

 

개선된 코드

선물 지수 계산을 일일이 하지 않고 giftFactor[giver]++, giftFactor[receiver]--로 구했다.

import java.util.*;

class Solution {
    
    public int solution(String[] friends, String[] gifts) {
        
        StringTokenizer st;
        int[][] giftInfo = new int[friends.length][friends.length];
        int[] giftFactor = new int[friends.length];
        Map<String, Integer> nameIdx = new HashMap<>();
        
        for(int i=0; i<friends.length; i++){
            nameIdx.put(friends[i], i);
        }
        
        for(int i=0; i<gifts.length; i++){
            
            st = new StringTokenizer(gifts[i]);
            String f1 = st.nextToken();
            String f2 = st.nextToken();
                 
            giftInfo[nameIdx.get(f1)][nameIdx.get(f2)]++;
            giftFactor[nameIdx.get(f1)]++;
            giftFactor[nameIdx.get(f2)]--;
        }
        
        int[] result = new int[friends.length];
        
        for(int i=0; i<giftInfo.length; i++){
            for(int j=i+1; j<giftInfo.length; j++){
                
                if(i==j) continue;
                
                if(giftInfo[i][j] != giftInfo[j][i]){          
                    int val = giftInfo[i][j] > giftInfo[j][i] ? i : j;
                    result[val]++;
                    
                } else {           
                    if(giftFactor[i] != giftFactor[j]){
                        int val = giftFactor[i] > giftFactor[j] ? i : j;
                        result[val]++;
                    }
                }
            }
        }
        int ans = Arrays.stream(result).max().getAsInt();
        return ans;
    }   
}

 

다른 사람은 이렇게 풀었던데 맨 마지막에 ans 구하는 부분이 굉장히 좋은 아이디어인 것 같다.

리턴값이 받은 선물의 최댓값이기 때문에 모든 사람이 받은 선물 개수를 가지고있을 필요 없이 max값만 알아내면 된다.

첫 번째 for루프를 통해 각 행에 해당하는 사람이 더 많이 받았거나 (giftInfo[i][j] > giftInfo[j][i]) 선물 지수가 더 크면 (giftInfo[i][j] == giftInfo[j][i] && giftFactor[i] > giftFactor[j]) cnt를 1씩 늘린다.

이렇게 모든 사람을 검사하고 나서 max값을 리턴하면 된다.

import java.util.*;

class Solution {
    public int solution(String[] friends, String[] gifts) {
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < friends.length; i++) {
            map.put(friends[i], i);
        }
        int[] index = new int[friends.length];
        int[][] record = new int[friends.length][friends.length];

        for (String str : gifts) {
            String[] cur = str.split(" ");
            index[map.get(cur[0])]++;
            index[map.get(cur[1])]--;
            record[map.get(cur[0])][map.get(cur[1])]++;
        }

       int ans = 0;
       for (int i = 0; i < friends.length; i++) {
           int cnt = 0;
           for (int j = 0; j < friends.length; j++) {
               if(i == j) continue;
               if (record[i][j] > record[j][i]) cnt++;
               else if (record[i][j] == record[j][i] && index[i] > index[j]) cnt++; 
           }
           ans = Math.max(cnt, ans);
       }
        return ans;
    }

 

아이디어를 적용해서 내 코드도 수정해보았다.

import java.util.*;

class Solution {
    
    public int solution(String[] friends, String[] gifts) {
        
        StringTokenizer st;
        int[][] giftInfo = new int[friends.length][friends.length];
        int[] giftFactor = new int[friends.length];
        Map<String, Integer> nameIdx = new HashMap<>();
        
        for(int i=0; i<friends.length; i++){
            nameIdx.put(friends[i], i);
        }
        
        for(int i=0; i<gifts.length; i++){
            
            st = new StringTokenizer(gifts[i]);
            String f1 = st.nextToken();
            String f2 = st.nextToken();
                 
            giftInfo[nameIdx.get(f1)][nameIdx.get(f2)]++;
            giftFactor[nameIdx.get(f1)]++;
            giftFactor[nameIdx.get(f2)]--;
        }
        
        int[] result = new int[friends.length];
        int ans = 0;
        
        for(int i=0; i<giftInfo.length; i++){
            int cnt = 0;
            
            for(int j=0; j<giftInfo.length; j++){
                
                if(i==j) continue;
                
                if(giftInfo[i][j] > giftInfo[j][i]) cnt++;
                else if(giftInfo[i][j] == giftInfo[j][i] && giftFactor[i] > giftFactor[j]) cnt++;
            }
            ans = Math.max(ans, cnt);
        }
        return ans;
    }   
}

 

그리고 나는 StringTokenizer가 더 익숙해서 썼는데

String.split("구분자")로 문자열을 구분자로 잘라서 String 배열로 쓰는 방법도 있다.

속도는 느리지만 가독성 있는 방법이라서 좋은 것 같다.

for (String str : gifts) {
    String[] cur = str.split(" ");
    index[map.get(cur[0])]++;
    index[map.get(cur[1])]--;
    record[map.get(cur[0])][map.get(cur[1])]++;
}
for(int i=0; i<gifts.length; i++){
    st = new StringTokenizer(gifts[i]);
    String f1 = st.nextToken();
    String f2 = st.nextToken();
                 
    giftInfo[nameIdx.get(f1)][nameIdx.get(f2)]++;
    giftFactor[nameIdx.get(f1)]++;
    giftFactor[nameIdx.get(f2)]--;
}

출처

https://tech.kakao.com/posts/610