binary-tree-typed 2.4.4 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cjs/index.cjs +152 -70
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs-legacy/index.cjs +150 -68
  4. package/dist/cjs-legacy/index.cjs.map +1 -1
  5. package/dist/esm/index.mjs +152 -71
  6. package/dist/esm/index.mjs.map +1 -1
  7. package/dist/esm-legacy/index.mjs +150 -69
  8. package/dist/esm-legacy/index.mjs.map +1 -1
  9. package/dist/types/common/error.d.ts +23 -0
  10. package/dist/types/common/index.d.ts +1 -0
  11. package/dist/types/data-structures/binary-tree/binary-tree.d.ts +10 -0
  12. package/dist/types/data-structures/binary-tree/red-black-tree.d.ts +7 -1
  13. package/dist/types/data-structures/graph/abstract-graph.d.ts +44 -0
  14. package/dist/types/data-structures/graph/directed-graph.d.ts +1 -0
  15. package/dist/types/data-structures/graph/undirected-graph.d.ts +14 -0
  16. package/dist/types/data-structures/queue/deque.d.ts +41 -1
  17. package/dist/types/types/data-structures/queue/deque.d.ts +6 -0
  18. package/dist/umd/binary-tree-typed.js +149 -67
  19. package/dist/umd/binary-tree-typed.js.map +1 -1
  20. package/dist/umd/binary-tree-typed.min.js +3 -3
  21. package/dist/umd/binary-tree-typed.min.js.map +1 -1
  22. package/package.json +2 -2
  23. package/src/common/error.ts +60 -0
  24. package/src/common/index.ts +2 -0
  25. package/src/data-structures/base/iterable-element-base.ts +3 -2
  26. package/src/data-structures/binary-tree/binary-indexed-tree.ts +6 -5
  27. package/src/data-structures/binary-tree/binary-tree.ts +113 -42
  28. package/src/data-structures/binary-tree/bst.ts +11 -3
  29. package/src/data-structures/binary-tree/red-black-tree.ts +20 -0
  30. package/src/data-structures/binary-tree/tree-map.ts +8 -7
  31. package/src/data-structures/binary-tree/tree-multi-map.ts +4 -4
  32. package/src/data-structures/binary-tree/tree-multi-set.ts +5 -4
  33. package/src/data-structures/binary-tree/tree-set.ts +7 -6
  34. package/src/data-structures/graph/abstract-graph.ts +106 -1
  35. package/src/data-structures/graph/directed-graph.ts +4 -0
  36. package/src/data-structures/graph/undirected-graph.ts +95 -0
  37. package/src/data-structures/hash/hash-map.ts +13 -2
  38. package/src/data-structures/heap/heap.ts +4 -3
  39. package/src/data-structures/heap/max-heap.ts +2 -3
  40. package/src/data-structures/matrix/matrix.ts +9 -10
  41. package/src/data-structures/priority-queue/max-priority-queue.ts +2 -3
  42. package/src/data-structures/queue/deque.ts +71 -3
  43. package/src/data-structures/trie/trie.ts +2 -1
  44. package/src/types/data-structures/queue/deque.ts +7 -0
  45. package/src/utils/utils.ts +4 -2
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { DijkstraResult, EntryCallback, GraphOptions, VertexKey } from '../../types';
10
10
  import { uuidV4 } from '../../utils';
11
+ import { ERR } from '../../common';
11
12
  import { IterableEntryBase } from '../base';
12
13
  import { IGraph } from '../../interfaces';
13
14
  import { Heap } from '../heap';
@@ -274,7 +275,7 @@ export abstract class AbstractGraph<
274
275
  const newEdge = this.createEdge(srcOrEdge, dest, weight, value);
275
276
  return this._addEdge(newEdge);
276
277
  } else {
277
- throw new Error('dest must be a Vertex or vertex key while srcOrEdge is an Edge');
278
+ throw new TypeError(ERR.invalidArgument('dest must be a Vertex or vertex key when srcOrEdge is an Edge.', 'Graph'));
278
279
  }
279
280
  }
280
281
  }
@@ -1078,4 +1079,108 @@ export abstract class AbstractGraph<
1078
1079
  protected _getVertexKey(vertexOrKey: VO | VertexKey): VertexKey {
1079
1080
  return vertexOrKey instanceof AbstractVertex ? vertexOrKey.key : vertexOrKey;
1080
1081
  }
1082
+
1083
+ /**
1084
+ * The edge connector string used in visual output.
1085
+ * Override in subclasses (e.g., '--' for undirected, '->' for directed).
1086
+ */
1087
+ protected get _edgeConnector(): string {
1088
+ return '--';
1089
+ }
1090
+
1091
+ /**
1092
+ * Generate a text-based visual representation of the graph.
1093
+ *
1094
+ * **Adjacency list format:**
1095
+ * ```
1096
+ * Graph (5 vertices, 6 edges):
1097
+ * A -> B (1), C (2)
1098
+ * B -> D (3)
1099
+ * C -> (no outgoing edges)
1100
+ * D -> A (1)
1101
+ * E (isolated)
1102
+ * ```
1103
+ *
1104
+ * @param options - Optional display settings.
1105
+ * @param options.showWeight - Whether to show edge weights (default: true).
1106
+ * @returns The visual string.
1107
+ */
1108
+ override toVisual(options?: { showWeight?: boolean }): string {
1109
+ const showWeight = options?.showWeight ?? true;
1110
+ const vertices = [...this._vertexMap.values()];
1111
+ const vertexCount = vertices.length;
1112
+ const edgeCount = this.edgeSet().length;
1113
+
1114
+ const lines: string[] = [`Graph (${vertexCount} vertices, ${edgeCount} edges):`];
1115
+
1116
+ for (const vertex of vertices) {
1117
+ const neighbors = this.getNeighbors(vertex);
1118
+ if (neighbors.length === 0) {
1119
+ lines.push(` ${vertex.key} (isolated)`);
1120
+ } else {
1121
+ const edgeStrs = neighbors.map(neighbor => {
1122
+ const edge = this.getEdge(vertex, neighbor);
1123
+ if (edge && showWeight && edge.weight !== undefined && edge.weight !== 1) {
1124
+ return `${neighbor.key} (${edge.weight})`;
1125
+ }
1126
+ return `${neighbor.key}`;
1127
+ });
1128
+ lines.push(` ${vertex.key} ${this._edgeConnector} ${edgeStrs.join(', ')}`);
1129
+ }
1130
+ }
1131
+
1132
+ return lines.join('\n');
1133
+ }
1134
+
1135
+ /**
1136
+ * Generate DOT language representation for Graphviz.
1137
+ *
1138
+ * @param options - Optional display settings.
1139
+ * @param options.name - Graph name (default: 'G').
1140
+ * @param options.showWeight - Whether to label edges with weight (default: true).
1141
+ * @returns DOT format string.
1142
+ */
1143
+ toDot(options?: { name?: string; showWeight?: boolean }): string {
1144
+ const name = options?.name ?? 'G';
1145
+ const showWeight = options?.showWeight ?? true;
1146
+ const isDirected = this._edgeConnector === '->';
1147
+ const graphType = isDirected ? 'digraph' : 'graph';
1148
+ const edgeOp = isDirected ? '->' : '--';
1149
+
1150
+ const lines: string[] = [`${graphType} ${name} {`];
1151
+
1152
+ // Add all vertices (ensures isolated vertices appear)
1153
+ for (const vertex of this._vertexMap.values()) {
1154
+ lines.push(` "${vertex.key}";`);
1155
+ }
1156
+
1157
+ // Add edges
1158
+ const visited = new Set<string>();
1159
+ for (const vertex of this._vertexMap.values()) {
1160
+ for (const neighbor of this.getNeighbors(vertex)) {
1161
+ const edgeId = isDirected
1162
+ ? `${vertex.key}->${neighbor.key}`
1163
+ : [vertex.key, neighbor.key].sort().join('--');
1164
+ if (visited.has(edgeId)) continue;
1165
+ visited.add(edgeId);
1166
+
1167
+ const edge = this.getEdge(vertex, neighbor);
1168
+ const label = edge && showWeight && edge.weight !== undefined && edge.weight !== 1
1169
+ ? ` [label="${edge.weight}"]`
1170
+ : '';
1171
+ lines.push(` "${vertex.key}" ${edgeOp} "${neighbor.key}"${label};`);
1172
+ }
1173
+ }
1174
+
1175
+ lines.push('}');
1176
+ return lines.join('\n');
1177
+ }
1178
+
1179
+ /**
1180
+ * Print the graph to console.
1181
+ * @param options - Display settings passed to `toVisual`.
1182
+ */
1183
+ override print(options?: { showWeight?: boolean }): void {
1184
+ console.log(this.toVisual(options));
1185
+ }
1081
1186
  }
@@ -180,6 +180,10 @@ export class DirectedGraph<
180
180
  super(options);
181
181
  }
182
182
 
183
+ protected override get _edgeConnector(): string {
184
+ return '->';
185
+ }
186
+
183
187
  protected _outEdgeMap: Map<VO, EO[]> = new Map<VO, EO[]>();
184
188
 
185
189
  get outEdgeMap(): Map<VO, EO[]> {
@@ -566,6 +566,101 @@ export class UndirectedGraph<
566
566
  };
567
567
  }
568
568
 
569
+ /**
570
+ * Find biconnected components using edge-stack Tarjan variant.
571
+ * A biconnected component is a maximal biconnected subgraph.
572
+ * @returns Array of edge arrays, each representing a biconnected component.
573
+ * @remarks Time O(V + E), Space O(V + E)
574
+ */
575
+ getBiconnectedComponents(): EO[][] {
576
+ const dfn = new Map<VO, number>();
577
+ const low = new Map<VO, number>();
578
+ const edgeStack: EO[] = [];
579
+ const components: EO[][] = [];
580
+ let time = 0;
581
+
582
+ const dfs = (vertex: VO, parent: VO | undefined) => {
583
+ dfn.set(vertex, time);
584
+ low.set(vertex, time);
585
+ time++;
586
+
587
+ const neighbors = this.getNeighbors(vertex);
588
+ let childCount = 0;
589
+
590
+ for (const neighbor of neighbors) {
591
+ const edge = this.getEdge(vertex, neighbor);
592
+ if (!edge) continue;
593
+
594
+ if (!dfn.has(neighbor)) {
595
+ childCount++;
596
+ edgeStack.push(edge);
597
+ dfs(neighbor, vertex);
598
+ low.set(vertex, Math.min(low.get(vertex)!, low.get(neighbor)!));
599
+
600
+ // Articulation point found — pop edges to form a component
601
+ if (
602
+ (parent === undefined && childCount > 1) ||
603
+ (parent !== undefined && low.get(neighbor)! >= dfn.get(vertex)!)
604
+ ) {
605
+ const component: EO[] = [];
606
+ let e: EO | undefined;
607
+ do {
608
+ e = edgeStack.pop();
609
+ if (e) component.push(e);
610
+ } while (e && e !== edge);
611
+ if (component.length > 0) components.push(component);
612
+ }
613
+ } else if (neighbor !== parent && dfn.get(neighbor)! < dfn.get(vertex)!) {
614
+ // Back edge (only push once per undirected edge)
615
+ edgeStack.push(edge);
616
+ low.set(vertex, Math.min(low.get(vertex)!, dfn.get(neighbor)!));
617
+ }
618
+ }
619
+ };
620
+
621
+ for (const vertex of this.vertexMap.values()) {
622
+ if (!dfn.has(vertex)) {
623
+ dfs(vertex, undefined);
624
+ // Remaining edges form a component
625
+ if (edgeStack.length > 0) {
626
+ components.push([...edgeStack]);
627
+ edgeStack.length = 0;
628
+ }
629
+ }
630
+ }
631
+
632
+ return components;
633
+ }
634
+
635
+ /**
636
+ * Detect whether the graph contains a cycle.
637
+ * Uses DFS with parent tracking.
638
+ * @returns `true` if a cycle exists, `false` otherwise.
639
+ * @remarks Time O(V + E), Space O(V)
640
+ */
641
+ hasCycle(): boolean {
642
+ const visited = new Set<VO>();
643
+
644
+ const dfs = (vertex: VO, parent: VO | undefined): boolean => {
645
+ visited.add(vertex);
646
+ for (const neighbor of this.getNeighbors(vertex)) {
647
+ if (!visited.has(neighbor)) {
648
+ if (dfs(neighbor, vertex)) return true;
649
+ } else if (neighbor !== parent) {
650
+ return true; // back edge = cycle
651
+ }
652
+ }
653
+ return false;
654
+ };
655
+
656
+ for (const vertex of this.vertexMap.values()) {
657
+ if (!visited.has(vertex)) {
658
+ if (dfs(vertex, undefined)) return true;
659
+ }
660
+ }
661
+ return false;
662
+ }
663
+
569
664
  /**
570
665
  * Get bridges discovered by `tarjan()`.
571
666
  * @returns Array of edges that are bridges.
@@ -15,6 +15,7 @@ import type {
15
15
  } from '../../types';
16
16
  import { IterableEntryBase } from '../base';
17
17
  import { isWeakKey, rangeCheck } from '../../utils';
18
+ import { ERR } from '../../common';
18
19
 
19
20
  /**
20
21
  * Hash-based map. Supports object keys and custom hashing; offers O(1) average set/get/has.
@@ -534,8 +535,8 @@ export class LinkedHashMap<K = any, V = any, R = [K, V]> extends IterableEntryBa
534
535
  if (this.isEntry(rawElement)) {
535
536
  return rawElement;
536
537
  }
537
- throw new Error(
538
- 'If `entryOrRawElements` does not adhere to [key,value], provide `options.toEntryFn` to transform raw records.'
538
+ throw new TypeError(
539
+ ERR.invalidArgument('If elements do not adhere to [key, value], provide options.toEntryFn to transform raw records.', 'HashMap')
539
540
  );
540
541
  };
541
542
 
@@ -797,6 +798,16 @@ export class LinkedHashMap<K = any, V = any, R = [K, V]> extends IterableEntryBa
797
798
  }
798
799
 
799
800
  protected _deleteNode(node: HashMapLinkedNode<K, V | undefined>): boolean {
801
+ // Remove from hash table
802
+ const key: unknown = node.key;
803
+ if (isWeakKey(key)) {
804
+ this._objMap.delete(key);
805
+ } else {
806
+ const hash = this._hashFn(key as K);
807
+ delete this._noObjMap[hash];
808
+ }
809
+
810
+ // Remove from linked list
800
811
  const { prev, next } = node;
801
812
  prev.next = next;
802
813
  next.prev = prev;
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { Comparator, DFSOrderPattern, ElementCallback, HeapOptions } from '../../types';
10
10
  import { IterableElementBase } from '../base';
11
+ import { ERR } from '../../common';
11
12
 
12
13
  /**
13
14
  * Binary heap with pluggable comparator; supports fast insertion and removal of the top element.
@@ -611,7 +612,7 @@ export class Heap<E = any, R = any> extends IterableElementBase<E, R> {
611
612
  thisArg?: unknown
612
613
  ): Heap<EM, RM> {
613
614
  const { comparator, toElementFn, ...rest } = options ?? {};
614
- if (!comparator) throw new TypeError('Heap.map requires options.comparator for EM');
615
+ if (!comparator) throw new TypeError(ERR.comparatorRequired('Heap.map'));
615
616
  const out = this._createLike<EM, RM>([], { ...rest, comparator, toElementFn });
616
617
  let i = 0;
617
618
  for (const x of this) {
@@ -641,7 +642,7 @@ export class Heap<E = any, R = any> extends IterableElementBase<E, R> {
641
642
 
642
643
  protected readonly _DEFAULT_COMPARATOR: Comparator<E> = (a: E, b: E): number => {
643
644
  if (typeof a === 'object' || typeof b === 'object') {
644
- throw TypeError('When comparing object types, define a custom comparator in options.');
645
+ throw new TypeError(ERR.comparatorRequired('Heap'));
645
646
  }
646
647
  if (a > b) return 1;
647
648
  if (a < b) return -1;
@@ -783,7 +784,7 @@ export class FibonacciHeap<E> {
783
784
  constructor(comparator?: Comparator<E>) {
784
785
  this.clear();
785
786
  this._comparator = comparator || this._defaultComparator;
786
- if (typeof this.comparator !== 'function') throw new Error('FibonacciHeap: comparator must be a function.');
787
+ if (typeof this.comparator !== 'function') throw new TypeError(ERR.notAFunction('comparator', 'FibonacciHeap'));
787
788
  }
788
789
 
789
790
  protected _root?: FibonacciHeapNode<E>;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { HeapOptions } from '../../types';
8
8
  import { Heap } from './heap';
9
+ import { ERR } from '../../common';
9
10
 
10
11
  /**
11
12
  * @template E
@@ -33,9 +34,7 @@ export class MaxHeap<E = any, R = any> extends Heap<E, R> {
33
34
  super(elements, {
34
35
  comparator: (a: E, b: E): number => {
35
36
  if (typeof a === 'object' || typeof b === 'object') {
36
- throw TypeError(
37
- `When comparing object types, a custom comparator must be defined in the constructor's options parameter.`
38
- );
37
+ throw new TypeError(ERR.comparatorRequired('MaxHeap'));
39
38
  }
40
39
  if (a < b) return 1;
41
40
  if (a > b) return -1;
@@ -6,6 +6,7 @@
6
6
  * @license MIT License
7
7
  */
8
8
  import type { MatrixOptions } from '../../types';
9
+ import { ERR } from '../../common';
9
10
 
10
11
  /**
11
12
  *
@@ -150,7 +151,7 @@ export class Matrix {
150
151
  */
151
152
  add(matrix: Matrix): Matrix | undefined {
152
153
  if (!this.isMatchForCalculate(matrix)) {
153
- throw new Error('Matrix dimensions must match for addition.');
154
+ throw new Error(ERR.matrixDimensionMismatch('addition'));
154
155
  }
155
156
 
156
157
  const resultData: number[][] = [];
@@ -186,7 +187,7 @@ export class Matrix {
186
187
  */
187
188
  subtract(matrix: Matrix): Matrix | undefined {
188
189
  if (!this.isMatchForCalculate(matrix)) {
189
- throw new Error('Matrix dimensions must match for subtraction.');
190
+ throw new Error(ERR.matrixDimensionMismatch('subtraction'));
190
191
  }
191
192
 
192
193
  const resultData: number[][] = [];
@@ -221,7 +222,7 @@ export class Matrix {
221
222
  */
222
223
  multiply(matrix: Matrix): Matrix | undefined {
223
224
  if (this.cols !== matrix.rows) {
224
- throw new Error('Matrix dimensions must be compatible for multiplication (A.cols = B.rows).');
225
+ throw new Error(ERR.matrixDimensionMismatch('multiplication (A.cols must equal B.rows)'));
225
226
  }
226
227
 
227
228
  const resultData: number[][] = [];
@@ -259,7 +260,7 @@ export class Matrix {
259
260
  */
260
261
  transpose(): Matrix {
261
262
  if (this.data.some(row => row.length !== this.rows)) {
262
- throw new Error('Matrix must be rectangular for transposition.');
263
+ throw new Error(ERR.matrixNotRectangular());
263
264
  }
264
265
 
265
266
  const resultData: number[][] = [];
@@ -288,7 +289,7 @@ export class Matrix {
288
289
  inverse(): Matrix | undefined {
289
290
  // Check if the matrix is square
290
291
  if (this.rows !== this.cols) {
291
- throw new Error('Matrix must be square for inversion.');
292
+ throw new Error(ERR.matrixNotSquare());
292
293
  }
293
294
 
294
295
  // Create an augmented matrix [this | I]
@@ -318,7 +319,7 @@ export class Matrix {
318
319
 
319
320
  if (pivotRow === this.rows) {
320
321
  // Matrix is singular, and its inverse does not exist
321
- throw new Error('Matrix is singular, and its inverse does not exist.');
322
+ throw new Error(ERR.matrixSingular());
322
323
  }
323
324
 
324
325
  // Swap rows to make the pivot the current row
@@ -329,7 +330,7 @@ export class Matrix {
329
330
 
330
331
  if (pivotElement === 0) {
331
332
  // Handle division by zero
332
- throw new Error('Matrix is singular, and its inverse does not exist (division by zero).');
333
+ throw new Error(ERR.matrixSingular());
333
334
  }
334
335
 
335
336
  augmentedMatrix._scaleRow(i, 1 / pivotElement);
@@ -367,9 +368,7 @@ export class Matrix {
367
368
  */
368
369
  dot(matrix: Matrix): Matrix | undefined {
369
370
  if (this.cols !== matrix.rows) {
370
- throw new Error(
371
- 'Number of columns in the first matrix must be equal to the number of rows in the second matrix for dot product.'
372
- );
371
+ throw new Error(ERR.matrixDimensionMismatch('dot product (A.cols must equal B.rows)'));
373
372
  }
374
373
 
375
374
  const resultData: number[][] = [];
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { PriorityQueueOptions } from '../../types';
9
9
  import { PriorityQueue } from './priority-queue';
10
+ import { ERR } from '../../common';
10
11
 
11
12
  /**
12
13
  * Max-oriented priority queue (max-heap) built on {@link PriorityQueue}.
@@ -28,9 +29,7 @@ export class MaxPriorityQueue<E = any, R = any> extends PriorityQueue<E, R> {
28
29
  super(elements, {
29
30
  comparator: (a: E, b: E): number => {
30
31
  if (typeof a === 'object' || typeof b === 'object') {
31
- throw TypeError(
32
- `When comparing object types, a custom comparator must be defined in the constructor's options parameter.`
33
- );
32
+ throw new TypeError(ERR.comparatorRequired('MaxPriorityQueue'));
34
33
  }
35
34
  if (a < b) return 1;
36
35
  if (a > b) return -1;
@@ -154,12 +154,15 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
154
154
  * @returns New Deque instance.
155
155
  */
156
156
 
157
+ constructor(elements?: IterableWithSizeOrLength<E>, options?: DequeOptions<E, R>);
158
+ constructor(elements: IterableWithSizeOrLength<R>, options: DequeOptions<E, R> & { toElementFn: (rawElement: R) => E });
157
159
  constructor(elements: IterableWithSizeOrLength<E> | IterableWithSizeOrLength<R> = [], options?: DequeOptions<E, R>) {
158
160
  super(options);
159
161
 
160
162
  if (options) {
161
- const { bucketSize } = options;
163
+ const { bucketSize, autoCompactRatio } = options;
162
164
  if (typeof bucketSize === 'number') this._bucketSize = bucketSize;
165
+ if (typeof autoCompactRatio === 'number') this._autoCompactRatio = autoCompactRatio;
163
166
  }
164
167
 
165
168
  let _size: number;
@@ -191,6 +194,34 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
191
194
  return this._bucketSize;
192
195
  }
193
196
 
197
+ protected _autoCompactRatio = 0.5;
198
+
199
+ /**
200
+ * Get the auto-compaction ratio.
201
+ * When `elements / (bucketCount * bucketSize)` drops below this ratio after
202
+ * enough shift/pop operations, the deque auto-compacts.
203
+ * @remarks Time O(1), Space O(1)
204
+ * @returns Current ratio threshold. 0 means auto-compact is disabled.
205
+ */
206
+ get autoCompactRatio(): number {
207
+ return this._autoCompactRatio;
208
+ }
209
+
210
+ /**
211
+ * Set the auto-compaction ratio.
212
+ * @remarks Time O(1), Space O(1)
213
+ * @param value - Ratio in [0,1]. 0 disables auto-compact.
214
+ */
215
+ set autoCompactRatio(value: number) {
216
+ this._autoCompactRatio = value;
217
+ }
218
+
219
+ /**
220
+ * Counter for shift/pop operations since last compaction check.
221
+ * Only checks ratio every `_bucketSize` operations to minimize overhead.
222
+ */
223
+ protected _compactCounter = 0;
224
+
194
225
  protected _bucketFirst = 0;
195
226
 
196
227
  /**
@@ -366,6 +397,7 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
366
397
  }
367
398
  }
368
399
  this._length -= 1;
400
+ this._autoCompact();
369
401
  return element;
370
402
  }
371
403
 
@@ -390,6 +422,7 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
390
422
  }
391
423
  }
392
424
  this._length -= 1;
425
+ this._autoCompact();
393
426
  return element;
394
427
  }
395
428
 
@@ -768,11 +801,44 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
768
801
  * @returns void
769
802
  */
770
803
 
804
+ /**
805
+ * (Protected) Trigger auto-compaction if space utilization drops below threshold.
806
+ * Only checks every `_bucketSize` operations to minimize hot-path overhead.
807
+ * Uses element-based ratio: `elements / (bucketCount * bucketSize)`.
808
+ */
809
+ protected _autoCompact(): void {
810
+ if (this._autoCompactRatio <= 0 || this._bucketCount <= 1) return;
811
+
812
+ this._compactCounter++;
813
+ if (this._compactCounter < this._bucketSize) return;
814
+ this._compactCounter = 0;
815
+
816
+ const utilization = this._length / (this._bucketCount * this._bucketSize);
817
+ if (utilization < this._autoCompactRatio) {
818
+ this.shrinkToFit();
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Compact the deque by removing unused buckets.
824
+ * @remarks Time O(N), Space O(1)
825
+ * @returns True if compaction was performed (bucket count reduced).
826
+ */
827
+ /**
828
+ * Compact the deque by removing unused buckets.
829
+ * @remarks Time O(N), Space O(1)
830
+ * @returns True if compaction was performed (bucket count reduced).
831
+ */
832
+ compact(): boolean {
833
+ const before = this._bucketCount;
834
+ this.shrinkToFit();
835
+ return this._bucketCount < before;
836
+ }
837
+
771
838
  shrinkToFit(): void {
772
839
  if (this._length === 0) return;
773
840
  const newBuckets = [] as E[][];
774
- if (this._bucketFirst === this._bucketLast) return;
775
- else if (this._bucketFirst < this._bucketLast) {
841
+ if (this._bucketFirst <= this._bucketLast) {
776
842
  for (let i = this._bucketFirst; i <= this._bucketLast; ++i) {
777
843
  newBuckets.push(this._buckets[i]);
778
844
  }
@@ -787,6 +853,8 @@ export class Deque<E = any, R = any> extends LinearBase<E, R> {
787
853
  this._bucketFirst = 0;
788
854
  this._bucketLast = newBuckets.length - 1;
789
855
  this._buckets = newBuckets;
856
+ this._bucketCount = newBuckets.length;
857
+ this._compactCounter = 0;
790
858
  }
791
859
 
792
860
  /**
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { ElementCallback, TrieOptions } from '../../types';
10
10
  import { IterableElementBase } from '../base';
11
+ import { ERR } from '../../common';
11
12
 
12
13
  /**
13
14
  * Node used by Trie to store one character and its children.
@@ -651,7 +652,7 @@ export class Trie<R = any> extends IterableElementBase<string, R> {
651
652
  for (const x of this) {
652
653
  const v = thisArg === undefined ? callback(x, i++, this) : callback.call(thisArg, x, i++, this);
653
654
  if (typeof v !== 'string') {
654
- throw new TypeError(`Trie.map callback must return string; got ${typeof v}`);
655
+ throw new TypeError(ERR.callbackReturnType('string', typeof v, 'Trie.map'));
655
656
  }
656
657
  newTrie.add(v);
657
658
  }
@@ -2,4 +2,11 @@ import { LinearBaseOptions } from '../base';
2
2
 
3
3
  export type DequeOptions<E, R> = {
4
4
  bucketSize?: number;
5
+
6
+ /**
7
+ * When the ratio of used buckets to total buckets falls below this threshold
8
+ * after a shift/pop, auto-compact is triggered. Set to 0 to disable.
9
+ * Default: 0.5 (compact when less than half the buckets are in use).
10
+ */
11
+ autoCompactRatio?: number;
5
12
  } & LinearBaseOptions<E, R>;
@@ -77,8 +77,10 @@ export const getMSB = (value: number): number => {
77
77
  * error message to be thrown if the index is out of bounds. By default, if no message is provided when
78
78
  * calling the `rangeCheck` function, the message "Index out of bounds." will be used.
79
79
  */
80
- export const rangeCheck = (index: number, min: number, max: number, message = 'Index out of bounds.'): void => {
81
- if (index < min || index > max) throw new RangeError(message);
80
+ export const rangeCheck = (index: number, min: number, max: number, message?: string): void => {
81
+ if (index < min || index > max) {
82
+ throw new RangeError(message ?? `Index ${index} is out of range [${min}, ${max}].`);
83
+ }
82
84
  };
83
85
 
84
86
  /**