https://leetcode.com/problems/two-sum-ii-input-array-is-sorted

 

오름차순으로 정렬된 int [] numbers가 주어졌을 때 합이 target이 되는 두 수를 찾아라.

정확히 하나의 답이 있다고 보장.

두 개의 포인터를 가지고 합을 확인하는 과정이 필요하다.

이때 정렬 돼 있다는 조건을 활용하기 위해 투 포인터를 양 쪽 끝에서 출발해 범위를 좁혀온다.

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int left = 0;
        int right = numbers.length - 1;

        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum > target) {
                right--;
            } else if (sum < target) {
                left++;
            } else {
                return new int[]{left + 1, right + 1};
            }
        }
        return null; // 어차피 답은 반드시 존재한다.
    }
}

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 15 - 3Sum  (0) 2024.10.04
LeetCode 11 - Container With Most Water  (0) 2024.10.04
LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 151 - Reverse Words in a String  (1) 2024.10.03
LeetCode 12 - Integer to Roman  (0) 2024.10.03

https://leetcode.com/problems/zigzag-conversion

주어진 문자열 s를 numRows에 맞게 지그재그 패턴으로 만든 다음 row별로 읽어서 합친다.

예를 들어 s = “PAYPALISHIRING”, numRows = 4 이면 output은 “PINALSIGYAHRPI”가 된다.

P     I    N
A   L S  I G
Y A   H R
P     I

규칙을 살펴보자.

나는 String [] 배열이 numRows 수만큼 존재한다고 가정하겠다.

그렇다면 배열의 크기를 정하는게 중요해지는데 numRows = 1인 경우 가장 긴 길이의 배열이 필요할 것이다.

따라서 s의 길이만큼의 배열을 생성하도록 하겠다.

문자의 할당은 어떻게 할 것인가.

문자의 할당 방식은 아래쪽 방향 이동, 오른쪽 위 대각선 방향 이동 두 가지가 존재한다.

우선은 아래 방향으로 이동하다가 최대로 이동할 수 있는 곳(numRows)에 도달하면 방향을 바꿔 오른쪽 대각선 방향 이동을 하게 하면 된다.

class Solution {

    private static final int[][] DIRECTION = {{1, 0}, {-1, 1}};

    public String convert(String s, int numRows) {
        if (numRows == 1) {
            return s;
        }

        int n = s.length();
        char[][] rows = new char[numRows][n];
        for (char[] row : rows) {
            Arrays.fill(row, ' ');
        }
        // 시작 위치
        int r = 0;
        int c = 0;
        int d = 0;

        for (char ch : s.toCharArray()) {

            rows[r][c] = ch;

            r += DIRECTION[d][0];
            c += DIRECTION[d][1]; 

            if (!isValidPosition(r, c, rows)) {
                r -= DIRECTION[d][0];
                c -= DIRECTION[d][1];   
                d = (d + 1) % DIRECTION.length;
                r += DIRECTION[d][0];
                c += DIRECTION[d][1]; 
            }
        }
        
        StringBuilder sb = new StringBuilder();
        for (char[] row : rows) {
            for (char ch : row) {
                if (ch != ' ') {
                    sb.append(ch);
                }
            }
        }

        return sb.toString();
    }

    private boolean isValidPosition(int r, int c, char[][] rows) {
        return r >= 0 && r < rows.length && c >= 0 && c < rows[r].length;
    }
}

 

코드를 개선해보자. 현재 낭비되는 공간이 많다.

또한 대각선 이동도 정확한 열의 위치가 필요한 것이 아니다. 단지 어느 행에 위치하는지가 중요하다.

2차원 배열이 아니라 StringBuilder 배열로 변경하면 각 row를 처리하면서 공간 낭비를 줄일 수 있다.

class Solution {
    public String convert(String s, int numRows) {
        if (numRows == 1 || s.length() <= numRows) {
            return s;
        }

        StringBuilder[] rows = new StringBuilder[numRows];
        for (int i = 0; i < numRows; i++) {
            rows[i] = new StringBuilder();
        }
        
        int currentRow = 0;
        boolean goingDown = false;

        for (char c : s.toCharArray()) {
            rows[currentRow].append(c);

            if (currentRow == 0 || currentRow == numRows - 1) {
                // 방향 전환
                goingDown = !goingDown;
            }

            currentRow += goingDown ? 1 : -1;
        }

        StringBuilder result = new StringBuilder();
        for (StringBuilder row : rows) {
            result.append(row);
        }

        return result.toString();
    }
}

https://leetcode.com/problems/reverse-words-in-a-string

 

문자열에서 단어들을 꺼내 역순으로 반환하는 문제이다. 각 단어들은 하나의 공백을 사이에 두고

합쳐진다.

예를 들어 “ hello world “는 “world hello”로 반환된다.

보다시피 원본 문자열에 띄어쓰기가 여러 개 존재할 수도 있다.

가장 먼저 떠오른 방법은 문자열 리스트에 그냥 단어별로 파싱 해서 넣고 조작하는 것이다.

class Solution {
    public String reverseWords(String s) {
        List<String> words = new ArrayList<>();

        StringBuilder word = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') {
                if (!word.isEmpty()) {
                    words.add(word.toString());
                }
                word = new StringBuilder();
            } else {
                word.append(s.charAt(i));
            }
        }
        if (!word.isEmpty()) {
            words.add(word.toString());
        }

        StringBuilder answer = new StringBuilder();
        for (int i = words.size() - 1; i > 0; i--) {
            answer.append(words.get(i)).append(" ");
        }
        answer.append(words.get(0));

        return answer.toString();
    }
}

앞에서부터 문자를 하나씩 탐색하며 단어를 완성시켜 나간다.

그러다가 공백을 만나면 기존에 쌓아왔던 문자들을 하나의 단어로 판정하고 리스트에 넣고 쌓아오던 문자열을 초기화한다.

최종적으로는 역순으로 뒤에서부터 단어를 꺼내 결과를 생성한다.

그런데 자바에서 제공해 주는 메서드를 통해 더 간단하게 해결이 가능하다.

class Solution {
    public String reverseWords(String s) {
        
        String[] words = s.trim().split("\\\\s+");// 좌우 공백제거, 공백이 1개 이상인 것을 기준으로 분리
        StringBuilder reversed = new StringBuilder();
        
        for (int i = words.length - 1; i >= 0; i--) {
            reversed.append(words[i]);
            if (i > 0) reversed.append(" ");
        }

        return reversed.toString();
    }
}

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 167 - Two Sum II - Input Array Is Sorted  (0) 2024.10.03
LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 12 - Integer to Roman  (0) 2024.10.03
LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02

https://leetcode.com/problems/integer-to-roman

 

Symbol 별로 숫자가 할당되어 있다.

10진수의 수를 이제 로마수로 변경해야 한다. 변경은 다음과 같은 과정을 통한다.

  1. 수가 4 혹은 9로 시작하지 않는다면 input으로부터 뺄 수 있는 최대 symbol을 선택한다. 그리고 해당 symbol을 결과에 추가한다. 그리고 해당 숫자를 뺀 나머지를 로마수로 재할당한다.
  2. 만약 수가 4 혹은 9로 시작하다면, 해당 값을 뺄샘 형태로 변환하여 로마수로 표현한다. 예를 들어 4는 5(V)에서 1(I)을 뺀 값이 되므로 IV가 된다.
  3. 10의 거듭제곱은 최대 3번까지 연속해서 붙일 수 있다. 하지만 5(V), 50(L), 500(D)은 여러 번 반복해서 사용할 수 없다. 만약 어떤 기호가 4번 반복해서 사용해야 할 경우에는 뺄셈 형태로 변환한다.

우선 하나씩 생각해보자.

주어진 숫자 num의 첫자리 숫자를 어떻게 판단 할 것인가?

4냐 400이냐에 따라 조합이 달라지기 때문에 첫 자리 숫자가 어떤 수인지를 넘어 자릿수까지 판단을 해야 한다.

문자열로 판단하는 게 낫겠다는 생각이 든다. int num = 3749라고 가정해 보자.

문자열로 바꾸면 “3749”, 길이가 4인 문자열이 된다.

첫자리는 3 * 10 ^4 그다음은 7 * 10^3, 410^2, 910^1 이렇게 된다. 매 순간 판단하는 숫자가 문자열의 길이와 chartAt(n)으로 편리하게 구할 수 있다.

class Solution {

    private static final Map<Integer, String> symbolMap = new LinkedHashMap<>();

    static {
        symbolMap.put(1000, "M");
        symbolMap.put(500, "D");
        symbolMap.put(100, "C");
        symbolMap.put(50, "L");
        symbolMap.put(10, "X");
        symbolMap.put(5, "V");
        symbolMap.put(1, "I");
    }

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();
        String number = Integer.toString(num);
        int length = number.length();

        for (int i = 0; i < length; i++) {
            int coefficient = number.charAt(i) - '0';
            int exponent = length - i - 1;
            
            int now = coefficient * (int)Math.pow(10, exponent);
            
            if (coefficient == 4 || coefficient == 9) {
                for (int key : symbolMap.keySet()) {
                    if (key > now && symbolMap.containsKey(key - now)) {
                        sb.append(symbolMap.get(key - now));
                        sb.append(symbolMap.get(key));
                    }
                }
            } else {
                for (int key : symbolMap.keySet()) {
                    while (now >= key) {
                        now -= key;
                        sb.append(symbolMap.get(key));
                    }
                }
            }
        }

        return sb.toString();
    }
}

일단 통과는 됐다.

간단하게 설명하자면 우선 LinkedHashMap을 사용해 데이터를 삽입된 순서대로 저장되도록 처리했다.

그 후 주어진 num을 문자로 변경하고 이 문자열을 조작해 문제를 풀었다.

그런데 매 숫자마다 일일이 확인하고 변환하는 과정 때문에 효율적이지 않다.

그러면 애초에 가능한 로마 symbol을 다 저장해 놓으면 되는 거 아닐까? 생각보다 개수가 몇 개 없으니 가능한 방식이다.

class Solution {

    private static final Map<Integer, String> SYMBOL_MAP = new LinkedHashMap<>();

    static {
        SYMBOL_MAP.put(1000, "M");
        SYMBOL_MAP.put(900, "CM");
        SYMBOL_MAP.put(500, "D");
        SYMBOL_MAP.put(400, "CD");
        SYMBOL_MAP.put(100, "C");
        SYMBOL_MAP.put(90, "XC");
        SYMBOL_MAP.put(50, "L");
        SYMBOL_MAP.put(40, "XL");
        SYMBOL_MAP.put(10, "X");
        SYMBOL_MAP.put(9, "IX");
        SYMBOL_MAP.put(5, "V");
        SYMBOL_MAP.put(4, "IV");
        SYMBOL_MAP.put(1, "I");
    }

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();

        for (int key : SYMBOL_MAP.keySet()) {
            while (num >= key) {
                num -= key;
                sb.append(SYMBOL_MAP.get(key));
            }
        }

        return sb.toString();
    }
}

그런데 더 개선할 점이 있는 것 같다.

데이터셋 저장을 LinkedHashMap을 사용함으로써 삽입 순서를 유지한 것은 좋았지만 각 반복에서 Map의 엔트리에 접근하는 데 추가적인 동작이 들어간다.

LinkedHashMap은 내부적으로 LinkedList를 사용하니까 메모리 접근이 배열에 비해서 덜 효율적일 수 있다.

따라서 해당 저장을 두 개의 배열로 나누어 저장해서 접근하면 조금 더 효율적인 결과를 얻을 수 있을 것이다.

class Solution {
    private static final int[] VALUES = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
    private static final String[] SYMBOLS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};

    public String intToRoman(int num) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < VALUES.length; i++) {
            while (num >= VALUES[i]) {
                num -= VALUES[i];
                sb.append(SYMBOLS[i]);
            }
        }

        return sb.toString();
    }
}

이 문제는 결국 공간과 시간의 트레이드오프를 보여줬던 문제였던 것 같다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 6 - Zigzag Conversion  (1) 2024.10.03
LeetCode 151 - Reverse Words in a String  (1) 2024.10.03
LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02
LeetCode 150 - Candy  (1) 2024.09.30

https://leetcode.com/problems/roman-to-integer

 

symbol 별로 숫자값이 할당 돼 있다.

처음 구상은 주어진 문자열 s를 char로 바꿔 하나씩 symbol을 확인해 가며 숫자로 변경하고 더하는 방식이었다.

하지만 문제에는 예외가 있었다. 큰 숫자에서 작은 숫자 순서로 나오는 로마 숫자이지만 작은 숫자 다음 큰 숫자가 나오면 뺄셈이 적용된다는 것이다.

예를 들어 문자열 s = “MCMXCIV”일 떄를 보자.

이는 [’M’, ‘C’, ‘M’, ‘X’, ‘C’, ‘I’, ‘V’] 로 바뀐다. 이를 숫자로 바꾸면 [1000, 100, 1000, 10, 100, 1, 5]가 된다.

이제 합을 누적 해 가야 한다. 단 다음 수보다 작은 수의 경우는 뺄 샘을 해준다.

1000 - 100 +1000 - 10 + 100 - 1 + 5 = 1994 가된다.

class Solution {

    static Map<Character, Integer> symbolMap = new HashMap<>();

    public int romanToInt(String s) {
        int n = s.length();
        if (n == 0) {
            return 0;
        }
        int answer = 0;
        init();

        for (int i = 0; i < n - 1; i++) {
            int symbolValue = symbolMap.get(s.charAt(i));
            int nextSymbolValue = symbolMap.get(s.charAt(i + 1));

            if (symbolValue < nextSymbolValue) {
                answer -= symbolValue;
            } else {
                answer += symbolValue;
            }

        }
        return answer + symbolMap.get(s.charAt(n - 1));
    }

    private void init() {
        symbolMap.put('I', 1);
        symbolMap.put('V', 5);
        symbolMap.put('X', 10);
        symbolMap.put('L', 50);
        symbolMap.put('C', 100);
        symbolMap.put('D', 500);
        symbolMap.put('M', 1000);
    }
}

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 151 - Reverse Words in a String  (1) 2024.10.03
LeetCode 12 - Integer to Roman  (0) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02
LeetCode 150 - Candy  (1) 2024.09.30
LeetCode 134 - Gas Station  (1) 2024.09.29

https://leetcode.com/problems/trapping-rain-water/description

 

얼마나 많은 양의 물을 가둘 수 있을까?

주어지는 배열은 각 지점의 높이이다.

idx에 가둬진 물의 양이란 idx 높이 위에 쌓인 물과도 같다.

물이 쌓인다는 것은 양 옆에 자신보다 높이가 큰 벽이 존재해야 가능하다.

예제를 보며 파악해 보자. int [ ] heights = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]이다.

idx = 5일 때를 보자. idx = 5인 지점의 높이는 0이다. 이곳에 물이 쌓이기 위해서는 양 옆으로 0보다 큰 높이가 존재해야 한다.

여기서 물이 쌓이기 위한 조건이 결정됐다. 그건 바로 자신의 위치 양 옆 방향으로 자신보다 큰 높이가 존재해야 한다는 것이다.

그렇다면 양은 어떻게 결정이 될까?

자신을 기준으로 해서 좌우의 자신보다 높이가 높은 것 중 작은 것과 자신의 높이의 차이만큼 쌓이다.

어떻게, 얼마나 쌓이는지는 확인이 됐다.

양 끝점은 한쪽 벽이 없기에 어차피 물이 쌓이지 않는다. 그리고 양 쪽의 높이를 확인해야 하기 때문에 가장 높이가 높은 지점의 위치를 정해두면 두 지점 중 한 지점이 정해지니까 나머지의 파악이 쉬워진다.

class Solution {
    public int trap(int[] height) {
        int maxIdx = findMaxIdx(height);
        int l = 0;
        int r = height.length - 1;

        int left = height[l];
        int right = height[r];

        int answer = 0;
        // left to max
        for (int i = 1; i < maxIdx; i++) {
            left = Math.max(left, height[i - 1]);
            if (left > height[i]) {
                answer += left - height[i];
            }
        }

        // right to max;
        for (int i = r - 1; i > maxIdx; i--) {
            right = Math.max(right, height[i + 1]);
            if (right > height[i]) {
                answer += right - height[i];
            }
        }

        return answer;
    }
    
    private int findMaxIdx(int[] height) {
        int maxHeight = 0;
        int maxIdx = 0;

        for (int i = 0; i < height.length; i++) {
            if (maxHeight < height[i]) {
                maxHeight = height[i];
                maxIdx = i;
            }
        }

        return maxIdx;
    }
}

우선은 이렇게 풀었다. 그렇지만 반복문을 3번이나 순회하고 있다.

또한 배열의 길이가 0이나 1인 경우에 대한 대처가 부족하다.

class Solution {
    public int trap(int[] height) {
        // 엣지 케이스 처리
        if (height == null || height.length <= 2) return 0;
        
        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        int water = 0;
        
        // 배열을 한 번만 순회하며 물의 양을 계산
        while (left < right) {
            if (height[left] < height[right]) {
                // 왼쪽 벽이 더 낮은 경우
                if (height[left] >= leftMax) {
                    leftMax = height[left];
                } else {
                    water += leftMax - height[left];
                }
                left++;
            } else {
                // 오른쪽 벽이 더 낮거나 같은 경우
                if (height[right] >= rightMax) {
                    rightMax = height[right];
                } else {
                    water += rightMax - height[right];
                }
                right--;
            }
        }
        
        return water;
    }
}

이렇게 하면 한번의 반복을 수행한다.

또한 left, right 둘 중 최대 높이에 도달하면 나머지 한쪽만 계속 이동을 하면서 가둬진 물을 추가해 나간다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 12 - Integer to Roman  (0) 2024.10.03
LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 150 - Candy  (1) 2024.09.30
LeetCode 134 - Gas Station  (1) 2024.09.29
LeetCode 238 - Product of Array Except Self  (0) 2024.09.29

https://leetcode.com/problems/candy/

 

n 명의 줄 서있는 아이들.

각각의 아이들에는 rating이 할당 돼 있다. 이런 아이들에게 다음 조건에 따라 사탕을 줄 것이다.

  • 각각 아이들은 최소한 하나의 사탕을 받아야만 합니다.
  • rating이 높은 아이는 인접한 아이보다 더 많은 사탕을 받습니다.

아이들에게 나눠줘야 하는 사탕의 최소 개수를 구하여라.

어차피 1개씩은 다 받아야 하니까 모든 아이에게 1개씩을 나눠주고 보자.

i 기준에서 i-1과 i+1의 값을 비교해야 한다. 양 옆의 값 중 자신보다 작은 값이 존재한다면 사탕을 양 옆보다 더 줘야 한다.

예시를 보겠다.

[1, 0, 2] 일 때 우선 가운데 1번 idx의 아이는 사탕을 1개만 받아도 된다.

0번 idx는 1번 idx보다 rating이 크므로 2개를 받아야 한다.

2번 idx는 1번 idx보다 rating이 크므로 2개를 받아야 한다.

양쪽 방향에서 중앙으로 접근해 가능 방식으로 푸는 게 좋을 것 같다.

  • 왼쪽에서 오른쪽 방향으로 훑으면서 오른쪽 이웃과 비교
  • 오른쪽에서 왼쪽 방향으로 훑으면서 왼쪽 이웃과 비교

그런데 이 두 결과를 어떻게 합쳐야 할까?

class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] candies = new int[n];
        Arrays.fill(candies, 1);

        // left to right;
        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1]) {
                candies[i] = candies[i - 1] + 1;
            }
        }

        // right to left;
        for (int i = n - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                // 이미 더 많다면 추가할 필요가 없다.
                if (candies[i] > candies[i + 1]) continue;
                candies[i] = candies[i + 1] + 1;
            }
        }

        return Arrays.stream(candies).sum();
    }
}

우선 왼쪽 → 오른쪽 방향으로 진행하면서 사탕을 분배한다.

그 후 오른쪽 → 왼쪽 방향으로 진행하면서 사탕을 조건에 따라 분배해야 하는데 만약 rating이 오른쪽보다 높은데도 이미 주어진 사탕이 오른쪽 사탕보다 많다면 더 이상 추가해 줄 필요가 없다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 13 - Roman to Integer  (1) 2024.10.03
LeetCode 42 - Trapping Rain Water  (0) 2024.10.02
LeetCode 134 - Gas Station  (1) 2024.09.29
LeetCode 238 - Product of Array Except Self  (0) 2024.09.29
LeetCode 380 - Insert Delete GetRandom O(1)  (0) 2024.09.29

https://leetcode.com/problems/gas-station

 

n 개의 gas station 원형 루트. gas [i] = gas의 양

i → i + 1 까지 가는데 cost [i] 만큼의 gas 가 필요하다.

어느 지점에서 시작해야 시계방향으로 완주할 수 있을까. 만약 완주가 불가능하다면 -1을 반환한다.

즉 현재 가지고 있는 gas가 cost보다 커야만 이동이 가능하다.

순회하는 것을 표현하는 방법은 몇가지 있겠지만 대표적으로 모듈러 연산을 사용하거나 같은 배열일 이어 붙여서 사이즈를 늘리는 방법이 떠오른다.

가장 간단한 풀이는 모든 점을 시작점으로 삼아 진행해 보는 것이다.

하지만 이 방법은 O(n^2)의 시간복잡도를 갖게 되고 문제 조건에서 n은 10^5이다. 도저히 써먹을 수 없다.

생각해 보자.

만약 총 gas가 총 cost보다 작다면 이거는 어느 지점에서 시작해도 정답을 구할 수 없다.

gas - cost를 순이익이라고 했을 때 누적 순이익 < 0 이 되는 지점이 있다면 그 지점 이전은 시작점이 될 수 없다. 왜냐하면 해당 지점에서 기름이 바닥이 날 것이기 때문이다.

따라서 그다음 지점부터 다시 시작해야 한다.

특히 문제 조건을 다시 유심히 살펴보자. 해결책이 있다면 유일하다는 조건이 있다. 그 다음 지점부터 다시 시작하면 답을 구할 수 있다는 것이다. 어차피 그 이전 지점들은 이미 답에서 탈락했으니까!

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int totalGas = 0;
        int totalCost = 0;
        int currentGas = 0;
        int start = 0;

        for (int i = 0; i < gas.length; i++) {
            totalGas += gas[i];
            totalCost += cost[i];
            currentGas += gas[i] - cost[i]; // 누적 순이익

            if (currentGas < 0) {
                // 출발점 탈락. 그 이후 점부터 가능
                start = i + 1;
                currentGas = 0;
            }
        }

        return totalGas >= totalCost ? start : -1;
    }
}

최종적으로 O(n)의 시간복잡도를 갖는 풀이가 완성된다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 42 - Trapping Rain Water  (0) 2024.10.02
LeetCode 150 - Candy  (1) 2024.09.30
LeetCode 238 - Product of Array Except Self  (0) 2024.09.29
LeetCode 380 - Insert Delete GetRandom O(1)  (0) 2024.09.29
LeetCode 274 - H-Index  (0) 2024.09.29

https://leetcode.com/problems/product-of-array-except-self

 

int [ ] answer;

answer [i] = nums에서 i 위치를 제외한 모든 원소의 곱

O(n)의 시간복잡도와 나눗셈 연산 없이 해결해야 한다.

가장 쉬운 방법은 모든 원소의 곱을 구하고 순회하며 나눗셈 연산을 하는 것이다.

하지만 나눗셈은 사용하면 안 되는 조건이 있다.

그렇다면 자신을 제외한 왼쪽곱, 오른쪽곱의 배열을 만들어 두 배열의 값을 곱해서 반환하면 된다.

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int[] left = new int[nums.length];
        Arrays.fill(left, 1);
        int p = 1;
        for (int i = 1; i < nums.length; i++) {
            left[i] = p * nums[i - 1]; 
            p *= nums[i - 1];
        }
        
        p = 1;
        int[] right = new int[nums.length];
        Arrays.fill(right, 1);
        for (int i = nums.length - 2; i >= 0; i--) {
            right[i] = p * nums[i + 1];
            p *= nums[i + 1];
        }

        int[] answer = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            answer[i] = left[i] * right[i];
        }

        return answer;
    }
}

그런데 위의 풀이를 보면 left, right, answer라는 추가 공간이 많이 사용됐다.

생각해 보면 그냥 answer 배열 하나에 다 처리를 해도 될 것 같다.

또한 배열을 1로 채워주고 있는데 시작 지점만 채워주면 된다.

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] answer = new int[n];

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

        int right = 1;
        for (int i = n - 1; i >= 0; i--) {
            answer[i] *= right;
            right *= nums[i];
        }

        return answer;
    }
}

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 150 - Candy  (1) 2024.09.30
LeetCode 134 - Gas Station  (1) 2024.09.29
LeetCode 380 - Insert Delete GetRandom O(1)  (0) 2024.09.29
LeetCode 274 - H-Index  (0) 2024.09.29
LeetCode 45 - Jump Game2  (0) 2024.09.29

https://leetcode.com/problems/insert-delete-getrandom-o1/

 

RandomizedSet이라는 class를 작성하는 문제이다.

각 메서드에는 구현해야 할 조건이 있다.

  • RandomizedSet() 생성자이다.
  • bool insert(int val) set에 val을 삽입하는 메서드이다. 만약 item이 존재하지 않았다면 true, 그렇지 않다면 false를 반환한다.
  • bool remove(int val) set에서 val을 제거한다. 만약 item이 존재했었다면 true, 그렇지 않다면 false를 반환한다.
  • int getRandom() 현재 set에서 random 원소를 반환한다. 문제 조건상 이 메서드가 호출됐을 때 적어도 하나의 원소가 set에 존재하는 것이 보장된다. 각각의 원소는 동일한 반환 확률을 가지고 있어야 한다.

이러한 메서드들을 평균 O(1)의 시간복잡도로 구현해야 한다.

사실 위의 세 개 메서드는 전혀 문제가 안 된다. Set 인터페이스에 다 구현 돼 있다.

Set 인터페이스의 add와 remove가 정확히 위의 insert, remove 메서드와 동일한 동작을 한다.

핵심은 동일한 확률로 하나를 꺼내 반환하는 getRandom 메서드이다.

하지만 O(1)의 시간복잡도로 꺼내려면 Set을 사용하면 안된다. Set에 있는 데이터를 꺼내려면 순회를 해야 한다.

다시 자료구조부터 살펴보며 선택해 보자.

우선 ArrayList다. 이는 마지막 요소 추가 및 무작위 접근이 O(1)로 가능하지만 특정 값 삭제가 O(N)이다.

HashMap은 삽입, 삭제, 검색이 O(1)로 가능하지만 무작위 요소 선택이 불가능하다.

getRandom은 ArrayList를 사용하면 해결이 가능할 것 같다. 그러면 이제 삽입, 삭제의 문제가 된다.

ArrayList내의 값(val)의 위치를 관리하는 별도 자료구조가 있으면 좋겠다. 그리고 접근이 빨라야 한다. 이게 HashMap이다.

class RandomizedSet {

    private List<Integer> numbers = new ArrayList<>();
    private Map<Integer, Integer> valueMap = new HashMap<>();
    private Random rand;

    public RandomizedSet() {
        rand = new Random();
    }
    
    public boolean insert(int val) {
        if (valueMap.containsKey(val)) {
            return false;
        }

        valueMap.put(val, numbers.size());
        numbers.add(val);
        return true;
    }
    
    public boolean remove(int val) {
        if (!valueMap.containsKey(val)) return false;

        int idxForDelete = valueMap.get(val);
        int lastIdx = numbers.size() - 1;
        int lastValue = numbers.get(lastIdx);
        // 삭제 위치의 값을 마지막 값과 위치 변경        
        numbers.set(idxForDelete, lastValue);
        valueMap.put(lastValue, idxForDelete);

        numbers.remove(lastIdx);
        valueMap.remove(val);
        return true;
    }
    
    public int getRandom() {
        return numbers.get(rand.nextInt(numbers.size()));
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

ArrayList에서 특정 위치의 값을 삭제하는 것은 O(N)의 시간복잡도를 갖는다. 그에 반해 마지막 값의 삭제는 O(1)이다.

따라서 두 값을 교환하면 결과적으로 원하는 위치의 값을 O(1)로 삭제하는 것과 같은 효과를 얻을 수 있다.

'Algorithm > LeetCode' 카테고리의 다른 글

LeetCode 134 - Gas Station  (1) 2024.09.29
LeetCode 238 - Product of Array Except Self  (0) 2024.09.29
LeetCode 274 - H-Index  (0) 2024.09.29
LeetCode 45 - Jump Game2  (0) 2024.09.29
LeetCode 55 - Jump Game  (1) 2024.09.29

+ Recent posts