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

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

프로그래머스 '타겟넘버' 문제를 풀며, 제목과 같은 에러를 만났었습니다.

 

이에, 해당 문제를 해결할 수 있는 방법인 nonlocal과 global 변수 범위 지정 키워드에 대해서 정리하고자 합니다.

def solution(numbers, target):
    answer = 0
    length = len(numbers)

    def dfs(i, val):
        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

 

처음에는 위와 같이 코드를 작성하였습니다. 

저는 solution 함수 내에 answer 변수가 선언 되어 있으므로, dfs 함수 내에서도 사용할 수 있을 것이라 생각하였습니다.

 

에러 코드를 보면 '할당되지 않아서' 에러가 발생함을 알 수 있습니다.

 

파이썬 컴파일러는 dfs 내의 answer 변수를 solution 함수 내의 answer 변수와 동일한 변수로 보지 않고, dfs 함수 내의 '로컬 변수'로 인식하여 발생하는 에러였습니다.

 

def solution(numbers, target):
    answer = 0
    length = len(numbers)
    visited = [False] * length

    def dfs(i, val):
        nonlocal answer
        
        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

첫번째 해결 방법은 dfs 함수 내에 answer라는 변수에 nonlocal 이라는 keyword를 지정해주는 것입니다.

해당 키워드를 지정하게 되면, answer이라는 변수를 dfs 함수 내의 로컬 변수로 취급하지 않고 solution 함수 내의 answer 변수를 의미한다고 설정해주는 것입니다.

 

def solution(numbers, target):
    global answer
    answer = 0
    length = len(numbers)
    visited = [False] * length

    def dfs(i, val):
        global answer

        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

두번째 해결 방법은 global이라는 keyword를 지정해주는 것입니다. 

global 변수는 해당 변수를 전역 변수로 사용하겠다는 것을 의미합니다.

따라서 위와 같이 각 함수 내에 global이라는 keyword를 사용하여 answer이라는 변수를 로컬 변수가 아닌 전역 변수로 설정해주는 방법이 있습니다.

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

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

풀이방법:

  • 중복된 숫자를 제거하는 문제
  • list를 탐색하며 now 변수를 활용해 연속적으로 나오는 동일한 숫자를 제거

풀이코드:

def solution(arr):
    now = arr[0]
    answer = [now]
    for i in arr:
        if now != i:
            answer.append(i)
            now = i
        else:
            continue
    return answer

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

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

풀이방법:

  • 주어진 숫자들을 모두 사용하되, 덧셈과 뺄셈을 이용해서 target과 동일한 값을 만드는 경우의 수를 구하는 문제
  • dfs를 통해 모든 원소를 더한 마지막 단계부터 시작하여 이전 단계로 돌아가며 해당 단계의 원소의 값을 더했을 때와  뺐을 때의 모든 경우의 수를 탐색

 

풀이코드:

def solution(numbers, target):
    answer = 0
    length = len(numbers)

    def dfs(i, val):
        nonlocal answer

        if val == target and i == length - 1:
            answer += 1
            return 0

        if i + 1 < length:
            dfs(i + 1, val + numbers[i + 1])
            dfs(i + 1, val - numbers[i + 1])

    dfs(-1, 0)
    return answer

https://school.programmers.co.kr/learn/courses/30/lessons/86491/solution_groups?language=python3&type=my 

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

풀이방법:

  • 명함은 옆으로 눕혀서 넣을 수 있으며, 모든 명함이 들어갈 수 있는 최소 크기를 구하는 문제
  • 따라서, 주어진 각 명함의 가로와 세로의 크기 중에서 큰 값들 중 제일 큰 값과 작은 값들 중 제일 큰 값을 구해야함

 

풀이코드:

def solution(sizes):
    maxv,minv = 0,0

    for card in sizes:
        maxv = max(card) if max(card) > maxv else maxv
        minv = min(card) if min(card) > minv else minv
    return maxv*minv

코딩 테스트 연습을 하던 중 다음과 같은 상황을 맞이하여 엄청 당황했었습니다.

s = [1,2,3]
for i in range(len(s)):
    a = s
    print(a,s)
    a.pop()

 

위 코드를 동작 시켰을 때, 제가 기대한 결과는 a라는 list에만 변화가 생기는 것이었습니다. 하지만 다음과 같은 결과가 나온 것입니다.

a: [1, 2, 3] s: [1, 2, 3]
a: [1, 2] s: [1, 2]
a: [1] s: [1]

파이썬의 copy 개념이 제대로 잡혀있지 않아 발생한 문제였습니다.

 


copy가 동작하는 방식은 데이터가 immutable / mutable 인지 그리고 shallow copy / deep copy인지에 따라 달라집니다.

  • immutable한 자료형은 int형, str형 과 같은 자료형이며 mutable한 자료형은 list, set, dictionary와 같은 자료형입니다.
  • shallow copy에는 '=', [:] , copy.copy(), 객체.copy() 연산이 있습니다.

 

shallow copy일 경우, 다음과 같이 동작합니다.

서로 다른 변수가 같은 객체를 참조하고 있는 형태 입니다. 바로 문제가 발생했던 상황이죠.

변수가 immutable할 경우 이 상황에서 문제가 발생하지 않습니다. immutable한 자료형은 바뀌지 않기 때문에 데이터에 변화가 생길 경우 새로운 객체가 생성됩니다.

 

하지만 문제는 mutable한 자료형에서 발생합니다. mutable한 자료형은 말 그대로 바뀔 수 있기 때문에 객체 자체가 바뀌게 됩니다.

 

코드로 살펴보겠습니다.

 

<immutable한 자료형에서의 shallow copy>

a = 3
b = a

print('a,b:',a,b)
print('ID of a and b:',id(a),id(b))

# a,b: 3 3
# ID of a and b: 140423687498096 140423687498096

b = 5
print('a,b:',a,b)
print('ID of a and b:',id(a),id(b))

# a,b: 3 5
# ID of a and b: 140654810294640 140654810294704

처음에는 동일한 객체를 참조하고 있지만, 값이 변하게 되면서 b가 새로운 객체를 참조하고 있음을 알 수 있습니다.

 

<mutable한 자료형에서의 shallow copy>

listA = [1,2,3]
listB = listA

print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [1, 2, 3]
# ID of listA and listB: 140613739648320 140613739648320

listB.append([1,2,3])
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3, [1, 2, 3]] [1, 2, 3, [1, 2, 3]]
# ID of listA and listB: 140613739648320 140613739648320

mutable한 자료형의 경우, 객체가 새로 생성되는 것이 아니라 객체 자체에서 변화가 생깁니다. 

저는 이러한 것을 고려하지 않고 '=' 을 통해 shallow copy를 하였기 때문에 문제가 발생했던 것이었습니다.

 

이러한 문제를 [:]을 통해 간단히 해결할 수 있습니다.(완벽한 방식은 아닙니다.)

listA = [1,2,3]
listB = listA[:]

print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [1, 2, 3]
# ID of listA and listB: 140613739648320 140613739648320

listB[0] = 99
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [99, 2, 3]
# ID of listA and listB: 140457913193792 140457913193600

listB.append([1,2,3])
print('listA,listB:',listA,listB)
print('ID of listA and listB:',id(listA),id(listB))

# listA,listB: [1, 2, 3] [99, 2, 3, [1, 2, 3]]
# ID of listA and listB: 140457913193792 140457913193600

listA = listB[:]

listB[3].append(999)

print('listA,listB:',listA,listB)
# ID of listA and listB: 140524217406784 140524217406592
# listA,listB: [99, 2, 3, [1, 2, 3, 999]] [99, 2, 3, [1, 2, 3, 999]]

보시면 [:]를 활용할 경우, listA와 listB의 주소가 다르기 때문에 Deep copy로 보이기도 합니다.

하지만 마지막을 보시면 [1,2,3] 객체에 999를 append하였을 때 두 리스트 모두 반영되는 것을 알 수 있습니다.

따라서, 완벽한 방식은 아니지요. 하지만 때에 따라 활용할 수는 있을 것 같습니다.


해결 방안

 

  • 자료형을 고려하여 shallow copy와 deep copy를 적절히 사용하는 것이 가장 좋은 방법이라고 생각합니다.

 

Reference

https://blockdmask.tistory.com/576

'파이썬 > 이론' 카테고리의 다른 글

[Python] Generator  (0) 2023.03.02
[Python] Python의 메모리 관리(Garbage Collector)  (0) 2023.02.14

파이썬 알고리즘 강의를 듣던 중 generator가 메모리 효율성에 큰 영향을 미친다는 얘기를 듣고 한번 정리해보고자 합니다.


 

generator란?

간단하게 말하자면, 데이터에 순차적으로 접근할 수 있는 객체인 iterator를 반환해주는 함수 입니다.

 

그렇다면 generator를 어떻게 활용할까요?

 


generator 활용법

제너레이터 활용법은 크게 두가지 입니다.

  • yield 와 next()의 활용
  • () 의 활용

 

1. yield와 next()를 활용한 코드

def func(l:list):
    for i in l:
        yield i +10


nums = func([1,2,3,4,5])

print(func(nums))   #<generator object func at 0x7fe75008f580>
print(next(nums))   #11
print(next(nums))   #12
print(next(nums))   #13
print(next(nums))   #14
print(next(nums))   #15

 

첫번째 print() 구문을 보시면 yield 구문을 활용하여 generator가 생성되신 것을 확인하실 수 있습니다.

 

def func(l:list):
    for i in l:
        yield i +10


nums = func([1,2,3,4,5])

print(func(nums))   #<generator object func at 0x7fe75008f580>

for i in nums:
    print(i)

 

위와 같이 반복문을 활용할 수도 있는데요. 결과는 위 코드와 같은 결과를 보입니다.

 

 

2. ()를 활용한 코드

nums = (i + 10 for i in range(1,6))

print(nums) # <generator object <genexpr> at 0x7fc340036190>

for i in nums:
    print(i)

이렇듯 구문을 ()로 감싸주게 되면 generator가 생성되는 것을 확인 하실 수 있습니다.

 

generator의 동작 방법은 알아보았습니다.

근데 이러한 동작 방법이 어떻게 메모리 효율성에 영향을 미치는 것일까요?

 

 


generator가 memory 효율성에 미치는 영향

 

Python 주로 데이터를 다룰 때 활용하는 언어입니다(데이터 수집, 인공지능 등). 데이터에서 유의미한 결과를 얻기 위해서는 정말 방대한 데이터를 활용할텐데요. 큰 양의 데이터를 한번에 담게 되면 메모리 부족 현상이 발생하여 프로그램이 갑자기 중단되는 등의 문제가 발생할 수 있습니다.

 

이때 generator는 개발자에게 엄청난 도움을 주게 됩니다.

generator는 모든 값의 순서를 기억한 상태로 동작하기 전까지 메모리를 할당하지 않기 때문입니다.

 

그렇기에 generator를 활용하게 되면 데이터를 한번에 적재하지 않아도 되기 때문에 메모리 소비를 줄일 수 있습니다. 즉, 한번에 적재하고 활용하는 방식 보다 훨씬 안정적인 방법이라고 할 수 있습니다.

 

또한 필요할 때 마다 호출하여 사용하는 지연 평가 방식이 가능하기 때문에 메모리를 효율적으로 사용할 수 있습니다.

'파이썬 > 이론' 카테고리의 다른 글

[Python] copy  (0) 2023.03.24
[Python] Python의 메모리 관리(Garbage Collector)  (0) 2023.02.14

[풀이 방법]

  • 시간을 다루는 문제이므로, 'HH:MM' 형태로 주어진 시간을 분 단위로 통일하여 활용한다.
  • 이전 사람의 종료 시간과 현재 사람의 시작시간을 비교하여, 종료시간 + 10분(청소 시간) 보다 시작시간이 뒤에 있을 경우 기존의 방을 배정하고 아닐 경우 새로운 방을 배정한다.

[풀이 코드]

import heapq


def solution(book_time):
    answer = 0
    heap = []
    bt = [(int(s[:2]) * 60 + int(s[3:]), int(e[:2]) * 60 + int(e[3:])) for s, e in book_time]
    bt.sort()

    for s, e in bt:
        if not heap:
            heapq.heappush(heap, e + 10)
            continue

        if heap[0] <= s:
            heapq.heappop(heap)
        else:
            answer += 1

        heapq.heappush(heap, e + 10)

    return answer + 1

https://leetcode.com/problems/watering-plants/

 

Watering Plants - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

 

문제: 들고 다닐 수 있는 물의 양과 각 식물에 필요한 물의 양이 주어질 때, 모든 식물에 물을 주기 위해 이동해야하는 거리를 구하는 문제.

 

문제 설명:

우물은 index가 -1에 해당하는 위치에 있다고 가정을 하며, 각 식물이 필요로 하는 물의 양은 1차원 배열로 주어지고 한번에 들고 다닐 수 있는 물의 양은 int형으로 주어집니다.

 

위 예시를 한번 보면, 총 4개의 식물에 물을 주어야하며 들고 다닐 수 있는 물의 용량은 5입니다.

index가 0에 해당하는 식물에 도달하여 물을 주면, 이동 거리는 1이 되며 남은 물의 용량은 3이 됩니다.

index가 1에 해당하는 식물에 도달하여 물을 주면, 이동거리는 2가 되며 남은 물의 용량은 1이 됩니다.

이때, 다음 식물은 3만큼의 물의 양이 필요한데 현재 가지고 있는 물의 양은 1이므로 우물에 다녀와야 합니다.

따라서, 2칸 뒤로 이동해 물을 채운 후 다음 식물에 해당하는 위치로 이동해 물을 주어야 합니다.

 

이 과정을 반복하여 모든 식물에 물을 주는데 필요한 거리를 구하는 문제 입니다.

 

풀이 코드:

class Solution:
    def wateringPlants(self, plants: List[int], capacity: int) -> int:
        now = -1
        steps = 0
        cap = capacity
        for i in range(len(plants)):
            steps += (i - now)
            cap -= plants[i]
            now = i
            if i != len(plants) - 1 and cap < plants[i + 1]:
                steps += (i + 1) * 2
                cap = capacity

        return steps

문제에서는 위의 예시에서 index = 1 인 위치에서 물을 채워오는 거리를 2칸 뒤로 간 후, 다음 위치인 index = 2인 위치로 3칸 앞으로 이동하는 계산 방식을 사용했습니다.

 

하지만 저는 우물에 들려 물을 채운 후 index = 2인 위치로 향할 때, index = 1인 위치에서 index = -1 로 왕복을 한 후 앞으로 1칸 전진하는 계산 방식을 사용했습니다.

https://leetcode.com/problems/defuse-the-bomb/

 

Defuse the Bomb - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

 

문제: 문제에서 주어진 조건에 따라, 배열 원소의 합을 반환하는 문제

 

문제 설명:

정수가 담긴 배열과 k라는 값이 주어집니다.

 

다음과 같이 총 3가지 조건에 따라 문제를 풀이하는 방법이 달라집니다. 

  • k > 0 일 경우, 배열의 i번째 값은 i번째 이후 k개의 원소의 합으로 대체됩니다.

예를 들어, [5, 7, 1, 4] 이라는 배열과 k = 3 이라는 값이 주어질 경우, 0번째는 7+1+4 (==12) 로 대체되고 1번째 값은 1+4+5(==10)으로 대체됩니다.

 

따라서 최종 반환되는 배열은 [12, 10, 16, 13] 이 됩니다.

 

  • k < 0 일 경우, 배열의 i번째 값은 i번째 이전 k개의 원소의 합으로 대체됩니다.

예를 들어, [2, 4, 9, 3] 이라는 배열과 k = -2 라는 값이 주어질 경우, 0번째는 3+9 (==12)로 대체 되고 1번째 값은 2+3 (==5)로 대체됩니다.

 

따라서 최종 반환되는 배열은 [12, 5, 6, 13] 이 됩니다.

 

  • 마지막으로 k = 0 일 경우, 배열의 모든 원소는 0이 되어 반환됩니다.

풀이 코드:

class Solution:
    def decrypt(self, code: List[int], k: int) -> List[int]:
        length = len(code)
        lst = []
        if k > 0:
            for i in range(length):
                s = 0
                for j in range(1, k + 1):
                    s += code[(i + j) % length]
                lst.append(s)

        elif k < 0:
            for i in range(length):
                s = 0
                for j in range(1, abs(k) + 1):
                    s += code[i + (-1 * j)]
                lst.append(s)
        else:
            return [0] * length
        return lst

https://leetcode.com/problems/unique-number-of-occurrences/

 

Unique Number of Occurrences - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

 

문제: 숫자로 이루어진 배열에서 각 원소의 갯수가 중복되는지 판별하는 문제

 

문제 설명:

 

문제에서 주어진 예시로 설명을 해보겠습니다.

 

예제 1번을 보면, [1, 2, 2, 1, 1, 3] 이라는 배열이 있습니다.

원소 1은 3개, 원소 2는 2개, 원소 3은 1개로 각 원소의 갯수가 중복되지 않으므로 True를 반환해줍니다.

 

예제 2번을 보면, 원소 1과 2가 둘다 1로 원소의 갯수가 중복이 되므로 False를 반환해줍니다.

 

저는 이 문제를 파이썬의 Counter 함수를 통해서 풀었는데, Counter 함수를 사용하게 되면 다음과 같은 결과가 나옵니다

 

from collections import Counter
arr= [1,2,2,1,1,3]
print(Counter(arr))

# 출력한 결과
# Counter({1: 3, 2: 2, 3: 1})

Counter를 쓰면 key-value 쌍으로 counter 형의 데이터가 만들어집니다.

 

이를 바탕으로, value 값들을 통해 중복된 값이 있는지 여부를 판별해주었습니다.

 

풀이 코드:

from collections import Counter
from collections import deque


class Solution:
    def uniqueOccurrences(self, arr: List[int]) -> bool:
        cntarr = Counter(arr)
        counterval = deque(cntarr.values())

        while counterval:
            v = counterval.pop()
            if v in counterval:
                return False
        return True

 

+ Recent posts