LeetCode 933. Number of Recent Calls

📊 결과


💻 내 코드

class RecentCounter {
    Deque<Integer> deque;
    int t;
    int count;
    public RecentCounter() {
        deque = new ArrayDeque<Integer>();
        t = 0;
        count = 0;
    }
    
    public int ping(int t) {
        deque.addLast(t);
        count++;
        
        this.t = t;
        while(!deque.isEmpty() && deque.peekFirst() < t-3000){
            deque.removeFirst();
            count--;
        }
        while(!deque.isEmpty() && deque.peekLast() > t){
            deque.removeLast();
            count--;
        }

        return count;
    }
}

📝 평가

✔ 잘한 점

  1. Deque 선택: 양쪽에서 삭제 가능한 자료구조 사용
  2. 슬라이딩 윈도우 개념: 3000ms 범위 유지 시도
  3. 오래된 요청 제거: 왼쪽에서 범위 밖 요청 제거
  4. 정답 도출: 모든 테스트 케이스 통과

✦ 개선점

  1. 불필요한 로직:

     while(!deque.isEmpty() && deque.peekLast() > t){
         deque.removeLast();
         count--;
     }
    
    • 이 부분은 절대 실행되지 않음!
    • 왜? t는 항상 증가하는 값 (문제 조건)
    • deque.peekLast()는 방금 추가한 t이므로 절대 t보다 클 수 없음
  2. 불필요한 변수들:

     int t;      // 사용 안 함
     int count;  // deque.size()로 대체 가능
    
  3. 변수 섀도잉:

     public int ping(int t) {  // 파라미터 t
         this.t = t;            // 멤버 변수 t
    
    • 혼란스러운 네이밍
  4. 메모리: count를 따로 관리할 필요 없이 deque.size() 사용 가능


✨ 최적화된 풀이

방법 1: 깔끔한 Queue 방식 (추천 ⭐⭐⭐⭐⭐)

class RecentCounter {
    private Queue<Integer> requests;
    
    public RecentCounter() {
        requests = new LinkedList<>();
    }
    
    public int ping(int t) {
        // 새 요청 추가
        requests.offer(t);
        
        // 3000ms 이전 요청 제거
        while (requests.peek() < t - 3000) {
            requests.poll();
        }
        
        return requests.size();
    }
}

개선 포인트:

시간복잡도: O(1) amortized (각 요청은 최대 1번 추가, 1번 제거) 공간복잡도: O(W) (W = 3000ms 내 요청 수, 최대 3000개)


방법 2: Deque 활용 (원본과 유사하지만 개선)

class RecentCounter {
    private Deque<Integer> deque;
    
    public RecentCounter() {
        deque = new ArrayDeque<>();
    }
    
    public int ping(int t) {
        deque.addLast(t);
        
        // 범위 밖 요청 제거
        while (!deque.isEmpty() && deque.peekFirst() < t - 3000) {
            deque.removeFirst();
        }
        
        return deque.size();
    }
}

방법 3: TreeMap 활용 (오버킬이지만 확장성)

class RecentCounter {
    private TreeMap<Integer, Integer> map;  // <time, count>
    
    public RecentCounter() {
        map = new TreeMap<>();
    }
    
    public int ping(int t) {
        map.put(t, map.getOrDefault(t, 0) + 1);
        
        // t-3000 미만 제거
        map.headMap(t - 3000, false).clear();
        
        return map.values().stream().mapToInt(Integer::intValue).sum();
    }
}

용도: 같은 시간에 여러 요청이 올 수 있는 경우


방법 4: 배열 기반 (메모리 최적화)

class RecentCounter {
    private int[] times;
    private int start, end;
    
    public RecentCounter() {
        times = new int[10000];  // 충분한 크기
        start = 0;
        end = 0;
    }
    
    public int ping(int t) {
        times[end++] = t;
        
        while (start < end && times[start] < t - 3000) {
            start++;
        }
        
        return end - start;
    }
}

장점: 객체 생성 오버헤드 없음


📊 성능 비교

방법 시간복잡도 공간복잡도 Runtime Memory 가독성 추천도
원본 코드 O(1) amortized O(W) 24ms 55.98MB ⭐⭐⭐ ⭐⭐⭐
Queue O(1) amortized O(W) 18-20ms 54MB ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Deque O(1) amortized O(W) 18-20ms 54MB ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
TreeMap O(log W) O(W) 30-40ms 56MB ⭐⭐⭐ ⭐⭐
배열 O(1) amortized O(1) 고정 16-18ms 53MB ⭐⭐⭐ ⭐⭐⭐⭐

W = 3000ms 윈도우 내 요청 수 (최대 3000개)


💡 핵심 인사이트

문제 이해

요구사항:

예시:

RecentCounter counter = new RecentCounter();
counter.ping(1);     // [1] → 1
counter.ping(100);   // [1, 100] → 2
counter.ping(3001);  // [100, 3001] → 2 (1은 범위 밖)
counter.ping(3002);  // [100, 3001, 3002] → 3

슬라이딩 윈도우 패턴

시간 →
     ┌────── 3000ms ──────┐
     │                    │
┌────┴────┬────┬────┬─────▼──┬────┐
│ 1      │100 │3001│3002   │    │
└─────────┴────┴────┴────────┴────┘
  제거      유지  유지  현재

ping(3002) 호출 시:
- 1은 3002-3000=2보다 작음 → 제거
- 100, 3001은 범위 내 → 유지
- 3002 추가
→ 총 3개

배운 점

  1. 문제 조건 활용

     문제: ping 호출은 strictly increasing order
     즉, t는 항상 증가
        
     → deque의 뒤쪽은 확인할 필요 없음!
     → 오른쪽 제거 로직 불필요
    
  2. 적절한 자료구조

     Queue: 앞에서 제거, 뒤에 추가 → 완벽!
     Deque: 양쪽 작업 가능하지만 한쪽만 사용
     Array: 메모리 효율적이지만 크기 제한
    
  3. 불필요한 상태 관리 피하기

     // Bad: 수동 카운트 관리
     int count;
     count++;
     count--;
        
     // Good: 자료구조의 메서드 활용
     return queue.size();
    
  4. Amortized O(1) 이해

     각 요청은:
     - 정확히 1번 추가
     - 최대 1번 제거
        
     n개 요청 → 총 2n번 연산 → O(n)
     평균 → O(1) per request
    

핵심 개념

슬라이딩 윈도우(Sliding Window):

고정된 범위를 유지하며 이동
- 범위 밖 = 제거
- 새 요청 = 추가

Queue의 특성:

FIFO (First In First Out)
- 오래된 요청이 앞에
- 새 요청이 뒤에
→ 자연스럽게 시간 순서 유지

🎯 개선 후 코드

추천: Queue 방식 (가장 간결하고 명확)

class RecentCounter {
    private Queue<Integer> requests;
    
    public RecentCounter() {
        requests = new LinkedList<>();
    }
    
    public int ping(int t) {
        // 새 요청 추가
        requests.offer(t);
        
        // 3000ms 이전의 요청들 제거
        // t는 항상 증가하므로 앞에서만 제거
        while (requests.peek() < t - 3000) {
            requests.poll();
        }
        
        // 현재 윈도우 내 요청 수
        return requests.size();
    }
}

/**
 * 시간복잡도: O(1) amortized
 * 공간복잡도: O(W) where W ≤ 3000
 * 
 * Runtime: 18-20ms (상위 85%)
 * Memory: 54MB
 */

개선 효과:


📚 관련 개념

알고리즘 패턴

연관 개념

  1. 시간 윈도우 문제

    • 최근 N초 내 이벤트
    • 이동 평균(Moving Average)
    • 속도 제한(Rate Limiting)
  2. 실무 활용

    • API Rate Limiting: 분당 요청 수 제한
    • 로그 분석: 최근 N분간 에러 수
    • 모니터링: 실시간 메트릭 집계
    • 게임: 스킬 쿨다운 관리
    • 네트워크: 패킷 전송률 계산
  3. 관련 자료구조

    • Circular Buffer
    • Time Series Database
    • Sliding Window Counter

다음 단계

비슷한 문제

  1. LeetCode 346. Moving Average from Data Stream ⭐ (Premium)

    • 슬라이딩 윈도우로 평균 계산
  2. LeetCode 362. Design Hit Counter ⭐⭐ (Premium)

    • 최근 5분간 히트 수 계산
  3. LeetCode 353. Design Snake Game ⭐⭐ (Premium)

    • Queue로 뱀의 몸 관리
  4. LeetCode 239. Sliding Window Maximum ⭐⭐⭐⭐

    • 슬라이딩 윈도우 + Deque
  5. LeetCode 1429. First Unique Number ⭐⭐ (Premium)

    • Queue + HashSet

심화 학습

  1. Rate Limiter 구현: Token Bucket, Leaky Bucket
  2. Time Series 데이터베이스 동작 원리
  3. Circular Buffer 구현

연습 포인트 체크리스트


🔍 동작 시각화

상세 예시

RecentCounter counter = new RecentCounter();

ping(1):
  queue: [1]
  range: [1-3000, 1] = [-2999, 1]
  count: 1

ping(100):
  queue: [1, 100]
  range: [100-3000, 100] = [-2900, 100]
  count: 2

ping(3001):
  queue: [1, 100, 3001]
  1 < 3001-3000 = 1  1 제거!
  queue: [100, 3001]
  range: [1, 3001]
  count: 2

ping(3002):
  queue: [100, 3001, 3002]
  100 >= 3002-3000 = 2  유지
  range: [2, 3002]
  count: 3

ping(6000):
  queue: [100, 3001, 3002, 6000]
  100 < 3000  제거
  3001 < 3000  제거
  3002 < 3000  제거
  queue: [6000]
  range: [3000, 6000]
  count: 1

💭 원본 코드의 문제점 상세 분석

1. 불필요한 뒤쪽 제거 로직

// ❌ 절대 실행되지 않는 코드
while(!deque.isEmpty() && deque.peekLast() > t){
    deque.removeLast();
    count--;
}

왜 실행 안 되나?

deque.addLast(t);  // 방금 t를 추가했음
// deque.peekLast() == t
// t > t는 항상 false!

증명:


2. 불필요한 count 관리

// Bad
int count;
deque.addLast(t);
count++;
deque.removeFirst();
count--;
return count;

// Good
deque.addLast(t);
deque.removeFirst();
return deque.size();  // 자동 관리!

🏷️ Keywords

#Queue #SlidingWindow #TimeWindow #DataStream #FIFO #Deque #슬라이딩윈도우 #시계열데이터 #RateLimiting #RecentCounter #AmortizedO1 #EventProcessing
#LeetCodeEasy #코딩테스트 #실시간처리 #윈도우관리