CodingTest/Leetcode

[Leetcode]#496. Next Greater Element I

Jay_J 2024. 6. 7. 11:27

문제 : 

 

예시 :

 

 

문제는 보기보다 간단하다. 두개의 배열이 파라미터로 주어지고, nums1을 순회하며 nums1[i]보다 큰 값이 nums2[j]의 "오른쪽" 에 있는지 확인하고, 있다면, 결과 리스트에 값을 추가하고, 없다면 -1을 추가한다. 

 

아래는 내가 제출해 통과한 코드이다.

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        ans =[]
        for i in range(len(nums1)):
            idx = nums2.index(nums1[i])
            found = False
            for j in range(idx + 1, len(nums2)):
                if(nums1[i] < nums2[j]):
                    ans.append(nums2[j])
                    found = True
                    break
            if not found:
                ans.append(-1)
        return ans

 

- 먼저, 결과값들을 담을 배열 ans를 선언한다.

- 우리의 목적은 nums1에 있는 값들보다 큰 값을 찾아야 하므로, nums1은 무조건 처음부터 끝까지 순회해야 한다.

- 이 문제는 정렬을 할 수도없고, nums1[i]보다 큰 값을 찾기 위해 배열의 처음부터 검색 할 수 없었다. 문제의 조건을 다시 읽어보면 왜 그런지 알 것이다.

- 그래서 idx라는 변수를 선언해 nums1[i]는 nums[j]의 몇번째 인덱스에 있는지 저장한다. 

- 그 인덱스를 찾으면, 그 인덱스 이전의 값은 nums[i]보다 크던 말던 검사 할 필요가 없다. 또한, 우리는 nums2배열의 특정 값의 오른쪽에서만 값이 큰지 아닌지만 확인하면 되므로, 불필요한 검사 1회를 줄이기 위해 idx + 1을 해 주었다.

- 조건을 검사하고, 조건에 부합하면 .append를 이용해 결과 리스트에 값을 담아준다. 그리고 그 다음 요소들은 더 큰 값이 있던 말던 우리는 더 큰 값 하나만 찾으면 되므로 break를 통해 for문을 빠져 나온다.

-마지막으로, 위에서 선언한 found라는 변수를 확인해주고, 만약 배열의 끝까지 도달하더라도 찾지 못했다면 -1을 더해준다.

 

아래는 내가 받은 report이다.

 

현재 코드의 시간 복잡도는 nums1의 각 요소에 대해 nums2를 반복해서 검색하기 때문에 O(n * m)이다. 여기서 n은 nums1의 길이, m은 nums2의 길이이다.

 

여기서 난 어떠한 자료구조도 사용하지 않고 brute force를 사용해 풀었지만, 이 문제는 stack을 활용해도 쉽게 풀릴 문제였다. 스택을 사용한 방법의 시간 복잡도는 O(n + m)이다.

 

다음은 solution에서 가져온 최적화된 코드이다.

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        next_greater = {}
        stack = []
        
        # Iterate through nums2 to find the next greater elements
        for num in nums2:
            while stack and stack[-1] < num:
                next_greater[stack.pop()] = num
            stack.append(num)
        
        # Use the next_greater dictionary to build the result for nums1
        ans = [next_greater.get(num, -1) for num in nums1]
        
        return ans

 

- nums2를 순회하며 스택을 사용해 각 요소의 다음 큰 요소를 찾는다.

- while stack은 stack에 요소가 남아있는동안 실행된다. 

- stack[-1] < num은 stack의 맨 위에 있는 요소가 num보다 작을동안 실행된다.
-스택의 상단에 있는 요소가 현재 요소보다 작으면, 스택에서 제거하고 그 요소의 다음 큰 요소로 현재 요소를 저장한다.
-nums2를 다 순회한 후, next_greater 딕셔너리에 각 요소의 다음 큰 요소가 저장된다.
-nums1의 각 요소에 대해 next_greater 딕셔너리에서 값을 찾아 결과 리스트를 만든다.

 

현재 코드의 시간 복잡도가 비효율적인 이유와 개선된 코드가 더 효율적인 이유는 다음과 같다:

현재 코드의 시간 복잡도
현재 코드는 `nums1`의 각 요소에 대해 `nums2`에서 해당 요소를 찾고, 그 다음에 다음 큰 요소를 찾기 위해 `nums2`의 나머지 부분을 탐색한다. 구체적으로는:

1. `nums1`의 각 요소에 대해 `nums2.index(nums1[i])`를 호출한다. 이 연산은 최악의 경우 O(m) 시간이 걸린다.
2. 그 다음, `nums2`의 나머지 요소를 탐색하여 다음 큰 요소를 찾는다. 이 또한 최악의 경우 O(m) 시간이 걸린다.

따라서 전체 시간 복잡도는 O(n * m)이다. 여기서 `n`은 `nums1`의 길이, `m`은 `nums2`의 길이이다.

최적화된 코드의 시간 복잡도
최적화된 코드는 스택을 사용하여 다음 큰 요소를 찾는다. 이 방법은 각 요소를 한 번씩만 처리하므로 시간 복잡도가 개선된다:

1. `nums2`를 한 번 순회하며 스택을 사용하여 다음 큰 요소를 찾는다. 각 요소는 스택에 한 번 추가되고, 한 번 제거된다. 이 과정은 O(m) 시간이 걸린다.
2. `nums1`의 각 요소에 대해 `next_greater` 딕셔너리에서 값을 조회한다. 딕셔너리 조회는 평균적으로 O(1) 시간이 걸리므로, 이 과정은 O(n) 시간이 걸린다.

따라서 전체 시간 복잡도는 O(m + n)이다.

왜 최적화된 코드가 더 효율적인가?
최적화된 코드는 각 요소를 한 번씩만 처리하고, 다음 큰 요소를 찾기 위해 스택을 사용한다. 이 방법은 요소를 중복해서 탐색하지 않기 때문에 시간 복잡도가 개선된다. `nums1`과 `nums2`의 길이가 클수록 이 차이는 더욱 두드러진다.