Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

75번 문제 : 최대슬라이딩 윈도우 - 큐를 통한 최적화에 대한 반례 #25

Closed
jsrimr opened this issue Aug 31, 2020 · 3 comments

Comments

@jsrimr
Copy link

jsrimr commented Aug 31, 2020

안녕하세요, 책을 통해 공부하던 중 20장의 75번 큐를 위한 최적화에 대해 질문이 있어 메시지 드립니다.

최댓값이 윈도우에서 빠지면 current_max 를 -inf 로 초기화하고 current_max 가 -inf 인 경우에는 window 에서 max 값을 구해 current_max 를 구하게 되는데요, 이렇게 되면 입력배열이 [1억, 99999999, 99999998...1] 이 주어지는 경우에 매번 새로 max 값을 구해야해서 비효율적이지 않나 하는 의문이 들어 메시지 드립니다. 코드가 이 경우를 어떻게 커버하는 것일까요?

유명한 문제이고, 많은 사람들의 감수를 거친 책의 풀이가 틀렸을 리가 없겠다는 생각이 들지만 질문하면서 저한테 깨달음이 있지 않을까 하는 마음에 메시지 드립니다!

다시 한 번 좋은 책 집필해주셔서 감사합니다~

@likejazz
Copy link
Collaborator

likejazz commented Sep 1, 2020

네, 맞습니다.
그런 경우가 빅오 챕터에서 얘기하는 최악의 경우입니다.

애초에 데이터가 정렬되어 있다는 정보를 사전에 알 수 있다면 어떤식으로든 최적화가 가능하겠지만 데이터가 어떤 상태인지 전혀 모르는 상황에서는 어떠한 알고리즘도 최악의 경우를 피하기 어렵습니다. 비슷한 예로 퀵소트가 있는데, 대부분의 경우에는 가장 빠른 속도를 보이지만 이미 정렬된 데이터가 입력값으로 주어지면 매 번 불필요한 파티셔닝 작업을 하게되고 버블소트와 다를 바 없는 성능이 나옵니다. 하지만 항상 느린 버블소트와 달리 퀵소트는 대부분의 데이터에서는 훨씬 더 빠른 속도를 보여주기 때문에 퀵소트를 좀 더 최적화된 알고리즘이라 얘기할 수 있는거죠.

정말로 최악의 경우를 방지하고 싶다면 맨 처음에 입력값이 역순인지 확인하고, 이 경우 다른 알고리즘을 적용하도록 분기할 수 있겠습니다. 책에서도 소개한 바 있는 팀소트 또한 입력값이 애초에 정렬된 상태인지 확인하는 로직이 포함되어 있고, 이 경우 이후 정렬 과정을 생략하기 때문에 O(n)에 정렬이 가능합니다.

@jsrimr
Copy link
Author

jsrimr commented Sep 1, 2020

답변감사합니다,

데이터가 어떤 상태인지 전혀 모르는 상황에서는 최악의 경우를 피하기 어려운게 맞지만 이번 문제는 특별히 어떤 경우에도 linear time 에 답을 낼 수 있는 알고리즘이 있는 것 같아서요~

아래는 코드인데, 큐가 항상 maximum 을 O(1)에 내놓을 수 있도록 준비시키는 알고리즘입니다. 큐는 왼쪽에 있는 숫자일수록 크기가 크도록 유지하며, 새로운 숫자가 큐에 들어올 때 오른쪽에서부터 기존의 숫자들을 비교해 새로운 숫자보다 작으면 큐에서 뺴는 방식으로 동작합니다. for 문 안에 while 문이 들어가긴 하지만 모든 while 문이 실행되는 시간은 결국 입력리스트 길이에 비례하기 때문에 O(n) 이 유지됩니다.

from typing import List
from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        #  [3  -1  -3] 5 -> 앞에 있는 숫자일수록 크기라도 해야 하는데, 작으면 밀려버림 -> [5]
        # [5] 3 -> [5,3]
        # [5,3] 6 -> [6]

        # [1,3,-1] -> [3,-1]
        # [3,-1] -3 -> [3, -1, -3]
        # [3, -1 -3] 5 -> [5]


        Q = deque()
        answer = []

        def control(i):
            if Q and Q[0] == i-k :
                Q.popleft()

            while Q and nums[Q[-1]] < nums[i]:
                Q.pop()

            Q.append(i)

        for i in range(k):
            control(i)
            
        answer.append(nums[Q[0]])

        for i in range(k, len(nums)):
            control(i)
            answer.append(nums[Q[0]])


        return answer

@likejazz
Copy link
Collaborator

likejazz commented Sep 1, 2020

네, 좋은 알고리즘이네요.

입력값에 관계 없이 항상 고른 성능을 낸다는 점에서 매우 훌륭한 알고리즘 같습니다. 당연한 얘기지만 책 보다 더 좋은 알고리즘도 얼마든지 있을 수 있습니다. 좋은 알고리즘이 있으면 많이 소개해주세요.

한 가지 아쉬운 점은 일반적인 경우에는 기존 책에 있는 알고리즘 보다 성능이 더 떨어지는거 같습니다. 최악의 경우가 항상 있는건 아니기 때문에 전체적인 성능이 떨어진다면 좋은 알고리즘이라 할 수 없겠죠. 실제로 리트코드에도 제출해보면 기존 보다 2배 정도 더 늦게 실행됩니다. 이 부분이 개선된다면 훨씬 더 좋은 알고리즘이 될 수 있을거 같네요.

@likejazz likejazz closed this as completed Sep 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants