Workspace IndexAlgorithms › Day 8

Order Statistics TODO

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

Concept

An order statistic is the k-th smallest element an array would have if sorted, and the problem of finding just that element without sorting everything is called the selection problem. Quickselect reuses quicksort's partition step but only recurses into the side that contains the pivot, giving expected linear time — though a consistently bad pivot choice can degrade it to quadratic time in the worst case. Median-of-medians groups elements into blocks of 5, takes the median of each group, and then uses the median of those medians as the pivot, which guarantees that a fixed fraction of elements is eliminated at every step. That guarantee makes the recurrence solve to linear time even in the worst case, but the constant factor is large enough that it's slow in practice. So real implementations typically start with Quickselect and fall back to the deterministic median-of-medians selection once recursion depth crosses a threshold — the introselect pattern.

When you're computing p99 latency or pulling the top N items, sorting the entire array pays an unnecessary log-factor tax, while a naive Quickselect can be dragged into its worst case by adversarial input.

Code & Formula

# Day 8: 순서 통계 — Quickselect로 k번째로 작은 원소를 전체 정렬 없이 찾기
# 퀵정렬의 분할을 재사용하되 필요한 한쪽 구간만 재귀해 기대 O(n) 시간에 선택한다.

import random

def quickselect(arr, k):
    """arr에서 k번째로 작은 원소(0-indexed)를 반환."""
    arr = arr[:]
    lo, hi = 0, len(arr) - 1
    while True:
        if lo == hi:
            return arr[lo]
        pivot = arr[random.randint(lo, hi)]
        lt, gt, i = lo, hi, lo
        while i <= gt:
            if arr[i] < pivot:
                arr[i], arr[lt] = arr[lt], arr[i]
                lt += 1; i += 1
            elif arr[i] > pivot:
                arr[i], arr[gt] = arr[gt], arr[i]
                gt -= 1
            else:
                i += 1
        if k < lt:
            hi = lt - 1
        elif k > gt:
            lo = gt + 1
        else:
            return pivot

random.seed(3)
data = [random.randint(0, 1000) for _ in range(1000)]
k = 500
result = quickselect(data, k)
expected = sorted(data)[k]
print(f"데이터 {len(data)}개 중 {k}번째(0-indexed)로 작은 값 = {result}")
print(f"sorted()로 검증한 값 = {expected}, 일치 = {result == expected}")

Exercise

Implement Quickselect to find the k-th element of an integer array, then count comparisons on a sorted input and an all-equal-values input, and plot those against a random-pivot version.

Practical Connection

Whether you're computing the median or a quantile of gas prices or order-book fill prices on-chain or off-chain, using a selection algorithm instead of sorting cuts the work and lets you state an explicit worst-case time bound against adversarial input.

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 8 / 100 · A. 고급 알고리즘·자료구조 (Day 1–19)

Quickselect·중위수의 중위수(결정론적 선택)

개념

순서 통계란 배열을 정렬했을 때 k번째로 작은 원소를 뜻하며, 전체 정렬 없이 그 원소만 찾는 문제를 선택 문제라 한다. Quickselect는 퀵정렬의 분할을 재사용하되 피벗이 속한 한쪽 구간만 재귀하므로 기대 시간이 선형이지만, 피벗이 계속 치우치면 최악에는 제곱 시간이 된다. 중위수의 중위수는 원소를 5개씩 묶어 각 그룹의 중위수를 구하고 그 중위수들의 중위수를 피벗으로 삼아, 매 단계에서 일정 비율 이상의 원소가 확실히 제거되도록 보장한다. 이 보장 덕분에 재귀식이 선형으로 풀려 최악에도 선형 시간이 되지만 상수 계수가 커서 실무에서는 느리다. 그래서 실제 구현은 보통 Quickselect로 시작해 재귀 깊이가 임계치를 넘으면 결정론적 선택으로 전환하는 introselect 방식을 쓴다.

p99 지연을 계산하거나 상위 N개를 뽑는 작업에서 전체 정렬을 돌리면 불필요한 로그 배수를 지불하게 되고, 순진한 Quickselect는 적대적 입력에서 최악 케이스로 끌려갈 수 있다.

코드 · 수식

# Day 8: 순서 통계 — Quickselect로 k번째로 작은 원소를 전체 정렬 없이 찾기
# 퀵정렬의 분할을 재사용하되 필요한 한쪽 구간만 재귀해 기대 O(n) 시간에 선택한다.

import random

def quickselect(arr, k):
    """arr에서 k번째로 작은 원소(0-indexed)를 반환."""
    arr = arr[:]
    lo, hi = 0, len(arr) - 1
    while True:
        if lo == hi:
            return arr[lo]
        pivot = arr[random.randint(lo, hi)]
        lt, gt, i = lo, hi, lo
        while i <= gt:
            if arr[i] < pivot:
                arr[i], arr[lt] = arr[lt], arr[i]
                lt += 1; i += 1
            elif arr[i] > pivot:
                arr[i], arr[gt] = arr[gt], arr[i]
                gt -= 1
            else:
                i += 1
        if k < lt:
            hi = lt - 1
        elif k > gt:
            lo = gt + 1
        else:
            return pivot

random.seed(3)
data = [random.randint(0, 1000) for _ in range(1000)]
k = 500
result = quickselect(data, k)
expected = sorted(data)[k]
print(f"데이터 {len(data)}개 중 {k}번째(0-indexed)로 작은 값 = {result}")
print(f"sorted()로 검증한 값 = {expected}, 일치 = {result == expected}")

연습

정수 배열에서 k번째 원소를 찾는 Quickselect를 직접 구현한 뒤, 정렬된 입력과 모두 같은 값인 입력에서 비교 횟수를 세어 무작위 피벗 버전과 그래프로 비교해 보라.

실무 · Verex 연결

가스 가격이나 오더북 체결가의 중위수·분위수를 온체인 오프체인 어느 쪽에서 계산하든, 정렬 대신 선택 알고리즘을 쓰면 연산량이 줄고 적대적 입력에 대한 최악 시간 상한을 명시할 수 있다.

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

← 7. 스트리밍/스케치 알고리즘9. 문자열 인덱스 →