지난 세션(4/27)에서는 수학 파트(소수 판별, 약수, GCD, LCM)를 한 번에 정리하며 4문제 전부 1차 정답으로 끝냈다. 다만 함수 작성에서 같은 값을 가진 변수 2개, 불필요한 엣지 분기 같은 비-슬림한 패턴이 나와서 "코드 슬림화 체크리스트 3가지"를 신규 약점으로 정리했다. 함수 작성 자력 정답은 21차 → 22차 → 23차로 3세션 연속 진행 중.
오늘은 가벼운 보강 세션. 두 가지 트랙으로 갔다. 먼저 4/20에 학습한 90도 회전 공식이 이번엔 8일 공백이라 망각 점검 — 인덱스 직접 / zip 패턴 두 형태로 빈칸과 디버깅 한 문제씩. 그다음 어제 신규 약점인 슬림화 의식을 보강하기 위해 약수 함수 작성 후 직접 작성한 풀이를 슬림화 체크리스트로 자기 점검.
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] = 7 | arr[2][0] | arr[n-1-j][i] = arr[2][0] ✓ |
| rotated[0][1] = 4 | arr[1][0] | arr[n-1-j][i] = arr[1][0] ✓ |
| rotated[0][2] = 1 | arr[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 자리 바뀌는 거 항상 주의.
같은 회전을 Python 답게 한 줄로 쓰는 방법도 있다. 이게 시험 디버깅에 자주 등장.
[list(row) for row in zip(*arr[::-1])]
이 한 줄을 두 단계로 분해해서 이해하면 외우기 쉬워진다.
arr[::-1] — 행 순서 뒤집기원본: 뒤집기 후:
[1, 2, 3] [7, 8, 9]
[4, 5, 6] → [4, 5, 6]
[7, 8, 9] [1, 2, 3]
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에 학습한 "뒤집기 + 전치" 분해가 도구로 자리잡은 형태.
어제(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 분리 + reverse | 1줄 (제곱수 체크) | 가장 김 | O(√n + d) |
| 한 리스트 + sorted | 1줄 (제곱수 체크) | 중간 | O(√n + d log d) |
| set + sorted | 0줄 | 가장 짧음 | O(√n + d log d) |
세 가지 모두 정답이고 trade-off가 있다. 다만 어제 학습한 슬림화 체크리스트에 항목 하나가 추가된다.
💡 슬림화 자기 점검 — 추가 메타 질문
4. 이 분기 정말 필요한가? 자료구조 바꾸면 사라지지 않나?"리스트 + 중복 체크 분기" 자리에 "set으로 자동 중복 제거"가 거의 항상 한 번 더 짧게 만들어준다. 약수 문제뿐 아니라 경계 순회(20차 좌표 visited), 부분합 중복 등에도 동일.
| 측면 | Java | Python |
|---|---|---|
| HashSet 사용 | Set<Integer> set = new HashSet<>(); | result = set() |
| 선언 무게 | 타입 명시 + 제네릭 | 한 단어 |
| 변환 비용 | new ArrayList<>(set) + sort | sorted(result) 한 번에 |
Java에서는 자료구조 바꾸는 게 선언/변환 비용 때문에 약간의 결심이 필요한 행동인데, Python은 그 비용이 거의 없다. 그래서 Python에선 "분기를 자료구조로 흡수"하는 게 더 자연스러운 선택지가 된다.
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 시작 — 그래프 탐색 본격 진입.