From 7cf5bc679cadc8ca1bdb1c277e2eb2d99ae575af Mon Sep 17 00:00:00 2001 From: "jin.park" Date: Tue, 20 Aug 2024 10:47:22 +0900 Subject: [PATCH] Improve queues with circular buffers --- README.md | 32 +++-- package.json | 3 +- src/Queue.ts | 177 ++++++++++++++++---------- src/exceptions/BaseException.ts | 10 +- src/exceptions/EmptyQueueException.ts | 12 +- test/queue.test.ts | 70 +++++----- 6 files changed, 175 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index e33990b..7d9b455 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,23 @@ NPM Version Package License NPM Downloads - Javascript - Javascript - + JavaScript + TypeScript

-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 @@ -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"; @@ -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"; @@ -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 @@ -149,4 +153,6 @@ console.timeEnd('ElegantQueue Dequeue Time'); ```bash console.time ElegantQueue Dequeue Time: 5 ms -``` \ No newline at end of file +``` + +**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. \ No newline at end of file diff --git a/package.json b/package.json index 208c1e3..2e16cbb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Queue.ts b/src/Queue.ts index d6b080b..f455109 100644 --- a/src/Queue.ts +++ b/src/Queue.ts @@ -1,81 +1,118 @@ -import { EmptyQueueException } from "./exceptions/EmptyQueueException"; +import { EmptyQueueException } from './exceptions/EmptyQueueException'; -export class Queue{ - private _data: (T | undefined)[]; - private _head: number; - private _tail: number; +class Node { + data: (T | undefined)[]; + next: Node | 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(size); + } +} + +export class Queue { + private _head: Node | null = null; + private _tail: Node | 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(this._arraySize); + this._head = this._tail; + } else if (this._tailIndex === this._arraySize) { + // Last array is full, add a new node + const newNode = new Node(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; } - - \ No newline at end of file +} diff --git a/src/exceptions/BaseException.ts b/src/exceptions/BaseException.ts index 3149ca4..a59f429 100644 --- a/src/exceptions/BaseException.ts +++ b/src/exceptions/BaseException.ts @@ -1,6 +1,6 @@ export abstract class BaseException extends Error { - constructor(message: string) { - super(message); - this.name = "BaseException"; - } - } \ No newline at end of file + constructor(message: string) { + super(message); + this.name = 'BaseException'; + } +} diff --git a/src/exceptions/EmptyQueueException.ts b/src/exceptions/EmptyQueueException.ts index 8ce3a8e..5d1e3df 100644 --- a/src/exceptions/EmptyQueueException.ts +++ b/src/exceptions/EmptyQueueException.ts @@ -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"; - } - } \ No newline at end of file + constructor() { + super('Queue is empty'); + this.name = 'EmptyQueueException'; + } +} diff --git a/test/queue.test.ts b/test/queue.test.ts index 815deb6..efb4cac 100644 --- a/test/queue.test.ts +++ b/test/queue.test.ts @@ -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(); + 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; @@ -53,13 +56,12 @@ describe('Queue Performance Comparison', () => { it('ElegantQueue performance', () => { console.time('ElegantQueue Enqueue Time'); - const numbers: Array = []; + const elegantQueue = new Queue(); 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'); @@ -68,4 +70,4 @@ describe('Queue Performance Comparison', () => { } console.timeEnd('ElegantQueue Dequeue Time'); }); -}); \ No newline at end of file +});