Deep DiveJLS 21 정수 연산·Java 배열/JVMS 21을 대조하고 SlidingWindowDemo의 고정·가변·빈 입력·실패 계약을 OpenJDK 21.0.11에서 실행
검증 근거 보기

JLS 21 정수 연산·Java 배열/JVMS 21을 대조하고 SlidingWindowDemo의 고정·가변·빈 입력·실패 계약을 OpenJDK 21.0.11에서 실행

슬라이딩 윈도: 연속 구간의 중복 계산과 단조성 경계를 분리하기

고정 길이 합과 양수 배열의 가변 길이 창을 분리하고, 창 상태 불변식·음수 반례·long 누적합·stream buffer 비용을 Java로 검증한다.

On this page목차 11원문 Markdown ↗

길이 kk인 모든 연속 구간 합을 매번 처음부터 더하면 인접한 두 구간의 k1k-1개 원소를 중복 계산한다. 슬라이딩 윈도는 나가는 값과 들어오는 값만 반영해 이 중복을 제거한다. 다만 가변 길이 창은 값의 부호 같은 추가 단조성이 필요하다.

1. 두 문제의 계약을 섞지 않는다

  1. maxFixedWindowSum(values,width): 정확히 width개인 연속 구간 중 최대 합을 반환한다. width가 0 이하이거나 배열보다 크면 계약 위반이다.
  2. minLengthAtLeast(positive,target): 모든 값이 양수일 때 합이 target 이상인 가장 짧은 연속 구간 길이를 반환한다. 없으면 0이다.

고정 창은 음수도 허용한다. 가변 창은 음수가 있으면 left를 줄였을 때 합이 오히려 커질 수 있어 현재 이동 규칙이 무너진다. 이름이 같은 패턴이라고 전제를 공유하지 않는다.

2. 고정 창 상태 전이

[2,-1,3,5,-2], width 3을 본다.

창 index원소이전 합에서 제거/추가현재 합best
[0,3)2,-1,3초기 3개 합44
[1,4)-1,3,5-2 +577
[2,5)3,5,-2-(-1) -267

불변식: right를 처리한 뒤 current는 정확히 마지막 width개 값의 합이고 best는 지금까지 완성된 모든 width 창의 최대 합이다. 이전 창에서 빠지는 values[right-width]를 빼고 새 값을 더하면 겹치는 원소를 다시 더하지 않아도 정확하다.

3. 양수 가변 창의 이동 증명

[2,3,1,2,4,3], target 7에서 right를 늘리며 합을 키운다. 합이 7 이상이면 left를 오른쪽으로 옮겨 가능한 한 줄인다. 모든 값이 양수이므로 right를 고정했을 때 left를 옮길수록 합은 단조 감소한다.

불변식: 내부 while 종료 후 현재 window 합은 target보다 작고, 그 직전에 제거한 left를 포함한 창들은 해당 right에서 가능한 target 이상 후보로 평가됐다. 각 left/right가 되돌아가지 않으므로 모든 최소 후보를 놓치지 않는다.

음수 반례 [2,-1,2], target 3에서는 합이 target 미만이라고 right만 늘리는 규칙의 의미가 흔들리고, left 제거가 합을 키울 수도 있다. 일반 정수 배열의 shortest subarray는 prefix sum과 monotonic deque 같은 다른 알고리즘이 필요하다.

실행 재현

아래 코드는 설명용 조각이 아니라 main에 정상·빈 입력·중복·경계·실패 계약을 함께 넣은 완전한 Java 21 프로그램이다.

public final class SlidingWindowDemo {
    static long maxFixedWindowSum(int[] values, int width) {
        if (values == null) throw new IllegalArgumentException("values must not be null");
        if (width <= 0 || width > values.length) throw new IllegalArgumentException("invalid width");
        long current = 0;
        for (int i = 0; i < width; i++) current += values[i];
        long best = current;
        for (int right = width; right < values.length; right++) {
            current += values[right];
            current -= values[right - width];
            best = Math.max(best, current);
        }
        return best;
    }

    static int minLengthAtLeast(int[] positive, long target) {
        if (positive == null) throw new IllegalArgumentException("positive must not be null");
        if (target <= 0) throw new IllegalArgumentException("target must be positive");
        long sum = 0;
        int left = 0;
        int best = Integer.MAX_VALUE;
        for (int right = 0; right < positive.length; right++) {
            if (positive[right] <= 0) throw new IllegalArgumentException("all values must be positive");
            sum += positive[right];
            while (sum >= target) {
                best = Math.min(best, right - left + 1);
                sum -= positive[left++];
            }
        }
        return best == Integer.MAX_VALUE ? 0 : best;
    }

    static void check(boolean condition, String message) {
        if (!condition) throw new AssertionError(message);
    }

    public static void main(String[] args) {
        check(maxFixedWindowSum(new int[] {2, -1, 3, 5, -2}, 3) == 7, "fixed");
        check(minLengthAtLeast(new int[] {2, 3, 1, 2, 4, 3}, 7) == 2, "variable");
        check(minLengthAtLeast(new int[0], 7) == 0, "empty");
        System.out.println("fixed max=7, variable minLength=2, missing=0");
    }
}
javac --release 21 SlidingWindowDemo.java
java SlidingWindowDemo

직접 실행한 출력은 다음과 같다.

fixed max=7, variable minLength=2, missing=0

컴파일러와 런타임은 javac 21.0.11, OpenJDK 21.0.11을 사용했다. assert 옵션에 의존하지 않고 실패 시 AssertionError를 던지므로 위 명령 그대로 검증된다.

4. 누적합 overflow와 실패 입력

width가 크면 int 원소의 합이 int 범위를 쉽게 넘는다. long currentint를 더해 64-bit로 계산한다. 그래도 데이터 규모가 long 범위를 넘을 수 있는 도메인이면 Math.addExact나 더 큰 수 타입, overflow 정책이 필요하다.

가변 method는 non-positive 원소를 조용히 처리하지 않고 예외로 거절한다. 잘못된 전제에서 그럴듯한 답을 반환하는 것보다 failure를 계약으로 드러내는 편이 안전하다.

5. 복잡도 유도

고정 창은 초기 width개를 더하고 나머지 nwidthn-width개를 한 번 처리하므로 Θ(n)\Theta(n)이다. 가변 창은 right가 nn번, left가 최대 nn번 움직여 합계 최대 2n2n회이므로 O(n)O(n)이다. 중첩 while이 보여도 pointer가 되돌아가지 않아 O(n2)O(n^2)가 아니다.

배열 batch 구현의 보조 공간은 O(1)O(1)이다. 실제 stream에서 나가는 값을 빼려면 최근 width개를 ring buffer에 보관해야 하므로 O(width)O(width) 메모리가 필요하다. 입력 배열을 이미 가진 경우와 streaming state를 구분한다.

6. JVM과 allocation

예제 loop는 primitive long/int만 갱신하고 새 객체를 만들지 않는다. 입력 int[]는 heap에 있고 local references/index는 frame 관점이다. stream API로 매 원소를 boxing하거나 window subList를 매번 만들면 논리상 O(n)O(n)이어도 allocation과 GC pressure가 커질 수 있다. JFR로 allocation source를 확인한다.

7. prefix sum·two pointers·deque와 비교

선택질의/전제시간상태
고정 sliding sum한 번 전체 scanO(n)O(n)O(1)O(1) batch
prefix sum여러 range-sum 질의build O(n)O(n), query O(1)O(1)O(n)O(n)
양수 가변 window합 단조성O(n)O(n)O(1)O(1)
monotonic dequewindow min/max 또는 일반 prefix 조건O(n)O(n)O(n)O(n) 최악

반복 range query라면 prefix sum 구축이 낫고, 모든 window의 최댓값 원소를 구하려면 합 한 개가 아니라 monotonic deque가 필요하다.

8. 실제 도메인 적용

rate-limit의 최근 60초 요청, telemetry moving aggregate, stream anomaly window에 쓸 수 있다. 그러나 processing-time과 event-time을 구분하고 late/out-of-order event, watermark, state TTL을 설계해야 한다. 관측 지표는 active windows, state bytes, late-event ratio, eviction lag, overflow/rejection이다.

9. 이건 피한다

  • 음수 배열에 양수 전용 가변 window를 쓴다. 합의 단조성이 깨져 정답을 놓친다. prefix sum+deque 등 계약에 맞는 알고리즘을 쓴다.
  • 매 창을 copyOfRange로 만든다. O(nk)O(nk) 복사와 allocation을 만든다. 나가는 값/들어오는 값만 갱신한다.
  • 합을 int에 둔다. 큰 window에서 overflow가 최대 비교를 왜곡한다. long 또는 명시적 overflow 정책을 쓴다.
  • batch 보조 공간 O(1)O(1)을 stream에도 그대로 쓴다. 나가는 값을 기억할 buffer가 필요하다. window state와 TTL을 산정한다.
  • width 오류를 0으로 반환한다. 실제 최대합 0과 계약 위반이 섞인다. 예외나 명시적 result로 구분한다.

Reference

대화

댓글

0
댓글을 불러오는 중입니다.