20260428 오늘의 학습: 2차원 회전 복습 + 약수 함수 슬림화

Yesol Lee·2026년 4월 28일

COS Python

목록 보기
25/30

지난 학습 요약

지난 세션(4/27)에서는 수학 파트(소수 판별, 약수, GCD, LCM)를 한 번에 정리하며 4문제 전부 1차 정답으로 끝냈다. 다만 함수 작성에서 같은 값을 가진 변수 2개, 불필요한 엣지 분기 같은 비-슬림한 패턴이 나와서 "코드 슬림화 체크리스트 3가지"를 신규 약점으로 정리했다. 함수 작성 자력 정답은 21차 → 22차 → 23차로 3세션 연속 진행 중.

오늘 수업 계획

오늘은 가벼운 보강 세션. 두 가지 트랙으로 갔다. 먼저 4/20에 학습한 90도 회전 공식이 이번엔 8일 공백이라 망각 점검 — 인덱스 직접 / zip 패턴 두 형태로 빈칸과 디버깅 한 문제씩. 그다음 어제 신규 약점인 슬림화 의식을 보강하기 위해 약수 함수 작성 후 직접 작성한 풀이를 슬림화 체크리스트로 자기 점검.


학습 내용 정리

1. 90도 시계방향 회전 — 인덱스 직접 매핑

n × n 2차원 배열을 시계 방향으로 90도 돌리는 가장 직관적인 풀이. 핵심 공식 한 줄을 외우면 끝난다.

rotated[i][j] = arr[n-1-j][i]

왜 이렇게 매핑되는가?

회전 후 i행은 원본 i열을 아래에서 위로 읽은 값으로 채워진다. 3×3 예시로 추적해보면 명확해진다.

원본:

[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

시계 방향 90도 회전 후 (기대):

[7, 4, 1]
[8, 5, 2]
[9, 6, 3]

회전 후 0행 [7, 4, 1]은 원본의 0열 [1, 4, 7]을 거꾸로 읽은 값이다. 그래서:

위치 (i, j)값매핑
rotated[0][0] = 7arr[2][0]arr[n-1-j][i] = arr[2][0] ✓
rotated[0][1] = 4arr[1][0]arr[n-1-j][i] = arr[1][0] ✓
rotated[0][2] = 1arr[0][0]arr[n-1-j][i] = arr[0][0] ✓

→ 행 인덱스에는 n-1-j(거꾸로), 열 인덱스에는 i(원본 열) 가 들어간다.

빈칸 풀이

def rotate_90(arr):
    n = len(arr)
    rotated = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            rotated[i][j] = arr[n - 1 - j][i]   # ← 핵심
    return rotated

⚠️ 함정 후보: arr[n-1-i][j]로 쓰면 반시계 방향이 된다. 시험 디버깅 문제로 충분히 등장 가능. i와 j 자리 바뀌는 거 항상 주의.


2. 90도 회전 — zip 패턴 (뒤집기 + 전치)

같은 회전을 Python 답게 한 줄로 쓰는 방법도 있다. 이게 시험 디버깅에 자주 등장.

[list(row) for row in zip(*arr[::-1])]

이 한 줄을 두 단계로 분해해서 이해하면 외우기 쉬워진다.

단계 1: arr[::-1] — 행 순서 뒤집기

원본:           뒤집기 후:
[1, 2, 3]      [7, 8, 9]
[4, 5, 6]  →   [4, 5, 6]
[7, 8, 9]      [1, 2, 3]

단계 2: zip(*뒤집힌_배열) — 전치(transpose)

zip은 여러 시퀀스에서 i번째 원소를 묶어주는 함수. * 언패킹으로 각 행을 인자로 펼쳐 넣으면 열 단위로 묶이는 효과가 난다.

zip([7,8,9], [4,5,6], [1,2,3])
→ (7,4,1), (8,5,2), (9,6,3)

list로 감싸면 회전 결과 완성. 인덱스를 직접 쓰지 않고 끝낸다.

디버깅 문제 — 어디가 버그?

def rotate_90(arr):
    flipped = arr[::]                              # ⓐ
    return [list(row) for row in zip(*flipped)]    # ⓑ

ⓐ에 버그가 있다. arr[::]은 단순 복사(슬라이싱으로 새 리스트 만들기)일 뿐, 순서를 뒤집지 않는다. 그래서 zip만 호출되면 그냥 전치(transpose)가 되어버려 회전이 아닌 전혀 다른 결과가 나온다.

한 줄 수정:

flipped = arr[::-1]

디버깅 풀이 흐름 (직접 설명)

"zip 부분이 마지막 행에서부터 하나씩 뽑아야 되니 마지막 행이 처음 오도록 뒤집어야 함"

zip의 동작이 첫 행부터 차례로 열 단위로 묶는 거라는 점을 이해하면, 시계 방향 회전 결과의 0행이 "원본 마지막 행의 0번째 값들"이 되려면 마지막 행이 맨 앞에 와야 한다는 게 자연스럽게 따라온다. 4/20에 학습한 "뒤집기 + 전치" 분해가 도구로 자리잡은 형태.


3. 약수 구하기 — 직접 작성 + 슬림화 비교

어제(4/27)에 √n 최적화 패턴으로 약수 짝꿍 구조를 다뤘다면, 오늘은 그걸 함수 작성으로 응용하면서 슬림화 의식을 점검.

문제

자연수 n의 모든 약수를 오름차순 리스트로 반환. 시간 복잡도 O(√n).

divisors(12)  # [1, 2, 3, 4, 6, 12]
divisors(36)  # [1, 2, 3, 4, 6, 9, 12, 18, 36]   ← 제곱수
divisors(7)   # [1, 7]                            ← 소수
divisors(1)   # [1]                               ← 엣지

직접 작성한 풀이

def divisors(n):
    small = []
    large = []
    i = 1
    while i * i <= n:
        if n % i == 0:                  # 나누어떨어짐(약수)
            small.append(i)
            if n // i != i:              # i로 n 나눈 수가 i 아니면 두 번째도 추가
                large.append(n // i)
        i += 1
    return small + large[::-1]           # large는 큰 수부터 들어가있으니 역순

1차 정답 — 5개 테스트 전부 통과.

이 풀이의 좋은 점:

  • 제곱수 함정(if n // i != i)을 정확히 처리
  • n == 1 같은 안전망 엣지 분기를 추가하지 않음 (메인 while 루프가 자연 처리)
  • 같은 값 변수 중복 없음

→ 어제(23차) 신규 약점이던 "안전망 분기 추가" 패턴이 1회 노출에서 발생하지 않음. 어제 학습한 슬림화 체크리스트 3가지가 자기 점검으로 작동한 신호.

슬림화 체크 — 한 발 더 짧게 가는 길

학습자 풀이는 시간 복잡도 측면에서 살짝 우위다 (reverse는 O(d), sorted는 O(d log d)). 의식적인 성능 선택이라면 그대로 둬도 좋다. 다만 코드 길이/가독성 관점에서 보면 더 짧은 형태가 있다.

대안 1: 한 리스트 + sorted

def divisors(n):
    result = []
    i = 1
    while i * i <= n:
        if n % i == 0:
            result.append(i)
            if i != n // i:
                result.append(n // i)
        i += 1
    return sorted(result)

리스트 분리 없이 한곳에 다 넣고 마지막에 정렬. 코드 짧아짐.

대안 2: set + sorted (가장 슬림)

def divisors(n):
    result = set()
    i = 1
    while i * i <= n:
        if n % i == 0:
            result.add(i)
            result.add(n // i)
        i += 1
    return sorted(result)

여기선 제곱수 분기(if i != n // i) 자체가 사라진다. set이 중복을 자동으로 제거해주니까. 메인 자료구조를 바꾸는 것만으로 분기 한 줄이 통째로 없어지는 패턴.

메타 포인트 — 자료구조 선택을 통한 슬림화

풀이분기 줄코드 길이시간복잡도
small/large 분리 + reverse1줄 (제곱수 체크)가장 김O(√n + d)
한 리스트 + sorted1줄 (제곱수 체크)중간O(√n + d log d)
set + sorted0줄가장 짧음O(√n + d log d)

세 가지 모두 정답이고 trade-off가 있다. 다만 어제 학습한 슬림화 체크리스트에 항목 하나가 추가된다.

💡 슬림화 자기 점검 — 추가 메타 질문
4. 이 분기 정말 필요한가? 자료구조 바꾸면 사라지지 않나?

"리스트 + 중복 체크 분기" 자리에 "set으로 자동 중복 제거"가 거의 항상 한 번 더 짧게 만들어준다. 약수 문제뿐 아니라 경계 순회(20차 좌표 visited), 부분합 중복 등에도 동일.

Java vs Python — 자료구조 선택의 무게

측면JavaPython
HashSet 사용Set<Integer> set = new HashSet<>();result = set()
선언 무게타입 명시 + 제네릭한 단어
변환 비용new ArrayList<>(set) + sortsorted(result) 한 번에

Java에서는 자료구조 바꾸는 게 선언/변환 비용 때문에 약간의 결심이 필요한 행동인데, Python은 그 비용이 거의 없다. 그래서 Python에선 "분기를 자료구조로 흡수"하는 게 더 자연스러운 선택지가 된다.


4. 공식 재노출 주기 관리 — 8일 공백 결과

4/20(20차)에서 90도 회전 공식을 5일 공백으로 망각했던 적이 있어, 그 후 재노출 주기를 의식하고 있었다. 오늘은 4/20 이후 8일 공백 상태에서 첫 재노출.

항목결과
인덱스 형태 (arr[n-1-j][i])정확히 떠올림 ✓
zip 패턴 (arr[::-1])디버깅 정답 + 자체 설명 ✓

8일 공백에도 두 형태 모두 안정 → 4/20 망각 사례 이후 첫 재노출에서 정착 신호. 다음 자연 재노출은 5/12 전후 오답 총복습 기간으로 예정.

📌 공백 주기 관리 메타 — "공식은 망각 사이클을 의식하고 다시 만지기"라는 학습 운영이 한 번 검증됨. 새 공식을 외울 때마다 다음 재노출 시점을 머릿속에 한 번 박아두는 게 효과적.


오늘의 결과

오늘 푼 문제는 3문제(빈칸 1 + 디버깅 1 + 함수 작성 1), 1차 정답률 3/3 (100%). 함수 작성 자력 정답이 21차 → 22차 → 23차 → 24차로 4세션 연속, 어제 신규 약점이던 슬림화 의식도 1회 노출에서 안전망 분기 추가 없이 통과. 다음 학습은 4/29(수)부터 BFS/DFS 시작 — 그래프 탐색 본격 진입.

profile
문서화를 좋아하는 개발자

0개의 댓글