Chapter 19

데이터와 알고리즘

지금까지는 컴퓨터를 더 빠르게 만드는 방법을 봤다. 클럭을 올리고, 캐시를 붙이고, 코어를 늘렸다. 그런데 같은 컴퓨터에서도 일하는 방법만 바꿔서 수백만 배 빨라지는 경우가 있다. 이 장은 그 “방법”, 즉 알고리즘과 데이터를 담는 그릇인 자료구조 이야기다. 수학 기호 없이 직접 돌려 보며 감을 잡자.

알고리즘은 레시피, 자료구조는 그릇

알고리즘(algorithm)은 문제를 푸는 절차다. 요리로 치면 레시피다. 같은 김치찌개도 레시피에 따라 30분이 걸리기도 하고 3시간이 걸리기도 한다. 자료구조(data structure)는 데이터를 담는 방식이다. 양념을 서랍에 아무렇게나 넣어 두면 찾을 때마다 다 뒤져야 하지만, 이름순으로 꽂아 두거나 칸마다 라벨을 붙이면 금방 찾는다.

알고리즘이 얼마나 빠른지는 초(秒)가 아니라 단계 수로 잰다. 초는 컴퓨터마다 다르지만, 단계 수는 방법 자체의 성질이기 때문이다. 그리고 가장 중요한 질문은 “데이터가 10배, 1000배로 늘면 단계 수는 어떻게 늘어나나”다.

정렬: 줄 세우기 경주

이진 탐색을 쓰려면 먼저 정렬해야 한다. 정렬은 컴퓨터가 가장 많이 하는 일 중 하나다. 쇼핑몰의 “낮은 가격순”, 메일함의 “최신순”, 검색 결과의 순위가 모두 정렬이다. 방법은 여러 가지다.

SIMULATOR

정렬 알고리즘 경주

알고리즘
처음 상태
 
비교 횟수0
옮긴 횟수0
네 가지 비교 (같은 데이터)—
노란 막대는 지금 비교 중인 두 칸이다. 해볼 것: 막대를 80개로 늘리고 “네 가지 동시 비교”를 눌러 보자. 버블은 약 n²/2번(3,000번 넘게), 병합과 퀵은 약 n×log₂n번(500번 안팎) 비교한다. “거의 정렬됨”에서는 삽입 정렬이 갑자기 1등이 된다. 데이터의 모양에 따라 최고의 방법이 달라진다.

빅오: 데이터가 늘면 시간은 어떻게 늘까

정렬 경주에서 본 것처럼, 중요한 것은 지금 몇 번인지보다 데이터가 늘 때 단계 수가 늘어나는 모양이다. 이것을 나타내는 표기가 빅오(Big-O)다. O(n)은 “데이터가 2배면 시간도 2배”, O(n²)은 “데이터가 2배면 시간은 4배”라는 뜻이다. 컴퓨터를 10배 빠르게 사도, O(n²) 알고리즘으로는 데이터를 약 3배밖에 더 처리하지 못한다.

CHART

1초에 10억 단계를 하는 컴퓨터라면

가로축과 세로축 모두 로그 눈금이다(한 칸 = 10배). 해볼 것: n을 100만으로 두고 O(n²)과 O(n log n)의 시간을 비교해 보자. 같은 컴퓨터에서 몇 분 대 몇 밀리초다. O(2ⁿ)은 n이 60만 넘어도 우주의 나이보다 오래 걸린다. 16장의 무차별 대입이 바로 이런 모양이다.
예측해 보기

버블 정렬(O(n²))로 1만 명의 성적을 정렬하는 데 1초가 걸렸다. 10만 명이면 얼마나 걸릴까?

데이터가 10배가 되면 n²은 몇 배가 될까?

n이 10배면 n²은 100배다. 같은 일을 O(n log n) 정렬로 하면 약 12배 남짓만 늘어난다. 데이터가 커질수록 알고리즘의 차이가 컴퓨터 성능 차이를 압도한다.

그릇 고르기: 배열, 연결 리스트, 해시 테이블

데이터를 메모리에 어떻게 놓느냐에 따라 잘하는 일이 다르다. 6장의 메모리 사물함을 떠올리자.

배열: 나란히 붙은 칸 사과 배 귤 감 k번째 칸 = 시작 주소 + k → 바로 접근 · 캐시 친화적 중간에 끼우려면 뒤를 전부 한 칸씩 밀어야 함 연결 리스트: 다음 칸 주소를 들고 있음 사과 배 귤 끼우기·빼기는 화살표만 바꾸면 끝 k번째를 찾으려면 처음부터 따라가야 함 · 메모리에 흩어짐 해시 테이블: 이름으로 칸 번호를 바로 계산 "귤" 해시 함수 → 3 0 1 2 귤 4 데이터가 아무리 많아도 거의 한 번에 찾는다 (평균 O(1)). 대신 순서는 없다. 파이썬의 딕셔너리, 자바스크립트의 객체, 데이터베이스의 색인이 이 원리를 쓴다.
그림 19-1. 세 가지 그릇. 무엇을 자주 하느냐(순서대로 읽기, 끼우기, 이름으로 찾기)에 따라 알맞은 그릇이 다르다.

해시 테이블(hash table)은 16장의 해시 함수를 응용한다. 이름을 해시 함수에 넣어 나온 수를 칸 번호로 쓰면, 찾을 때도 같은 계산으로 바로 그 칸에 간다. 문제는 서로 다른 이름이 같은 칸 번호를 받는 충돌(collision)이다. 그때는 그 칸에 줄을 세운다.

SIMULATOR

해시 테이블에 이름 넣고 찾기

 
칸 수
해시 계산—
찾을 때 확인한 횟수—
가장 긴 줄—
이 장난감 해시 함수는 글자 번호(유니코드)를 모두 더해 칸 수로 나눈 나머지를 쓴다. 해볼 것: 20명을 넣고 칸 수를 5칸 → 31칸으로 바꿔 보자. 칸이 넉넉하면 줄이 짧아져 거의 한 번에 찾는다. 실제 해시 테이블은 줄이 길어지기 전에 칸 수를 자동으로 두 배로 늘린다.

지도 앱은 어떻게 길을 찾을까

지도 앱의 길 찾기, 10장의 라우터가 패킷 경로를 고르는 일, 게임 캐릭터가 벽을 피해 움직이는 일은 모두 같은 문제다. 장소를 점으로, 길을 선으로 놓은 그래프(graph)에서 최단 경로(shortest path)를 찾는 것이다. 아래 격자에서 벽을 그리고 세 가지 방법을 비교해 보자.

SIMULATOR

미로에서 길 찾기

방법
 
살펴본 칸—
찾은 경로 길이—
최단 경로인가—
칸을 누르거나 끌면 벽을 세우거나 지운다. 초록 S에서 빨강 G로 간다. 연한 칸이 알고리즘이 살펴본 곳이다. BFS는 출발점에서 가까운 순서로 물결처럼 퍼져 반드시 최단 경로를 찾지만 많이 살펴본다. 목표 쪽으로 직진은 적게 살펴보지만 벽에 막히면 돌아가는 길을 고를 수 있다. A*는 “지금까지 온 거리 + 남은 거리 어림값”이 작은 칸부터 보아, 최단 경로를 보장하면서도 덜 살펴본다. 실제 지도 앱은 여기에 도로 속도와 교통 정보를 가중치로 더한다.

핵심 정리

  1. 알고리즘은 문제를 푸는 절차, 자료구조는 데이터를 담는 방식이다. 속도는 초가 아니라 단계 수로 잰다.
  2. 정렬된 데이터에서 이진 탐색은 10억 개 중에서도 30번 만에 찾는다.
  3. 정렬은 방법에 따라 n²/2번(버블)과 n log n번(병합·퀵)으로 비교 횟수가 크게 다르다.
  4. 빅오는 데이터가 늘 때 시간이 늘어나는 모양이다. 큰 데이터에서는 알고리즘이 하드웨어보다 중요하다.
  5. 해시 테이블은 이름에서 칸 번호를 계산해 거의 한 번에 찾는다. 길 찾기는 그래프의 최단 경로 문제다.

확인 퀴즈

이진 탐색을 쓰기 위한 조건은?

가운데 값과 비교해 “앞쪽이냐 뒤쪽이냐”를 판단하려면 순서가 정해져 있어야 한다.

약 100만(≈2²⁰) 개의 정렬된 데이터에서 이진 탐색은 최악의 경우 몇 번 확인할까?

한 번에 절반씩 줄어들므로 2를 20번 곱하면 100만이 넘는다. 즉 20번이면 충분하다.

O(n²) 알고리즘에서 데이터가 3배가 되면 시간은?

3² = 9배. 그래서 큰 데이터에는 O(n log n) 같은 더 완만한 알고리즘이 필요하다.

해시 테이블에서 서로 다른 두 키가 같은 칸 번호를 받는 것을 무엇이라 하나?

충돌이 생기면 같은 칸에 줄을 세우거나 다른 빈칸을 찾는다. 칸 수가 넉넉하면 충돌이 줄어든다.

출발점에서 가까운 칸부터 차례로 살펴보는 BFS의 특징은?

물결처럼 거리 1, 2, 3… 순서로 퍼지므로 처음 목표에 닿는 순간의 경로가 가장 짧다. 대신 사방을 다 살펴 느릴 수 있다.