Skip to content

Instantly share code, notes, and snippets.

@Phryxia
Last active October 1, 2022 02:54
Show Gist options
  • Save Phryxia/8b013da1ad945c6cd88989485e58d2ea to your computer and use it in GitHub Desktop.
Save Phryxia/8b013da1ad945c6cd88989485e58d2ea to your computer and use it in GitHub Desktop.
Simple CircularQueue implementation for JavaScript/TypeScript
class CircularQueue {
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))));
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
}
}
class CircularQueue<T> implements Iterable<T> {
private data: T[] = new Array(4);
private head: number = 0;
private tail: number = 0;
private _size: number = 0;
constructor(public readonly growthRate: number = 2) {}
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))));
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]() {
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

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
}

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