Skip to content

Commit

Permalink
Improve queues with circular buffers
Browse files Browse the repository at this point in the history
  • Loading branch information
seongjin605 committed Aug 20, 2024
1 parent 7c04dee commit 7cf5bc6
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 129 deletions.
32 changes: 19 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
<a href="https://img.shields.io/npm/v/elegant-queue?logo=nodedotjs" target="_blank"><img src="https://img.shields.io/npm/v/elegant-queue?logo=npm" alt="NPM Version" /></a>
<a href="https://img.shields.io/npm/l/elegant-queue" target="_blank"><img src="https://img.shields.io/npm/l/elegant-queue" alt="Package License" /></a>
<a href="https://img.shields.io/npm/dm/elegant-queue" target="_blank"><img src="https://img.shields.io/npm/dm/elegant-queue" alt="NPM Downloads" /></a>
<a href="https://shields.io/badge/JavaScript-F7DF1E?logo=JavaScript&logoColor=000&style=flat-square" target="_blank"><img src="https://shields.io/badge/JavaScript-F7DF1E?logo=JavaScript&logoColor=000&style=flat-square" alt="Javascript" /></a>
<a href="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" target="_blank"><img src="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" alt="Javascript" /></a>
<!--<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/seongjin605/elegant-queue/main" alt="CircleCI" /></a>-->
<a href="https://shields.io/badge/JavaScript-F7DF1E?logo=JavaScript&logoColor=000&style=flat-square" target="_blank"><img src="https://shields.io/badge/JavaScript-F7DF1E?logo=JavaScript&logoColor=000&style=flat-square" alt="JavaScript" /></a>
<a href="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" target="_blank"><img src="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" alt="TypeScript" /></a>
</p>

In general, to use a Queue using JS or TS, it may be more common or simpler to use an Array.
## Overview

Let's take a look at the [Description](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift#return_value) of the `shift()` function as described by MSDN
"The shift() method removes the element at the zeroth index and shifts the values at consecutive indexes down, then returns the removed value. If the length property is 0, undefined is returned."
In JavaScript and TypeScript, arrays are often used to implement queues. The built-in `shift()` method removes the element at the zeroth index and shifts the remaining elements down, which has O(n) time complexity due to the re-indexing required.

As you can see, using a queue with O(N) time complexity or using `shift()` to get the first value in an array has O(N) time complexity.
To address this inefficiency when processing large amounts of data, designing a queue with O(1) time complexity can solve many problems.
### Why Circular Buffers?

To optimize queue operations, especially with large datasets, a circular buffer is a highly effective solution. It allows both `enqueue` and `dequeue` operations to be performed in O(1) time complexity by managing elements in a fixed-size array with wrapping pointers.

**Key Benefits:**

- **Memory Efficiency:** A circular buffer uses a fixed-size array and wraps around, eliminating the need for continuous resizing and minimizing memory overhead.
- **Consistent O(1) Performance:** Operations remain constant time, avoiding the performance pitfalls of array resizing and shifting.
- **Avoids Memory Fragmentation:** Efficient memory use and reduced risk of fragmentation, even with dynamic queue sizes.


## 📚 Getting Started
Expand Down Expand Up @@ -69,7 +74,7 @@ This method checks if the queue is empty. It returns `true` if `_head` is equal

## 🌈 Examples

### Use Example
### Usage Example
```typescript
import { Queue } from "elegant-queue";

Expand All @@ -82,7 +87,7 @@ console.log(item); // 1
console.log(queue); // [2, 3, 4, 5, 6]
```

### Exception Example
### Exception Handling Example
```typescript
import { Queue, EmptyQueueException } from "elegant-queue";

Expand All @@ -100,8 +105,7 @@ try {


## ⚡️ Performance (1 million numbers)
The test results below were written on my local PC, so your performance results may vary.
But I can assure you that `dequeue()` in **Elegant Queue** is definitely faster on large amounts of data than using `shift()` in the **built-in array**.
The following benchmarks compare elegant-queue with a standard array-based queue.

### Array Queue performance:
```typescript
Expand Down Expand Up @@ -149,4 +153,6 @@ console.timeEnd('ElegantQueue Dequeue Time');
```bash
console.time
ElegantQueue Dequeue Time: 5 ms
```
```

**Note:** The `shift()` method in arrays has O(n) time complexity due to the need to re-index elements after removal. In contrast, `elegant-queue` provides O(1) time complexity for both enqueue and dequeue operations by utilizing a circular buffer design, making it significantly faster for large datasets.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "elegant-queue",
"version": "1.0.5",
"version": "1.0.6",
"description": "To address this inefficiency when processing large amounts of data, designing a queue with O(1) time complexity can solve many problems.",
"main": "dist/cjs/index.js",
"module": "dist/mjs/index.js",
"types": "dist/mjs/index.d.ts",
"scripts": {
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"build": "rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && bash ./fixup",
"prepare": "rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && bash ./fixup && husky install",
"test": "jest",
Expand Down
177 changes: 107 additions & 70 deletions src/Queue.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,118 @@
import { EmptyQueueException } from "./exceptions/EmptyQueueException";
import { EmptyQueueException } from './exceptions/EmptyQueueException';

export class Queue<T>{
private _data: (T | undefined)[];
private _head: number;
private _tail: number;
class Node<T> {
data: (T | undefined)[];
next: Node<T> | null = null;

/**
* Initializes a new instance of the Queue class with the given array.
* @param array
*/
constructor(array: T[]) {
this._data = array;
this._head = 0;
this._tail = array.length;
}

/**
* Adds a new item to the end of the queue.
* @param value
*/
enqueue(value: T) {
this._data[this._tail] = value;
this._tail++;
}

/**
* Removes and returns the item at the front of the queue.
* @throws EmptyQueueException - If the queue is empty.
* @returns The item that was removed from the front of the queue.
*/
dequeue() {
if (this.isEmpty()) {
throw new EmptyQueueException();
}

const value = this._data[this._head];
this._data[this._head] = undefined;
this._head++;
return value;
}
constructor(size: number) {
this.data = new Array<T | undefined>(size);
}
}

export class Queue<T> {
private _head: Node<T> | null = null;
private _tail: Node<T> | null = null;
private _headIndex: number = 0;
private _tailIndex: number = 0;
private _arraySize: number;
private _size: number = 0;

/**
* Returns the item at the front of the queue without removing it.
* @throws EmptyQueueException - If the queue is empty.
* @returns The item at the front of the queue.
*/
peek() {
if (this.isEmpty()) {
throw new EmptyQueueException();
}
return this._data[this._head];
/**
* Initializes a new instance of the Queue class with a customizable array size.
* @param arraySize The size of each array block (default: 4096).
*/
constructor(arraySize: number = 4096) {
this._arraySize = arraySize;
}

/**
* Adds a new item to the end of the queue.
* @param value The value to be added to the queue.
*/
enqueue(value: T) {
if (this._tail === null) {
// First element, initialize the linked list
this._tail = new Node<T>(this._arraySize);
this._head = this._tail;
} else if (this._tailIndex === this._arraySize) {
// Last array is full, add a new node
const newNode = new Node<T>(this._arraySize);
this._tail.next = newNode;
this._tail = newNode;
this._tailIndex = 0;
}

/**
* Clears all items from the queue and resets its state.
*/
clear() {
this._data = [];
this._head = 0;
this._tail = 0;
this._tail.data[this._tailIndex++] = value;
this._size++;
}

/**
* Removes and returns the item at the front of the queue.
* @throws EmptyQueueException - If the queue is empty.
* @returns The item that was removed from the front of the queue.
*/
dequeue(): T | undefined {
if (this.isEmpty()) {
throw new EmptyQueueException();
}
const value = this._head!.data[this._headIndex];
// This step is optional and may not be required.
// It doesn't actually have a significant impact on the behavior of the queue, but is added for better memory management.
this._head!.data[this._headIndex] = undefined;
this._headIndex++;
this._size--;

/**
* Returns the number of items currently in the queue.
* @returns The number of items in the queue.
*/
size() {
return this._tail - this._head;
// If the current array is empty, move to the next node
if (this._headIndex === this._arraySize) {
this._head = this._head!.next;
this._headIndex = 0;

// If we removed the last node, reset the tail as well
if (this._head === null) {
this._tail = null;
this._tailIndex = 0;
}
}

/**
* Checks if the queue is empty.
* @returns `true` if the queue is empty, `false` otherwise.
*/
isEmpty() {
return this._head === this._tail;
return value;
}

/**
* Returns the item at the front of the queue without removing it.
* @throws EmptyQueueException - If the queue is empty.
* @returns The item at the front of the queue.
*/
peek(): T | undefined {
if (this.isEmpty()) {
throw new EmptyQueueException();
}
return this._head!.data[this._headIndex];
}

/**
* Clears all items from the queue and resets its state.
*/
clear() {
this._head = null;
this._tail = null;
this._headIndex = 0;
this._tailIndex = 0;
this._size = 0;
}

/**
* Returns the number of items currently in the queue.
* @returns The number of items in the queue.
*/
size(): number {
return this._size;
}

/**
* Checks if the queue is empty.
* @returns `true` if the queue is empty, `false` otherwise.
*/
isEmpty(): boolean {
return this._size === 0;
}


}
10 changes: 5 additions & 5 deletions src/exceptions/BaseException.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export abstract class BaseException extends Error {
constructor(message: string) {
super(message);
this.name = "BaseException";
}
}
constructor(message: string) {
super(message);
this.name = 'BaseException';
}
}
12 changes: 6 additions & 6 deletions src/exceptions/EmptyQueueException.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BaseException } from "./BaseException";
import { BaseException } from './BaseException';

export class EmptyQueueException extends BaseException {
constructor() {
super("Queue is empty");
this.name = "EmptyQueueException";
}
}
constructor() {
super('Queue is empty');
this.name = 'EmptyQueueException';
}
}
70 changes: 36 additions & 34 deletions test/queue.test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@

import { Queue } from "../src";
import { EmptyQueueException } from "../src/exceptions/EmptyQueueException";
import { Queue } from '../src';
import { EmptyQueueException } from '../src/exceptions/EmptyQueueException';

describe('Queue Test', () => {
test('Queue Logic Test', () => {
const queue = new Queue([1, 2, 3, 4, 5]);
console.log(queue);
expect(queue.size()).toBe(5);

queue.enqueue(6);
expect(queue.size()).toBe(6);

expect(queue.dequeue()).toBe(1);
expect(queue.size()).toBe(5);

expect(queue.peek()).toBe(2);
expect(queue.size()).toBe(5);

expect(queue.dequeue()).toBe(2);
expect(queue.size()).toBe(4);

queue.clear();
expect(queue.size()).toBe(0);

expect(queue.isEmpty()).toBe(true);
expect(queue.size()).toBe(0);

expect(() => queue.dequeue()).toThrowError(EmptyQueueException);
});
});
test('Queue Logic Test', () => {
const queue = new Queue<number>();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.enqueue(4);
queue.enqueue(5);

console.log(queue);
expect(queue.size()).toBe(5);

queue.enqueue(6);
expect(queue.size()).toBe(6);

expect(queue.dequeue()).toBe(1);
expect(queue.size()).toBe(5);

expect(queue.peek()).toBe(2);
expect(queue.size()).toBe(5);


expect(queue.dequeue()).toBe(2);
expect(queue.size()).toBe(4);

queue.clear();
expect(queue.size()).toBe(0);

expect(queue.isEmpty()).toBe(true);
expect(queue.size()).toBe(0);

expect(() => queue.dequeue()).toThrowError(EmptyQueueException);
});
});

const LARGE_DATA_SIZE = 1_000_000;

Expand All @@ -53,13 +56,12 @@ describe('Queue Performance Comparison', () => {

it('ElegantQueue performance', () => {
console.time('ElegantQueue Enqueue Time');
const numbers: Array<number> = [];
const elegantQueue = new Queue<number>();

for (let i = 0; i < LARGE_DATA_SIZE; i++) {
numbers.push(i);
elegantQueue.enqueue(i);
}

const elegantQueue = new Queue(numbers);
console.timeEnd('ElegantQueue Enqueue Time');

console.time('ElegantQueue Dequeue Time');
Expand All @@ -68,4 +70,4 @@ describe('Queue Performance Comparison', () => {
}
console.timeEnd('ElegantQueue Dequeue Time');
});
});
});

0 comments on commit 7cf5bc6

Please sign in to comment.