0%
프로그래밍 · 기말 · Complexity · Data Structures · Recursion

프로그래밍 기말 — 교과서형 정리 + 손코딩

복잡도·자료구조·메모리·재귀를 한 권으로 — 마지막은 시험 단골 손코딩 10제

Table of Contents · 차례

  1. §1 · 시험 구조와 읽는 법
  2. Ⅰ. 복잡도 (Complexity)
  3. §2 · Time Complexity & Big-O
  4. §3 · Space Complexity & Time-Space Tradeoff
  5. §4 · Searching & Sorting
  6. Ⅱ. 자료구조 (Data Structures)
  7. §5 · List · Stack · Queue · Dict · Set
  8. Ⅲ. 메모리와 재귀 (Memory & Recursion)
  9. §6 · Memory Model — Stack vs Heap
  10. §7 · Recursion
  11. §8 · Memoization & DP 입문
  12. Ⅳ. 손코딩 & 마무리
  13. §9 · 손코딩 훈련 뱅크 (현실 사례 15제) ⭐
  14. §10 · 시험 직전 핵심 요약 · A4 치트시트

§1 Exam Structure & How to Read — 시험 구조와 읽는 법

이 노트는 프로그래밍 기말 시험범위(4개 강의 — 복잡도 ①②, 자료구조, 메모리·재귀·메모이제이션)를 처음부터 끝까지 교과서처럼 읽도록 구성했습니다. 큰 흐름은 셋입니다 — 복잡도(코드가 얼마나 잘 확장되는가), 자료구조(데이터를 어떻게 담아야 빠른가), 메모리·재귀(함수 호출이 실제로 어떻게 동작하는가). 마지막 §9는 사용자가 가장 중요하다고 한 손코딩입니다.

시험 구조

📅 6/11(목) 9:00–11:00, E7-233 · 범위: ~6/02 강의 전체 + P-set #7 · Office Hour 6/09.

⚠️ T/F 감점 전략 — 맞으면 +점, 틀리면 −1점, 빈칸 0점 구조이므로 찍기는 기댓값상 불리하다(반반이면 0점, 모르면 손해). "항상/절대(always/never)", "모든(every)", "~할 수 없다(cannot)" 같은 극단 단어가 들어간 진술은 대개 거짓이다(예: "Quick Sort는 항상 O(n log n)" → 거짓, worst는 O(n²)). 본문의 함정 표시와 §10 비교표를 이런 진술 판별에 그대로 쓰세요.

1.1 한눈에 보는 복잡도 위계 — 이것만은 반사적으로

절대 암기

증가 속도 순서(좋음→나쁨): O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ). n=1,000일 때 대략 1 · 10 · 1,000 · 10,000 · 1,000,000 · ∞. 이 한 줄이 객관식·T/F의 절반을 커버한다.

§2 Time Complexity & Big-O — 시간 복잡도

The Key Idea — 왜 필요한가

코드가 빠른지 느린지 재는 방법은 둘이다. ① wall-clock time(스톱워치로 실제 시간 측정) — 컴퓨터·다른 프로그램·입력에 따라 들쭉날쭉. ② counting operations(입력이 커질 때 연산 수가 어떻게 느는지 셈) — 기계 독립적이고 수학적. 우리는 ②를 쓰고, 그 추세를 Big-O로 표기한다. 핵심은 정확한 횟수가 아니라 추세(trend), 즉 입력 n이 커질 때의 상한(upper bound)이다.

operations ↑ input size n → O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ)
복잡도별 증가 곡선 — 같은 입력 n에서 위로 갈수록 폭발적으로 느려진다. O(2ⁿ)은 n이 조금만 커져도 거의 수직으로 치솟고, O(log n)은 거의 눕는다. 이 순서 감각이 객관식·T/F의 절반이다. (개념: 강의 0428 Comparison of Growth Rates)
예제 — 연산 세기
# 연산 수는?
def sum_list(numbers):
    total = 0            # 1 operation
    for num in numbers:  # n operations
        total += num     # n operations
    return total         # 1 operation
# 합 = 2n + 2 → 추세는 n에 비례 → O(n)

n=10이면 22번, n=1,000이면 2,002번 → n에 선형으로 증가. 상수 2와 +2는 버린다.

2.1 Big-O 단순화 3규칙 — simplification

규칙

상수를 버린다: O(3n)→O(n), O(n²/2)→O(n²), O(100)→O(1). ② 낮은 차수 항을 버린다: O(n²+n)→O(n²), O(n+log n)→O(n), O(2ⁿ+n³)→O(2ⁿ). ③ n이 아주 커지면 가장 큰 항(dominant term)만 의미가 있다. ("O"는 "Order of"의 약자 — 증가의 상한.)

Big-OBig-O
3n + 5O(n)n² + n log nO(n²)
n²/2 + 100nO(n²)2ⁿ + n³O(2ⁿ)
100O(1)log₂n + log₁₀nO(log n) (밑 무시)
2n³ + 100n² + 5n + 10O(n³)n + 1000O(n)

2.2 복잡도별 대표 코드 패턴 — 코드만 보고 즉답

패턴복잡도비유 / 예
lst[i], dict[k], len(x), n%2O(1)주소로 바로 접근 — "성배"
for loop 1개 (n번)O(n)섞인 카드에서 한 장 찾기
중첩 for loop (n×n)O(n²)모든 쌍 비교 (30명→900번)
매번 절반씩 줄임O(log n)전화번호부 펼치기(binary search)
분할정복 + 합치기O(n log n)merge sort, quick sort(평균)
매번 2번 재귀 호출O(2ⁿ)naive Fibonacci
식별 팁: 단일 loop=n, 중첩=n², 절반씩=log n, 모든 부분집합/2갈래 재귀=2ⁿ. O(n²)의 전형은 "모든 쌍 검사"다 — 아래 코드의 n(n−1)/2 ≈ n²/2 → O(n²).
def has_duplicate(lst):          # O(n²)
    for i in range(len(lst)):
        for j in range(i+1, len(lst)):
            if lst[i] == lst[j]:
                return True
    return False
# set을 쓰면 O(n)으로 개선 가능 → §3, §5에서 다시
✏️ Quick Check§2 시간복잡도

다음 두 코드의 시간복잡도는?
(a) for i in range(n): for j in range(n): print(i,j)
(b) while n > 1: n = n // 2

풀이 보기

(a) 중첩 loop, 각 n번 → O(n²). (b) 매번 절반으로 줄임 → O(log n).

§3 Space Complexity & Time-Space Tradeoff — 공간 복잡도

The Key Idea

시간이 "얼마나 오래?"라면 공간은 "메모리를 얼마나 쓰나?"이다. 비유: 요리에서 시간=조리 시간, 공간=필요한 냄비·팬 수. 핵심 규칙: 입력 자체는 세지 않는다 — 알고리즘이 추가로 할당하는(extra) 메모리만 센다.

할당하는 것공간
단일 변수 (int/float/bool)O(1)
길이 n 새 list/arrayO(n)
n×n 2D matrixO(n²)
재귀 (깊이 n)O(n) ← 자주 출제

3.1 O(1) vs O(n) 공간 — 같은 시간, 다른 공간 — in-place의 힘

리스트 뒤집기를 두 가지로. 둘 다 시간은 O(n)이지만 공간이 다르다.

# O(n) space — 새 list 생성          # O(1) space — 제자리 교환(in-place)
def rev1(nums):                       def rev2(nums):
    result = []                            l, r = 0, len(nums)-1
    for i in range(len(nums)-1,-1,-1):       while l < r:
        result.append(nums[i])                 nums[l], nums[r] = nums[r], nums[l]
    return result                            l += 1; r -= 1
왼쪽은 길이 n 새 리스트를 만들어 O(n) 공간, 오른쪽은 포인터 둘만 쓰고 원본을 바꿔 O(1) 공간. "둘 다 O(n) 시간이지만 공간이 다르다"가 단골 진술이다.

3.2 Time-Space Tradeoff — 메모리를 써서 시간을 산다

정의

"이 단어가 전에 나왔나?"를 풀 때 — ⓐ 추가 메모리 없이 매번 전체를 훑으면 Time O(n²), Space O(1). ⓑ set에 본 단어를 기억하면 Time O(n), Space O(n). ⓑ가 훨씬 빠르고 메모리를 더 쓴다. 실무 원칙: 메모리는 대체로 싸다 → 속도를 택하라.

✏️ Quick Check§3 공간복잡도

def reverse(nums): return nums[::-1] 의 Time / Space는?

풀이 보기

슬라이싱은 새 리스트를 만든다 → Time O(n), Space O(n). (제자리가 아님에 주의.)

§4 Searching & Sorting — 탐색과 정렬

4.1 Linear vs Binary Search — 선형 vs 이진 탐색

Linear SearchBinary Search
시간O(n)O(log n)
조건아무 list나 OK정렬된 list 필수
방식하나씩 전부 확인매번 절반을 버림
n=10⁶ 최악최대 1,000,000번약 20번 (log₂10⁶≈20)
⚠️ 이진 탐색은 정렬을 전제한다. "정렬 안 된 리스트에 binary search" 진술은 거짓. 코드는 §9 손코딩 1번 참고.

4.2 정렬 4종 비교 — 반드시 외울 표

정렬BestAverageWorstSpace특징
Bubble SortO(n)O(n²)O(n²)O(1)인접 비교·swap, "큰 값이 위로 떠오름"
Selection SortO(n²)O(n²)O(n²)O(1)최솟값 찾아 앞으로, 정렬돼 있어도 O(n²)
Merge SortO(n log n)O(n log n)O(n log n)O(n)분할정복, 항상 일정, merge에 추가메모리
Quick SortO(n log n)O(n log n)O(n²)O(log n)pivot 분할, in-place, 실무 최速
함정 모음

Bubble best O(n)(이미 정렬 시 한 번 훑고 끝) vs Selection 항상 O(n²)(정렬돼 있어도 최솟값 탐색은 함). ② Quick worst O(n²)(나쁜 pivot, 예: 이미 정렬된 입력에 첫 원소 pivot). ③ 공간: Merge O(n)(병합 버퍼) vs Quick O(log n)(재귀 깊이, in-place). Merge는 "항상 O(n log n)이지만 메모리를 더 쓴다".

예제 — 추적
# Merge Sort: [5,3,8,1]          # Selection Sort: [5,3,8,1]
[5,3,8,1] → [5,3] [8,1]              min=1 → [1,3,8,5]
[5][3] [8][1]   (base)               min=3 → [1,3,8,5]
[3,5] [1,8]     (merge)              min=5 → [1,3,5,8] ✓
[1,3,5,8]       (merge) ✓
✏️ Quick Check§4 정렬

(a) Quick Sort의 worst-case 시간은? (b) 이미 정렬된 배열에 Selection Sort를 쓰면?

풀이 보기

(a) O(n²) — pivot이 매번 최소/최대로 뽑힐 때. (b) 여전히 O(n²) — Selection은 입력 상태와 무관. (Bubble이라면 best O(n).)

§5 Data Structures — List · Stack · Queue · Dict · Set

The Key Idea

자료구조는 데이터를 조직·저장하는 방식이다. 구조마다 잘하는 연산과 못하는 연산이 다르다. 옷장 비유: 바닥에 쌓기(추가 쉽지만 찾기 어려움) vs 색깔별 정렬(찾기 빠름) vs 라벨 서랍(즉시 접근). 가장 자주 하는 연산을 가장 빠르게 만드는 구조를 고르는 것이 핵심 — 잘못 고르면 O(n) 해법이 O(n²)가 된다.

5.1 List (Array) — 순서·위치 접근

index 0부터 시작, 연속 메모리(contiguous memory)에 저장. 그래서 위치 접근이 빠르다.

연산복잡도이유
lst[i] (index 접근)O(1)주소 직접 계산
x in lst (검색)O(n)전수 확인
lst.append(x)O(1)뒤에 추가
lst.insert(0, x)O(n)⚠️ 뒤 원소 전부 shift!
lst.pop()O(1)뒤에서 제거
lst.pop(0)O(n)⚠️ 전부 shift!

5.2 Stack — LIFO — Last In, First Out

접시 더미 비유. 맨 위에서만 넣고 뺀다 — 중간 접근 불가. 세 연산 모두 O(1): push(x)=append, pop()=top 제거+반환, peek()=stack[-1](제거 없이 확인).

# 문자열 뒤집기 — stack의 대표 용도
stack = []
for ch in "HELLO":
    stack.append(ch)
result = ""
while stack:
    result += stack.pop()
# "OLLEH"
⚠️ 빈 stack에서 pop()IndexError. 항상 if stack:로 확인. 실생활: Undo/Redo(Ctrl+Z), 브라우저 뒤로가기, 함수 호출 스택(§6).

5.3 Queue — FIFO — First In, First Out

카페 줄 비유. 뒤로 들어와 앞으로 나간다. enqueue=append, dequeue=popleft, peek=q[0] — 모두 O(1).

from collections import deque
q = deque()
q.append("A"); q.append("B")   # enqueue
front = q.popleft()              # dequeue → "A", O(1)
⭐ 빈출 — 왜 list 말고 deque?

list.pop(0)O(n)(앞에서 빼면 전부 shift) vs deque.popleft()O(1). 큐는 반드시 deque를 써라. 실생활: 프린트 큐, OS 작업 스케줄링, 메시지 큐.

5.4 Dictionary & Set — 해싱으로 O(1)

둘 다 hash function으로 메모리 위치를 직접 계산 → 평균 O(1). Dict는 key→value, Set은 unique values.

Dictionary (Hash Map)Set
Lookup / Insert / DeleteO(1) 평균O(1) 평균
저장key→value 쌍unique 값만
중복key 중복 X값 중복 X
index 접근없음 (key로)없음 (무순서)
멤버십 테스트 x in ...: List는 O(n)(전수) vs Set은 O(1)(해싱). 이 차이가 아래 case study의 핵심이다.

5.5 자료구조 선택 가이드 & Case Study — 매우 중요

필요한 것고를 구조
위치(index)로 접근?List
LIFO (undo/뒤집기)?Stack
FIFO (선착순)?Queue (deque)
key로 빠른 lookup?Dictionary
unique만, 빠른 멤버십?Set
⭐ Case Study — 중복 찾기 (단골)
# O(n²) — nested loop            # O(n) — set
for i in range(len(nums)):           seen = set()
    for j in range(i+1,len(nums)):     for x in nums:
        if nums[i]==nums[j]:                if x in seen: return True
            return True                    seen.add(x)

같은 문제, set으로 O(n²)→O(n)(n=10,000이면 약 100배 빠름). "올바른 자료구조가 복잡도를 바꾼다"의 대표 예.

§6 Memory Model — Stack vs Heap — 메모리 모델

The Key Idea

"O(n) 공간"이 실제로 어디에 사는가? 메모리는 거대한 바이트 배열이다 — 각 바이트에 주소(index)와 값(0–255). 변수 x = 5는 "이름 x → 주소 0x1000 → 값 5"의 연결이다. 타입은 몇 바이트를 읽을지를 정한다(bool 1, int 보통 4, long/float/double 8, 객체 참조 보통 8 — 데이터가 아니라 주소를 담음).

① 메모리 영역 STACK ▼ 지역변수·프레임 HEAP ▲ 객체·list·dict Code + data 크기 작음·매우 빠름 · 큼·느림 ② 콜 스택: main → average → sum sum: a=4, b=6 ← top average: a=4, b=6, total=? main: result=? 호출하면 ▲ push return하면 ▼ pop (즉시 사라짐)
왼쪽: 실행 중 메모리는 STACK(빠름·지역변수)과 HEAP(큼·객체)으로 나뉜다. 오른쪽: 함수가 호출될 때마다 프레임이 쌓이고(push), return하면 즉시 사라진다(pop). sum이 끝나면 그 a,b는 즉시 회수된다 — 그래서 지역변수가 안 남는다. (개념: 강의 0526 Memory Has Regions · The Call Stack)

6.1 Stack vs Heap — 두 영역

StackHeap
크기작음 (1–8MB/thread) (RAM까지)
속도매우 빠름 (포인터만 이동)느림 (빈 chunk 탐색)
관리자동 (LIFO, frame pop)GC(Java/Python) 또는 수동(C malloc/free)
저장지역변수, 매개변수, return 주소객체, list, dict, string
수명함수 끝나면 즉시 사라짐참조가 남아있는 한 살아있음
def f():
    n = 5                # int 객체는 HEAP, 슬롯 n(참조)은 STACK
    items = [1,2,3,4,5]   # list 객체는 HEAP, 슬롯 items(주소)는 STACK
    return items[0]
"Stack은 공짜, Heap은 돈이 든다" — Stack 할당은 포인터 N바이트 이동(CPU 1명령), Heap은 빈 공간 탐색+장부 갱신+GC. 그래서 "불필요한 할당을 피하라"가 성능 조언이다.

6.2 The Call Stack — 함수 호출의 동작

함수가 호출되면 frame이 stack에 push된다. frame 안에는 ① 매개변수(인자 복사본) ② 지역변수 ③ return 주소(끝나면 돌아갈 곳). 함수가 return하면 frame이 즉시 pop되고 지역변수는 사라진다.

예제 — main → average → sum
def sum(a,b):     return a+b
def average(a,b): total = sum(a,b); return total/2
def main():      result = average(4,6)

# sum(4,6) 실행 중 stack 상태:
  sum:     a=4, b=6           ← top
  average: a=4, b=6, total=?
  main:    result=?
# sum이 10 반환 → sum frame pop → average의 total=10

지역변수가 안 남는 이유: suma,bsum이 실행되는 동안만 존재. return하면 그 바이트는 다음에 push될 frame 차지가 된다. 그게 stack의 본질 — 안 남아도 되니까 빠르다.

⚠️ Stack은 무한하지 않다 — Windows 1MB, Linux/macOS 8MB/thread. Python은 기본 약 1,000 nested calls 제한(sys.getrecursionlimit()). 넘으면 stack overflow → RecursionError.

§7 Recursion — 재귀

The Key Idea

"이 폴더의 모든 파일을 세라(하위·하위의 하위 폴더 포함)" — 반복문으로는 worklist+stack을 손수 흉내내야 하지만, 재귀로는 두 줄이다. 재귀 함수는 자기 자신을 호출하는 함수이고, tree·자기유사(self-similar) 구조에 자연스럽다.

정의 — 두 부분

모든 재귀 함수 = ① Base case(멈춤 조건) — 가장 작은 입력, 답이 자명, 재귀 호출 없음. ② Recursive case(줄이기) — 더 작은 같은 문제로 표현하고 재귀 호출. 설계 3질문: 가장 단순한 입력의 답?(base) · 한 step 줄이는 법?(reduce) · 작은 답으로 큰 답 만드는 법?(combine).

7.1 Factorial — 표준 패턴 — trace까지

def factorial(n):
    if n == 0:                # ← base case
        return 1
    return n * factorial(n-1)  # ← recursive case

# 내려가며 쌓임:                  되돌아오며 곱함:
# factorial(4)→4*factorial(3)     factorial(0)=1
#   factorial(3)→3*factorial(2)   factorial(1)=1*1=1
#     factorial(2)→2*factorial(1) factorial(2)=2*1=2
#       factorial(1)→1*factorial(0) factorial(3)=3*2=6
#         factorial(0)→1 (base)   factorial(4)=4*6=24

가장 깊을 때 5개 frame(factorial 4,3,2,1,0)이 동시에 stack에 존재 → 하나씩 pop. 이게 §6의 call stack이 그대로 일하는 모습이다. 재귀는 마법이 아니라 함수가 함수를 부르는 것이고, bookkeeping은 stack이 한다.

⚠️ Base case 먼저 써라. 없으면 factorial(-1)→factorial(-2)→… 무한 → frame이 안 멈추고 쌓여 RecursionError. Traceback이 곧 stack(top=가장 최근 호출).

7.2 Fibonacci — 가장 중요한 빈출 — Time/Space 함정

def fib(n):
    if n == 0: return 0          # base 1
    if n == 1: return 1          # base 2
    return fib(n-1) + fib(n-2)    # ← 두 번 재귀 → trace가 tree
# 0,1,1,2,3,5,8,13,21,34,55,...
fib(5) fib(4) fib(3) fib(3) fib(2) fib(2) fib(1) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) fib(1) fib(0)
fib(5) 호출 트리 — 각 호출이 2갈래로 번진다. 빨강이미 왼쪽에서 계산한 값을 또 계산하는 중복(fib(3) 2번·fib(2) 3번·fib(1)·fib(0)은 더 여러 번). 이 중복 폭증이 O(2ⁿ)의 정체다. memoization은 각 fib(k)를 딱 한 번만 계산해 이 트리를 길이 n의 직선으로 붕괴시킨다 → O(n)(§8). (개념: 강의 0526 The fib(5) Call Tree)
⭐ Naive Fibonacci 복잡도 (시험 단골)

Time: O(2ⁿ) — 각 호출이 2갈래로 번져 중복 subproblem 폭증(fib(5)≈15노드, fib(30)≈260만). Space: O(n) — call tree는 O(2ⁿ) 노드지만, 동시에 stack에 있는 frame은 한 경로(root→leaf)뿐이라 최대 깊이 n. "일반 규칙: 재귀의 space ≈ 최대 재귀 깊이".

⚠️ 가장 헷갈리는 함정: Time O(2ⁿ) ≠ Space O(2ⁿ). Space는 O(n)이다. "fib의 공간복잡도는 O(2ⁿ)" 진술은 거짓.

7.3 재귀 vs 반복, 흔한 버그 — 동등하지만 다름

Recursion 적합Iteration 적합
tree/graph 구조 (파일시스템, JSON)단순 linear 순회 (합·카운트)
분할정복 (merge/quick sort, binary search)깊이가 매우 큼(10,000+) → overflow 위험
정의 자체가 재귀 (factorial, 하노이탑)성능 critical, frame overhead 회피
표현력은 동등(모든 재귀↔모든 loop 변환 가능, 차이는 명확성·스타일). 흔한 재귀 버그 5: ①base case 없음→RecursionError ②잘못된 base case→음수/빈입력 crash ③진행 없음(f(n) 대신 f(n-1)) ④return 빼먹음→None 반환 ⑤mutable state 공유→예측불가.

§8 Memoization & DP 입문 — O(2ⁿ) → O(n)

The Key Idea

naive fib가 느린 이유는 같은 subproblem을 반복 계산하기 때문(fib(3)을 여러 번). 고치는 법: 처음 계산한 답을 기억(cache)해 두고, 같은 입력이 또 오면 다시 계산하지 않고 꺼낸다. 이게 memoization — 전형적인 "공간을 써서 시간을 산다(space-for-time)".

memo = {}
def fib(n):
    if n in memo: return memo[n]   # cache hit → 즉시 반환
    if n < 2: return n
    memo[n] = fib(n-1) + fib(n-2)
    return memo[n]
효과

같은 재귀, 다른 복잡도. Time O(2ⁿ) → O(n)(각 fib(k)를 단 한 번 계산 — 2갈래 tree가 길이 n의 일직선으로 붕괴). Space는 stack O(n) + memo O(n). 안전 조건: 함수가 순수(pure)해야(같은 입력→같은 출력, 부작용 없음) 하고, 입력이 hashable(dict key로 쓰일 수 있어야)해야 한다. 이 기법을 일반화하면 Dynamic Programming(DP)이다.

✏️ Quick Check§7–8 재귀

(a) naive Fibonacci의 Time/Space는? (b) memoization 적용 후 Time은? (c) 재귀 함수의 space complexity는 대략 무엇에 비례?

풀이 보기

(a) Time O(2ⁿ), Space O(n). (b) O(n). (c) 최대 재귀 깊이(max recursion depth).

✦ ✦ ✦

§9 손코딩 훈련 뱅크 — Hand-Coding Bank · 현실 사례 15제 ⭐

기말 코딩 10문항 대비. 핵심 패턴은 — 현실적인 상황(사례)이 주어지고, 거기서 수업에 배운 개념이 무엇인지 알아채 코드로 구현하는 것이다. 그래서 아래 문제는 모두 실생활 시나리오 → 떠올릴 개념 → 구현 순서다. 각 문제는 먼저 직접 손으로 짜 본 뒤 "Show Solution"을 펼쳐 모범답안·복잡도를 확인하세요.

💡 푸는 법: 문제를 읽고 먼저 "이건 어떤 자료구조/기법이지?"를 정한다 — 최근 것부터 되돌리면 Stack, 선착순이면 Queue, 중복/빠른 포함검사면 Set, 세기/매핑이면 Dict, 정렬된 데서 찾기면 Binary search, 자기 안에 같은 모양이 들어있으면(폴더·조직도) 재귀, 같은 계산 반복이면 메모이제이션.

자료구조를 알아채는 사례 — Stack · Queue · Set · Dict

① 🌐 브라우저 뒤로가기★☆☆ · Stack

사용자가 페이지를 차례로 방문하고("visit") 가끔 뒤로가기("back")를 누른다. 동작 목록을 받아 현재 보고 있는 페이지를 반환하라. 예: [("visit","A"),("visit","B"),("back",None),("visit","C")]"C".

def current_page(actions):
    history = []                      # stack
    for kind, page in actions:
        if kind == "visit":
            history.append(page)      # push
        elif kind == "back" and len(history) > 1:
            history.pop()             # pop
    return history[-1] if history else None

"가장 최근 것부터 되돌린다" = LIFO = Stack. push/pop 모두 O(1). ⚠️ 빈 history pop 방지(len>1).

② ☕ 카페 주문 처리(선착순)★★☆ · Queue

카페에서 주문이 들어온 순서대로 음료를 만들어 내보낸다. 주문 리스트를 받아 처리되는 순서대로 출력하되, 앞에서 빼는 연산이 O(1)이 되게 하라.

from collections import deque
def serve(orders):
    q = deque(orders)
    while q:
        drink = q.popleft()          # dequeue, O(1)
        print(f"made {drink}")

"선착순 = FIFO = Queue". ⚠️ list.pop(0)는 O(n)(전부 shift)이라 함정 — 반드시 deque.popleft() O(1).

③ ✉️ 회원가입 중복 이메일 차단★★☆ · Set

가입 신청 이메일이 순서대로 들어온다. 이미 가입된 이메일이면 거절해야 한다. 중복 없이 가입에 성공한 이메일 목록을 반환하라. (가입자가 10만 명이어도 빠르게.)

def register(emails):
    seen = set()                     # O(1) 포함검사
    accepted = []
    for e in emails:
        if e not in seen:
            seen.add(e)
            accepted.append(e)
    return accepted

"중복 막기 + 빠른 포함검사" = Set. e in seen이 O(1) → 전체 O(n). 리스트로 in 검사하면 O(n)×n = O(n²)(함정).

④ 📊 로그에서 최다 발생 이벤트★★☆ · Dict

서버 로그(이벤트 이름 리스트)를 받아 가장 많이 발생한 이벤트를 반환하라. 예: ["login","view","login","buy","login"]"login".

def top_event(logs):
    freq = {}                        # dict: 이벤트 → 횟수
    for ev in logs:
        freq[ev] = freq.get(ev, 0) + 1   # O(1) 갱신
    return max(freq, key=freq.get)

"세기/매핑" = Dict(hash map). 한 번 훑어 O(n). freq.get(ev, 0)로 없는 key 안전 처리(KeyError 회피).

복잡도·탐색을 알아채는 사례 — Binary Search

⑤ 📇 정렬된 사용자 명단에서 이름 검색★★☆ · Binary Search

이름이 사전순으로 정렬된 거대한 사용자 명단(100만 명)에서 특정 이름이 몇 번째에 있는지 찾아라(없으면 −1). 전수조사(O(n))는 너무 느리다 — O(log n)으로.

def find_user(names, target):
    lo, hi = 0, len(names) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if names[mid] == target: return mid
        elif names[mid] < target: lo = mid + 1
        else: hi = mid - 1
    return -1

"정렬된 데서 빠르게 찾기" = Binary Search, O(log n)(100만 → 약 20번). ⚠️ 정렬이 전제 — 안 돼 있으면 못 쓴다. 문자열도 <로 사전순 비교 가능.

재귀를 알아채는 사례 — "안에 같은 모양"이 들어있을 때

⑥ 📁 폴더 안 전체 파일 수 세기★★★ · Recursion

폴더가 {"files": int, "subfolders": [폴더, ...]}로 표현된다. 하위 폴더·하위의 하위까지 포함한 총 파일 수를 구하라. (폴더 안에 폴더 — 자기 자신과 같은 모양!)

def count_files(folder):
    total = folder["files"]              # 직속 파일
    for sub in folder["subfolders"]:
        total += count_files(sub)          # 같은 문제, 더 작은 입력
    return total

"하위 폴더를 세는 것 = 더 작은 같은 문제" → 재귀. base case는 subfolders가 빈 폴더(loop가 안 돌고 직속 파일만 반환). 반복문으로 짜면 worklist+stack을 손수 만들어야 한다.

⑦ 🏢 조직도 전체 인원 세기★★☆ · Recursion

회사 조직도에서 각 사람이 {"name": str, "reports": [부하직원, ...]}로 표현된다. 어떤 매니저 아래 직·간접 포함 전체 인원(본인 제외)을 구하라.

def team_size(person):
    total = len(person["reports"])       # 직속 부하
    for r in person["reports"]:
        total += team_size(r)              # 그 부하의 팀까지
    return total

트리 구조(사람 안에 사람) → 재귀. §6의 call stack이 깊이만큼 frame을 쌓는다. 파일 폴더(⑥)와 정확히 같은 골격.

메모이제이션·2D 격자를 알아채는 사례

⑧ 💱 환율 조회 결과 캐싱★★☆ · Memoization

fetch_rate(currency)는 외부 서버를 호출해 느리다(1초). 같은 통화를 여러 번 물어봐도 한 번만 실제 조회하고 이후엔 즉시 답하도록 캐싱하라.

cache = {}
def get_rate(currency):
    if currency in cache:            # cache hit → 즉시
        return cache[currency]
    rate = fetch_rate(currency)        # 느린 실제 조회 (1회만)
    cache[currency] = rate
    return rate

"같은 입력의 반복 계산을 기억" = 메모이제이션(space-for-time). §8 fib와 똑같은 패턴. 안전 조건: 같은 통화→같은 환율(순수), 입력이 hashable.

⑨ 🖼️ 사진 좌우 반전 (픽셀 격자, 제자리)★★★ · 2D list · O(1) 공간

흑백 사진이 2차원 리스트(각 행이 픽셀 값 리스트)로 주어진다. 거울처럼 좌우 반전하되 새 이미지를 만들지 말고 제자리(in-place)로 바꿔라. (각 행을 뒤집는 문제.)

def flip_horizontal(img):
    for row in img:
        l, r = 0, len(row) - 1
        while l < r:                    # two-pointer 제자리 교환
            row[l], row[r] = row[r], row[l]
            l += 1; r -= 1

각 행을 in-place reverse(§3) → 추가 메모리 O(1)(새 격자 안 만듦), 시간 O(행×열). "이미지=격자=2D 리스트"를 알아채는 게 핵심.

⑩ 🏆 두 반 성적표 합치기 (정렬 유지)★★☆ · Merge

A반·B반 성적이 각각 오름차순 정렬되어 있다. 둘을 합쳐 전체 정렬된 하나의 리스트로 만들어라(다시 정렬하지 말고 한 번 훑어서).

def merge_scores(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i += 1
        else:           res.append(b[j]); j += 1
    res.extend(a[i:]); res.extend(b[j:])   # 남은 꼬리
    return res

두 포인터로 한 번씩 → O(n+m). 합친 뒤 sorted()로 다시 정렬하면 O(N log N)으로 낭비 — merge sort의 핵심 단계가 바로 이것.

교수님 연구분야(NLP·데이터) 맛 — 현실 사례 심화

교수님은 언어·멀티모달 AI(LaMI Lab)·Google/Amazon/NAVER 출신이라, 현실 사례에 가벼운 데이터/ML 색채가 섞일 수 있다. 단, 푸는 도구는 똑같이 수업에서 배운 자료구조·복잡도다.
⑪ 🔤 단어 → 정수 ID 사전 만들기 (vocabulary)★★☆ · Dict · NLP

NLP에서 텍스트를 모델에 넣으려면 각 고유 단어에 정수 ID를 부여한다(tokenizer의 vocabulary). 단어 리스트를 받아 처음 등장한 순서대로 0,1,2…를 매긴 {단어: id} 사전을 만들어라.

def build_vocab(words):
    vocab = {}
    for w in words:
        if w not in vocab:        # 새 단어 → 다음 ID
            vocab[w] = len(vocab)
    return vocab

"고유 항목에 번호" = Dict + O(1) 멤버십. 전체 O(n). len(vocab)가 자연스럽게 다음 ID가 된다.

⑫ 📈 실시간 가격 이동평균 (최근 k개)★★☆ · Queue(deque)

가격이 스트림으로 들어온다. 매 시점 최근 k개의 평균을 출력하라. 가장 오래된 값은 자동으로 빠져야 한다(window 유지).

from collections import deque
def moving_average(prices, k=3):
    window = deque(maxlen=k)        # 꽉 차면 가장 오래된 게 자동 pop
    out = []
    for p in prices:
        window.append(p)
        out.append(sum(window) / len(window))
    return out

"오래된 것부터 밀어냄 = FIFO = Queue". deque(maxlen=k)가 sliding window를 O(1)로 관리. 슬라이딩 윈도우는 스트리밍 지표의 단골.

⑬ 🎯 추천: 아직 안 본 항목만 고르기★★☆ · Set

전체 영화 목록 all_movies와 사용자가 이미 본 목록 watched가 주어진다. 안 본 영화만 추천 리스트로 반환하라(영화가 수만 개여도 빠르게).

def to_recommend(all_movies, watched):
    seen = set(watched)            # O(1) 포함검사
    return [m for m in all_movies if m not in seen]

watchedSet으로 바꾸면 포함검사 O(1) → 전체 O(n). 리스트로 not in 하면 O(n×m)(함정). "차집합" 발상.

입문 명강의 단골 — 한 단계 위 (MIT 6.0001 · Berkeley CS61A)

아래 둘은 해외 입문 명강의의 시험 단골이면서, 우리가 배운 재귀+메모이제이션 / 정렬+포인터를 한 번에 묻는 "조금 어려운" 유형이다. 배점 큰 코딩 문제로 나오기 좋다.
⑭ 💰 동전 거스름돈 경우의 수 (count change)★★★ · 트리 재귀 → 메모이제이션

동전 종류 coins(예: [1,5,10,25], 무한 사용 가능)로 금액 amount를 만드는 경우의 수를 구하라(순서 무시). 예: 15 → 6가지. (Berkeley CS61A의 대표 트리 재귀 문제.)

발상 — 각 동전마다 "이 동전을 (한 번 더) 쓴다" vs "이 동전은 더 안 쓰고 다음 종류로" 두 갈래. 이 2갈래 분기가 §7의 트리 재귀다.

def count_change(amount, coins):
    memo = {}
    def helper(amt, i):
        if amt == 0: return 1           # 딱 맞춤 → 1가지
        if amt < 0 or i == len(coins): return 0
        if (amt, i) in memo: return memo[(amt, i)]
        memo[(amt, i)] = (helper(amt - coins[i], i)   # i동전 쓰기
                          + helper(amt, i + 1))        # i동전 그만
        return memo[(amt, i)]
    return helper(amount, 0)

메모 없이는 같은 (금액, 동전위치)를 반복 계산해 지수적. memo로 각 상태를 1번만 → 사실상 DP. §7(트리 재귀)+§8(메모이제이션)을 한 문제로 묶은 전형이다.

⑮ 🛒 예산에 딱 맞는 두 상품 (two-pointer)★★☆ · 정렬 + 두 포인터

가격이 오름차순 정렬된 상품 목록에서 합이 정확히 budget인 두 상품의 인덱스 쌍을 반환하라(없으면 None). 모든 쌍 검사(O(n²)) 말고 O(n)으로.

def find_pair(prices, budget):
    lo, hi = 0, len(prices) - 1
    while lo < hi:
        s = prices[lo] + prices[hi]
        if s == budget: return (lo, hi)
        elif s < budget: lo += 1      # 합 부족 → 왼쪽을 키움
        else: hi -= 1                  # 합 초과 → 오른쪽을 줄임
    return None

양 끝에서 좁혀오는 two-pointer: 정렬돼 있으니 합이 크면 hi--, 작으면 lo++. Time O(n), Space O(1). "정렬됨"을 이용하는 점이 binary search와 통한다.

📌 추가로 손에 익혀둘 클래식(개념 정의 자체가 코드로 자주 나옴): factorial(재귀 base+곱), memoized fibonacci(§8), 괄호 짝 검사(코드에디터 — Stack), 유클리드 GCD gcd(a,b)=gcd(b,a%b)(재귀), 중첩 리스트 flatten(트리 재귀), 빠른 거듭제곱 x^n=(x^(n/2))²(분할정복 O(log n)). 모두 "배운 base case+재귀 / 자료구조"로 분해하면 풀린다.

§10 시험 직전 핵심 요약 · A4 치트시트 — 그대로 손글씨로 옮기기

아래는 A4 양면(영어 손글씨) 치트시트 압축본. 시험장 반입용으로 그대로 옮겨 적으세요.

Complexity Cheat Card

Order: O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) · n=1000: 1·10·1000·10⁴·10⁶·∞
Simplify: drop constants, drop lower terms, keep dominant.
Patterns: 1 loop=O(n), nested=O(n²), halving=O(log n), divide&merge=O(n log n), 2-way recursion=O(2ⁿ).
Space: count EXTRA only. var=O(1), len-n list=O(n), n×n=O(n²), recursion≈max depth=O(n). in-place=O(1).

Data Structure Ops
구조AccessSearchInsertDelete
ListO(1) by indexO(n)O(1) end / O(n) frontO(1) end / O(n) front
Stack (LIFO)O(1) topO(n)O(1) pushO(1) pop
Queue (deque, FIFO)O(1) frontO(n)O(1) enqO(1) deq
Dict / SetO(1) by keyO(1)O(1)O(1)

list.pop(0)=O(n) vs deque.popleft()=O(1) · x in list=O(n) vs x in set=O(1).

Sorting
정렬BestAvgWorstSpace
BubbleO(n)O(n²)O(n²)O(1)
SelectionO(n²)O(n²)O(n²)O(1)
MergeO(n log n)O(n log n)O(n log n)O(n)
QuickO(n log n)O(n log n)O(n²)O(log n)
Memory · Recursion Cheat Card

Stack: small, fast, auto-LIFO, locals/params/return-addr, dies on return. Heap: large, slow, GC, objects/lists.
Call stack: frame push on call, pop on return. Python limit ≈1000.
Recursion = base case + recursive case (write base FIRST). factorial O(n)/O(n).
naive fib: Time O(2ⁿ), Space O(n) (tree=2ⁿ nodes, stack=n depth). memoization→Time O(n) (space-for-time, needs pure+hashable).
Errors: empty stack pop→IndexError · no/bad base case→RecursionError · missing key→KeyError.

⚠️ 마지막 자가점검 — ① 복잡도 순서 외웠나? ② DS 4연산 표? ③ 정렬 4종 Avg/Worst/Space? ④ list.pop(0) vs deque.popleft() 함정? ⑤ fib Time O(2ⁿ)·Space O(n) 안 헷갈리나? ⑥ Stack(자료구조) ≠ Stack(메모리영역) 구분? ⑦ T/F는 극단 단어(always/never)=의심, 확실한 것만! → 다음은 실전 모의고사 2회분으로.