한양과학기술고등학교

에듀테크 프로젝트 대시보드

접미사 배열 (Suffix Array)

접미사 배열 (Suffix Array)

긴 글 하나를 한 번만 전처리해 두면, 그 안에서 어떤 단어든 O(m log n) 만에 찾을 수 있다. 유전체(DNA) 검색, 전문(full-text) 검색 엔진, 표절 탐지의 뼈대가 되는 문자열 자료구조.

핵심 아이디어: 문자열의 모든 접미사를 사전순으로 정렬해 시작 위치만 배열로 저장한다. 같은 접두사로 시작하는 접미사들은 정렬 후 딱 붙어 있으므로, 패턴 검색이 "정렬된 배열에서 이분 탐색" 문제로 바뀐다.

1. 접미사란?

문자열 S = "banana" 의 접미사는 각 위치에서 끝까지 잘라낸 조각이다. 길이 n 이면 접미사도 n 개.

i=0  banana
i=1  anana
i=2  nana
i=3  ana
i=4  na
i=5  a

관례상 맨 끝에 그 무엇보다 작은 특수문자 $ 를 붙인다. 그러면 어떤 접미사도 다른 접미사의 접두사가 되지 않아 정렬이 깔끔해진다.

2. 접미사 배열 = 접미사를 정렬한 순서

접미사 문자열을 통째로 저장하면 O(n²) 메모리다. 대신 정렬된 순서의 시작 인덱스만 배열 SA 에 담는다. "banana$" 의 결과:

정렬된 접미사        SA[k]
$                    6
a$                   5
ana$                 3
anana$               1
banana$              0
na$                  4
nana$                2

SA = [6, 5, 3, 1, 0, 4, 2]

아래 표: 왼쪽은 입력 순서, 오른쪽은 사전순 정렬(= 접미사 배열). LCP = 바로 위 접미사와 공유하는 접두사 길이.

입력 순서

i접미사

정렬된 순서 (SA)

kSA[k]접미사LCP

3. 패턴 검색 — 정렬 배열에서 이분 탐색

패턴 P 로 시작하는 접미사는 정렬된 SA 안에서 연속 구간을 이룬다. 그 구간의 왼쪽 끝(lower bound) 과 오른쪽 끝(upper bound) 을 각각 이분 탐색으로 찾으면, 구간에 든 SA 값들이 곧 모든 등장 위치다. 비교 한 번에 최대 m 글자를 보므로 O(m log n).

kSA[k]접미사
패턴을 입력하고 "검색 실행"을 누르세요.

4. LCP 배열 — 이웃 접미사의 공통 접두사

LCP[k] 는 정렬 후 k 번째와 k−1 번째 접미사가 공유하는 가장 긴 공통 접두사 길이다. 이 배열 하나로 여러 문제가 즉답이 된다.

현재 문자열의 LCP 하이라이트

5. 만드는 법과 복잡도

방법구축 시간메모
단순 정렬O(n² log n)접미사 n개를 문자열 비교(최대 n글자)로 sort. 짧은 글엔 충분.
접두사 배가법
(prefix doubling)
O(n log² n)길이 1 → 2 → 4 … 접두사 기준 순위를 매기며 두 배씩. 구현이 간결해 대회 단골.
SA-IS / DC3O(n)선형 시간. 실제 검색 엔진·유전체 도구가 쓰는 방식.

Python — 단순 버전 + 이분 탐색

def build_suffix_array(s):
    s += "$"
    # (접미사, 시작 인덱스) 를 사전순 정렬 → 인덱스만 추출
    return [i for _, i in sorted((s[i:], i) for i in range(len(s)))]

def search(s, sa, p):
    # p 로 시작하는 접미사들의 [lo, hi) 구간을 이분 탐색
    lo, hi, n = 0, len(sa), len(sa)
    while lo < hi:                 # lower bound
        mid = (lo + hi) // 2
        if s[sa[mid]:sa[mid]+len(p)] < p: lo = mid + 1
        else: hi = mid
    start = lo
    hi = n
    while lo < hi:                 # upper bound
        mid = (lo + hi) // 2
        if s[sa[mid]:sa[mid]+len(p)] <= p: lo = mid + 1
        else: hi = mid
    return sorted(sa[start:lo])   # 모든 등장 위치

sa = build_suffix_array("banana")
print(sa)                        # [6, 5, 3, 1, 0, 4, 2]
print(search("banana$", sa, "ana"))  # [1, 3]

6. 다른 자료구조와 비교

메모리다중 패턴 검색비고
KMPO(m)패턴마다 O(n) 재스캔텍스트가 계속 바뀔 때 유리
트라이 / 접미사 트리O(n) 지만 상수 큼O(m)가장 빠르지만 포인터가 많아 무겁다
접미사 배열정수 n개O(m log n)캐시 친화적, 고정 텍스트에 최적
어디에 쓰이나 — BWA · Bowtie 유전체 정렬, bzip2 의 버로우즈–휠러 변환 계산, 코드 표절 탐지, 전문 검색의 접미사 인덱스. "한 번 만들고 수없이 질의하는" 상황이면 거의 항상 후보다.