chatGPT가 생각한 정렬

 

 

1. K번째 수 (Lv. 1)

https://school.programmers.co.kr/learn/courses/30/lessons/42748

# 이해
# 1. 슬라이싱
# 2. 정렬
# 3. 인덱스 return

def solution(array, commands):
    answer = []
    
    for command in commands:
        i, j, k = command[0], command[1], command[2]
        new_arr = array[i-1 : j]
        new_arr.sort()
        answer.append(new_arr[k - 1])
    
    return answer

 

 

 

2. 가장 큰 수 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42746

# 이해
# 문자열로 된 숫자를 조합해서, 생성 가능한 숫자 중 가장 큰 수 return

# 풀이
# numbers를 문자열의 배열로 변환
# 내림차순 정렬
# 배열 내의 원소를 하나로 합침

# 문제
# 문제의 조건에 따르면 우선순위가 다음과 같아야 함. 3 > 30
# 그러나 일반적인 정렬은 다음과 같이 정렬됨. 30 > 3
# 문제의 의도대로 정렬이 되려면, 3은 33으로 취급해야 함.

def solution(numbers):
    str_numbers = list(map(str, numbers))
    str_numbers.sort(key=lambda x: x*3, reverse=True)
    print(str_numbers)
    answer = ''.join(str_numbers)
    
    return str(int(answer))

 

아래 방법은 주목할 만하다.

`str_numbers.sort(key=lambda x: x*3, reverse=True)`

 

문자열로 바꿔서 내림차순으로 정렬하면, 예컨대 3이 30보다 작은 수로 인식된다.

그러나 실제로는 3이 더 큰 수로 인식되어야 한다.

 

이것을 해결하기 위해 lambda로 같은 문자열을 반복한 후에 정렬을 한다.

`x*2`이 아니라 `x*3`을 하는 이유는, 입력된 숫자의 범위가 1 <= x <= 1000이기 때문이다.

 

예컨대 `x*2` 조건으로 9와 991을 정렬하는 경우, 99와 991991로 3번째 문자열 비교 때 991991이 우선 순위가 높아지게 된다.

이런 문제를 방지하고자 `x*3`을 사용해야 한다.

 

 

3. H-Index (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42747

# 이해
# 주어진 배열을 h를 기준으로 둘로 나눈다.
# 이때 h 이상인 배열의 길이가 h 이상이 된다. h 이하인 배열의 길이가 h 이하가 된다. 이것이 h-index.
# 이분탐색처럼 느껴지기도 한다.
# 그러나 n의 크기가 1000 이하 이므로 O(N ** 2)으로 완전탐색해도 되겠다.

# 풀이
# 배열을 정렬한다.
# 피봇 h를 정한다. 대략 길이의 절반쯤 위치의 인덱스로
# 피봇을 기준으로 큰 값과 작은 값을 계산한다.
# 피봇을 적당히 옮겨서 h의 최댓값을 구한다.


def solution(citations):
    citations.sort()
    n = len(citations)
    h_index = 0
    
    for i, citation in enumerate(citations):
        h = min(citation, n - i)
        h_index = max(h_index, h)
    
    return h_index

 

이 문제도 꽤 오래 고민했다.

`h = min(citation, n - i)` 부분이 직관적으로 와닿지 않았다.


h-index의 정의에 따르면,

"논문 중 h번 이상 인용된 논문이 h편 이상이고, 나머지 논문이 h번 이하 인용되었을 때, h의 최대값이 h-index"이다.

 

이를 효과적으로 계산하기 위해 다음 두 값을 고려하자.

  1. citation: 현재 논문의 인용 횟수
  2. n - i: 현재 논문을 포함해서 그 이상 인용된 논문의 수. (여기서 n은 논문의 총 수, i는 현재 논문의 인덱스(0부터 시작))

예를 들어, 논문이 인용 횟수에 따라 오름차순으로 정렬된 상태에서, 특정 위치 i에서의 논문 인용 횟수가 10회라고 가정하자.

그리고 이 논문이 전체 논문 중에서 상위 15번째에 위치한다면 (n - i = 15), 다음 두 가지를 고려해야 한다.

  • 현재 논문은 10회 인용됨.
  • 현재 논문을 포함하여 15편의 논문이 10회 이상 인용.

여기서 h-index의 조건을 만족시키기 위해 두 값 중 작은 값을 선택하자.

즉, min(10, 15) = 10이다.

이는 이 논문이 적어도 10번 이상 인용된 10편의 논문 중 하나임을 보장한다.

 

그러나 만약 이 논문의 인용 횟수가 20회이고, 같은 조건에서 n - i = 15라면,
min(20, 15) = 15가 된다.

이는 15편의 논문이 최소 15회 이상 인용되었음을 보여주며, h-index가 될 수 있는 후보이다.

출처: GeeksforGeeks


 

1. 더 맵게 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42626

# 이해
# 최소 힙
# 값이 작은 2개를 pop()해서, 특정 공식으로 계산된 결과를 push()
# 최소 값이 K를 넘을 때까지 반복

# 풀이
# scoville = heapq.heapify(scoville)
# cnt = 0
# while True:
    # scoville을 정렬하여, [0]번째 원소가 K보다 작으면 (heap은 subscription이 가능한가?)
        # first = heapq.heappop(scoville)
        # second = heapq.heappop(scoville)
        # new = first + (second * 2)
        # heapq.heappush(scoville, new)
        # cnt += 1
    # 작지 않으면 return cnt
    

import heapq

def solution(scoville, K):
    answer = 0
    heapq.heapify(scoville)
    
    while len(scoville) >= 2:
        if scoville[0] < K:
            first = heapq.heappop(scoville)
            second = heapq.heappop(scoville)
            new = first + (second * 2)
            heapq.heappush(scoville, new)
            answer += 1
        else:
            return answer
    
    return answer if scoville[0] >= K else -1

 

힙의 크기가 1 이하가 되면, 더 이상 계산을 진행할 수 없으므로 while의 범위를 설정한다.

while을 벗어난 뒤에도 마지막으로 조건을 만족하는지 확인한 뒤에 결과를 return한다.

 

 

2. 디스크 컨트롤러 (Lv. 3)

https://school.programmers.co.kr/learn/courses/30/lessons/42627

# 이해 1
# 전체 작업 소요 시간의 평균이 최소가 되도록
# 매번 가능한 작업 중 소요 시간이 작은 것을 먼저 수행한다.
    # 반례가 있다.
    # [[0, 3], [4, 10], [5, 1], [6, 1]]
    # 3, 14, 15, 16 == 11.25
        # 3 + 10 + 10 + 10
    # 3, 6, 7, 17 == 8.25
        # 3, 1, 1, 10
# 끝나는 시간을 기준으로 해야 할 듯하다.
# 배열을 탐색하여, 원소 별로 끝나는 시간(시작 시간 + 소요 시간)을 구한다.
# 끝나는 시간이 빠른 것 순서대로 처리한다.

# 이해2
# 이해 1은 틀렸다 !! 정확히는 문제의 조건에 부합하지 않는다.
# <제한사항>의 마지막 항목: "하드디스크가 작업을 수행하고 있지 않을 때에는 먼저 요청이 들어온 작업부터 처리합니다."
# 따라서 수행 가능한 작업이 있다면 즉시 실행한다.
# 수행 가능한 작업이 여러 개가 있다면 소요 시간이 짧은 것을 실행한다.

# 풀이
# 현재 시간을 표시할 변수 now = 0
# 총 대기 시간 변수 total_waiting_time = 0
# 현재 가능한 작업 큐 ready_queue = []
# 현재 index = 0

import heapq

def solution(jobs):
    jobs.sort()
    now = 0
    total_waiting_time = 0
    index = 0
    ready_queue = []

    while index < len(jobs) or ready_queue:
        while index < len(jobs) and jobs[index][0] <= now: # 현재 시간 now보다 작은(실행 가능한) 작업 모두 추가
            heapq.heappush(ready_queue, (jobs[index][1], jobs[index][0]))
            index += 1

        if ready_queue:
            duration, start = heapq.heappop(ready_queue)
            now += duration
            total_waiting_time += now - start
        else:
            now = jobs[index][0]

    return total_waiting_time // len(jobs)

 

3시간 이상 풀이한 듯하다 ..

적어도 1시간은 문제의 조건을 놓쳐서 헤맸다.

 

각 작업의 요청부터 종료 시간의 평균을 최소로 하려면, 작업이 종료되는 시간을 기준을 해야 한다고 판단했다.

가령 다음과 같은 테스트 케이스가 있다고 가정하자. `[[0, 3], [4, 10], [5, 1], [6, 1]]`

 

매 순간 가능한 작업 중 소요 시간이 가장 짧은 것을 선택하면 다음과 같다. `[0, 3] -> [4, 10] -> [5, 1] -> [6, 1]`

이 경우 요청부터 종료까지 3 + 10 + 10 + 10 -> 평균 8.25초가 소요된다.

 

끝나는 시간을 기준으로 가장 먼저 끝나는 작업을 선택하면 다음과 같다. `[0, 3] -> [5, 1] -> [6, 1] -> [4, 10]`

이 경우는 3 + 1 + 1 + 13 -> 평균 4.5초가 소요된다.

 

그러나 문제의 조건 중 다음을 놓쳤다. `하드디스크가 작업을 수행하고 있지 않을 때에는 먼저 요청이 들어온 작업부터 처리합니다.`

조건을 만족하려면, 반드시 `현재 가능한 작업 중 소요 시간이 가장 짧은 것을 선택`해서 수행하면 된다.

 

현재 작업의 시작 시간이 now 보다 작다면 ready_queue에 추가한다. `while index < len(jobs) and jobs[index][0] <= now:`

현재 작업의 시작 시간이 now 보다 크다면  now를 현재 작업의 시작 시간에 맞춘다. `else:`

 

 

3. 이중우선순위큐 (Lv. 3)

https://school.programmers.co.kr/learn/courses/30/lessons/42628

# 이해
# 명령어를 받아서 힙을 구성
# 명령어에 따라 최솟값 혹은 최댓값을 pop()
# 모든 명령어를 수행하고 남은 힙의 최솟값과 최댓값을 return

# 풀이
# 명령어의 순서를 미리 기록한다.
# 가령 최솟값 삭제면 False, 최댓값 삭제면 True
# 힙에 추가하는 명령어를 실행할 때마다, 순서 배열을 보면서 최댓값 삭제(True)인 경우 -값을 추가한다.
# 모든 명령어는 최솟값 삭제로 실행한다.

import heapq

def solution(operations):
    answer = []
    heap = []
    
    for operation in operations:
        op, num = operation.split()
        
        if op == "I":
            heapq.heappush(heap, int(num))
        elif heap:
            if num == "-1":
                heapq.heappop(heap)
            else: # num == "1"
                heap.remove(max(heap))
    
    return [0, 0] if len(heap) == 0 else [max(heap), min(heap)]

 

O(NlogN) 풀이가 떠오르지 않아서 구글링을 했다.
다들 그냥 remove 함수를 사용한다.

 

O(N^2) 아닌가...? N == 10 ** 6이니까 10 ** 12로, 시간 초과 기준(10 ** 9)을 아득히 넘는다.
완성된 힙을 탐색하는 것이 아니라, 완성해가면서 탐색하는 것이기 때문에 가능한가보다.

 

다시 한번 느꼈다. 프로그래머스에서 문제 풀 때는 시간 복잡도는 신경 쓰는 것이 아니다.
N이 1억이나 10억이면 이분 탐색 문제이고, 아니라면 일단 O(N ** 2)으로 풀이하자.

 

 

 

출처: Better Programming


 

1. 같은 숫자는 싫어 (Lv. 1)

https://school.programmers.co.kr/learn/courses/30/lessons/12906

# 이해
# 스택에 연속된 숫자가 저장되면 안 됨.

# 풀이
# arr을 탐색하며 stack에 추가
# 추가하기 전, stack의 top과 같은 경우 pass, 다른 경우 push

def solution(arr):
    stack = []
    
    for elem in arr:
        if stack:
            if elem != stack[-1]:
                stack.append(elem)
        else:
            stack.append(elem)
    
    return stack

 

 

 

기능개발 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42586

# 이해
# progresses의 원소는 100%를 달성하면 배포된다.
# 매일 speeds의 원소만큼 progress가 진행된다.
# 가장 앞에 있는 원소가 100%를 달성한 순간에, 100%를 달성한 모든 원소를 배포한다.

# 풀이
# progresses의 각 원소를 100에서 빼고, 그 값을 speeds로 나누어 남은 일자를 계산한다. remain_days: [Int]
# remain_days를 탐색하며, 내림차순으로 정렬된 경우 tmp에 더하고, 
    # 내림차순이 깨진 순간 tmp를 answer에 append하고, tmp를 초기화 한다.
# answer를 return한다.

import math

def solution(progresses, speeds):
    answer = []
    remain_days = [math.ceil((100 - progress) / speed) for progress, speed in zip(progresses, speeds)]
    
    current_max_day = remain_days[0]
    count = 0
    
    for day in remain_days:
        if day <= current_max_day:
            count += 1
        else:
            answer.append(count)
            current_max_day = day
            count = 1
    
    answer.append(count)
    
    return answer

 

`zip()` 함수는 여러 개의 iterators를 인자로 받아, 인덱스를 기준으로 짝 지은 튜플을 반환하는 함수이다.

위 코드에서 math.ceil()는 필수적이지 않다. `-((progress - 100) // speed)`의 형태로 작성해도 같은 결과이다.

 

 

 

3. 올바른 괄호 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/12909

나의 풀이

# 이해
# 괄호가 올바르게 짝지어지면 True
# 하나라도 틀리면 False

# 풀이
# "("인 경우
    # stack이 비었으면 추가한다.
    # stack에 원소가 있다면, stack의 top이 "("이라면 추가. ")"이라면 return False
# ")"인 경우
    # stack이 비었으면 return False
    # stack에 원소가 있다면, stack의 top이 "("이라면 stack.pop(), ")"이라면 return False

def solution(brackets):
    stack = []
    
    for bracket in brackets:
        if bracket == "(":
            if not stack:
                stack.append(bracket)
            else:
                if stack[-1] == "(":
                    stack.append(bracket)
                else:
                    return False
        
        else: # bracket == ")"
            if not stack:
                return False
            else:
                if stack[-1] == "(":
                    stack.pop()
                else:
                    return False
                
    return True if not stack else False

 

더 쉬운 풀이

def is_pair(s):
    wt = 0
    
    for c in s :
        if c == '(' :
            wt += 1
        elif c == ')':
            wt -= 1
        
        if wt < 0: 
            return False
        
    return wt == 0

 

스택에 성질을 활용하면 `나의 풀이`처럼 모든 케이스를 대응할 필요가 없다.

 

 

 

4. 프로세스 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42587

나의 풀이

# 이해
# priorities 배열을 우선순위에 따라 pop() 할 때, 
# location 위치에 있는 원소가 몇 번째로 pop() 되는지 return

# 풀이
# max_value = max(priorities)
# priorities_with_hash = [[i, hash(i)] for i in priorities]
# max_value_hash = priorities_with_hash[location][1]
# order = 0
# while len(priorities_with_hash) >= 0:
    # current = popleft()
    # current[0] >= max_value, 
        # if current[1] == max_value_hash:
            # return order
        # order += 1
    # current[0] < max_value:
        # priorities_with_hash.append(current)

from collections import deque
import random

def solution(priorities, location):
    order = 1
    priorities_with_hash = deque([[priorities[i], i] for i in range(len(priorities))])
    max_value = max(priorities)
    target_hash = priorities_with_hash[location][1]
    
    while len(priorities_with_hash) >= 0:
        current = priorities_with_hash.popleft()
        
        if current[0] >= max_value: 
            if current[1] == target_hash:
                return order
            max_value = max([elem[0] for elem in priorities_with_hash])
            order += 1
            
        else: # current[0] < max_value
            priorities_with_hash.append(current)
            
    return order

 

더 나은 풀이

def solution(priorities, location):
    queue =  [(i,p) for i,p in enumerate(priorities)]
    answer = 0
    while True:
        cur = queue.pop(0)
        if any(cur[1] < q[1] for q in queue):
            queue.append(cur)
        else:
            answer += 1
            if cur[0] == location:
                return answer

 

`any()`: 인자로 받은 iterators 중에 하나라도 True이면, 전체를 True로 반환한다.

 

 

 

5. 다리를 지나는 트럭 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42583

# 이해
# bridge_length는 다리에 올라갈 수 있는 트럭 수. 동시에 트럭이 이동해야 하는 길이.
# weight는 다리가 버틸 수 있는 무게. 트럭의 개수가 bridge_length보다 적어도, 무게 기준을 만족해야 한다.
# 모든 트럭이 다리를 건너는 데까지 걸리는 시간을 return

# 풀이
# 정석적인 원형 큐로 풀이해보자
# bridge = deque([])
# bridge.append(truck_weights[0])
# bridge_sum += truck_weights[0]
# time = 0

# while True:
    # time += 1
    # out_truck = bridge.popleft()
    # bridge_sum -= out_truck
    # if out_truckt > 0 and len(truck_weights) == 0:
        # return time
    
    # if bridge_sum + truck_weights[0] < weight:
        # in_truck = truck_weights.popleft()
        # bridge.append(in_truck)
        # bridge_sum += in_truck
        
    # else:
        # bridge.append(out_truck)

from collections import deque
        
def solution(bridge_length, weight, truck_weights):
    
    bridge = deque([0] * bridge_length)
    truck_weights = deque(truck_weights)
    bridge_sum = 0
    time = 0

    while truck_weights or bridge_sum > 0:
        time += 1
        out_truck = bridge.popleft()
        bridge_sum -= out_truck

        if truck_weights and bridge_sum + truck_weights[0] <= weight:
            in_truck = truck_weights.popleft()
            bridge.append(in_truck)
            bridge_sum += in_truck
        else:
            bridge.append(0)
    
    return time

 

`if truck_weights and bridge_sum + truck_weights[0] <= weight:` 이러한 방식이 좋다.

반복할 때마다 판단해야 하는 조건이 있다면, if이든 while이든 조건문에 포함시키자.

 

 

 

6. 주식가격(Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42584

나의 풀이 (큐)

from collections import deque

def solution(prices):
    queue = deque(prices)
    answer = []
    
    while queue:
        price = queue.popleft()
        sec = 0
        for q in queue:
            sec += 1
            if price > q:
                break 
        answer.append(sec)        
    return answer

 

복잡할 것이 없는 풀이이다.


그러나 30분 동안 고민했다.

주어진 배열의 길이가 10 ** 5이어서, O(N ** 2)은 시간 초과가 발생할 수 있다고 판단했기 때문이다.

실제로는 시간 초과가 발생하지 않았다.

계속해서 queue의 크기를 줄여나가기  때문인 듯하다.

 

실제로 코테에서도 웬만한 크기(10 ** 4 ~ 10 ** 5)라면 일단 완전탐색으로 풀이하자.

그렇게 부분 점수 먼저 확보해 놓고, 시간복잡도를 개선하는 것이 훨씬 현실적인 코테 접근법이다.

 

더 나은 풀이 (스택)

# 이해
# 배열의 각 원소가 오름차순이 지속되는 길이를 각각 return
# 다음 원소와 비교하여, 값이 작아지지 않을 때마다 += 1
# 일단 지속 시간을 1초 증가시키고, 그 뒤에 오름차순 여부를 판단한다.
# 마지막 원소는 0초이다.

# 풀이
# 일단 이중 반복문으로 풀자

from collections import deque

def solution(prices):
    length = len(prices)
    answer = [i for i in range(length - 1, -1, -1)]
    
    stack = [0]
    for i in range(1, length):
        while stack and prices[stack[-1]] > prices[i]:
            j = stack.pop()
            answer[j] = i - j
        stack.append(i)
    
    return answer

 

https://velog.io/@soo5717/프로그래머스-주식가격-Python

위 블로그 글을 참고한 풀이이다.

 

 

 

 

chatGPT가 생각하는 Optional

 

Optional Type

옵셔널 타입이란 값이 있을 수도 있고, 없을 수도 있는 타입을 말한다.

 

Swift에서는 값이 없음을 표시하기 위해 nil을 사용한다.

참조가 없음을 의미하는 다른 언어의 nil과는 사뭇 다르다.

 

var message: String = "My String"
message = nil // Error

 

Swift의 타입은 기본적으로 Non-Optional 타입이다. 즉, 반드시 값이 있어야 한다.

이러한 타입에 nil을 할당하면 컴파일 에러가 발생한다.

 

var myString1: String?
var myString2: Optional<String>

 

 

이것이 옵셔널 타입을 선언하는 방식이다.

위 두 변수는 이름만 다르고 같은 변수이다.

옵셔널 타입의 변수의 초깃값은 기본적으로 nil로 설정된다.

 

enum Optional<T> {
    case None
    case Some(T)
}

 

옵셔널 타입은 내부적으로 Enumertaion으로 정의한다.

 

우리가 어떤 변수를 옵셔널 타입을 선언할 때,

해당 변수에 nil을 할당했다면, None 값을 갖는 것이다.

해당 변수에 특정 값을 할당했다면, <T>에 대응하는 associated value를 갖게 되는 것이다.

 

 

Optional Type의 필요성

옵셔널은 런타임 오류를 방지하는 데에 그 목적이 있다.

옵셔널을 사용하면 컴파일 단계에서 변수 초기화 문제를 발견할 수 있다.

 

int i;
MyObject *m;

-(int)myMethodWithValue:(int)i {
    return i*2;
}

NSLog(@"Value: %d",[m myMethodWithValue:5]);

 

Objective-C에 이런 코드가 있다.

i와 m을 선언하고, 초기화는 하지 않는다고 가정하자.

이 코드를 실행하면 의도한 10(5 * 2)의 값이 아니라, 0의 값을 얻게 된다.

 

왜 why?

옵젝씨에서는 값을 초기화하지 않은 정수형 변수를 0으로 초기화하나 보다.

중요한 것은 에러조차 없이 이상하게 동작한다는 것이다.

이런 문제는 찾아내기 굉장히 어렵다.

 

var myString: String
print(myString) // ERROR: Variable 'myString' used before being initialized

 

같은 상황에서 Swift는 컴파일 에러를 띄워준다.

값이 필요하면 반드시 초기화를 하고,

값이 없을 수도 있는 경우에는 무조건 Optional 변수를 사용하라는 뜻이다. 참 따뜻하다.

 

 

Optional Unwrapping

옵셔널 타입의 변수를 사용하기 전에는 반드시 값이 있는지 없는지 검증해야 한다.

nil이 할당되어 있는 변수를 사용하려다가는 런타임 에러가 발생하면서 앱이 깨지고 만다. 와장창.

 

옵셔널 변수의 값을 얻어내는 것을 Optional Unwrapping이라고 한다.

두 가지 방법을 제시한다.

 

Forced unwrapping

// #1
var myString1: String?
myString1 = "test"
var test: String = myString1!
print(myString1) // test

// #2
var myString1: String?
myString1 = nil
var test: String = myString1! // Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
print(test)

 

옵셔널 변수의 끝에 느낌표(!)를 붙이면, 안에 있는 값을 강제로 얻을 수 있다.

프로그래밍에서 강제란... 보통은 하면 안 되는 것이다.

 

Swift에서도 같다.

웬만한 경우에는 Force unwrapping을 사용하지 않을 것을 권장한다.

 

#1번의 경우에는 정상적으로 unwrapping이 된다.

#2번의 경우에는 컴파일러가 myString1에는 값이 있을 것이라고 굳게 믿고 코드를 실행한다.

아뿔싸. 값이 없다. 런타임 에러를 만나고 앱이 깨진다.

 

Optional Binding

// #1
if let constantName = optionalVariable {
    statements
}

// #2
if var variableName = optional {
    statements
}

 

보다 평화롭고 권장되는 방법이다.

등호를 기준으로 오른쪽에 있는 옵셔널 변수의 값을 확인한다.

nil이 아닌 값이라면, constantName 변수에 할당한다.

if statement 범위에서 constantName 변수를 자유롭게 사용할 수 있다.

 

세간의 인식과는 다르게 `if let` 뿐만 아니라 `if var`도 가능하다.

 

var myString3: String?
myString3 = "Space, the final frontier"

if let tempVar = myString3 {
    print(tempVar) // "Space, the final frontier"
} else {
    print("No value")
}

print(tempVar) // Compile Error: Cannot find 'tempVar' in scope

 

실전 예시를 보면 이렇다.

함수 내부에서 tempVar는 잘 사용할 수 있다.

함수를 벗어나서 사용하려고 하면, 컴파일 에러가 발생한다. 당연하다.

 

if let tmp1 = optional1, let tmp2 = optional2, let tmp3 = optional3 {
    print(tmp1)
    print(tmp2)
    ...
}

 

위와 같이 하나의 optional binding line에 여러 개의 옵셔널 변수를 unwrapping하는 것도 가능하다.

이때 하나의 변수라도 nil이면, 모든 unwrapping에 실패한다.

 

if let myOptional = myOptional {
    print(myOptional)
} else {
    print("myOptional was nil")
}

 

같은 이름의 변수에 unwrapping 결과물을 할당하는 것도 가능하다.

변수 이름 작명에 고심하는 개발자를 배려하는 따뜻한 조처이다.

 

Optional types with tuples

var tuple1: (one: String, two: Int)?
var tuple2: (one: String, two: Int?)

 

튜플을 통째로 옵셔널로 만들 수 있다.

튜플의 원소 하나만을 옵셔널로 만들 수도 있다.

 

 

Optional Chaining

var tireSize = car?.tires?.tireSize

 

프로퍼티, 메서드, subscript를 호출할 때, 그것의 return 값이 optional인 경우 위와 같이 사용할 수 있다.

 

chaining된 값 중에서 하나라도 nil이면, 전체 변수가 nil이 된다.

모든 값이 nil이 아니라면, 전체 변수는 optional unwrapping된 값을 얻는다.

 

The nil coalescing operator

  optionalA ?? defaultValue

 

병합 연산자라고들 하는 것 같다. 발음은 코얼레싱에 가깝다.

 

삼항 연산자(tenary operator)와 유사한 방식으로 동작한다.

좌항의 값이 nil이라면 defaultValue가 할당된다.

nil이 아니라면 optional unwrapping된 값이 할당된다.

 

varr defaultName = "Jon"
var optionalA: String?
var optionalB: String?
optionalB = "Buddy"

var nameA = optionalA ?? defaultName // "Jon"
var nameB = optionalB ?? defaultName // "Buddy"

 

어렵지 않다.

optionalA는 nil이니까 defaultName.

optionalB는 nil이 아니니까 "Buddy"

 

var nameC1 = optionalA ?? defaultName
var nameC2 = optionalA != nil ? optionalA! : defaultName

 

위 두 문장은 같은 코드이다.

coalescing operator를 적극 활용하자.

 

 


참고

<Mastering Swift 5.3>, Jon Hoffman, 6th Edition

Chapter 3: Learning about Variables, Constants, Strings, and Operators

출처: 위키피디아


1. 폰켓몬 (Lv. 1)

https://school.programmers.co.kr/learn/courses/30/lessons/1845

# 이해
# N마리 중에서 N/2마리를 선택.
# 이때 가장 많은 종류를 선택하고, 그때의 가짓수를 return
# nums의 길이는 10,000이므로 O(n^2)까지는 괜찮겠다. 프로그래머스는 10억에서 시간 초과가 발생하므로.

# 풀이
# nums+1(0부터 200,000) 크기의 visited 배열을 만든다.
# nums를 탐색하며, 방문하지 않은 포켓몬일 경우 result, cnt에 += 1 하고, 방문 처리한다.
# 매 탐색마다 cnt가 len(nums) // 2 이하인지 확인한다. 초과하면 result를 return 한다.

def solution(nums):
    return min(len(nums) // 2, len(set(nums)))

 

최초에는 방문 처리를 통해 최소 횟수로 최대 개수를 확보하려고 했다.

그러나 모든 포켓몬이 다른 종류인 경우 최대 방문 가능 횟수(`len(nums) // 2`) == 정답이다.

포켓몬의 종류가 `len(nums) // 2`보다 작은 경우 그것이 정답이다.

 

 

2. 완주하지 못한 선수 (Lv. 1)

https://school.programmers.co.kr/learn/courses/30/lessons/42576

 

최초 풀이 (정렬)

# 이해
# completion에 있지만, participant에 없는 사람 return
# 동명이인이 있을 수 있다.

# 풀이
# 정렬한다.
# 순서대로 나오지 않으면 즉시 return 한다.

def solution(participant, completion):
    participant.sort()
    completion.sort()
    for i in range(len(completion)):
        if participant[i] != completion[i]:
            return participant[i]
    return participant[len(participant)-1]

 

더 나은 풀이 (hash 함수)

def solution(participant, completion):
    
    dic = {}
    tmp = 0
    
    for part in participant:
        dic[hash(part)] = part
        tmp += hash(part)
    
    for comp in completion:
        tmp -= hash(comp)
        
    return dic[tmp]

 

hash 함수의 성질을 이용한 풀이이다.

배열의 각 원소를 탐색하며, hash()로 생성된 고유 숫자를 더하고 빼면, 하나의 고유 숫자만이 남는다는 것을 응용했다.

 

 

3. 전화번호 목록 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42577

# 이해
# 어떤 번호가 다른 번호의 접두어인 경우 false
# 아니라면 true
# str.startswith() / str.endswith() -> Bool

# 풀이
# phone_book을 탐색하며, 딕셔너리에서 현재 단어가 없다면,
# 현재 단어를 1글자씩 tmp에 더해가며 딕셔너리가 True가 반환되는 것이 있는지 확인.
# 있으면 return 없으면 완성된 단어를 True로 갱신


def solution(phone_book):
    dic = {}
    
    for pb in phone_book:
        dic[pb] = 1
    
    for pb in phone_book:
        tmp = ""
        for pb_chr in pb:
            tmp += pb_chr
            if tmp in dic and tmp != pb:  # 여기서도 동일하게 'if' 사용
                return False
    
    return True

 

단어 배열을 탐색하며, 완성된 단어는 모두 방문 처리한다.

단어 배열을 다시 탐색하며, 각 단어를 이중 탐색한다.

 

한 글자씩 더해가며 딕셔너리에 있는 단어인지 판단한다.

이때, 한 글자씩 더해가다가 단어가 완성된 경우는 제외한다. 문제의 조건에 중복된 단어는 없기 때문이다

 

 

4. 의상 (Lv. 2)

https://school.programmers.co.kr/learn/courses/30/lessons/42578

# 이해
# 패션 조합의 가짓수
# 하루에 최소 한 개의 의상
# 한 종류에서는 하나의 옷만 선택
# 모든 종류를 선택할 수도, 하나의 종류만 선택할 수도, 복수의 종류만 선택할 수도 있다.

# 풀이
# from itertools import combinations
# type별로 카운트 횟수를 딕셔너리에 기록함
# type에 대한 조합을 구함.
# 모든 조합에 대하여, type에 대응하는 카운트 횟수를 곱해 answer에 더함.
# answer 반환.

def solution(clothes):
    answer = 1  # 곱셈을 위해 1부터 시작
    dic = {}
    
    # 각 옷의 종류별로 개수를 계산
    for _, type in clothes:
        dic[type] = dic.get(type, 0) + 1
    
    # 각 종류별로 옷을 선택하는 경우의 수 계산 (해당 종류의 옷을 안 입는 경우 포함)
    for count in dic.values():
        answer *= (count + 1)  # 해당 종류의 옷을 선택하지 않는 경우도 포함하여 +1
    
    return answer - 1  # 아무것도 입지 않는 경우의 수 1을 뺌

 

최초에는 combinations를 사용하여 복잡하게 풀이했다.

더 나은 풀이를 참고하니 깨달음을 얻었다.

 

선택하지 않는 것을 포함한 모든 조합의 수를 구하는 것 == 케이스마다 선택 가능한 `경우의 수 + 1` 모두 곱하기

 

 

5. 베스트앨범 (Lv. 3)

https://school.programmers.co.kr/learn/courses/30/lessons/42579

# 이해
# 1. 해당 장르의 재생 수
# 2. 동일 장르 내 곡의 재생수
# 3. 고유번호가 낮은 것

# 풀이
# genre_count = { 장르: 총 재생 횟수 }
# genre_dic = { 장르: [고유번호, 재생 횟수] }
# genre_count -> 재생 횟수 -> 고유 번호 순서대로 정렬
# 각 장르마다 2개씩 끊어서 고유 번호 저장

def solution(genres, plays):
    answer = []
    
    genre_count = {}
    genre_dic = {}
    
    for i in range(len(genres)):
        if genres[i] in genre_count:
            genre_count[genres[i]] += plays[i]
        else:
            genre_count[genres[i]] = plays[i]
        
        genre_dic.setdefault(genres[i], []).append([i, plays[i]])
    
    # genre_count의 values의 내림차순으로 정렬
    genre_count_sorted = sorted(genre_count.items(), key=lambda x: x[1], reverse=True)
    
    for genre, _ in genre_count_sorted:
        sorted_songs = sorted(genre_dic[genre], key=lambda x: (-x[1], x[0]))
        answer.extend([song[0] for song in sorted_songs[:2]])
    
    
    return answer

 

최초에는 정말 복잡하게 풀었다.

 

`dictionary.setdefault(val, default_val)` 함수를 기억하자.

lambda로 정렬 함수를 작성할 때 `-, +` 부호를 활용하여 `내림차순, 오름차순`을 쉽게 표현할 수 있다는 것을 기억하자.

 

구현 문제가 아닌 이상 코딩테스트의 코드는 과도하게 복잡해지지 않는다는 것을 기억하자.

실전에서는 어찌 됐든 풀이하는 게 맞다. 그러나 코드의 엔트로피가 올라가고 있다면 뭔가 잘못 됐다는 것을 의식하자.

 

 

 

유용한 문법

global 키워드

a = 0
array = [1, 2, 3]

def func():
    global a # 전역 변수를 사용하겠다.
    a += 1
    
    array.append(4) # 배열은 global 없이 사용 가능하다.

for i in range(10):
    func()

print(a) // 10
print(array) # [1, 2, 3, 4]

 

우선 순위: 지역 > 전역

함수 내에서 전역 변수를 사용하고 싶다면, global로 해당 변수를 함수 스코프로 가져와야 함.

 

배열은 global 없이도 사용 가능하다.

그러나 함수 내에 같은 이름의 배열이 있다면, 지역 배열 우선이다.

 

packing, unpacking

def operator(a, b):
    add_var = a + b
    subtract_var = a - b
    multiply_var = a * b
    divide_var = a / b
    return add_var, subtract_var, multiply_var, divide_var

a, b, c, d = operator(7, 3)
print(a, b, c, d) # 10, 7, 21, 2.33333

 

Python은 한 번에 여러 개의 값을 return 할 수 있다. 이것이 packing.

return된 값들을 한 번에 여러 개의 변수에 할당할 수 있다. 이것이 unpacking.

 

람다 표현식

def add(a, b):
    return a + b
 
# 일반적인 함수 표현식
print(add(3, 7)) # 10

# 람다 표현식
print((lambda a, b: a + b)(3, 7)) # 10

 

간단한 함수의 경우, 함수의 정의 없이 lambda 표현식을 사용하여 표현할 수 있다.

자바스크립트의 arrow 표현식이나, Swift의 closure와 비슷한 듯하다.

 

sort 혹은 map 함수에 자주 쓰인다.

# 정렬할 때
array = [("홍길동", 50), ("이순신", 32), ("아무개", 74)]

def my_key(x):
    return x[1]

print(sorted(array, key = my_key)) # [("이순신", 32), ("홍길동", 50), ("아무개", 74)]
print(sorted(array, key = lambda x: x[1]) # [("이순신", 32), ("홍길동", 50), ("아무개", 74)]
# 여러 개 리스트에
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]

result = map(lambda a, b: a + b, list1, list2)
print(list(result)) # [7, 9, 11, 13, 15]

 

 

표준 라이브러리

내장 함수

입출력, 정렬 등 기본 함수 제공.

 

itertools

반복되는 형태의 데이터를 처리.

순열, 조합, 중복 순열, 중복 조합 등

 

heapq

힙(heap, 최대값 or 최소값을 빠르게 찾아내기 위한 완전이진트리) 자료구조 제공.

일반적으로 우선순위 큐 기능을 구현하기 위해 사용.

 

bisect

이진 탐색(Binary Search) 기능 제공.

 

collections

deque, counter 등

 

math

수학적 기능 제공,.

팩토리얼, 제곱근 최대공약수(GCD), 삼각함수 등 함수.

pi 등 상수.

 

내장 함수

# sum
result = sum([1, 2, 3, 4, 5])
print(result) # 15

# min, max
min_res = min(7, 3, 5, 2)
max_res = max(7, 3, 5, 2)
print(min_res, max_res) # 2, 7

# eval
result = eval("(3 + 5) * 7") # 띄어쓰기 해도 된다. 짱이썬.
print(result) # 56
# sorted()
# 기본적으로 오름차순
result = sorted([9, 1, 8, 5 , 4]) # [1, 4, 5, 8, 9]
reverse_result = sorted([9, 1, 8, 5, 4], reverse = True) # [9, 8, 5, 4, 1]

# sorted() with key
array = [("홍길동", 50), ("이순신", 32), ("아무개", 74)]
result = sorted(array, key = lambda x: x[1], reverse = True) # [("이순신", 75), ("아무개", 50), ("홍길동", 35)]

 

itertools

# 순열(permutation)
from itertools import permutations

data = ["A", "B", "C"]
result = list(permutations(data, 3))
# [('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]
# 조합(combinations)
from itertools import combinations  

data = ["A", "B", "C"]
result = list(combinations(data, 2))

print(result)
# 중복 순열(product)
from itertools import product  

data = ["A", "B", "C"]
result = list(product(data, repeat = 2))

print(result)
# [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
# 중복 조합(combinations_with_replacement)
from itertools import combinations_with_replacement  

data = ["A", "B", "C"]
result = list(combinations_with_replacement(data, 2))

print(result)
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]

 

Collections - Counter

# Counter
from collections import Counter

counter = Counter(["red", "blue", "red", "green", "blue", "blue"])
counter_dict = dict(Counter(["red", "blue", "red", "green", "blue", "blue"]))
print(counter) # Counter({'blue': 3, 'red': 2, 'green': 1})
print(counter_dict) # {'red': 2, 'blue': 3, 'green': 1}

 

math

import math

def lcm(a, b): # lcm(최소공약수)의 성질을 이용해 구하는 함수
    return a * b // math.gcd(a, b) # gcd는 최대공약수

print(lcm(21, 14))

 

 

출처

이것이 취업을 위한 코딩 테스트다 with 파이썬, 나동빈, 2021

8. Leader lesson - Learn to Code - Codility


문제 분석

 배열을 쪼개, 두 배열 모두 같은 leader를 갖게 하는 index를 모두 return

의사 코드

 leftCount = 0, rightCount = 배열의 길이, leftLeaderCount = 0, rightLeaderCount = 0
 1 .leader 구함
 2. 배열 탐색하면 leader를 만나면
     leftLeaderCount += 1
     rightLeaderCount -= 1
leftSize += 1
rightSize -= 1
leftLeaderCount > leftSize / 2 && rightLeaderCount > rightSize / 2일 때, answer += 1

풀이

import Foundation
import Glibc

public func solution(_ A : inout [Int]) -> Int {
    // leader 구한다.
    let sortedA = A.sorted()
    let candidate = sortedA[A.count / 2]
    var cnt = 0
    for elem in A {
        if candidate == elem {
            cnt += 1
        }
    }
    let leader = cnt > A.count / 2 ? candidate : -1 

    var leftSize = 0
    var leftLeaderCount = 0
    var rightSize = A.count
    var rightLeaderCount = cnt
    var answer = 0

    for elem in A {
        if elem == leader {
            leftLeaderCount += 1
            rightLeaderCount -= 1
        }

        leftSize += 1
        rightSize -= 1

        if leftLeaderCount > leftSize / 2 && rightLeaderCount > rightSize / 2 {
            answer += 1
        }
    }

    return answer
}

Dominator coding task - Learn to Code - Codility


의사 코드

 1. leader 후보 찾기
     배열을 탐색하며, size == 0이라며면,
         해당 값을 value에 할당한다. size += 1
     size == 0이 아니라면,
         새로운 값이라면, size -= 1
         새로운 값이 아니라면, size += 1
    
     2. 후보 개수 세기
     candidate = -1
     if size > 0이라면,
         candidate = value
     배열에서 candidate와 같은 값의 index를 저장한다.
     return candidate의 개수 > 배열의 크기 / 2 이면 ? index 배열 : - 1

풀이

import Foundation
import Glibc

public func solution(_ A : inout [Int]) -> Int {
    var size = 0
    var value = 0

    for elem in A {
        if size == 0 {
            value = elem
            size += 1
        } else {
            if value != elem {
                size -= 1
            } else {
                size += 1
            }
        }
    }

    var candidate = -1
    var count = 0
    var idxArray = -1
    if size > 0 {
        candidate = value
    }

    for i in 0..<A.count {
        if A[i] == candidate {
            count += 1
            idxArray = i
        }
    }

    return count > (A.count / 2) ? idxArray : -1
}

+ Recent posts