├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── mirror.yml │ ├── gh-pages.yml │ ├── release.yml │ └── pr.yml ├── src ├── data-types │ ├── index.ts │ └── map.ts ├── trie │ ├── index.ts │ └── TrieNode.ts ├── print │ ├── printTree.ts │ ├── types.ts │ ├── printBinary.ts │ └── index.ts ├── avl │ ├── index.ts │ ├── __tests__ │ │ ├── AvlMap.fuzzing.spec.ts │ │ ├── AvlBstNumNumMap.spec.ts │ │ ├── AvlMap.spec.ts │ │ └── AvlSet.spec.ts │ ├── types.ts │ ├── AvlMap.ts │ ├── AvlBstNumNumMap.ts │ ├── AvlMapOld.ts │ ├── AvlSet.ts │ └── util.ts ├── splay │ ├── index.ts │ ├── util2.ts │ └── util.ts ├── llrb-tree │ ├── index.ts │ ├── __tests__ │ │ ├── llrb-utils.ts │ │ └── LlrbTree.fuzzing.spec.ts │ └── util.ts ├── red-black │ ├── index.ts │ ├── __tests__ │ │ ├── RbMap.fuzzing.spec.ts │ │ ├── RbMap.spec.ts │ │ ├── utils.ts │ │ └── RbMap.traces.spec.ts │ ├── util │ │ └── print.ts │ ├── types.ts │ ├── RbMap.ts │ └── util.ts ├── SortedMap │ ├── constants.ts │ ├── util.ts │ ├── index.ts │ ├── __tests__ │ │ └── map.spec.ts │ ├── SortedMapNode.ts │ └── SortedMapIterator.ts ├── radix │ ├── index.ts │ ├── __tests__ │ │ ├── radix.spec.ts │ │ ├── binaryRadix.spec.ts │ │ ├── Slice.spec.ts │ │ ├── BinaryRadixTree.fuzzing.spec.ts │ │ └── RadixTree.spec.ts │ ├── BinaryTrieNode.ts │ ├── RadixTree.ts │ ├── BinaryRadixTree.ts │ ├── Slice.ts │ ├── binaryRadix.ts │ └── radix.ts ├── types2.ts ├── util │ ├── first.ts │ ├── next.ts │ ├── print.ts │ ├── swap.ts │ ├── __tests__ │ │ └── swap.spec.ts │ └── index.ts ├── TreeNode.ts ├── __tests__ │ ├── types.ts │ ├── Tree.spec.ts │ ├── TraceReplay.ts │ ├── MapFuzzer.ts │ ├── util.ts │ └── util.spec.ts ├── types.ts ├── Tree.ts ├── __bench__ │ ├── payloads.ts │ ├── bench.map.insert.nums.native.ts │ ├── runBenchmark.ts │ ├── bench.map.insert.nums.libs.ts │ └── bench.map.delete.nums.libs.ts └── util2.ts ├── profiler └── rb-remove.js ├── tsconfig.build.json ├── SECURITY.md ├── tslint.json ├── tsconfig.json ├── .gitignore ├── README.md └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: streamich 2 | -------------------------------------------------------------------------------- /src/data-types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './map'; 2 | -------------------------------------------------------------------------------- /src/trie/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TrieNode'; 2 | -------------------------------------------------------------------------------- /src/print/printTree.ts: -------------------------------------------------------------------------------- 1 | export * from 'tree-dump/lib/printTree'; 2 | -------------------------------------------------------------------------------- /src/print/types.ts: -------------------------------------------------------------------------------- 1 | export type * from 'tree-dump/lib/types'; 2 | -------------------------------------------------------------------------------- /src/avl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './util'; 3 | -------------------------------------------------------------------------------- /src/print/printBinary.ts: -------------------------------------------------------------------------------- 1 | export * from 'tree-dump/lib/printBinary'; 2 | -------------------------------------------------------------------------------- /src/splay/index.ts: -------------------------------------------------------------------------------- 1 | export * from './util'; 2 | export * from './util2'; 3 | -------------------------------------------------------------------------------- /src/llrb-tree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LlrbTree'; 2 | export * from './util'; 3 | -------------------------------------------------------------------------------- /src/red-black/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './util'; 3 | -------------------------------------------------------------------------------- /src/SortedMap/constants.ts: -------------------------------------------------------------------------------- 1 | export const enum IteratorType { 2 | NORMAL = 0, 3 | REVERSE = 1, 4 | } 5 | -------------------------------------------------------------------------------- /src/print/index.ts: -------------------------------------------------------------------------------- 1 | export * from './printTree'; 2 | export * from './printBinary'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/SortedMap/util.ts: -------------------------------------------------------------------------------- 1 | export function throwIteratorAccessError() { 2 | throw new RangeError('Iterator access denied!'); 3 | } 4 | -------------------------------------------------------------------------------- /src/SortedMap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SortedMap'; 2 | export * from './SortedMapNode'; 3 | export * from './SortedMapIterator'; 4 | export * from './constants'; 5 | -------------------------------------------------------------------------------- /src/radix/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RadixTree'; 2 | export * from './radix'; 3 | export * from './BinaryRadixTree'; 4 | export * from './BinaryTrieNode'; 5 | export * from './Slice'; 6 | -------------------------------------------------------------------------------- /src/types2.ts: -------------------------------------------------------------------------------- 1 | export interface HeadlessNode2 { 2 | p2: HeadlessNode2 | undefined; 3 | l2: HeadlessNode2 | undefined; 4 | r2: HeadlessNode2 | undefined; 5 | } 6 | 7 | export type Comparator2 = (a: T, b: T) => number; 8 | -------------------------------------------------------------------------------- /src/util/first.ts: -------------------------------------------------------------------------------- 1 | import type {HeadlessNode} from '../types'; 2 | 3 | export const first = (root: N | undefined): N | undefined => { 4 | let curr = root; 5 | while (curr) 6 | if (curr.l) curr = curr.l as N; 7 | else return curr; 8 | return curr; 9 | }; 10 | -------------------------------------------------------------------------------- /src/util/next.ts: -------------------------------------------------------------------------------- 1 | import {first} from './first'; 2 | import type {HeadlessNode} from '../types'; 3 | 4 | export const next = (curr: N): N | undefined => { 5 | const r = curr.r as N | undefined; 6 | if (r) return first(r); 7 | let p = curr.p as N; 8 | while (p && p.r === curr) { 9 | curr = p; 10 | p = p.p as N; 11 | } 12 | return p; 13 | }; 14 | -------------------------------------------------------------------------------- /src/TreeNode.ts: -------------------------------------------------------------------------------- 1 | import {ITreeNode} from './types'; 2 | 3 | export class TreeNode implements ITreeNode { 4 | public p: ITreeNode | undefined = undefined; 5 | public l: ITreeNode | undefined = undefined; 6 | public r: ITreeNode | undefined = undefined; 7 | 8 | constructor( 9 | public k: K, 10 | public v: V, 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /profiler/rb-remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NODE_ENV=production node --prof profiler/rb-remove.js 3 | * node --prof-process isolate-0xnnnnnnnnnnnn-v8.log 4 | */ 5 | 6 | const {RbMap} = require('../lib/red-black/RbMap'); 7 | 8 | const map = new RbMap(); 9 | const iterations = 30000000; 10 | 11 | for (let j = 0; j < iterations; j++) { 12 | map.set(j, j); 13 | } 14 | 15 | for (let j = 0; j < iterations; j++) { 16 | map.del(j); 17 | } 18 | -------------------------------------------------------------------------------- /src/avl/__tests__/AvlMap.fuzzing.spec.ts: -------------------------------------------------------------------------------- 1 | import {MapFuzzer} from '../../__tests__/MapFuzzer'; 2 | import {AvlMap} from '../AvlMap'; 3 | 4 | describe('AvlMap fuzzing', () => { 5 | for (let i = 0; i < 50; i++) { 6 | test(`map instance ${i}`, () => { 7 | const map = new AvlMap(); 8 | const fuzzer = new MapFuzzer(map); 9 | for (let j = 0; j < 1000; j++) fuzzer.runStep(); 10 | }); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/red-black/__tests__/RbMap.fuzzing.spec.ts: -------------------------------------------------------------------------------- 1 | import {MapFuzzer} from '../../__tests__/MapFuzzer'; 2 | import {RbMap} from '../RbMap'; 3 | 4 | describe('RbMap fuzzing', () => { 5 | for (let i = 0; i < 50; i++) { 6 | test(`map instance ${i}`, () => { 7 | const map = new RbMap(); 8 | const fuzzer = new MapFuzzer(map); 9 | for (let j = 0; j < 1000; j++) fuzzer.runStep(); 10 | }); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/types.ts: -------------------------------------------------------------------------------- 1 | import type {SonicMap} from '../types'; 2 | 3 | export type Trace = TraceStep[]; 4 | export type TraceStep = TraceStepInsert | TraceStepDelete | TraceStepClear; 5 | export type TraceStepInsert = ['insert', number, number]; 6 | export type TraceStepDelete = ['delete', number]; 7 | export type TraceStepClear = ['clear']; 8 | 9 | export type FuzzerSonicMap = Pick, 'set' | 'del' | 'clear' | 'size' | 'get'>; 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | }, 5 | "exclude": [ 6 | "src/demo", 7 | "src/__tests__", 8 | "src/**/__tests__/**/*.*", 9 | "src/**/__bench__/**/*.*", 10 | "src/**/__mocks__/**/*.*", 11 | "src/**/__jest__/**/*.*", 12 | "src/**/__mocha__/**/*.*", 13 | "src/**/__tap__/**/*.*", 14 | "src/**/__tape__/**/*.*", 15 | "*.test.ts", 16 | "*.spec.ts" 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "lockFileMaintenance": { 5 | "enabled": true, 6 | "automerge": true 7 | }, 8 | "rangeStrategy": "replace", 9 | "postUpdateOptions": ["yarnDedupeHighest"], 10 | "packageRules": [ 11 | { 12 | "matchUpdateTypes": ["minor", "patch"], 13 | "matchCurrentVersion": "!/^0/", 14 | "automerge": true 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. The latest major version 6 | will support security patches. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please report (suspected) security vulnerabilities to 11 | **[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond 12 | within 48 hours. If the issue is confirmed, we will release a patch as soon 13 | as possible depending on complexity. 14 | -------------------------------------------------------------------------------- /src/avl/__tests__/AvlBstNumNumMap.spec.ts: -------------------------------------------------------------------------------- 1 | import {AvlBstNumNumMap} from '../AvlBstNumNumMap'; 2 | 3 | test('smoke test', () => { 4 | const tree = new AvlBstNumNumMap(); 5 | tree.set(1, 1); 6 | tree.set(3, 5); 7 | tree.set(4, 5); 8 | tree.set(3, 15); 9 | tree.set(4.1, 0); 10 | tree.set(44, 123); 11 | // console.log(tree + ''); 12 | expect(tree.get(44)).toBe(123); 13 | const keys: number[] = []; 14 | tree.forEach((node) => keys.push(node.k)); 15 | expect(keys).toEqual([1, 3, 4, 4.1, 44]); 16 | }); 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-common" 4 | ], 5 | "rules": { 6 | "unified-signatures": false, 7 | "no-conditional-assignment": false, 8 | "variable-name": false, 9 | "one-variable-per-declaration": false, 10 | "no-unnecessary-class": false, 11 | "no-unnecessary-initializer": false, 12 | "no-this-assignment": false, 13 | "no-duplicate-imports": false, 14 | "no-angle-bracket-type-assertion": false, 15 | "ban-comma-operator": false, 16 | "no-unused-expression": false, 17 | "no-implicit-dependencies": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/util/print.ts: -------------------------------------------------------------------------------- 1 | import {printBinary} from '../print/printBinary'; 2 | import type {HeadlessNode, ITreeNode} from '../types'; 3 | 4 | const stringify = JSON.stringify; 5 | 6 | export const print = (node: undefined | HeadlessNode, tab: string = ''): string => { 7 | if (!node) return '∅'; 8 | const {l, r, k, v} = node as ITreeNode; 9 | const content = k !== undefined ? ` { ${stringify(k)} = ${stringify(v)} }` : ''; 10 | return ( 11 | node.constructor.name + 12 | content + 13 | printBinary(tab, [l ? (tab) => print(l, tab) : null, r ? (tab) => print(r, tab) : null]) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | mirror: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Push To Gitlab 15 | env: 16 | token: ${{ secrets.GITLAB_TOKEN }} 17 | run: | 18 | git config user.name "streamich" 19 | git config user.email "sonic-forest+streamich@users.noreply.github.com" 20 | git remote add mirror "https://oauth2:${token}@gitlab.com/streamich/sonic-forest.git" 21 | git push mirror master 22 | -------------------------------------------------------------------------------- /src/red-black/util/print.ts: -------------------------------------------------------------------------------- 1 | import {printBinary} from '../../print/printBinary'; 2 | import type {IRbTreeNode, RbHeadlessNode} from '../types'; 3 | 4 | const stringify = JSON.stringify; 5 | 6 | export const print = (node: undefined | RbHeadlessNode | IRbTreeNode, tab: string = ''): string => { 7 | if (!node) return '∅'; 8 | const {b, l, r, k, v} = node as IRbTreeNode; 9 | const content = k !== undefined ? ` { ${stringify(k)} = ${stringify(v)} }` : ''; 10 | const bfFormatted = !b ? ` [red]` : ''; 11 | return ( 12 | node.constructor.name + 13 | `${bfFormatted}` + 14 | content + 15 | printBinary(tab, [l ? (tab) => print(l, tab) : null, r ? (tab) => print(r, tab) : null]) 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | gh-pages: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: yarn 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn typedoc 22 | - run: yarn coverage 23 | - run: yarn build:pages 24 | - name: Publish to gh-pages 25 | uses: peaceiris/actions-gh-pages@v4 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./gh-pages 29 | -------------------------------------------------------------------------------- /src/red-black/types.ts: -------------------------------------------------------------------------------- 1 | export interface IRbTreeNode { 2 | /** Parent. */ 3 | p: IRbTreeNode | undefined; 4 | /** Left. */ 5 | l: IRbTreeNode | undefined; 6 | /** Right. */ 7 | r: IRbTreeNode | undefined; 8 | /** Node key. */ 9 | k: K; 10 | /** Node value. */ 11 | v: V; 12 | /** Whether the node is "black". */ 13 | b: boolean; 14 | } 15 | 16 | export interface RbHeadlessNode { 17 | p: RbHeadlessNode | undefined; 18 | l: RbHeadlessNode | undefined; 19 | r: RbHeadlessNode | undefined; 20 | /** Whether the node is "black". */ 21 | b: boolean; 22 | } 23 | 24 | export interface RbNodeReference { 25 | /** 26 | * Immutable read-only key of the node. 27 | */ 28 | readonly k: N['k']; 29 | 30 | /** 31 | * Mutable value of the node. The fastest way to update mutate tree nodes 32 | * is to get hold of ${@link RbNodeReference} and update this value directly. 33 | */ 34 | v: N['v']; 35 | } 36 | -------------------------------------------------------------------------------- /src/__tests__/Tree.spec.ts: -------------------------------------------------------------------------------- 1 | import {Tree} from '../Tree'; 2 | 3 | test('works', () => { 4 | const tree = new Tree(); 5 | expect(tree.size).toBe(0); 6 | tree.set(1, 'a'); 7 | expect(tree.size).toBe(1); 8 | expect(tree.get(1)).toBe('a'); 9 | expect(tree.getOrNextLower(1)).toBe('a'); 10 | expect(tree.getOrNextLower(2)).toBe('a'); 11 | tree.set(5, 'b'); 12 | expect(tree.get(1)).toBe('a'); 13 | expect(tree.get(5)).toBe('b'); 14 | expect(tree.getOrNextLower(2)).toBe('a'); 15 | expect(tree.getOrNextLower(5)).toBe('b'); 16 | expect(tree.getOrNextLower(6)).toBe('b'); 17 | tree.set(6, 'c'); 18 | expect(tree.getOrNextLower(6)).toBe('c'); 19 | expect(tree.getOrNextLower(6.1)).toBe('c'); 20 | tree.set(5.5, 'd'); 21 | expect(tree.getOrNextLower(5.5)).toBe('d'); 22 | expect(tree.getOrNextLower(5.6)).toBe('d'); 23 | tree.set(5.4, 'e'); 24 | expect(tree.getOrNextLower(5.45)).toBe('e'); 25 | tree.set(5.45, 'f'); 26 | expect(tree.getOrNextLower(5.45)).toBe('f'); 27 | }); 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | if: 10 | ${{ github.event_name == 'push' && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/next') }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: yarn 22 | - run: yarn install --frozen-lockfile 23 | - run: yarn prettier:check 24 | - run: yarn lint 25 | - run: yarn test:ci --ci 26 | - run: yarn build 27 | - name: Semantic Release 28 | uses: cycjimmy/semantic-release-action@v4 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /src/avl/types.ts: -------------------------------------------------------------------------------- 1 | import type {ITreeNode} from '../types'; 2 | 3 | export interface IAvlTreeNode { 4 | /** Parent. */ 5 | p: IAvlTreeNode | undefined; 6 | /** Left. */ 7 | l: IAvlTreeNode | undefined; 8 | /** Right. */ 9 | r: IAvlTreeNode | undefined; 10 | /** Node key. */ 11 | k: K; 12 | /** Node value. */ 13 | v: V; 14 | /** Balance factor. */ 15 | bf: number; 16 | } 17 | 18 | export interface AvlHeadlessNode { 19 | p: AvlHeadlessNode | undefined; 20 | l: AvlHeadlessNode | undefined; 21 | r: AvlHeadlessNode | undefined; 22 | /** Balance factor. */ 23 | bf: number; 24 | } 25 | 26 | export interface AvlNodeReference> { 27 | /** 28 | * Immutable read-only key of the node. 29 | */ 30 | readonly k: N['k']; 31 | 32 | /** 33 | * Mutable value of the node. The fastest way to update mutate tree nodes 34 | * is to get hold of ${@link AvlNodeReference} and update this value directly. 35 | */ 36 | v: N['v']; 37 | } 38 | -------------------------------------------------------------------------------- /src/llrb-tree/__tests__/llrb-utils.ts: -------------------------------------------------------------------------------- 1 | import {assertRedBlackTree} from '../../red-black/__tests__/utils'; 2 | import {LlrbNode} from '../LlrbTree'; 3 | 4 | export const assertLlrbTree = (root?: LlrbNode): void => { 5 | // First, verify it's a valid red-black tree 6 | assertRedBlackTree(root as any); 7 | 8 | if (!root) return; 9 | 10 | // Then, verify the left-leaning property 11 | assertLeftLeaningProperty(root); 12 | }; 13 | 14 | const assertLeftLeaningProperty = (node: LlrbNode): void => { 15 | // Left-leaning property: if a node has only one red child, it must be the left child 16 | const {l, r, b} = node; 17 | 18 | // If right child is red, left child must also be red (no lone right red links) 19 | if (r && !r.b && (!l || l.b)) { 20 | throw new Error( 21 | `Left-leaning property violated: node has red right child but black/null left child at key ${node.k}`, 22 | ); 23 | } 24 | 25 | // Recursively check children 26 | if (l) assertLeftLeaningProperty(l); 27 | if (r) assertLeftLeaningProperty(r); 28 | }; 29 | -------------------------------------------------------------------------------- /src/trie/TrieNode.ts: -------------------------------------------------------------------------------- 1 | import {print, toRecord} from '../radix/radix'; 2 | import {first, next} from '../util'; 3 | import type {Printable} from '../print/types'; 4 | import type {ITreeNode} from '../types'; 5 | 6 | export class TrieNode implements ITreeNode, Printable { 7 | public p: TrieNode | undefined = undefined; 8 | public l: TrieNode | undefined = undefined; 9 | public r: TrieNode | undefined = undefined; 10 | public children: TrieNode | undefined = undefined; 11 | 12 | constructor( 13 | public k: string, 14 | public v: V, 15 | ) {} 16 | 17 | public forChildren(callback: (child: TrieNode, index: number) => void): void { 18 | let child = first(this.children); 19 | let i = 0; 20 | while (child) { 21 | callback(child, 0); 22 | i++; 23 | child = next(child); 24 | } 25 | } 26 | 27 | public toRecord(prefix?: string, record?: Record): Record { 28 | return toRecord(this, prefix, record); 29 | } 30 | 31 | public toString(tab: string = ''): string { 32 | return print(this, tab); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | unit: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.x] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: yarn 24 | - run: yarn install --frozen-lockfile 25 | - run: yarn test:ci --ci 26 | lint-and-co: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | node-version: [20.x] 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: yarn 38 | - run: yarn install --frozen-lockfile 39 | - run: yarn build 40 | - run: yarn lint 41 | - run: yarn prettier:check 42 | - run: yarn typedoc 43 | -------------------------------------------------------------------------------- /src/__tests__/TraceReplay.ts: -------------------------------------------------------------------------------- 1 | import {assertMapContents} from './util'; 2 | import type {SonicMap} from '../types'; 3 | import type {Trace, TraceStep} from './types'; 4 | 5 | export class TraceReplay { 6 | public readonly twin: Map = new Map(); 7 | 8 | constructor( 9 | public readonly trace: Trace, 10 | public readonly beforeStep?: (step: TraceStep, replay: TraceReplay) => void, 11 | ) {} 12 | 13 | public run(map: SonicMap): void { 14 | this.twin.clear(); 15 | for (const step of this.trace) { 16 | const [type, k, v] = step; 17 | this.beforeStep?.(step, this); 18 | switch (type) { 19 | case 'insert': { 20 | map.set(k, v); 21 | this.twin.set(k, v); 22 | this.assertContents(map); 23 | break; 24 | } 25 | case 'delete': { 26 | map.del(k); 27 | this.twin.delete(k); 28 | this.assertContents(map); 29 | break; 30 | } 31 | case 'clear': { 32 | map.clear(); 33 | this.twin.clear(); 34 | this.assertContents(map); 35 | break; 36 | } 37 | } 38 | } 39 | } 40 | 41 | public assertContents(map: SonicMap): void { 42 | assertMapContents(map, this.twin); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SortedMap/__tests__/map.spec.ts: -------------------------------------------------------------------------------- 1 | import {SortedMap} from '../SortedMap'; 2 | 3 | test('numbers from 0 to 100', () => { 4 | const map = new SortedMap(); 5 | for (let i = 0; i <= 100; i++) { 6 | map.setElement(i, i); 7 | expect(map.size()).toBe(i + 1); 8 | } 9 | for (let i = 0; i <= 100; i++) { 10 | map.eraseElementByKey(i); 11 | expect(map.size()).toBe(100 - i); 12 | } 13 | }); 14 | 15 | test('numbers going both directions from 50', () => { 16 | const map = new SortedMap(); 17 | for (let i = 1; i <= 100; i++) { 18 | map.setElement(50 + i, 50 + i); 19 | map.setElement(50 - i, 50 - i); 20 | expect(map.size()).toBe((i - 1) * 2 + 2); 21 | } 22 | for (let i = 1; i <= 100; i++) { 23 | map.eraseElementByKey(50 - i); 24 | map.eraseElementByKey(50 + i); 25 | } 26 | expect(map.size()).toBe(0); 27 | }); 28 | 29 | test('random numbers from 0 to 100', () => { 30 | const map = new SortedMap(); 31 | for (let i = 0; i <= 1000; i++) { 32 | const num = (Math.random() * 100) | 0; 33 | const found = map.getElementByKey(num) !== undefined; 34 | if (!found) map.setElement(num, num); 35 | } 36 | const size1 = map.size(); 37 | expect(size1 > 4).toBe(true); 38 | for (let i = 0; i <= 400; i++) { 39 | const num = (Math.random() * 100) | 0; 40 | map.eraseElementByKey(num); 41 | } 42 | const size2 = map.size(); 43 | expect(size2 < size1).toBe(true); 44 | }); 45 | -------------------------------------------------------------------------------- /src/util/swap.ts: -------------------------------------------------------------------------------- 1 | import type {HeadlessNode} from '../types'; 2 | 3 | /** 4 | * Swaps two node positions in a binary tree. 5 | * 6 | * @param x Node to swap 7 | * @param y Another node to swap 8 | * @returns New root node 9 | */ 10 | export const swap = (root: N, x: N, y: N): N => { 11 | const xp = x.p; 12 | const xl = x.l; 13 | const xr = x.r; 14 | const yp = y.p; 15 | const yl = y.l; 16 | const yr = y.r; 17 | if (yl === x) { 18 | x.l = y; 19 | y.p = x; 20 | } else { 21 | x.l = yl; 22 | if (yl) yl.p = x; 23 | } 24 | if (yr === x) { 25 | x.r = y; 26 | y.p = x; 27 | } else { 28 | x.r = yr; 29 | if (yr) yr.p = x; 30 | } 31 | if (xl === y) { 32 | y.l = x; 33 | x.p = y; 34 | } else { 35 | y.l = xl; 36 | if (xl) xl.p = y; 37 | } 38 | if (xr === y) { 39 | y.r = x; 40 | x.p = y; 41 | } else { 42 | y.r = xr; 43 | if (xr) xr.p = y; 44 | } 45 | if (!xp) { 46 | root = y; 47 | y.p = undefined; 48 | } else if (xp !== y) { 49 | y.p = xp; 50 | if (xp) { 51 | if (xp.l === x) { 52 | xp.l = y; 53 | } else { 54 | xp.r = y; 55 | } 56 | } 57 | } 58 | if (!yp) { 59 | root = x; 60 | x.p = undefined; 61 | } else if (yp !== x) { 62 | x.p = yp; 63 | if (yp) { 64 | if (yp.l === y) { 65 | yp.l = x; 66 | } else { 67 | yp.r = x; 68 | } 69 | } 70 | } 71 | return root; 72 | }; 73 | -------------------------------------------------------------------------------- /src/__tests__/MapFuzzer.ts: -------------------------------------------------------------------------------- 1 | import {assertMapContents} from './util'; 2 | import type {FuzzerSonicMap, Trace} from './types'; 3 | 4 | export class MapFuzzer { 5 | public readonly twin: Map = new Map(); 6 | public readonly trace: Trace = []; 7 | 8 | constructor(public readonly map: FuzzerSonicMap) {} 9 | 10 | public runStep(): void { 11 | const insertCount = Math.random() < 0.5 ? 0 : Math.round(Math.random() * 10); 12 | const deleteCount = Math.random() < 0.5 ? Math.round(Math.random() * 40) : Math.round(Math.random() * 10); 13 | const doClear = Math.random() < 0.1; 14 | for (let i = 0; i < insertCount; i++) { 15 | const number = Math.round(Math.random() * 100); 16 | this.map.set(number, number); 17 | this.twin.set(number, number); 18 | this.trace.push(['insert', number, number]); 19 | this.assertContents(); 20 | } 21 | for (let i = 0; i < deleteCount; i++) { 22 | const number = Math.round(Math.random() * 100); 23 | this.map.del(number); 24 | this.twin.delete(number); 25 | this.trace.push(['delete', number]); 26 | this.assertContents(); 27 | } 28 | if (doClear) { 29 | this.map.clear(); 30 | this.twin.clear(); 31 | this.trace.push(['clear']); 32 | this.assertContents(); 33 | } 34 | } 35 | 36 | public assertContents(): void { 37 | try { 38 | assertMapContents(this.map, this.twin); 39 | } catch (error) { 40 | // tslint:disable-next-line no-console 41 | console.log('Trace:', JSON.stringify(this.trace)); 42 | throw error; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "removeComments": true, 7 | "noImplicitAny": true, 8 | "allowJs": false, 9 | "allowSyntheticDefaultImports": true, 10 | "skipDefaultLibCheck": true, 11 | "skipLibCheck": true, 12 | "importHelpers": true, 13 | "pretty": true, 14 | "sourceMap": false, 15 | "strict": true, 16 | "jsx": "react", 17 | "esModuleInterop": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "noEmitHelpers": true, 20 | "noEmitOnError": true, 21 | "noErrorTruncation": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noImplicitReturns": true, 24 | "declaration": true, 25 | "downlevelIteration": true, 26 | "isolatedModules": false, 27 | "lib": ["es2018", "es2017", "esnext", "dom", "esnext.asynciterable"], 28 | "outDir": "./lib" 29 | }, 30 | "include": ["src"], 31 | "exclude": [ 32 | "node_modules", 33 | "lib", 34 | "es6", 35 | "es2020", 36 | "esm", 37 | "docs", 38 | "README.md" 39 | ], 40 | "typedocOptions": { 41 | "entryPoints": [ 42 | "src/avl/index.ts", 43 | "src/red-black/index.ts", 44 | "src/radix/index.ts", 45 | "src/util/index.ts", 46 | "src/SortedMap/index.ts", 47 | "src/llrb-tree/index.ts", 48 | "src/splay/index.ts", 49 | "src/trie/index.ts", 50 | "src/print/index.ts", 51 | "src/data-types/index.ts", 52 | "src/types.ts", 53 | "src/types2.ts" 54 | ], 55 | "out": "typedocs" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type {Printable} from 'tree-dump'; 2 | 3 | export interface ITreeNode { 4 | /** Parent. */ 5 | p: ITreeNode | undefined; 6 | /** Left. */ 7 | l: ITreeNode | undefined; 8 | /** Right. */ 9 | r: ITreeNode | undefined; 10 | /** Node key. */ 11 | k: K; 12 | /** Node value. */ 13 | v: V; 14 | } 15 | 16 | export interface HeadlessNode { 17 | p: HeadlessNode | undefined; 18 | l: HeadlessNode | undefined; 19 | r: HeadlessNode | undefined; 20 | } 21 | 22 | export type Comparator = (a: T, b: T) => number; 23 | 24 | export interface SonicNodePublicReference> { 25 | /** 26 | * Immutable read-only key of the node. 27 | */ 28 | readonly k: N['k']; 29 | 30 | /** 31 | * Mutable value of the node. The fastest way to update mutate tree nodes 32 | * is to get hold of ${@link AvlNodeReference} and update this value directly. 33 | */ 34 | v: N['v']; 35 | } 36 | 37 | export interface SonicMap = ITreeNode> extends Printable { 38 | root: Node | undefined; 39 | comparator: Comparator; 40 | get(k: K): V | undefined; 41 | del(k: K): boolean; 42 | clear(): void; 43 | has(k: K): boolean; 44 | size(): number; 45 | isEmpty(): boolean; 46 | next: (curr: N) => N | undefined; 47 | set(k: K, v: V): SonicNodePublicReference; 48 | first(): SonicNodePublicReference | undefined; 49 | last(): SonicNodePublicReference | undefined; 50 | find(k: K): SonicNodePublicReference | undefined; 51 | getOrNextLower(k: K): SonicNodePublicReference | undefined; 52 | forEach(fn: (node: SonicNodePublicReference) => void): void; 53 | iterator0(): () => undefined | SonicNodePublicReference; 54 | iterator(): Iterator>; 55 | entries(): IterableIterator>; 56 | } 57 | -------------------------------------------------------------------------------- /src/__tests__/util.ts: -------------------------------------------------------------------------------- 1 | import {print} from '../util/print'; 2 | import type {HeadlessNode} from '../types'; 3 | import type {FuzzerSonicMap} from './types'; 4 | 5 | export const assertTreeLinks = (node: HeadlessNode): void => { 6 | const {l, r, p} = node; 7 | if (l) { 8 | if (l.p !== node) { 9 | // tslint:disable-next-line: no-console 10 | console.log('at node:\n\n' + print(node)); 11 | throw new Error('Left child has wrong parent'); 12 | } 13 | assertTreeLinks(l); 14 | } else if (l !== undefined) { 15 | // tslint:disable-next-line: no-console 16 | console.log('at node:\n\n' + print(node)); 17 | throw new Error('Empty left child is not undefined'); 18 | } 19 | if (r) { 20 | if (r.p !== node) { 21 | // tslint:disable-next-line: no-console 22 | console.log('at node:\n\n' + print(node)); 23 | throw new Error('Right child has wrong parent'); 24 | } 25 | assertTreeLinks(r); 26 | } else if (r !== undefined) { 27 | // tslint:disable-next-line: no-console 28 | console.log('at node:\n\n' + print(node)); 29 | throw new Error('Empty right child is not undefined'); 30 | } 31 | if (p) { 32 | if (p.l !== node && p.r !== node) { 33 | // tslint:disable-next-line: no-console 34 | console.log('at node:\n\n' + print(node)); 35 | throw new Error('Parent does not link to node'); 36 | } 37 | } else if (p !== undefined) { 38 | // tslint:disable-next-line: no-console 39 | console.log('at node:\n\n' + print(node)); 40 | throw new Error('Empty parent is not undefined'); 41 | } 42 | }; 43 | 44 | export const assertMapContents = (map: FuzzerSonicMap, twin: Map): void => { 45 | if (map.size() !== twin.size) { 46 | throw new Error(`Size mismatch: ${map.size()} !== ${twin.size}`); 47 | } 48 | for (const [key, value] of twin) { 49 | if (map.get(key) !== value) { 50 | throw new Error(`Value mismatch for key ${key}: ${map.get(key)} !== ${value}`); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/radix/__tests__/radix.spec.ts: -------------------------------------------------------------------------------- 1 | import {TrieNode} from '../../trie/TrieNode'; 2 | import {insert} from '../radix'; 3 | import {first, last} from '../../util'; 4 | 5 | test('can insert a node with no common prefix', () => { 6 | const root = new TrieNode('', undefined); 7 | let cnt_ = 0; 8 | const cnt = () => cnt_++; 9 | insert(root, 'abc', cnt()); 10 | insert(root, 'abcd', cnt()); 11 | insert(root, 'abcde', cnt()); 12 | insert(root, 'abcdx', cnt()); 13 | insert(root, 'g', cnt()); 14 | insert(root, 'gg', cnt()); 15 | insert(root, 'ga', cnt()); 16 | insert(root, 'gb', cnt()); 17 | insert(root, 'gc', cnt()); 18 | insert(root, 'gd', cnt()); 19 | insert(root, 'ge', cnt()); 20 | insert(root, 'gf', cnt()); 21 | insert(root, 'gg', cnt()); 22 | insert(root, 'gh', cnt()); 23 | insert(root, 'gh', cnt()); 24 | insert(root, 'aa', cnt()); 25 | insert(root, 'aa', cnt()); 26 | insert(root, 'aaa', cnt()); 27 | insert(root, 'aaa', cnt()); 28 | expect(root.toRecord()).toMatchObject({ 29 | abc: 0, 30 | abcd: 1, 31 | abcde: 2, 32 | abcdx: 3, 33 | aa: 16, 34 | aaa: 18, 35 | g: 4, 36 | ga: 6, 37 | gb: 7, 38 | gc: 8, 39 | gd: 9, 40 | ge: 10, 41 | gf: 11, 42 | gg: 12, 43 | gh: 14, 44 | }); 45 | }); 46 | 47 | test('constructs common prefix', () => { 48 | const root = new TrieNode('', undefined); 49 | insert(root, 'GET /users/{user}', 1); 50 | insert(root, 'GET /posts/{post}', 2); 51 | expect(first(root.children)).toBe(last(root.children)); 52 | const child = first(root.children); 53 | expect(child?.k).toBe('GET /'); 54 | expect(child?.v).toBe(undefined); 55 | expect(child?.p).toBe(undefined); 56 | expect(child?.l).toBe(undefined); 57 | expect(child?.r).toBe(undefined); 58 | expect(child?.children).not.toBe(undefined); 59 | expect(first(child?.children!)!.k).toBe('posts/{post}'); 60 | expect(last(child?.children!)!.k).toBe('users/{user}'); 61 | }); 62 | 63 | test('constructs common prefix from HTTP routes', () => { 64 | const root = new TrieNode('', undefined); 65 | insert(root, 'GET /users', 1); 66 | insert(root, 'POST /users', 2); 67 | insert(root, 'PUT /users', 3); 68 | expect(root.toRecord()).toMatchObject({ 69 | 'GET /users': 1, 70 | 'POST /users': 2, 71 | 'PUT /users': 3, 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/radix/BinaryTrieNode.ts: -------------------------------------------------------------------------------- 1 | import {Slice} from './Slice'; 2 | import {Printable} from '../print/types'; 3 | import type {ITreeNode} from '../types'; 4 | import {first, next} from '../util'; 5 | 6 | export class BinaryTrieNode implements ITreeNode, Printable { 7 | public p: BinaryTrieNode | undefined = undefined; 8 | public l: BinaryTrieNode | undefined = undefined; 9 | public r: BinaryTrieNode | undefined = undefined; 10 | public children: BinaryTrieNode | undefined = undefined; 11 | 12 | constructor( 13 | public k: Slice, 14 | public v: V, 15 | ) {} 16 | 17 | public forChildren(callback: (child: BinaryTrieNode, index: number) => void): void { 18 | let child = first(this.children); 19 | let i = 0; 20 | while (child) { 21 | callback(child, i); 22 | i++; 23 | child = next(child); 24 | } 25 | } 26 | 27 | public toRecord(prefix?: Uint8Array, record?: Record): Record { 28 | if (!record) record = {}; 29 | const currentPrefix = prefix ? new Uint8Array([...prefix, ...this.k.toUint8Array()]) : this.k.toUint8Array(); 30 | 31 | if (this.v !== undefined) { 32 | // Convert Uint8Array to string representation for record key 33 | const key = Array.from(currentPrefix).join(','); 34 | record[key] = this.v; 35 | } 36 | 37 | let child = first(this.children); 38 | while (child) { 39 | child.toRecord(currentPrefix, record); 40 | child = next(child); 41 | } 42 | 43 | return record; 44 | } 45 | 46 | public toString(tab: string = ''): string { 47 | const value = this.v === undefined ? '' : ` = ${JSON.stringify(this.v)}`; 48 | const childrenNodes: BinaryTrieNode[] = []; 49 | this.forChildren((child) => childrenNodes.push(child)); 50 | 51 | let result = `${this.constructor.name} ${this.k.toString()}${value}`; 52 | 53 | if (childrenNodes.length > 0) { 54 | result += '\n'; 55 | childrenNodes.forEach((child, index) => { 56 | const isLast = index === childrenNodes.length - 1; 57 | const prefix = isLast ? '└── ' : '├── '; 58 | const childTab = tab + (isLast ? ' ' : '│ '); 59 | result += tab + prefix + child.toString(childTab).replace(/\n/g, '\n' + childTab); 60 | if (!isLast) result += '\n'; 61 | }); 62 | } 63 | 64 | return result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/radix/RadixTree.ts: -------------------------------------------------------------------------------- 1 | import {Printable} from '../print/types'; 2 | import {TrieNode} from '../trie/TrieNode'; 3 | import {insert, find, remove} from './radix'; 4 | 5 | /** 6 | * Radix tree (compressed trie) implementation for string keys. 7 | * 8 | * A radix tree is a space-optimized trie where nodes with single children 9 | * are merged with their parents. This provides efficient storage and lookup 10 | * for strings with common prefixes, making it ideal for applications like 11 | * routing, autocomplete, and IP routing tables. 12 | * 13 | * Key features: 14 | * - Compressed storage of string keys 15 | * - O(k) operations where k is the key length 16 | * - Automatic prefix compression 17 | * - Memory efficient for sparse key spaces 18 | * 19 | * @example 20 | * ```typescript 21 | * const tree = new RadixTree(); 22 | * tree.set('/api/users', 'users-handler'); 23 | * tree.set('/api/posts', 'posts-handler'); 24 | * tree.set('/api/posts/new', 'new-post-handler'); 25 | * 26 | * console.log(tree.get('/api/users')); // 'users-handler' 27 | * console.log(tree.size); // 3 28 | * ``` 29 | * 30 | * @template V - The type of values stored in the tree 31 | */ 32 | export class RadixTree extends TrieNode implements Printable { 33 | /** The number of key-value pairs stored in the tree */ 34 | public size: number = 0; 35 | 36 | /** 37 | * Creates a new radix tree instance. 38 | */ 39 | constructor() { 40 | super('', undefined as any as V); 41 | } 42 | 43 | /** 44 | * Inserts or updates a key-value pair in the radix tree. 45 | * 46 | * @param key - The string key to insert 47 | * @param value - The value to associate with the key 48 | */ 49 | public set(key: string, value: V): void { 50 | this.size += insert(this, key, value); 51 | } 52 | 53 | /** 54 | * Retrieves the value associated with the given key. 55 | * 56 | * @param key - The string key to lookup 57 | * @returns The associated value, or undefined if the key is not found 58 | */ 59 | public get(key: string): V | undefined { 60 | const node = find(this, key) as TrieNode | undefined; 61 | return node && node.v; 62 | } 63 | 64 | /** 65 | * Removes a key-value pair from the radix tree. 66 | * 67 | * @param key - The string key to remove 68 | * @returns true if the key was found and removed, false otherwise 69 | */ 70 | public delete(key: string): boolean { 71 | const removed = remove(this, key); 72 | if (removed) this.size--; 73 | return removed; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | .vscode 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | # Build folders 120 | lib/ 121 | es6/ 122 | es2020/ 123 | esm/ 124 | typedocs/ 125 | 126 | .DS_Store 127 | out.bin 128 | 129 | /gh-pages/ 130 | -------------------------------------------------------------------------------- /src/Tree.ts: -------------------------------------------------------------------------------- 1 | import {find, findOrNextLower, first, insert, last, next, remove} from './util'; 2 | import {splay} from './splay/util'; 3 | import {TreeNode} from './TreeNode'; 4 | import type {Comparator, ITreeNode} from './types'; 5 | 6 | const defaultComparator: Comparator = (a, b) => a - b; 7 | 8 | export class Tree { 9 | public root: ITreeNode | undefined = undefined; 10 | public size: number = 0; 11 | 12 | constructor(public readonly comparator: Comparator = defaultComparator as any) {} 13 | 14 | public set(key: K, value: V): void { 15 | const node = new TreeNode(key, value); 16 | this.root = insert(this.root, node, this.comparator); 17 | this.root = splay(this.root, node, 15); 18 | this.size++; 19 | } 20 | 21 | /** 22 | * Same as `set` but does not splay the tree. 23 | */ 24 | public setFast(key: K, value: V): void { 25 | const node = new TreeNode(key, value); 26 | this.root = insert(this.root, node, this.comparator); 27 | this.size++; 28 | } 29 | 30 | public get(key: K): V | undefined { 31 | const node = find(this.root, key, this.comparator); 32 | return node ? node.v : undefined; 33 | } 34 | 35 | public getOrNextLower(key: K): V | undefined { 36 | const node = findOrNextLower(this.root, key, this.comparator); 37 | return node ? node.v : undefined; 38 | } 39 | 40 | public has(key: K): boolean { 41 | return !!find(this.root, key, this.comparator); 42 | } 43 | 44 | public delete(key: K): V | undefined { 45 | const node = find(this.root, key, this.comparator); 46 | if (!node) return undefined; 47 | this.root = remove(this.root, node); 48 | this.size--; 49 | return node.v; 50 | } 51 | 52 | public max(): V | undefined { 53 | return last(this.root)?.v; 54 | } 55 | 56 | public iterator(): () => V | undefined { 57 | let curr = first(this.root); 58 | return () => { 59 | const res = curr; 60 | if (curr) curr = next(curr); 61 | return res ? res.v : undefined; 62 | }; 63 | } 64 | 65 | public toString(tab: string = ''): string { 66 | return `${this.constructor.name}${this.root ? this.toStringNode(this.root, tab + '', '') : ' ∅'}`; 67 | } 68 | 69 | protected toStringNode(node: ITreeNode, tab: string, side: 'l' | 'r' | ''): string { 70 | let str = `\n${tab}${side === 'l' ? ' ←' : side === 'r' ? ' →' : '└─'} ${node.constructor.name} ${node.k}`; 71 | if (node.l) str += this.toStringNode(node.l, tab + ' ', 'l'); 72 | if (node.r) str += this.toStringNode(node.r, tab + ' ', 'r'); 73 | return str; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/avl/AvlMap.ts: -------------------------------------------------------------------------------- 1 | import {insert, insertLeft, insertRight, remove, print} from './util'; 2 | import {createMap} from '../data-types/map'; 3 | import type {Comparator, HeadlessNode, ITreeNode, SonicMap} from '../types'; 4 | import type {IAvlTreeNode} from './types'; 5 | 6 | /** 7 | * AVL tree node implementation with balance factor tracking. 8 | * 9 | * An AVL node stores key-value pairs and maintains balance information 10 | * to ensure the tree remains height-balanced for optimal performance. 11 | * 12 | * @template K - The type of the key 13 | * @template V - The type of the value 14 | */ 15 | export class AvlNode implements IAvlTreeNode { 16 | /** Parent node reference */ 17 | public p: AvlNode | undefined = undefined; 18 | /** Left child node reference */ 19 | public l: AvlNode | undefined = undefined; 20 | /** Right child node reference */ 21 | public r: AvlNode | undefined = undefined; 22 | /** Balance factor: height(right) - height(left), must be -1, 0, or 1 for AVL property */ 23 | public bf: number = 0; 24 | 25 | /** 26 | * Creates a new AVL tree node. 27 | * 28 | * @param k - The immutable key for this node 29 | * @param v - The mutable value for this node 30 | */ 31 | constructor( 32 | public readonly k: K, 33 | public v: V, 34 | ) {} 35 | } 36 | 37 | /** 38 | * High-performance AVL tree-based sorted map implementation. 39 | * 40 | * This AVL map provides O(log n) insertion, deletion, and lookup operations 41 | * while maintaining tree balance through automatic rotations. It's optimized 42 | * for scenarios requiring fast insertions and stable node references. 43 | * 44 | * @example 45 | * ```typescript 46 | * const map = new AvlMap(); 47 | * const nodeRef = map.set(1, 'one'); 48 | * console.log(nodeRef.v); // 'one' 49 | * nodeRef.v = 'ONE'; // Direct mutation of node value 50 | * console.log(map.get(1)); // 'ONE' 51 | * ``` 52 | * 53 | * @template K - The type of keys stored in the map 54 | * @template V - The type of values stored in the map 55 | */ 56 | export const AvlMap = createMap( 57 | AvlNode, 58 | insert as >(root: N | undefined, node: N, comparator: Comparator) => N, 59 | insertLeft as >(root: N, node: N, parent: N) => N, 60 | insertRight as >(root: N, node: N, parent: N) => N, 61 | remove as >(root: N | undefined, n: N) => N | undefined, 62 | print as (node: undefined | HeadlessNode | ITreeNode, tab?: string) => string, 63 | ); 64 | 65 | export type AvlMap = SonicMap>; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sonic Forest 2 | 3 | High performance (binary) tree and sorted map implementation for JavaScript in TypeScript. 4 | 5 | ## Features 6 | 7 | - AVL tree implementation 8 | - AVL sorted map implementation 9 | - AVL sorted set implementation 10 | - Red-black (RB) tree implementation 11 | - Red-black (RB) tree sorted map implementation 12 | - Left-leaning Red-black (LLRB) tree implementation 13 | - Radix tree implementation (string keys) 14 | - Binary radix tree implementation (Uint8Array keys) 15 | - Splay tree implementation 16 | - Various utility methods for binary trees 17 | 18 | This package implements the fastest insertion into self-balancing binary tree out of any 19 | NPM package. Both, AVL and Red-black tree insertion implementations of `sonic-forest` a faster 20 | than inserts in [`js-sdsl`](https://www.npmjs.com/package/js-sdsl) implementation. 21 | 22 | However, deletions from a binary tree are faster in `js-sdsl`. But, deletions in `sonic-forest` 23 | delete exactly the node, which contains the key. Unlike, in `js-sdsl` and all other 24 | binary tree libraries, where those libraries find the in-order-sucessor or -predecessor, which 25 | is a leaf node, and delete that instead. As such, one can keep pointers to `sonic-forest` AVL 26 | and Red-black tree nodes, and those pointers will stay valid even after deletions. 27 | 28 | ## Binary Radix Tree 29 | 30 | The binary radix tree implementation supports `Uint8Array` keys, making it suitable for binary data like: 31 | 32 | - Binary protocol routing 33 | - File system paths as binary data 34 | - Cryptographic hashes 35 | - Network packet classification 36 | - Any binary blob keys 37 | 38 | ### Key Features 39 | 40 | - **Efficient slicing**: Uses `Slice` class to reference portions of `Uint8Array` without copying data 41 | - **Prefix compression**: Automatically compresses common prefixes to save memory 42 | - **Binary-safe**: Works with any byte sequence, including null bytes 43 | - **Same API**: Provides similar interface to the string-based radix tree 44 | 45 | ### Usage Example 46 | 47 | ```typescript 48 | import { BinaryRadixTree } from 'sonic-forest'; 49 | 50 | const tree = new BinaryRadixTree(); 51 | 52 | // Insert binary keys 53 | tree.set(new Uint8Array([0x47, 0x45, 0x54, 0x20]), 'GET '); // "GET " 54 | tree.set(new Uint8Array([0x50, 0x4F, 0x53, 0x54]), 'POST'); // "POST" 55 | tree.set(new Uint8Array([0x50, 0x55, 0x54, 0x20]), 'PUT '); // "PUT " 56 | 57 | // Retrieve values 58 | console.log(tree.get(new Uint8Array([0x47, 0x45, 0x54, 0x20]))); // "GET " 59 | 60 | // Delete keys 61 | tree.delete(new Uint8Array([0x50, 0x4F, 0x53, 0x54])); // Remove POST 62 | 63 | console.log(tree.size); // 2 64 | ``` 65 | -------------------------------------------------------------------------------- /src/SortedMap/SortedMapNode.ts: -------------------------------------------------------------------------------- 1 | import type {IRbTreeNode} from '../red-black/types'; 2 | 3 | export class TreeNode implements IRbTreeNode { 4 | l: TreeNode | undefined = undefined; 5 | r: TreeNode | undefined = undefined; 6 | p: TreeNode | undefined = undefined; 7 | 8 | constructor( 9 | public k: K, 10 | public v: V, 11 | public b = false, 12 | ) {} 13 | 14 | prev() { 15 | let prev: TreeNode = this; 16 | const isRootOrHeader = prev.p!.p === prev; 17 | if (isRootOrHeader && !prev.b) prev = prev.r!; 18 | else if (prev.l) { 19 | prev = prev.l; 20 | while (prev.r) prev = prev.r; 21 | } else { 22 | if (isRootOrHeader) return prev.p!; 23 | let v = prev.p!; 24 | while (v.l === prev) { 25 | prev = v; 26 | v = prev.p!; 27 | } 28 | prev = v; 29 | } 30 | return prev; 31 | } 32 | 33 | next() { 34 | let next: TreeNode = this; 35 | if (next.r) { 36 | next = next.r; 37 | while (next.l) next = next.l; 38 | return next; 39 | } else { 40 | let v = next.p!; 41 | while (v.r === next) { 42 | next = v; 43 | v = next.p!; 44 | } 45 | if (next.r !== v) return v; 46 | else return next; 47 | } 48 | } 49 | 50 | rRotate() { 51 | const p = this.p!; 52 | const r = this.r!; 53 | const l = r.l; 54 | if (p.p === this) p.p = r; 55 | else if (p.l === this) p.l = r; 56 | else p.r = r; 57 | r.p = p; 58 | r.l = this; 59 | this.p = r; 60 | this.r = l; 61 | if (l) l.p = this; 62 | return r; 63 | } 64 | 65 | lRotate() { 66 | const p = this.p!; 67 | const l = this.l!; 68 | const r = l.r; 69 | if (p.p === this) p.p = l; 70 | else if (p.l === this) p.l = l; 71 | else p.r = l; 72 | l.p = p; 73 | l.r = this; 74 | this.p = l; 75 | this.l = r; 76 | if (r) r.p = this; 77 | return l; 78 | } 79 | } 80 | 81 | export class TreeNodeEnableIndex extends TreeNode { 82 | _size = 1; 83 | 84 | rRotate() { 85 | const parent = super.rRotate() as TreeNodeEnableIndex; 86 | this.compute(); 87 | parent.compute(); 88 | return parent; 89 | } 90 | 91 | lRotate() { 92 | const parent = super.lRotate() as TreeNodeEnableIndex; 93 | this.compute(); 94 | parent.compute(); 95 | return parent; 96 | } 97 | 98 | compute() { 99 | this._size = 1; 100 | if (this.l) { 101 | this._size += (this.l as TreeNodeEnableIndex)._size; 102 | } 103 | if (this.r) { 104 | this._size += (this.r as TreeNodeEnableIndex)._size; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/__bench__/payloads.ts: -------------------------------------------------------------------------------- 1 | const naturalNumbersVerySmallList = Array.from({length: 10}, (_, i) => i * 5 + 10); 2 | const naturalNumbersSmallList = Array.from({length: 100}, (_, i) => i * 5 + 100); 3 | const naturalNumbers = Array.from({length: 1000}, (_, i) => i * 5 + 1000); 4 | const naturalNumbersLongList = Array.from({length: 10000}, (_, i) => i * 5 + 10000); 5 | const naturalNumbersVeryLongList = Array.from({length: 100000}, (_, i) => i * 5 + 100000); 6 | const naturalNumbersReverse = Array.from({length: 1000}, (_, i) => 1000 - i); 7 | const naturalNumbersRandomSmallList = Array.from({length: 100}, (_, i) => Math.floor(Math.random() * 100)); 8 | const naturalNumbersRandom = Array.from({length: 1000}, (_, i) => Math.floor(Math.random() * 1000)); 9 | const naturalNumbersRandomLargeList = Array.from({length: 10000}, (_, i) => Math.floor(Math.random() * 10000)); 10 | // const naturalNumbersRandomVeryLargeList = Array.from({length: 100000}, (_, i) => Math.floor(Math.random() * 100000)); 11 | 12 | export const numbers = [ 13 | { 14 | name: (json: any) => `Random ${naturalNumbersRandomSmallList.length} numbers`, 15 | data: naturalNumbersRandomSmallList, 16 | }, 17 | { 18 | name: (json: any) => `Random ${naturalNumbersRandom.length} numbers`, 19 | data: naturalNumbersRandom, 20 | }, 21 | { 22 | name: (json: any) => `Random ${naturalNumbersRandomLargeList.length} numbers`, 23 | data: naturalNumbersRandomLargeList, 24 | }, 25 | // { 26 | // name: (json: any) => `Random ${naturalNumbersRandomVeryLargeList.length} numbers`, 27 | // data: naturalNumbersRandomVeryLargeList, 28 | // }, 29 | { 30 | name: (json: any) => 31 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 32 | data: naturalNumbersVerySmallList, 33 | }, 34 | { 35 | name: (json: any) => 36 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 37 | data: naturalNumbersSmallList, 38 | }, 39 | { 40 | name: (json: any) => 41 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 42 | data: naturalNumbers, 43 | }, 44 | { 45 | name: (json: any) => 46 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 47 | data: naturalNumbersLongList, 48 | }, 49 | { 50 | name: (json: any) => 51 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 52 | data: naturalNumbersVeryLongList, 53 | }, 54 | { 55 | name: (json: any) => 56 | `${(json as any).length} natural numbers from ${(json as any)[0]} to ${(json as any)[(json as any).length - 1]}`, 57 | data: naturalNumbersReverse, 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /src/radix/BinaryRadixTree.ts: -------------------------------------------------------------------------------- 1 | import {Printable} from '../print/types'; 2 | import {BinaryTrieNode} from './BinaryTrieNode'; 3 | import {Slice} from './Slice'; 4 | import {insert, find, remove} from './binaryRadix'; 5 | 6 | /** 7 | * Binary radix tree implementation for Uint8Array keys. 8 | * 9 | * A binary radix tree is a compressed trie optimized for binary data keys. 10 | * It provides efficient storage and lookup for binary keys such as protocol 11 | * headers, file paths as binary data, cryptographic hashes, and network 12 | * packet classification. 13 | * 14 | * Key features: 15 | * - Optimized for Uint8Array keys 16 | * - Efficient prefix compression using Slice references 17 | * - O(k) operations where k is the key length in bytes 18 | * - Memory efficient through slice-based key storage 19 | * - Binary-safe (handles null bytes and arbitrary data) 20 | * 21 | * @example 22 | * ```typescript 23 | * const tree = new BinaryRadixTree(); 24 | * 25 | * // HTTP method routing with binary keys 26 | * tree.set(new Uint8Array([0x47, 0x45, 0x54, 0x20]), 'GET '); // "GET " 27 | * tree.set(new Uint8Array([0x50, 0x4F, 0x53, 0x54]), 'POST'); // "POST" 28 | * tree.set(new Uint8Array([0x50, 0x55, 0x54, 0x20]), 'PUT '); // "PUT " 29 | * 30 | * console.log(tree.get(new Uint8Array([0x47, 0x45, 0x54, 0x20]))); // "GET " 31 | * console.log(tree.size); // 3 32 | * ``` 33 | * 34 | * @template V - The type of values stored in the tree 35 | */ 36 | export class BinaryRadixTree extends BinaryTrieNode implements Printable { 37 | /** The number of key-value pairs stored in the tree */ 38 | public size: number = 0; 39 | 40 | /** 41 | * Creates a new binary radix tree instance. 42 | */ 43 | constructor() { 44 | super(new Slice(new Uint8Array(), 0, 0), undefined as any as V); 45 | } 46 | 47 | /** 48 | * Inserts or updates a key-value pair in the binary radix tree. 49 | * 50 | * @param key - The Uint8Array key to insert 51 | * @param value - The value to associate with the key 52 | */ 53 | public set(key: Uint8Array, value: V): void { 54 | this.size += insert(this, key, value); 55 | } 56 | 57 | /** 58 | * Retrieves the value associated with the given binary key. 59 | * 60 | * @param key - The Uint8Array key to lookup 61 | * @returns The associated value, or undefined if the key is not found 62 | */ 63 | public get(key: Uint8Array): V | undefined { 64 | const node = find(this, key) as BinaryTrieNode | undefined; 65 | return node && node.v; 66 | } 67 | 68 | /** 69 | * Removes a key-value pair from the binary radix tree. 70 | * 71 | * @param key - The Uint8Array key to remove 72 | * @returns true if the key was found and removed, false otherwise 73 | */ 74 | public delete(key: Uint8Array): boolean { 75 | const removed = remove(this, key); 76 | if (removed) this.size--; 77 | return removed; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/avl/AvlBstNumNumMap.ts: -------------------------------------------------------------------------------- 1 | import {insert, insertLeft, insertRight, print} from './util'; 2 | import {printTree} from '../print/printTree'; 3 | import {findOrNextLower, first, next} from '../util'; 4 | import type {Printable} from '../print/types'; 5 | import type {HeadlessNode} from '../types'; 6 | import type {AvlNodeReference, IAvlTreeNode} from './types'; 7 | 8 | export class NumNumItem implements IAvlTreeNode { 9 | public p: NumNumItem | undefined = undefined; 10 | public l: NumNumItem | undefined = undefined; 11 | public r: NumNumItem | undefined = undefined; 12 | public bf: number = 0; 13 | constructor( 14 | public readonly k: number, 15 | public v: number, 16 | ) {} 17 | } 18 | 19 | const comparator = (a: number, b: number) => a - b; 20 | 21 | export class AvlBstNumNumMap implements Printable { 22 | public root: NumNumItem | undefined = undefined; 23 | 24 | public insert(k: number, v: number): AvlNodeReference { 25 | const item = new NumNumItem(k, v); 26 | this.root = insert(this.root, item, comparator); 27 | return item; 28 | } 29 | 30 | public set(k: number, v: number): AvlNodeReference { 31 | const root = this.root; 32 | if (!root) return this.insert(k, v); 33 | let next: NumNumItem | undefined = root, 34 | curr: NumNumItem | undefined = next; 35 | let cmp: number = 0; 36 | do { 37 | curr = next; 38 | cmp = comparator(k, curr.k); 39 | if (cmp === 0) return (curr.v = v), curr; 40 | } while ((next = cmp < 0 ? ((curr as any).l as NumNumItem) : ((curr as any).r as NumNumItem))); 41 | const node = new NumNumItem(k, v); 42 | this.root = cmp < 0 ? (insertLeft(root, node, curr) as NumNumItem) : (insertRight(root, node, curr) as NumNumItem); 43 | return node; 44 | } 45 | 46 | public find(k: number): AvlNodeReference | undefined { 47 | let curr: NumNumItem | undefined = this.root; 48 | while (curr) { 49 | const cmp = comparator(k, curr.k); 50 | if (cmp === 0) return curr; 51 | curr = cmp < 0 ? ((curr as any).l as NumNumItem) : ((curr as any).r as NumNumItem); 52 | } 53 | return undefined; 54 | } 55 | 56 | public get(k: number): number | undefined { 57 | return this.find(k)?.v; 58 | } 59 | 60 | public has(k: number): boolean { 61 | return !!this.find(k); 62 | } 63 | 64 | public getOrNextLower(k: number): NumNumItem | undefined { 65 | return (findOrNextLower(this.root, k, comparator) as NumNumItem) || undefined; 66 | } 67 | 68 | public forEach(fn: (node: NumNumItem) => void): void { 69 | const root = this.root; 70 | if (!root) return; 71 | let curr = first(root); 72 | do fn(curr!); 73 | while ((curr = next(curr as HeadlessNode) as NumNumItem | undefined)); 74 | } 75 | 76 | public toString(tab: string): string { 77 | return this.constructor.name + printTree(tab, [(tab) => print(this.root, tab)]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/red-black/RbMap.ts: -------------------------------------------------------------------------------- 1 | import {insert, insertLeft, insertRight, remove, print} from './util'; 2 | import {createMap} from '../data-types/map'; 3 | import type {Comparator, HeadlessNode, ITreeNode, SonicMap} from '../types'; 4 | import type {IRbTreeNode} from './types'; 5 | 6 | /** 7 | * Red-Black tree node implementation with color tracking. 8 | * 9 | * A Red-Black node stores key-value pairs and maintains color information 10 | * (red or black) to ensure the tree satisfies Red-Black tree properties 11 | * for guaranteed O(log n) operations. 12 | * 13 | * @template K - The type of the key 14 | * @template V - The type of the value 15 | */ 16 | export class RbNode implements IRbTreeNode { 17 | /** Parent node reference */ 18 | public p: RbNode | undefined = undefined; 19 | /** Left child node reference */ 20 | public l: RbNode | undefined = undefined; 21 | /** Right child node reference */ 22 | public r: RbNode | undefined = undefined; 23 | /** Color flag: false = red, true = black */ 24 | public b: boolean = false; 25 | 26 | /** 27 | * Creates a new Red-Black tree node. 28 | * 29 | * @param k - The immutable key for this node 30 | * @param v - The mutable value for this node 31 | */ 32 | constructor( 33 | public readonly k: K, 34 | public v: V, 35 | ) {} 36 | } 37 | 38 | /** 39 | * High-performance Red-Black tree-based sorted map implementation. 40 | * 41 | * This Red-Black map provides O(log n) insertion, deletion, and lookup operations 42 | * with guaranteed balance through red-black tree properties. It offers excellent 43 | * worst-case performance and is suitable for applications requiring predictable 44 | * operation times. 45 | * 46 | * Red-Black trees maintain the following properties: 47 | * 1. Every node is either red or black 48 | * 2. The root is always black 49 | * 3. No two red nodes are adjacent 50 | * 4. Every path from root to leaf contains the same number of black nodes 51 | * 52 | * @example 53 | * ```typescript 54 | * const map = new RbMap(); 55 | * const nodeRef = map.set(1, 'one'); 56 | * console.log(nodeRef.v); // 'one' 57 | * console.log(map.get(1)); // 'one' 58 | * map.del(1); // Remove the node 59 | * ``` 60 | * 61 | * @template K - The type of keys stored in the map 62 | * @template V - The type of values stored in the map 63 | */ 64 | export const RbMap = createMap( 65 | RbNode, 66 | insert as >(root: N | undefined, node: N, comparator: Comparator) => N, 67 | insertLeft as >(root: N, node: N, parent: N) => N, 68 | insertRight as >(root: N, node: N, parent: N) => N, 69 | remove as >(root: N | undefined, n: N) => N | undefined, 70 | print as (node: undefined | HeadlessNode | ITreeNode, tab?: string) => string, 71 | ); 72 | 73 | export type RbMap = SonicMap>; 74 | -------------------------------------------------------------------------------- /src/llrb-tree/__tests__/LlrbTree.fuzzing.spec.ts: -------------------------------------------------------------------------------- 1 | import {assertRedBlackTree} from '../../red-black/__tests__/utils'; 2 | import {assertLlrbTree} from './llrb-utils'; 3 | import {LlrbTree} from '../LlrbTree'; 4 | import {next} from '../../util'; 5 | 6 | const randomInt = (max: number) => Math.floor(Math.random() * max); 7 | 8 | describe('LlrbTree fuzzing', () => { 9 | for (let instance = 0; instance < 50; instance++) { 10 | test(`fuzzing instance ${instance}`, () => { 11 | const tree = new LlrbTree(); 12 | const shadowMap = new Map(); 13 | const operations = randomInt(500) + 100; // 100-600 operations 14 | 15 | for (let op = 0; op < operations; op++) { 16 | const key = randomInt(100) + 1; // Keys 1-100 17 | const action = Math.random(); 18 | 19 | if (action < 0.6) { 20 | // 60% insertions 21 | const value = key * 2; // Some deterministic value 22 | tree.set(key, value); 23 | shadowMap.set(key, value); 24 | 25 | // Validate tree properties after every 10th insertion 26 | if (op % 10 === 0) { 27 | assertRedBlackTree(tree.root); 28 | assertLlrbTree(tree.root); 29 | } 30 | } else if (action < 0.9) { 31 | // 30% deletions 32 | const deleted = tree.del(key); 33 | const shadowDeleted = shadowMap.delete(key); 34 | 35 | expect(deleted).toBe(shadowDeleted); 36 | 37 | // Validate tree properties after every deletion 38 | if (tree.root) { 39 | assertRedBlackTree(tree.root); 40 | assertLlrbTree(tree.root); 41 | } 42 | } else { 43 | // 10% lookups (validation) 44 | const treeValue = tree.get(key); 45 | const shadowValue = shadowMap.get(key); 46 | expect(treeValue).toBe(shadowValue); 47 | } 48 | 49 | // Periodically validate the entire tree contents match 50 | if (op % 50 === 0) { 51 | expect(tree.size()).toBe(shadowMap.size); 52 | 53 | // Check all keys in shadow map exist in tree 54 | for (const [k, v] of shadowMap) { 55 | expect(tree.get(k)).toBe(v); 56 | } 57 | 58 | // Check tree doesn't have extra keys 59 | let treeSize = 0; 60 | let current = tree.min; 61 | while (current) { 62 | expect(shadowMap.has(current.k)).toBe(true); 63 | expect(shadowMap.get(current.k)).toBe(current.v); 64 | treeSize++; 65 | current = next(current); 66 | } 67 | expect(treeSize).toBe(shadowMap.size); 68 | } 69 | } 70 | 71 | // Final validation 72 | expect(tree.size()).toBe(shadowMap.size); 73 | if (tree.root) { 74 | assertRedBlackTree(tree.root); 75 | assertLlrbTree(tree.root); 76 | } 77 | 78 | // Validate all entries 79 | for (const [k, v] of shadowMap) { 80 | expect(tree.get(k)).toBe(v); 81 | } 82 | }); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /src/radix/Slice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Efficient slice reference to a portion of a Uint8Array without copying data. 3 | * This avoids creating new Uint8Array instances for every node in the radix tree. 4 | */ 5 | export class Slice { 6 | constructor( 7 | public readonly data: Uint8Array, 8 | public readonly start: number, 9 | public readonly length: number, 10 | ) {} 11 | 12 | /** 13 | * Get byte at the given index within this slice. 14 | */ 15 | public at(index: number): number { 16 | if (index < 0 || index >= this.length) { 17 | throw new Error(`Index ${index} out of bounds for slice of length ${this.length}`); 18 | } 19 | return this.data[this.start + index]; 20 | } 21 | 22 | /** 23 | * Create a new slice that represents a substring of this slice. 24 | */ 25 | public substring(start: number, length?: number): Slice { 26 | if (start < 0 || start > this.length) { 27 | throw new Error(`Start ${start} out of bounds for slice of length ${this.length}`); 28 | } 29 | const newLength = length !== undefined ? Math.min(length, this.length - start) : this.length - start; 30 | return new Slice(this.data, this.start + start, newLength); 31 | } 32 | 33 | /** 34 | * Compare this slice with another slice for equality. 35 | */ 36 | public equals(other: Slice): boolean { 37 | if (this.length !== other.length) return false; 38 | for (let i = 0; i < this.length; i++) { 39 | if (this.at(i) !== other.at(i)) return false; 40 | } 41 | return true; 42 | } 43 | 44 | /** 45 | * Compare this slice with another slice lexicographically. 46 | * Returns negative if this < other, positive if this > other, 0 if equal. 47 | */ 48 | public compare(other: Slice): number { 49 | const minLength = Math.min(this.length, other.length); 50 | for (let i = 0; i < minLength; i++) { 51 | const thisByte = this.at(i); 52 | const otherByte = other.at(i); 53 | if (thisByte !== otherByte) { 54 | return thisByte - otherByte; 55 | } 56 | } 57 | return this.length - other.length; 58 | } 59 | 60 | /** 61 | * Create a new Uint8Array containing the data from this slice. 62 | * Use sparingly as this creates a copy. 63 | */ 64 | public toUint8Array(): Uint8Array { 65 | return this.data.slice(this.start, this.start + this.length); 66 | } 67 | 68 | /** 69 | * Get string representation for debugging. 70 | */ 71 | public toString(): string { 72 | return `Slice(${Array.from(this.toUint8Array()).join(',')})`; 73 | } 74 | 75 | /** 76 | * Find the length of common prefix between this slice and another slice. 77 | */ 78 | public getCommonPrefixLength(other: Slice): number { 79 | const len = Math.min(this.length, other.length); 80 | let i = 0; 81 | for (; i < len && this.at(i) === other.at(i); i++); 82 | return i; 83 | } 84 | 85 | /** 86 | * Create a slice from a Uint8Array. 87 | */ 88 | public static fromUint8Array(data: Uint8Array): Slice { 89 | return new Slice(data, 0, data.length); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/util/__tests__/swap.spec.ts: -------------------------------------------------------------------------------- 1 | import {assertTreeLinks} from '../../__tests__/util'; 2 | import {HeadlessNode} from '../../types'; 3 | import {swap} from '../swap'; 4 | import {print} from '../print'; 5 | import {IRbTreeNode} from '../../red-black'; 6 | import {linkLeft, linkRight, n} from '../../red-black/__tests__/utils'; 7 | 8 | test('immediate left child at root', () => { 9 | const x: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 10 | const y: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 11 | let root = x; 12 | x.l = y; 13 | y.p = x; 14 | assertTreeLinks(root); 15 | root = swap(root, x, y); 16 | expect(root).toBe(y); 17 | assertTreeLinks(root); 18 | expect(y.l).toBe(x); 19 | expect(x.p).toBe(y); 20 | expect(x.l).toBe(undefined); 21 | expect(y.p).toBe(undefined); 22 | // console.log(print(root)); 23 | }); 24 | 25 | test('immediate right child at root', () => { 26 | const x: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 27 | const y: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 28 | let root = x; 29 | x.r = y; 30 | y.p = x; 31 | assertTreeLinks(root); 32 | root = swap(root, x, y); 33 | expect(root).toBe(y); 34 | assertTreeLinks(root); 35 | expect(y.r).toBe(x); 36 | expect(x.p).toBe(y); 37 | expect(x.l).toBeUndefined(); 38 | expect(y.p).toBe(undefined); 39 | // console.log(print(root)); 40 | }); 41 | 42 | test('immediate left child not at root', () => { 43 | const z: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 44 | const x: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 45 | const y: HeadlessNode = {p: undefined, l: undefined, r: undefined}; 46 | x.l = y; 47 | y.p = x; 48 | z.l = x; 49 | x.p = z; 50 | let root = z; 51 | assertTreeLinks(root); 52 | root = swap(root, x, y); 53 | expect(root).toBe(z); 54 | assertTreeLinks(root); 55 | expect(y.l).toBe(x); 56 | expect(x.p).toBe(y); 57 | expect(x.l).toBe(undefined); 58 | expect(y.p).toBe(z); 59 | // console.log(print(root)); 60 | }); 61 | 62 | test('root and leaf nodes', () => { 63 | const nn20: IRbTreeNode = n(-20, true); 64 | const nn10: IRbTreeNode = n(-10, true); 65 | const nn5: IRbTreeNode = n(-5, true); 66 | const n10: IRbTreeNode = n(10, true); 67 | const n20: IRbTreeNode = n(20, true); 68 | const n40: IRbTreeNode = n(40, true); 69 | const n50: IRbTreeNode = n(50, true); 70 | const n60: IRbTreeNode = n(60, false); 71 | const n80: IRbTreeNode = n(80, true); 72 | let root = n10; 73 | linkLeft(root, nn10); 74 | linkRight(root, n40); 75 | linkLeft(nn10, nn20); 76 | linkRight(nn10, nn5); 77 | linkLeft(n40, n20); 78 | linkRight(n40, n60); 79 | linkLeft(n60, n50); 80 | linkRight(n60, n80); 81 | assertTreeLinks(root); 82 | const x = root; 83 | const y = n20; 84 | // console.log(print(root)); 85 | root = swap(root, x, y); 86 | // console.log(print(root)); 87 | assertTreeLinks(root); 88 | expect(root).toBe(y); 89 | expect(y.p).toBe(undefined); 90 | }); 91 | -------------------------------------------------------------------------------- /src/splay/util2.ts: -------------------------------------------------------------------------------- 1 | import type {HeadlessNode2} from '../types2'; 2 | 3 | export const splay2 = (root: N, node: N): N => { 4 | const p = node.p2; 5 | if (!p) return root; 6 | const pp = p.p2; 7 | const l2 = p.l2 === node; 8 | if (!pp) { 9 | if (l2) rSplay2(node, p as N); 10 | else lSplay2(node, p as N); 11 | return node; 12 | } 13 | const l1 = pp.l2 === p; 14 | if (l1) { 15 | if (l2) { 16 | root = llSplay2(root, node, p as N, pp as N); 17 | } else { 18 | root = lrSplay2(root, node, p as N, pp as N); 19 | } 20 | } else { 21 | if (l2) { 22 | root = rlSplay2(root, node, p as N, pp as N); 23 | } else { 24 | root = rrSplay2(root, node, p as N, pp as N); 25 | } 26 | } 27 | return splay2(root, node); 28 | }; 29 | 30 | const rSplay2 = (c2: N, c1: N): void => { 31 | const b = c2.r2; 32 | c2.p2 = undefined; 33 | c2.r2 = c1; 34 | c1.p2 = c2; 35 | c1.l2 = b; 36 | if (b) b.p2 = c1; 37 | }; 38 | 39 | const lSplay2 = (c2: N, c1: N): void => { 40 | const b = c2.l2; 41 | c2.p2 = undefined; 42 | c2.l2 = c1; 43 | c1.p2 = c2; 44 | c1.r2 = b; 45 | if (b) b.p2 = c1; 46 | }; 47 | 48 | const rrSplay2 = (root: N, c3: N, c2: N, c1: N): N => { 49 | const b = c2.l2; 50 | const c = c3.l2; 51 | const p = c1.p2; 52 | c3.p2 = p; 53 | c3.l2 = c2; 54 | c2.p2 = c3; 55 | c2.l2 = c1; 56 | c2.r2 = c; 57 | c1.p2 = c2; 58 | c1.r2 = b; 59 | if (b) b.p2 = c1; 60 | if (c) c.p2 = c2; 61 | if (!p) root = c3; 62 | else if (p.l2 === c1) p.l2 = c3; 63 | else p.r2 = c3; 64 | return root; 65 | }; 66 | 67 | const llSplay2 = (root: N, c3: N, c2: N, c1: N): N => { 68 | const b = c2.r2; 69 | const c = c3.r2; 70 | const p = c1.p2; 71 | c3.p2 = p; 72 | c3.r2 = c2; 73 | c2.p2 = c3; 74 | c2.l2 = c; 75 | c2.r2 = c1; 76 | c1.p2 = c2; 77 | c1.l2 = b; 78 | if (b) b.p2 = c1; 79 | if (c) c.p2 = c2; 80 | if (!p) root = c3; 81 | else if (p.l2 === c1) p.l2 = c3; 82 | else p.r2 = c3; 83 | return root; 84 | }; 85 | 86 | const lrSplay2 = (root: N, c3: N, c2: N, c1: N): N => { 87 | const c = c3.l2; 88 | const d = c3.r2; 89 | const p = c1.p2; 90 | c3.p2 = p; 91 | c3.l2 = c2; 92 | c3.r2 = c1; 93 | c2.p2 = c3; 94 | c2.r2 = c; 95 | c1.p2 = c3; 96 | c1.l2 = d; 97 | if (c) c.p2 = c2; 98 | if (d) d.p2 = c1; 99 | if (!p) root = c3; 100 | else if (p.l2 === c1) p.l2 = c3; 101 | else p.r2 = c3; 102 | return root; 103 | }; 104 | 105 | const rlSplay2 = (root: N, c3: N, c2: N, c1: N): N => { 106 | const c = c3.r2; 107 | const d = c3.l2; 108 | const p = c1.p2; 109 | c3.p2 = p; 110 | c3.l2 = c1; 111 | c3.r2 = c2; 112 | c2.p2 = c3; 113 | c2.l2 = c; 114 | c1.p2 = c3; 115 | c1.r2 = d; 116 | if (c) c.p2 = c2; 117 | if (d) d.p2 = c1; 118 | if (!p) root = c3; 119 | else if (p.l2 === c1) p.l2 = c3; 120 | else p.r2 = c3; 121 | return root; 122 | }; 123 | -------------------------------------------------------------------------------- /src/splay/util.ts: -------------------------------------------------------------------------------- 1 | import type {HeadlessNode} from '../types'; 2 | 3 | export const splay = (root: N, node: N, repeat: number): N => { 4 | const p = node.p; 5 | if (!p) return root; 6 | const pp = p.p; 7 | const l2 = p.l === node; 8 | if (!pp) { 9 | if (l2) rSplay(node, p as N); 10 | else lSplay(node, p as N); 11 | return node; 12 | } 13 | const l1 = pp.l === p; 14 | if (l1) { 15 | if (l2) { 16 | root = llSplay(root, node, p as N, pp as N); 17 | } else { 18 | root = lrSplay(root, node, p as N, pp as N); 19 | } 20 | } else { 21 | if (l2) { 22 | root = rlSplay(root, node, p as N, pp as N); 23 | } else { 24 | root = rrSplay(root, node, p as N, pp as N); 25 | } 26 | } 27 | if (repeat > 1) return splay(root, node, repeat - 1); 28 | return root; 29 | }; 30 | 31 | export const rSplay = (c2: N, c1: N): void => { 32 | const b = c2.r; 33 | c2.p = undefined; 34 | c2.r = c1; 35 | c1.p = c2; 36 | c1.l = b; 37 | if (b) b.p = c1; 38 | }; 39 | 40 | export const lSplay = (c2: N, c1: N): void => { 41 | const b = c2.l; 42 | c2.p = undefined; 43 | c2.l = c1; 44 | c1.p = c2; 45 | c1.r = b; 46 | if (b) b.p = c1; 47 | }; 48 | 49 | export const rrSplay = (root: N, c3: N, c2: N, c1: N): N => { 50 | const b = c2.l; 51 | const c = c3.l; 52 | const p = c1.p; 53 | c3.p = p; 54 | c3.l = c2; 55 | c2.p = c3; 56 | c2.l = c1; 57 | c2.r = c; 58 | c1.p = c2; 59 | c1.r = b; 60 | if (b) b.p = c1; 61 | if (c) c.p = c2; 62 | if (!p) root = c3; 63 | else if (p.l === c1) p.l = c3; 64 | else p.r = c3; 65 | return root; 66 | }; 67 | 68 | export const llSplay = (root: N, c3: N, c2: N, c1: N): N => { 69 | const b = c2.r; 70 | const c = c3.r; 71 | const p = c1.p; 72 | c3.p = p; 73 | c3.r = c2; 74 | c2.p = c3; 75 | c2.l = c; 76 | c2.r = c1; 77 | c1.p = c2; 78 | c1.l = b; 79 | if (b) b.p = c1; 80 | if (c) c.p = c2; 81 | if (!p) root = c3; 82 | else if (p.l === c1) p.l = c3; 83 | else p.r = c3; 84 | return root; 85 | }; 86 | 87 | export const lrSplay = (root: N, c3: N, c2: N, c1: N): N => { 88 | const c = c3.l; 89 | const d = c3.r; 90 | const p = c1.p; 91 | c3.p = p; 92 | c3.l = c2; 93 | c3.r = c1; 94 | c2.p = c3; 95 | c2.r = c; 96 | c1.p = c3; 97 | c1.l = d; 98 | if (c) c.p = c2; 99 | if (d) d.p = c1; 100 | if (!p) root = c3; 101 | else if (p.l === c1) p.l = c3; 102 | else p.r = c3; 103 | return root; 104 | }; 105 | 106 | export const rlSplay = (root: N, c3: N, c2: N, c1: N): N => { 107 | const c = c3.r; 108 | const d = c3.l; 109 | const p = c1.p; 110 | c3.p = p; 111 | c3.l = c1; 112 | c3.r = c2; 113 | c2.p = c3; 114 | c2.l = c; 115 | c1.p = c3; 116 | c1.r = d; 117 | if (c) c.p = c2; 118 | if (d) d.p = c1; 119 | if (!p) root = c3; 120 | else if (p.l === c1) p.l = c3; 121 | else p.r = c3; 122 | return root; 123 | }; 124 | -------------------------------------------------------------------------------- /src/util2.ts: -------------------------------------------------------------------------------- 1 | import type {Comparator2, HeadlessNode2} from './types2'; 2 | 3 | export const first2 = (root: N | undefined): N | undefined => { 4 | let curr = root; 5 | while (curr) 6 | if (curr.l2) curr = curr.l2 as N; 7 | else return curr; 8 | return curr; 9 | }; 10 | 11 | export const last2 = (root: N | undefined): N | undefined => { 12 | let curr = root; 13 | while (curr) 14 | if (curr.r2) curr = curr.r2 as N; 15 | else return curr; 16 | return curr; 17 | }; 18 | 19 | export const next2 = (curr: N): N | undefined => { 20 | if (curr.r2) { 21 | curr = curr.r2 as N; 22 | while (curr.l2) curr = curr.l2 as N; 23 | return curr; 24 | } 25 | let p = curr.p2 as N; 26 | while (p && p.r2 === curr) { 27 | curr = p; 28 | p = p.p2 as N; 29 | } 30 | return p; 31 | }; 32 | 33 | export const prev2 = (curr: N): N | undefined => { 34 | if (curr.l2) { 35 | curr = curr.l2 as N; 36 | while (curr.r2) curr = curr.r2 as N; 37 | return curr; 38 | } 39 | let p = curr.p2 as N; 40 | while (p && p.l2 === curr) { 41 | curr = p; 42 | p = p.p2 as N; 43 | } 44 | return p; 45 | }; 46 | 47 | const insertRight2 = (node: HeadlessNode2, p: HeadlessNode2): void => { 48 | const r = (node.r2 = p.r2); 49 | p.r2 = node; 50 | node.p2 = p; 51 | if (r) r.p2 = node; 52 | }; 53 | 54 | const insertLeft2 = (node: HeadlessNode2, p: HeadlessNode2): void => { 55 | const l = (node.l2 = p.l2); 56 | p.l2 = node; 57 | node.p2 = p; 58 | if (l) l.p2 = node; 59 | }; 60 | 61 | export const insert2 = (root: N | undefined, node: N, comparator: Comparator2): N => { 62 | if (!root) return node; 63 | let curr: N | undefined = root; 64 | while (curr) { 65 | const cmp = comparator(node, curr); 66 | const next: N | undefined = cmp < 0 ? (curr.l2 as N) : (curr.r2 as N); 67 | if (!next) { 68 | if (cmp < 0) insertLeft2(node, curr); 69 | else insertRight2(node, curr); 70 | break; 71 | } else curr = next; 72 | } 73 | return root; 74 | }; 75 | 76 | export const remove2 = (root: N | undefined, node: N): N | undefined => { 77 | const p = node.p2; 78 | const l = node.l2; 79 | const r = node.r2; 80 | node.p2 = node.l2 = node.r2 = undefined; 81 | if (!l && !r) { 82 | if (!p) return undefined; 83 | else if (p.l2 === node) p.l2 = undefined; 84 | else p.r2 = undefined; 85 | return root; 86 | } else if (l && r) { 87 | let mostRightChildFromLeft = l; 88 | while (mostRightChildFromLeft.r2) mostRightChildFromLeft = mostRightChildFromLeft.r2; 89 | mostRightChildFromLeft.r2 = r; 90 | r.p2 = mostRightChildFromLeft; 91 | if (!p) { 92 | l.p2 = undefined; 93 | return l as N; 94 | } 95 | if (p.l2 === node) p.l2 = l; 96 | else p.r2 = l; 97 | l.p2 = p; 98 | return root; 99 | } 100 | const child = (l || r)!; 101 | child.p2 = p; 102 | if (!p) return child as N; 103 | else if (p.l2 === node) p.l2 = child; 104 | else p.r2 = child; 105 | return root; 106 | }; 107 | -------------------------------------------------------------------------------- /src/red-black/__tests__/RbMap.spec.ts: -------------------------------------------------------------------------------- 1 | import {RbMap} from '../RbMap'; 2 | 3 | test('smoke test', () => { 4 | const tree = new RbMap(); 5 | tree.set(1, 1); 6 | tree.set(3, 5); 7 | tree.set(4, 5); 8 | tree.set(3, 15); 9 | tree.set(4.1, 0); 10 | tree.set(44, 123); 11 | // console.log(tree + ''); 12 | expect(tree.get(44)).toBe(123); 13 | const keys: number[] = []; 14 | tree.forEach((node) => keys.push(node.k)); 15 | expect(keys).toEqual([1, 3, 4, 4.1, 44]); 16 | }); 17 | 18 | describe('.first()/next() iteration', () => { 19 | test('for empty map, returns finished iterator', () => { 20 | const tree = new RbMap(); 21 | const entry = tree.first(); 22 | expect(entry).toEqual(undefined); 23 | }); 24 | 25 | test('can iterate through map entries', () => { 26 | const tree = new RbMap(); 27 | tree.set('a', 1); 28 | tree.set('b', 2); 29 | tree.set('c', 3); 30 | const list: [string, number][] = []; 31 | for (let entry = tree.first(); entry; entry = tree.next(entry)) { 32 | list.push([entry.k, entry.v]); 33 | } 34 | expect(list).toEqual([ 35 | ['a', 1], 36 | ['b', 2], 37 | ['c', 3], 38 | ]); 39 | }); 40 | }); 41 | 42 | describe('.iterator0()', () => { 43 | test('for empty map, returns finished iterator', () => { 44 | const tree = new RbMap(); 45 | const iterator = tree.iterator0(); 46 | const entry = iterator(); 47 | expect(entry).toEqual(undefined); 48 | }); 49 | 50 | test('can iterate through map entries', () => { 51 | const tree = new RbMap(); 52 | tree.set('a', 1); 53 | tree.set('b', 2); 54 | tree.set('c', 3); 55 | const list: [string, number][] = []; 56 | const iterator = tree.iterator0(); 57 | for (let entry = iterator(); entry; entry = iterator()) { 58 | list.push([entry.k, entry.v]); 59 | } 60 | expect(list).toEqual([ 61 | ['a', 1], 62 | ['b', 2], 63 | ['c', 3], 64 | ]); 65 | }); 66 | }); 67 | 68 | describe('.iterator()', () => { 69 | test('for empty map, returns finished iterator', () => { 70 | const tree = new RbMap(); 71 | const iterator = tree.iterator(); 72 | const entry = iterator.next(); 73 | expect(entry).toEqual({done: true, value: undefined}); 74 | }); 75 | 76 | test('can iterate through map entries', () => { 77 | const tree = new RbMap(); 78 | tree.set('a', 1); 79 | tree.set('b', 2); 80 | tree.set('c', 3); 81 | const iterator = tree.iterator(); 82 | const list: [string, number][] = []; 83 | for (let entry = iterator.next(); !entry.done; entry = iterator.next()) { 84 | list.push([entry.value!.k, entry.value!.v]); 85 | } 86 | expect(list).toEqual([ 87 | ['a', 1], 88 | ['b', 2], 89 | ['c', 3], 90 | ]); 91 | }); 92 | }); 93 | 94 | describe('for...of iteration', () => { 95 | test('can iterate through map entries', () => { 96 | const tree = new RbMap(); 97 | tree.set('a', 1); 98 | tree.set('b', 2); 99 | tree.set('c', 3); 100 | const list: [string, number][] = []; 101 | for (const entry of tree.entries()) { 102 | list.push([entry.k, entry.v]); 103 | } 104 | expect(list).toEqual([ 105 | ['a', 1], 106 | ['b', 2], 107 | ['c', 3], 108 | ]); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/avl/__tests__/AvlMap.spec.ts: -------------------------------------------------------------------------------- 1 | import {AvlMap} from '../AvlMap'; 2 | 3 | test('smoke test', () => { 4 | const tree = new AvlMap(); 5 | tree.set(1, 1); 6 | tree.set(3, 5); 7 | tree.set(4, 5); 8 | tree.set(3, 15); 9 | tree.set(4.1, 0); 10 | tree.set(44, 123); 11 | // console.log(tree + ''); 12 | expect(tree.get(44)).toBe(123); 13 | const keys: number[] = []; 14 | tree.forEach((node) => keys.push(node.k)); 15 | expect(keys).toEqual([1, 3, 4, 4.1, 44]); 16 | }); 17 | 18 | describe('.first()/next() iteration', () => { 19 | test('for empty map, returns finished iterator', () => { 20 | const tree = new AvlMap(); 21 | const entry = tree.first(); 22 | expect(entry).toEqual(undefined); 23 | }); 24 | 25 | test('can iterate through map entries', () => { 26 | const tree = new AvlMap(); 27 | tree.set('a', 1); 28 | tree.set('b', 2); 29 | tree.set('c', 3); 30 | const list: [string, number][] = []; 31 | for (let entry = tree.first(); entry; entry = tree.next(entry)) { 32 | list.push([entry.k, entry.v]); 33 | } 34 | expect(list).toEqual([ 35 | ['a', 1], 36 | ['b', 2], 37 | ['c', 3], 38 | ]); 39 | }); 40 | }); 41 | 42 | describe('.iterator0()', () => { 43 | test('for empty map, returns finished iterator', () => { 44 | const tree = new AvlMap(); 45 | const iterator = tree.iterator0(); 46 | const entry = iterator(); 47 | expect(entry).toEqual(undefined); 48 | }); 49 | 50 | test('can iterate through map entries', () => { 51 | const tree = new AvlMap(); 52 | tree.set('a', 1); 53 | tree.set('b', 2); 54 | tree.set('c', 3); 55 | const list: [string, number][] = []; 56 | const iterator = tree.iterator0(); 57 | for (let entry = iterator(); entry; entry = iterator()) { 58 | list.push([entry.k, entry.v]); 59 | } 60 | expect(list).toEqual([ 61 | ['a', 1], 62 | ['b', 2], 63 | ['c', 3], 64 | ]); 65 | }); 66 | }); 67 | 68 | describe('.iterator()', () => { 69 | test('for empty map, returns finished iterator', () => { 70 | const tree = new AvlMap(); 71 | const iterator = tree.iterator(); 72 | const entry = iterator.next(); 73 | expect(entry).toEqual({done: true, value: undefined}); 74 | }); 75 | 76 | test('can iterate through map entries', () => { 77 | const tree = new AvlMap(); 78 | tree.set('a', 1); 79 | tree.set('b', 2); 80 | tree.set('c', 3); 81 | const iterator = tree.iterator(); 82 | const list: [string, number][] = []; 83 | for (let entry = iterator.next(); !entry.done; entry = iterator.next()) { 84 | list.push([entry.value!.k, entry.value!.v]); 85 | } 86 | expect(list).toEqual([ 87 | ['a', 1], 88 | ['b', 2], 89 | ['c', 3], 90 | ]); 91 | }); 92 | }); 93 | 94 | describe('for...of iteration', () => { 95 | test('can iterate through map entries', () => { 96 | const tree = new AvlMap(); 97 | tree.set('a', 1); 98 | tree.set('b', 2); 99 | tree.set('c', 3); 100 | const list: [string, number][] = []; 101 | for (const entry of tree.entries()) { 102 | list.push([entry.k, entry.v]); 103 | } 104 | expect(list).toEqual([ 105 | ['a', 1], 106 | ['b', 2], 107 | ['c', 3], 108 | ]); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sonic-forest", 3 | "private": false, 4 | "publishConfig": { 5 | "access": "public" 6 | }, 7 | "version": "0.0.1", 8 | "description": "High-performance (binary) tree and sorted map implementation (AVL, Splay, Radix, Red-Black)", 9 | "author": { 10 | "name": "streamich", 11 | "url": "https://github.com/streamich" 12 | }, 13 | "homepage": "https://github.com/streamich/sonic-forest", 14 | "repository": "streamich/sonic-forest", 15 | "license": "Apache-2.0", 16 | "funding": { 17 | "type": "github", 18 | "url": "https://github.com/sponsors/streamich" 19 | }, 20 | "keywords": [ 21 | "tree", 22 | "binary tree", 23 | "binary search tree", 24 | "bst", 25 | "avl", 26 | "red-black", 27 | "rb-tree", 28 | "avl-tree", 29 | "splay", 30 | "splay tree", 31 | "radix" 32 | ], 33 | "engines": { 34 | "node": ">=10.0" 35 | }, 36 | "main": "lib/index.js", 37 | "types": "lib/index.d.ts", 38 | "typings": "lib/index.d.ts", 39 | "files": [ 40 | "LICENSE", 41 | "lib/" 42 | ], 43 | "scripts": { 44 | "prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,tsx,js,jsx}\"", 45 | "prettier:check": "prettier --ignore-path .gitignore --list-different 'src/**/*.{ts,tsx,js,jsx}'", 46 | "lint": "yarn tslint", 47 | "tslint": "tslint 'src/**/*.{js,jsx,ts,tsx}' -t verbose --project .", 48 | "clean": "rimraf lib typedocs coverage gh-pages yarn-error.log", 49 | "build": "tsc --project tsconfig.build.json --module commonjs --target es2020 --outDir lib", 50 | "jest": "node -r ts-node/register ./node_modules/.bin/jest", 51 | "test": "jest --maxWorkers 7", 52 | "test:ci": "yarn jest --maxWorkers 3 --no-cache", 53 | "coverage": "yarn test --collectCoverage", 54 | "typedoc": "typedoc", 55 | "build:pages": "rimraf gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage", 56 | "deploy:pages": "gh-pages -d gh-pages", 57 | "publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages" 58 | }, 59 | "peerDependencies": { 60 | "tslib": "2" 61 | }, 62 | "dependencies": { 63 | "tree-dump": "^1.0.0" 64 | }, 65 | "devDependencies": { 66 | "@types/benchmark": "^2.1.2", 67 | "@types/jest": "^29.5.12", 68 | "benchmark": "^2.1.4", 69 | "jest": "^29.7.0", 70 | "js-sdsl": "^4.4.2", 71 | "prettier": "^3.2.5", 72 | "rimraf": "^5.0.0", 73 | "sorted-btree": "^1.8.1", 74 | "ts-jest": "^29.1.2", 75 | "ts-node": "^10.9.2", 76 | "tslib": "^2.6.2", 77 | "tslint": "^6.1.3", 78 | "tslint-config-common": "^1.6.2", 79 | "typedoc": "^0.25.12", 80 | "typescript": "^5.4.4" 81 | }, 82 | "jest": { 83 | "verbose": true, 84 | "testEnvironmentOptions": { 85 | "url": "http://localhost/" 86 | }, 87 | "moduleFileExtensions": [ 88 | "ts", 89 | "js" 90 | ], 91 | "transform": { 92 | "^.+\\.ts$": "ts-jest" 93 | }, 94 | "transformIgnorePatterns": [], 95 | "testRegex": ".*/(__tests__|__jest__|demo)/.*\\.(test|spec)\\.ts$" 96 | }, 97 | "prettier": { 98 | "arrowParens": "always", 99 | "printWidth": 120, 100 | "tabWidth": 2, 101 | "useTabs": false, 102 | "semi": true, 103 | "singleQuote": true, 104 | "trailingComma": "all", 105 | "bracketSpacing": false 106 | }, 107 | "release": { 108 | "branches": [ 109 | "master", 110 | "next" 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/red-black/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import {assertTreeLinks} from '../../__tests__/util'; 2 | import {find, first, next} from '../../util'; 3 | import {IRbTreeNode, RbHeadlessNode} from '../types'; 4 | import {insert, remove, print} from '../util'; 5 | 6 | export const node = (k: K, v: V, black: boolean = false): IRbTreeNode => ({ 7 | k, 8 | v, 9 | b: black, 10 | p: undefined, 11 | l: undefined, 12 | r: undefined, 13 | }); 14 | 15 | export const n = (val: number, black: boolean = false) => node(val, '' + val, black); 16 | 17 | export const linkLeft = (parent: IRbTreeNode, child: IRbTreeNode) => { 18 | parent.l = child; 19 | child.p = parent; 20 | }; 21 | 22 | export const linkRight = (parent: IRbTreeNode, child: IRbTreeNode) => { 23 | parent.r = child; 24 | child.p = parent; 25 | }; 26 | 27 | export const comparator = (a: number, b: number) => a - b; 28 | 29 | export const setup = () => { 30 | const root: {root: IRbTreeNode | undefined} = {root: undefined}; 31 | const ins = (...vals: number[]) => { 32 | vals.forEach((val) => (root.root = insert(root.root, n(val), comparator))); 33 | assertRedBlackTree(root.root); 34 | }; 35 | const del = (val: number) => { 36 | const node = find(root.root!, val, comparator) as IRbTreeNode; 37 | if (!node) return; 38 | root.root = remove(root.root!, node); 39 | assertRedBlackTree(root.root); 40 | }; 41 | return {root, ins, del}; 42 | }; 43 | 44 | export const assertTreeBlackHeight = (node: RbHeadlessNode): number => { 45 | const {l, r} = node; 46 | if (!l && !r) return node.b ? 1 : 0; 47 | const lh = l ? assertTreeBlackHeight(l) : 0; 48 | const rh = r ? assertTreeBlackHeight(r) : 0; 49 | if (lh !== rh) { 50 | // tslint:disable-next-line: no-console 51 | console.log('at node:\n\n' + print(node)); 52 | throw new Error('Black height mismatch'); 53 | } 54 | return lh + (node.b ? 1 : 0); 55 | }; 56 | 57 | export const assertRedBlackTree = (root?: RbHeadlessNode | (RbHeadlessNode & {b: 0 | 1})): void => { 58 | if (!root) return; 59 | if (!!root.p) { 60 | // tslint:disable-next-line: no-console 61 | console.log('root:\n\n' + print(root)); 62 | throw new Error('Root has parent'); 63 | } 64 | if (!root.b) { 65 | // tslint:disable-next-line: no-console 66 | console.log('root:\n\n' + print(root)); 67 | throw new Error('Root is not black'); 68 | } 69 | assertTreeLinks(root); 70 | assertTreeBlackHeight(root); 71 | let curr = first(root); 72 | let prev: typeof curr; 73 | while (curr) { 74 | const {b, l, r, p} = curr; 75 | if (prev) { 76 | if ((prev as IRbTreeNode).k > (curr as IRbTreeNode).k) { 77 | // tslint:disable-next-line: no-console 78 | console.log('at node:\n\n' + print(curr)); 79 | throw new Error('Node is out of order'); 80 | } 81 | } 82 | if (!b) { 83 | if (p && !p.b) { 84 | // tslint:disable-next-line: no-console 85 | console.log('at node:\n\n' + print(curr)); 86 | throw new Error('Red node has red parent'); 87 | } 88 | if (l && !l.b) { 89 | // tslint:disable-next-line: no-console 90 | console.log('at node:\n\n' + print(curr)); 91 | throw new Error('Red node has red left child'); 92 | } 93 | if (r && !r.b) { 94 | // tslint:disable-next-line: no-console 95 | console.log('at node:\n\n' + print(curr)); 96 | throw new Error('Red node has red right child'); 97 | } 98 | } 99 | prev = curr; 100 | curr = next(curr); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/SortedMap/SortedMapIterator.ts: -------------------------------------------------------------------------------- 1 | import {IteratorType} from './constants'; 2 | import {throwIteratorAccessError} from './util'; 3 | import type {SortedMap} from './SortedMap'; 4 | import type {TreeNode, TreeNodeEnableIndex} from './SortedMapNode'; 5 | 6 | export class OrderedMapIterator { 7 | /** 8 | * @internal 9 | */ 10 | _node: TreeNode; 11 | /** 12 | * @internal 13 | */ 14 | protected _header: TreeNode; 15 | /** 16 | * @internal 17 | */ 18 | // protected constructor( 19 | // node: TreeNode, 20 | // header: TreeNode, 21 | // iteratorType?: IteratorType 22 | // ) { 23 | // super(iteratorType); 24 | 25 | // } 26 | 27 | pre: () => this; 28 | next: () => this; 29 | 30 | readonly container: SortedMap; 31 | 32 | /** 33 | * @description Iterator's type. 34 | * @example 35 | * console.log(container.end().iteratorType === IteratorType.NORMAL); // true 36 | */ 37 | readonly iteratorType: IteratorType; 38 | 39 | constructor( 40 | node: TreeNode, 41 | header: TreeNode, 42 | container: SortedMap, 43 | iteratorType: IteratorType = IteratorType.NORMAL, 44 | ) { 45 | this._node = node; 46 | this._header = header; 47 | this.iteratorType = iteratorType; 48 | if (this.iteratorType === IteratorType.NORMAL) { 49 | this.pre = function () { 50 | if (this._node === this._header.l) { 51 | throwIteratorAccessError(); 52 | } 53 | this._node = this._node.prev(); 54 | return this; 55 | }; 56 | 57 | this.next = function () { 58 | if (this._node === this._header) { 59 | throwIteratorAccessError(); 60 | } 61 | this._node = this._node.next(); 62 | return this; 63 | }; 64 | } else { 65 | this.pre = function () { 66 | if (this._node === this._header.r) { 67 | throwIteratorAccessError(); 68 | } 69 | this._node = this._node.next(); 70 | return this; 71 | }; 72 | 73 | this.next = function () { 74 | if (this._node === this._header) { 75 | throwIteratorAccessError(); 76 | } 77 | this._node = this._node.prev(); 78 | return this; 79 | }; 80 | } 81 | this.container = container; 82 | } 83 | 84 | /** 85 | * @description Get the sequential index of the iterator in the tree container.
86 | * Note: 87 | * This function only takes effect when the specified tree container `enableIndex = true`. 88 | * @returns The index subscript of the node in the tree. 89 | * @example 90 | * const st = new OrderedSet([1, 2, 3], true); 91 | * console.log(st.begin().next().index); // 1 92 | */ 93 | get index() { 94 | let _node = this._node as TreeNodeEnableIndex; 95 | const root = this._header.p as TreeNodeEnableIndex; 96 | if (_node === this._header) { 97 | if (root) { 98 | return root._size - 1; 99 | } 100 | return 0; 101 | } 102 | let index = 0; 103 | if (_node.l) { 104 | index += (_node.l as TreeNodeEnableIndex)._size; 105 | } 106 | while (_node !== root) { 107 | const _parent = _node.p as TreeNodeEnableIndex; 108 | if (_node === _parent.r) { 109 | index += 1; 110 | if (_parent.l) { 111 | index += (_parent.l as TreeNodeEnableIndex)._size; 112 | } 113 | } 114 | _node = _parent; 115 | } 116 | return index; 117 | } 118 | isAccessible() { 119 | return this._node !== this._header; 120 | } 121 | 122 | copy() { 123 | return new OrderedMapIterator(this._node, this._header, this.container, this.iteratorType); 124 | } 125 | 126 | equals(iter: OrderedMapIterator) { 127 | return this._node === iter._node; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/__bench__/bench.map.insert.nums.native.ts: -------------------------------------------------------------------------------- 1 | // npx ts-node src/__bench__/bench.map.insert.nums.native.ts 2 | 3 | import {runBenchmark, IBenchmark} from './runBenchmark'; 4 | import {Tree} from '../Tree'; 5 | import {AvlBstNumNumMap} from '../avl/AvlBstNumNumMap'; 6 | import {AvlMap} from '../avl/AvlMap'; 7 | import {RbMap} from '../red-black/RbMap'; 8 | import {RadixTree} from '../radix/RadixTree'; 9 | import * as payloads from './payloads'; 10 | 11 | const benchmark: IBenchmark = { 12 | name: 'Numeric inserts into maps', 13 | warmup: 1000, 14 | payloads: payloads.numbers, 15 | runners: [ 16 | { 17 | name: 'Array[number]', 18 | setup: () => { 19 | return (num: unknown) => { 20 | const map: Array = []; 21 | const numbers = num as number[]; 22 | const length = numbers.length; 23 | for (let i = 0; i < length; i++) { 24 | const key = numbers[i]; 25 | map[key] = key; 26 | } 27 | }; 28 | }, 29 | }, 30 | { 31 | name: 'Record', 32 | setup: () => { 33 | return (num: unknown) => { 34 | const map: Record = {}; 35 | const numbers = num as number[]; 36 | const length = numbers.length; 37 | for (let i = 0; i < length; i++) { 38 | const key = numbers[i]; 39 | map[key] = key; 40 | } 41 | }; 42 | }, 43 | }, 44 | { 45 | name: 'Map', 46 | setup: () => { 47 | return (num: unknown) => { 48 | const map = new Map(); 49 | const numbers = num as number[]; 50 | const length = numbers.length; 51 | for (let i = 0; i < length; i++) { 52 | const key = numbers[i]; 53 | map.set(key, key); 54 | } 55 | }; 56 | }, 57 | }, 58 | { 59 | name: 'json-joy AvlBstNumNumMap', 60 | setup: () => { 61 | return (num: unknown) => { 62 | const map = new AvlBstNumNumMap(); 63 | const numbers = num as number[]; 64 | const length = numbers.length; 65 | for (let i = 0; i < length; i++) { 66 | const key = numbers[i]; 67 | map.set(key, key); 68 | } 69 | }; 70 | }, 71 | }, 72 | { 73 | name: 'json-joy AvlMap', 74 | setup: () => { 75 | return (num: unknown) => { 76 | const map = new AvlMap(); 77 | const numbers = num as number[]; 78 | const length = numbers.length; 79 | for (let i = 0; i < length; i++) { 80 | const key = numbers[i]; 81 | map.set(key, key); 82 | } 83 | }; 84 | }, 85 | }, 86 | { 87 | name: 'json-joy RbMap', 88 | setup: () => { 89 | return (num: unknown) => { 90 | const map = new RbMap(); 91 | const numbers = num as number[]; 92 | const length = numbers.length; 93 | for (let i = 0; i < length; i++) { 94 | const key = numbers[i]; 95 | map.set(key, key); 96 | } 97 | }; 98 | }, 99 | }, 100 | { 101 | name: 'json-joy Tree', 102 | setup: () => { 103 | return (num: unknown) => { 104 | const map = new Tree(); 105 | const numbers = num as number[]; 106 | const length = numbers.length; 107 | for (let i = 0; i < length; i++) { 108 | const key = numbers[i]; 109 | map.set(key, key); 110 | } 111 | }; 112 | }, 113 | }, 114 | { 115 | name: 'json-joy RadixTree', 116 | setup: () => { 117 | return (num: unknown) => { 118 | const map = new RadixTree(); 119 | const numbers = num as number[]; 120 | const length = numbers.length; 121 | for (let i = 0; i < length; i++) { 122 | const key = numbers[i]; 123 | map.set('' + key, key); 124 | } 125 | }; 126 | }, 127 | }, 128 | ], 129 | }; 130 | 131 | runBenchmark(benchmark); 132 | -------------------------------------------------------------------------------- /src/avl/__tests__/AvlSet.spec.ts: -------------------------------------------------------------------------------- 1 | import {AvlSet} from '../AvlSet'; 2 | 3 | test('can add numbers to set', () => { 4 | const set = new AvlSet(); 5 | expect(set.size()).toBe(0); 6 | expect(set.has(1)).toBe(false); 7 | set.add(1); 8 | expect(set.size()).toBe(1); 9 | expect(set.has(1)).toBe(true); 10 | set.add(24); 11 | set.add(42); 12 | set.add(42); 13 | expect(set.size()).toBe(3); 14 | expect(set.has(24)).toBe(true); 15 | expect(set.has(42)).toBe(true); 16 | expect(set.has(25)).toBe(false); 17 | }); 18 | 19 | test('can remove numbers from set', () => { 20 | const set = new AvlSet(); 21 | set.add(1); 22 | set.add(24); 23 | set.add(42); 24 | expect(set.has(1)).toBe(true); 25 | expect(set.has(24)).toBe(true); 26 | expect(set.has(42)).toBe(true); 27 | set.del(24); 28 | expect(set.has(1)).toBe(true); 29 | expect(set.has(24)).toBe(false); 30 | expect(set.has(42)).toBe(true); 31 | set.del(1); 32 | expect(set.has(1)).toBe(false); 33 | expect(set.has(24)).toBe(false); 34 | expect(set.has(42)).toBe(true); 35 | expect(set.size()).toBe(1); 36 | set.del(42); 37 | expect(set.has(1)).toBe(false); 38 | expect(set.has(24)).toBe(false); 39 | expect(set.has(42)).toBe(false); 40 | expect(set.size()).toBe(0); 41 | }); 42 | 43 | test('can store structs', () => { 44 | class Struct { 45 | constructor( 46 | public x: number, 47 | public y: number, 48 | ) {} 49 | } 50 | const set = new AvlSet((a, b) => { 51 | const dx = a.x - b.x; 52 | return dx === 0 ? a.y - b.y : dx; 53 | }); 54 | set.add(new Struct(0, 0)); 55 | set.add(new Struct(0, 1)); 56 | expect(set.size()).toBe(2); 57 | set.del(new Struct(0, 0)); 58 | expect(set.size()).toBe(1); 59 | expect(set.has(new Struct(0, 0))).toBe(false); 60 | expect(set.has(new Struct(0, 1))).toBe(true); 61 | set.add(new Struct(2, 3)); 62 | set.add(new Struct(3, 3)); 63 | expect(set.size()).toBe(3); 64 | expect(set.has(new Struct(3, 3))).toBe(true); 65 | expect(set.has(new Struct(2, 3))).toBe(true); 66 | }); 67 | 68 | describe('.first()/next() iteration', () => { 69 | test('for empty map, returns finished iterator', () => { 70 | const tree = new AvlSet(); 71 | const entry = tree.first(); 72 | expect(entry).toEqual(undefined); 73 | }); 74 | 75 | test('can iterate through map entries', () => { 76 | const tree = new AvlSet(); 77 | tree.add(1); 78 | tree.add(2); 79 | tree.add(3); 80 | const list: number[] = []; 81 | for (let entry = tree.first(); entry; entry = tree.next(entry)) { 82 | list.push(entry.k); 83 | } 84 | expect(list).toEqual([1, 2, 3]); 85 | }); 86 | }); 87 | 88 | describe('.iterator0()', () => { 89 | test('for empty map, returns finished iterator', () => { 90 | const tree = new AvlSet(); 91 | const iterator = tree.iterator0(); 92 | const entry = iterator(); 93 | expect(entry).toEqual(undefined); 94 | }); 95 | 96 | test('can iterate through map entries', () => { 97 | const tree = new AvlSet(); 98 | tree.add(1); 99 | tree.add(2); 100 | tree.add(3); 101 | const list: number[] = []; 102 | const iterator = tree.iterator0(); 103 | for (let entry = iterator(); entry; entry = iterator()) { 104 | list.push(entry.k); 105 | } 106 | expect(list).toEqual([1, 2, 3]); 107 | }); 108 | }); 109 | 110 | describe('.iterator()', () => { 111 | test('for empty map, returns finished iterator', () => { 112 | const tree = new AvlSet(); 113 | const iterator = tree.iterator(); 114 | const entry = iterator.next(); 115 | expect(entry).toEqual({done: true, value: undefined}); 116 | }); 117 | 118 | test('can iterate through map entries', () => { 119 | const tree = new AvlSet(); 120 | tree.add(1); 121 | tree.add(2); 122 | tree.add(3); 123 | const list: number[] = []; 124 | const iterator = tree.iterator(); 125 | for (let entry = iterator.next(); !entry.done; entry = iterator.next()) { 126 | list.push(entry.value!.k); 127 | } 128 | expect(list).toEqual([1, 2, 3]); 129 | }); 130 | }); 131 | 132 | describe('for...of iteration', () => { 133 | test('can iterate through map entries', () => { 134 | const tree = new AvlSet(); 135 | tree.add(1); 136 | tree.add(2); 137 | tree.add(3); 138 | const list: number[] = []; 139 | for (const entry of tree.entries()) { 140 | list.push(entry.k); 141 | } 142 | expect(list).toEqual([1, 2, 3]); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /src/radix/__tests__/binaryRadix.spec.ts: -------------------------------------------------------------------------------- 1 | import {BinaryTrieNode} from '../BinaryTrieNode'; 2 | import {Slice} from '../Slice'; 3 | import {insert} from '../binaryRadix'; 4 | import {first, last} from '../../util'; 5 | 6 | test('can insert a node with no common prefix', () => { 7 | const root = new BinaryTrieNode(new Slice(new Uint8Array(), 0, 0), undefined); 8 | let cnt_ = 0; 9 | const cnt = () => cnt_++; 10 | 11 | insert(root, new Uint8Array([1, 2, 3]), cnt()); 12 | insert(root, new Uint8Array([1, 2, 3, 4]), cnt()); 13 | insert(root, new Uint8Array([1, 2, 3, 4, 5]), cnt()); 14 | insert(root, new Uint8Array([1, 2, 3, 4, 255]), cnt()); 15 | insert(root, new Uint8Array([100]), cnt()); 16 | insert(root, new Uint8Array([100, 100]), cnt()); 17 | insert(root, new Uint8Array([100, 1]), cnt()); 18 | insert(root, new Uint8Array([100, 2]), cnt()); 19 | insert(root, new Uint8Array([100, 3]), cnt()); 20 | insert(root, new Uint8Array([100, 4]), cnt()); 21 | insert(root, new Uint8Array([100, 5]), cnt()); 22 | insert(root, new Uint8Array([100, 6]), cnt()); 23 | insert(root, new Uint8Array([100, 100]), cnt()); // duplicate 24 | insert(root, new Uint8Array([100, 7]), cnt()); 25 | insert(root, new Uint8Array([100, 7]), cnt()); // duplicate 26 | insert(root, new Uint8Array([1, 1]), cnt()); 27 | insert(root, new Uint8Array([1, 1]), cnt()); // duplicate 28 | insert(root, new Uint8Array([1, 1, 1]), cnt()); 29 | insert(root, new Uint8Array([1, 1, 1]), cnt()); // duplicate 30 | 31 | expect(root.toRecord()).toMatchObject({ 32 | '1,2,3': 0, 33 | '1,2,3,4': 1, 34 | '1,2,3,4,5': 2, 35 | '1,2,3,4,255': 3, 36 | '1,1': 16, 37 | '1,1,1': 18, 38 | '100': 4, 39 | '100,1': 6, 40 | '100,2': 7, 41 | '100,3': 8, 42 | '100,4': 9, 43 | '100,5': 10, 44 | '100,6': 11, 45 | '100,100': 12, 46 | '100,7': 14, 47 | }); 48 | }); 49 | 50 | test('constructs common prefix', () => { 51 | const root = new BinaryTrieNode(new Slice(new Uint8Array(), 0, 0), undefined); 52 | // Binary equivalent of similar prefixed data 53 | insert(root, new Uint8Array([71, 69, 84, 32, 47, 117, 115, 101, 114, 115]), 1); // "GET /users" 54 | insert(root, new Uint8Array([71, 69, 84, 32, 47, 112, 111, 115, 116, 115]), 2); // "GET /posts" 55 | 56 | expect(first(root.children)).toBe(last(root.children)); 57 | const child = first(root.children); 58 | expect(child?.k.toUint8Array()).toEqual(new Uint8Array([71, 69, 84, 32, 47])); // "GET /" 59 | expect(child?.v).toBe(undefined); 60 | expect(child?.p).toBe(undefined); 61 | expect(child?.l).toBe(undefined); 62 | expect(child?.r).toBe(undefined); 63 | expect(child?.children).not.toBe(undefined); 64 | 65 | // Check children order (should be sorted by byte values) 66 | const firstChild = first(child?.children!); 67 | const lastChild = last(child?.children!); 68 | expect(firstChild!.k.toUint8Array()).toEqual(new Uint8Array([112, 111, 115, 116, 115])); // "posts" 69 | expect(lastChild!.k.toUint8Array()).toEqual(new Uint8Array([117, 115, 101, 114, 115])); // "users" 70 | }); 71 | 72 | test('constructs common prefix from binary protocol routes', () => { 73 | const root = new BinaryTrieNode(new Slice(new Uint8Array(), 0, 0), undefined); 74 | // Binary equivalent of HTTP methods with common prefix 75 | insert(root, new Uint8Array([71, 69, 84, 32, 47, 117, 115, 101, 114, 115]), 1); // "GET /users" 76 | insert(root, new Uint8Array([80, 79, 83, 84, 32, 47, 117, 115, 101, 114, 115]), 2); // "POST /users" 77 | insert(root, new Uint8Array([80, 85, 84, 32, 47, 117, 115, 101, 114, 115]), 3); // "PUT /users" 78 | 79 | expect(root.toRecord()).toMatchObject({ 80 | '71,69,84,32,47,117,115,101,114,115': 1, 81 | '80,79,83,84,32,47,117,115,101,114,115': 2, 82 | '80,85,84,32,47,117,115,101,114,115': 3, 83 | }); 84 | }); 85 | 86 | test('handles empty keys', () => { 87 | const root = new BinaryTrieNode(new Slice(new Uint8Array(), 0, 0), undefined); 88 | insert(root, new Uint8Array([]), 1); 89 | insert(root, new Uint8Array([1]), 2); 90 | 91 | expect(root.toRecord()).toMatchObject({ 92 | '': 1, 93 | '1': 2, 94 | }); 95 | }); 96 | 97 | test('handles single byte differences', () => { 98 | const root = new BinaryTrieNode(new Slice(new Uint8Array(), 0, 0), undefined); 99 | insert(root, new Uint8Array([1, 2, 3, 100]), 1); 100 | insert(root, new Uint8Array([1, 2, 3, 101]), 2); 101 | insert(root, new Uint8Array([1, 2, 3, 102]), 3); 102 | 103 | expect(root.toRecord()).toMatchObject({ 104 | '1,2,3,100': 1, 105 | '1,2,3,101': 2, 106 | '1,2,3,102': 3, 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/llrb-tree/util.ts: -------------------------------------------------------------------------------- 1 | import {LlrbNode} from './LlrbTree'; 2 | import type {Comparator} from '../types'; 3 | 4 | /** 5 | * Check if a node is red (non-black). 6 | */ 7 | export function isRed(node: LlrbNode | undefined): boolean { 8 | return node ? !node.b : false; 9 | } 10 | 11 | /** 12 | * Flip colors of node and its children. 13 | */ 14 | export function colorFlip(node: LlrbNode): void { 15 | node.b = !node.b; 16 | if (node.l) node.l.b = !node.l.b; 17 | if (node.r) node.r.b = !node.r.b; 18 | } 19 | 20 | /** 21 | * Rotate left with parent pointer updates. 22 | */ 23 | export function rotateLeft(node: LlrbNode): LlrbNode { 24 | const x = node.r!; 25 | node.r = x.l; 26 | if (x.l) x.l.p = node; 27 | x.l = node; 28 | x.p = node.p; 29 | node.p = x; 30 | 31 | // Update parent's child pointer 32 | if (x.p) { 33 | if (x.p.l === node) x.p.l = x; 34 | else x.p.r = x; 35 | } 36 | 37 | x.b = node.b; 38 | node.b = false; 39 | return x; 40 | } 41 | 42 | /** 43 | * Rotate right with parent pointer updates. 44 | */ 45 | export function rotateRight(node: LlrbNode): LlrbNode { 46 | const x = node.l!; 47 | node.l = x.r; 48 | if (x.r) x.r.p = node; 49 | x.r = node; 50 | x.p = node.p; 51 | node.p = x; 52 | 53 | // Update parent's child pointer 54 | if (x.p) { 55 | if (x.p.l === node) x.p.l = x; 56 | else x.p.r = x; 57 | } 58 | 59 | x.b = node.b; 60 | node.b = false; 61 | return x; 62 | } 63 | 64 | /** 65 | * Move red link to the left. 66 | */ 67 | export function moveRedLeft(node: LlrbNode): LlrbNode { 68 | colorFlip(node); 69 | if (node.r && (node.r.l ? !node.r.l.b : false)) { 70 | node.r = rotateRight(node.r); 71 | node = rotateLeft(node); 72 | colorFlip(node); 73 | } 74 | return node; 75 | } 76 | 77 | /** 78 | * Move red link to the right. 79 | */ 80 | export function moveRedRight(node: LlrbNode): LlrbNode { 81 | colorFlip(node); 82 | if (node.l && (node.l.l ? !node.l.l.b : false)) { 83 | node = rotateRight(node); 84 | colorFlip(node); 85 | } 86 | return node; 87 | } 88 | 89 | /** 90 | * Balance the LLRB tree after modifications. 91 | */ 92 | export function balance(node: LlrbNode): LlrbNode { 93 | if (node.r ? !node.r.b : false) { 94 | node = rotateLeft(node); 95 | } 96 | if ((node.l ? !node.l.b : false) && node.l && (node.l.l ? !node.l.l.b : false)) { 97 | node = rotateRight(node); 98 | } 99 | if ((node.l ? !node.l.b : false) && (node.r ? !node.r.b : false)) { 100 | colorFlip(node); 101 | } 102 | return node; 103 | } 104 | 105 | /** 106 | * Delete the minimum node from the subtree. 107 | */ 108 | export function deleteMin(node: LlrbNode): LlrbNode | undefined { 109 | if (!node.l) { 110 | return undefined; 111 | } 112 | if (!(node.l ? !node.l.b : false) && node.l && !(node.l.l ? !node.l.l.b : false)) { 113 | node = moveRedLeft(node); 114 | } 115 | node.l = deleteMin(node.l!); 116 | if (node.l) node.l.p = node; 117 | return balance(node); 118 | } 119 | 120 | /** 121 | * Find the minimum node in the subtree. 122 | */ 123 | export function min(node: LlrbNode): LlrbNode { 124 | while (node.l) { 125 | node = node.l; 126 | } 127 | return node; 128 | } 129 | 130 | /** 131 | * Delete a node with the given key from the subtree. 132 | */ 133 | export function deleteNode( 134 | node: LlrbNode | undefined, 135 | k: K, 136 | comparator: Comparator, 137 | ): LlrbNode | undefined { 138 | if (!node) return undefined; 139 | 140 | const cmp = comparator(k, node.k); 141 | 142 | if (cmp < 0) { 143 | if (!node.l) return node; 144 | if (!(node.l ? !node.l.b : false) && node.l && !(node.l.l ? !node.l.l.b : false)) { 145 | node = moveRedLeft(node); 146 | } 147 | node.l = deleteNode(node.l!, k, comparator); 148 | if (node.l) node.l.p = node; 149 | } else { 150 | if (node.l ? !node.l.b : false) { 151 | node = rotateRight(node); 152 | } 153 | 154 | if (cmp === 0 && !node.r) { 155 | return undefined; 156 | } 157 | 158 | if (!node.r) return node; 159 | if (!(node.r ? !node.r.b : false) && !(node.r.l ? !node.r.l.b : false)) { 160 | node = moveRedRight(node); 161 | } 162 | 163 | if (comparator(k, node.k) === 0) { 164 | const minNode = min(node.r!); 165 | node.k = minNode.k; 166 | node.v = minNode.v; 167 | node.r = deleteMin(node.r!); 168 | if (node.r) node.r.p = node; 169 | } else { 170 | node.r = deleteNode(node.r, k, comparator); 171 | if (node.r) node.r.p = node; 172 | } 173 | } 174 | 175 | return balance(node); 176 | } 177 | -------------------------------------------------------------------------------- /src/avl/AvlMapOld.ts: -------------------------------------------------------------------------------- 1 | import {insert, insertLeft, insertRight, remove, print} from './util'; 2 | import {printTree} from '../print/printTree'; 3 | import {findOrNextLower, first, last, next} from '../util'; 4 | import type {Comparator, HeadlessNode, SonicMap} from '../types'; 5 | import type {AvlNodeReference, IAvlTreeNode} from './types'; 6 | 7 | export class AvlNode implements IAvlTreeNode { 8 | public p: AvlNode | undefined = undefined; 9 | public l: AvlNode | undefined = undefined; 10 | public r: AvlNode | undefined = undefined; 11 | public bf: number = 0; 12 | constructor( 13 | public readonly k: K, 14 | public v: V, 15 | ) {} 16 | } 17 | 18 | const defaultComparator = (a: unknown, b: unknown) => (a === b ? 0 : (a as any) < (b as any) ? -1 : 1); 19 | 20 | export class AvlMap implements SonicMap> { 21 | public root: AvlNode | undefined = undefined; 22 | public readonly comparator: Comparator; 23 | 24 | constructor(comparator?: Comparator) { 25 | this.comparator = comparator || defaultComparator; 26 | } 27 | 28 | public set(k: K, v: V): AvlNodeReference> { 29 | const root = this.root; 30 | if (!root) { 31 | const item = new AvlNode(k, v); 32 | this.root = insert(this.root, item, this.comparator); 33 | this._size++; 34 | return item; 35 | } 36 | const comparator = this.comparator; 37 | let next: AvlNode | undefined = root, 38 | curr: AvlNode | undefined = next; 39 | let cmp: number = 0; 40 | do { 41 | curr = next; 42 | cmp = comparator(k, curr.k); 43 | if (cmp === 0) return (curr.v = v), curr; 44 | } while ((next = cmp < 0 ? (curr.l as AvlNode) : (curr.r as AvlNode))); 45 | const node = new AvlNode(k, v); 46 | this.root = 47 | cmp < 0 ? (insertLeft(root, node, curr) as AvlNode) : (insertRight(root, node, curr) as AvlNode); 48 | this._size++; 49 | return node; 50 | } 51 | 52 | public find(k: K): AvlNodeReference> | undefined { 53 | const comparator = this.comparator; 54 | let curr: AvlNode | undefined = this.root; 55 | while (curr) { 56 | const cmp = comparator(k, curr.k); 57 | if (cmp === 0) return curr; 58 | curr = cmp < 0 ? (curr.l as AvlNode) : (curr.r as AvlNode); 59 | } 60 | return undefined; 61 | } 62 | 63 | public get(k: K): V | undefined { 64 | return this.find(k)?.v; 65 | } 66 | 67 | public del(k: K): boolean { 68 | const node = this.find(k); 69 | if (!node) return false; 70 | this.root = remove(this.root, node as IAvlTreeNode); 71 | this._size--; 72 | return true; 73 | } 74 | 75 | public clear(): void { 76 | this._size = 0; 77 | this.root = undefined; 78 | } 79 | 80 | public has(k: K): boolean { 81 | return !!this.find(k); 82 | } 83 | 84 | public _size: number = 0; 85 | 86 | public size(): number { 87 | return this._size; 88 | } 89 | 90 | public isEmpty(): boolean { 91 | return !this.root; 92 | } 93 | 94 | public getOrNextLower(k: K): AvlNode | undefined { 95 | return (findOrNextLower(this.root, k, this.comparator) as AvlNode) || undefined; 96 | } 97 | 98 | public forEach(fn: (node: AvlNode) => void): void { 99 | let curr = this.first(); 100 | if (!curr) return; 101 | do fn(curr!); 102 | while ((curr = next(curr as HeadlessNode) as AvlNode | undefined)); 103 | } 104 | 105 | public first(): AvlNode | undefined { 106 | const root = this.root; 107 | return root ? first(root) : undefined; 108 | } 109 | 110 | public last(): AvlNode | undefined { 111 | const root = this.root; 112 | return root ? last(root) : undefined; 113 | } 114 | 115 | public readonly next = next; 116 | 117 | public iterator0(): () => undefined | AvlNode { 118 | let curr = this.first(); 119 | return () => { 120 | if (!curr) return; 121 | const value = curr; 122 | curr = next(curr as HeadlessNode) as AvlNode | undefined; 123 | return value; 124 | }; 125 | } 126 | 127 | public iterator(): Iterator> { 128 | const iterator = this.iterator0(); 129 | return { 130 | next: () => { 131 | const value = iterator(); 132 | const res = >>{value, done: !value}; 133 | return res; 134 | }, 135 | }; 136 | } 137 | 138 | public entries(): IterableIterator> { 139 | return {[Symbol.iterator]: () => this.iterator()}; 140 | } 141 | 142 | public toString(tab: string): string { 143 | return this.constructor.name + printTree(tab, [(tab) => print(this.root, tab)]); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/__bench__/runBenchmark.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-console */ 2 | 3 | import * as Benchmark from 'benchmark'; 4 | import * as os from 'os'; 5 | import * as fs from 'fs'; 6 | 7 | export interface Runner { 8 | name: string | ((data: unknown) => string); 9 | setup: (data: unknown) => (data: unknown) => void; 10 | } 11 | 12 | export interface Payload { 13 | name: string | ((data: unknown) => string); 14 | data: unknown; 15 | } 16 | 17 | export interface IBenchmark { 18 | name: string; 19 | description?: string; 20 | warmup?: number; 21 | payloads?: Payload[]; 22 | test?: (payload: unknown, result: unknown) => boolean; 23 | runners: Runner[]; 24 | } 25 | 26 | export type PayloadResult = [suite: Benchmark.Suite, payload: Payload, events: Benchmark.Event[]]; 27 | 28 | export const runBenchmark = (benchmark: IBenchmark): PayloadResult[] => { 29 | const title = 'Benchmark: ' + (benchmark.name || '[unknown benchmark]'); 30 | console.log('='.repeat(100 - title.length - 2) + ' ' + title); 31 | 32 | const warmup = !benchmark.warmup ? 'Not specified' : `${benchmark.warmup}x`; 33 | const version = process.version; 34 | const arch = os.arch(); 35 | const cpu = os.cpus()[0].model; 36 | 37 | console.log('Warmup:', warmup, ', Node.js:', version, ', Arch:', arch, ', CPU:', cpu); 38 | 39 | const result: PayloadResult[] = []; 40 | 41 | for (const payload of benchmark.payloads || [{name: 'No payload', data: undefined, test: undefined}]) { 42 | const suite = new Benchmark.Suite(); 43 | const data = payload?.data; 44 | const name = payload?.name || '[unknown payload]'; 45 | const title = typeof name === 'function' ? name(data) : name; 46 | console.log('-'.repeat(100 - title.length - 2) + ' ' + title); 47 | 48 | for (const runner of benchmark.runners) { 49 | const fn = runner.setup(data); 50 | if (benchmark.warmup) for (let i = 0; i < benchmark.warmup; i++) fn(data); 51 | let isCorrect: undefined | boolean = undefined; 52 | if (benchmark.test) { 53 | try { 54 | isCorrect = benchmark.test(data, fn(data)); 55 | } catch { 56 | isCorrect = false; 57 | } 58 | } 59 | const icon = isCorrect === undefined ? '' : isCorrect ? '👍' : '👎'; 60 | suite.add((icon ? icon + ' ' : '') + (typeof runner.name === 'function' ? runner.name(data) : runner.name), () => 61 | fn(data), 62 | ); 63 | } 64 | 65 | const events: Benchmark.Event[] = []; 66 | suite.on('cycle', (event: Benchmark.Event) => { 67 | events.push(event); 68 | console.log(String(event.target)); 69 | }); 70 | suite.on('complete', () => { 71 | console.log(`Fastest is ${suite.filter('fastest').map('name')}`); 72 | }); 73 | suite.run(); 74 | 75 | result.push([suite, payload, events]); 76 | } 77 | 78 | return result; 79 | }; 80 | 81 | export interface IBenchmarkResult { 82 | id: number; 83 | name?: string; 84 | count: number; 85 | cycles: number; 86 | hz: number; 87 | compiled: (() => void) | string; 88 | error: Error; 89 | fn: (() => void) | string; 90 | aborted: boolean; 91 | running: boolean; 92 | setup: (() => void) | string; 93 | teardown: (() => void) | string; 94 | stats: Benchmark.Stats; 95 | times: Benchmark.Times; 96 | } 97 | 98 | export const formatSuite = ([suite, payload, events]: PayloadResult): string => { 99 | let str = ''; 100 | const name = typeof payload.name === 'function' ? payload.name(payload.data) : payload.name; 101 | str += `\n## Payload: __${name}__\n`; 102 | str += '\n'; 103 | for (const event of events) { 104 | str += `- ${event.target}\n`; 105 | } 106 | str += '\n'; 107 | str += `Fastest is __${suite.filter('fastest').map('name')}__\n`; 108 | str += '\n'; 109 | return str; 110 | }; 111 | 112 | export const formatSuites = (benchmark: IBenchmark, result: PayloadResult[]): string => { 113 | let str = ''; 114 | str += `# Benchmark report: __${benchmark.name}__\n`; 115 | str += '\n'; 116 | const warmup = !benchmark.warmup ? 'Not specified' : `${benchmark.warmup}x`; 117 | const version = process.version; 118 | const arch = os.arch(); 119 | const cpu = os.cpus()[0].model; 120 | str += `> Warmup: ${warmup}, Node.js: ${version}, Arch: ${arch}, CPU: ${cpu}\n`; 121 | str += '\n'; 122 | if (benchmark.description) str += benchmark.description + '\n'; 123 | str += '\n'; 124 | for (const res of result) str += formatSuite(res); 125 | return str; 126 | }; 127 | 128 | export const runBenchmarkAndSave = (benchmark: IBenchmark, path: string): void => { 129 | fs.mkdirSync(path, {recursive: true}); 130 | const results = runBenchmark(benchmark); 131 | const markdown = formatSuites(benchmark, results); 132 | fs.writeFileSync(path + `/${benchmark.name.replace(/[^a-z0-9]/gi, '-').toLowerCase()}.md`, markdown); 133 | }; 134 | -------------------------------------------------------------------------------- /src/radix/__tests__/Slice.spec.ts: -------------------------------------------------------------------------------- 1 | import {Slice} from '../Slice'; 2 | 3 | describe('Slice', () => { 4 | test('can create slice from Uint8Array', () => { 5 | const data = new Uint8Array([1, 2, 3, 4, 5]); 6 | const slice = Slice.fromUint8Array(data); 7 | expect(slice.length).toBe(5); 8 | expect(slice.start).toBe(0); 9 | expect(slice.data).toBe(data); 10 | }); 11 | 12 | test('can access bytes by index', () => { 13 | const data = new Uint8Array([10, 20, 30, 40, 50]); 14 | const slice = Slice.fromUint8Array(data); 15 | expect(slice.at(0)).toBe(10); 16 | expect(slice.at(2)).toBe(30); 17 | expect(slice.at(4)).toBe(50); 18 | }); 19 | 20 | test('throws error for out of bounds access', () => { 21 | const data = new Uint8Array([1, 2, 3]); 22 | const slice = Slice.fromUint8Array(data); 23 | expect(() => slice.at(-1)).toThrow(); 24 | expect(() => slice.at(3)).toThrow(); 25 | }); 26 | 27 | test('can create substring', () => { 28 | const data = new Uint8Array([1, 2, 3, 4, 5]); 29 | const slice = Slice.fromUint8Array(data); 30 | const sub = slice.substring(1, 3); 31 | expect(sub.length).toBe(3); 32 | expect(sub.at(0)).toBe(2); 33 | expect(sub.at(1)).toBe(3); 34 | expect(sub.at(2)).toBe(4); 35 | }); 36 | 37 | test('can create substring without length', () => { 38 | const data = new Uint8Array([1, 2, 3, 4, 5]); 39 | const slice = Slice.fromUint8Array(data); 40 | const sub = slice.substring(2); 41 | expect(sub.length).toBe(3); 42 | expect(sub.at(0)).toBe(3); 43 | expect(sub.at(1)).toBe(4); 44 | expect(sub.at(2)).toBe(5); 45 | }); 46 | 47 | test('equals works correctly', () => { 48 | const data1 = new Uint8Array([1, 2, 3]); 49 | const data2 = new Uint8Array([1, 2, 3]); 50 | const data3 = new Uint8Array([1, 2, 4]); 51 | 52 | const slice1 = Slice.fromUint8Array(data1); 53 | const slice2 = Slice.fromUint8Array(data2); 54 | const slice3 = Slice.fromUint8Array(data3); 55 | 56 | expect(slice1.equals(slice2)).toBe(true); 57 | expect(slice1.equals(slice3)).toBe(false); 58 | }); 59 | 60 | test('compare works correctly', () => { 61 | const slice1 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 62 | const slice2 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 63 | const slice3 = Slice.fromUint8Array(new Uint8Array([1, 2, 4])); 64 | const slice4 = Slice.fromUint8Array(new Uint8Array([1, 2])); 65 | 66 | expect(slice1.compare(slice2)).toBe(0); 67 | expect(slice1.compare(slice3)).toBeLessThan(0); 68 | expect(slice3.compare(slice1)).toBeGreaterThan(0); 69 | expect(slice1.compare(slice4)).toBeGreaterThan(0); 70 | expect(slice4.compare(slice1)).toBeLessThan(0); 71 | }); 72 | 73 | test('toUint8Array creates copy', () => { 74 | const data = new Uint8Array([1, 2, 3]); 75 | const slice = Slice.fromUint8Array(data); 76 | const copy = slice.toUint8Array(); 77 | 78 | expect(copy).toEqual(data); 79 | expect(copy).not.toBe(data); // Different object 80 | }); 81 | 82 | test('substring slice toUint8Array works correctly', () => { 83 | const data = new Uint8Array([1, 2, 3, 4, 5]); 84 | const slice = Slice.fromUint8Array(data); 85 | const sub = slice.substring(1, 3); 86 | const subArray = sub.toUint8Array(); 87 | 88 | expect(subArray).toEqual(new Uint8Array([2, 3, 4])); 89 | }); 90 | }); 91 | 92 | describe('getCommonPrefixLength', () => { 93 | test('finds common prefix of identical slices', () => { 94 | const slice1 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 95 | const slice2 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 96 | expect(slice1.getCommonPrefixLength(slice2)).toBe(3); 97 | }); 98 | 99 | test('finds common prefix of different slices', () => { 100 | const slice1 = Slice.fromUint8Array(new Uint8Array([1, 2, 3, 4])); 101 | const slice2 = Slice.fromUint8Array(new Uint8Array([1, 2, 5, 6])); 102 | expect(slice1.getCommonPrefixLength(slice2)).toBe(2); 103 | }); 104 | 105 | test('finds common prefix with different lengths', () => { 106 | const slice1 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 107 | const slice2 = Slice.fromUint8Array(new Uint8Array([1, 2])); 108 | expect(slice1.getCommonPrefixLength(slice2)).toBe(2); 109 | }); 110 | 111 | test('handles no common prefix', () => { 112 | const slice1 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 113 | const slice2 = Slice.fromUint8Array(new Uint8Array([4, 5, 6])); 114 | expect(slice1.getCommonPrefixLength(slice2)).toBe(0); 115 | }); 116 | 117 | test('handles empty slices', () => { 118 | const slice1 = Slice.fromUint8Array(new Uint8Array([])); 119 | const slice2 = Slice.fromUint8Array(new Uint8Array([1, 2, 3])); 120 | expect(slice1.getCommonPrefixLength(slice2)).toBe(0); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/radix/__tests__/BinaryRadixTree.fuzzing.spec.ts: -------------------------------------------------------------------------------- 1 | import {BinaryRadixTree} from '../BinaryRadixTree'; 2 | 3 | // Helper to generate deterministic keys for reliable testing 4 | const generateTestKey = (index: number): Uint8Array => { 5 | const bytes = []; 6 | let num = index + 1; // Ensure we start from 1, not 0 7 | while (num > 0) { 8 | bytes.push((num % 5) + 1); // Values 1-5 9 | num = Math.floor(num / 5); 10 | } 11 | return new Uint8Array(bytes.slice(0, 3)); // Limit to 3 bytes max 12 | }; 13 | 14 | // Helper to convert Uint8Array to string for Map keys 15 | const keyToString = (key: Uint8Array): string => { 16 | return JSON.stringify(Array.from(key)); 17 | }; 18 | 19 | // Helper to convert string back to Uint8Array 20 | const stringToKey = (str: string): Uint8Array => { 21 | return new Uint8Array(JSON.parse(str)); 22 | }; 23 | 24 | describe('BinaryRadixTree fuzzing', () => { 25 | test('deterministic insertion fuzzing', () => { 26 | const tree = new BinaryRadixTree(); 27 | const shadowMap = new Map(); 28 | 29 | // Insert 20 deterministic keys 30 | for (let i = 0; i < 20; i++) { 31 | const key = generateTestKey(i); 32 | const keyString = keyToString(key); 33 | const value = i * 10; 34 | 35 | tree.set(key, value); 36 | shadowMap.set(keyString, value); 37 | 38 | // Validate after each insertion 39 | expect(tree.size).toBe(shadowMap.size); 40 | expect(tree.get(key)).toBe(value); 41 | } 42 | 43 | // Final validation - check all keys exist 44 | for (const [keyStr, value] of shadowMap) { 45 | const originalKey = stringToKey(keyStr); 46 | expect(tree.get(originalKey)).toBe(value); 47 | } 48 | 49 | expect(tree.size).toBe(shadowMap.size); 50 | }); 51 | 52 | test('simple insertion fuzzing with shuffled order', () => { 53 | const tree = new BinaryRadixTree(); 54 | const shadowMap = new Map(); 55 | 56 | // Generate a set of test keys first 57 | const testKeys = []; 58 | for (let i = 0; i < 15; i++) { 59 | testKeys.push(generateTestKey(i)); 60 | } 61 | 62 | // Insert keys in shuffled order using deterministic shuffle 63 | const shuffledIndices = [0, 7, 3, 11, 1, 9, 5, 13, 2, 8, 4, 12, 6, 10, 14]; 64 | 65 | for (const index of shuffledIndices) { 66 | const key = testKeys[index]; 67 | const keyString = keyToString(key); 68 | const value = `value-${index}`; 69 | 70 | tree.set(key, value); 71 | shadowMap.set(keyString, value); 72 | 73 | expect(tree.size).toBe(shadowMap.size); 74 | } 75 | 76 | // Validate all keys exist 77 | for (const [keyStr, value] of shadowMap) { 78 | const originalKey = stringToKey(keyStr); 79 | expect(tree.get(originalKey)).toBe(value); 80 | } 81 | 82 | expect(tree.size).toBe(shadowMap.size); 83 | }); 84 | 85 | test('edge case fuzzing', () => { 86 | const tree = new BinaryRadixTree(); 87 | const shadowMap = new Map(); 88 | 89 | // Test edge cases (skip empty key as it seems to have a bug) 90 | const edgeCases = [ 91 | new Uint8Array([1]), // Single byte 92 | new Uint8Array([1, 1]), // Repeated bytes 93 | new Uint8Array([1, 2, 3]), // Sequential bytes 94 | new Uint8Array([5, 4, 3, 2, 1]), // Reverse sequence 95 | ]; 96 | 97 | for (let i = 0; i < edgeCases.length; i++) { 98 | const key = edgeCases[i]; 99 | const keyString = keyToString(key); 100 | const value = i * 100; 101 | 102 | tree.set(key, value); 103 | shadowMap.set(keyString, value); 104 | 105 | expect(tree.size).toBe(shadowMap.size); 106 | expect(tree.get(key)).toBe(value); 107 | } 108 | 109 | // Test overwriting existing keys 110 | for (let i = 0; i < edgeCases.length; i++) { 111 | const key = edgeCases[i]; 112 | const keyString = keyToString(key); 113 | const newValue = i * 200; // Different value 114 | 115 | tree.set(key, newValue); 116 | shadowMap.set(keyString, newValue); 117 | 118 | expect(tree.size).toBe(shadowMap.size); // Size should not change 119 | expect(tree.get(key)).toBe(newValue); 120 | } 121 | 122 | // Final validation 123 | for (const [keyStr, value] of shadowMap) { 124 | const originalKey = stringToKey(keyStr); 125 | expect(tree.get(originalKey)).toBe(value); 126 | } 127 | }); 128 | 129 | test('prefix relationship fuzzing', () => { 130 | const tree = new BinaryRadixTree(); 131 | const shadowMap = new Map(); 132 | 133 | // Test keys that are prefixes of each other 134 | const prefixKeys = [ 135 | new Uint8Array([1]), 136 | new Uint8Array([1, 2]), 137 | new Uint8Array([1, 2, 3]), 138 | new Uint8Array([1, 2, 3, 4]), 139 | new Uint8Array([2]), 140 | new Uint8Array([2, 1]), 141 | new Uint8Array([2, 1, 3]), 142 | ]; 143 | 144 | for (let i = 0; i < prefixKeys.length; i++) { 145 | const key = prefixKeys[i]; 146 | const keyString = keyToString(key); 147 | const value = `prefix-${i}`; 148 | 149 | tree.set(key, value); 150 | shadowMap.set(keyString, value); 151 | 152 | expect(tree.size).toBe(shadowMap.size); 153 | expect(tree.get(key)).toBe(value); 154 | } 155 | 156 | // Validate all keys still exist correctly 157 | for (const [keyStr, value] of shadowMap) { 158 | const originalKey = stringToKey(keyStr); 159 | expect(tree.get(originalKey)).toBe(value); 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/__bench__/bench.map.insert.nums.libs.ts: -------------------------------------------------------------------------------- 1 | // npx ts-node src/__bench__/bench.map.insert.nums.libs.ts 2 | 3 | import {runBenchmark, IBenchmark} from './runBenchmark'; 4 | import {Tree} from '../Tree'; 5 | import {AvlBstNumNumMap} from '../avl/AvlBstNumNumMap'; 6 | import {RadixTree} from '../radix/RadixTree'; 7 | import * as payloads from './payloads'; 8 | import BTree from 'sorted-btree'; 9 | import {OrderedMap} from 'js-sdsl'; 10 | import {AvlMap} from '../avl/AvlMap'; 11 | import {AvlMap as AvlMapOld} from '../avl/AvlMapOld'; 12 | import {RbMap} from '../red-black/RbMap'; 13 | import {LlrbTree} from '../llrb-tree/LlrbTree'; 14 | import {SortedMap} from '../SortedMap/SortedMap'; 15 | 16 | const benchmark: IBenchmark = { 17 | name: 'Numeric inserts into maps', 18 | warmup: 100, 19 | payloads: payloads.numbers, 20 | runners: [ 21 | // { 22 | // name: 'json-joy AvlBstNumNumMap', 23 | // setup: () => { 24 | // return (num: unknown) => { 25 | // const map = new AvlBstNumNumMap(); 26 | // const numbers = num as number[]; 27 | // const length = numbers.length; 28 | // for (let i = 0; i < length; i++) { 29 | // const key = numbers[i]; 30 | // map.set(key, key); 31 | // } 32 | // }; 33 | // }, 34 | // }, 35 | // { 36 | // name: 'json-joy AvlMapOld', 37 | // setup: () => { 38 | // return (num: unknown) => { 39 | // const map = new AvlMapOld(); 40 | // const numbers = num as number[]; 41 | // const length = numbers.length; 42 | // for (let i = 0; i < length; i++) { 43 | // const key = numbers[i]; 44 | // map.set(key, key); 45 | // } 46 | // }; 47 | // }, 48 | // }, 49 | { 50 | name: 'json-joy AvlMap', 51 | setup: () => { 52 | return (num: unknown) => { 53 | const map = new AvlMap(); 54 | const numbers = num as number[]; 55 | const length = numbers.length; 56 | for (let i = 0; i < length; i++) { 57 | const key = numbers[i]; 58 | map.set(key, key); 59 | } 60 | }; 61 | }, 62 | }, 63 | { 64 | name: 'json-joy RbMap', 65 | setup: () => { 66 | return (num: unknown) => { 67 | const map = new RbMap(); 68 | const numbers = num as number[]; 69 | const length = numbers.length; 70 | for (let i = 0; i < length; i++) { 71 | const key = numbers[i]; 72 | map.set(key, key); 73 | } 74 | }; 75 | }, 76 | }, 77 | // { 78 | // name: 'json-joy LlrbTree', 79 | // setup: () => { 80 | // return (num: unknown) => { 81 | // const map = new LlrbTree(); 82 | // const numbers = num as number[]; 83 | // const length = numbers.length; 84 | // for (let i = 0; i < length; i++) { 85 | // const key = numbers[i]; 86 | // map.set(key, key); 87 | // } 88 | // }; 89 | // }, 90 | // }, 91 | { 92 | name: 'json-joy SortedMap', 93 | setup: () => { 94 | return (num: unknown) => { 95 | const map = new SortedMap(); 96 | const numbers = num as number[]; 97 | const length = numbers.length; 98 | for (let i = 0; i < length; i++) { 99 | const key = numbers[i]; 100 | map.setElement(key, key); 101 | } 102 | }; 103 | }, 104 | }, 105 | // { 106 | // name: 'json-joy Tree', 107 | // setup: () => { 108 | // return (num: unknown) => { 109 | // const map = new Tree(); 110 | // const numbers = num as number[]; 111 | // const length = numbers.length; 112 | // for (let i = 0; i < length; i++) { 113 | // const key = numbers[i]; 114 | // map.set(key, key); 115 | // } 116 | // }; 117 | // }, 118 | // }, 119 | // { 120 | // name: 'json-joy RadixTree', 121 | // setup: () => { 122 | // return (num: unknown) => { 123 | // const map = new RadixTree(); 124 | // const numbers = num as number[]; 125 | // const length = numbers.length; 126 | // for (let i = 0; i < length; i++) { 127 | // const key = numbers[i]; 128 | // map.set('' + key, key); 129 | // } 130 | // }; 131 | // }, 132 | // }, 133 | // { 134 | // name: 'sorted-btree BTree', 135 | // setup: () => { 136 | // return (num: unknown) => { 137 | // const map = new BTree(); 138 | // const numbers = num as number[]; 139 | // const length = numbers.length; 140 | // for (let i = 0; i < length; i++) { 141 | // const key = numbers[i]; 142 | // map.set(key, key); 143 | // } 144 | // }; 145 | // }, 146 | // }, 147 | { 148 | name: 'js-sdsl OrderedMap', 149 | setup: () => { 150 | return (num: unknown) => { 151 | const map = new OrderedMap(); 152 | const numbers = num as number[]; 153 | const length = numbers.length; 154 | for (let i = 0; i < length; i++) { 155 | const key = numbers[i]; 156 | map.setElement(key, key); 157 | } 158 | }; 159 | }, 160 | }, 161 | ], 162 | }; 163 | 164 | runBenchmark(benchmark); 165 | -------------------------------------------------------------------------------- /src/__bench__/bench.map.delete.nums.libs.ts: -------------------------------------------------------------------------------- 1 | // npx ts-node src/__bench__/bench.map.delete.nums.libs.ts 2 | 3 | import {runBenchmark, IBenchmark} from './runBenchmark'; 4 | import {Tree} from '../Tree'; 5 | import {RadixTree} from '../radix/RadixTree'; 6 | import * as payloads from './payloads'; 7 | import BTree from 'sorted-btree'; 8 | import {OrderedMap} from 'js-sdsl'; 9 | import {AvlMap} from '../avl/AvlMap'; 10 | import {AvlMap as AvlMapOld} from '../avl/AvlMapOld'; 11 | import {RbMap} from '../red-black/RbMap'; 12 | import {SortedMap} from '../SortedMap/SortedMap'; 13 | 14 | const benchmark: IBenchmark = { 15 | name: 'Numeric deletes from maps', 16 | warmup: 100, 17 | payloads: payloads.numbers, 18 | runners: [ 19 | // { 20 | // name: 'json-joy AvlMapOld', 21 | // setup: () => { 22 | // return (num: unknown) => { 23 | // const map = new AvlMapOld(); 24 | // const numbers = num as number[]; 25 | // const length = numbers.length; 26 | // for (let i = 0; i < length; i++) { 27 | // const key = numbers[i]; 28 | // map.set(key, key); 29 | // } 30 | // for (let i = 0; i < length; i++) { 31 | // const key = numbers[i]; 32 | // map.del(key); 33 | // } 34 | // }; 35 | // }, 36 | // }, 37 | { 38 | name: 'json-joy AvlMap', 39 | setup: () => { 40 | return (num: unknown) => { 41 | const map = new AvlMap(); 42 | const numbers = num as number[]; 43 | const length = numbers.length; 44 | for (let i = 0; i < length; i++) { 45 | const key = numbers[i]; 46 | map.set(key, key); 47 | } 48 | for (let i = 0; i < length; i++) { 49 | const key = numbers[i]; 50 | map.del(key); 51 | } 52 | }; 53 | }, 54 | }, 55 | { 56 | name: 'json-joy RbMap', 57 | setup: () => { 58 | return (num: unknown) => { 59 | const map = new RbMap(); 60 | const numbers = num as number[]; 61 | const length = numbers.length; 62 | for (let i = 0; i < length; i++) { 63 | const key = numbers[i]; 64 | map.set(key, key); 65 | } 66 | for (let i = 0; i < length; i++) { 67 | const key = numbers[i]; 68 | map.del(key); 69 | } 70 | }; 71 | }, 72 | }, 73 | { 74 | name: 'json-joy SortedMap', 75 | setup: () => { 76 | return (num: unknown) => { 77 | const map = new SortedMap(); 78 | const numbers = num as number[]; 79 | const length = numbers.length; 80 | for (let i = 0; i < length; i++) { 81 | const key = numbers[i]; 82 | map.setElement(key, key); 83 | } 84 | for (let i = 0; i < length; i++) { 85 | const key = numbers[i]; 86 | map.eraseElementByKey(key); 87 | } 88 | }; 89 | }, 90 | }, 91 | // { 92 | // name: 'json-joy Tree', 93 | // setup: () => { 94 | // return (num: unknown) => { 95 | // const map = new Tree(); 96 | // const numbers = num as number[]; 97 | // const length = numbers.length; 98 | // for (let i = 0; i < length; i++) { 99 | // const key = numbers[i]; 100 | // map.set(key, key); 101 | // } 102 | // for (let i = 0; i < length; i++) { 103 | // const key = numbers[i]; 104 | // map.delete(key); 105 | // } 106 | // }; 107 | // }, 108 | // }, 109 | // { 110 | // name: 'json-joy RadixTree', 111 | // setup: () => { 112 | // return (num: unknown) => { 113 | // const map = new RadixTree(); 114 | // const numbers = num as number[]; 115 | // const length = numbers.length; 116 | // for (let i = 0; i < length; i++) { 117 | // const key = numbers[i]; 118 | // map.set('' + key, key); 119 | // } 120 | // for (let i = 0; i < length; i++) { 121 | // const key = numbers[i]; 122 | // map.delete('' + key); 123 | // } 124 | // }; 125 | // }, 126 | // }, 127 | // { 128 | // name: 'sorted-btree BTree', 129 | // setup: () => { 130 | // return (num: unknown) => { 131 | // const map = new BTree(); 132 | // const numbers = num as number[]; 133 | // const length = numbers.length; 134 | // for (let i = 0; i < length; i++) { 135 | // const key = numbers[i]; 136 | // map.set(key, key); 137 | // } 138 | // for (let i = 0; i < length; i++) { 139 | // const key = numbers[i]; 140 | // map.delete(key); 141 | // } 142 | // }; 143 | // }, 144 | // }, 145 | { 146 | name: 'js-sdsl OrderedMap', 147 | setup: () => { 148 | return (num: unknown) => { 149 | const map = new OrderedMap(); 150 | const numbers = num as number[]; 151 | const length = numbers.length; 152 | for (let i = 0; i < length; i++) { 153 | const key = numbers[i]; 154 | map.setElement(key, key); 155 | } 156 | for (let i = 0; i < length; i++) { 157 | const key = numbers[i]; 158 | map.eraseElementByKey(key); 159 | } 160 | }; 161 | }, 162 | }, 163 | ], 164 | }; 165 | 166 | runBenchmark(benchmark); 167 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import {first} from './first'; 2 | import {next} from './next'; 3 | import type {Comparator, HeadlessNode, ITreeNode} from '../types'; 4 | 5 | export {first, next}; 6 | 7 | /** 8 | * Finds the rightmost (maximum) node in a binary tree. 9 | * 10 | * @template N - The node type extending HeadlessNode 11 | * @param root - The root node to search from 12 | * @returns The rightmost node, or undefined if tree is empty 13 | */ 14 | export const last = (root: N | undefined): N | undefined => { 15 | let curr = root; 16 | while (curr) 17 | if (curr.r) curr = curr.r as N; 18 | else return curr; 19 | return curr; 20 | }; 21 | 22 | /** 23 | * Finds the in-order predecessor of a given node in a binary tree. 24 | * 25 | * @template N - The node type extending HeadlessNode 26 | * @param curr - The node to find the predecessor of 27 | * @returns The predecessor node, or undefined if none exists 28 | */ 29 | export const prev = (curr: N): N | undefined => { 30 | if (curr.l) { 31 | curr = curr.l as N; 32 | while (curr.r) curr = curr.r as N; 33 | return curr; 34 | } 35 | let p = curr.p as N; 36 | while (p && p.l === curr) { 37 | curr = p; 38 | p = p.p as N; 39 | } 40 | return p; 41 | }; 42 | 43 | const size_ = (root: N): number => { 44 | const l = root.l; 45 | const r = root.r; 46 | return 1 + (l ? size_(l) : 0) + (r ? size_(r) : 0); 47 | }; 48 | 49 | /** 50 | * Calculates the total number of nodes in a binary tree. 51 | * 52 | * @template N - The node type extending HeadlessNode 53 | * @param root - The root node of the tree 54 | * @returns The total number of nodes in the tree 55 | */ 56 | export const size = (root: N | undefined): number => { 57 | return root ? size_(root) : 0; 58 | }; 59 | 60 | /** 61 | * Searches for a node with the given key in a binary search tree. 62 | * 63 | * @template K - The key type 64 | * @template V - The value type 65 | * @param root - The root node to search from 66 | * @param key - The key to search for 67 | * @param comparator - Function to compare keys 68 | * @returns The node containing the key, or undefined if not found 69 | */ 70 | export const find = ( 71 | root: ITreeNode | undefined, 72 | key: K, 73 | comparator: Comparator, 74 | ): ITreeNode | undefined => { 75 | let curr: ITreeNode | undefined = root; 76 | while (curr) { 77 | const cmp = comparator(key, curr.k); 78 | if (cmp === 0) return curr; 79 | curr = cmp < 0 ? curr.l : curr.r; 80 | } 81 | return curr; 82 | }; 83 | 84 | /** 85 | * Finds the node with the given key, or the largest key smaller than the given key. 86 | * This is useful for range queries and finding the "floor" of a key. 87 | * 88 | * @template K - The key type 89 | * @template V - The value type 90 | * @param root - The root node to search from 91 | * @param key - The key to search for 92 | * @param comparator - Function to compare keys 93 | * @returns The node with the key or the next lower node, or undefined if none exists 94 | */ 95 | export const findOrNextLower = ( 96 | root: ITreeNode | undefined, 97 | key: K, 98 | comparator: Comparator, 99 | ): ITreeNode | undefined => { 100 | let curr: ITreeNode | undefined = root; 101 | let result: ITreeNode | undefined = undefined; 102 | while (curr) { 103 | const cmp = comparator(curr.k, key); 104 | if (cmp === 0) return curr; 105 | if (cmp > 0) curr = curr.l; 106 | else { 107 | const next = curr.r; 108 | result = curr; 109 | if (!next) return result; 110 | curr = next; 111 | } 112 | } 113 | return result; 114 | }; 115 | 116 | export const insertRight = (node: HeadlessNode, p: HeadlessNode): void => { 117 | const r = (node.r = p.r); 118 | p.r = node; 119 | node.p = p; 120 | if (r) r.p = node; 121 | }; 122 | 123 | export const insertLeft = (node: HeadlessNode, p: HeadlessNode): void => { 124 | const l = (node.l = p.l); 125 | p.l = node; 126 | node.p = p; 127 | if (l) l.p = node; 128 | }; 129 | 130 | export const insert = ( 131 | root: ITreeNode | undefined, 132 | node: ITreeNode, 133 | comparator: Comparator, 134 | ): ITreeNode => { 135 | if (!root) return node; 136 | const key = node.k; 137 | let curr: ITreeNode | undefined = root; 138 | while (curr) { 139 | const cmp = comparator(key, curr.k); 140 | const next: ITreeNode | undefined = cmp < 0 ? curr.l : curr.r; 141 | /** @todo perf: see if it is possible to take this condition outside of the loop. */ 142 | if (!next) { 143 | if (cmp < 0) insertLeft(node, curr); 144 | else insertRight(node, curr); 145 | break; 146 | } else curr = next; 147 | } 148 | return root; 149 | }; 150 | 151 | export const remove = (root: N | undefined, node: N): N | undefined => { 152 | const p = node.p; 153 | const l = node.l; 154 | const r = node.r; 155 | node.p = node.l = node.r = undefined; 156 | if (!l && !r) { 157 | if (!p) return undefined; 158 | else if (p.l === node) p.l = undefined; 159 | else p.r = undefined; 160 | return root; 161 | } else if (l && r) { 162 | let mostRightChildFromLeft = l; 163 | while (mostRightChildFromLeft.r) mostRightChildFromLeft = mostRightChildFromLeft.r; 164 | mostRightChildFromLeft.r = r; 165 | r.p = mostRightChildFromLeft; 166 | if (!p) { 167 | l.p = undefined; 168 | return l as N; 169 | } 170 | if (p.l === node) p.l = l; 171 | else p.r = l; 172 | l.p = p; 173 | return root; 174 | } 175 | const child = (l || r)!; 176 | child.p = p; 177 | if (!p) return child as N; 178 | else if (p.l === node) p.l = child; 179 | else p.r = child; 180 | return root; 181 | }; 182 | -------------------------------------------------------------------------------- /src/avl/AvlSet.ts: -------------------------------------------------------------------------------- 1 | import {insert, insertLeft, remove, insertRight, print} from './util'; 2 | import {printTree} from '../print/printTree'; 3 | import {findOrNextLower, first, next} from '../util'; 4 | import type {Printable} from '../print/types'; 5 | import type {Comparator, HeadlessNode} from '../types'; 6 | import type {AvlNodeReference, IAvlTreeNode} from './types'; 7 | 8 | /** 9 | * AVL tree node for set implementation (stores only values, no separate keys). 10 | * 11 | * @template V - The type of values stored in the set 12 | */ 13 | export class AvlSetNode implements IAvlTreeNode { 14 | /** Parent node reference */ 15 | public p: AvlSetNode | undefined = undefined; 16 | /** Left child node reference */ 17 | public l: AvlSetNode | undefined = undefined; 18 | /** Right child node reference */ 19 | public r: AvlSetNode | undefined = undefined; 20 | /** Balance factor: height(right) - height(left) */ 21 | public bf: number = 0; 22 | /** Value is undefined for set nodes (key serves as both key and value) */ 23 | public v: undefined = undefined; 24 | 25 | /** 26 | * Creates a new AVL set node. 27 | * 28 | * @param k - The value to store (serves as both key and value) 29 | */ 30 | constructor(public readonly k: V) {} 31 | } 32 | 33 | const defaultComparator = (a: unknown, b: unknown) => (a === b ? 0 : (a as any) < (b as any) ? -1 : 1); 34 | 35 | /** 36 | * High-performance AVL tree-based sorted set implementation. 37 | * 38 | * This set maintains elements in sorted order and ensures no duplicates. 39 | * All operations (add, remove, has) are O(log n) due to the self-balancing 40 | * nature of AVL trees. 41 | * 42 | * @example 43 | * ```typescript 44 | * const set = new AvlSet(); 45 | * set.add(3); 46 | * set.add(1); 47 | * set.add(2); 48 | * 49 | * console.log(set.has(2)); // true 50 | * console.log(set.size); // 3 51 | * 52 | * // Iterate in sorted order 53 | * for (const node of set) { 54 | * console.log(node.k); // 1, 2, 3 55 | * } 56 | * ``` 57 | * 58 | * @template V - The type of values stored in the set 59 | */ 60 | export class AvlSet implements Printable { 61 | /** Root node of the AVL tree */ 62 | public root: AvlSetNode | undefined = undefined; 63 | /** Comparator function for ordering values */ 64 | public readonly comparator: Comparator; 65 | 66 | /** 67 | * Creates a new AVL set instance. 68 | * 69 | * @param comparator - Function to compare values. Defaults to natural ordering. 70 | */ 71 | constructor(comparator?: Comparator) { 72 | this.comparator = comparator || defaultComparator; 73 | } 74 | 75 | private insert(value: V): AvlNodeReference> { 76 | const item = new AvlSetNode(value); 77 | this.root = insert(this.root, item, this.comparator); 78 | return item; 79 | } 80 | 81 | /** 82 | * Adds a value to the set if it doesn't already exist. 83 | * 84 | * @param value - The value to add to the set 85 | * @returns Reference to the node containing the value (existing or newly created) 86 | */ 87 | public add(value: V): AvlNodeReference> { 88 | const root = this.root; 89 | if (!root) return this.insert(value); 90 | const comparator = this.comparator; 91 | let next: AvlSetNode | undefined = root, 92 | curr: AvlSetNode | undefined = next; 93 | let cmp: number = 0; 94 | do { 95 | curr = next; 96 | cmp = comparator(value, curr.k); 97 | if (cmp === 0) return curr; 98 | } while ((next = cmp < 0 ? (curr.l as AvlSetNode) : (curr.r as AvlSetNode))); 99 | const node = new AvlSetNode(value); 100 | this.root = 101 | cmp < 0 ? (insertLeft(root, node, curr) as AvlSetNode) : (insertRight(root, node, curr) as AvlSetNode); 102 | return node; 103 | } 104 | 105 | private find(k: V): AvlNodeReference> | undefined { 106 | const comparator = this.comparator; 107 | let curr: AvlSetNode | undefined = this.root; 108 | while (curr) { 109 | const cmp = comparator(k, curr.k); 110 | if (cmp === 0) return curr; 111 | curr = cmp < 0 ? (curr.l as AvlSetNode) : (curr.r as AvlSetNode); 112 | } 113 | return undefined; 114 | } 115 | 116 | public del(k: V): void { 117 | const node = this.find(k); 118 | if (!node) return; 119 | this.root = remove(this.root, node as AvlSetNode); 120 | } 121 | 122 | public clear(): void { 123 | this.root = undefined; 124 | } 125 | 126 | public has(k: V): boolean { 127 | return !!this.find(k); 128 | } 129 | 130 | public size(): number { 131 | const root = this.root; 132 | if (!root) return 0; 133 | let curr = first(root); 134 | let size = 1; 135 | while ((curr = next(curr as HeadlessNode) as AvlSetNode | undefined)) size++; 136 | return size; 137 | } 138 | 139 | public isEmpty(): boolean { 140 | return !this.root; 141 | } 142 | 143 | public getOrNextLower(k: V): AvlSetNode | undefined { 144 | return (findOrNextLower(this.root, k, this.comparator) as AvlSetNode) || undefined; 145 | } 146 | 147 | public forEach(fn: (node: AvlSetNode) => void): void { 148 | let curr = this.first(); 149 | if (!curr) return; 150 | do fn(curr!); 151 | while ((curr = next(curr as HeadlessNode) as AvlSetNode | undefined)); 152 | } 153 | 154 | public first(): AvlSetNode | undefined { 155 | const root = this.root; 156 | return root ? first(root) : undefined; 157 | } 158 | 159 | public readonly next = next; 160 | 161 | public iterator0(): () => undefined | AvlSetNode { 162 | let curr = this.first(); 163 | return () => { 164 | if (!curr) return undefined; 165 | const value = curr; 166 | curr = next(curr as HeadlessNode) as AvlSetNode | undefined; 167 | return value; 168 | }; 169 | } 170 | 171 | public iterator(): Iterator> { 172 | const iterator = this.iterator0(); 173 | return { 174 | next: () => { 175 | const value = iterator(); 176 | const res = >>{value, done: !value}; 177 | return res; 178 | }, 179 | }; 180 | } 181 | 182 | public entries(): IterableIterator> { 183 | return {[Symbol.iterator]: () => this.iterator()}; 184 | } 185 | 186 | public toString(tab: string): string { 187 | return this.constructor.name + printTree(tab, [(tab) => print(this.root, tab)]); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/data-types/map.ts: -------------------------------------------------------------------------------- 1 | import {findOrNextLower, first, next, prev} from '../util'; 2 | import {printTree} from '../print/printTree'; 3 | import type {Comparator, HeadlessNode, ITreeNode, SonicMap, SonicNodePublicReference} from '../types'; 4 | 5 | const defaultComparator = (a: unknown, b: unknown) => (a === b ? 0 : (a as any) < (b as any) ? -1 : 1); 6 | 7 | export const createMap = ( 8 | Node: new (k: K, v: V) => ITreeNode, 9 | insert: >(root: N | undefined, node: N, comparator: Comparator) => N, 10 | insertLeft: >(root: N, node: N, parent: N) => N, 11 | insertRight: >(root: N, node: N, parent: N) => N, 12 | remove: >(root: N | undefined, n: N) => N | undefined, 13 | print: (node: undefined | HeadlessNode | ITreeNode, tab?: string) => string, 14 | ) => { 15 | class SonicMapImpl implements SonicMap> { 16 | /** 17 | * Minimum node. The node with the smallest key in the tree. 18 | */ 19 | public min: ITreeNode | undefined = undefined; 20 | /** 21 | * Root node. Typically approximates the middle of the tree. 22 | */ 23 | public root: ITreeNode | undefined = undefined; 24 | /** 25 | * Maximum node. The node with the largest key in the tree. 26 | */ 27 | public max: ITreeNode | undefined = undefined; 28 | /** 29 | * Comparator function. Used to relatively compare keys. 30 | */ 31 | public readonly comparator: Comparator; 32 | 33 | constructor(comparator?: Comparator) { 34 | this.comparator = comparator || defaultComparator; 35 | } 36 | 37 | public set(k: K, v: V): SonicNodePublicReference> { 38 | const root = this.root; 39 | if (root === undefined) { 40 | this._size = 1; 41 | const node = new Node(k, v); 42 | return (this.root = this.min = this.max = insert(void 0, node, this.comparator) as ITreeNode); 43 | } 44 | const comparator = this.comparator; 45 | let cmp: number; 46 | const max = this.max!; 47 | cmp = comparator(k, max.k); 48 | if (cmp === 0) return (max.v = v), max; 49 | else if (cmp > 0) { 50 | const node = (this.max = new Node(k, v)); 51 | this.root = insertRight(root, node, max) as ITreeNode; 52 | this._size++; 53 | return node; 54 | } 55 | const min = this.min!; 56 | cmp = comparator(k, min.k); 57 | if (cmp === 0) return (min.v = v), min; 58 | else if (cmp < 0) { 59 | const node = (this.min = new Node(k, v)); 60 | this.root = insertLeft(root, node, min) as ITreeNode; 61 | this._size++; 62 | return node; 63 | } 64 | let curr: ITreeNode | undefined = root; 65 | do { 66 | cmp = comparator(k, curr.k); 67 | if (cmp === 0) return (curr.v = v), curr; 68 | else if (cmp > 0) { 69 | const r = curr.r; 70 | if (r === undefined) { 71 | const node = new Node(k, v); 72 | this.root = insertRight(root, node, curr) as ITreeNode; 73 | this._size++; 74 | return node; 75 | } 76 | curr = r as ITreeNode; 77 | } else if (cmp < 0) { 78 | const l = curr.l; 79 | if (l === undefined) { 80 | const node = new Node(k, v); 81 | this.root = insertLeft(root, node, curr) as ITreeNode; 82 | this._size++; 83 | return node; 84 | } 85 | curr = l as ITreeNode; 86 | } 87 | } while (true); 88 | } 89 | 90 | public find(k: K): SonicNodePublicReference> | undefined { 91 | const comparator = this.comparator; 92 | let curr: ITreeNode | undefined = this.root; 93 | while (curr) { 94 | const cmp = +comparator(k, curr.k); 95 | if (cmp === 0) return curr; 96 | curr = cmp < 0 ? (curr.l as ITreeNode) : (curr.r as ITreeNode); 97 | } 98 | return; 99 | } 100 | 101 | public get(k: K): V | undefined { 102 | return this.find(k)?.v; 103 | } 104 | 105 | public del(k: K): boolean { 106 | const node = this.find(k) as ITreeNode; 107 | if (!node) return false; 108 | if (node === this.max) this.max = prev(node); 109 | if (node === this.min) this.min = next(node); 110 | this.root = remove(this.root, node as ITreeNode); 111 | this._size--; 112 | return true; 113 | } 114 | 115 | public clear(): void { 116 | this._size = 0; 117 | this.root = this.min = this.max = undefined; 118 | } 119 | 120 | public has(k: K): boolean { 121 | return !!this.find(k); 122 | } 123 | 124 | public _size: number = 0; 125 | 126 | public size(): number { 127 | return this._size; 128 | } 129 | 130 | public isEmpty(): boolean { 131 | return !this.min; 132 | } 133 | 134 | public getOrNextLower(k: K): ITreeNode | undefined { 135 | return (findOrNextLower(this.root, k, this.comparator) as ITreeNode) || undefined; 136 | } 137 | 138 | public forEach(fn: (node: ITreeNode) => void): void { 139 | let curr = this.first(); 140 | if (!curr) return; 141 | do fn(curr!); 142 | while ((curr = next(curr as HeadlessNode) as ITreeNode | undefined)); 143 | } 144 | 145 | public first(): ITreeNode | undefined { 146 | return this.min; 147 | } 148 | 149 | public last(): ITreeNode | undefined { 150 | return this.max; 151 | } 152 | 153 | public readonly next = next; 154 | 155 | public iterator0(): () => undefined | ITreeNode { 156 | let curr = this.first(); 157 | return () => { 158 | if (!curr) return; 159 | const value = curr; 160 | curr = next(curr as HeadlessNode) as ITreeNode | undefined; 161 | return value; 162 | }; 163 | } 164 | 165 | public iterator(): Iterator> { 166 | const iterator = this.iterator0(); 167 | return { 168 | next: () => { 169 | const value = iterator(); 170 | const res = >>{value, done: !value}; 171 | return res; 172 | }, 173 | }; 174 | } 175 | 176 | public entries(): IterableIterator> { 177 | return {[Symbol.iterator]: () => this.iterator()}; 178 | } 179 | 180 | public toString(tab: string): string { 181 | return this.constructor.name + printTree(tab, [(tab) => print(this.root, tab)]); 182 | } 183 | } 184 | 185 | return SonicMapImpl; 186 | }; 187 | -------------------------------------------------------------------------------- /src/avl/util.ts: -------------------------------------------------------------------------------- 1 | import {printBinary} from '../print/printBinary'; 2 | import type {Comparator} from '../types'; 3 | import type {AvlHeadlessNode, IAvlTreeNode} from './types'; 4 | 5 | const stringify = JSON.stringify; 6 | 7 | const rebalanceAfterInsert = ( 8 | root: AvlHeadlessNode, 9 | node: AvlHeadlessNode, 10 | child: AvlHeadlessNode, 11 | ): AvlHeadlessNode => { 12 | const p = node.p; 13 | if (!p) return root; 14 | const isLeft = node === p.l; 15 | let bf = p.bf | 0; 16 | if (isLeft) p.bf = ++bf; 17 | else p.bf = --bf; 18 | switch (bf) { 19 | case 0: 20 | return root; 21 | case 1: 22 | case -1: 23 | return rebalanceAfterInsert(root, p, node); 24 | default: { 25 | const isChildLeft = child === node.l; 26 | if (isLeft) { 27 | if (isChildLeft) return llRotate(p, node), node.p ? root : node; 28 | else return lrRotate(p, node, child), child.p ? root : child; 29 | } else { 30 | if (isChildLeft) return rlRotate(p, node, child), child.p ? root : child; 31 | else return rrRotate(p, node), node.p ? root : node; 32 | } 33 | } 34 | } 35 | }; 36 | 37 | const llRotate = (n: AvlHeadlessNode, nl: AvlHeadlessNode): void => { 38 | const p = n.p; 39 | const nlr = nl.r; 40 | nl.p = p; 41 | nl.r = n; 42 | n.p = nl; 43 | n.l = nlr; 44 | nlr && (nlr.p = n); 45 | p && (p.l === n ? (p.l = nl) : (p.r = nl)); 46 | let nbf = n.bf; 47 | let nlbf = nl.bf; 48 | nbf += -1 - (nlbf > 0 ? nlbf : 0); 49 | nlbf += -1 + (nbf < 0 ? nbf : 0); 50 | n.bf = nbf; 51 | nl.bf = nlbf; 52 | }; 53 | 54 | const rrRotate = (n: AvlHeadlessNode, nr: AvlHeadlessNode): void => { 55 | const p = n.p; 56 | const nrl = nr.l; 57 | nr.p = p; 58 | nr.l = n; 59 | n.p = nr; 60 | n.r = nrl; 61 | nrl && (nrl.p = n); 62 | p && (p.l === n ? (p.l = nr) : (p.r = nr)); 63 | let nbf = n.bf; 64 | let nrbf = nr.bf; 65 | nbf += 1 - (nrbf < 0 ? nrbf : 0); 66 | nrbf += 1 + (nbf > 0 ? nbf : 0); 67 | n.bf = nbf; 68 | nr.bf = nrbf; 69 | }; 70 | 71 | const lrRotate = (n: AvlHeadlessNode, nl: AvlHeadlessNode, nlr: AvlHeadlessNode): void => { 72 | rrRotate(nl, nlr); 73 | llRotate(n, nlr); 74 | }; 75 | 76 | const rlRotate = (n: AvlHeadlessNode, nr: AvlHeadlessNode, nrl: AvlHeadlessNode): void => { 77 | llRotate(nr, nrl); 78 | rrRotate(n, nrl); 79 | }; 80 | 81 | export const insertRight = (root: AvlHeadlessNode, n: AvlHeadlessNode, p: AvlHeadlessNode): AvlHeadlessNode => { 82 | p.r = n; 83 | n.p = p; 84 | p.bf--; 85 | return p.l ? root : rebalanceAfterInsert(root, p, n); 86 | }; 87 | 88 | export const insertLeft = (root: AvlHeadlessNode, n: AvlHeadlessNode, p: AvlHeadlessNode): AvlHeadlessNode => { 89 | p.l = n; 90 | n.p = p; 91 | p.bf++; 92 | return p.r ? root : rebalanceAfterInsert(root, p, n); 93 | }; 94 | 95 | export const insert = >(root: N | undefined, node: N, comparator: Comparator): N => { 96 | if (!root) return node; 97 | const key = node.k; 98 | let curr: N | undefined = root; 99 | let next: N | undefined = undefined; 100 | let cmp: number = 0; 101 | while ((next = ((cmp = comparator(key, curr.k)) < 0 ? curr.l : curr.r))) curr = next; 102 | return (cmp < 0 ? insertLeft(root, node, curr) : insertRight(root, node, curr)) as N; 103 | }; 104 | 105 | export const remove = >(root: N | undefined, n: N): N | undefined => { 106 | if (!root) return n; 107 | const p = n.p; 108 | const l = n.l; 109 | const r = n.r; 110 | n.p = n.l = n.r = undefined; 111 | if (l && r) { 112 | const lr = l.r; 113 | if (!lr) { 114 | p && (p.l === n ? (p.l = l) : (p.r = l)); 115 | l.p = p; 116 | l.r = r; 117 | r.p = l; 118 | const nbf = n.bf; 119 | if (p) return (l.bf = nbf), lRebalance(root, l, 1) as N; 120 | const lbf = nbf - 1; 121 | l.bf = lbf; 122 | if (lbf >= -1) return l as N; 123 | const rl = r.l; 124 | return r.bf > 0 ? ((rlRotate(l, r, rl!), rl) as N) : (rrRotate(l, r), r as N); 125 | } else { 126 | let v = l; // in-order predecessor 127 | let tmp: typeof v | undefined = v; 128 | while ((tmp = v.r)) v = tmp; 129 | const vl = v.l; 130 | const vp = v.p; 131 | const vc = vl; 132 | p && (p.l === n ? (p.l = v) : (p.r = v)); 133 | v.p = p; 134 | v.r = r; 135 | v.bf = n.bf; 136 | l !== v && ((v.l = l), (l.p = v)); 137 | r.p = v; 138 | vp && (vp.l === v ? (vp.l = vc) : (vp.r = vc)); 139 | vc && (vc.p = vp); 140 | return rRebalance(p ? root : v, vp!, 1) as N; 141 | } 142 | } 143 | const c = (l || r) as N | undefined; 144 | c && (c.p = p); 145 | if (!p) return c; 146 | return p.l === n ? ((p.l = c), lRebalance(root, p, 1) as N) : ((p.r = c), rRebalance(root, p, 1) as N); 147 | }; 148 | 149 | const lRebalance = (root: AvlHeadlessNode | undefined, n: AvlHeadlessNode, d: 1 | 0): AvlHeadlessNode | undefined => { 150 | let bf = n.bf | 0; 151 | bf -= d; 152 | n.bf = bf; 153 | let nextD: 1 | 0 = d; 154 | if (bf === -1) return root; 155 | if (bf < -1) { 156 | const u = n.r!; 157 | if (u.bf <= 0) { 158 | if (u.l && u.bf === 0) nextD = 0; 159 | rrRotate(n, u); 160 | n = u; 161 | } else { 162 | const ul = u.l!; 163 | rlRotate(n, u, ul); 164 | n = ul; 165 | } 166 | } 167 | const p = n.p; 168 | if (!p) return n; 169 | return p.l === n ? lRebalance(root, p, nextD) : rRebalance(root, p, nextD); 170 | }; 171 | 172 | const rRebalance = (root: AvlHeadlessNode | undefined, n: AvlHeadlessNode, d: 1 | 0): AvlHeadlessNode | undefined => { 173 | let bf = n.bf | 0; 174 | bf += d; 175 | n.bf = bf; 176 | let nextD: 1 | 0 = d; 177 | if (bf === 1) return root; 178 | if (bf > 1) { 179 | const u = n.l!; 180 | if (u.bf >= 0) { 181 | if (u.r && u.bf === 0) nextD = 0; 182 | llRotate(n, u); 183 | n = u; 184 | } else { 185 | const ur = u.r!; 186 | lrRotate(n, u, ur); 187 | n = ur; 188 | } 189 | } 190 | const p = n.p; 191 | if (!p) return n; 192 | return p.l === n ? lRebalance(root, p, nextD) : rRebalance(root, p, nextD); 193 | }; 194 | 195 | export const print = (node: undefined | AvlHeadlessNode | IAvlTreeNode, tab: string = ''): string => { 196 | if (!node) return '∅'; 197 | const {bf, l, r, k, v} = node as IAvlTreeNode; 198 | const vFormatted = 199 | v && typeof v === 'object' && v.constructor === Object 200 | ? stringify(v) 201 | : v && typeof v === 'object' 202 | ? (v as any).toString(tab) 203 | : stringify(v); 204 | const content = k !== undefined ? ` { ${stringify(k)} = ${vFormatted} }` : ''; 205 | const bfFormatted = bf ? ` [${bf}]` : ''; 206 | return ( 207 | node.constructor.name + 208 | `${bfFormatted}` + 209 | content + 210 | printBinary(tab, [l ? (tab) => print(l, tab) : null, r ? (tab) => print(r, tab) : null]) 211 | ); 212 | }; 213 | -------------------------------------------------------------------------------- /src/radix/__tests__/RadixTree.spec.ts: -------------------------------------------------------------------------------- 1 | import {RadixTree} from '../RadixTree'; 2 | import {TrieNode} from '../../trie/TrieNode'; 3 | 4 | describe('.set()', () => { 5 | test('starts empty', () => { 6 | const tree = new RadixTree(); 7 | expect(tree.toRecord()).toStrictEqual({}); 8 | }); 9 | 10 | test('can insert a single entry', () => { 11 | const tree = new RadixTree(); 12 | tree.set('foo', 'bar'); 13 | expect(tree.toRecord()).toStrictEqual({foo: 'bar'}); 14 | }); 15 | 16 | test('can rewrite a single key', () => { 17 | const tree = new RadixTree(); 18 | tree.set('foo', 'bar'); 19 | tree.set('foo', 'baz'); 20 | expect(tree.toRecord()).toStrictEqual({foo: 'baz'}); 21 | }); 22 | 23 | test('can insert two keys', () => { 24 | const tree = new RadixTree(); 25 | tree.set('foo', 'bar'); 26 | tree.set('baz', 'qux'); 27 | expect(tree.toRecord()).toStrictEqual({foo: 'bar', baz: 'qux'}); 28 | }); 29 | 30 | test('can set prefixes of the first key', () => { 31 | const tree = new RadixTree(); 32 | tree.set('foo', 1); 33 | tree.set('fo', 2); 34 | tree.set('f', 3); 35 | expect(tree.toRecord()).toStrictEqual({foo: 1, fo: 2, f: 3}); 36 | }); 37 | 38 | test('can insert an empty key', () => { 39 | const tree = new RadixTree(); 40 | tree.set('', 1); 41 | expect(tree.toRecord()).toStrictEqual({'': 1}); 42 | }); 43 | 44 | test('can insert keys that contain the previous ones', () => { 45 | const tree = new RadixTree(); 46 | tree.set('', 1); 47 | tree.set('f', 2); 48 | tree.set('fo', 3); 49 | tree.set('foo', 4); 50 | expect(tree.toRecord()).toStrictEqual({'': 1, f: 2, fo: 3, foo: 4}); 51 | }); 52 | 53 | test('can insert adjacent keys', () => { 54 | const tree = new RadixTree(); 55 | tree.set('a', 1); 56 | tree.set('b', 2); 57 | tree.set('c', 3); 58 | tree.set('b1', 4); 59 | tree.set('b3', 5); 60 | tree.set('b2', 6); 61 | expect(tree.toRecord()).toStrictEqual({ 62 | a: 1, 63 | b: 2, 64 | c: 3, 65 | b1: 4, 66 | b3: 5, 67 | b2: 6, 68 | }); 69 | }); 70 | }); 71 | 72 | describe('.get()', () => { 73 | test('return "undefined" from empty tree', () => { 74 | const tree = new RadixTree(); 75 | expect(tree.get('foo')).toBe(undefined); 76 | }); 77 | 78 | test('return "undefined" if key is not found', () => { 79 | const tree = new RadixTree(); 80 | tree.set('foo', 1); 81 | expect(tree.get('bar')).toBe(undefined); 82 | }); 83 | 84 | test('can retrieve a single set key', () => { 85 | const tree = new RadixTree(); 86 | tree.set('foo', 1); 87 | expect(tree.get('foo')).toBe(1); 88 | }); 89 | 90 | test('can retrieve a single set key', () => { 91 | const tree = new RadixTree(); 92 | tree.set('foo', 1); 93 | expect(tree.get('foo')).toBe(1); 94 | }); 95 | 96 | test('can retrieve from multiple set keys', () => { 97 | const tree = new RadixTree(); 98 | tree.set('fo', 1); 99 | tree.set('foo', 2); 100 | tree.set('f', 3); 101 | tree.set('bar', 4); 102 | tree.set('b', 5); 103 | tree.set('barr', 6); 104 | expect(tree.get('fo')).toBe(1); 105 | expect(tree.get('foo')).toBe(2); 106 | expect(tree.get('f')).toBe(3); 107 | expect(tree.get('bar')).toBe(4); 108 | expect(tree.get('b')).toBe(5); 109 | expect(tree.get('barr')).toBe(6); 110 | }); 111 | }); 112 | 113 | describe('.delete()', () => { 114 | test('can delete an existing key', () => { 115 | const tree = new RadixTree(); 116 | tree.set('foo', 'bar'); 117 | expect(tree.size).toBe(1); 118 | expect(tree.get('foo')).toBe('bar'); 119 | tree.delete('foo'); 120 | expect(tree.size).toBe(0); 121 | expect(tree.get('foo')).toBe(undefined); 122 | }); 123 | 124 | test('can delete deeply nested trees', () => { 125 | const tree = new RadixTree(); 126 | tree.set('bbb', 1); 127 | tree.set('bbbb', 2); 128 | tree.set('bb', 3); 129 | tree.set('bba', 4); 130 | tree.set('bbc', 5); 131 | tree.set('bbba', 6); 132 | tree.set('bbbc', 7); 133 | tree.set('aaa', 8); 134 | tree.set('abb', 9); 135 | tree.set('aab', 10); 136 | tree.set('ba', 11); 137 | tree.set('baa', 12); 138 | tree.set('bcc', 13); 139 | expect(tree.size).toBe(13); 140 | expect(tree.get('bbbb')).toBe(2); 141 | tree.delete('bbbb'); 142 | expect(tree.size).toBe(12); 143 | expect(tree.get('bbbb')).toBe(undefined); 144 | expect(tree.get('bb')).toBe(3); 145 | tree.delete('bb'); 146 | tree.delete('bb'); 147 | expect(tree.size).toBe(11); 148 | expect(tree.get('bb')).toBe(undefined); 149 | expect(tree.get('bba')).toBe(4); 150 | tree.delete('bba'); 151 | expect(tree.size).toBe(10); 152 | tree.delete('bbb'); 153 | expect(tree.size).toBe(9); 154 | tree.delete('bbba'); 155 | expect(tree.size).toBe(8); 156 | tree.delete('bbbc'); 157 | tree.delete('bbbc'); 158 | expect(tree.size).toBe(7); 159 | tree.delete('bbc'); 160 | tree.delete('bbc'); 161 | expect(tree.size).toBe(6); 162 | // console.log(tree + ''); 163 | }); 164 | }); 165 | 166 | describe('.size', () => { 167 | test('increments when new keys are inserted', () => { 168 | const tree = new RadixTree(); 169 | expect(tree.size).toBe(0); 170 | tree.set('foo', 1); 171 | expect(tree.size).toBe(1); 172 | tree.set('bar', 1); 173 | expect(tree.size).toBe(2); 174 | }); 175 | 176 | test('does not increment the size when value is overwritten', () => { 177 | const tree = new RadixTree(); 178 | tree.set('foo', 1); 179 | expect(tree.size).toBe(1); 180 | tree.set('foo', 1); 181 | expect(tree.size).toBe(1); 182 | }); 183 | }); 184 | 185 | describe('.forChildren()', () => { 186 | test('can iterate through root level nodes', () => { 187 | const tree = new RadixTree(); 188 | tree.set('foo', 1); 189 | tree.set('fo', 2); 190 | tree.set('bar', 3); 191 | const res: TrieNode[] = []; 192 | tree.forChildren((child) => res.push(child)); 193 | expect(res.length).toBe(2); 194 | expect(res[0].k).toBe('bar'); 195 | expect(res[1].k).toBe('fo'); 196 | }); 197 | }); 198 | 199 | describe('router', () => { 200 | test('can add HTTP routes', () => { 201 | const tree = new RadixTree(); 202 | tree.set('GET /users', 1); 203 | tree.set('GET /files', 2); 204 | tree.set('PUT /files', 3); 205 | expect(tree.get('GET /users')).toBe(1); 206 | expect(tree.get('GET /files')).toBe(2); 207 | expect(tree.get('PUT /files')).toBe(3); 208 | tree.set('POST /files', 4); 209 | expect(tree.size).toBe(4); 210 | expect(tree.get('GET /users')).toBe(1); 211 | expect(tree.get('GET /files')).toBe(2); 212 | expect(tree.get('PUT /files')).toBe(3); 213 | expect(tree.get('POST /files')).toBe(4); 214 | tree.set('POST /posts', 5); 215 | expect(tree.get('GET /users')).toBe(1); 216 | expect(tree.get('GET /files')).toBe(2); 217 | expect(tree.get('PUT /files')).toBe(3); 218 | expect(tree.get('POST /files')).toBe(4); 219 | expect(tree.get('POST /posts')).toBe(5); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /src/__tests__/util.spec.ts: -------------------------------------------------------------------------------- 1 | import {Tree} from '../Tree'; 2 | import {TreeNode} from '../TreeNode'; 3 | import {ITreeNode} from '../types'; 4 | import {find, findOrNextLower, insert, insertLeft, insertRight, remove, size} from '../util'; 5 | import {splay} from '../splay/util'; 6 | 7 | type TNode = ITreeNode; 8 | const comparator = (a: number, b: number): number => a - b; 9 | 10 | describe('insert()', () => { 11 | test('can set root', () => { 12 | let root: TNode | undefined = undefined; 13 | const node = new TreeNode(1, 'a'); 14 | root = insert(root, node, comparator); 15 | expect(root).toBe(node); 16 | }); 17 | 18 | test('can insert after root', () => { 19 | let root: TNode | undefined = undefined; 20 | const node1 = new TreeNode(1, 'a'); 21 | const node2 = new TreeNode(2, 'b'); 22 | root = insert(root, node1, comparator); 23 | root = insert(root, node2, comparator); 24 | expect(root.r).toBe(node2); 25 | }); 26 | 27 | test('can insert before root', () => { 28 | let root: TNode | undefined = undefined; 29 | const node1 = new TreeNode(1, 'a'); 30 | const node2 = new TreeNode(2, 'b'); 31 | const node3 = new TreeNode(-1, 'b'); 32 | root = insert(root, node1, comparator); 33 | root = insert(root, node2, comparator); 34 | root = insert(root, node3, comparator); 35 | expect(root.l).toBe(node3); 36 | }); 37 | 38 | test('can insert twice before', () => { 39 | let root: TNode | undefined = undefined; 40 | const node1 = new TreeNode(1, 'a'); 41 | const node2 = new TreeNode(0, 'b'); 42 | const node3 = new TreeNode(-1, 'c'); 43 | root = insert(root, node1, comparator); 44 | root = insert(root, node2, comparator); 45 | root = insert(root, node3, comparator); 46 | expect(root).toBe(node1); 47 | expect(root.l).toBe(node2); 48 | expect(root.l!.l).toBe(node3); 49 | }); 50 | 51 | test('can insert twice after', () => { 52 | let root: TNode | undefined = undefined; 53 | const node1 = new TreeNode(1, 'a'); 54 | const node2 = new TreeNode(2, 'b'); 55 | const node3 = new TreeNode(3, 'c'); 56 | root = insert(root, node1, comparator); 57 | root = insert(root, node2, comparator); 58 | root = insert(root, node3, comparator); 59 | expect(root).toBe(node1); 60 | expect(root.r).toBe(node2); 61 | expect(root.r!.r).toBe(node3); 62 | }); 63 | 64 | test('can insert zig-zag', () => { 65 | let root: TNode | undefined = undefined; 66 | const node1 = new TreeNode(1, 'a'); 67 | const node2 = new TreeNode(3, 'b'); 68 | const node3 = new TreeNode(2, 'c'); 69 | root = insert(root, node1, comparator); 70 | root = insert(root, node2, comparator); 71 | root = insert(root, node3, comparator); 72 | expect(root).toBe(node1); 73 | expect(root.r).toBe(node2); 74 | expect(root.r!.l).toBe(node3); 75 | }); 76 | 77 | test('can insert zig-zag-zig', () => { 78 | let root: TNode | undefined = undefined; 79 | const node1 = new TreeNode(1, 'a'); 80 | const node2 = new TreeNode(5, 'b'); 81 | const node3 = new TreeNode(2, 'c'); 82 | const node4 = new TreeNode(3, 'c'); 83 | root = insert(root, node1, comparator); 84 | root = insert(root, node2, comparator); 85 | root = insert(root, node3, comparator); 86 | root = insert(root, node4, comparator); 87 | expect(root).toBe(node1); 88 | expect(root.r).toBe(node2); 89 | expect(root.r!.l).toBe(node3); 90 | expect(root.r!.l!.r).toBe(node4); 91 | }); 92 | }); 93 | 94 | describe('find()', () => { 95 | test('can find element', () => { 96 | let root: TNode | undefined = undefined; 97 | const node1 = new TreeNode(1, 'a'); 98 | const node2 = new TreeNode(5, 'b'); 99 | const node3 = new TreeNode(2, 'c'); 100 | const node4 = new TreeNode(3, 'c'); 101 | root = insert(root, node1, comparator); 102 | root = insert(root, node2, comparator); 103 | root = insert(root, node3, comparator); 104 | root = insert(root, node4, comparator); 105 | expect(find(root, 3, comparator)?.v).toBe('c'); 106 | expect(find(root, 5, comparator)?.v).toBe('b'); 107 | expect(find(root, 1, comparator)?.v).toBe('a'); 108 | }); 109 | }); 110 | 111 | describe('findOrNextLower()', () => { 112 | test('can find element', () => { 113 | let root: TNode | undefined = undefined; 114 | const node1 = new TreeNode(1, 'a'); 115 | const node2 = new TreeNode(5, 'b'); 116 | const node3 = new TreeNode(2, 'c'); 117 | const node4 = new TreeNode(3, 'c'); 118 | root = insert(root, node1, comparator); 119 | root = insert(root, node2, comparator); 120 | root = insert(root, node3, comparator); 121 | root = insert(root, node4, comparator); 122 | expect(findOrNextLower(root, 3, comparator)?.v).toBe('c'); 123 | expect(findOrNextLower(root, 5, comparator)?.v).toBe('b'); 124 | expect(findOrNextLower(root, 1, comparator)?.v).toBe('a'); 125 | }); 126 | 127 | test('returns next lower element', () => { 128 | let root: TNode | undefined = undefined; 129 | const node1 = new TreeNode(1, 'a'); 130 | const node2 = new TreeNode(5, 'b'); 131 | const node3 = new TreeNode(2, 'c'); 132 | const node4 = new TreeNode(3, 'c'); 133 | root = insert(root, node1, comparator); 134 | root = insert(root, node2, comparator); 135 | root = insert(root, node3, comparator); 136 | root = insert(root, node4, comparator); 137 | expect(findOrNextLower(root, 4, comparator)?.v).toBe('c'); 138 | expect(findOrNextLower(root, 6, comparator)?.v).toBe('b'); 139 | expect(findOrNextLower(root, 7, comparator)?.v).toBe('b'); 140 | expect(findOrNextLower(root, 1, comparator)?.v).toBe('a'); 141 | expect(findOrNextLower(root, 2, comparator)?.v).toBe('c'); 142 | expect(findOrNextLower(root, 0, comparator)?.v).toBe(undefined); 143 | expect(findOrNextLower(root, -123, comparator)?.v).toBe(undefined); 144 | }); 145 | }); 146 | 147 | describe('remove()', () => { 148 | test('can find element', () => { 149 | let root: TNode | undefined = undefined; 150 | const node1 = new TreeNode(1, 'a'); 151 | const node2 = new TreeNode(5, 'b'); 152 | const node3 = new TreeNode(2, 'c'); 153 | const node4 = new TreeNode(3, 'c'); 154 | root = insert(root, node1, comparator); 155 | root = insert(root, node2, comparator); 156 | root = insert(root, node3, comparator); 157 | root = insert(root, node4, comparator); 158 | root = remove(root, node3); 159 | expect(find(root, 2, comparator)?.v).toBe(undefined); 160 | expect(find(root, 1, comparator)?.v).toBe('a'); 161 | expect(find(root, 5, comparator)?.v).toBe('b'); 162 | expect(find(root, 3, comparator)?.v).toBe('c'); 163 | }); 164 | }); 165 | 166 | describe('splay()', () => { 167 | test('keeps all 4 elements in the tree', () => { 168 | const tree = new Tree(); 169 | const node0 = new TreeNode(0, '0'); 170 | tree.root = node0; 171 | const node3 = new TreeNode(3, '3'); 172 | insertRight(node3, node0); 173 | const node2 = new TreeNode(2, '2'); 174 | insertLeft(node2, node3); 175 | const node4 = new TreeNode(4, '4'); 176 | insertRight(node4, node3); 177 | // console.log(tree + ''); 178 | tree.root = splay(tree.root, node4, 5); 179 | // console.log(tree + ''); 180 | expect(size(tree.root)).toBe(4); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /src/radix/binaryRadix.ts: -------------------------------------------------------------------------------- 1 | import {BinaryTrieNode} from './BinaryTrieNode'; 2 | import {Slice} from './Slice'; 3 | import {findOrNextLower, first, insertLeft, insertRight, next, remove as plainRemove} from '../util'; 4 | 5 | /** 6 | * @param root Root of the Binary Radix Tree 7 | * @param path Associative key to insert (Uint8Array) 8 | * @param value Value to insert 9 | * @returns Number of new nodes created 10 | */ 11 | export const insert = (root: BinaryTrieNode, path: Uint8Array, value: unknown): number => { 12 | let curr: BinaryTrieNode | undefined = root; 13 | let k = Slice.fromUint8Array(path); 14 | 15 | main: while (curr) { 16 | let child: BinaryTrieNode | undefined = curr.children; 17 | if (!child) { 18 | curr.children = new BinaryTrieNode(k, value); 19 | return 1; 20 | } 21 | 22 | const firstByte = k.length > 0 ? k.at(0) : -1; 23 | let prevChild: BinaryTrieNode | undefined = undefined; 24 | let cmp: boolean = false; 25 | 26 | child: while (child) { 27 | prevChild = child; 28 | const childFirstByte = child.k.length > 0 ? child.k.at(0) : -1; 29 | 30 | if (childFirstByte === firstByte) { 31 | const commonPrefixLength = child.k.getCommonPrefixLength(k); 32 | const isChildKContained = commonPrefixLength === child.k.length; 33 | const isKContained = commonPrefixLength === k.length; 34 | const areKeysEqual = isChildKContained && isKContained; 35 | 36 | if (areKeysEqual) { 37 | child.v = value; 38 | return 0; 39 | } 40 | 41 | if (isChildKContained) { 42 | k = k.substring(commonPrefixLength); 43 | curr = child; 44 | continue main; 45 | } 46 | 47 | if (isKContained) { 48 | const newChild = new BinaryTrieNode(child.k.substring(commonPrefixLength), child.v); 49 | newChild.children = child.children; 50 | child.k = k.substring(0, commonPrefixLength); 51 | child.v = value; 52 | child.children = newChild; 53 | return 1; 54 | } 55 | 56 | if (commonPrefixLength) { 57 | const newChild = new BinaryTrieNode(child.k.substring(commonPrefixLength), child.v); 58 | newChild.children = child.children; 59 | child.k = child.k.substring(0, commonPrefixLength); 60 | child.v = undefined; 61 | child.children = newChild; 62 | curr = child; 63 | k = k.substring(commonPrefixLength); 64 | continue main; 65 | } 66 | } 67 | 68 | cmp = childFirstByte > firstByte; 69 | if (cmp) child = child.l; 70 | else child = child.r; 71 | } 72 | 73 | if (prevChild) { 74 | const node = new BinaryTrieNode(k, value); 75 | if (cmp) insertLeft(node, prevChild); 76 | else insertRight(node, prevChild); 77 | return 1; 78 | } 79 | break; 80 | } 81 | return 0; 82 | }; 83 | 84 | /** Finds the node which matches `key`, if any. */ 85 | export const find = (node: BinaryTrieNode, key: Uint8Array): undefined | BinaryTrieNode => { 86 | if (key.length === 0) return node; 87 | 88 | const keySlice = Slice.fromUint8Array(key); 89 | let offset: number = 0; 90 | 91 | while (node) { 92 | const remainingKey = keySlice.substring(offset); 93 | if (remainingKey.length === 0) return node; 94 | 95 | const child = findOrNextLower(node.children, remainingKey, (a: Slice, b: Slice) => { 96 | const aByte = a.length > 0 ? a.at(0) : -1; 97 | const bByte = b.length > 0 ? b.at(0) : -1; 98 | return aByte > bByte ? 1 : -1; 99 | }) as BinaryTrieNode | undefined; 100 | 101 | if (!child) return undefined; 102 | 103 | const childKey = child.k; 104 | const childKeyLength = childKey.length; 105 | let commonPrefixLength = 0; 106 | const limit = Math.min(childKeyLength, remainingKey.length); 107 | 108 | for ( 109 | ; 110 | commonPrefixLength < limit && childKey.at(commonPrefixLength) === remainingKey.at(commonPrefixLength); 111 | commonPrefixLength++ 112 | ); 113 | 114 | if (!commonPrefixLength) return undefined; 115 | offset += commonPrefixLength; 116 | 117 | if (offset === key.length) return child; 118 | if (commonPrefixLength < childKeyLength) return undefined; 119 | 120 | node = child; 121 | } 122 | return undefined; 123 | }; 124 | 125 | /** Finds the node which matches `key`, and returns a list of all its parents. */ 126 | export const findWithParents = (node: BinaryTrieNode, key: Uint8Array): undefined | BinaryTrieNode[] => { 127 | if (key.length === 0) return undefined; 128 | 129 | const list: BinaryTrieNode[] = [node]; 130 | const keySlice = Slice.fromUint8Array(key); 131 | let offset: number = 0; 132 | 133 | while (node) { 134 | const remainingKey = keySlice.substring(offset); 135 | 136 | const child = findOrNextLower(node.children, remainingKey, (a: Slice, b: Slice) => { 137 | const aByte = a.length > 0 ? a.at(0) : -1; 138 | const bByte = b.length > 0 ? b.at(0) : -1; 139 | return aByte > bByte ? 1 : -1; 140 | }) as BinaryTrieNode | undefined; 141 | 142 | if (!child) return undefined; 143 | 144 | const childKey = child.k; 145 | const childKeyLength = childKey.length; 146 | let commonPrefixLength = 0; 147 | const limit = Math.min(childKeyLength, remainingKey.length); 148 | 149 | for ( 150 | ; 151 | commonPrefixLength < limit && childKey.at(commonPrefixLength) === remainingKey.at(commonPrefixLength); 152 | commonPrefixLength++ 153 | ); 154 | 155 | if (!commonPrefixLength) return undefined; 156 | offset += commonPrefixLength; 157 | 158 | if (commonPrefixLength < childKeyLength) return undefined; 159 | list.push(child); 160 | 161 | if (offset === key.length) return list; 162 | node = child; 163 | } 164 | return undefined; 165 | }; 166 | 167 | export const remove = (root: BinaryTrieNode, key: Uint8Array): boolean => { 168 | if (key.length === 0) { 169 | const deleted = root.v !== undefined; 170 | root.v = undefined; 171 | return deleted; 172 | } 173 | 174 | const list = findWithParents(root, key); 175 | if (!list) return false; 176 | 177 | const length = list.length; 178 | const lastIndex = length - 1; 179 | const last = list[lastIndex]; 180 | const deleted = last.v !== undefined; 181 | last.v = undefined; 182 | 183 | for (let i = lastIndex; i >= 1; i--) { 184 | const child = list[i]; 185 | const parent = list[i - 1]; 186 | if (child.v || child.children) break; 187 | parent.children = plainRemove(parent.children, child); 188 | } 189 | 190 | return deleted; 191 | }; 192 | 193 | export const toRecord = ( 194 | node: BinaryTrieNode | undefined, 195 | prefix: Uint8Array = new Uint8Array(), 196 | record: Record = {}, 197 | ): Record => { 198 | if (!node) return record; 199 | 200 | const currentPrefix = new Uint8Array([...prefix, ...node.k.toUint8Array()]); 201 | 202 | if (node.v !== undefined) { 203 | const key = Array.from(currentPrefix).join(','); 204 | record[key] = node.v; 205 | } 206 | 207 | let child = first(node.children); 208 | if (!child) return record; 209 | 210 | do toRecord(child, currentPrefix, record); 211 | while ((child = next(child!))); 212 | 213 | return record; 214 | }; 215 | 216 | export const print = (node: BinaryTrieNode, tab: string = ''): string => { 217 | return node.toString(tab); 218 | }; 219 | -------------------------------------------------------------------------------- /src/radix/radix.ts: -------------------------------------------------------------------------------- 1 | import {printTree} from '../print/printTree'; 2 | import {TrieNode} from '../trie/TrieNode'; 3 | import {findOrNextLower, first, insertLeft, insertRight, next, remove as plainRemove} from '../util'; 4 | 5 | const stringify = JSON.stringify; 6 | 7 | const getCommonPrefixLength = (a: string, b: string): number => { 8 | const len = Math.min(a.length, b.length); 9 | let i = 0; 10 | for (; i < len && a[i] === b[i]; i++); 11 | return i; 12 | }; 13 | 14 | /** 15 | * @param root Root of the Radix Tree 16 | * @param path Associative key to insert 17 | * @param value Value to insert 18 | * @returns Number of new nodes created 19 | */ 20 | export const insert = (root: TrieNode, path: string, value: unknown): number => { 21 | let curr: TrieNode | undefined = root; 22 | let k = path; 23 | main: while (curr) { 24 | let child: TrieNode | undefined = curr.children; 25 | if (!child) { 26 | curr.children = new TrieNode(k, value); 27 | return 1; 28 | } 29 | const char = k[0]; 30 | let prevChild: TrieNode | undefined = undefined; 31 | let cmp: boolean = false; 32 | child: while (child) { 33 | prevChild = child; 34 | const childChar = child.k[0]; 35 | if (childChar === char) { 36 | const commonPrefixLength = getCommonPrefixLength(child.k, k); 37 | const isChildKContained = commonPrefixLength === child.k.length; 38 | const isKContained = commonPrefixLength === k.length; 39 | const areKeysEqual = isChildKContained && isKContained; 40 | if (areKeysEqual) { 41 | child.v = value; 42 | return 0; 43 | } 44 | if (isChildKContained) { 45 | k = k.substring(commonPrefixLength); 46 | curr = child; 47 | continue main; 48 | } 49 | if (isKContained) { 50 | const newChild = new TrieNode(child.k.substring(commonPrefixLength), child.v); 51 | newChild.children = child.children; 52 | child.k = k.substring(0, commonPrefixLength); 53 | child.v = value; 54 | child.children = newChild; 55 | return 1; 56 | } 57 | if (commonPrefixLength) { 58 | const newChild = new TrieNode(child.k.substring(commonPrefixLength), child.v); 59 | newChild.children = child.children; 60 | child.k = child.k.substring(0, commonPrefixLength); 61 | child.v = undefined; 62 | child.children = newChild; 63 | curr = child; 64 | k = k.substring(commonPrefixLength); 65 | continue main; 66 | } 67 | } 68 | cmp = childChar > char; 69 | if (cmp) child = child.l; 70 | else child = child.r; 71 | } 72 | if (prevChild) { 73 | const node = new TrieNode(k, value); 74 | if (cmp) insertLeft(node, prevChild); 75 | else insertRight(node, prevChild); 76 | return 1; 77 | } 78 | break; 79 | } 80 | return 0; 81 | }; 82 | 83 | /** Finds the node which matches `key`, if any. */ 84 | export const find = (node: TrieNode, key: string): undefined | TrieNode => { 85 | if (!key) return node; 86 | const len: number = key.length; 87 | let offset: number = 0; 88 | while (node) { 89 | /** @todo perf: inline this function call. */ 90 | const child = findOrNextLower(node.children, key[offset], (cmp1: string, cmp2: string) => 91 | cmp1[0] > cmp2[0] ? 1 : -1, 92 | ) as TrieNode | undefined; 93 | if (!child) return undefined; 94 | const childKey = child.k; 95 | const childKeyLength = childKey.length; 96 | let commonPrefixLength = 0; 97 | const limit = Math.min(childKeyLength, len - offset); 98 | for ( 99 | ; 100 | commonPrefixLength < limit && childKey[commonPrefixLength] === key[offset + commonPrefixLength]; 101 | commonPrefixLength++ 102 | ); 103 | if (!commonPrefixLength) return undefined; 104 | offset += commonPrefixLength; 105 | if (offset === len) return child; 106 | if (commonPrefixLength < childKeyLength) return undefined; 107 | node = child; 108 | } 109 | return undefined; 110 | }; 111 | 112 | /** Finds the node which matches `key`, and returns a list of all its parents. */ 113 | export const findWithParents = (node: TrieNode, key: string): undefined | TrieNode[] => { 114 | if (!key) return undefined; 115 | const list: TrieNode[] = [node]; 116 | const len: number = key.length; 117 | let offset: number = 0; 118 | while (node) { 119 | const child = findOrNextLower(node.children, key[offset], (cmp1: string, cmp2: string) => 120 | cmp1[0] > cmp2[0] ? 1 : -1, 121 | ) as TrieNode | undefined; 122 | if (!child) return undefined; 123 | const childKey = child.k; 124 | const childKeyLength = childKey.length; 125 | let commonPrefixLength = 0; 126 | const limit = Math.min(childKeyLength, len - offset); 127 | for ( 128 | ; 129 | commonPrefixLength < limit && childKey[commonPrefixLength] === key[offset + commonPrefixLength]; 130 | commonPrefixLength++ 131 | ); 132 | if (!commonPrefixLength) return undefined; 133 | offset += commonPrefixLength; 134 | if (commonPrefixLength < childKeyLength) return undefined; 135 | list.push(child); 136 | if (offset === len) return list; 137 | node = child; 138 | } 139 | return undefined; 140 | }; 141 | 142 | export const remove = (root: TrieNode, key: string): boolean => { 143 | if (!key) { 144 | const deleted = root.v !== undefined; 145 | root.v = undefined; 146 | return deleted; 147 | } 148 | const list = findWithParents(root, key); 149 | if (!list) return false; 150 | const length = list.length; 151 | const lastIndex = length - 1; 152 | const last = list[lastIndex]; 153 | const deleted = last.v !== undefined; 154 | last.v = undefined; 155 | for (let i = lastIndex; i >= 1; i--) { 156 | const child = list[i]; 157 | const parent = list[i - 1]; 158 | if (child.v || child.children) break; 159 | parent.children = plainRemove(parent.children, child); 160 | } 161 | return deleted; 162 | }; 163 | 164 | export const toRecord = ( 165 | node: TrieNode | undefined, 166 | prefix: string = '', 167 | record: Record = {}, 168 | ): Record => { 169 | if (!node) return record; 170 | prefix += node.k; 171 | if (node.v !== undefined) record[prefix] = node.v; 172 | let child = first(node.children); 173 | if (!child) return record; 174 | do toRecord(child, prefix, record); 175 | while ((child = next(child!))); 176 | return record; 177 | }; 178 | 179 | export const print = (node: TrieNode, tab: string = ''): string => { 180 | const detailedPrint = node.v && typeof node.v === 'object' && node.v.constructor !== Object; 181 | const value = 182 | node.v && typeof node.v === 'object' 183 | ? Array.isArray(node.v) 184 | ? stringify(node.v) 185 | : node.v.constructor === Object 186 | ? stringify(node.v) 187 | : '' // `[${node.v.constructor.name}]` 188 | : node.v === undefined 189 | ? '' 190 | : stringify(node.v); 191 | const childrenNodes: TrieNode[] = []; 192 | node.forChildren((child) => childrenNodes.push(child)); 193 | return ( 194 | `${node.constructor.name} ${JSON.stringify(node.k)}${value ? ' = ' + value : ''}` + 195 | printTree(tab, [ 196 | !detailedPrint ? null : (tab) => (node.v as any).toString(tab), 197 | ...childrenNodes.map((child) => (tab: string) => print(child, tab)), 198 | ]) 199 | ); 200 | }; 201 | -------------------------------------------------------------------------------- /src/red-black/util.ts: -------------------------------------------------------------------------------- 1 | import {swap} from '../util/swap'; 2 | import {print} from '../util/print'; 3 | import type {Comparator} from '../types'; 4 | import type {IRbTreeNode, RbHeadlessNode} from './types'; 5 | 6 | export {print}; 7 | 8 | export const insert = >(root: N | undefined, n: N, comparator: Comparator): N => { 9 | if (!root) return (n.b = true), n; 10 | const key = n.k; 11 | let curr: N | undefined = root; 12 | let next: N | undefined = undefined; 13 | let cmp: number = 0; 14 | while ((next = ((cmp = comparator(key, curr.k)) < 0 ? curr.l : curr.r))) curr = next; 15 | return (cmp < 0 ? insertLeft(root, n, curr) : insertRight(root, n, curr)) as N; 16 | }; 17 | 18 | export const insertRight = (root: RbHeadlessNode, n: RbHeadlessNode, p: RbHeadlessNode): RbHeadlessNode => { 19 | const g = p.p; 20 | p.r = n; 21 | n.p = p; 22 | if (p.b || !g) return root; 23 | const top = rRebalance(n, p, g); 24 | return top.p ? root : top; 25 | }; 26 | 27 | export const insertLeft = (root: RbHeadlessNode, n: RbHeadlessNode, p: RbHeadlessNode): RbHeadlessNode => { 28 | const g = p.p; 29 | p.l = n; 30 | n.p = p; 31 | if (p.b || !g) return root; 32 | const top = lRebalance(n, p, g); 33 | return top.p ? root : top; 34 | }; 35 | 36 | const rRebalance = (n: RbHeadlessNode, p: RbHeadlessNode, g: RbHeadlessNode): RbHeadlessNode => { 37 | const gl = g.l; 38 | const zigzag = gl === p; 39 | const u = zigzag ? g.r : gl; 40 | const uncleIsBlack = !u || u.b; 41 | if (uncleIsBlack) { 42 | g.b = false; 43 | if (zigzag) { 44 | lrRotate(g, p, n); 45 | // rRotate(p, n); 46 | // lRotate(g, n); 47 | n.b = true; 48 | return n; 49 | } 50 | p.b = true; 51 | rRotate(g, p); 52 | return p; 53 | } 54 | return recolor(p, g, u); 55 | }; 56 | 57 | const lRebalance = (n: RbHeadlessNode, p: RbHeadlessNode, g: RbHeadlessNode): RbHeadlessNode => { 58 | const gr = g.r; 59 | const zigzag = gr === p; 60 | const u = zigzag ? g.l : gr; 61 | const uncleIsBlack = !u || u.b; 62 | if (uncleIsBlack) { 63 | g.b = false; 64 | if (zigzag) { 65 | rlRotate(g, p, n); 66 | // lRotate(p, n); 67 | // rRotate(g, n); 68 | n.b = true; 69 | return n; 70 | } 71 | p.b = true; 72 | lRotate(g, p); 73 | return p; 74 | } 75 | return recolor(p, g, u); 76 | }; 77 | 78 | const recolor = (p: RbHeadlessNode, g: RbHeadlessNode, u?: RbHeadlessNode): RbHeadlessNode => { 79 | p.b = true; 80 | if (u) u.b = true; 81 | const gg = g.p; 82 | if (gg) { 83 | g.b = false; 84 | if (gg.b) return g; 85 | } else { 86 | g.b = true; 87 | return g; 88 | } 89 | const ggg = gg.p; 90 | if (!ggg) return gg; 91 | return gg.l === g ? lRebalance(g, gg, ggg) : rRebalance(g, gg, ggg); 92 | }; 93 | 94 | const lRotate = (n: RbHeadlessNode, nl: RbHeadlessNode): void => { 95 | const p = n.p; 96 | const nlr = nl.r; 97 | ((nl.r = n).l = nlr) && (nlr.p = n); 98 | ((n.p = nl).p = p) && (p.l === n ? (p.l = nl) : (p.r = nl)); 99 | }; 100 | 101 | const rRotate = (n: RbHeadlessNode, nr: RbHeadlessNode): void => { 102 | const p = n.p; 103 | const nrl = nr.l; 104 | ((nr.l = n).r = nrl) && (nrl.p = n); 105 | ((n.p = nr).p = p) && (p.l === n ? (p.l = nr) : (p.r = nr)); 106 | }; 107 | 108 | const lrRotate = (g: RbHeadlessNode, p: RbHeadlessNode, n: RbHeadlessNode): void => { 109 | const gg = g.p; 110 | const nl = n.l; 111 | const nr = n.r; 112 | gg && (gg.l === g ? (gg.l = n) : (gg.r = n)); 113 | n.p = gg; 114 | n.l = p; 115 | n.r = g; 116 | p.p = g.p = n; 117 | (p.r = nl) && (nl.p = p); 118 | (g.l = nr) && (nr.p = g); 119 | }; 120 | 121 | const rlRotate = (g: RbHeadlessNode, p: RbHeadlessNode, n: RbHeadlessNode): void => { 122 | const gg = g.p; 123 | const nl = n.l; 124 | const nr = n.r; 125 | gg && (gg.l === g ? (gg.l = n) : (gg.r = n)); 126 | n.p = gg; 127 | n.l = g; 128 | n.r = p; 129 | g.p = p.p = n; 130 | (g.r = nl) && (nl.p = g); 131 | (p.l = nr) && (nr.p = p); 132 | }; 133 | 134 | export const remove = >(root: N, n: N): N | undefined => { 135 | const originalNode = n; 136 | const r = n.r as N | undefined; 137 | const l = n.l as N | undefined; 138 | let child: N | undefined; 139 | if (r) { 140 | let inOrderSuccessor = r as N; 141 | if (inOrderSuccessor) 142 | while (true) { 143 | const next = inOrderSuccessor.l as N | undefined; 144 | if (next) inOrderSuccessor = next; 145 | else break; 146 | } 147 | n = inOrderSuccessor; 148 | child = n.r as N | undefined; 149 | } else if (!n.p) { 150 | if (l) { 151 | l.b = true; 152 | l.p = undefined; 153 | } 154 | return l; 155 | } else { 156 | child = r || l; 157 | } 158 | if (n !== originalNode) { 159 | originalNode.k = n.k; 160 | originalNode.v = n.v; 161 | const b = n.b; 162 | n.b = originalNode.b; 163 | originalNode.b = b; 164 | root = swap(root, originalNode, n); 165 | n = originalNode; 166 | } 167 | if (child) { 168 | const p = n.p! as N; 169 | child.p = p; 170 | if (p.l === n) p.l = child; 171 | else p.r = child; 172 | if (!child.b) child.b = true; 173 | else root = correctDoubleBlack(root, child); 174 | } else { 175 | if (n.b) root = correctDoubleBlack(root, n); 176 | const p2 = n.p as N; 177 | if (p2) { 178 | if (n === p2.l) p2.l = undefined; 179 | else p2.r = undefined; 180 | } else { 181 | n.b = true; 182 | return n; 183 | } 184 | } 185 | return root; 186 | }; 187 | 188 | const correctDoubleBlack = >(root: N, n: N): N => { 189 | LOOP: while (true) { 190 | const p = n.p as N | undefined; 191 | if (!p) return n; 192 | const pl = p.l; 193 | const isLeftChild = n === pl; 194 | let s = (isLeftChild ? p.r : pl) as N; 195 | const sl = s.l; 196 | if (s && !s.b && (!sl || sl.b)) { 197 | const sr = s.r; 198 | if (!sr || sr.b) { 199 | if (isLeftChild) rRotate(p, s); 200 | else lRotate(p, s); 201 | p.b = false; 202 | s.b = true; 203 | if (!s.p) root = s; 204 | } 205 | } 206 | if (p.b && s.b && (!sl || sl.b)) { 207 | const sr = s.r; 208 | if (!sr || sr.b) { 209 | s.b = false; 210 | n = p; 211 | continue LOOP; 212 | } 213 | } 214 | if (!p.b) { 215 | const pl = p.l; 216 | s = (n === pl ? p.r : pl) as N; 217 | const sl = s.l; 218 | if (s.b && (!sl || sl.b)) { 219 | const sr = s.r; 220 | if (!sr || sr.b) { 221 | const sr = s.r; 222 | if (!sr || sr.b) { 223 | s.b = false; 224 | p.b = true; 225 | return root; 226 | } 227 | } 228 | } 229 | } 230 | if (s.b) { 231 | const sl = s.l; 232 | const sr = s.r; 233 | if (n === p.l && (!sr || sr.b) && sl && !sl.b) { 234 | sl.b = true; 235 | s.b = false; 236 | lRotate(s, sl); 237 | } else if (n === p.r && (!sl || sl.b) && sr && !sr.b) { 238 | sr.b = true; 239 | s.b = false; 240 | rRotate(s, sr); 241 | } 242 | if (!s.p) return s; 243 | const pl = p.l; 244 | s = (n === pl ? p.r : pl) as N; 245 | } 246 | s.b = p.b; 247 | p.b = true; 248 | if (n === p.l) { 249 | s.r!.b = true; 250 | rRotate(p, s); 251 | } else { 252 | s.l!.b = true; 253 | lRotate(p, s); 254 | } 255 | return s.p ? root : s; 256 | } 257 | }; 258 | -------------------------------------------------------------------------------- /src/red-black/__tests__/RbMap.traces.spec.ts: -------------------------------------------------------------------------------- 1 | import {TraceReplay} from '../../__tests__/TraceReplay'; 2 | import {Trace} from '../../__tests__/types'; 3 | import {RbMap} from '../RbMap'; 4 | import {assertRedBlackTree} from './utils'; 5 | 6 | const trace1: Trace = [ 7 | ['insert', 47, 47], 8 | ['insert', 20, 20], 9 | ['insert', 14, 14], 10 | ['insert', 88, 88], 11 | ['insert', 71, 71], 12 | ['insert', 100, 100], 13 | ['insert', 8, 8], 14 | ['insert', 53, 53], 15 | ['insert', 46, 46], 16 | ['insert', 52, 52], 17 | ['delete', 41], 18 | ['delete', 41], 19 | ['delete', 36], 20 | ['delete', 67], 21 | ['delete', 68], 22 | ['delete', 0], 23 | ['delete', 77], 24 | ['delete', 27], 25 | ['delete', 7], 26 | ['delete', 75], 27 | ['delete', 62], 28 | ['delete', 11], 29 | ['delete', 31], 30 | ['delete', 1], 31 | ['delete', 79], 32 | ['delete', 80], 33 | ['delete', 96], 34 | ['delete', 14], 35 | ]; 36 | 37 | const trace2: Trace = [ 38 | ['delete', 12], 39 | ['delete', 15], 40 | ['delete', 94], 41 | ['delete', 27], 42 | ['delete', 52], 43 | ['delete', 6], 44 | ['delete', 44], 45 | ['delete', 67], 46 | ['delete', 9], 47 | ['delete', 87], 48 | ['delete', 52], 49 | ['delete', 50], 50 | ['delete', 17], 51 | ['delete', 5], 52 | ['delete', 89], 53 | ['delete', 60], 54 | ['delete', 24], 55 | ['delete', 20], 56 | ['delete', 32], 57 | ['delete', 31], 58 | ['delete', 27], 59 | ['delete', 76], 60 | ['delete', 31], 61 | ['delete', 28], 62 | ['delete', 54], 63 | ['delete', 3], 64 | ['delete', 40], 65 | ['delete', 28], 66 | ['delete', 44], 67 | ['delete', 31], 68 | ['delete', 38], 69 | ['delete', 2], 70 | ['delete', 94], 71 | ['delete', 9], 72 | ['delete', 89], 73 | ['delete', 44], 74 | ['delete', 62], 75 | ['delete', 66], 76 | ['delete', 54], 77 | ['delete', 89], 78 | ['delete', 73], 79 | ['delete', 9], 80 | ['delete', 27], 81 | ['delete', 100], 82 | ['delete', 24], 83 | ['insert', 12, 12], 84 | ['insert', 4, 4], 85 | ['insert', 37, 37], 86 | ['insert', 20, 20], 87 | ['insert', 50, 50], 88 | ['delete', 34], 89 | ['delete', 91], 90 | ['delete', 75], 91 | ['delete', 58], 92 | ['delete', 24], 93 | ['delete', 66], 94 | ['delete', 46], 95 | ['delete', 74], 96 | ['delete', 73], 97 | ['clear'], 98 | ['insert', 27, 27], 99 | ['insert', 36, 36], 100 | ['insert', 61, 61], 101 | ['insert', 6, 6], 102 | ['insert', 10, 10], 103 | ['insert', 52, 52], 104 | ['insert', 94, 94], 105 | ['delete', 83], 106 | ['delete', 6], 107 | ['delete', 14], 108 | ['delete', 51], 109 | ['delete', 96], 110 | ['delete', 24], 111 | ['delete', 79], 112 | ['delete', 88], 113 | ['delete', 19], 114 | ['delete', 73], 115 | ['delete', 13], 116 | ['delete', 74], 117 | ['delete', 94], 118 | ['delete', 66], 119 | ['delete', 24], 120 | ['delete', 43], 121 | ['delete', 47], 122 | ['delete', 59], 123 | ['delete', 24], 124 | ['delete', 37], 125 | ['insert', 42, 42], 126 | ['insert', 20, 20], 127 | ['insert', 77, 77], 128 | ['insert', 26, 26], 129 | ['insert', 42, 42], 130 | ['delete', 32], 131 | ['delete', 10], 132 | ['delete', 37], 133 | ['delete', 16], 134 | ['delete', 32], 135 | ['delete', 84], 136 | ['delete', 53], 137 | ['delete', 99], 138 | ['insert', 7, 7], 139 | ['insert', 77, 77], 140 | ['insert', 9, 9], 141 | ['insert', 23, 23], 142 | ['insert', 46, 46], 143 | ['insert', 95, 95], 144 | ['insert', 33, 33], 145 | ['insert', 19, 19], 146 | ['insert', 26, 26], 147 | ['delete', 65], 148 | ['delete', 61], 149 | ['delete', 38], 150 | ['delete', 6], 151 | ['delete', 13], 152 | ['delete', 41], 153 | ['delete', 85], 154 | ['delete', 81], 155 | ['delete', 6], 156 | ['delete', 59], 157 | ['delete', 77], 158 | ['delete', 28], 159 | ['delete', 13], 160 | ['delete', 27], 161 | ['delete', 74], 162 | ['delete', 46], 163 | ['clear'], 164 | ['insert', 97, 97], 165 | ['insert', 10, 10], 166 | ['insert', 74, 74], 167 | ['insert', 87, 87], 168 | ['insert', 81, 81], 169 | ['insert', 48, 48], 170 | ['delete', 98], 171 | ['delete', 48], 172 | ['delete', 96], 173 | ['delete', 97], 174 | ['delete', 69], 175 | ['delete', 43], 176 | ['delete', 94], 177 | ['delete', 56], 178 | ['delete', 85], 179 | ['delete', 26], 180 | ['delete', 31], 181 | ['delete', 60], 182 | ['delete', 48], 183 | ['delete', 49], 184 | ['delete', 83], 185 | ['delete', 49], 186 | ['delete', 39], 187 | ['delete', 8], 188 | ['delete', 89], 189 | ['delete', 74], 190 | ['delete', 15], 191 | ['delete', 88], 192 | ['delete', 38], 193 | ['delete', 94], 194 | ['delete', 25], 195 | ['delete', 60], 196 | ['delete', 3], 197 | ['delete', 73], 198 | ['delete', 76], 199 | ['delete', 30], 200 | ['delete', 12], 201 | ['delete', 88], 202 | ['delete', 81], 203 | ['delete', 30], 204 | ['delete', 55], 205 | ['delete', 100], 206 | ['delete', 97], 207 | ['delete', 64], 208 | ['delete', 87], 209 | ]; 210 | 211 | const trace3: Trace = [ 212 | ['insert', 13, 13], 213 | ['insert', 17, 17], 214 | ['insert', 63, 63], 215 | ['delete', 99], 216 | ['delete', 66], 217 | ['delete', 4], 218 | ['delete', 33], 219 | ['delete', 33], 220 | ['delete', 92], 221 | ['delete', 17], 222 | ['delete', 23], 223 | ['delete', 96], 224 | ['delete', 27], 225 | ['delete', 56], 226 | ['delete', 63], 227 | ]; 228 | 229 | const trace4: Trace = [ 230 | ['insert', 35, 35], 231 | ['insert', 56, 56], 232 | ['insert', 78, 78], 233 | ['insert', 14, 14], 234 | ['insert', 1, 1], 235 | ['insert', 52, 52], 236 | ['insert', 88, 88], 237 | ['delete', 31], 238 | ['delete', 24], 239 | ['delete', 26], 240 | ['delete', 45], 241 | ['delete', 10], 242 | ['delete', 74], 243 | ['delete', 71], 244 | ['delete', 84], 245 | ['delete', 80], 246 | ['delete', 27], 247 | ['delete', 74], 248 | ['delete', 17], 249 | ['delete', 47], 250 | ['delete', 5], 251 | ['delete', 38], 252 | ['delete', 3], 253 | ['delete', 24], 254 | ['delete', 45], 255 | ['delete', 75], 256 | ['delete', 87], 257 | ['delete', 70], 258 | ['delete', 12], 259 | ['delete', 34], 260 | ['delete', 89], 261 | ['delete', 33], 262 | ['delete', 72], 263 | ['delete', 95], 264 | ['delete', 78], 265 | ['delete', 90], 266 | ['delete', 41], 267 | ['delete', 32], 268 | ['delete', 12], 269 | ['delete', 26], 270 | ['delete', 54], 271 | ['delete', 92], 272 | ['delete', 43], 273 | ['delete', 2], 274 | ['delete', 18], 275 | ['delete', 6], 276 | ['delete', 4], 277 | ['delete', 37], 278 | ['delete', 15], 279 | ['delete', 85], 280 | ['delete', 38], 281 | ['delete', 98], 282 | ['delete', 48], 283 | ['delete', 33], 284 | ['delete', 67], 285 | ['delete', 2], 286 | ['delete', 70], 287 | ['delete', 54], 288 | ['delete', 81], 289 | ['delete', 30], 290 | ['delete', 68], 291 | ['delete', 76], 292 | ['delete', 91], 293 | ['delete', 22], 294 | ['delete', 32], 295 | ['delete', 57], 296 | ['delete', 15], 297 | ['delete', 37], 298 | ['delete', 82], 299 | ['delete', 58], 300 | ['delete', 1], 301 | ['delete', 34], 302 | ['delete', 8], 303 | ['delete', 37], 304 | ['delete', 98], 305 | ['delete', 70], 306 | ['delete', 1], 307 | ['insert', 85, 85], 308 | ['insert', 99, 99], 309 | ]; 310 | 311 | const trace5: Trace = [ 312 | ['insert', 59, 59], 313 | ['insert', 86, 86], 314 | ['insert', 56, 56], 315 | ['insert', 43, 43], 316 | ['insert', 24, 24], 317 | ['delete', 59], 318 | ['insert', 90, 90], 319 | ]; 320 | 321 | test('trace 1', () => { 322 | const map = new RbMap(); 323 | const replay = new TraceReplay(trace1, (step) => { 324 | // console.log(step); 325 | // console.log(map + ''); 326 | assertRedBlackTree(map.root as any); 327 | }); 328 | replay.run(map); 329 | }); 330 | 331 | test('trace 2', () => { 332 | const map = new RbMap(); 333 | const replay = new TraceReplay(trace2, (step) => { 334 | assertRedBlackTree(map.root as any); 335 | }); 336 | replay.run(map); 337 | }); 338 | 339 | test('trace 3', () => { 340 | const map = new RbMap(); 341 | const replay = new TraceReplay(trace3, (step) => { 342 | assertRedBlackTree(map.root as any); 343 | }); 344 | replay.run(map); 345 | }); 346 | 347 | test('trace 4', () => { 348 | const map = new RbMap(); 349 | const replay = new TraceReplay(trace4, (step) => { 350 | assertRedBlackTree(map.root as any); 351 | }); 352 | replay.run(map); 353 | }); 354 | 355 | test('trace 5', () => { 356 | const map = new RbMap(); 357 | const replay = new TraceReplay(trace5, (step) => { 358 | // console.log(step); 359 | // console.log(map + ''); 360 | assertRedBlackTree(map.root as any); 361 | }); 362 | replay.run(map); 363 | }); 364 | --------------------------------------------------------------------------------