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;
}
}
불필요한 로직:
while(!deque.isEmpty() && deque.peekLast() > t){
deque.removeLast();
count--;
}
t는 항상 증가하는 값 (문제 조건)deque.peekLast()는 방금 추가한 t이므로 절대 t보다 클 수 없음불필요한 변수들:
int t; // 사용 안 함
int count; // deque.size()로 대체 가능
변수 섀도잉:
public int ping(int t) { // 파라미터 t
this.t = t; // 멤버 변수 t
메모리: count를 따로 관리할 필요 없이 deque.size() 사용 가능
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개)
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();
}
}
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();
}
}
용도: 같은 시간에 여러 요청이 올 수 있는 경우
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개)
요구사항:
ping(t) 호출 시, 최근 3000ms 내의 요청 수 반환[t-3000, t] 범위의 요청 개수예시:
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개
문제 조건 활용
문제: ping 호출은 strictly increasing order
즉, t는 항상 증가
→ deque의 뒤쪽은 확인할 필요 없음!
→ 오른쪽 제거 로직 불필요
적절한 자료구조
Queue: 앞에서 제거, 뒤에 추가 → 완벽!
Deque: 양쪽 작업 가능하지만 한쪽만 사용
Array: 메모리 효율적이지만 크기 제한
불필요한 상태 관리 피하기
// Bad: 수동 카운트 관리
int count;
count++;
count--;
// Good: 자료구조의 메서드 활용
return queue.size();
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
*/
개선 효과:
시간 윈도우 문제
실무 활용
관련 자료구조
LeetCode 346. Moving Average from Data Stream ⭐ (Premium)
LeetCode 362. Design Hit Counter ⭐⭐ (Premium)
LeetCode 353. Design Snake Game ⭐⭐ (Premium)
LeetCode 239. Sliding Window Maximum ⭐⭐⭐⭐
LeetCode 1429. First Unique Number ⭐⭐ (Premium)
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
// ❌ 절대 실행되지 않는 코드
while(!deque.isEmpty() && deque.peekLast() > t){
deque.removeLast();
count--;
}
왜 실행 안 되나?
deque.addLast(t); // 방금 t를 추가했음
// deque.peekLast() == t
// t > t는 항상 false!
증명:
t는 항상 증가 (문제 조건: strictly increasing)deque.peekLast()는 가장 최근에 추가된 값tdeque.peekLast() > t는 항상 거짓// Bad
int count;
deque.addLast(t);
count++;
deque.removeFirst();
count--;
return count;
// Good
deque.addLast(t);
deque.removeFirst();
return deque.size(); // 자동 관리!
#Queue #SlidingWindow #TimeWindow #DataStream #FIFO #Deque #슬라이딩윈도우
#시계열데이터 #RateLimiting #RecentCounter #AmortizedO1 #EventProcessing
#LeetCodeEasy #코딩테스트 #실시간처리 #윈도우관리