Skip to content

Instantly share code, notes, and snippets.

@Phryxia
Last active January 13, 2026 20:42
Show Gist options
  • Select an option

  • Save Phryxia/8b013da1ad945c6cd88989485e58d2ea to your computer and use it in GitHub Desktop.

Select an option

Save Phryxia/8b013da1ad945c6cd88989485e58d2ea to your computer and use it in GitHub Desktop.
Simple CircularQueue implementation for JavaScript/TypeScript
class CircularQueue {
/**
* @param {number} growthRate Rate of growth when additional allocation is required. It must be larger than 1.
*/
constructor(growthRate) {
this.growthRate = growthRate
this.data = new Array(4);
this.head = 0;
this.tail = 0;
this.size = 0;
}
grow() {
const length = this.data.length;
this.data = this.data
.slice(this.head, length)
.concat(this.data.slice(0, this.head))
.concat(new Array(Math.max(1, Math.floor(length * (this.growthRate - 1)))))
this.head = 0;
this.tail = length;
}
push(el) {
this.data[this.tail] = el;
this.tail = (this.tail + 1) % this.data.length;
if (this.tail === this.head) {
this.grow();
}
this.size += 1;
return el
}
pop() {
if (this.size <= 0) return undefined;
const result = this.data[this.head];
this.head = (this.head + 1) % this.data.length;
this.size -= 1;
return result;
}
front() {
if (this.size <= 0) return undefined;
return this.data[this.head];
}
[Symbol.iterator]() {
const data = [...this.data]
const tail = this.tail
let currentIndex = this.head
return {
next() {
if (currentIndex === tail) return { done: true }
const oldIndex = currentIndex
currentIndex = (currentIndex + 1) % data.length
return {
value: data[oldIndex],
done: false,
}
},
[Symbol.iterator]() {
return this
}
}
}
isNotEmpty() {
return this._size > 0
}
}
export class Queue<T> implements Iterable<T> {
private data: T[] = new Array(4)
private head: number = 0
private tail: number = 0
private _size: number = 0
public readonly growthRate: number
/**
* @param growthRate Rate of growth when additional allocation is required. It must be larger than 1.
*/
constructor(growthRate: number = 2) {
this.growthRate = growthRate
}
private grow(): void {
const length = this.data.length
this.data = this.data
.slice(this.head, length)
.concat(this.data.slice(0, this.head))
.concat(new Array(Math.max(1, Math.floor(length * (this.growthRate - 1)))))
this.head = 0
this.tail = length
}
public push(el: T): T {
this.data[this.tail] = el
this.tail = (this.tail + 1) % this.data.length
if (this.tail === this.head) {
this.grow()
}
this._size += 1
return el
}
public pop(): T | undefined {
if (this._size <= 0) return undefined
const result = this.data[this.head]
this.head = (this.head + 1) % this.data.length
this._size -= 1
return result
}
public front(): T | undefined {
if (this._size <= 0) return undefined
return this.data[this.head]
}
public size(): number {
return this._size
}
public [Symbol.iterator](): IterableIterator<T> {
const data = [...this.data]
const tail = this.tail
let currentIndex = this.head
return {
next(): IteratorResult<T> {
if (currentIndex === tail) return { value: undefined, done: true }
const oldIndex = currentIndex
currentIndex = (currentIndex + 1) % data.length
return {
value: data[oldIndex],
done: false,
}
},
[Symbol.iterator](): IterableIterator<T> {
return this
},
}
}
public isNotEmpty(): this is NonEmptyQueue<T> {
return this._size > 0
}
}
interface NonEmptyQueue<T> extends Queue<T> {
pop(): T
}
@Phryxia
Copy link
Author

Phryxia commented Jan 12, 2022

You can implement queue using linked list, but that's a little bit slower because of the allocations for each nodes.
(See https://www.measurethat.net/Benchmarks/Show/16655/1/circular-queue-vs-linked-list-queue-v2)

@Phryxia
Copy link
Author

Phryxia commented Apr 19, 2022

Changes

2026-01-14

Support followings

2024-09-09

  • Fix wrong size growth #see (Thank you tom288!)

2022-04-19

Now this queue supports iteration protocols.

const q = new CircularQueue<number>()
q.push(1)
q.push(2)
q.push(3)
for (const x of q) {
  console.log(x)
}

// print 1, 2, 3

Note that iterator ignores any changes to the queue after the iterator's creation. This ensure the concurrency safety.

@Phryxia
Copy link
Author

Phryxia commented Oct 1, 2022

Now this queue provides improved DX using isNotEmpty. This returns type assertion which ensures that return value of the pop is T, not T | undefined.

if (q.isNotEmpty()) {
  const value = q.pop() // you don't have to put ! here
}

@tom288
Copy link

tom288 commented Sep 9, 2024

This is really nice.

I believe this is wrong:

     .concat(new Array(Math.max(1, Math.floor(length * this.growthRate))));

As it means a growth rate of this.growthRate + 1 is used. This should be used instead:

     .concat(new Array(Math.max(1, Math.floor(length * (this.growthRate - 1)))));

@Phryxia
Copy link
Author

Phryxia commented Sep 9, 2024

@tom288

As it means a growth rate of this.growthRate + 1 is used. This should be used instead:

Yes you're right. Thank you sir! I'll edit asap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment