본문 바로가기
TIL

코딩도장 시간 활용하기 (22.09.13 TIL)

by winteringg 2022. 9. 13.

주간 일과 시간 중에 코딩도장 이라고, 알고리즘 문제를 하루에 한 개씩 푸는 시간이 있다. 매번 할 때마다 해결 방법을 고민하기에도 시간이 모자라서 다소 뻔한 풀이 방법으로 문제를 풀어내고는 했었는데, 이렇게만 해서는 시야가 넓어질 것 같지가 않았다. 그래서 이제는 답 제출 후 시간이 많이 남으면 답을 제출 한 뒤에 다른 사람들이 한 코드를 보면서 이렇게도 풀 수 있구나~ 하고 혼자 분석해보는 시간을 가져보려고 한다.
오늘 푼 것은 프로그래머스 문제 중에 '제일 작은 수 제거하기' 라는 문제였고, 정수를 저장한 배열에서 가장 작은 수를 제거하는 배열을 리턴하는 함수를 완성하는 미션이였다. 제일 많은 사람들이 푼 방법은 아래와 같은데 나도 이런 방식으로 풀어냈다. (힌트 살짝 보긴 함 ㅎㅎ)

class Solution {
    public int[] solution(int[] arr) {
        if (arr.length == 1) {
            return new int[] {-1};
        }

        //배열의 길이가 1이 아닐 때, 가장 작은 수 하나 뺀 크기로 배열 초기화
        int[] answer = new int[arr.length - 1];

        //임의로 하나의 값을 제일 작은 수로 가정함
        int min = arr[0];

        for (int i = 0; i < arr.length; i += 1) {
            //두 인자값 중 더 작은 값을 min 에 저장
            min = Math.min(min, arr[i]);
        }

        //반복문 증감 인덱스용 변수 초기화
        int x = 0;

        //가장 작은 수 제외한 배열 만들기
        for (int i = 0; i < arr.length; i += 1) {
            //arr 배열의 값이 가장 작은 수인 min 값과 같다면 continue
            if (arr[i] == min) {
                continue;
            }

            //arr 배열의 값이 가장 작은 수인 min 과 같지 않다면 arr 배열에 순서대로 넣어준다.
            answer[x++] = arr[i];
        }

        return answer;
    }
}


그런데 다른 사람의 풀이를 보던 중 스트림과 람다식을 이용한 방법이 단 몇 줄 만에 끝나는 것을 보고 한 번 시도해보고 싶어서 따라해봤다. 코드는 아래와 같다.

import java.util.Arrays;
import java.util.List;

class Solution {
    public int[] solution(int[] arr) {
        if (arr.length == 1) {
            return new int[] {-1};
        }
		
        // arr 배열의 최소값을 구해서 int 형으로 반환함
        int min = Arrays.stream(arr).min().getAsInt();

		// arr 배열에서 최소값 min과 같지 않은 것만 배열로 생성함
        return Arrays.stream(arr).filter(i -> i != min).toArray();
    }
}

사실 스트림과 람다식이라고 하면 나한테는 너무 어렵고 난해한 기술로 다가왔기 때문에 기존에는 아예 시도해 보려는 생각 조차 안해봤었지만 이제는 피할 수 없는 시기가 온 것 같다!
먼저 스트림은 java 8 부터 추가된 것으로, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 도와주는 반복자이다. 쉽게 말해 배열이나 컬렉션으로 원하는 값을 얻을 때 for 문의 도배를 방지하기 위해 나온 개념인데 람다식을 사용하면 코드가 훨씬 간결해진다. 하지만 스트림은 한 번에 모든 것이 수행되기 때문에 디버깅이 힘들고, 한 번 쓰면 close 되기 때문에 재활용이 힘든 것과, 속도가 느리다는 단점이 있다. 실제로 위의 두 코드 풀이의 속도를 비교해보면 stream 이 훨씬 느렸다.


스트림은 선언, 가공, 반환 세 부분으로 이루어진다.

Arrays.stream(arr).boxed().sorted().collect(Collectors.toList())

Arrays.stream(arr) // 선언

.boxed().sorted() // 가공

.collect(Collectors.toList()) // 반환

=> arr 배열을 정렬 후 List 타입으로 반환




Arrays.stream(arr).findFirst().getAsInt()

Arrays.stream(arr) // 선언

.findFirst() // 가공

.getAsInt() // 반환

=> arr 배열의 첫번째 값을 int 타입으로 반환




list.stream().skip(1).collect(Collectors.toList())

list.stream() // 선언

.skip(1) // 가공

.collect(Collectors.toList()) // 반환

list명의 첫번째 값은 생략한 뒤 list 타입으로 반환

1) 선언 : 배열, 컬렉션 등을 스트림 형태로 만들기
- 각각 배열과 컬렉션을 사용하는 경우나 직접 값을 넣어 사용하는 경우인데, 꼭 stream 을 선언한 후 값을 넣고 사용할 필요는 없고 바로 가공 형태로 사용해도 된다.

// 1) 선언 : 배열, 컬렉션 등을 스트림 형태로 만들기
Stream<데이터타입> stream명 = Arrays.stream(배열명);
Stream<데이터타입> stream명 = 리스트명.stream();
Stream<데이터타입> stream명 = Stream.of('값', '값'....);

2) 가공 : 스트림을 필요한 형태로 가공하기 (가공 메서드 사용)

// 2) 가공 : 스트림을 필요한 형태로 가공하기 (가공 메서드 사용)
Arrays.stream(배열명).가공메소드...
리스트명.stream.가공메소드...

// count() 배열, 컬렉션 크기 확인할 때 사용
Arrays.stream(arr).count();
list.stream().count();

// sorted() 오름차순으로 정렬할 때 사용
Arrays.stream(arr).sorted();

// sorted(Comparator.reverseOrder()) 역정렬
Arrays.stream(arr).sorted(Comparator.reverseOrder());

// boxed() Int, Long, Double 배열로 stream 을 만들었을 경우 각종 메서드를 사용하기 위해 사용
// 컬렉션 스트림에서는 사용하지 않는 메서드
Arrays.stream(arr).boxed().sorted();



// 그 외에 다른 메서드들
findFirst() //스트림의 처음 값 가져오기
skip(배열크기 - 1).findFirst() //스트림의 마지막 값 가져오기
skip(값) //값의 인덱스는 생략하고 나머지를 가져옴
limit(값) //값의 인덱스까지 가져옴
distinct() //중복값은 생략
max(데이터타입::compare) //최대값
min(데이터타입::compare) //최소값
average() //평균값. 배열일 경우에는 바로 사용되지만, 컬렉션의 경우에는 mapToDouble() 을 이용해 한 번 바꿔준 후 사용함
sum() //합계. average() 와 설명 동일함

3) 반환 : 가공한 값을 원하는 형태로 가져오기

// 3) 반환 : 가공한 값을 원하는 형태로 가져오기

// 값이 하나만 있는 경우
get()
getAsInt()

// 배열, 컬렉션 형태로 가져오는 경우
toArray()

// 해당하는 개수 반환
collect(Collectors.counting())

// 모든 값을 합치면서 "|" 를 붙여줌.
//"" 빈 문자열을 쓰면 값만 붙어서 나옴.
collect(Collectors.joining("|"))

// 값을 int, double, long 형태로 변환한 뒤 해당 형태의 평균을 구해 반환
collect(Collectors.averagingInt(val -> Integer.parseInt(val))
collect(Collectors.averagingDouble(val -> Double.parseDouble(val))
collect(Collectors.averagingLong(val -> Long.parseLong(val))


스트림은 람다식과 자주 쓰인다. 자바에서 람다는 기존의 클래스에 메서드를 만들고 객체화 한 뒤에 끌어쓰는 방식이 아니라 그 때 바로 만들어서 사용하는 방식이다. 람다의 핵심은 (파라미터) -> { 수행할 코드 } 를 통해 메서드를 정의하지 않고도 메서드처럼 사용할 수 있다는 것이다.
파라미터와 코드가 각 하나씩일 경우에는 파라미터 -> 코드 이렇게 사용해도 되지만, 파라미터나 코드가 늘어날수록 (파라미터1, 파라미터2) -> { 코드 } 이런 식으로도 사용한다.

// 각 인덱스의 값을 파라미터로 넘기고 코드 수행
forEach((파라미터) -> {코드})

// 스트림 중 하나의 값이라도 조건에 맞으면 true
anyMatch((파라미터) -> {코드})

// 스트림 중 하나의 값이라도 조건에 맞지 않으면 true
noneMatch((파라미터) -> {코드})

// 스트림의 값이 모두 조건에 맞아야 true
allMatch((파라미터) -> {코드})

// 요소들을 조건에 따라 걸러내는 작업. 코드에 맞는 값만 가져옴
filter(파라미터) -> {코드}

// 스트림의 값을 모두 하나로 합칠 때 사용.
//데이터 타입과 sum 으로 하나로 합친 뒤, 마지막에 값을 더해서 가져옴
reduce(값, 데이터타입::sum)

댓글