Circular queue (ring buffer) adalah queue berbasis array yang menggunakan trik modulo agar slot yang sudah di-dequeue dapat dipakai ulang tanpa menggeser elemen. Tanpa trik ini, queue array statis menjadi O(n) per dequeue (karena geser) atau memori membengkak (karena slot tidak pernah dipakai ulang). Pada Chapter ini kita akan membedah trik modulo (tail + 1) % capacity, masalah ambiguitas “kosong vs penuh” saat head == tail, dan dua solusi untuk masalah tersebut. Circular queue adalah dasar dari banyak sistem produksi: kernel ring buffer, network packet queue, dan audio buffer streaming.
Mengapa Circular Queue #
Array queue naif (menggeser elemen pada erase(begin())) O(n) per dequeue. Solusi naive lain: gunakan index front_idx yang maju — dequeue O(1), tetapi memori membengkak karena slot yang ditinggalkan tidak pernah dipakai ulang.
Circular queue menyelesaikan kedua masalah:
enqueuedandequeuekeduanya O(1).- Slot yang sudah di-dequeue dipakai ulang — memori statis tidak membengkak.
Saat tail mencapai akhir array, ia wrap ke 0 dengan operasi modulo. Head juga wrap dengan cara yang sama.
Trik Modulo #
Operasi kunci pada circular queue:
tail_ = (tail_ + 1) % capacity;Bila tail_ di posisi capacity - 1 (indeks terakhir), maka (capacity - 1 + 1) % capacity = 0 — tail wrap ke awal array. Operasi yang sama berlaku untuk head.
Masalah Ambiguitas Kosong vs Penuh #
Saat head_ == tail_, kondisi ini bisa berarti:
- Queue kosong — tidak ada elemen, head dan tail sama-sama menunjuk ke awal.
- Queue penuh — head dan tail sama setelah elemen terakhir di-enqueue (atau awalnya head sama dengan tail sebelum operasi).
Dua solusi:
Solusi A: Field size_ terpisah #
Simpan size_ eksplisit. empty() = size_ == 0, full() = size_ == cap_. Buffer penuh bisa menampung cap_ elemen.
Solusi B: Sacrifice satu slot #
Alokasikan cap + 1 slot, tetapi hanya isi cap elemen. empty() = head_ == tail_, full() = (tail_ + 1) % buf.size() == head_. Memori overhead: 1 slot per queue.
Implementasi Circular Queue (Solusi B: Sacrifice Slot) #
Kode: CircularQueue #
template <typename T>
class CircularQueue {
public:
explicit CircularQueue(std::size_t cap)
: buf_(cap + 1), head_(0), tail_(0) {} // +1 untuk pembeda kosong/penuh
void enqueue(const T& v) {
if (full()) throw std::overflow_error("queue penuh");
buf_[tail_] = v;
tail_ = (tail_ + 1) % buf_.size();
}
T dequeue() {
if (empty()) throw std::out_of_range("queue kosong");
T v = buf_[head_];
head_ = (head_ + 1) % buf_.size();
return v;
}
const T& front() const {
if (empty()) throw std::out_of_range("queue kosong");
return buf_[head_];
}
bool empty() const { return head_ == tail_; }
bool full() const { return (tail_ + 1) % buf_.size() == head_; }
std::size_t size() const {
return (tail_ + buf_.size() - head_) % buf_.size();
}
private:
std::vector<T> buf_;
std::size_t head_;
std::size_t tail_;
};Line-by-line #
buf_(cap + 1): alokasikan satu slot lebih banyak dari kapasitas yang diminta. Slot ekstra ini tidak pernah diisi — hanya sebagai pembeda kosong vs penuh.tail_ = (tail_ + 1) % buf_.size();(dienqueue): majukan tail dengan modulo. Bila tail di indeks terakhir, wrap ke 0.head_ = (head_ + 1) % buf_.size();(didequeue): majukan head dengan modulo yang sama.bool empty() const { return head_ == tail_; }: queue kosong ketika head mengejar tail (tidak ada elemen di antara keduanya).bool full() const { return (tail_ + 1) % buf_.size() == head_; }: queue penuh ketika tail “hampir” mengejar head — slot ekstra mencegah kondisi ini menjadi sama dengan empty.std::size_t size() const { return (tail_ + buf_.size() - head_) % buf_.size(); }: hitung elemen antara head dan tail.+ buf_.size()menghindari hasil negatif pada modulo (bila tail < head setelah wrap).
Demo Visual Circular Queue #
Kode berikut mencetak isi buffer dan posisi head/tail setelah setiap operasi — sehingga kita bisa melihat wrap secara visual:
void dump() const {
std::cout << " buf = [";
for (std::size_t i = 0; i < buf_.size(); ++i) {
std::cout << buf_[i];
if (i + 1 < buf_.size()) std::cout << ", ";
}
std::cout << "] head=" << head_ << " tail=" << tail_
<< " size=" << size() << (empty() ? " (empty)" : (full() ? " (full)" : ""))
<< '\n';
}Kode Lengkap dengan Demo #
#include <iostream>
#include <vector>
#include <stdexcept>
template <typename T>
class CircularQueue {
public:
explicit CircularQueue(std::size_t cap)
: buf_(cap + 1), head_(0), tail_(0) {} // +1 untuk pembeda kosong/penuh
void enqueue(const T& v) {
if (full()) throw std::overflow_error("queue penuh");
buf_[tail_] = v;
tail_ = (tail_ + 1) % buf_.size();
}
T dequeue() {
if (empty()) throw std::out_of_range("queue kosong");
T v = buf_[head_];
head_ = (head_ + 1) % buf_.size();
return v;
}
const T& front() const {
if (empty()) throw std::out_of_range("queue kosong");
return buf_[head_];
}
bool empty() const { return head_ == tail_; }
bool full() const { return (tail_ + 1) % buf_.size() == head_; }
std::size_t size() const {
return (tail_ + buf_.size() - head_) % buf_.size();
}
void dump() const {
std::cout << " buf = [";
for (std::size_t i = 0; i < buf_.size(); ++i) {
std::cout << buf_[i];
if (i + 1 < buf_.size()) std::cout << ", ";
}
std::cout << "] head=" << head_ << " tail=" << tail_
<< " size=" << size() << (empty() ? " (empty)" : (full() ? " (full)" : ""))
<< '\n';
}
private:
std::vector<T> buf_;
std::size_t head_;
std::size_t tail_;
};
int main() {
CircularQueue<int> cq(4); // kapasitas efektif 4 (butuh 5 slot)
std::cout << "Circular queue kapasitas 4 (buffer 5 slot):\n";
cq.dump();
std::cout << "\n--- enqueue 4 elemen ---\n";
for (int i = 1; i <= 4; ++i) {
cq.enqueue(i);
std::cout << "enqueue(" << i << ")";
cq.dump();
}
std::cout << "full? " << std::boolalpha << cq.full() << '\n';
std::cout << "\n--- dequeue 2 ---\n";
std::cout << "deq = " << cq.dequeue() << '\n';
std::cout << "deq = " << cq.dequeue() << '\n';
cq.dump();
std::cout << "\n--- enqueue 5, 6 (wrap) ---\n";
cq.enqueue(5);
cq.dump();
cq.enqueue(6);
cq.dump();
std::cout << "full? " << cq.full() << '\n';
std::cout << "\n--- dequeue semua ---\n";
while (!cq.empty()) {
std::cout << "deq = " << cq.dequeue() << '\n';
cq.dump();
}
return 0;
}Output yang Dihasilkan:
Circular queue kapasitas 4 (buffer 5 slot):
buf = [0, 0, 0, 0, 0] head=0 tail=0 size=0 (empty)
--- enqueue 4 elemen ---
enqueue(1) buf = [1, 0, 0, 0, 0] head=0 tail=1 size=1
enqueue(2) buf = [1, 2, 0, 0, 0] head=0 tail=2 size=2
enqueue(3) buf = [1, 2, 3, 0, 0] head=0 tail=3 size=3
enqueue(4) buf = [1, 2, 3, 4, 0] head=0 tail=4 size=4 (full)
full? true
--- dequeue 2 ---
deq = 1
deq = 2
buf = [1, 2, 3, 4, 0] head=2 tail=4 size=2
--- enqueue 5, 6 (wrap) ---
buf = [1, 2, 3, 4, 5] head=2 tail=0 size=3
buf = [6, 2, 3, 4, 5] head=2 tail=1 size=4 (full)
full? true
--- dequeue semua ---
deq = 3
buf = [6, 2, 3, 4, 5] head=3 tail=1 size=3
deq = 4
buf = [6, 2, 3, 4, 5] head=4 tail=1 size=2
deq = 5
buf = [6, 2, 3, 4, 5] head=0 tail=1 size=1
deq = 6
buf = [6, 2, 3, 4, 5] head=1 tail=1 size=0 (empty)enqueue(4)→head=0 tail=4 size=4 (full): 4 elemen dimuat, tail berada di indeks 4 (slot ekstra). Karena(4+1)%5=0dan0 == head,full()true — slot ekstra belum ditulis, mencegah ambiguitas dengan empty.deq = 1, deq = 2→head=2 tail=4: head maju dua kali. Tail tetap di 4. Size jadi 2.enqueue(5)→tail=0(wrap!): tail dari indeks 4 wrap ke 0 berkat modulo. Elemen 5 ditulis dibuf_[4](sebelum wrap), tail sekarang di 0.enqueue(6)→buf_[0] = 6: elemen 6 ditulis dibuf_[0]— slot yang tadinya di-dequeue (elemen 1). Inilah inti circular buffer: slot reuse.deq = 3, 4, 5, 6: dequeue mengikuti urutan enqueue — 3 dan 4 dulu (sisa dari batch pertama), lalu 5 dan 6 (yang baru saja di-enqueue di slot yang dipakai ulang). Head berakhir di1setelah semua di-dequeue.- Akhir
head=1 tail=1 size=0 (empty): head dan tail sama — empty terdeteksi dengan benar, bukan full.
Aplikasi Nyata Circular Queue #
| Aplikasi | Cara Pakai |
|---|---|
| Kernel ring buffer (dmesg) | Log kernel disimpan di circular buffer ukuran tetap; entry lama ditimpa entry baru |
| Audio buffer (ALSA, PulseAudio) | Sample audio menunggu diputar; producer (app) dan consumer (sound card) berbagi ring |
| Network packet queue | Paket menunggu transmisi; kapasitas tetap untuk membatasi backpressure |
| Keyboard buffer (BIOS) | Keystroke menunggu OS baca; buffer 16 byte klasik di BIOS IBM PC |
| OpenGL swap chain | Frame buffer yang siap ditampilkan di-queue circular |
Ringkasan #
- Circular queue = array queue dengan trik modulo
(idx + 1) % capacityuntuk wrap pointer. - Slot yang sudah di-dequeue dipakai ulang — tidak ada geser elemen, tidak ada memori membengkak.
- Masalah ambiguitas
head == tail: dua solusi — fieldsize_terpisah, atau sacrifice satu slot. - Implementasi solusi B (
cap + 1slot) memungkinkanemptydanfulldideteksi murni darihead_dantail_. - Aplikasi nyata: kernel ring buffer, audio buffer, network queue, keyboard buffer, swap chain grafis.