728x90

문제: https://school.programmers.co.kr/learn/courses/30/lessons/120844

 


1️⃣ 첫 번째 코드 (리스트 변환 사용)

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Solution {
    public int[] solution(int[] numbers, String direction) {
        List<Integer> list = Arrays.stream(numbers).boxed().collect(Collectors.toList());

        if (direction.equals("right")) {
            list.add(0, list.get(list.size() - 1)); // 마지막 원소를 맨 앞에 삽입
            list.remove(list.size() - 1); // 마지막 원소 제거
        } else {
            list.add(list.size(), list.get(0)); // 첫 번째 원소를 맨 뒤에 삽입
            list.remove(0); // 첫 번째 원소 제거
        }
        return list.stream().mapToInt(Integer::intValue).toArray();
    }
}

🔹 특징

  1. 배열을 List<Integer>로 변환한 후, 리스트 연산을 통해 이동을 수행.
  2. add(0, element) 또는 add(size, element)를 사용해 요소를 앞뒤로 이동.
  3. 마지막에 stream()을 사용하여 다시 int[]로 변환.

🟢 장점

  • 코드가 직관적이며 리스트의 메서드를 활용하여 쉽게 이해할 수 있음.

🔴 단점

  • 성능이 비효율적
    • Arrays.stream(numbers).boxed().collect(Collectors.toList()) → O(N)
    • add(index, element)와 remove(index) → O(N)
    • 최종적으로 list.stream().mapToInt(Integer::intValue).toArray() → O(N)
    • 전체 시간 복잡도: O(N) + O(N) + O(N) = O(N)
  • 불필요한 오토박싱 & 언박싱 발생
    • int를 Integer로 변환(boxing), 다시 int로 변환(unboxing) → 성능 저하 가능성 있음.

2️⃣ 두 번째 코드 (배열 인덱스 이동 사용)

class Solution {
    public int[] solution(int[] numbers, String direction) {
        int len = numbers.length;
        int[] answer = new int[len];
        boolean rightMove = "right".equals(direction);
        
        for (int i = 0; i < len; i++) {
            answer[rightMove ? (i + 1) % len : (i - 1 + len) % len] = numbers[i];
        }
        return answer;
    }
}

🔹 특징

  1. 새로운 배열 answer을 생성하고 수학적 연산을 이용해 인덱스를 조정하여 값을 삽입.
  2. 오른쪽 이동(right) → answer[(i + 1) % len] = numbers[i];
  3. 왼쪽 이동(left) → answer[(i - 1 + len) % len] = numbers[i];

🟢 장점

  • 성능이 매우 우수함 (O(N))
    • 단순 반복문과 인덱스 연산만 사용하여 추가적인 리스트 변환 없이 처리.
    • 리스트 변환, 박싱/언박싱이 없어 불필요한 성능 손실 없음.
  • 메모리 사용량이 적음
    • int[]만 사용하므로 추가적인 객체 생성이 없음.

🔴 단점

  • answer[rightMove ? (i + 1) % len : (i - 1 + len) % len] = numbers[i];
    → 가독성이 살짝 떨어질 수 있음.
    → 하지만 수학적 연산이므로 이해하면 훨씬 효율적임.

🏆 최종 결론: 두 번째 코드가 더 좋음!

첫 번째 코드 (리스트 변환) 두 번째 코드 (배열 인덱스 연산)

시간 복잡도 O(N) + O(N) + O(N) = O(N) O(N)
메모리 사용 추가적으로 List<Integer> 사용 int[] 배열만 사용 (메모리 절약)
성능 리스트 변환과 박싱/언박싱으로 성능 저하 빠른 배열 인덱스 계산
가독성 직관적이지만 리스트 변환 과정이 필요 다소 수학적이지만 최적화됨
추천 여부 ❌ 비효율적 ✅ 최적

💡 결론:

두 번째 코드(배열 인덱스 연산 방식)더 빠르고 메모리 효율적이며 최적화됨.

728x90
728x90

문제: https://school.programmers.co.kr/learn/courses/30/lessons/120835

 

 

1️⃣ 첫 번째 코드 분석 (이중 반복문)

class Solution {
    public int[] solution(int[] emergency) {
        int[] answer = new int[emergency.length];

        for(int i = 0; i < answer.length; i++){
            if(answer[i] != 0){
                continue;
            }
            int idx = 1;
            for(int j = 0; j < answer.length; j++){
                if(emergency[i] < emergency[j]){
                    idx++;
                }
            }
            answer[i] = idx;
        }
        return answer;
    }
}

📌 특징

  • 각 요소에 대해 다른 모든 요소와 비교하여 순위를 매김.
  • 시간 복잡도: O(N²) (이중 반복문)

🔴 단점

  • 성능이 좋지 않음
    → emergency의 길이가 길어질수록 성능이 급격히 저하됨.
  • 불필요한 if(answer[i] != 0) 검사가 포함됨 (이 코드에서는 불필요한 로직).
  • 가독성이 떨어짐 (배열을 두 번 순회하여 index를 계산하는 것이 직관적이지 않음).

2️⃣ 두 번째 코드 분석 (정렬 + 해시맵)

import java.util.*;

class Solution {
    public int[] solution(int[] emergency) {
        int[] arr = emergency.clone();
        Arrays.sort(arr);
        Map<Integer, Integer> indexMap = new HashMap<>();
        for(int i = 0; i < arr.length; i++) {
            indexMap.put(arr[i], arr.length - i);
        }
        int[] answer = new int[emergency.length];
        for(int i = 0; i < emergency.length; i++) {
            answer[i] = indexMap.get(emergency[i]);
        }
        return answer;
    }
}

📌 특징

  • emergency 배열을 정렬하여 작은 값부터 큰 값까지 순위를 매김.
  • 해시맵(indexMap)을 이용하여 각 값의 순위를 빠르게 찾음.
  • 시간 복잡도: O(N log N) (정렬) + O(N) (해시맵 탐색) = O(N log N)

✅ 장점

  • 성능이 훨씬 좋음
    → O(N²) 대신 O(N log N)으로 성능 개선.
  • 가독성이 높음
    → 정렬 후 순위를 매기는 과정이 명확하고 직관적.
  • 불필요한 연산을 줄임
    → 중복 비교 없이 한 번의 정렬과 해시맵 탐색만으로 순위 결정.

3️⃣ 세 번째 코드 분석 (정렬)

import java.util.Arrays;

class solution {
    public int[] solution(int[] emergency) {
        int n = emergency.length;
        int[] answer = new int[n];
        Integer[] sortedIndex = new Integer[n];
        for( int i = 0; i < n; i++ ) {
            sortedIndex[i] = i;
        }

        Arrays.sort(sortedIndex, (i1, i2)-> emergency[i2] - emergency[i1]);

        for( int i = 0; i < n; i++ ) {
            answer[sortedIndex[i]] = i + 1;
        }

        return answer;
    }
}

📌 특징

  • emergency 배열의 Index를 정렬.
  • 정렬된 Index를 활용하여 각 값의 Rank를 매김.
  • 시간 복잡도: O(N) (Index 적용) + O(N log N) (정렬) + O(N) (answer작성) = O(N log N)

✅ 장점

  • 성능이 훨씬 좋음
    → O(N²) 대신 O(N log N)으로 성능 개선.
  • 가독성이 높음
    → 정렬 후 순위를 매기는 과정이 명확하고 직관적.

4️⃣ 네 번째 코드 분석 (우선순위 큐)

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

public class Solution {
	public int[] solution(int[] emergency) {
        int n = emergency.length;
        int[] answer = new int[n];
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o[0]));
        for( int i = 0; i < n; i++ ) {
            pq.add(new int[]{emergency[i], i});
        }

        for( int i = 0; i < n; i++ ) {
            answer[pq.poll()[1]] = n - i;
        }

        return answer;
    }
}

📌 특징

  • 우선순위 큐를 활용하여 정렬된 데이터를 사용.
  • 시간 복잡도: O(log n) ( PriorityQueue 요소 추가) + O(N log N) ( PriorityQueue 요소 추출) = O(N log N)

🔴 단점

  • 우선순위 큐 사용시 큐 값을 꺼내는 데 추가적인 시간 오버헤드 발생

🏆 결론: 세 번째 코드가 더 낫다!

  • 성능: O(N log N) vs O(N²) → 두번째와 세번째 코드가 훨씬 빠름.
  • 가독성: 정렬된 배열을 활용함으로써 가독성 향상
  • 유지보수성: 코드가 짧고 이해하기 쉬움.
728x90
728x90

주제

금주 테코테코의 주제는 Stack입니다.

 

Stack 자료구조를 활용하거나 Stack자료구조의 작동원리를 이해하여 아래 문제를 해결하였습니다.

 

문제

1.  올바른 괄호

'(' 또는 ')' 로만 이루어진 문자열 s가 주어졌을 때, 문자열 s가 올바른 괄호이면 true를 return 하고, 올바르지 않은 괄호이면 false를 return 하는 solution 함수를 완성해 주세요.

 

코드

public class 올바른_괄호 {

    boolean solution(String s) {

        Node first = null;
        Node last = null;

        int len = s.length();
        for(int i = 0; i < len; i++) {
            char c = s.charAt(i);
            if(c == '(') {
                if(first == null) {
                    first = new Node();
                    last = first;
                    continue;
                }
                last = last.newNextNode();
                continue;
            }
            if(first == null) return false;
            Node prev = last.removeNowNode();
            if(prev == null) {
                first = null;
            }
            last = prev;
        }

        return last == null;
    }

    public static class Node {
        public Node prevNode;
        public Node nextNode;

        public Node() {
        }

        public Node(Node prevNode) {
            this.prevNode = prevNode;
        }

        public static Node from (Node prev) {
            return new Node(prev);
        }

        public Node newNextNode() {
            this.nextNode = Node.from(this);
            return this.nextNode;
        }

        public Node removeNowNode() {
            Node prev = this.prevNode;
            if(prev == null) return null;
            prev.nextNode = null;
            return prev;
        }
    }
}

 

2.  괄호_회전하기

대괄호, 중괄호, 그리고 소괄호로 이루어진 문자열 s가 매개변수로 주어집니다. 이 s를 왼쪽으로 x (0 ≤ x < (s의 길이)) 칸만큼 회전시켰을 때 s가 올바른 괄호 문자열이 되게 하는 x의 개수를 return 하도록 solution 함수를 완성해주세요.

 

코드

package com.codingTest.programers.level2;

import java.util.*;

public class 괄호_회전하기 {
    Map<Character, Integer> map;

    public 괄호_회전하기() {
        map = new HashMap<>();
        map.put('(', 0);
        map.put('[', 1);
        map.put('{', 2);
        map.put(')', 3);
        map.put(']', 4);
        map.put('}', 5);
    }

    public int solution(String s) {
        int len = s.length();

        int answer = 0;

        boolean flag;
        Stack<Character> checkPoint;

        int point = 0;
        for(int i = 0; i < len; i++){
            flag = false;
            checkPoint = new Stack<>();
            for(int j = 0; j < len; j++){
                char c = s.charAt((j + point)%len);
                int index = map.get(c);
                if(index < 3) {
                    checkPoint.push(c);
                } else {
                    if(checkPoint.isEmpty()) {
                        flag = true;
                        break;
                    }
                    char check = checkPoint.pop();
                    int prevIndex = map.get(check);
                    if(prevIndex != index%3) {
                        flag = true;
                        break;
                    }
                }
            }
            if(checkPoint.isEmpty() && !flag) {
                answer++;
            }
            point++;
        }

        return answer;
    }
}
728x90
728x90

이번에 소개할 문제는 문자열 압축이라는 알고리즘 문제이다.

 

해당 문제는 데이터를 처리할 때 최소한의 메모리를 사용하여 문자열을 저장하고 싶은 게 핵심인 문제인 거 같다.

 

예를 들어보면 "가나다라마바사아"라는 문자열이 존재한다고 가정해보자.

이때 해당 문자열을 그대로 저장하게 되면 8글자를 Byte로 저장하게 된다.

위와 같이 중복된 글자가 없으면 그대로 저장하는 것이 데이터의 정확도상 당연한 것이라 생각한다.

 

하지만 "가가가나나나나나나다다다다다라라라마마바"라는 문자열이 있다고 생각해보자.

위와 같은 문자열이 있을 때 해당 문자열을 그대로 저장하는 것은 20개의 글자를 Byte로 저장하는 것이다.

이럴 때 동일한 문자가 연달아 존재할 때 몇 개인지를 앞에 명시하여 보다 짧은 문자열로 저장하는 것이 훨씬 적은 데이터를 가지고 저장을 할 수 있다.

 

또한 문자열을 줄일 때 1 단어 단위로 줄이면 "3가6나5다3라2마바"와 같은 형태로 저장되는 것이고,

2 단위로 줄이면 "가가가나2나나나다2다다라라라마마바"라는 문자열로 저장되는 식으로 몇 개 단위로 잘라 저장하느냐에 따라 저장되는 데이터 양에 차이를 주게 된다.

 

다시 문제로 돌아가서 해당 문제는 데이터를 최소한 짧은 문자열로 저장하고 싶은데 n단위로 잘라 저장했을 때 가장 적을 경우 가장 짧은 문자열일 경우 해당 문자열의 length를 구하여라로 생각하면 될 거 같다.

 

우선 default를 지정하도록 하자.

int answer = s.length();

해당 default로 지정한 경우는 어떤 방법으로 문자열을 압축한다 하더라도 기본 문자열보다 길어질 수 없기 때문에 기본 문자열의 length를 default로 지정하였다.

 

이후 먼저 중요하게 생각한 것은 문자열을 자를 건데 몇 개 단위로 자를지에 관해 먼저 생각하기로 하였다.

for(int i = 1; i< s.length() /2 +1; i++) {

}

문자열을 자를 거지만 절반을 넘어갈 경우 어차피 default와 길이는 같을 테니 적절하게 max를 지정해 주었다.

 

이후 잘라 만들어진 문자열과 이전 값을 비교하기 위한 변수와 몇 번째 압축인지를 알기 위한 변수를 선언해 주었다.

// i 는 자르는 갯수
for(int i = 1; i< s.length() /2 +1; i++) {
    StringBuffer sb = new StringBuffer();

    //이전값
    String bef = "";

    // 압축 count default 1
    int zipNum = 1;
}

이후부터는 subString으로 문자열을 자르기 위해 반복문을 돌려 문자열을 자르는 startPoint와 endPoint를 지정해 주도록 하자.

for (int j = 0; j < s.length(); j++) {

    //마지막 index
    int endNum = i+j > s.length() ? s.length() : i+j;

    //현재 
    String sub = s.substring(j, endNum);

}

위에서 나온 sub의 값은 현재 잘려 나온 문자열의 값이다.

 

현재 잘려나온 값을 알았으니 이제 해당 값이 압축이 가능한 문자열인지 check를 해볼 차례이다.

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
				
        //마지막 index
        int endNum = i+j > s.length() ? s.length() : i+j;

        //현재 
        String sub = s.substring(j, endNum);
    
        if (!sub.equals(bef)) {
        
            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;
        }
    
    }
}

bef = 이전 값. 

sub = 현재 값.

위 두 값을 비교하여 같을 경우 압축 가능한 문자열이니 zipNum을 올려주며 끝나고,

문자열이 같지 않을 경우 압축할 수 없는 값이니 다음 단계를 진행해 주었다.

 

if (!sub.equals(bef)) {

    if(zipNum > 1) {
        sb.append(zipNum);
    }

    if (!bef.equals("")) {

        if (endNum != s.length()) {
            sb.append(bef);
        }
        else {
            sb.append(bef).append(sub);
        }	
    }

    bef = sub;
    zipNum = 1;
}
else {
    zipNum++;

    if (endNum == s.length()) {
        sb.append(zipNum).append(bef);
    }
}

자른 문자열이 이전과 이후가 같을 확률은 다를 경우보다 높아 if문 앞 조건에서 처리를 할 수 있도록 하였다.

 

zipNum이 1보다 클 경우 해당 문자열은 이미 앞에서 문자열 압축이 이뤄진 것이니 압축이 된 문자열 숫자를 붙여주고,

이전 문자를 붙여줌과 동시에 현재 잘린 문자열이 기존 문자열의 마지막 단계가 아닐 경우 bef에 값을 저장하여 이후 들어올 잘린 문자열이 현재 값과 비교를 할 수 있게 조절해준다.

 

문자열이 서로 같을 경우 zipNum을 1올려줌과 동시에 현재 문자열이 마지막 문자열인지 체크하여 값을 추가해주도록 하여 값이 빠지는 경우가 생기지 않게 해 주었다.

 

이대로 끝났으면 좋겠지만 아직 2가지를 더 처리를 해 주어야 한다.

 

우선 현재 문자열을 자를 때 j의 값을 사용하게 되는데 해당 값은 무조건 1씩 오르기 때문에 2개 이상의 단어를 자르게 될 시 값이 중복되어 나오는 경우가 생기게 된다.

 

설명을 하자면 본인이 원하는 경우는 1, 3, 5, 7, 9와 같이 2단계씩 건너뛰려 하는데 이 코드를 지금 상태로 사용하게 될 시 1, 2, 3, 4, 5, 6, 7, 8, 9와 같이 1단계씩 건너 2 단어씩 잘라 앞 문자열의 뒷 단어와 뒷 문자열의 앞 단어가 곂치는 현상이 발생하는 것이다.

 

이를 해결하기 위해선 

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
			
        int endNum = i+j > s.length() ? s.length() : i+j;

        if (!sub.equals(bef)) {
					
            if(zipNum > 1) {
                sb.append(zipNum);
            }

            if (!bef.equals("")) {

                if (endNum != s.length()) {
                    sb.append(bef);
                }
                else {
                    sb.append(bef).append(sub);
                }	
            }

            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;

            if (endNum == s.length()) {
                sb.append(zipNum).append(bef);
            }
        }
    
    	//단계를 조절하기 위해 값을 현재 end값 -1로 명시해준다.
        j = endNum - 1;
    }
}

j = 문자열 자르는 단계 (index)

 

j를 마지막 잘랐던 index번호로 변경해주면 반복문이 작동하며 j++로 인해 의도했던 번호보다 1이 많아지게 되어 -1을 해주면 정상적으로 작동하게 된다.

 

subString의 용도상 startIndex부터 ~ endIndex전까지 잘라주게 되는데 위와 같이 해주면 이전 문자열의 index 다음 index부터 문자열을 잘라주게 되어 값의 중복 없이 정상적으로 작동하게 된다.

 

마지막으로

answer = answer > sb.toString().length() ? sb.toString().length() : answer;

잘랐던 문자열을 toString()으로 하나의 문자열로 뽑아 보았을 때 해당 문자열의 길이가 이전에 잘라서 합친 문자열보다 짧을 경우 해당 값을 정답으로 제출하기 위해 비교하며 저장한다.

 


제출 코드

class Solution {
    public int solution(String s) {
    
        int answer = s.length();
	
		for(int i = 1; i< s.length() /2 +1; i++) {
        
			StringBuffer sb = new StringBuffer();			
			String bef = "";
			int zipNum = 1;
            
			for (int j = 0; j < s.length(); j++) {
				
				int endNum = i+j > s.length() ? s.length() : i+j;				
				String sub = s.substring(j, endNum);
                
				if (!sub.equals(bef)) {
					
					if(zipNum > 1) {
						sb.append(zipNum);
					}
					
					if (!bef.equals("")) {

						if (endNum != s.length()) {
							sb.append(bef);
						}
						else {
							sb.append(bef).append(sub);
						}	
					}
					
					bef = sub;
					zipNum = 1;
				}
				else {
                
					zipNum++;
					
					if (endNum == s.length()) {
						sb.append(zipNum).append(bef);
					}
				}
				j = endNum - 1;
			}
			answer = answer > sb.toString().length() ? sb.toString().length() : answer;
		}
		
        return answer;
    }
}

 


해당 문제를 풀면서 stream을 사용하며 풀거나 재귀 함수를 사용할 시 훨씬 코드가 줄어들게 되지만 아직 실력이 부족하기도 하고 외부 라이브러리를 사용하지  않고 직접 돌아가는 상황을 작성해보고 싶어 위와 같이 작성하게 되었다.

 

다음에 stream이나 재귀함수를 공부하며 Refactoring 해 보아야겠다.

728x90

+ Recent posts