지난 세션(4/28)에서는 90도 회전 공식과 약수 함수 슬림화로 3문제 전부 1차 정답을 받았다. 8일 공백에도 arr[n-1-j][i] 공식을 정확히 떠올리며 정착 신호 확인. 함수 작성 4세션 연속 자력 정답으로 추세를 굳혔다.
시험까지 일주일 남은 시점에서 약 10일 만에 다시 책상 앞에 앉았다. 바로 90분 타이머 모의고사로 들어가는 건 무리라 판단해, 전 영역을 한 문제씩 가볍게 노출하는 워밍업 트랙을 짰다. 총 11문제. 자료형/조건/반복 → 리스트/문자열/딕셔너리/집합 → 정렬/함수/예외 → 스택/이진탐색/수학/구현 순서로, 지금까지 배운 모든 개념을 자연스럽게 한 번씩 점검한다. 컨디션 회복이 1차 목표, 90분 타이머 진입 가능 여부 진단이 2차 목표.
a부터 b까지(둘 다 포함) 정수 중 3의 배수의 합을 반환. 음수 입력도 처리해야 한다.
def sum_multiples_of_3(a, b):
total = 0
for i in range(a, b+1):
if i % 3 == 0:
total += i
return total
한 줄 컴프리헨션 형태도 익혀두자 — 시험 빈칸으로 자주 나오는 형태.
return sum(i for i in range(a, b+1) if i % 3 == 0)
수업 중 질문: 테스트케이스
(-5, 5)의 기대값이 왜3이지?-3 + 0 + 3이면0아닌가?학습자가 첫 문제부터 출제자 검산을 시작했다. 정확한 지적이고 내 계산 실수였다. 정정:
-3 + 0 + 3 = 0이 맞다. 참고로 Python에서-3 % 3 == 0,0 % 3 == 0둘 다 성립하므로n % 3 == 0조건 하나로 음수까지 잘 잡힌다. 검증 습관이 10일 공백에도 살아있다는 좋은 신호.
리스트를 뒤집은 후 k 이상인 값만 필터링.
def filter_reverse(nums, k):
return [i for i in nums if i >= k][::-1]
문제는 "뒤집은 후 필터"였는데 풀이는 "필터한 후 뒤집기"다. 결과는 같다 — 둘 다 상대 순서를 보존하는 연산이라 교환 법칙이 성립하기 때문. 그리고 필터로 먼저 줄인 뒤 뒤집는 게 작업량이 더 적어서 살짝 효율적이기까지 하다.
[i for i in nums if i >= k][::-1] # 효율적
[i for i in nums[::-1] if i >= k] # 문제 그대로
→ 메모해둘 패턴: 두 연산의 순서를 바꿔도 결과가 같은지 자문하는 습관. 시험에서 빈칸 답이 두 형태 모두 정답인 경우가 종종 있다.
대소문자와 공백을 무시하고 회문 판별.
def is_palindrome(s):
word = s.replace(" ", "").lower()
return word == word[::-1]
2단계 공식: (1) 정규화(공백 제거 + 소문자), (2) word == word[::-1] 비교 한 줄. 회문 패턴은 손에 박힌 상태.
사이드 노트: 만약 숫자나 구두점도 무시해야 하는 강한 회문 문제라면
re.sub(r'[^a-z0-9]', '', word)패턴이 필요하다. 오늘은 워밍업이라 안 갔지만 응용 문제에서 떠올릴 수 있게 익혀둔다.
가장 많이 등장한 글자와 횟수를 튜플로. 동률이면 먼저 나온 글자 우선.
from collections import Counter
def most_char(s):
return Counter(s).most_common(1)[0]
Counter는 dict 상속이라 삽입 순서를 보존 (Python 3.7+)most_common(n)은 내부적으로 heapq.nlargest를 쓰는데, 이건 동률일 때 입력 순서를 유지하는 안정 정렬→ most_common(1)[0]은 거의 공식처럼 외워도 된다.
수업 중 질문:
dict.get()방식으로도 풀어보다가 막혔다. 이 코드는 왜 에러나?tmp_dict = {} for c in s: tmp_dict[c] = tmp_dict.get(c, 0) + 1 return max(tmp_dict, key=lambda x: x[1]) # IndexError 발생원인은
max(tmp_dict, ...)가 딕셔너리에서 key만 꺼낸다는 것. 그래서lambda x: x[1]의x는 글자 한 개('a','p'등)가 되고, 한 글자짜리 문자열에x[1]을 하면 IndexError가 난다.수정:
.items()를 넘긴다.return max(tmp_dict.items(), key=lambda x: x[1])이렇게 하면
x가('a', 1)같은 튜플이 되고x[1]이 횟수가 된다. 그리고 동률 처리는Counter버전과 동일 —dict.items()가 삽입 순서로 순회하고max는 먼저 본 max를 반환하므로 "먼저 등장한 글자"가 자동으로 우선된다.
| 표현 | 순회 시 꺼내는 것 |
|---|---|
for x in dict | key만 |
for x in dict.values() | value만 |
for x in dict.items() | (key, value) 튜플 |
max / min / sorted / for 모두 같은 규칙. 한 번 의식해두면 비슷한 에러 다시 안 난다.
두 문자열에 모두 등장하는 글자를 알파벳순으로.
def common_chars(a, b):
return sorted(set(a) & set(b))
| 기호 | 의미 | 예시 (A={1,2,3}, B={2,3,4}) |
|---|---|---|
A & B | 교집합 (intersection) | {2, 3} |
A \| B | 합집합 (union) | {1, 2, 3, 4} |
A - B | 차집합 (difference) | {1} |
A ^ B | 대칭차 (symmetric difference) | {1, 4} |
처음 작성한 풀이는 sorted(list(set(a) & set(b))) 였다. list() 호출이 불필요하다. sorted()는 어떤 iterable이든 받아서 항상 list를 반환하기 때문.
이게 4/27에 새로 잡힌 약점인 "불필요 변환/분기 추가" 패턴이다. 자연 노출에서 1회 발생. 의식하면 다음번엔 안 나올 가능성 높음.
함수 작성 후 자기 점검 한 마디:
"이 변환/분기 정말 필요한가? 자료구조나 호출 결과를 그대로 넘기면 사라지지 않나?"
(이름, 점수) 튜플 리스트를 점수 내림차순, 동점이면 이름 오름차순으로 정렬.
def sort_students(students):
return sorted(students, key=lambda x: (-x[1], x[0]))
key=lambda x: (-x[1], x[0])
┌─────┐ ┌────┐
점수 이름
내림 오름
-x[1]로 먼저 비교 → 음수로 만들어서 큰 점수가 앞에x[0](이름)으로 오름차순 비교→ 음수 트릭으로 내림차순 + 두 번째 키로 동률 처리. COS Pro 빈출 1순위 패턴.
nums[idx]를 안전하게 반환. 범위를 벗어나면 default 반환.
def safe_get(nums, idx, default=None):
try:
return nums[idx]
except IndexError:
return default
수업 중 질문: try/except 구문이 바로 안 떠올라서 옛날 자료를 찾아봤다. 나도 모르게 try-catch로 생각하고 있더라.
Java 출신이라 자연스러운 슬립이고, 7차 학습 이후 한 달 반 만의 자연 노출이라 더 그렇다. 로직은 완전히 손에 있는데 문법 키워드만 슬쩍 미끄러진 상태. 다시 굳히는 차원에서 매핑 정리.
try:
risky()
except IndexError as e: # catch → except, (X) → as
handle(e)
except (KeyError, ValueError): # 여러 개 잡기 (튜플)
handle()
else: # try가 예외 없이 끝났을 때만 (Java엔 없음)
print("성공")
finally:
cleanup()
| Java | Python | 메모 |
|---|---|---|
try | try | 동일 |
catch (Exception e) | except Exception as e | 키워드/문법 다름 |
finally | finally | 동일 |
| ❌ 없음 | else | try가 성공했을 때만 실행 (보너스 절) |
시험 함정 포인트:
return이 try에 있어도 finally는 반드시 실행된다else는 try가 예외 없이 끝났을 때만, except보다 먼저 위치nums[-1]은 마지막 원소를 정상 반환하고, 범위를 벗어난 음수(nums[-10] for len=3)에서만 IndexError를 던진다. C/Java라면 음수 인덱스 자체에서 별도 분기를 짜야 했을 텐데 Python은 한 줄에 처리됨.
( ) [ ] { } 6가지 괄호의 짝과 순서를 모두 검사.
def is_balanced(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for c in s:
if c in pairs.values():
stack.append(c)
elif c in pairs:
if not stack or stack[-1] != pairs[c]:
return False
stack.pop()
return not stack
19차에서 학습한 두 가지가 다시 자연 재노출에서 안정적으로 나왔다.
not stack 단락평가로 stack[-1] 호출을 보호3가지 False 조건도 모두 처리:
1. 빈 스택에서 닫는 괄호 등장 (not stack)
2. 짝이 안 맞음 (stack[-1] != pairs[c])
3. 끝났는데 스택에 남은 여는 괄호 (return not stack)
c in pairs.values()는 매번 values 순회라 O(n)이고, c in pairs는 dict 해시라 O(1). 문자열 상수로 꺼내두면 더 빠르고 시험 답안 같은 느낌:
opens = "([{"
pairs = {")": "(", "]": "[", "}": "{"}
for c in s:
if c in opens:
stack.append(c)
elif c in pairs:
if not stack or stack[-1] != pairs[c]:
return False
stack.pop()
정렬된 리스트에서 target 인덱스 반환. 없으면 -1.
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
while left <= right (등호 포함, 원소 1개 케이스 처리)(left + right) // 2 (정수 나눗셈, /면 float이 되어 인덱싱 에러)mid + 1 / mid - 1 (무한루프 방지)right = -1이라 루프 안 들어감)/ vs // 디버깅 함정은 22차에서 직접 잡아본 문제. 그 경험이 자산이 됐다.
def is_prime(n):
if n <= 1:
return False
i = 2
while i * i <= n:
if n % i == 0:
return False
i += 1
return True
수업 중 질문:
range(2, int(n**0.5) + 1)이 형태는 뭐지?while 형태와 완전히 같은 범위를 도는 for 형태다. 메커니즘만 다르다.
# 정수 연산만 사용 (안전)
i = 2
while i * i <= n:
...
i += 1
# 동등한 for 형태 (sqrt 직접 계산)
for i in range(2, int(n**0.5) + 1):
...
조각조각 뜯어보면 (n=25 기준):
| 표현 | 값 | 의미 |
|---|---|---|
n ** 0.5 | 5.0 | √n 계산 (float 반환) |
int(...) | 5 | 소수점 버림 (양수에선 floor) |
+ 1 | 6 | range 끝값은 미포함이라 +1 |
range(2, 6) | 2, 3, 4, 5 | i가 2~5까지 순회 |
| 형태 | 장점 | 단점 |
|---|---|---|
while i*i <= n | 정수 연산만 사용 (안전) | for문 한 줄로 못 끝냄 |
range(2, int(n**0.5)+1) | for문 한 줄로 깔끔 | float 거치므로 아주 큰 수에서 정밀도 함정 가능 |
→ COS Pro 입력 크기에서는 둘 다 정답. 빈칸으로 어느 쪽이 나와도 헷갈리지 않게 둘 다 알아둔다.
N×N 행렬을 시계방향으로 90도 돌린 새 리스트를 반환.
def rotate(arr):
n = len(arr)
rotated = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
rotated[j][n - 1 - i] = arr[i][j]
return rotated
rotated[j][n-1-i] = arr[i][j] # 원본 → 회전 매핑 (이번 풀이)
rotated[i][j] = arr[n-1-j][i] # 회전 → 원본 매핑 (4/28 학습)
루프를 도는 관점이 다를 뿐, 결과는 동일.
수업 중 질문: 이건 시계방향이지? 만약 반대로(반시계) 돌린다면?
| 방향 | 원본 → 회전 매핑 | 회전 → 원본 매핑 | zip 한 줄형 |
|---|---|---|---|
| CW (시계) | rotated[j][n-1-i] = arr[i][j] | rotated[i][j] = arr[n-1-j][i] | zip(*arr[::-1]) |
| CCW (반시계) | rotated[n-1-j][i] = arr[i][j] | rotated[i][j] = arr[j][n-1-i] | list(zip(*arr))[::-1] |
원본 CW (시계) CCW (반시계)
1 2 3 7 4 1 3 6 9
4 5 6 → 8 5 2 2 5 8
7 8 9 9 6 3 1 4 7
arr[n-1-j][i]arr[j][n-1-i]rotated[i][j] = arr[n-1-i][n-1-j] (양쪽 다 뒤집힘)rotated[i][j] = arr[j][i] 또는 zip(*arr)CW 두 번 = 180도 = CCW 두 번. 회전 패밀리는 이 4가지가 전부다.
11문제 1차 정답률 11/11 (100%) — 약 10일 공백 후 컨디션 회복용 워밍업 완료. 자연 노출 약점 1건(불필요한 list() 변환)만 발생, 의식 후 다음 노출 정착 판정 예정. 90분 타이머 모의고사 진입 가능 신호 확인 — 다음 세션은 구름EDU 기출 4회차 풀타임 풀이.