Workspace IndexAlgorithms › Day 9

String Indexing TODO

Algorithms · Day 9 / 100 · A. Advanced Algorithms & Data Structures (Day 1-19)

Concept

A suffix array is an index that sorts all suffixes of a string lexicographically and stores just their starting positions in an array. Because it's sorted, an arbitrary pattern search can be done by binary search, and pairing it with an LCP array — the longest common prefix length between adjacent suffixes — lets you answer queries like repeated substrings or the count of distinct substrings in near-linear time. A suffix automaton (DAWG) encodes the same information as a state machine: it's the minimal deterministic automaton that recognizes every substring of the string, and its state count stays linear in the input length. An automaton can be built online, one character at a time, which makes it well suited to streaming, while a suffix array's compact memory footprint favors large static text. What both share at their core is turning "scan for the pattern every time" into "preprocess the text once and answer queries in constant or logarithmic time."

When the text is fixed — logs, traces — but search happens over and over, a grep-style linear scan turns straight into a cost the moment the data grows.

Code & Formula

# Day 9: 문자열 인덱스 — 서픽스 배열로 부분 문자열 검색을 이분 탐색으로 처리
# 모든 접미사를 정렬해 배열로 두면, 패턴 검색이 선형 스캔 대신 O(log n · m)에 끝난다.

def build_suffix_array(text):
    return sorted(range(len(text)), key=lambda i: text[i:])

def search(text, sa, pattern):
    lo, hi = 0, len(sa)
    while lo < hi:
        mid = (lo + hi) // 2
        if text[sa[mid]:sa[mid] + len(pattern)] < pattern:
            lo = mid + 1
        else:
            hi = mid
    if lo == len(sa) or text[sa[lo]:sa[lo] + len(pattern)] != pattern:
        return []
    hits, i = [sa[lo]], lo + 1
    while i < len(sa) and text[sa[i]:sa[i] + len(pattern)] == pattern:
        hits.append(sa[i]); i += 1
    return sorted(hits)

text = "the quick brown fox jumps over the lazy dog the fox runs"
sa = build_suffix_array(text)
print("서픽스 배열(앞 10개 시작 위치) =", sa[:10])
for pattern in ["fox", "the", "cat"]:
    print(f"search('{pattern}') -> 위치 {search(text, sa, pattern)}")

Exercise

Build a suffix array plus LCP array for a 10MB log file, then run the same 1,000 substring queries via plain linear scan versus binary search over the suffix array, and compare the timings.

Practical Connection

When you're repeatedly searching node logs or transaction traces for a specific address, function selector, or event signature, building the index once is operationally far more stable than doing a full scan every time.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

문자열 인덱스 TODO

Algorithms · Day 9 / 100 · A. 고급 알고리즘·자료구조 (Day 1–19)

서픽스 배열·오토마톤 (로그·트레이스 검색)

개념

서픽스 배열은 문자열의 모든 접미사를 사전순으로 정렬한 뒤 그 시작 위치만 배열로 보관하는 인덱스이다. 정렬돼 있으므로 임의의 패턴 검색을 이분 탐색으로 처리할 수 있고, 인접한 접미사의 공통 접두사 길이를 담은 LCP 배열을 함께 두면 반복 부분문자열, 서로 다른 부분문자열 개수 같은 질의도 선형에 가깝게 풀린다. 서픽스 오토마톤(DAWG)은 같은 정보를 상태 기계로 표현한 것으로, 문자열의 모든 부분문자열을 인식하는 최소 결정적 오토마톤이며 상태 수가 입력 길이에 선형으로 유지된다. 오토마톤은 온라인으로 한 글자씩 추가하며 만들 수 있어 스트리밍에 유리하고, 서픽스 배열은 메모리가 조밀해 대용량 정적 텍스트에 유리하다. 둘 다 "패턴을 매번 스캔한다"를 "텍스트를 한 번 전처리하고 질의는 상수·로그 시간에 답한다"로 바꾸는 것이 본질이다.

로그·트레이스처럼 텍스트는 고정돼 있는데 검색은 수없이 반복되는 상황에서, grep식 선형 스캔은 데이터가 커지는 순간 그대로 비용이 된다.

코드 · 수식

# Day 9: 문자열 인덱스 — 서픽스 배열로 부분 문자열 검색을 이분 탐색으로 처리
# 모든 접미사를 정렬해 배열로 두면, 패턴 검색이 선형 스캔 대신 O(log n · m)에 끝난다.

def build_suffix_array(text):
    return sorted(range(len(text)), key=lambda i: text[i:])

def search(text, sa, pattern):
    lo, hi = 0, len(sa)
    while lo < hi:
        mid = (lo + hi) // 2
        if text[sa[mid]:sa[mid] + len(pattern)] < pattern:
            lo = mid + 1
        else:
            hi = mid
    if lo == len(sa) or text[sa[lo]:sa[lo] + len(pattern)] != pattern:
        return []
    hits, i = [sa[lo]], lo + 1
    while i < len(sa) and text[sa[i]:sa[i] + len(pattern)] == pattern:
        hits.append(sa[i]); i += 1
    return sorted(hits)

text = "the quick brown fox jumps over the lazy dog the fox runs"
sa = build_suffix_array(text)
print("서픽스 배열(앞 10개 시작 위치) =", sa[:10])
for pattern in ["fox", "the", "cat"]:
    print(f"search('{pattern}') -> 위치 {search(text, sa, pattern)}")

연습

10MB짜리 로그 파일에 대해 서픽스 배열 + LCP를 만들고, 동일한 부분문자열 질의 1000개를 단순 스캔과 이분 탐색으로 각각 돌려 시간을 비교해 보기.

실무 · Verex 연결

노드 로그나 트랜잭션 트레이스에서 특정 주소·셀렉터·이벤트 시그니처를 반복 검색할 때, 인덱스를 한 번 세워 두는 쪽이 매번 전체 스캔하는 것보다 운영상 훨씬 안정적이다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 8. 순서 통계10. 세그먼트 트리 심화 →