문제 : 

 

이게 왜 easy인지 모르겠다. 이정도면 충분히 Medium 줄만도 한데... 

 

먼저, 주어지는 파라미터는 2개이다. 

 

1. 한개의 문자열

2. 여러개의 문자열들을 담은 리스트

 

일단 우리가 해야 할 일은, 문자열이 파라미터로 주어지고, 거기서 숫자나 공백이 아닌 영문 알파벳을 추출한 뒤, 두번째로 주어진 리스트에서 첫번째로 추출한 문자들이 등장하는지 확인하고, 등장한다면, 각 문자의 등장 횟수가 처음 주어진 문자열의 각 문자의 등장 횟수보다 적지 않으며, 마지막으로 그 중 길이가 가장 짧은 문자를 리턴하면 된다.

 

설명도 어렵다... 어쨌든 위 이미지의 설명과 예시를 본다면 문제 이해가 더 잘 될것이다.

 

먼저, 우리가 최종적으로 알아야 할 몇가지들부터 나열하면 우리가 쓸 알고리즘이나 자료구조를 좀 더 쉽게 파악 할 수 있을것이다.

 

요구사항:

 

1.  조건을 만족하면서 가장 짧은 문자열을 리턴.

2."각 문자의 등장 횟수가 처음 주어진 문자열의 각 문자의 등장 횟수보다 적지 않으며" 를 고려하면, 각 문자의 출현 횟수를 계속 tracking 하고 있어야 한다.

 

1. 로부터 내가 생각해낸 로직은 - 그렇다면, 두번째 파라미터로 주어진 여러 문자열을 담은 리스트들을 길이 순으로 정렬하면 어떨까?

그렇다면 첫번째 요소부터 조건을 비교 한 후, 모든 조건이 성립된다면 곧바로 리턴을 해버리고 끝내버린다. 예를들어, 리스트에 문자열들이 무작위 순서로 존재한다면, 처음부터 끝까지 모든 요소를 검사 한 후, 또 그중 가장 짧은 길이의 문자열을 리턴하는것은 너무 소모가 많다. 왜냐하면 아래 코드에서 보이겠지만, 문자 하나의 조건을 검사하는데 드는 비용은 O(n x m)이기 때문에 비싸다. 그렇기 때문에 모든 문자열을 검사하기 보단, 주어진 리스트의 문자열들을 문자열의 길이를 기준으로 정렬 한 후, 리스트의 첫 요소부터 차례로(이땐 맨 처음에는 가장 짧은 문자열이 들어가 있을 것이다.)

 

2. 로부터 내가 생각해낸 자료구조는 해시테이블(딕셔너리)이다. 먼저, 처음으로 주어진 문자열을 순회하며, 각 문자별로 등장 횟수를 값으로 매핑하여 딕셔너리에 저장한다. 위 이미지의 맨 처음 예시에서 딕셔너리의 모습은 dict = {s:2, p:1, t:1} 이다. 이후, 두번째 파라미터의 words에서 단어를 하나하나씩 꺼내와 별도의 해시 테이블에 같은 방식으로 출현 횟수와 함께 매핑시켜 준다. 

 

위 이미지의 첫번째 예시의 첫번째 loop 을 보면 다음과 같다. 첫 단어는 step이고, 두번째 딕셔너리에는 {s : 1, p : 1, : t : 1}과 같이 담길 것이다(여기선 두 딕셔너리의 비교를 위해 첫 딕셔너리에 존재하지 않는 값은 두번째 딕셔너리에도 안 넣어줬다). 이렇게 두번째 딕셔너리에 모든 철자의 출현 횟수를 담아 준 뒤, 첫번째 딕셔너리와 두번째 딕셔너리를 비교하여 조건이 맞다면 그자리에서 바로 검사중인 단어를 리턴하고 끝내 버린다.

 

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

 

class Solution:

    def is_dict2_gte_dict1(self, dict1, dict2):
        for key in dict1:
            if dict1[key] > dict2.get(key, 0):
                return False
        return True

    def shortestCompletingWord(self, licensePlate: str, words: List[str]) -> str:
        dict1 = {}
        lower = licensePlate.lower()
        for i in lower:
            if i.isalpha() and i in dict1:
                dict1[i] += 1
            elif i.isalpha() and i not in dict1:
                dict1[i] = 1
        
        alph = list(dict1.keys())

        words.sort(key=len)

        for word in words:
            dict2 = {}
            for i in word:
                if i in alph and i in dict2:
                    dict2[i] += 1
                elif i in alph and i not in dict2:
                    dict2[i] = 1
            if self.is_dict2_gte_dict1(dict1, dict2):
                return word

 

- 먼저, helper 메서드를 사용했다.  is_dict2_gte_dict1 함수는 두개의 딕셔너리에 대하여 각 요소의 출현 횟수 범위가 범위를 벗어나는지 아닌지를 판별하는 메서드이다.

 

- 메인 함수로 들어와 빈 딕셔너리 dict1을 선언해주고 for문을 이용하여 등장하는 알파벳의 각 철자당 출현 횟수를 딕셔너리에 저장한다. 아 그리고 이 문제에서 대문자 소문자는 신경쓰지 않으므로 파이썬의 lower() 함수를 이용하여 문자열을 소문자로 바꿔주었다. 여기서 주의할점은 파이썬의 문자열은 리스트와 달리 불변의 객체이기 때문에 strings.lower()을 하면 원본 배열은 바뀌지 않고, 위의 코드와 같이 새로운 변수에 담아 주어야 한다.

 

- 그 후, 비교를 위해 추출해낸 알파벳들을 list(dict.keys())를 통해 리스트에 담아준다. 그리고 위에서 언급한 문자열의 길이대로 리스트를 정렬 해주기 위해 .sort() 메서드의 인자로 key = len을 넘겨주면 문자열의 길이를 기반으로 리스트를 오름차순 정렬 해준다. 이렇게 하면 가장 짧은 문자열부터 가장 긴 문자열대로 리스트가 정렬 될 것이다.

 

- 이제, words 리스트에서 문자열들을 하나씩 가져 올때마다 빈 딕셔너리를 선언 해주고, 그 문자열의 철자들을 하나하나씩 같은 방식으로 검사하여 두번째 딕셔너리(dict2)에 넣어준다. 여기서 if와 elif의 조건문을 잘 보면, 문자를 검사하면서 첫번째 딕셔너리 dict1에 존재하는 키값들만 넣어 주었다. 이렇게 해야 두 딕셔너리를 검사 하기가 수월하기 때문이다.

 

- 마지막으로, 헬퍼 메서드를 이용하여 조건이 맞는지 확인 한 후, 조건이 맞다면 return word를 통해 함수를 끝내 버린다.

 

아래는 내가 받은 report이다.

 

문제 :

 

사실 이 문제는 저번주에 시도했던 문제이지만 풀어내지 못했다. 런타임이 말도 안되게 길게 나왔는지 자꾸 RuntimeExceptionError가 발생했다. 그런데 오늘은 정말 한번의 시도만에 풀어버려서 사실 살짝 놀랐다 ㅎㅎ 한달째 꾸준히 하루에 2-3문제씩 알고리즘을 푸는 중인데 뭔가 문제를 풀면 풀수록 어떠한 자료구조를 써야 하는지가 생각난다. 작년 이맘때쯤엔 같은 문제를 봐도 뭐가 뭔지 몰랐고 정말 막막했지만, 이젠 뭔가 자료구조 정도는 생각해 낼 수 있다. 

 

----

어쨌든! 이 문제는 간단하다. 정수타입의 리스트가 주어지고, 저 리스트에서  maximum Harmonious subsequece의 길이를 구하는것이다. 여기서 maximum Harmonious subsequece란, 배열에서 최댓값과 최솟값의 차이는 1이여야 한다. 그러한 배열들의 가장 긴 길이를 리턴하면 된다.

 

처음 든 생각:

- brute force를 생각했다. 사실 이걸 brute force로 풀면 말도안되는 런타임이 나올것이다. 아마 작년 이맘때쯤 이 문제를 못 풀었던 이유도 비슷한 이유가 아닐까 싶다. 하지만 이건 말도안되는 방법이므로 패스! 물론 이렇게 푼다해도 통과는 하겠지만, 실제 코테에서는 이런식으로 푼다면 다른 사람들에게 밀릴것이다.

 

- 우리가 필요한것은 결국 max length이고, 배열의 길이는 그 배열 요소의 출현 횟수(frequency)에 정비례 한다. 위의 1번 예시에서, 저중 두 수의 차이가 1인 숫자들의 조합은 (1, 2), (2, 3) 이다. 이걸 조합이 아니라 배열의 관점에서 본다면 [1,2,2,2], [2,2,2,3,3] 이다. 이 두 배열중 길이가 더 긴 후자의 배열 == 5 를 리턴하면 된다. 

 

- 길이(==frequency)가 필요하다면, 각 요소의 출현 횟수를 저장할 수 있는 딕셔너리를 선언해준다. 

 

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

class Solution:
    def findLHS(self, nums: List[int]) -> int:
        dict ={}
        maxLen = 0

        nums.sort()

        for i in nums:
            if i in dict:
                dict[i] += 1
            else:
                dict[i] = 1

        keys = list(dict.keys())
        
        for i in range(len(keys) - 1):
            current_key = keys[i]
            next_key = keys[i + 1]
            if next_key - current_key == 1 and (dict.get(current_key) + dict.get(next_key) > maxLen):
                maxLen = dict.get(current_key) + dict.get(next_key)
        
        return maxLen

 

- 먼저, 빈 딕셔너리를 선언하고, max길이를 찾을 때마다 계속 업데이트 해 줄 maxLen이라는 변수를 0으로 초기화 해 준다.

 

- 다음으로 nums.sort()를 해준 이유는 다음과 같다;  배열을 정렬하여 현재 값과 다음값이 1이 차이날때만 나머지 로직을 수행 하도록 해 주었다. 정렬을 하면 현재 값과 다음값의 차이가 1이 아니라면 그 이후의 요소들은 무시 할 수 있다. 정렬된 배열은 오름차순으로 정렬되므로 이미 다음값과 현재값의 차이가 1이 아니라면 그 다음의 값들은 1 이상 차이날것이다.

 

- 이제 선언한 딕셔너리에 각 요소와 그 요소의 출현 횟수를 매핑 시켜준다. 나는 (숫자 : 출현횟수)와 같이 매핑 시켜 주었다.

 

- 다음으로, 현재값과 다음값의 차이가 1인지를 비교하려면 딕셔너리의 키값들을 가져와야 한다. list(dict.keys())를 이용해 딕셔너리의 키값들을 리스트로 받아오고, 키값들을 순회한다. 순회할때는 두가지 조건이 있다. 1. (다음 key값 - 현재 key값)이 1인경우 AND 두 요소의 value값을 더했을때(두 요소의 출현 횟수를 더한 값은 두 요소를 배열로 만들었을때의 길이와 같다.), 이 값이 현재 maxLen보다 클때만(어차피 현재 길이가 maxLen보다 작다면 아무것도 할 필요가 없다) maxLen을 업데이트 해 주고, 마지막에 maxLen을 리턴 해 준다.

 

아래는 내가 받은 report이다.

 

'CodingTest > Leetcode' 카테고리의 다른 글

[LeetCode] #804. Unique Morse Code Words  (0) 2024.06.27
[LeetCode] # 748. Shortest Completing Word  (0) 2024.06.26
[LeetCode] #290. Word Pattern  (0) 2024.06.21
[LeetCode] #205 Isomorphic strings  (0) 2024.06.21
[LeetCode] #202. Happy Number  (0) 2024.06.19

문제 : 

 

두개의 문자열들이 인자로 주어지고, 첫번째 문자열과 두번째 문자열의 패턴이 같으면 True를 리턴하고, 다르다면 False를 리턴한다. 이건 너무 쉬운 문제라 보자마자 딕셔너리가 생각났다. 

 

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

class Solution:
    def wordPattern(self, pattern: str, s: str) -> bool:
        
        pattern_list = list(pattern)
        s_list = s.split()
        word = {}

        if len(pattern_list) != len(s_list):
            return False

        for pattern, s in zip(pattern_list, s_list):
            if pattern in word:
                if word[pattern] != s:
                    return False
            elif(s not in word.values()):
                word[pattern] = s
            else:
                return False
        return True

 

- 먼저, 주어진 파라미터를 보면 리스트나 튜플같은 시퀀스 객체(순서가 있어 인덱스로 참조 할 수 있는 객체)가 아니라 그냥 문자열로 주어졌기 때문에 우선 이 문자열들을 필요한 기준으로 나누어 리스트로 만들어줘야 한다. 

 

- pattern_list는 인자로 주어진 pattern이라는 문자열을 한 글자씩 추출해 리스트로 반환한다. 예를들어, 문자열이 'abcd'이고, 이 문자열을 list()를 통해 변환시키면 리스트에는 ['a', 'b', 'c', 'd']가 담길 것이다.

 

- 두번째 문자열은 첫번째 문자열과 약간 다르다. 첫번째 문자열은 그냥 문자 하나씩 기준으로 나누어주면 됐지만, 두번째 인자로 주어진 문자열은 공백을 기준으로 문자가 나뉘어져 있고, 한 글자씩 뽑아야 할게 아니라 공백을 기준으로 나누어진 단어들을 가져와야 하기 때문에 파이썬의 .split()함수를 써서 문자열을 공백을 기준으로 나누어 리스트에 담아주었다. 여기서 만약 쉼표나 다른 구분자를 기준으로 나누고싶다면, split()함수의 파라미터로 구분자를 넘겨주면 된다. 예를들어, 쉼표를 기준으로 문자열을 나누고 싶다면, split.(',')와 같이 쓰면 된다.

 

- 다음으로, 패턴과 문자를 짝지어 매핑할 빈 딕셔너리를 선언해준다.

 

- for문을 순회하며 조건을 체크한다.

   - 여기서 이전에 썼던 아주 유용한 메서드와 기술(?)을 써먹을 수 있었다 ㅎㅎ 바로 zip과 for a, b! 왜냐하면 우리는 반복문을 돌며 패턴          리스트와 문자 리스트와 비교를 계속 해 주어야 하기 때문에 두개를 동시에 비교 할 수 있도록 zip과 2개의 피연산자를 지정해주었다.

   - 반복문을 돌며, 만약 패턴이 딕셔너리 안에 존재한다면, 패턴에 대응하는 value가 현재 문자와 같은지 확인한 후, 그렇지 않으면 False        를 반환한다.

  - 만약 패턴이 딕셔너리 안에 존재하지 않는다면, 이제 이 패턴과 현재 루프에서 비교중인 문자를 매핑하여 딕셔너리에 넣어주어야 하는데,

     그 전에 이미 그 value가 딕셔너리 안에 있는지 확인 해준다. 예를들어 pattern이 'aabb'이고, 문자가 'dog dog dog dog'일때를 살          펴보자. 세번째 루프(aabb에서 첫번째 b와 문자에서 세번째 dog을 돌고있는 루프)에선 'b'는 딕셔너리 안에 존재하지 않으므로 그대          로 딕셔너리 안으로 들어가게 되어 딕셔너리는  {'a':dog, 'b':dog}과 같이 패턴이 망가지게 된다. 여기서의 패턴은 일대일 대응이지만,        이 예시에서는 일대일 대응이 될 순 없다. 그래서! 한가지 체크를 더 해준다. 지금 딕셔너리에 넣고자 하는 단어가 이미 딕셔너리 안에
      있는지 확인 한 후, 없다면 딕셔너리 안에 매핑을 시켜 넣어주고, 있다면 더이상 일대일 대응이 아니므로 False를 반환한다.

 

 

 

 

 

 

문제 : 

 

두개의 문자열이 인자들로 주어지고, 두 문자열이 동형인지 판별하면 된다. 여기에서의 "동형"은 각 문자열의 문자들이 동일한 알파벳으로 매핑된다는 것이다. 예를들어, egg - add는 다음과 같은 이유로 동형이다. (e : a || g : d || g : d) 와 같이 각 문자들이 일대일로 매핑된다.

 

먼저 "매핑"이라는걸 생각하자마자 떠오른건 당연 딕셔너리(해시테이블)이다. 그렇다면 이 문제를 딕셔너리로 어떻게 풀 수 있을까?

 

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

class Solution:
    def isIsomorphic(self, s: str, t: str) -> bool:

        if len(s) != len(t):
            return False

        s_to_t = {}
        t_to_s = {}

        for char_s, char_t in zip(s, t):
            if char_s in s_to_t:
                if s_to_t[char_s] != char_t:
                    return False
            else:
                s_to_t[char_s] = char_t

            if char_t in t_to_s:
                if t_to_s[char_t] != char_s:
                    return False
            else:
                t_to_s[char_t] = char_s
        return True

 

- 먼저, 두 문자열이 동형이기 위해서는 두 문자열의 길이가 같아야 한다. 길이가 다르면 동형이 될 수 없다. 또한, 길이가 다르다면 for문을 돌 필요도 없기 때문에(성능 향상), 맨 처음에 길이가 다르다면 곧바로 False를 리턴한다.

 

- 다음으로, 두개의 빈 딕셔너리를 선언해 준다.

 

- for문을 순회하며 만약 s의 문자가 s딕셔너리 안에 있다면, 동형인지 판별해주고, 없다면 딕셔너리 안에 넣어준다. 같은 일을 t에 대해서도 반복한다.

 

- 만약 이 for loop에서 False에 걸리지 않는다면, 두 문자열은 동형이므로 마지막에 return True를 리턴한다.

 

새로 알게된 내용 : 

 

- 두 개의 인자를 사용하는 `for`문:

파이썬에서는 `for`문을 사용할 때, 순회할 iterable 객체가 튜플이나 리스트와 같은 여러 값을 포함하고 있으면, 이를 풀어서 두 개 이상의 변수에 할당할 수 있다.

예를 들어, 다음과 같은 리스트가 있다고 하자:

pairs = [(1, 'a'), (2, 'b'), (3, 'c')]


이 리스트는 3개의 튜플을 포함하고 있다. 각 튜플은 두 개의 값을 가지고 있다. 이 리스트를 `for`문으로 순회하면서 각 튜플의 값을 풀어서 두 변수에 할당할 수 있다:

for number, letter in pairs:
    print(number, letter)


위 코드는 다음과 같이 출력된다 : 

1 a
2 b
3 c

 

-`zip` 함수:

`zip` 함수는 여러 iterable 객체들을 병렬로 순회하면서, 각 iterable 객체의 요소를 하나씩 묶어 튜플을 생성한다. 쉽게 말해, 여러 개의 리스트나 튜플을 동시에 순회할 수 있게 해주는 함수이다.

예를 들어, 두 개의 리스트가 있다고 해보자:

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']


`zip` 함수를 사용하면, 각 리스트의 요소를 하나씩 묶은 튜플을 생성할 수 있다:

for item1, item2 in zip(list1, list2):
    print(item1, item2)


위 코드는 다음과 같이 출력된다 :

1 a
2 b
3 c

`zip` 함수는 전달받은 iterable 객체들이 동일한 길이를 가지는 경우에 각 요소를 묶는다. 만약 길이가 다르다면, 가장 짧은 iterable 객체의 길이에 맞춰서 순회가 종료된다.

문제 :

정수가 주어지고, 그 정수의 각 자릿수를 1이 될때까지 제곱하여 더해준다. 만약, 결괏값이 1으로 수렴한다면 True를 반환하고, 그렇지 않다면(무한루프), False를 반환한다.

 

이 문제를 처음 보고 써야할 자료구조를 생각해내지 못했다. 그냥 brute force로 계속 곱해 1이 된다면 True, 아니면 False를 반환하려고 했는데, 무한 루프를 돌 경우를 알려주는 플래그는 존재하지 않는다.. 그래서 도대체 어떻게 해결하지 한참을 고민했다. 결국 오늘 문제는 내 스스로 해결하지 못하고 Solution의 도움을 받았다. 아래 Solution을 보고 코드를 분석 해보도록 하겠다.

 

아래는 내가 제출한 코드가 아닌 Solution에서 가져온 코드이다.

class Solution:
    def isHappy(self, n: int) -> bool:
        hset = set()
        while n != 1:
            if n in hset:
                return False
            hset.add(n)
            n = sum([int(i) ** 2 for i in str(n)])
        else:
            return True

 

- 집합 선언:
hset = set()


hset은 정수를 저장하는 집합이다. 집합은 중복을 허용하지 않으므로, 이전에 계산된 값이 다시 나타날 경우 무한 루프에 빠진 것을 감지할 수 있다.

- 반복문:

while n != 1:
n이 1이 아닐 때까지 반복한다. 1이 되면 True를 반환하고 종료한다.

- 무한 루프 감지:


if n in hset:
    return False
현재 n이 hset에 이미 존재하면, 이는 무한 루프에 빠졌다는 의미이므로 False를 반환한다.

- 집합에 현재 숫자 추가:


hset.add(n)
현재 n을 집합에 추가한다.

-새로운 숫자 계산:


n = sum([int(i) ** 2 for i in str(n)])
현재 숫자의 각 자릿수를 제곱하여 더한 값을 새로운 n으로 설정한다. 이를 위해 숫자를 문자열로 변환하고, 각 자릿수를 제곱하여 합한다.

- 결과 반환:
else:
    return True
n이 1이 되면 True를 반환합니다.

 

사실 이 문제는 자료구조 / 알고리즘적 사고 보다는 수학적 사고에 가까운 문제였다고 생각한다. 중복을 허용하지 않는 집합을 사용해 무한루프에 빠지는지 아닌지를 판별하는게 가장 큰 이 문제의 trick이였는데, 나는 생각해내지 못했다 :(

문제 :

 

문제는 간단하다. 배열이 주어지고, 그 배열에서 "증가하는" subsequence의 길이를 구하는것이다. 첫번째 예시를 보면

[1,3,5,4,7]에서의 증가하는 가장 긴 subsequence는 [1,3,5]이다. 왜냐하면 맨 뒤에 7이 있음에도 불구하고, 그 사이에 5보다 작은 4가 있기에 충족 조건인 "증가하는" subsequence가 될 순 없다.

 

처음에 여러가지 생각을 했다. 어느 자료구조를 써야 효율적으로 풀어낼 수 있을까? 부터 생각했다. 우선, 이 배열은 정렬을 해서는 안 된다. 정렬을 하면 주어진 리스트가 무조건 increasing하는 리스트가 되므로, 정렬을 할 순 없다. 그렇다면, 키 - 값으로 인덱스나 출현 횟수를 저장 할 딕셔너리는 무의미해 보였다. 그래서 일단 단순한 iteration과 조건문으로 구현을 해 보았다. 아래는 내가 제출해 통과한 코드이다.

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        longest = 1
        result = []

        if len(nums) == 1:
            return 1

        for i in range(len(nums) - 1):
            if nums[i] < nums[i+1]:
                longest += 1
            else:
                result.append(longest)
                longest = 1
            result.append(longest)
        
        return max(result)

 

- 먼저, 루프를 돌며 계속 업데이트 될 longest라는 가장 긴 subsquence의 길이를 담을 변수를 선언하고 1로 초기화 했다.

 

- 다음은 result라는 빈 리스트를 선언했다. 이 배열에는 가장 긴 subsequence의 길이를 루프를 돌며 담을것이고, 마지막엔 max(result)를 하여, 이 배열에 담긴 최댓값을 리턴한다. 아래에서 좀 더 자세히 설명 하겠다.

 

- for문을 이용해 nums라는 배열을 순회하며, 현재 인덱스의 다음값이 현재 인덱스보다 크다면(증가하는 subsequence 조건), longest를 업데이트 한다.

 

- 만약 현재 인덱스의 다음값이 현재 인덱스보다 작다면(조건을 벗어나는 케이스), 우리는 현재까지 업데이트된 longest를 결과 배열에 추가 해 주고, longest를 다시 1로 업데이트 해 준다.

 

- 모든 루프를 순회하고 난 뒤, 결과 배열에 더해진 요소들중 최댓값을 리턴한다.

 

위의 [1,3,5,4,7]을 예로 들면 다음과 같다.

 

1. 첫번째 루프 : 1 < 3 이므로, longest는 2로 업데이트 된다.

2. 두번째 루프 : 3 < 5 이므로, longest는 3으로 업데이트 된다.

3. 세번째 루프 : 5 < 4 이므로, if문에서 벗어나 else문으로 진입하여 이전까지 업데이트 된 longest == 3을 result 배열에 추가 해주고, longest를 다시 1로 초기화 해 준다.

4. 네번째 루프: 4 < 7 이므로, if문에서 걸려 longest는 2가 되고, 결과 배열에 2를 추가 해준다.

5. 결과 배열에는 [3,2]의 값들이 담겨있고, 여기서 max를 호출하면 3이 리턴된다.

 

하지만 뭔가 런타임이 길게 나왔다. 뭐 짧게 나오리라곤 생각을 안 했다. 여기서 런타임을 줄일만한 특정한 자료구조나 알고리즘을 사용하지 않았기 때문!

 

----------

 

아래는 GPT가 알려준 런타임이 길게 나온 이유와 그에 대한 해결 방안이다.

현재 주어진 코드는 주어진 리스트에서 가장 긴 연속 증가하는 부분 수열(LCIS)의 길이를 찾는 문제를 해결합니다. 하지만, 이 코드는 비효율적인 부분이 있어서 런타임이 길어질 수 있습니다.

문제점 분석
1. **리스트 추가 연산**: `result.append(longest)`는 매번 리스트에 값을 추가하기 때문에 시간이 걸립니다.
2. **불필요한 리스트 사용**: 최종 결과를 계산하기 위해 리스트 `result`를 사용하지만, 이는 불필요하게 메모리를 사용하고 추가적인 시간이 걸립니다.
3. **최대값 계산**: `max(result)`는 리스트에서 최대값을 찾기 위해 추가적인 O(n) 시간을 소모합니다.

해결 방안
1. **리스트를 사용하지 않고 바로 최대값을 계산**: 리스트 대신 단순히 현재까지의 최대값을 추적하여 메모리 사용과 시간을 절약할 수 있습니다.

개선된 코드
아래는 개선된 코드를 보여줍니다.

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        if not nums:
            return 0

        longest = 1
        current_length = 1

        for i in range(1, len(nums)):
            if nums[i] > nums[i-1]:
                current_length += 1
                longest = max(longest, current_length)
            else:
                current_length = 1

        return longest


 설명
1. **초기화**:
   - `longest`는 가장 긴 연속 증가하는 부분 수열의 길이를 추적합니다.
   - `current_length`는 현재 증가하는 부분 수열의 길이를 추적합니다.
2. **반복문**: 인덱스 `i`를 1부터 시작하여 리스트를 순회합니다.
   - `if nums[i] > nums[i-1]`: 현재 숫자가 이전 숫자보다 크면 `current_length`를 증가시키고, `longest`를 업데이트합니다.
   - `else`: 증가하지 않으면, `current_length`를 1로 리셋합니다.
3. **결과 반환**: 최종적으로 `longest`를 반환합니다.

 최적화 이유
- **리스트 사용 제거**: 불필요한 리스트 사용을 제거하여 메모리 사용을 줄이고, 리스트에 값을 추가하는 시간을 절약합니다.
- **실시간 최대값 갱신**: 최대값을 실시간으로 갱신하여 `max()` 함수의 추가 호출을 피합니다.
- **O(n) 시간 복잡도**: 개선된 코드는 전체 리스트를 한 번만 순회하므로 시간 복잡도는 O(n)입니다.

이 방법을 사용하면 주어진 리스트에서 가장 긴 연속 증가하는 부분 수열의 길이를 효율적으로 찾을 수 있습니다.

 

 

문제:

문제는 간단하다. 주어진 배열은 반복되는 값이 한개 있고, 또  missing된 값이 하나 있다. 결과 배열에 이 두 숫자를 "순서대로" 담야아 한다.

 

아래는 첫번째 제출한 코드이다:

class Solution:
    def findErrorNums(self, nums: List[int]) -> List[int]:
        dict ={}
        result =[]
        nums.sort()
        n = len(nums)
        repeated = 0

        expected = sum(range(1, n + 1))
        actual = sum(nums)

        for i in range(len(nums)):
            dict[nums[i]] = nums.count(nums[i])

        for key, value in dict.items():
            if value == 2:
                repeated = key
                result.append(key)
                break
        missing = expected - actual + repeated
        result.append(missing)
        
        return result

- 처음 접근은 딕셔너리를 이용했다. 딕셔너리에 키 - 값으로, 여기서의 키는 리스트의 요소이고, 값은 그 요소의 출현 횟수를 저장한다. 그 후, 리스트를 순회하며 count가 2인 값을 찾아 결과 배열에 더해준후, break로 빠져 나온다(어차피 이 문제에서 반복되는 숫자는 한개이므로 그 이후의 리스트 순회는 무의미하다).

 

- 이제 빠진 값을 찾아야 하는데 여기서 애를 많이 썼다. 단순히 오름차순으로 정렬 한 후, 1부터 시작하는 count를 증가시켜 없는 값을 찾으려고 했으나, [1,2,2,3,4,5,6,7,8,9]같은 케이스에선 먹히지 않았다. 이 케이스에선 '10'이 없는 상태인데, 인덱스 번호 2번에서 if문에 걸리지만, 저기서 없는 수가 10인지, 3인지는 판별이 불가능하다. 그래서 새로운 접근법은, 반복되는 수를 저장할 변수 repeated를 선언하고, 원래 배열(반복되는 숫자가 없이 정상적인 배열일때를 의미함)의 합을 expected라는 변수에 저장한다. 그 후, actual이라는 변수에 현재 리스트(반복되는 값이 존재하는 불량 리스트)의 합을 더해준다. 여기서 없어진 값을 찾는 가장 쉬운 방법은 expected - actual + repeated이다. 그래서 마지막에 결괏값을 더해준다.

 

하지만 ,,, 런타임이 말도 안되게 길게 나왔다. Trouble shooting 시간이다!

그래서 코드를 다시 한번 살펴봤다. 살펴보니

for i in range(len(nums)):
            dict[nums[i]] = nums.count(nums[i])

        for key, value in dict.items():
            if value == 2:
                repeated = key
                result.append(key)
                break

여기서 딕셔너리에 키-값 쌍을 저장하고, 딕셔너리를 순회하며 반복 횟수가 2인 키를 찾는것은 똑같은 일을 두번 하는 헛수고였다.

난 이미 첫번째 for문에서 nums.count라는 메서드를 통해 매 요소마다 출현 횟수를 저장하고있다. 이 함수는 리스트에서 특정 값의 개수를 세는 함수로, 리스트의 각 요소에 대해 반복적으로 호출되면 시간 복잡도가 O(n^2)이 된다. 하지만, 여기서 이미 count를 얻을 수 있는데, 내가 굳이 그 카운트를 딕셔너리에 저장하고, 다시 딕셔너리를 순회하며 값을 찾는건 무의미한 짓이였다. 이렇게 되면 딕셔너리의 최대 장점인 검색, 삽입, 삭제에 O(1)의 시간이 소요되는것을 전혀 활용하지 못하는것이고(for문을 통해 딕셔너리의 요소들을 하나씩 확인하기 때문), 그냥 .count()를 호출할때 count가 2인것을 검사하면 됐다.

 

아래는 위의 불필요한 딕셔너리를 빼고 통과한 코드이다.

class Solution:
    def findErrorNums(self, nums: List[int]) -> List[int]:
        dict ={}
        result =[]
        nums.sort()
        n = len(nums)
        repeated = 0

        expected = sum(range(1, n + 1))
        actual = sum(nums)

        for i in range(len(nums)):
            if(nums.count(nums[i]) == 2):
                result.append(nums[i])
                repeated = nums[i]
                break
        missing = expected - actual + repeated
        result.append(missing)
        
        return result

 

위에서 언급 한것처럼, 불필요한 딕셔너리 코드는 제외했고, if(nums.count(nums[i])를 통해 루프를 순회함과 동시에 출현 횟수를 검사하여 출현 횟수가 2이면 곧바로 결과 배열에 추가 해주는 로직을 구현했다. 하지만 아직도 런타임이 만족 할만큼 좋게 나오진 않았다. 여기서 런타임을 잡아먹는 코드는 무엇일까?

 

아래 분석은 GPT의 도움을 받았다!

 

현재 코드에서 런타임이 많이 소모되는 주요 원인은 `nums.count(nums[i])` 부분이다. 이 함수는 리스트에서 특정 값의 개수를 세는 함수로, 리스트의 각 요소에 대해 반복적으로 호출되면 시간 복잡도가 O(n^2)이 된다. 이는 리스트가 길어질수록 비효율적이다.

이를 개선하기 위해 딕셔너리를 사용하여 각 숫자의 빈도를 한 번에 계산한 후, 이를 기반으로 반복된 숫자를 찾고, 누락된 숫자를 계산하는 것이 좋습니다.

개선된 코드
다음은 딕셔너리를 사용하여 런타임을 최적화한 코드이다:

class Solution:
    def findErrorNums(self, nums: List[int]) -> List[int]:
        num_count = {}
        n = len(nums)
        repeated = -1
        missing = -1
        
        # 빈도 수 계산
        for num in nums:
            if num in num_count:
                num_count[num] += 1
            else:
                num_count[num] = 1
        
        # 반복된 숫자와 누락된 숫자 찾기
        for i in range(1, n + 1):
            if i in num_count:
                if num_count[i] == 2:
                    repeated = i
            else:
                missing = i
        
        return [repeated, missing]


설명
1. 빈도 수 계산:
   - 딕셔너리 `num_count`를 사용하여 리스트 `nums`의 각 숫자의 빈도를 계산한다.
   - 이 과정은 O(n)의 시간 복잡도를 가진다.
2. 반복된 숫자와 누락된 숫자 찾기:
   - 1부터 n까지의 숫자를 순회하며, 각 숫자가 딕셔너리에 있는지 확인한다.
   - 숫자가 딕셔너리에 없으면 그 숫자가 누락된 숫자이다.
   - 숫자가 딕셔너리에 있고, 빈도가 2라면 그 숫자가 반복된 숫자이다.
   - 이 과정 역시 O(n)의 시간 복잡도를 가진다.

최적화된 이유
- 기존 코드에서는 `nums.count(nums[i])`가 O(n) 시간 복잡도를 가지며, 이를 리스트의 각 요소에 대해 호출하므로 전체적으로 O(n^2)의 시간 복잡도를 가진다.
- 개선된 코드는 딕셔너리를 사용하여 각 숫자의 빈도를 계산하고, 반복된 숫자와 누락된 숫자를 찾는 데 O(n) 시간 복잡도를 가진다.
- 따라서 최적화된 코드는 리스트의 크기가 커질수록 훨씬 더 효율적으로 작동한다.

 

결국 내가 생각한 로직이 맞았지만, count함수의 비용과, count함수를 사용하지 않고 딕셔너리에 빈도 수를 저장하는 방법을 몰랐던 터라 느린 런타임이 나왔다.

 

문제 :

 

문제는 간단하다. 배열의 위치(인덱스)를 바꾸지 말고, 파라미터로 주어진 k개의 요소만큼의 subarray를 검사하여, 합 / k가 최댓값이 되는 최대 평균값을 구하여라.

 

이 문제는 Sliding window를 활용하는 문제였다. 감격적인 순간! 배열문제를 풀면서 슬라이딩 윈도우를 종종 접하곤 했었는데, 사실 모르는 주제라 그냥 넘어가거나, brute force를 이용해 풀어왔다. 하지만 이번 기회에 sliding window를 직접 구현해보며 뭔지 알고 넘어 가겠다!

 

코드:

class Solution:
    def findMaxAverage(self, nums: List[int], k: int) -> float:
        max_sum = now_sum = sum(nums[:k])
        for idx in range(k, len(nums)):
            now_sum += nums[idx] - nums[idx - k]
            max_sum = max(max_sum, now_sum)
        
        return max_sum / k

 

 

코드를 살펴 보기 전, Sliding Window란 무엇일까?

 

슬라이딩 윈도우(Sliding Window) 기법은 배열이나 문자열과 같은 연속된 데이터 구조에서 특정 조건을 만족하는 연속된 부분 집합(서브 배열 또는 서브 문자열)을 효율적으로 탐색하는 방법이다. 이 기법은 고정된 크기의 윈도우(부분 집합)를 사용하여 배열의 시작부터 끝까지 이동하면서 필요한 계산을 수행한다. 슬라이딩 윈도우 기법은 시간 복잡도를 줄이고 효율적인 연산이 가능하게 한다.

슬라이딩 윈도우의 기본 원리

1. 고정 크기 슬라이딩 윈도우
고정된 크기의 윈도우를 사용하여 데이터를 처리한다. 윈도우는 한 번에 한 칸씩 오른쪽으로 이동한다.

 

슬라이딩 윈도우의 개념을 읽어보고 예제를 좀 살펴보니 그렇게 복잡한것 같지는 않다. 아래는 위 코드에 대한 설명이다!

설명:
1. 초기 윈도우의 합을 계산한다.
2. 윈도우를 한 칸씩 오른쪽으로 이동하면서 각 윈도우의 합을 계산한다. 이동할 때마다 윈도우의 첫 요소를 빼고, 새로운 요소를 더한다.
3. 최대 합을 갱신한다.

 

여기서 궁금했던점이 있다. "이동할 때마다 윈도우의 첫 요소를 빼고, 새로운 요소를 더한다." 굳이 왜 이렇게 할까?

 

슬라이딩 윈도우 기법에서 각 반복마다 첫 요소를 빼고 다음 요소를 더하는 이유는 효율성을 극대화하기 위해서이다. 이를 통해 시간 복잡도를 줄이고, 전체 리스트를 다시 계산하지 않고도 부분 합을 업데이트할 수 있다; 사실 나도 같은 생각을 했다. 왜냐하면 두번째 슬라이딩에서는 이전에 더했던 3개의 값 +  새로운 값을 더해야 했다. 그래서 처음에 생각했던것이 : DP로 풀어야 하나...? 계산을 이미 한 것을 또 하는것은 너무 큰 낭비이기 때문에 반복문이 돌때마다 새로 계산 해 주는건 말도 안된다고 생각했다.

슬라이딩 윈도우 기법의 원리
1. 초기 합계 계산: 첫 번째 윈도우의 합을 구한다. 예를 들어, 리스트 `[1, 12, -5, -6, 50, 3]`에서 윈도우 크기 `k=4`일 때, 첫 번째 윈도우의 합은 `1 + 12 + (-5) + (-6)`이다.
2. 윈도우 이동: 다음 윈도우로 이동할 때, 기존 윈도우의 첫 요소를 빼고, 새로운 윈도우의 마지막 요소를 더한다. 이렇게 하면 매번 새로운 윈도우의 합을 계산할 필요 없이 기존 윈도우의 합을 이용하여 효율적으로 계산할 수 있다. (저장할 테이블이 없는 DP같단 생각이 들었다.)

위의 예제를 살펴보면, 

- 첫 번째 윈도우
첫 번째 윈도우는 `nums[0:4]`로, 합계는 `1 + 12 + (-5) + (-6) = 2`이다.

- 두 번째 윈도우
다음 윈도우로 이동할 때, 첫 번째 요소 `1`을 빼고 다음 요소 `50`을 더한다:

새로운 윈도우 합계 = 기존 합계 - 첫 번째 요소 + 새로운 요소
                 = 2 - 1 + 50
                 = 51


- 세 번째 윈도우
다음 윈도우로 이동할 때, 첫 번째 요소 `12`를 빼고 다음 요소 `3`을 더한다:

새로운 윈도우 합계 = 기존 합계 - 첫 번째 요소 + 새로운 요소
                 = 51 - 12 + 3
                 = 42


결국 슬라이딩 윈도우 기법에서 첫 요소를 빼고 새로운 요소를 더하는 이유는, 매번 새로운 윈도우 합을 전체적으로 계산하지 않고도 부분 합을 효율적으로 업데이트하기 위함이다. 이를 통해 시간 복잡도를 O(n)으로 줄일 수 있어, 큰 리스트에서도 빠르게 동작할 수 있다.



 

 

+ Recent posts