개발 수학

합 공식: 이중 루프는 왜 n²일까

1 + 2 + … + n, 시그마, 두 배씩 늘리는 배열

4장 · Unit 4 · 읽는 시간 약 16분

목차 · 진행 0%

    『한입 알고리즘』 1장에 예산에 딱 맞는 상품 두 개를 고르는 문제가 있다. 가장 쉬운 풀이는 모든 쌍을 한 번씩 비교하는 이중 루프다.

    for (let i = 0; i < prices.length; i++) {
      for (let j = i + 1; j < prices.length; j++) {
        if (prices[i] + prices[j] === budget) return [i, j]
      }
    }

    책은 이 풀이가 "상품 n개에 n(n − 1)/2쌍, O(n²)"이라고 적는다. O(n²)는 입력 n이 커질 때 일이 n²에 비례해 늘어난다는 뜻으로, Big-O 표기법(Big-O notation)으로 적은 것이다(빅오 표기법 노트). 안쪽 루프는 i + 1부터 시작해서 i가 커질수록 점점 짧게 도는데, 왜 n × n과 같은 O(n²)일까? n(n − 1)/2라는 식은 어디서 왔을까? 이 Unit은 1부터 n까지 더한 합을 한 번에 구하는 방법과, 두 배씩 커지는 수의 합이 왜 작게 끝나는지를 본다.

    이중 루프의 비교 횟수: 하나씩 세기

    예측먼저 답해 보세요

    원소 5개 배열에서 for (i = 0; i < 5; i++) 안에 for (j = i + 1; j < 5; j++)를 두고 안쪽에서 비교를 한 번씩 한다. 비교는 모두 몇 번일까?

    선택지와 해설 보기
    실험 모든 쌍을 한 번씩 비교하면 몇 번?
    실험모든 쌍을 한 번씩 비교하면 몇 번?

    j가 i + 1부터 시작해서 같은 쌍을 두 번 보지 않습니다. "다음"을 눌러 i마다 안쪽 루프가 몇 번 도는지 세어 보세요.

    언어
    입력
    ▶ 다음에 실행할 줄(디버거가 멈춘 줄)
    방금 실행한 줄
    변수·출력·아래 설명은 방금 실행한 줄까지의 상태입니다.
    카드 안에서 ← → 키로도 넘길 수 있습니다.
    const n = 4let count = 0for (let i = 0; i < n; i++) {  for (let j = i + 1; j < n; j++) {    count++  }}console.log(`n = ${n}, 비교 ${count}번`)
    변수
    n
    4
    출력
     
    인덱스
    1. 0
    2. 1
    3. 2
    4. 3
    1/19단계.
    • 해볼 것
    • n = 4를 끝까지 실행해 본다
    • n = 6으로 바꿔 끝까지 실행하고 비교 횟수를 견줘 본다

    n = 4를 끝까지 실행하면 count가 6이 된다.

    n = 4면 i가 0일 때 안쪽 루프가 3번, 1일 때 2번, 2일 때 1번, 3일 때 0번 돈다. 비교 횟수는 3 + 2 + 1 = 6이다. n = 6이면 5 + 4 + 3 + 2 + 1 = 15번이다. 원소가 n개면 비교 횟수는 1부터 n − 1까지 더한 합이다.

    비교 횟수를 표로 그리면 모양이 보인다. 행은 i, 열은 j다. 원소 4개의 모든 (i, j) 칸은 4 × 4 = 16칸이고, 그중 코드가 비교하는 칸(✓)은 j > i인 대각선 위쪽뿐이다.

    i \ j0123
    0·✓✓✓
    1··✓✓
    2···✓
    3····

    16칸에서 대각선(i = j) 4칸을 빼면 12칸이고, 그 절반인 6칸이 ✓다. n개면 (n × n − n) ÷ 2 = n(n − 1)/2칸이다. n = 4를 넣으면 (16 − 4) ÷ 2 = (4 × 3) ÷ 2 = 6이다. n × n − n은 n을 n번 더한 것에서 한 번 뺀 것, 곧 n을 (n − 1)번 더한 것이라 n(n − 1)이다. 『한입 알고리즘』 1장의 n(n − 1)/2가 바로 이 수다. 안쪽 루프가 짧아져도 비교하는 칸은 여전히 n²의 절반쯤이다.

    가우스의 짝짓기: 1 + 2 + … + n을 한 번에

    1부터 5까지 더한 값을 하나씩 더하지 않고 구해 보자. 같은 수열을 앞에서부터 한 줄, 뒤에서부터 한 줄 적고 위아래로 짝을 지어 더한다. 수학자 가우스가 어릴 때 1부터 100까지의 합을 1 + 100, 2 + 99, …처럼 양 끝을 짝지어(합이 101인 짝 50개) 금방 구했다는 이야기가 전한다. 두 줄로 적는 것은 그 짝짓기를 눈에 보이게 한 것이다.

    앞에서부터12345
    뒤에서부터54321
    위아래 합66666

    1~5처럼 개수가 홀수면 한 줄에서 양 끝을 짝지을 때 가운데 3이 짝 없이 남는다. 줄을 두 번 적으면 모든 수에 짝이 생기고, 마지막에 반으로 나누면 된다. 위아래 합은 모두 6(= 5 + 1)이고 짝이 5개라서 두 줄을 합하면 5 × 6 = 30이다. 같은 수열을 두 번 더했으니 한 줄의 합은 그 절반인 15다. n으로 바꿔 적으면 이렇다.

    1+2+⋯+n=n(n+1)21 + 2 + \cdots + n = \frac{n(n+1)}{2}

    말로 읽으면 "1부터 n까지 더한 합은 n 곱하기 (n + 1)을 2로 나눈 값"이다. n = 100이면 100 × 101 ÷ 2 = 5050이다.

    이중 루프의 비교 횟수는 1부터 n − 1까지의 합이라서, 식의 n 자리에 n − 1을 넣으면 된다.

    1+2+⋯+(n−1)=(n−1)n21 + 2 + \cdots + (n-1) = \frac{(n-1)n}{2}

    n = 5면 4 × 5 ÷ 2 = 10으로, 원소 5개 이중 루프의 비교 횟수를 묻던 예측 문제의 답과 같다. n = 1,000이면 999 × 1000 ÷ 2 = 499,500, 약 50만 번이다.

    이 식의 위를 풀면 (n − 1) × n = n × n − 1 × n = n² − n이고, 2로 나누면 n²/2 − n/2다. n이 커지면 n²/2에 비해 n/2는 거의 보이지 않는다. n = 1,000에서 n²/2는 50만, n/2는 500이다. Big-O 표기법은 입력이 커질 때 일이 늘어나는 모양만 남기려고 상수 배(1/2)와 작은 쪽(n/2)을 버린다. 그래서 n²/2는 O(n²)이다. n이 두 배가 되면 비교는 약 네 배가 된다.

    n비교 횟수 n(n − 1)/2n²
    1045100
    1004,95010,000
    1,000499,5001,000,000
    10,00049,995,000100,000,000

    어느 행에서나 비교 횟수는 n²의 절반쯤이다. 절반이든 전부든 n이 10배가 되면 100배로 자라는 모양은 같다.

    시그마 기호와 등차수열: 더하기 루프를 식으로 적기

    1 + 2 + … + n처럼 여러 수를 더하라는 식은 시그마(summation) 기호 Σ로 줄여 적는다.

    ∑k=1nk\sum_{k=1}^{n} k

    읽는 법은 세 군데다. Σ 아래의 k = 1은 시작(k를 1부터), 위의 n은 끝(n까지), 오른쪽의 k는 매번 더할 것이다. "k를 1부터 n까지 바꿔 가며 k를 모두 더한다"고 읽는다. n = 4면 1 + 2 + 3 + 4 = 10이다.

    오른쪽이 k가 아니어도 된다. 이중 루프의 비교 횟수는 i마다 안쪽이 n − 1 − i번 도는 것을 i = 0부터 n − 1까지 더한 값이다.

    ∑i=0n−1(n−1−i)\sum_{i=0}^{n-1} (n - 1 - i)

    n = 4면 i = 0, 1, 2, 3을 넣어 3 + 2 + 1 + 0 = 6이다. 1절에서 n = 4로 센 비교 횟수와 같다.

    수학 다리시그마와 더하기 루프
    수학에서Σ 아래에 시작(k = 1), 위에 끝(n), 오른쪽에 더할 것(k)을 적습니다.
    코드에서let total = 0으로 두고 for (let k = 1; k <= n; k++) total += k처럼 시작·끝·더할 것을 루프에 적습니다.
    같은 점시작 값, 끝 값, 한 번에 더할 것, 이 세 가지로 합이 정해집니다.
    다른 점루프는 n번 돌며 하나씩 더해 O(n)이 걸립니다. 합 공식 n(n + 1)/2를 알면 루프 없이 계산 한 번(O(1))으로 끝납니다.

    1, 2, 3, 4, …처럼 같은 수만큼 일정하게 늘어나는 수의 줄을 등차수열(arithmetic sequence)이라고 한다. 이중 루프의 안쪽 반복 횟수(n − 1, n − 2, …, 1)도 1씩 줄어드는 등차수열이다. 10, 20, 30, …처럼 10씩 늘어나도 등차수열이고, 이때는 10 × (1 + 2 + 3 + …)로 묶어서 같은 짝짓기를 쓴다. 늘어나는 폭이 0이 아닌 등차수열은 n개를 더하면 n²에 비례하는 값이 나온다. 그래서 "반복할 때마다 일이 일정하게 하나씩 늘거나 주는" 코드는 O(n²)다.

    두 배씩 늘리는 배열: 1 + 2 + 4 + … + n은 2n보다 작다

    이번에는 1, 2, 4, 8, …처럼 같은 수(2)를 곱해 가며 커지는 수를 더해 보자. 이렇게 일정하게 곱해지는 수의 줄을 등비수열(geometric sequence)이라고 한다. 2를 여러 번 곱한 수, 곧 2의 거듭제곱(power)들이다.

    『한입 알고리즘』 0장의 보너스 문제에 이 합이 나온다. 동적 배열(dynamic array)은 꽉 차면 두 배 크기의 새 공간을 만들고 원소를 모두 옮겨 담는다. 용량 1에서 시작해 push를 8번 하면 꽉 찰 때마다 1, 2, 4개를 옮겨서 복사는 1 + 2 + 4 = 7번이다. 0장은 이 결과로 push 한 번의 평균이 O(1)이라고 설명한다. 여기서는 "왜 2n보다 작은가"만 본다.

    실험 1 + 2 + 4 + … + n은 2n을 넘을까?
    실험1 + 2 + 4 + … + n은 2n을 넘을까?

    두 배씩 커지는 수를 더하며 지금까지 합과 2n을 막대로 견줍니다.

    언어
    입력
    ▶ 다음에 실행할 줄(디버거가 멈춘 줄)
    방금 실행한 줄
    변수·출력·아래 설명은 방금 실행한 줄까지의 상태입니다.
    카드 안에서 ← → 키로도 넘길 수 있습니다.
    const n = 64let total = 0let k = 1while (k <= n) {  total += k  k *= 2}console.log(`합 ${total}, 2n = ${2 * n}`)
    변수
    n
    64
    출력
     
    1/26단계.
    • 해볼 것
    • n = 64를 끝까지 실행해 합이 2n보다 얼마나 작은지 본다

    n = 64를 끝까지 실행하면 합은 127, 2n은 128이다.

    1, 2, 4, …를 n까지 더하는 동안 지금까지 합은 늘 2n보다 작고, 끝까지 더해도 1이 모자란다. 단계마다 합을 적어 보면 규칙이 있다. 1을 더하면 합 1, 2를 더하면 합 3, 4를 더하면 합 7이다. 지금까지 합은 다음에 더할 수보다 늘 1 작다. 새로 더하는 수 하나가 앞의 것을 모두 합친 것보다 크다는 뜻이다.

    1+2+4+⋯+n=2n−11 + 2 + 4 + \cdots + n = 2n - 1

    n이 2의 거듭제곱일 때 성립하고, "두 배씩 커지는 수를 n까지 더하면 2n에서 1이 모자란다"고 읽는다. n = 8이면 1 + 2 + 4 + 8 = 15 = 16 − 1이다.

    그림으로 보면 더 분명하다. 길이 2n짜리 막대에 가장 큰 n부터 채운다. n은 막대의 절반이다. 다음 n/2는 남은 칸의 절반, n/4는 또 남은 칸의 절반이다. 조각을 아무리 더해도 오른쪽 끝에는 마지막 조각만큼 빈칸이 남는다.

    막대 전체 길이는 2(아래 줄로 읽으면 2n)입니다. 조각을 아무리 더해도 오른쪽 끝에 마지막 조각만큼 빈칸이 남습니다.

    같은 막대를 n을 1로 놓고 읽으면 n/2는 1/2, n/4는 1/4이니 1 + 1/2 + 1/4 + 1/8 + …이다. 막대 길이는 2이고, 조각을 끝없이 더해도 빈칸이 마지막 조각만큼 남아서 합은 2에 한없이 가까워질 뿐 넘지 않는다.

    1+12+14+⋯<21 + \frac{1}{2} + \frac{1}{4} + \cdots < 2

    "1에 그 절반, 또 그 절반을 계속 더해도 2보다 작다"고 읽는다. 1/2 + 1/4 + 1/8을 더하면 7/8이라 1에서 1/8이 모자라고, 1까지 합치면 1.875다.

    동적 배열로 돌아가면, 원소 n개를 넣는 동안 복사는 1 + 2 + 4 + … + n/2 = n − 1번이다. 마지막으로 옮기는 묶음은 n개가 아니라 n/2개(용량 n/2가 꽉 찼을 때)라서, 공식의 n 자리에 n/2를 넣으면 2(n/2) − 1 = n − 1이다. n이 2의 거듭제곱이 아니어도 마지막으로 늘린 크기가 2n을 넘지 않으니 복사는 2n보다 적다. 어떤 push 한 번은 원소 n/2개를 옮기느라 오래 걸리지만, n번 전체의 복사가 2n보다 적으니 한 번 평균은 2번이 안 된다. 이렇게 비싼 연산이 드물 때 전체 비용을 연산 횟수로 나눠 평균을 보는 방법을 분할 상환 분석(amortized analysis)이라고 한다.

    수의 줄예n까지 더한 합코드에서
    등차수열(같은 수만큼 늘어남)1, 2, 3, …, nn(n + 1)/2, 약 n²/2이중 루프, 스프레드로 배열을 매번 복사(원소를 하나 더할 때마다 지금까지의 원소를 전부 새 배열로 옮기니 1 + 2 + … + n번) → O(n²)
    등비수열(2를 곱함)1, 2, 4, …, n2n − 1, 2n보다 작음두 배씩 늘리는 배열의 복사 → 전체 O(n), 평균 O(1)

    확인: 복잡도 설명에서 틀린 줄 찾기

    검토 기준은 앞 절에 다 나왔다. 합의 식에서 Big-O를 바르게 읽었는가(2절), n이 2배일 때 몇 배인지 맞는가(2절), push 한 번의 비용과 n번의 평균을 구분했는가(4절)를 줄마다 견준다.

    함정 찾기 AI가 쓴 이중 루프 복잡도 설명, 어느 줄이 틀렸을까?
    함정 찾기AI가 쓴 이중 루프 복잡도 설명, 어느 줄이 틀렸을까?
    내가 AI에게 한 요청
    “배열에 중복이 있는지 모든 쌍을 비교하는 함수의 시간 복잡도를 설명해 줘.”

    의심스러운 줄을 모두 눌러 표시한 뒤 “검토 끝”을 누르세요. 함정은 3개입니다.

    확인 문제 연습
    확인 문제연습

    1. for (i = 0; i < n; i++) 안에 for (j = 0; j < n; j++)가 있다(j도 0부터). n = 10이면 안쪽 줄은 몇 번 실행될까?

    2. Σ의 아래에 k = 1, 위에 4, 오른쪽에 k가 적힌 식(k를 1부터 4까지 바꿔 가며 k를 더한다)의 값은?

    3. 꽉 차면 두 배로 늘리는 배열에 push를 n번 할 때 원소 복사는 모두 몇 번쯤일까?

    4. 1 + 1/2 + 1/4 + 1/8 + …처럼 앞 수의 절반을 끝없이 더하면 합은 어떻게 될까?

    이 Unit이 쓰이는 곳

    • 『한입 알고리즘』 0장 Big-O 감각: 함정 찾기에서 스프레드로 배열을 매번 복사하면 1 + 2 + … + n번 복사해 O(n²)이 된다. 보너스에서 두 배씩 늘리는 동적 배열의 복사가 1 + 2 + 4 + … + n/2 = n − 1번이라 push 평균이 O(1)이다.
    • 『한입 알고리즘』 1장 해시: 모든 쌍을 비교하면 n(n − 1)/2쌍이다. 버킷 수를 두 배씩 키우는 리해시의 평균 비용이 상수라는 설명도 0장 보너스와 같은 계산이다.
    • 『한입 알고리즘』 2장 투 포인터: 원소 n개의 쌍과 연속 구간이 각각 약 n²/2개라서 모두 보면 O(n²)이다.
    • 『한입 알고리즘』 7장 정렬: 보너스의 역순 쌍 문제에서 n이 10만이면 모든 쌍이 약 50억 개(10만 × 10만 ÷ 2)다. 7장은 삽입 정렬이 최악 O(n²)이라고 적는데, 이 Unit으로 읽으면 최악의 경우 원소마다 앞쪽을 1, 2, …, n − 1칸씩 미는 횟수의 합이 n(n − 1)/2이기 때문이다.
    • 『한입 CS』 8장 데이터베이스: 낙관적 잠금에서 n명이 같은 행을 고치면 재시도가 (n − 1) + (n − 2) + … + 1 = n(n − 1)/2번이다.
    더 풀어 보기(선택) · 10칸씩 늘리는 배열은 왜 느릴까
    보너스 문제10칸씩 늘리는 배열은 왜 느릴까

    꽉 차면 두 배가 아니라 10칸씩 늘리는 배열이 있다. 처음 10칸에서 시작해 원소 n개를 넣는 동안, 꽉 찰 때마다 그때까지의 원소를 모두 새 공간으로 복사한다. n = 100과 n = 200일 때 복사는 모두 몇 번일까? n이 두 배가 되면 복사는 몇 배가 되는지, 이 배열의 push 평균 비용이 왜 O(1)이 아닌지 설명해 보자.

    n = 100 → 복사 ?번
    n = 200 → 복사 ?번
    다음Unit 5. 나머지 연산: 음수의 나머지는 언어마다 다르다Unit 5에서는 나머지 연산 %를 봅니다. 캐러셀 인덱스와 해시 버킷에 쓰이고, 음수를 나누면 JavaScript와 Python이 다른 답을 냅니다.

    『개발 수학』 Unit 4. 합 공식: 이중 루프는 왜 n²일까 · younggeun0. Claude와 함께 썼고, 저자가 아직 전체 내용을 검수하지 않은 초안입니다. 사실 주장은 공식 문서로, 예제 코드는 실제로 실행해 확인했습니다.

    보기 옵션