Skip to content

Commit 5ae11c2

Browse files
authored
feat(data-structures/unstable): add BinarySearchTree methods ceiling, floor, higher, lower (#6544)
1 parent fb12b30 commit 5ae11c2

File tree

4 files changed

+439
-1
lines changed

4 files changed

+439
-1
lines changed

data_structures/binary_search_tree_test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ Deno.test("BinarySearchTree handles README example", () => {
544544
]);
545545
});
546546

547-
Deno.test("BinarySearchTree.max() handles null ", () => {
547+
Deno.test("BinarySearchTree.max() handles null", () => {
548548
const tree = BinarySearchTree.from([1]);
549549
assert(!tree.isEmpty());
550550
tree.clear();

data_structures/deno.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"./unstable-bidirectional-map": "./unstable_bidirectional_map.ts",
77
"./binary-heap": "./binary_heap.ts",
88
"./binary-search-tree": "./binary_search_tree.ts",
9+
"./unstable-binary-search-tree": "./unstable_binary_search_tree.ts",
910
"./comparators": "./comparators.ts",
1011
"./red-black-tree": "./red_black_tree.ts"
1112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
import type { BinarySearchNode, Direction } from "./_binary_search_node.ts";
5+
import { BinarySearchTree as StableBinarySearchTree } from "./binary_search_tree.ts";
6+
import { internals } from "./_binary_search_tree_internals.ts";
7+
8+
const {
9+
getRoot,
10+
setRoot,
11+
setSize,
12+
getCompare,
13+
} = internals;
14+
15+
/**
16+
* An unbalanced binary search tree. The values are in ascending order by default,
17+
* using JavaScript's built-in comparison operators to sort the values.
18+
*
19+
* For performance, it's recommended that you use a self-balancing binary search
20+
* tree instead of this one unless you are extending this to create a
21+
* self-balancing tree. See {@link RedBlackTree} for an example of how BinarySearchTree
22+
* can be extended to create a self-balancing binary search tree.
23+
*
24+
* | Method | Average Case | Worst Case |
25+
* | ------------- | ------------ | ---------- |
26+
* | find(value) | O(log n) | O(n) |
27+
* | insert(value) | O(log n) | O(n) |
28+
* | remove(value) | O(log n) | O(n) |
29+
* | min() | O(log n) | O(n) |
30+
* | max() | O(log n) | O(n) |
31+
*
32+
* @example Usage
33+
* ```ts
34+
* import {
35+
* BinarySearchTree,
36+
* ascend,
37+
* descend,
38+
* } from "@std/data-structures";
39+
* import { assertEquals } from "@std/assert";
40+
*
41+
* const values = [3, 10, 13, 4, 6, 7, 1, 14];
42+
* const tree = new BinarySearchTree<number>();
43+
* values.forEach((value) => tree.insert(value));
44+
* assertEquals([...tree], [1, 3, 4, 6, 7, 10, 13, 14]);
45+
* assertEquals(tree.min(), 1);
46+
* assertEquals(tree.max(), 14);
47+
* assertEquals(tree.find(42), null);
48+
* assertEquals(tree.find(7), 7);
49+
* assertEquals(tree.remove(42), false);
50+
* assertEquals(tree.remove(7), true);
51+
* assertEquals([...tree], [1, 3, 4, 6, 10, 13, 14]);
52+
*
53+
* const invertedTree = new BinarySearchTree<number>(descend);
54+
* values.forEach((value) => invertedTree.insert(value));
55+
* assertEquals([...invertedTree], [14, 13, 10, 7, 6, 4, 3, 1]);
56+
* assertEquals(invertedTree.min(), 14);
57+
* assertEquals(invertedTree.max(), 1);
58+
* assertEquals(invertedTree.find(42), null);
59+
* assertEquals(invertedTree.find(7), 7);
60+
* assertEquals(invertedTree.remove(42), false);
61+
* assertEquals(invertedTree.remove(7), true);
62+
* assertEquals([...invertedTree], [14, 13, 10, 6, 4, 3, 1]);
63+
*
64+
* const words = new BinarySearchTree<string>((a, b) =>
65+
* ascend(a.length, b.length) || ascend(a, b)
66+
* );
67+
* ["truck", "car", "helicopter", "tank", "train", "suv", "semi", "van"]
68+
* .forEach((value) => words.insert(value));
69+
* assertEquals([...words], [
70+
* "car",
71+
* "suv",
72+
* "van",
73+
* "semi",
74+
* "tank",
75+
* "train",
76+
* "truck",
77+
* "helicopter",
78+
* ]);
79+
* assertEquals(words.min(), "car");
80+
* assertEquals(words.max(), "helicopter");
81+
* assertEquals(words.find("scooter"), null);
82+
* assertEquals(words.find("tank"), "tank");
83+
* assertEquals(words.remove("scooter"), false);
84+
* assertEquals(words.remove("tank"), true);
85+
* assertEquals([...words], [
86+
* "car",
87+
* "suv",
88+
* "van",
89+
* "semi",
90+
* "train",
91+
* "truck",
92+
* "helicopter",
93+
* ]);
94+
* ```
95+
*
96+
* @typeparam T The type of the values stored in the binary search tree.
97+
*/
98+
export class BinarySearchTree<T> extends StableBinarySearchTree<T> {
99+
/**
100+
* Construct an empty binary search tree.
101+
*
102+
* To create a binary search tree from an array like, an iterable object, or an
103+
* existing binary search tree, use the {@link BinarySearchTree.from} method.
104+
*
105+
* @param compare A custom comparison function to sort the values in the tree.
106+
* By default, the values are sorted in ascending order.
107+
*/
108+
constructor(compare?: (a: T, b: T) => number) {
109+
super(compare);
110+
}
111+
112+
/**
113+
* Creates a new binary search tree from an array like, an iterable object,
114+
* or an existing binary search tree.
115+
*
116+
* A custom comparison function can be provided to sort the values in a
117+
* specific order. By default, the values are sorted in ascending order,
118+
* unless a {@link BinarySearchTree} is passed, in which case the comparison
119+
* function is copied from the input tree.
120+
*
121+
* @example Creating a binary search tree from an array like
122+
* ```ts no-assert
123+
* import { BinarySearchTree } from "@std/data-structures";
124+
*
125+
* const tree = BinarySearchTree.from<number>([42, 43, 41]);
126+
* ```
127+
*
128+
* @example Creating a binary search tree from an iterable object
129+
* ```ts no-assert
130+
* import { BinarySearchTree } from "@std/data-structures";
131+
*
132+
* const tree = BinarySearchTree.from<number>((function*() {
133+
* yield 42;
134+
* yield 43;
135+
* yield 41;
136+
* })());
137+
* ```
138+
*
139+
* @example Creating a binary search tree from an existing binary search tree
140+
* ```ts no-assert
141+
* import { BinarySearchTree } from "@std/data-structures";
142+
*
143+
* const tree = BinarySearchTree.from<number>([42, 43, 41]);
144+
* const copy = BinarySearchTree.from(tree);
145+
* ```
146+
*
147+
* @example Creating a binary search tree from an array like with a custom comparison function
148+
* ```ts no-assert
149+
* import { BinarySearchTree, descend } from "@std/data-structures";
150+
*
151+
* const tree = BinarySearchTree.from<number>(
152+
* [42, 43, 41],
153+
* { compare: descend }
154+
* );
155+
* ```
156+
*
157+
* @typeparam T The type of the values stored in the binary search tree.
158+
* @param collection An array like, an iterable, or existing binary search tree.
159+
* @param options An optional options object to customize the comparison function.
160+
* @returns A new binary search tree created from the passed collection.
161+
*/
162+
static override from<T>(
163+
collection: ArrayLike<T> | Iterable<T> | StableBinarySearchTree<T>,
164+
options?: {
165+
compare?: (a: T, b: T) => number;
166+
},
167+
): BinarySearchTree<T>;
168+
/**
169+
* Create a new binary search tree from an array like, an iterable object, or
170+
* an existing binary search tree.
171+
*
172+
* A custom mapping function can be provided to transform the values before
173+
* inserting them into the tree.
174+
*
175+
* A custom comparison function can be provided to sort the values in a
176+
* specific order. A custom mapping function can be provided to transform the
177+
* values before inserting them into the tree. By default, the values are
178+
* sorted in ascending order, unless a {@link BinarySearchTree} is passed, in
179+
* which case the comparison function is copied from the input tree. The
180+
* comparison operator is used to sort the values in the tree after mapping
181+
* the values.
182+
*
183+
* @example Creating a binary search tree from an array like with a custom mapping function
184+
* ```ts no-assert
185+
* import { BinarySearchTree } from "@std/data-structures";
186+
*
187+
* const tree = BinarySearchTree.from<number, string>(
188+
* [42, 43, 41],
189+
* { map: (value) => value.toString() }
190+
* );
191+
* ```
192+
*
193+
* @typeparam T The type of the values in the passed collection.
194+
* @typeparam U The type of the values stored in the binary search tree.
195+
* @typeparam V The type of the `this` value when calling the mapping function. Defaults to `undefined`.
196+
* @param collection An array like, an iterable, or existing binary search tree.
197+
* @param options The options object to customize the mapping and comparison functions. The `thisArg` property can be used to set the `this` value when calling the mapping function.
198+
* @returns A new binary search tree containing the mapped values from the passed collection.
199+
*/
200+
static override from<T, U, V = undefined>(
201+
collection: ArrayLike<T> | Iterable<T> | StableBinarySearchTree<T>,
202+
options: {
203+
compare?: (a: U, b: U) => number;
204+
map: (value: T, index: number) => U;
205+
thisArg?: V;
206+
},
207+
): BinarySearchTree<U>;
208+
static override from<T, U, V>(
209+
collection: ArrayLike<T> | Iterable<T> | StableBinarySearchTree<T>,
210+
options?: {
211+
compare?: (a: U, b: U) => number;
212+
map?: (value: T, index: number) => U;
213+
thisArg?: V;
214+
},
215+
): BinarySearchTree<U> {
216+
const result = new BinarySearchTree<U>(options?.compare);
217+
const stableTree = super.from<T, U, V>(
218+
collection,
219+
// Cast to use types of `from<T, U, V = undefined>` instead of `from<T>`.
220+
// The latter happens by default, even though we call `super.from<T, U, V>`.
221+
options as { map: (value: T, index: number) => U },
222+
);
223+
setRoot(result, getRoot(stableTree));
224+
setSize(result, stableTree.size);
225+
return result;
226+
}
227+
228+
/**
229+
* Finds the node matching the given selection criteria.
230+
*
231+
* When searching for higher nodes, returns the lowest node that is higher than
232+
* the value. When searching for lower nodes, returns the highest node that is
233+
* lower than the value.
234+
*
235+
* By default, only accepts a node exactly matching the passed value and returns
236+
* it if found.
237+
*
238+
* @param value The value to search for
239+
* @param select Whether to accept nodes that are higher or lower than the value
240+
* @param returnIfFound Whether a node matching the value itself is accepted
241+
* @returns The node that matched, or null if none matched
242+
*/
243+
#findNode(
244+
value: T,
245+
select?: "higher" | "lower",
246+
returnIfFound: boolean = true,
247+
): BinarySearchNode<T> | null {
248+
const compare = getCompare(this);
249+
250+
let node: BinarySearchNode<T> | null = getRoot(this);
251+
let result: BinarySearchNode<T> | null = null;
252+
while (node) {
253+
const order = compare(value, node.value);
254+
if (order === 0 && returnIfFound) return node;
255+
256+
let direction: Direction = order < 0 ? "left" : "right";
257+
if (select === "higher" && order === 0) {
258+
direction = "right";
259+
} else if (select === "lower" && order === 0) {
260+
direction = "left";
261+
}
262+
263+
if (
264+
(select === "higher" && direction === "left") ||
265+
(select === "lower" && direction === "right")
266+
) {
267+
result = node;
268+
}
269+
270+
node = node[direction];
271+
}
272+
return result;
273+
}
274+
275+
/**
276+
* Finds the lowest (leftmost) value in the binary search tree which is
277+
* greater than or equal to the given value, or null if the given value
278+
* is higher than all elements of the tree.
279+
*
280+
* The complexity of this operation depends on the underlying structure of the
281+
* tree. Refer to the documentation of the structure itself for more details.
282+
*
283+
* @example Finding values in the tree
284+
* ```ts
285+
* import { BinarySearchTree } from "@std/data-structures/unstable-binary-search-tree";
286+
* import { assertEquals } from "@std/assert";
287+
*
288+
* const tree = BinarySearchTree.from<number>([42]);
289+
*
290+
* assertEquals(tree.ceiling(41), 42);
291+
* assertEquals(tree.ceiling(42), 42);
292+
* assertEquals(tree.ceiling(43), null);
293+
* ```
294+
*
295+
* @param value The value to search for in the binary search tree.
296+
* @returns The ceiling if it was found, or null if not.
297+
*/
298+
ceiling(value: T): T | null {
299+
return this.#findNode(value, "higher")?.value ?? null;
300+
}
301+
302+
/**
303+
* Finds the highest (rightmost) value in the binary search tree which is
304+
* less than or equal to the given value, or null if the given value
305+
* is lower than all elements of the tree.
306+
*
307+
* The complexity of this operation depends on the underlying structure of the
308+
* tree. Refer to the documentation of the structure itself for more details.
309+
*
310+
* @example Finding values in the tree
311+
* ```ts
312+
* import { BinarySearchTree } from "@std/data-structures/unstable-binary-search-tree";
313+
* import { assertEquals } from "@std/assert";
314+
*
315+
* const tree = BinarySearchTree.from<number>([42]);
316+
*
317+
* assertEquals(tree.floor(41), null);
318+
* assertEquals(tree.floor(42), 42);
319+
* assertEquals(tree.floor(43), 42);
320+
* ```
321+
*
322+
* @param value The value to search for in the binary search tree.
323+
* @returns The floor if it was found, or null if not.
324+
*/
325+
floor(value: T): T | null {
326+
return this.#findNode(value, "lower")?.value ?? null;
327+
}
328+
329+
/**
330+
* Finds the lowest (leftmost) value in the binary search tree which is
331+
* strictly greater than the given value, or null if the given value
332+
* is higher than or equal to all elements of the tree
333+
*
334+
* The complexity of this operation depends on the underlying structure of the
335+
* tree. Refer to the documentation of the structure itself for more details.
336+
*
337+
* @example Finding values in the tree
338+
* ```ts
339+
* import { BinarySearchTree } from "@std/data-structures/unstable-binary-search-tree";
340+
* import { assertEquals } from "@std/assert";
341+
*
342+
* const tree = BinarySearchTree.from<number>([42]);
343+
*
344+
* assertEquals(tree.higher(41), 42);
345+
* assertEquals(tree.higher(42), null);
346+
* assertEquals(tree.higher(43), null);
347+
* ```
348+
*
349+
* @param value The value to search for in the binary search tree.
350+
* @returns The higher value if it was found, or null if not.
351+
*/
352+
higher(value: T): T | null {
353+
return this.#findNode(value, "higher", false)?.value ?? null;
354+
}
355+
356+
/**
357+
* Finds the highest (rightmost) value in the binary search tree which is
358+
* strictly less than the given value, or null if the given value
359+
* is lower than or equal to all elements of the tree
360+
*
361+
* The complexity of this operation depends on the underlying structure of the
362+
* tree. Refer to the documentation of the structure itself for more details.
363+
*
364+
* @example Finding values in the tree
365+
* ```ts
366+
* import { BinarySearchTree } from "@std/data-structures/unstable-binary-search-tree";
367+
* import { assertEquals } from "@std/assert";
368+
*
369+
* const tree = BinarySearchTree.from<number>([42]);
370+
*
371+
* assertEquals(tree.lower(41), null);
372+
* assertEquals(tree.lower(42), null);
373+
* assertEquals(tree.lower(43), 42);
374+
* ```
375+
*
376+
* @param value The value to search for in the binary search tree.
377+
* @returns The lower value if it was found, or null if not.
378+
*/
379+
lower(value: T): T | null {
380+
return this.#findNode(value, "lower", false)?.value ?? null;
381+
}
382+
}

0 commit comments

Comments
 (0)