문제 : 

 

입력값으로 문자열 s가 주어지고, s에 나타나는 모든 모음들의 순서들을 뒤집어 문자열을 리턴하면 된다

 

이 문제는 TwoPointers를 이용해 풀면 생각보다 풀기 쉽다.

 

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

 

class Solution:
    def reverseVowels(self, s: str) -> str:
        vowels = set('aeiouAEIOU')
        idx = []
        chars = list(s)

        for i, char in enumerate(s):
            if char in vowels:
                idx.append(i)

        left = 0
        right = len(idx) - 1

        while left < right:
            chars[idx[left]], chars[idx[right]] = chars[idx[right]], chars[idx[left]]
            left += 1
            right -= 1

        return ''.join(chars)

 

- 먼저, 문자열을 돌면서 모음인지 아닌지 판별을 해야 하기 때문에, 모음들이 담긴 집합 vowels를 선언해 주었다. 여기서, 루프를 돌면서 if char in vowels를 해주는데, 여기서 리스트가 아니라 집합으로 선언한 이유는 시간 복잡도를 줄일 수 있어서 이다. 리스트는 반복 가능한 객체로써, in list를 호출하면 리스트의 첫번째 요소부터 끝까지 차례대로 검사하여 리스트 안에 특정 값이 있는지 확인한다[O(N)]. 반면, 검색, 삽입, 삭제에 O(1)의 실행 시간을 가지는 해시 테이블로 구현된 파이썬의 집합은 in을 호출 했을때 O(1)의 시간이 소요되므로, set를 사용 해주었다.

 

-이제, 모음이 나타나는 인덱스를 저장할 리스트 idx를 선언해주고,

 

-입력값으로 주어진 문자열 s를 list()를 통해 각 알파벳을 리스트에 하나씩 담아 리스트를 만들어 주었다.

 

- for i, char in enmerate(s): 다시 만난 친구이다. 자주 나오는것 같으니 꼭 기억을 해 놓자! enmerate(s)는 문자열 s의 각 요소와 인덱스를 동시에 참조 할 수 있게 해준다. 이 함수를 호출하면, in 앞에 있는 i, char에는 차례대로 인덱스, 현재 참조중인 요소가 담길 것이다.

 

- 만약, 현재 비교중인 문자가 모음이라면, 선언한 인덱스 배열에 인덱스를 append 해 준다. 여기까지 마치면, 주어진 입력값을 순회하며 모음이 나타나는 모든 인덱스가 idx배열에 담겼을 것이다.

 

- 이제 TwoPointer를 사용하기 위해 left와 right인덱스를 각각 초기화 해 주었다. 지금 보니 left, right보다 start, end가 더 직관적인 이름 인것 같다. 각각은 문자열의 첫번째 인덱스와 마지막 인덱스를 나타낸다.

 

- 이제 left < right가 될때까지 left와 right의 인덱스를 서로 바꿔준다. 바꿔주며 left와 right을 각각 업데이트 해 준다.

 

Two Pointer를 쓰는 문제중 비교적 난이도가 쉬운 문제였다.

문제 :

 

 

문제는 간단하다. 입력값으로 연결 리스트가 주어지고, 이 연결 리스트가 회문인지 판단하는것이다.

 

먼저, 이 문제를 해결하기 전에 알아야 할 것이 있다.

 

-여기서 head는 리스트가 아니라 '연결 리스트' 이다. 그래서 이 연결 리스트를 뒤집어 리스트로 반환한 후, head와 비교가 불가능하다(타입 불일치).

 

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

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        stk = []
        curr = head
        while curr:
            stk.append(curr.val)
            curr = curr.next
        reversed_list = stk[::-1]
        return stk == reversed_list

 

-먼저, 나는 스택을 이용해 주었다. 연결 리스트를 순회하며 스택에 차례대로 val들을 담아준다. 여기서 이 연결 리스트는 단방향 연결 리스트이므로 연결 리스트의 왼쪽 값부터 차례로 스택에 들어갈 것이다.

 

- while curr : curr(head를 참조하는 연결 리스트)이 빌때까지 계속 

 

-stk.append(curr.val) : 여기서 curr.val을 append해주어야 연결 리스트의 한개의 노드가 정상적으로 들어가고, 그냥 curr만 넣는다면 head의 모든 값이 담길 것이다.

 

- curr = curr.next를 통해 curr을 다음 노드의 값으로 업데이트 해 준다. 이런 식으로 while문을 순회하며 연결 리스트의 모든 값들을 스택에 넣어 준다.

 

- reversed_list = stk[::-1] : 이제, 슬라이싱을 이용해 스택을 뒤집는다.

 

-이후, 우리가 위에서 쌓은 스택과 방금 뒤집은 스택을 비교해 회문인지 판단한다.

 

  💬

스택만 생각 해낸다면, 생각보다 쉽게 풀릴 문제였다. 하지만 보자마자 연결 리스트라 지레 겁을 먹었다. ("단방향 연결 리스트를 어떻게 뒤에서부터 순회하지?") => 스택을 사용해 앞에서부터 하나씩 순회하면 되는구나!

 

파이썬에서 스택은 실제로 별도의 자료형이 없으며, 대신 리스트를 사용하여 스택의 기능을 구현할 수 있다. 리스트의 append 메서드와 pop 메서드를 사용하여 스택과 같은 LIFO(Last In, First Out) 동작을 수행할 수 있다. 따라서 파이썬에서 스택이라고 언급할 때, 일반적으로 리스트를 사용하여 구현된 스택을 의미한다.

 

 

 

문제:

 

문제는 간단하다. 입력값으로 문자열이 주어지고, 이 주어진 입력값 문자열을 모두 소문자로 바꾸고, 알파벳이 아닌 모든 특수문자들을 제거하고나서 이 문자열이 회문인지 판단하는 것이다. 사실 간단한 문제라 포스팅을 스킵 하려고 했지만 새로 쓰게된 라이브러리가 있어 기억하고자 포스팅을 하려고 한다!

 

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

class Solution:
    def isPalindrome(self, s: str) -> bool:
        lower_s = s.lower()
        revised = re.sub(r'[^a-zA-Z0-9]', '', lower_s)

        reversed_s = revised[::-1]

        if revised == reversed_s:
            return True
        else:
            return False

 

- 먼저, 파이썬의 .lower()함수를 이용해 모든 문자열을 소문자로 바꿔준다. 다시한번 복기하지만, 파이썬의 문자열은 immutable한 불변의 객체이므로, lower()을 호출한다고 문자열 자체가 바뀌는것이 아니라 새로운 변수에 할당 해 주어야 한다.

 

- 이제, 문자를 제외한 모든 특수문자를 제거 해줘야 하는데, 내가 처음에 생각한 방식은 lower_s에 대해 for문을 돌며 .isalum()메서드를 호출하여 새로운 str = [] 배열에 알파벳이라면 append를 해주고, "".join(str)을 활용해 문자열을 합치려고 했지만 이것보다 더 괜찮은 메서드나 라이브러리가 있을까 싶어 찾아 보던 중 파이썬의 re라는 라이브러리를 발견하였다.

더보기

파이썬의 `re` 모듈은 정규 표현식을 사용하여 문자열을 검색, 매칭, 수정할 수 있는 강력한 도구를 제공합니다. 정규 표현식은 특정한 패턴을 가진 문자열을 찾거나, 대체하거나, 분리하는 데 사용됩니다. `re` 모듈을 사용하면 복잡한 문자열 처리 작업을 효율적으로 수행할 수 있습니다.

 

주요 함수 및 메서드

1. `re.match(pattern, string)`
   - 문자열의 시작에서 정규 표현식 패턴과 일치하는 부분을 찾습니다.
   - 일치하면 `MatchObject`를 반환하고, 일치하지 않으면 `None`을 반환합니다.

   import re
   match = re.match(r'\d+', '123abc')
   if match:
       print(match.group())  # 출력: 123


2. `re.search(pattern, string)`
   - 문자열 전체에서 정규 표현식 패턴과 일치하는 첫 번째 부분을 찾습니다.
   - 일치하면 `MatchObject`를 반환하고, 일치하지 않으면 `None`을 반환합니다.

   import re
   search = re.search(r'\d+', 'abc123def')
   if search:
       print(search.group())  # 출력: 123


4. `re.findall(pattern, string)`
   - 문자열에서 정규 표현식 패턴과 일치하는 모든 부분을 리스트로 반환합니다.

   import re
   matches = re.findall(r'\d+', 'abc123def456ghi')
   print(matches)  # 출력: ['123', '456']


5. **`re.finditer(pattern, string)`**
   - 문자열에서 정규 표현식 패턴과 일치하는 모든 부분을 반복 가능한 `MatchObject`로 반환합니다.

   import re
   matches = re.finditer(r'\d+', 'abc123def456ghi')
   for match in matches:
       print(match.group())  # 출력: 123 456


6. **`re.sub(pattern, repl, string)`**
   - 문자열에서 정규 표현식 패턴과 일치하는 부분을 다른 문자열로 대체합니다.

   import re
   result = re.sub(r'\d+', 'NUM', 'abc123def456ghi')
   print(result)  # 출력: abcNUMdefNUMghi


7. **`re.split(pattern, string)`**
   - 정규 표현식 패턴을 기준으로 문자열을 분리하여 리스트로 반환합니다.

   import re
   result = re.split(r'\d+', 'abc123def456ghi')
   print(result)  # 출력: ['abc', 'def', 'ghi']



 주요 정규 표현식 메타문자

- `.`: 모든 문자 (개행 문자를 제외하고)
- `^`: 문자열의 시작
- `$`: 문자열의 끝
- `*`: 0회 이상 반복
- `+`: 1회 이상 반복
- `?`: 0회 또는 1회 반복
- `{n}`: 정확히 n회 반복
- `{n,}`: n회 이상 반복
- `{n,m}`: n회 이상 m회 이하 반복
- `[]`: 문자 클래스 (예: `[a-z]`는 소문자 알파벳 a부터 z까지)
- `|`: OR 연산자 (예: `a|b`는 'a' 또는 'b')
- `()`: 그룹핑

 예제

import re

# 예제 문자열
text = "A man, a plan, a canal: Panama"

# 모든 알파벳 문자를 찾는 정규 표현식
pattern = re.compile(r'[a-zA-Z]')

# findall을 사용하여 모든 매칭 결과를 리스트로 반환
matches = pattern.findall(text)
print(matches)  # 출력: ['A', 'm', 'a', 'n', 'a', 'p', 'l', 'a', 'n', 'a', 'c', 'a', 'n', 'a', 'l', 'P', 'a', 'n', 'a', 'm', 'a']

# 모든 매칭 결과를 하나의 문자열로 결합
result = ''.join(matches)
print(result)  # 출력: AmanaplanacanalPanama


이 예제에서는 `re.compile`을 사용하여 정규 표현식을 컴파일하고, `findall` 메서드를 사용하여 모든 알파벳 문자를 찾아 하나의 리스트로 반환합니다. 그 후 `join` 메서드를 사용하여 모든 매칭 결과를 하나의 문자열로 결합합니다.

파이썬의 `re` 모듈을 사용하면 복잡한 문자열 처리 작업을 효율적으로 수행할 수 있습니다. 정규 표현식을 사용하여 다양한 패턴 매칭, 대체, 분리 작업을 쉽게 할 수 있습니다.

 

이처럼 re모듈은 복잡한 문자열 처리 작업을 효율적으로 수행할 수 있다. 그래서 

revised = re.sub(r'[^a-zA-Z0-9]', '', lower_s)

를 통해 모든 특수문자를 제거해 주었다. re.sub 함수는 정규 표현식 패턴 r'[^a-zA-Z0-9]'에 매칭되는 모든 문자를 빈 문자열 ''로 대체한다. [^a-zA-Z0-9]는 알파벳 소문자 a-z, 대문자 A-Z, 숫자 0-9가 아닌 모든 문자를 의미한다. 여기서 a앞에 표현된 ^는 부정의 의미로 알파벳 소문자 a-z, 대문자 A-Z, 숫자 0-9가 아닌 모든 문자를 의미한다.

 

그럼 이제, 여기까지의 과정을 마치면 주어진 입력값 문자열의 모든 문자가 소문자로 바뀌었고, 여기에 나타나는 모든 특수문자들을 제거했다.

 

-이제 회문인지 판단할 일만 남았다. 방금 re 모듈을 사용해 특수문자가 제거된 문자열을 뒤집었을때 같은 문자열이면 회문이라 판단하고 끝낸다. 여기서, 문자열을 쉽게 뒤집는 방법이 등장한다.

 

더보기

문자열을 뒤집는 `reversed_s = revised[::-1]` 구문에 대해 설명하겠다. 이 구문은 슬라이싱을 사용하여 문자열을 뒤집는 파이썬의 간단하고 효율적인 방법이다.

💬슬라이싱(slicing)

파이썬에서 슬라이싱은 시퀀스(예: 문자열, 리스트, 튜플)에서 특정 범위의 요소를 추출하는 데 사용된다. 슬라이싱 구문은 다음과 같은 형태를 가진다:

sequence[start:stop:step]


- `start`: 슬라이싱을 시작할 인덱스 (포함)
- `stop`: 슬라이싱을 끝낼 인덱스 (포함되지 않음)
- `step`: 요소를 추출할 간격. 양수이면 왼쪽에서 오른쪽, 음수이면 오른쪽에서 왼쪽으로 추출. 기본값은 1이다. 음수인 경우 역순으로 슬라이싱한다.

문자열을 뒤집기 위해 슬라이싱을 사용할 때 `start`와 `stop` 인덱스를 지정하지 않고 `step`을 -1로 설정하면, 문자열의 마지막 문자부터 첫 번째 문자까지 역순으로 추출한다. 즉, 문자열을 뒤집는 효과를 준다.

reversed_s = revised[::-1]


- `revised`: 원래 문자열
- `[::-1]`: 슬라이싱 구문으로, 문자열의 끝에서 시작하여 처음까지(-1) 한 문자씩(step) 역순으로 추출

아래는 이 구문을 사용한 예제이다:

# 원래 문자열
revised = "Hello, World!"

# 문자열을 뒤집음
reversed_s = revised[::-1]

# 결과 출력
print(reversed_s)  # 출력: "!dlroW ,olleH"


동작 설명
1. `revised[::-1]` 구문은 문자열 `revised`를 슬라이싱한다.
2. `start`와 `stop` 인덱스를 지정하지 않았으므로, 문자열 전체를 대상으로 한다.
3. `step`을 -1로 설정하여 문자열을 역순으로 추출한니다.
4. 결과적으로, `reversed_s`는 `revised`의 역순 문자열이 된다.

 

문제 : 

 

정말 간단한 문제라 포스팅을 스킵하려고 했지만, 내가 까먹고 있던 부분이 있어 다시 상기하고자 포스팅을 하려고 한다. 문제는 간단하다.

nums라는 입력값 배열이 주어지고, 이 nums라는 배열은 2*n 의 길이이고, 이 nums배열에는 n + 1개의 서로 다른 숫자들이 포함되어 있고, 이중 특정 숫자는 n번 반복된다.

 

예를들어, nums의 배열이 [1,2,3,3]이라면, 여기서의 n은 2일 것이고, 이중 특정 숫자는 2번 반복 될 것이다. 이중 2번 반복되는 숫자는 3이며, 이땐 3을 리턴하면 된다.

 

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

 

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        n = len(nums) // 2
        count = Counter(nums)
        count_dict = dict(count)

        for key, val in count_dict.items():
            if val == n:
                return key
        
        return -1

 

- 먼저, 이 문제를 푸는데 필요한 n을 먼저 구해준다. n은 단순히 문자열의 길이에서 2로 나눠주면 된다.

 

- 이 Counter 라이브러리는 쓰는법을 알고나니 너무 편하다. 이 Counter라이브러리를 통해 각 숫자의 출현 횟수를 저장하고, 이걸 딕셔너리 객체로 변환한다. 이렇게 되면  count_dict안에는 (숫자 : 출현횟수)쌍으로 매핑이 되어 있을 것이다.

 

- 여기서 까먹었던 내용이 등장한다. 이 문제처럼 확인해야 하는게 2개라면,(번호와 출현 횟수를 동시에 검사해야 한다. 이유는 출현 횟수가 m이라면, 그 출현 횟수를 리턴하는게 아니라 그 출현 횟수에 맞는 키값을 리턴해야 한다) .items()를 통해 딕셔너리의 키-값을 동시에 순회한다. 이떄, key는 key에, value는 val에 자동으로 순서대로 할당 될 것이다.

 

- 만약, value(출현횟수)가 n과 일치한다면, key를 리턴한다.

문제 : 

 

 

문제는 입력값으로 여러개의 이메일 주소가 주어지면, @를 기준으로 로컬, 도메인 주소로 이메일 주소가 나뉜다. 예를들어, jghdg@naver.com이면, jghdg는 로컬주소, naver.com은 도메인 주소이다. 이때, 로컬 주소에는 다음과 같은 규칙이 따른다.

 

  • ' . '은 무시된다. 예를들어, jghdg.1234@naver.com이라면, ' . '은 무시되므로 jghdg1234@naver.com과 같다.
  • '+'뒤의 문자열들은 모두 무시된다. 예를들어, jghdg+1234@naver.com이라면, jghdg@naver.com이 된다.(도메인은 그대로이다.) 

이때, 실제 이메일을 받을 수 있는 서로 다른 이메일 주소의 갯수를 리턴하라.

 

 

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

class Solution:
    def numUniqueEmails(self, emails: List[str]) -> int:

        result = set()
        
        for email in emails:
            local_domain = list(email.split('@'))
            local = local_domain[0]
            domain = local_domain[1]

            revised_local = local.replace('.', '')
            plus_idx = revised_local.find('+')
            if plus_idx != -1:
                revised_local2 = revised_local[0:plus_idx]
            else:
                revised_local2 = revised_local
            

            final_addy = revised_local2  + "@" + domain
            result.add(final_addy)

        return len(result)

 

- 먼저, 여기서 결과를 집합으로 선언한 이유는 다음과 같다 : 모든 조건을 적용했을때('.'은 무시되고, '+'뒤의 문자열들은 무시될 때), 우리는 '중복되지 않는' 이메일 주소의 갯수를 구하면 된다. 그렇다면, 규칙을 적용해 새로운 이메일 주소를 만들고, 그 이메일 주소를 집합에 넣어주면 자동으로 중복된 값은 두번 이상 들어가지 않기 때문에 곧바로 집합의 크기를 리턴 하도록 집합을 선언 해 주었다.

 

- 다음으로 for email in emails를 통해 입력값으로 주어진 이메일들을 담은 리스트를 순회한다

    - 여기서 도메인과 로컬 주소를 나눠준다. local_domain은 현재 순회중인 email을 '@'를 기준으로 나눈 후, 각각 배열에 담는다. 이렇           게 되면, 배열의 0번째 인덱스에는 로컬 주소가 담길 것이고, 1번째 인덱스에는 도메인 주소가 담길 것이다. 그래서 local과 domain변         수에 각각 도메인과 로컬 주소를 담아 주었다.


- 이제 도메인과 로컬 주소를 나눠 각각 domain, local이라는 변수에 담아 주었다. 이제, 위에서 설명한 규칙을 로컬 주소에 적용할 차례이다. 로컬에 .replace(' . ', '')를 호출하여 .이 나타나는곳을 '' 즉, 빈 문자열로 바꿔준다. 여기서 ''를 하면 공백이 되는것이 아니라 그냥 .이 나타나는곳이 없어진다. 만약, . 이 나타나는곳을 실제 공백으로 바꾸고 싶다면, replace(' . ',  ' ')를 하면 된다. ('' 대신 가운데 한칸 띄워진 ' ' 가 들어갔다.) 이 단계를 마치면, 이제 이메일 문자열의 .는 모두 없어진 상태이다.

 

- 다음으로 두번째 조건인 + 뒤의 문자열들을 모두 무시하는 코드를 보자. 먼저, + 기호가 없을때는 .index()를 호출하면 런타임 에러가 발생하므로(예외 처리를 해도 되지만 코드가 복잡해진다), .find('+')를 이용해 문자열에 +가 등장하는지 먼저 찾아준다. 파이썬의  .find 메서드는 +를 찾으면 그 인덱스를 반환하고, 찾지 못하면 -1을 반환한다. .index()는 부분 문자열이 존재하지 않으면 예외를 발생시키므로, 예외 처리를 하지 않으면 프로그램이 중단될 수 있다. find()는 부분 문자열이 존재하지 않으면 -1을 반환하므로, 조건문을 사용하여 부분 문자열의 존재 여부를 쉽게 확인할 수 있다.

 

- 만약, .find()를 호출한 결과가 -1이 아니라면, 즉, 문자열 내에 +가 존재한다면, 문자열 슬라이싱을 이용해 +가 나타나는 인덱스의 전까지 revised_local2 라는 변수에 담아주고, 만약 문자열에 +가 나타나지 않는다면 revised_local2에 원래 revised_local을 할당해 그대로 사용한다.

 

이제, 로컬 주소의 모든 룰을 적용 했으므로, 로컬과 도메인을 다시 합쳐 이메일 주소를 만든다. 그 후, 맨 처음 선언한 결과 집합에 이 이메일들을 for문을 돌며 차례로 넣어주면, 중복되지 않는 이메일 주소들이 담길 것이고, 결과적으로 이 집합의 크기를 반환한다.

 

여기서 중요한 내용들은 

- 중복되지 않는 이메일 주소 찾기 :  집합 이용

- .find()메서드는 .index()보다 편리하다(예외처리 대신 -1의 케이스를 처리해주면 됨)

- 문자열 슬라이싱

 

아래는 내가 받은 report이다.

 

 

아래는 GPT를 통해 최적화 시킨 코드이다.

class Solution:
    def numUniqueEmails(self, emails: List[str]) -> int:
        result = set()
        
        for email in emails:
            local, domain = email.split('@')
            local = local.split('+')[0].replace('.', '')
            result.add(local + '@' + domain)

        return len(result)


최적화된 부분 설명

1. `split()` 메서드 최적화:
   - `email.split('@')` 결과를 변수 `local`과 `domain`에 직접 할당하여 리스트 인덱싱을 없앴습니다.
   - `local.split('+')[0]`를 사용하여 '+' 이전의 문자열을 한 번에 추출하고, 이후에 '.'을 제거했습니다.

2. 불필요한 변수 제거:
   - `revised_local2`라는 중간 변수를 제거하고, 바로 `local` 변수에 필요한 값을 할당했습니다.

3. 간결한 문자열 조작:
   - 문자열 조작을 간단하게 하기 위해 한 줄로 처리했습니다.

최적화된 코드의 흐름

1. 이메일을 '@'로 분리하여 `local`과 `domain`으로 나눕니다.
2. `local`을 '+'로 분리하여 '+' 이전의 문자열만 사용하고, '.'을 제거합니다.
3. 최종 이메일 주소를 `local`과 `domain`을 결합하여 만듭니다.
4. `result` 세트에 최종 이메일 주소를 추가합니다.
5. 세트의 크기를 반환하여 고유한 이메일 주소의 수를 계산합니다.

이 최적화된 코드는 더 간결하고 효율적으로 이메일 주소를 처리하여 고유한 이메일 주소의 수를 계산합니다.

문제 : 

 

문제는 간단하다. 두개의  문자열이 주어지고, 두 문자열에서 두번 이상 나타나지 않는 단어들을 리스트로 반환하면 된다.

 

항상 생각한다. 특정 요소의 출현 횟수를 비교, 연산하는 자료구조는 해시테이블이 먼저 떠오른다.

 

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

 

class Solution:
    def uncommonFromSentences(self, s1: str, s2: str) -> List[str]:
        l1 = s1.split()
        l2 = s2.split()
        total_l3 = l1 + l2

        count = Counter(total_l3)

        count_dict = dict(count)

        result = [key for key, value in count_dict.items() if value == 1]

        return result

 

- 먼저, 문자열을 공백을 기준으로 나누어 리스트에 담아준다. 예를들어, "this apple is sweet"이라는 문자열을  .split()을 통해 나누면 
list = ['this', 'apple', 'is', 'sweet']과 같이 될 것이다. 

 

- 이제 입력값으로 받은 두개의 문자열을 공백을 기준으로 나누었으면, 두 문자열을 합친다. 여기서 합치는 이유는 다음과 같다:
   - 내가 그 다음에 하려는 일은 딕셔너리 안에 각 문자열의 출현 횟수를 키 - 값 쌍으로 매핑시켜 넣고자 하는 것인데, 여기서 각 l1, l2 리스       트에는 문자열들이 한번밖에 들어가지 않으므로 어떤 문자열이 중복되어 나오는지 비교 할 수 없다. 예를들어, list = ['this', 'apple', 'is', 'sweet'] 여기서 각 문자열의 빈도를 계산하더라도 모두 1로 나온다(l2에 중복된 문자열이 있음에도 불구하고). 

- 합친 리스트는 다음과 같을 것이다. total_l3:["this","apple","is","sweet","this","apple","is","sour"]. 이제, 파이썬의 Counter 딕셔너리를 활용해 각 문자열의 출현 횟수를 매핑지어 딕셔너리에 담아준다. 위처럼 counter = Counter(total_l3)을 하고 dict()를 통해 명시적으로 딕셔녀리로 형 변환을 해주면 이제 이 딕셔너리 안에는 각 문자열과 그에 맞는 빈도가 담겨 있을 것이다.

 

- 이제, result라는 배열에서 key for key, value in count_dict.items() if value == 1을 통해 문제의 조건에 맞는 값들을 리스트에 담아 반환한다.

여기서 다시한번 이 리스트 컴프리헨션을 살펴보자.


📎 리스트 컴프리헨션의 기본 구조

[new_item for item in iterable if condition]


여기서:
- `new_item`은 새로운 리스트에 추가될 항목이다.
- `item`은 `iterable`로부터 각 반복에서 추출된 항목이다.
- `iterable`은 리스트, 튜플, 딕셔너리, 집합 등 반복 가능한 객체이다.
- `condition`은 선택 사항으로, 특정 조건을 만족하는 항목만 새로운 리스트에 포함된다.

문제 설명

keys_with_value_1 = [key for key, value in count_dict.items() if value == 1]


이 구조를 풀어서 설명하면 다음과 같다:

1. `count_dict.items()`:
   - 딕셔너리 `count_dict`의 모든 키-값 쌍을 반환한다.
   - 예를 들어, `{'apple': 3, 'banana': 2, 'orange': 1}`라면 `count_dict.items()`는 `[('apple', 3), ('banana', 2), ('orange', 1)]`와 같다.

2. `for key, value in count_dict.items()`:
   - 각 키-값 쌍을 순회하면서 `key`와 `value`에 할당한다.
   - 예를 들어, 첫 번째 반복에서는 `key`가 `'apple'`, `value`가 `3`이다.

3. `if value == 1`:
   - 값이 1인 경우에만 다음 단계로 진행한다.
   - 예를 들어, `('orange', 1)` 쌍에서만 조건이 참이 된다.

4. `key`:
   - 조건이 참인 경우, 해당 키를 새로운 리스트에 추가한다.
   - 위 예제에서는 `'orange'`가 리스트에 추가된다.

🎈 전체 예제 코드

from collections import Counter

# 예제 리스트
total_l3 = ["apple", "banana", "apple", "orange", "banana", "apple"]

# Counter 객체 생성
count = Counter(total_l3)

# Counter 객체를 딕셔너리처럼 사용
count_dict = dict(count)

# 값이 1인 키들을 리스트로 추출
keys_with_value_1 = [key for key, value in count_dict.items() if value == 1]

# 결과 출력
print(keys_with_value_1)
```

### 실행 결과

['orange']



이정도면 쉽게 풀어 낼 수 있다. 아래는 내가 받은 report이다.

 

문제 : 

 

입력값으로 문자열이 주어지고, 또한 리스트의 형태로 금지된 단어들이 주어진다. 입력값으로 주어진 문자열중, 가장 빈도가 높게 나타나는 "금지되지 않은" 단어를 리턴하는 것이다. 첫번째 예시를 보면 아마 이해가 더 잘 될 것이다. 하지만 조건이 몇가지 있다.

 

1. 대, 소문자를 신경쓰지 않음. 즉, 1번의 예시에서 BALL과 ball은 같은 취급을 당한다.

2. , . ! ? 같은 문장 부호는 모두 무시된다. 

 

즉, 첫번째 예시에서의 paragraph의 원형은 "Bob hit a ball, the hit BALL flew far after it was hit."이지만, 문제의 조건에 부합하려면 "bob hit a ball the hit ball flew far after it was hit"으로 바뀌어야 한다.

 

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

class Solution:
    def mostCommonWord(self, paragraph: str, banned: List[str]) -> str:
        lower_paragraph = paragraph.lower()
        translator = str.maketrans('', '', string.punctuation)
        cleaned_text = lower_paragraph.translate(translator)
        words = cleaned_text.split()
        
        banned_set = set(banned)
        word_counts = Counter(word for word in words if word not in banned_set)
        
        most_common_word, _ = word_counts.most_common(1)[0]
        
        return most_common_word

 

 

- 먼저, 문자열의 모든 문자들의 대/소문자 구분을 없애기 위해 .lower()함수를 이용해 소문자로 바꿔 주었다.

- `str.maketrans()` 메서드는 문자열의 문자 매핑 테이블을 생성하는 데 사용된다. 이 테이블은 `str.translate()` 메서드와 함께 사용되어 문자열의 문자를 다른 문자로 변환하거나 제거하는 데 사용된다. 이 메서드에 전달되는 세 가지 인수는 다음과 같다:

1. 첫 번째 인수: 변환할 문자의 문자열
2. 두 번째 인수: 변환 후의 문자의 문자열
3. 세 번째 인수: 제거할 문자의 문자열

다음은 `str.maketrans('', '', string.punctuation)`의 각 인수가 무엇을 의미하는지 알아보자:

1. 첫 번째 인수 (`''`): 변환할 문자의 문자열이다. 여기서는 빈 문자열이므로 변환할 문자가 없다.
2. 두 번째 인수 (`''`): 변환 후의 문자의 문자열이다. 여기서도 빈 문자열이므로 변환 후의 문자도 없다.
3. 세 번째 인수 (`string.punctuation`): 제거할 문자의 문자열이다. `string.punctuation`은 모든 문장 부호를 포함하는 문자열이다. 따라서 이 매개변수는 이 문자열에 포함된 모든 문자를 제거하라는 의미이다.

예제

아래는 `str.maketrans()`와 `str.translate()`를 사용하여 문자열에서 모든 문장 부호를 제거하는 코드이다:

import string

text = "Hi I am jihoo. and, you are so, nice!"

# 모든 문장 부호를 제거하기 위한 변환 테이블 생성
translator = str.maketrans('', '', string.punctuation)

# 변환 테이블을 사용하여 문장 부호 제거
cleaned_text = text.translate(translator)

print(cleaned_text)  # 출력: Hi I am jihoo and you are so nice


이 코드에서 `str.maketrans('', '', string.punctuation)`는 모든 문장 부호를 제거하기 위한 변환 테이블을 생성한다. `str.translate(translator)`는 이 변환 테이블을 사용하여 `text` 문자열에서 모든 문장 부호를 제거한다. 결과적으로 `cleaned_text`는 문장 부호가 제거된 문자열이 된다.

요약
- 첫 번째 인수와 두 번째 인수: 변환할 문자를 다른 문자로 매핑하는 데 사용된다. 여기서는 빈 문자열로 설정되어 변환할 문자가 없다.
- 세 번째 인수: 제거할 문자를 지정한다. 여기서는 `string.punctuation`을 사용하여 모든 문장 부호를 제거한다.

 

- 이제 말끔하게 trimmed된 문자열들을 순회하기 편하도록 .split()을 이용해 리스트에 담아준다.

- 여기서 Counter클래스를 처음 써보았다. Counter 클래스는 무엇일까?

더보기

`collections.Counter`란 무엇인가?
`collections.Counter`는 파이썬의 `collections` 모듈에 포함된 클래스이다. `Counter`는 주로 해시 가능한 객체(주로 문자열이나 숫자)의 개수를 셀 때 사용된다. 기본적으로는 딕셔너리와 비슷하지만, 요소의 빈도를 셀 때 매우 유용하게 사용할 수 있는 특수한 형태의 딕셔너리이다.

주요 기능 및 사용법
1. 초기화: `Counter` 객체는 문자열, 리스트, 튜플 또는 다른 iterable을 인수로 사용하여 초기화할 수 있다.

   from collections import Counter
   
   # 문자열을 인수로 사용
   c = Counter("hello")
   print(c)  # 출력: Counter({'l': 2, 'h': 1, 'e': 1, 'o': 1})
   
   # 리스트를 인수로 사용
   c = Counter(['apple', 'banana', 'apple', 'orange', 'banana', 'apple'])
   print(c)  # 출력: Counter({'apple': 3, 'banana': 2, 'orange': 1})
   
   # 키워드 인수를 사용
   c = Counter(a=2, b=3, c=1)
   print(c)  # 출력: Counter({'b': 3, 'a': 2, 'c': 1})



2. 요소의 개수 확인: `Counter` 객체는 각 요소의 개수를 저장한다. 딕셔너리와 비슷하게 키를 사용하여 각 요소의 개수를 확인할 수 있다.

   c = Counter("hello")
   print(c['l'])  # 출력: 2


3. 가장 흔한 요소 찾기: `most_common()` 메서드를 사용하여 가장 흔한 요소와 그 개수를 튜플 리스트 형태로 반환할 수 있다.

   c = Counter("hello")
   print(c.most_common(1))  # 출력: [('l', 2)]
   print(c.most_common(2))  # 출력: [('l', 2), ('h', 1)]


 most_common(1)`이란 무엇인가?
`most_common(n)` 메서드는 `Counter` 객체에서 가장 흔하게 등장하는 요소 상위 `n`개를 빈도수와 함께 튜플 형태로 반환합니다. `most_common(1)`은 가장 흔하게 등장하는 요소 하나를 반환한다.

예제

from collections import Counter

text = "Hi I am jihoo. and, you are so, nice!"
words = text.lower().split()

# 단어 빈도수 세기
counter = Counter(words)

# 가장 빈도가 높은 단어 찾기
most_common_word = counter.most_common(1)
print(most_common_word)  # 출력: [('hi', 1)] (예시)


위 코드에서 `counter.most_common(1)`은 단어 빈도수 중 가장 높은 단어와 그 빈도를 리스트 형태로 반환한다. 여기서 `most_common(1)`의 결과는 리스트 안에 튜플이 포함된 형태이다. 이 리스트의 첫 번째 요소를 사용하여 가장 흔한 단어를 얻을 수 있다.

 

문제 :

 

예시)

 

문제는 간단하다. a부터 z까지의 알파벳에 대응하는 모스부호 코드가 주어지고, 입력값으로 문자열들을 담은 리스트가 주어지면, 각 요소의 문자열을 모스 부호로 변환하여 중복되지 않는 모스 부호의 갯수를 리턴하면 된다. 위의 예시에서 보이듯, gin, zen, gig, msg의 단어들중 gin, zen과 gig, msg의 모스부호는 짝을 이뤄 중복되어 이 경우에는 2를 리턴한다. 

 

먼저, 이 문제에서 중요한 것이 두가지 있다.

 

1. 각각의 서로 다른 알파벳은 서로 다른 모스 부호와 매핑된다 => 그렇다면, 키-값 쌍을 지을 수 있는 해시 테이블을 이용한다. 만약 우리가 z에 담긴 모스 부호를 찾고싶다고 한들 a-z까지 선형 검색을 할 필요가 없어 O(1)의 실행시간으로 검색을 할 수 있는 장점이 있다. 이 문제에서는 어떤 알파벳이 어떤 모스부호에 대응하는지를 찾는게 주 목적이므로 검색에 가장 빠른 시간이 소요되는 해시 테이블을 이용했다.

 

2. 결국 리턴하고자 하는 값은 "중복되지 않는" 모스 부호의 갯수이다. 그렇다면 중복을 허용하지 않는 자료구조인 집합을 선언하고, 반복문을 돌며 집합에 모스 부호들을 담아준 후, 마지막엔 이 집합의 길이를 리턴하면 된다.

 

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

class Solution:
    def uniqueMorseRepresentations(self, words: List[str]) -> int:
        dicti = {'a':".-", 'b':"-...", 'c':"-.-.", 'd':"-..", 'e':".", 'f':"..-.", 'g':"--.", 'h':"....", 'i':"..", 'j':".---", 'k':"-.-", 'l':".-..", 'm':"--", 'n':"-.", 'o':"---", 'p':".--.", 'q':"--.-", 'r':".-.", 's':"...", 't':"-", 'u':"..-", 'v':"...-", 'w':".--", 'x':"-..-", 'y':"-.--", 'z':"--.."}

        moreSet = set()
        

        for word in words:
            morse = ""
            for i in word:
                morse += dicti.get(i)
            moreSet.add(morse)

        return len(moreSet)

 

- 엄청 간단하다. 먼저, 모스부호들을 먼저 딕셔너리에 담아야 한다(이 문제에선 이게 제일 오래 걸렸다.)

- 그 다음, 위에서 언급한 모스 부호들을 담아줄 집합을 선언한다.

- 입력값으로 주어진 words라는 문자열이 담긴 리스트를 순회한다.

           - 새로운 단어로 바뀔 때마다, 그 단어의 모스 부호들을 담을 morse라는 변수를 ""로 초기화 해 준다.

           - for i in word:를 통해 각 단어의 알파벳을 하나씩 가져와 dict1.get(i)를 통해 딕셔너리에서 그 문자에 해당하는 모스 부호를 검색

           하고, 이전에 선언한 morse부호에 담아준다. 여기서 주의 할 점은 파이썬의 문자열은 불변의 객체이기 때문에 리스트에서 사용하는 

           .append()를 사용할 수 없다. 문자열에 새로운 문자를 추가하려면 문자열 연결(concatenation)을 사용해야 한다. 
           - 한 단어의 모든 철자 검색을 마친 후, 그 모스 부호를 위에서 선언한 빈 집합에 담아준다.(여기서 모스 부호가 중복 된다면 어차피

            집합에 담기지 않을 것이다.)

           - 마지막으로, 우리가 원하는 값인 집합의 길이를 리턴 하고 함수를 끝낸다.

 

 

 

 

+ Recent posts