diff --git a/data_structures/unstable_deque.ts b/data_structures/unstable_deque.ts index a6af1aac742b..6a42df1f9c40 100644 --- a/data_structures/unstable_deque.ts +++ b/data_structures/unstable_deque.ts @@ -20,6 +20,7 @@ export type ReadonlyDeque = Pick< | "peekFront" | "peekBack" | "at" + | "includes" | "find" | "findIndex" | "toArray" @@ -44,6 +45,26 @@ function nextPowerOfTwo(n: number): number { * A double-ended queue backed by a ring buffer. Pushing, popping, and indexed * access stay fast as the deque grows. * + * | Method | Average Case | Worst Case | + * | ------------- | ------------ | ----------- | + * | pushBack() | O(1) | O(n) amort. | + * | pushFront() | O(1) | O(n) amort. | + * | popBack() | O(1) | O(1) | + * | popFront() | O(1) | O(1) | + * | peekFront() | O(1) | O(1) | + * | peekBack() | O(1) | O(1) | + * | at() | O(1) | O(1) | + * | isEmpty() | O(1) | O(1) | + * | clear() | O(1) | O(1) | + * | removeAt() | O(n) | O(n) | + * | removeFirst() | O(n) | O(n) | + * | includes() | O(n) | O(n) | + * | find() | O(n) | O(n) | + * | findIndex() | O(n) | O(n) | + * | retain() | O(n) | O(n) | + * | toArray() | O(n) | O(n) | + * | Deque.from() | O(n) | O(n) | + * * @experimental **UNSTABLE**: New API, yet to be vetted. * * @example Usage @@ -206,17 +227,11 @@ export class Deque implements Iterable, ReadonlyDeque { * @returns The new length of the deque. */ pushBack(value: T, ...rest: T[]): number { - if (this.#length === this.#capacity) { - if (this.#head === 0) this.#growWithoutCopying(); - else this.#grow(); - } + this.#maybeGrow(); this.#buffer[(this.#head + this.#length) & this.#mask] = value; this.#length++; for (let i = 0; i < rest.length; i++) { - if (this.#length === this.#capacity) { - if (this.#head === 0) this.#growWithoutCopying(); - else this.#grow(); - } + this.#maybeGrow(); this.#buffer[(this.#head + this.#length) & this.#mask] = rest[i]!; this.#length++; } @@ -246,12 +261,12 @@ export class Deque implements Iterable, ReadonlyDeque { */ pushFront(value: T, ...rest: T[]): number { for (let i = rest.length - 1; i >= 0; i--) { - if (this.#length === this.#capacity) this.#grow(); + this.#maybeGrow(); this.#head = (this.#head - 1) & this.#mask; this.#buffer[this.#head] = rest[i]!; this.#length++; } - if (this.#length === this.#capacity) this.#grow(); + this.#maybeGrow(); this.#head = (this.#head - 1) & this.#mask; this.#buffer[this.#head] = value; this.#length++; @@ -336,19 +351,44 @@ export class Deque implements Iterable, ReadonlyDeque { removeFirst(predicate: (value: T, index: number) => boolean): T | undefined { const i = this.#findIndex(predicate); if (i === -1) return undefined; + return this.#removeAtUnchecked(i); + } - const val = this.#buffer[(this.#head + i) & this.#mask] as T; - - const closerToFront = i < this.#length - i - 1; - if (closerToFront) { - this.#closeGapFromFront(i); - } else { - this.#closeGapFromBack(i); - } - - this.#length--; - this.#maybeShrink(); - return val; + /** + * Remove and return the element at the given index (0-based from front). + * Negative indices count from the back (`-1` is the last element). Returns + * `undefined` for out-of-range indices. The gap is closed by shifting + * whichever side (front or back) has fewer elements to move. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Removing by index + * ```ts + * import { Deque } from "@std/data-structures/unstable-deque"; + * import { assertEquals } from "@std/assert"; + * + * const deque = new Deque([10, 20, 30, 40]); + * assertEquals(deque.removeAt(1), 20); + * assertEquals([...deque], [10, 30, 40]); + * ``` + * + * @example Removing with a negative index + * ```ts + * import { Deque } from "@std/data-structures/unstable-deque"; + * import { assertEquals } from "@std/assert"; + * + * const deque = new Deque([10, 20, 30, 40]); + * assertEquals(deque.removeAt(-1), 40); + * assertEquals([...deque], [10, 20, 30]); + * ``` + * + * @param index The zero-based index. Negative values count from the back. + * @returns The removed element, or `undefined` if the index is out of range. + */ + removeAt(index: number): T | undefined { + if (index < 0) index += this.#length; + if (index < 0 || index >= this.#length) return undefined; + return this.#removeAtUnchecked(index); } /** @@ -474,6 +514,44 @@ export class Deque implements Iterable, ReadonlyDeque { return this.#buffer[(this.#head + index) & this.#mask]; } + /** + * Check whether the deque contains a value, using + * {@link https://tc39.es/ecma262/#sec-samevaluezero | SameValueZero} + * comparison (like {@linkcode Array.prototype.includes}). + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @example Checking for membership + * ```ts + * import { Deque } from "@std/data-structures/unstable-deque"; + * import { assertEquals } from "@std/assert"; + * + * const deque = new Deque([1, 2, 3]); + * assertEquals(deque.includes(2), true); + * assertEquals(deque.includes(99), false); + * ``` + * + * @example NaN is found (SameValueZero semantics) + * ```ts + * import { Deque } from "@std/data-structures/unstable-deque"; + * import { assertEquals } from "@std/assert"; + * + * const deque = new Deque([1, NaN, 3]); + * assertEquals(deque.includes(NaN), true); + * ``` + * + * @param value The value to search for. + * @returns `true` if the deque contains the value, otherwise `false`. + */ + includes(value: T): boolean { + for (let i = 0; i < this.#length; i++) { + const el = this.#buffer[(this.#head + i) & this.#mask]; + // SameValueZero: === for everything except NaN + if (el === value || (el !== el && value !== value)) return true; + } + return false; + } + /** * Remove all elements and release the backing buffer. * @@ -553,11 +631,15 @@ export class Deque implements Iterable, ReadonlyDeque { * @returns An array containing the deque's elements in order. */ toArray(): T[] { - const result = new Array(this.#length); - for (let i = 0; i < this.#length; i++) { - result[i] = this.#buffer[(this.#head + i) & this.#mask] as T; + const len = this.#length; + if (len === 0) return []; + const head = this.#head; + const end = head + len; + if (end <= this.#capacity) { + return this.#buffer.slice(head, end) as T[]; } - return result; + return (this.#buffer.slice(head) as T[]) + .concat(this.#buffer.slice(0, end - this.#capacity) as T[]); } /** @@ -655,13 +737,23 @@ export class Deque implements Iterable, ReadonlyDeque { return result; } unmappedValues = collection.toArray(); + } else if (!options?.map && Array.isArray(collection)) { + const len = collection.length; + const capacity = nextPowerOfTwo(len); + const buf = new Array(capacity); + for (let i = 0; i < len; i++) buf[i] = collection[i]; + result.#buffer = buf as (U | undefined)[]; + result.#head = 0; + result.#length = len; + result.#mask = capacity - 1; + return result; } else { unmappedValues = collection; } const mapped: U[] = options?.map ? Array.from(unmappedValues, options.map, options.thisArg) - : Array.from(unmappedValues as unknown as Iterable); + : Array.from(unmappedValues as ArrayLike & Iterable); const capacity = nextPowerOfTwo(mapped.length); result.#buffer = new Array(capacity); @@ -691,10 +783,23 @@ export class Deque implements Iterable, ReadonlyDeque { * * @returns An iterator yielding elements from front to back. */ - *[Symbol.iterator](): IterableIterator { - for (let i = 0; i < this.#length; i++) { - yield this.#buffer[(this.#head + i) & this.#mask] as T; - } + [Symbol.iterator](): IterableIterator { + let i = 0; + const length = this.#length; + const buffer = this.#buffer; + const head = this.#head; + const mask = this.#mask; + return { + [Symbol.iterator]() { + return this; + }, + next(): IteratorResult { + if (i < length) { + return { value: buffer[(head + i++) & mask] as T, done: false }; + } + return { value: undefined, done: true } as IteratorResult; + }, + }; } /** @@ -713,10 +818,22 @@ export class Deque implements Iterable, ReadonlyDeque { * * @returns An iterator yielding elements from back to front. */ - *reversed(): IterableIterator { - for (let i = this.#length - 1; i >= 0; i--) { - yield this.#buffer[(this.#head + i) & this.#mask] as T; - } + reversed(): IterableIterator { + let i = this.#length - 1; + const buffer = this.#buffer; + const head = this.#head; + const mask = this.#mask; + return { + [Symbol.iterator]() { + return this; + }, + next(): IteratorResult { + if (i >= 0) { + return { value: buffer[(head + i--) & mask] as T, done: false }; + } + return { value: undefined, done: true } as IteratorResult; + }, + }; } /** @@ -735,8 +852,17 @@ export class Deque implements Iterable, ReadonlyDeque { static #copyBuffer(source: Deque, capacity: number): (T | undefined)[] { const buffer = new Array(capacity); - for (let i = 0; i < source.#length; i++) { - buffer[i] = source.#buffer[(source.#head + i) & source.#mask]; + const head = source.#head; + const len = source.#length; + const srcBuf = source.#buffer; + const end = head + len; + if (end <= source.#capacity) { + for (let i = 0; i < len; i++) buffer[i] = srcBuf[head + i]; + } else { + const srcCap = source.#capacity; + let j = 0; + for (let i = head; i < srcCap; i++) buffer[j++] = srcBuf[i]; + for (let i = 0; j < len; i++) buffer[j++] = srcBuf[i]; } return buffer; } @@ -756,6 +882,19 @@ export class Deque implements Iterable, ReadonlyDeque { return -1; } + /** Extract value, close the gap, update length, and optionally shrink. */ + #removeAtUnchecked(index: number): T { + const val = this.#buffer[(this.#head + index) & this.#mask] as T; + if (index < this.#length - index - 1) { + this.#closeGapFromFront(index); + } else { + this.#closeGapFromBack(index); + } + this.#length--; + this.#maybeShrink(); + return val; + } + /** Close the gap at `i` by shifting elements before it one slot toward the back. */ #closeGapFromFront(i: number): void { for (let j = i; j > 0; j--) { @@ -777,6 +916,12 @@ export class Deque implements Iterable, ReadonlyDeque { this.#buffer[(this.#head + this.#length - 1) & this.#mask] = undefined; } + #maybeGrow(): void { + if (this.#length < this.#capacity) return; + if (this.#head === 0) this.#growWithoutCopying(); + else this.#grow(); + } + #grow(): void { this.#realloc(this.#capacity * 2); } diff --git a/data_structures/unstable_deque_test.ts b/data_structures/unstable_deque_test.ts index a037d59eb040..34bfd8ff7299 100644 --- a/data_structures/unstable_deque_test.ts +++ b/data_structures/unstable_deque_test.ts @@ -496,6 +496,107 @@ Deno.test("Deque.retain() triggers shrink on large deque", () => { assertEquals(deque.at(7), 127); }); +// -- includes() -- + +Deno.test("Deque.includes() returns true for a present value", () => { + const deque = new Deque([1, 2, 3]); + assertEquals(deque.includes(2), true); +}); + +Deno.test("Deque.includes() returns false for an absent value", () => { + const deque = new Deque([1, 2, 3]); + assertEquals(deque.includes(99), false); +}); + +Deno.test("Deque.includes() returns false on an empty deque", () => { + assertEquals(new Deque().includes(1), false); +}); + +Deno.test("Deque.includes() finds NaN via SameValueZero", () => { + const deque = new Deque([1, NaN, 3]); + assertEquals(deque.includes(NaN), true); +}); + +Deno.test("Deque.includes() works after wrap-around", () => { + const deque = new Deque(); + for (let i = 0; i < 6; i++) deque.pushBack(i); + for (let i = 0; i < 6; i++) deque.popFront(); + deque.pushBack(10, 20, 30); + assertEquals(deque.includes(20), true); + assertEquals(deque.includes(99), false); +}); + +// -- removeAt() -- + +Deno.test("Deque.removeAt() removes and returns element at positive index", () => { + const deque = new Deque([10, 20, 30, 40]); + assertEquals(deque.removeAt(1), 20); + assertEquals([...deque], [10, 30, 40]); +}); + +Deno.test("Deque.removeAt() supports negative indices", () => { + const deque = new Deque([10, 20, 30, 40]); + assertEquals(deque.removeAt(-1), 40); + assertEquals([...deque], [10, 20, 30]); + assertEquals(deque.removeAt(-2), 20); + assertEquals([...deque], [10, 30]); +}); + +Deno.test("Deque.removeAt() returns undefined for out-of-range index", () => { + const deque = new Deque([1, 2, 3]); + assertStrictEquals(deque.removeAt(3), undefined); + assertStrictEquals(deque.removeAt(-4), undefined); + assertEquals([...deque], [1, 2, 3]); +}); + +Deno.test("Deque.removeAt() removes the first element", () => { + const deque = new Deque([10, 20, 30]); + assertEquals(deque.removeAt(0), 10); + assertEquals([...deque], [20, 30]); +}); + +Deno.test("Deque.removeAt() removes the last element", () => { + const deque = new Deque([10, 20, 30]); + assertEquals(deque.removeAt(2), 30); + assertEquals([...deque], [10, 20]); +}); + +Deno.test("Deque.removeAt() on single-element deque", () => { + const deque = new Deque([42]); + assertEquals(deque.removeAt(0), 42); + assertEquals(deque.length, 0); +}); + +Deno.test("Deque.removeAt() works after wrap-around", () => { + const deque = new Deque(); + for (let i = 0; i < 6; i++) deque.pushBack(i); + for (let i = 0; i < 6; i++) deque.popFront(); + deque.pushBack(10, 20, 30, 40, 50); + assertEquals(deque.removeAt(2), 30); + assertEquals([...deque], [10, 20, 40, 50]); +}); + +Deno.test("Deque.removeAt() shifts front side for front-half index", () => { + const deque = new Deque([10, 20, 30, 40, 50]); + assertEquals(deque.removeAt(1), 20); + assertEquals([...deque], [10, 30, 40, 50]); +}); + +Deno.test("Deque.removeAt() shifts back side for back-half index", () => { + const deque = new Deque([10, 20, 30, 40, 50]); + assertEquals(deque.removeAt(3), 40); + assertEquals([...deque], [10, 20, 30, 50]); +}); + +Deno.test("Deque.removeAt() triggers shrink on large deque", () => { + const deque = new Deque(); + for (let i = 0; i < 128; i++) deque.pushBack(i); + for (let i = 0; i < 112; i++) deque.popFront(); + assertEquals(deque.removeAt(8), 120); + assertEquals(deque.length, 15); + assertEquals(deque.at(0), 112); +}); + // -- Symbol.toStringTag -- Deno.test("Deque has correct Symbol.toStringTag", () => { @@ -508,6 +609,7 @@ Deno.test("Deque has correct Symbol.toStringTag", () => { Deno.test("ReadonlyDeque exposes read-only methods", () => { const deque: ReadonlyDeque = new Deque([1, 2, 3, 4]); + assertEquals(deque.includes(3), true); assertEquals(deque.find((v) => v === 3), 3); assertEquals(deque.findIndex((v) => v === 3), 2); assertEquals(deque[Symbol.toStringTag], "Deque");