21. Remove Duplicate Letters

아현·2021년 3월 25일
0

Algorithm

목록 보기
22/400

리트코드

  • 중복된 문자를 제외하고 사전식 순서(Lexicographical Order)로 나열하라


1. 재귀를 이용한 분리 (52ms)


class Solution:
    def removeDuplicateLetters(self, s: str) -> str:
        #집합으로 정렬
        
        for char in sorted(set(s)):
            suffix = s[s.index(char):]
            
            #전체 집합과 접미사 집합이 일치할 때 분리 진행
            
            if set(s) == set(suffix):
                return char + self.removeDuplicateLetters(suffix.replace(char,''))
            
        return ''

  • 사전식 순서
    : 글자 그대로 사전에서 가장 먼저 찾을 수 있는 순서

    • bcabc에서 중복 문자를 제외하면 사전에서 가장 먼저 찾을 수 있는 문자열은 abc가 될 것이다.

    • 만약, 앞에 e 문자가 하나 더 붙은 ebcabc가 입력값이라면 결과는 eabc가 될 것이다.

      • e 문자 자체는 해당 문자열 내에서 사전 순으로는 가장 뒤에 있지만, e는 입력값에서 딱 한 번만 등장하고 a, b, c는 뒤이어 등장하기 때문에 e의 위치를 변경할 수 없기 때문이다.

      • 반면, 입력값이 ebcabce라면 첫 번째 e는 중복으로 제거할 수 있고, 마지막 e를 남겨서 결과는 abce가 될 수 있을 것이다.


  • 중복 문자를 제외한(여기서는 집합으로 처리) 알파벳 순으로 문자열 입력값을 모두 정렬한 다음, 가장 빠른 a부터 접미사 suffix를 분리하여 확인한다.

    • 다음 순서는 b인데, b의 경우 c,d가 뒤에 올 수 없기 때문에 이를 기준으로 분리할 수 없다.

    • 분리 가능 여부는 이 코드와 같이 전체 집합과 접미사 집합이 일치하는지 여부로 판별한다.

      • 집합은 중복된 문자가 제거된 자료형이므로, b를 기준으로 했을 때는 s = {'b', 'c', 'd'}, suffix = {'b', 'c'}가 되며 d를 처리할 수 없으므로 분리할 수 없다. 따라서 다음 순서인 c로 넘긴다.
      • 이제 c는 s = {'b', 'c', 'd'}, suffix = {'b','c', 'd'}로 일치 여부를 판별하는 if문이 True이므로 분리할 수 있다.

  • 이제 여기서 부터 실제로 분리를 처리하게 되는데, 현재 문자, 즉 c를 리턴하는 재귀호출 구조로 처리한다.

    • 또한 c는 이미 분리되는 기준점이 되었으므로 이후에 이어지는 모든 c는 replace( )로 제거한다.
    • 이렇게 하면 일종의 분할정복과 비슷한 형태로, suffix의 크기는 점점 줄어들게 되고 더 이상 남지 않을 때 백트래킹 되면서 결과가 조합된다.

<재귀를 이용한 분리>


  • cbacdcbc를 사전식 순서로 중복 문자를 제거하는 과정의 알고리즘은 다음과 같으며, 결과는 파란색 글자인 acdb가 된다.



2. 스택을 이용한 문자 제거 (32ms)


class Solution:
    def removeDuplicateLetters(self, s: str) -> str:
        counter, seen, stack = collections.Counter(s), set(), []
        
        for char in s:
            counter[char] -= 1
            if char in seen:
                continue
                
            #뒤에 붙일 문자가 남아 있다면 스택에서 제거
            while stack and char < stack[-1] and counter[stack[-1]] > 0:
                seen.remove(stack.pop())
            
            stack.append(char)
            seen.add(char)
            
        return ''.join(stack)



  • 먼저 스택에는 앞에서부터 차례대로 쌓아 나간다

  • 만약, 현재 문자 char가 스택에 쌓여 있는 문자 (이전 문자보다 앞선 문자)이고, 뒤에 다시 붙일 문자가 남아 있다면(카운터가 0 이상이라면), 쌓아둔 걸 꺼내서 없앤다.

    • 카운팅에는 collections.Counter()를 이용한다.

      • 이 모듈은 문자별 개수를 자동으로 카운팅해주기 때문에 다른 장에서도 여러 문제에 다양하게 활용된다.
  • seen은 집합 자료형(set)으로 이미 처리된 문자 여부를 확인하기 위해 사용했으며, 이처럼 이미 처리된 문자는 스킵한다.

  • 처리된 문자 여부를 확인하기 위해 in을 이용한 검색 연산으로 찾아냈다.

    • 해당 기능은 스택에는 존재하지 않는 연산이기 때문에 별도의 집합 자료형에만 사용했다.

    • 리스트는 검색도 잘 지원하기 때문에 굳이 스택이라는 자료구조로 한정짓지 않고 풀이한다면 seen변수 없이도 다음과 같은 형태로 얼마든지 풀이가 가능하다.

  	
    def removeDuplicateLetters(self, s:str) -> str:
        counter, stack = collections.Counter(s), []
        
        for char in s:
            counter[char] -= 1
            if char in stack:
                continue
                
            # 뒤에 붙일 문자가 남아 있다면 스택에서 제거
            
            while stack and char < stack[-1] and counter[stack[-1]] > 0:
                stack.pop()
                
             stack.append(char)
             
       return ''.join(stack)
  
  • 하지만, 다음과 같은 방식은 스택에 없는 검색 연산을 수행한 변칙적인 풀이 방법이기 때문에, 여기서는 정석대로 스택에서 가능한 연산만을 수행하고 검색 기능은 seen 변수에서 실행되게 하여 다음과 같이 풀이한다.



<스택 문자 제거 풀이>

  • cbacdcba에서 a가 들어올 때, 이미 이전에 들어와 있던 c와 b는 다음과 같이 제거된다.

    • 카운터가 0 이상인 문자인 c와 b는 뒤에 다시 붙일 문자가 남아 있기 때문이다.
profile
For the sake of someone who studies computer science

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN