에듀테크 프로젝트 대시보드
원형 버퍼 (Ring Buffer)
고정 크기 배열 하나로 큐를 만든다. 끝에 도달한 포인터가 0번으로 되돌아오면서(wrap-around) 배열을 원형처럼 재사용한다. 데이터를 옮기지 않고 넣고 빼기가 모두 O(1).
보통 배열 큐는 앞에서 빼면 빈 칸이 생기고, 그 칸을 메우려면 뒤 원소를 전부 한 칸씩 당겨야 한다(O(n)). 아니면 큐가 배열 오른쪽으로 계속 밀려나 결국 공간이 모자란다.
원형 버퍼는 칸을 당기지 않는다. 대신 쓰는 위치(head) 와 읽는 위치(tail) 두 개의 인덱스만 옮기고, 배열 끝을 넘으면 % 용량 으로 0번으로 되돌린다. 물리적으로는 일직선 배열이지만, 인덱스 계산 덕분에 논리적으로는 고리가 된다.
# 다음 위치는 항상 이렇게 구한다 head = (head + 1) % capacity tail = (tail + 1) % capacity
용량 8짜리 버퍼다. enqueue 는 head 위치에 값을 쓰고 head를 한 칸 전진, dequeue 는 tail 위치의 값을 꺼내고 tail을 한 칸 전진한다. 두 포인터가 배열 끝(7)을 지나면 0으로 감긴다.
H = head(쓰기) · T = tail(읽기)
포인터만 보면 함정이 있다. 버퍼가 완전히 비었을 때도 head == tail, 완전히 찼을 때도 head == tail 이 된다. 같은 신호가 정반대 상황을 뜻하는 것이다. 흔한 해법 세 가지:
| 방법 | 내용 |
|---|---|
| 개수 세기 | size 변수를 따로 둔다. size == 0 이면 empty, size == capacity 면 full. (이 데모가 쓰는 방식) |
| 한 칸 비우기 | 칸 하나를 항상 낭비한다. (head+1)%cap == tail 이면 full로 간주. 변수 없이 포인터만으로 판별. |
| 감기지 않은 인덱스 | head·tail을 계속 증가시키고 접근할 때만 % cap. head - tail 이 곧 size, == cap 이면 full. |
dmesg 링 로그.class RingBuffer: def __init__(self, capacity): self.buf = [None] * capacity self.cap = capacity self.head = 0 # 다음에 쓸 칸 self.tail = 0 # 다음에 읽을 칸 self.size = 0 def enqueue(self, x): if self.size == self.cap: raise IndexError("buffer full") # 또는 덮어쓰기 self.buf[self.head] = x self.head = (self.head + 1) % self.cap self.size += 1 def dequeue(self): if self.size == 0: raise IndexError("buffer empty") x = self.buf[self.tail] self.buf[self.tail] = None self.tail = (self.tail + 1) % self.cap self.size -= 1 return x
파이썬 표준 라이브러리의 collections.deque(maxlen=N) 이 바로 덮어쓰기형 원형 버퍼다.
% 용량. 데이터를 절대 옮기지 않고 넣기·빼기가 O(1). 끝이 처음으로 이어져 공간을 재활용하는, 스트리밍과 버퍼링의 기본 뼈대.