btree-core 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/b+tree.d.ts +429 -0
- package/b+tree.js +1545 -0
- package/b+tree.min.js +1 -0
- package/extended/bulkLoad.d.ts +14 -0
- package/extended/bulkLoad.js +113 -0
- package/extended/bulkLoad.min.js +1 -0
- package/extended/decompose.d.ts +1 -0
- package/extended/decompose.js +680 -0
- package/extended/decompose.min.js +1 -0
- package/extended/diffAgainst.d.ts +23 -0
- package/extended/diffAgainst.js +254 -0
- package/extended/diffAgainst.min.js +1 -0
- package/extended/forEachKeyInBoth.d.ts +19 -0
- package/extended/forEachKeyInBoth.js +73 -0
- package/extended/forEachKeyInBoth.min.js +1 -0
- package/extended/forEachKeyNotIn.d.ts +18 -0
- package/extended/forEachKeyNotIn.js +87 -0
- package/extended/forEachKeyNotIn.min.js +1 -0
- package/extended/index.d.ts +133 -0
- package/extended/index.js +200 -0
- package/extended/index.min.js +1 -0
- package/extended/intersect.d.ts +16 -0
- package/extended/intersect.js +44 -0
- package/extended/intersect.min.js +1 -0
- package/extended/parallelWalk.d.ts +1 -0
- package/extended/parallelWalk.js +188 -0
- package/extended/parallelWalk.min.js +1 -0
- package/extended/shared.d.ts +1 -0
- package/extended/shared.js +64 -0
- package/extended/shared.min.js +1 -0
- package/extended/subtract.d.ts +16 -0
- package/extended/subtract.js +35 -0
- package/extended/subtract.min.js +1 -0
- package/extended/union.d.ts +16 -0
- package/extended/union.js +36 -0
- package/extended/union.min.js +1 -0
- package/interfaces.d.ts +307 -0
- package/package.json +122 -0
- package/readme.md +420 -0
- package/sorted-array.d.ts +22 -0
- package/sorted-array.js +71 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildFromDecomposition = exports.decompose = void 0;
|
|
4
|
+
var b_tree_1 = require("../b+tree");
|
|
5
|
+
var shared_1 = require("./shared");
|
|
6
|
+
var parallelWalk_1 = require("./parallelWalk");
|
|
7
|
+
var decomposeLoadFactor = 0.7;
|
|
8
|
+
/**
|
|
9
|
+
* Decomposes two trees into disjoint nodes. Reuses interior nodes when they do not overlap/intersect with any leaf nodes
|
|
10
|
+
* in the other tree. Overlapping leaf nodes are broken down into new leaf nodes containing merged entries.
|
|
11
|
+
* The algorithm is a parallel tree walk using two cursors. The trailing cursor (behind in key space) is walked forward
|
|
12
|
+
* until it is at or after the leading cursor. As it does this, any whole nodes or subtrees it passes are guaranteed to
|
|
13
|
+
* be disjoint. This is true because the leading cursor was also previously walked in this way, and is thus pointing to
|
|
14
|
+
* the first key at or after the trailing cursor's previous position.
|
|
15
|
+
* The cursor walk is efficient, meaning it skips over disjoint subtrees entirely rather than visiting every leaf.
|
|
16
|
+
* Note: some of the returned leaves may be underfilled.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
function decompose(left, right, combineFn, ignoreRight) {
|
|
20
|
+
if (ignoreRight === void 0) { ignoreRight = false; }
|
|
21
|
+
var maxNodeSize = left._maxNodeSize;
|
|
22
|
+
var cmp = left._compare;
|
|
23
|
+
(0, b_tree_1.check)(left._root.size() > 0 && right._root.size() > 0, "decompose requires non-empty inputs");
|
|
24
|
+
// Holds the disjoint nodes that result from decomposition.
|
|
25
|
+
// Stored as parallel arrays of (height, node) to avoid creating many tiny tuples
|
|
26
|
+
var disjointHeights = [];
|
|
27
|
+
var disjointNodes = [];
|
|
28
|
+
// During the decomposition, leaves that are not disjoint are decomposed into individual entries
|
|
29
|
+
// that accumulate in this array in sorted order. They are flushed into leaf nodes whenever a reused
|
|
30
|
+
// disjoint subtree is added to the disjoint set.
|
|
31
|
+
// Note that there are unavoidable cases in which this will generate underfilled leaves.
|
|
32
|
+
// An example of this would be a leaf in one tree that contained keys [0, 100, 101, 102].
|
|
33
|
+
// In the other tree, there is a leaf that contains [2, 3, 4, 5]. This leaf can be reused entirely,
|
|
34
|
+
// but the first tree's leaf must be decomposed into [0] and [100, 101, 102]
|
|
35
|
+
var pendingKeys = [];
|
|
36
|
+
var pendingValues = [];
|
|
37
|
+
var tallestIndex = -1, tallestHeight = -1;
|
|
38
|
+
// During the upward part of the cursor walk, this holds the highest disjoint node seen so far.
|
|
39
|
+
// This is done because we cannot know immediately whether we can add the node to the disjoint set
|
|
40
|
+
// because its ancestor may also be disjoint and should be reused instead.
|
|
41
|
+
var highestDisjoint
|
|
42
|
+
// Have to do this as cast to convince TS it's ever assigned
|
|
43
|
+
= undefined;
|
|
44
|
+
var minSize = Math.floor(maxNodeSize / 2);
|
|
45
|
+
var onLeafCreation = function (leaf) {
|
|
46
|
+
var height = leaf.keys.length < minSize ? -1 : 0;
|
|
47
|
+
disjointHeights.push(height);
|
|
48
|
+
disjointNodes.push(leaf);
|
|
49
|
+
};
|
|
50
|
+
var addSharedNodeToDisjointSet = function (node, height) {
|
|
51
|
+
// flush pending entries
|
|
52
|
+
(0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
|
|
53
|
+
pendingKeys.length = 0;
|
|
54
|
+
pendingValues.length = 0;
|
|
55
|
+
// Don't share underfilled leaves, instead mark them as needing merging
|
|
56
|
+
if (node.isLeaf && node.keys.length < minSize) {
|
|
57
|
+
disjointHeights.push(-1);
|
|
58
|
+
disjointNodes.push(node.clone());
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
node.isShared = true;
|
|
62
|
+
disjointHeights.push(height);
|
|
63
|
+
disjointNodes.push(node);
|
|
64
|
+
}
|
|
65
|
+
if (height > tallestHeight) {
|
|
66
|
+
tallestIndex = disjointHeights.length - 1;
|
|
67
|
+
tallestHeight = height;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var addHighestDisjoint = function () {
|
|
71
|
+
if (highestDisjoint !== undefined) {
|
|
72
|
+
addSharedNodeToDisjointSet(highestDisjoint.node, highestDisjoint.height);
|
|
73
|
+
highestDisjoint = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// Mark all nodes at or above depthFrom in the cursor spine as disqualified (non-disjoint)
|
|
77
|
+
var disqualifySpine = function (cursor, depthFrom) {
|
|
78
|
+
var spine = cursor.spine;
|
|
79
|
+
for (var i = depthFrom; i >= 0; --i) {
|
|
80
|
+
var payload = spine[i].payload;
|
|
81
|
+
// Safe to early out because we always disqualify all ancestors of a disqualified node
|
|
82
|
+
// That is correct because every ancestor of a non-disjoint node is also non-disjoint
|
|
83
|
+
// because it must enclose the non-disjoint range.
|
|
84
|
+
if (payload.disqualified)
|
|
85
|
+
break;
|
|
86
|
+
payload.disqualified = true;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Cursor payload factory
|
|
90
|
+
var makePayload = function () { return ({ disqualified: false }); };
|
|
91
|
+
var pushLeafRange = function (leaf, from, toExclusive) {
|
|
92
|
+
var keys = leaf.keys;
|
|
93
|
+
var values = leaf.values;
|
|
94
|
+
for (var i = from; i < toExclusive; ++i) {
|
|
95
|
+
pendingKeys.push(keys[i]);
|
|
96
|
+
pendingValues.push(values[i]);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var onMoveInLeaf = function (leaf, payload, fromIndex, toIndex, startedEqual) {
|
|
100
|
+
(0, b_tree_1.check)(payload.disqualified === true, "onMoveInLeaf: leaf must be disqualified");
|
|
101
|
+
var start = startedEqual ? fromIndex + 1 : fromIndex;
|
|
102
|
+
if (start < toIndex)
|
|
103
|
+
pushLeafRange(leaf, start, toIndex);
|
|
104
|
+
};
|
|
105
|
+
var onExitLeaf = function (leaf, payload, startingIndex, startedEqual, cursorThis) {
|
|
106
|
+
highestDisjoint = undefined;
|
|
107
|
+
if (!payload.disqualified) {
|
|
108
|
+
highestDisjoint = { node: leaf, height: 0 };
|
|
109
|
+
if (cursorThis.spine.length === 0) {
|
|
110
|
+
// if we are exiting a leaf and there are no internal nodes, we will reach the end of the tree.
|
|
111
|
+
// In this case we need to add the leaf now because step up will not be called.
|
|
112
|
+
addHighestDisjoint();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
var start = startedEqual ? startingIndex + 1 : startingIndex;
|
|
117
|
+
var leafSize = leaf.keys.length;
|
|
118
|
+
if (start < leafSize)
|
|
119
|
+
pushLeafRange(leaf, start, leafSize);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var onStepUp = function (parent, height, payload, fromIndex, spineIndex, stepDownIndex, cursorThis) {
|
|
123
|
+
var children = parent.children;
|
|
124
|
+
var nextHeight = height - 1;
|
|
125
|
+
if (stepDownIndex !== stepDownIndex /* NaN: still walking up */
|
|
126
|
+
|| stepDownIndex === Number.POSITIVE_INFINITY /* target key is beyond edge of tree, done with walk */) {
|
|
127
|
+
if (!payload.disqualified) {
|
|
128
|
+
if (stepDownIndex === Number.POSITIVE_INFINITY) {
|
|
129
|
+
// We have finished our walk, and we won't be stepping down, so add the root
|
|
130
|
+
// Roots are allowed to be underfilled, so break the root up here if so to avoid
|
|
131
|
+
// creating underfilled interior nodes during reconstruction.
|
|
132
|
+
// Note: the main btree implementation allows underfilled nodes in general, this algorithm
|
|
133
|
+
// guarantees that no additional underfilled nodes are created beyond what was already present.
|
|
134
|
+
if (parent.keys.length < minSize) {
|
|
135
|
+
for (var i = fromIndex; i < children.length; ++i)
|
|
136
|
+
addSharedNodeToDisjointSet(children[i], nextHeight);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
addSharedNodeToDisjointSet(parent, height);
|
|
140
|
+
}
|
|
141
|
+
highestDisjoint = undefined;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
highestDisjoint = { node: parent, height: height };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
addHighestDisjoint();
|
|
149
|
+
var len = children.length;
|
|
150
|
+
for (var i = fromIndex + 1; i < len; ++i)
|
|
151
|
+
addSharedNodeToDisjointSet(children[i], nextHeight);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// We have a valid step down index, so we need to disqualify the spine if needed.
|
|
156
|
+
// This is identical to the step down logic, but we must also perform it here because
|
|
157
|
+
// in the case of stepping down into a leaf, the step down callback is never called.
|
|
158
|
+
if (stepDownIndex > 0) {
|
|
159
|
+
disqualifySpine(cursorThis, spineIndex);
|
|
160
|
+
}
|
|
161
|
+
addHighestDisjoint();
|
|
162
|
+
for (var i = fromIndex + 1; i < stepDownIndex; ++i)
|
|
163
|
+
addSharedNodeToDisjointSet(children[i], nextHeight);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var onStepDown = function (node, height, spineIndex, stepDownIndex, cursorThis) {
|
|
167
|
+
if (stepDownIndex > 0) {
|
|
168
|
+
// When we step down into a node, we know that we have walked from a key that is less than our target.
|
|
169
|
+
// Because of this, if we are not stepping down into the first child, we know that all children before
|
|
170
|
+
// the stepDownIndex must overlap with the other tree because they must be before our target key. Since
|
|
171
|
+
// the child we are stepping into has a key greater than our target key, this node must overlap.
|
|
172
|
+
// If a child overlaps, the entire spine overlaps because a parent in a btree always encloses the range
|
|
173
|
+
// of its children.
|
|
174
|
+
disqualifySpine(cursorThis, spineIndex);
|
|
175
|
+
var children = node.children;
|
|
176
|
+
var nextHeight = height - 1;
|
|
177
|
+
for (var i = 0; i < stepDownIndex; ++i)
|
|
178
|
+
addSharedNodeToDisjointSet(children[i], nextHeight);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var onEnterLeaf = function (leaf, destIndex, cursorThis, cursorOther) {
|
|
182
|
+
if (destIndex > 0
|
|
183
|
+
|| (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
|
|
184
|
+
// Similar logic to the step-down case, except in this case we also know the leaf in the other
|
|
185
|
+
// tree overlaps a leaf in this tree (this leaf, specifically). Thus, we can disqualify both spines.
|
|
186
|
+
cursorThis.leafPayload.disqualified = true;
|
|
187
|
+
cursorOther.leafPayload.disqualified = true;
|
|
188
|
+
disqualifySpine(cursorThis, cursorThis.spine.length - 1);
|
|
189
|
+
disqualifySpine(cursorOther, cursorOther.spine.length - 1);
|
|
190
|
+
pushLeafRange(leaf, 0, destIndex);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
// Need the max key of both trees to perform the "finishing" walk of which ever cursor finishes second
|
|
194
|
+
var maxKeyLeft = left._root.maxKey();
|
|
195
|
+
var maxKeyRight = right._root.maxKey();
|
|
196
|
+
var maxKey = cmp(maxKeyLeft, maxKeyRight) >= 0 ? maxKeyLeft : maxKeyRight;
|
|
197
|
+
// Initialize cursors at minimum keys.
|
|
198
|
+
var curA = (0, parallelWalk_1.createCursor)(left, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
|
|
199
|
+
var curB;
|
|
200
|
+
if (ignoreRight) {
|
|
201
|
+
var dummyPayload_1 = { disqualified: true };
|
|
202
|
+
var onStepUpIgnore = function (_1, _2, _3, _4, spineIndex, stepDownIndex, cursorThis) {
|
|
203
|
+
if (stepDownIndex > 0) {
|
|
204
|
+
disqualifySpine(cursorThis, spineIndex);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var onStepDownIgnore = function (_, __, spineIndex, stepDownIndex, cursorThis) {
|
|
208
|
+
if (stepDownIndex > 0) {
|
|
209
|
+
disqualifySpine(cursorThis, spineIndex);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var onEnterLeafIgnore = function (leaf, destIndex, _, cursorOther) {
|
|
213
|
+
if (destIndex > 0
|
|
214
|
+
|| (0, b_tree_1.areOverlapping)(leaf.minKey(), leaf.maxKey(), (0, parallelWalk_1.getKey)(cursorOther), cursorOther.leaf.maxKey(), cmp)) {
|
|
215
|
+
cursorOther.leafPayload.disqualified = true;
|
|
216
|
+
disqualifySpine(cursorOther, cursorOther.spine.length - 1);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
curB = (0, parallelWalk_1.createCursor)(right, function () { return dummyPayload_1; }, onEnterLeafIgnore, parallelWalk_1.noop, parallelWalk_1.noop, onStepUpIgnore, onStepDownIgnore);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
curB = (0, parallelWalk_1.createCursor)(right, makePayload, onEnterLeaf, onMoveInLeaf, onExitLeaf, onStepUp, onStepDown);
|
|
223
|
+
}
|
|
224
|
+
// The guarantee that no overlapping interior nodes are accidentally reused relies on the careful
|
|
225
|
+
// alternating hopping walk of the cursors: WLOG, cursorA always--with one exception--walks from a key just behind (in key space)
|
|
226
|
+
// the key of cursorB to the first key >= cursorB. Call this transition a "crossover point." All interior nodes that
|
|
227
|
+
// overlap cause a crossover point, and all crossover points are guaranteed to be walked using this method. Thus,
|
|
228
|
+
// all overlapping interior nodes will be found if they are checked for on step-down.
|
|
229
|
+
// The one exception mentioned above is when they start at the same key. In this case, they are both advanced forward and then
|
|
230
|
+
// their new ordering determines how they walk from there.
|
|
231
|
+
// The one issue then is detecting any overlaps that occur based on their very initial position (minimum key of each tree).
|
|
232
|
+
// This is handled by the initial disqualification step below, which essentially emulates the step down disqualification for each spine.
|
|
233
|
+
// Initialize disqualification w.r.t. opposite leaf.
|
|
234
|
+
var initDisqualify = function (cur, other) {
|
|
235
|
+
var minKey = (0, parallelWalk_1.getKey)(cur);
|
|
236
|
+
var otherMin = (0, parallelWalk_1.getKey)(other);
|
|
237
|
+
var otherMax = other.leaf.maxKey();
|
|
238
|
+
if ((0, b_tree_1.areOverlapping)(minKey, cur.leaf.maxKey(), otherMin, otherMax, cmp))
|
|
239
|
+
cur.leafPayload.disqualified = true;
|
|
240
|
+
for (var i = 0; i < cur.spine.length; ++i) {
|
|
241
|
+
var entry = cur.spine[i];
|
|
242
|
+
// Since we are on the left side of the tree, we can use the leaf min key for every spine node
|
|
243
|
+
if ((0, b_tree_1.areOverlapping)(minKey, entry.node.maxKey(), otherMin, otherMax, cmp))
|
|
244
|
+
entry.payload.disqualified = true;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
initDisqualify(curA, curB);
|
|
248
|
+
initDisqualify(curB, curA);
|
|
249
|
+
var leading = curA;
|
|
250
|
+
var trailing = curB;
|
|
251
|
+
var order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
|
|
252
|
+
// Walk both cursors in alternating hops
|
|
253
|
+
while (true) {
|
|
254
|
+
var areEqual = order === 0;
|
|
255
|
+
if (areEqual) {
|
|
256
|
+
var key = (0, parallelWalk_1.getKey)(leading);
|
|
257
|
+
var vA = curA.leaf.values[curA.leafIndex];
|
|
258
|
+
var vB = curB.leaf.values[curB.leafIndex];
|
|
259
|
+
// Perform the actual merge of values here. The cursors will avoid adding a duplicate of this key/value
|
|
260
|
+
// to pending because they respect the areEqual flag during their moves.
|
|
261
|
+
var combined = combineFn(key, vA, vB);
|
|
262
|
+
if (combined !== undefined) {
|
|
263
|
+
pendingKeys.push(key);
|
|
264
|
+
pendingValues.push(combined);
|
|
265
|
+
}
|
|
266
|
+
var outTrailing = (0, parallelWalk_1.moveForwardOne)(trailing, leading);
|
|
267
|
+
var outLeading = (0, parallelWalk_1.moveForwardOne)(leading, trailing);
|
|
268
|
+
if (outTrailing || outLeading) {
|
|
269
|
+
if (!outTrailing || !outLeading) {
|
|
270
|
+
// In these cases, we pass areEqual=false because a return value of "out of tree" means
|
|
271
|
+
// the cursor did not move. This must be true because they started equal and one of them had more tree
|
|
272
|
+
// to walk (one is !out), so they cannot be equal at this point.
|
|
273
|
+
if (outTrailing) {
|
|
274
|
+
(0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, false);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
(0, parallelWalk_1.moveTo)(trailing, leading, maxKey, false, false);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
order = cmp((0, parallelWalk_1.getKey)(leading), (0, parallelWalk_1.getKey)(trailing));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
if (order < 0) {
|
|
286
|
+
var tmp = trailing;
|
|
287
|
+
trailing = leading;
|
|
288
|
+
leading = tmp;
|
|
289
|
+
}
|
|
290
|
+
var _a = (0, parallelWalk_1.moveTo)(trailing, leading, (0, parallelWalk_1.getKey)(leading), true, areEqual), out = _a[0], nowEqual = _a[1];
|
|
291
|
+
if (out) {
|
|
292
|
+
(0, parallelWalk_1.moveTo)(leading, trailing, maxKey, false, areEqual);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
else if (nowEqual) {
|
|
296
|
+
order = 0;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
order = -1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Ensure any trailing non-disjoint entries are added
|
|
304
|
+
(0, shared_1.makeLeavesFrom)(pendingKeys, pendingValues, maxNodeSize, decomposeLoadFactor, onLeafCreation);
|
|
305
|
+
// In cases like full interleaving, no leaves may be created until now
|
|
306
|
+
if (tallestHeight < 0 && disjointHeights.length > 0) {
|
|
307
|
+
tallestIndex = 0;
|
|
308
|
+
}
|
|
309
|
+
return { heights: disjointHeights, nodes: disjointNodes, tallestIndex: tallestIndex };
|
|
310
|
+
}
|
|
311
|
+
exports.decompose = decompose;
|
|
312
|
+
/**
|
|
313
|
+
* Constructs a B-Tree from the result of a decomposition (set of disjoint nodes).
|
|
314
|
+
* @internal
|
|
315
|
+
*/
|
|
316
|
+
function buildFromDecomposition(constructor, branchingFactor, decomposed, cmp, maxNodeSize) {
|
|
317
|
+
var heights = decomposed.heights, nodes = decomposed.nodes, tallestIndex = decomposed.tallestIndex;
|
|
318
|
+
(0, b_tree_1.check)(heights.length === nodes.length, "Decompose result has mismatched heights and nodes.");
|
|
319
|
+
var disjointEntryCount = heights.length;
|
|
320
|
+
// Now we have a set of disjoint subtrees and we need to merge them into a single tree.
|
|
321
|
+
// To do this, we start with the tallest subtree from the disjoint set and, for all subtrees
|
|
322
|
+
// to the "right" and "left" of it in sorted order, we append them onto the appropriate side
|
|
323
|
+
// of the current tree, splitting nodes as necessary to maintain balance.
|
|
324
|
+
// A "side" is referred to as a frontier, as it is a linked list of nodes from the root down to
|
|
325
|
+
// the leaf level on that side of the tree. Each appended subtree is appended to the node at the
|
|
326
|
+
// same height as itself on the frontier. Each tree is guaranteed to be at most as tall as the
|
|
327
|
+
// current frontier because we start from the tallest subtree and work outward.
|
|
328
|
+
var initialRoot = nodes[tallestIndex];
|
|
329
|
+
var frontier = [initialRoot];
|
|
330
|
+
var rightContext = {
|
|
331
|
+
branchingFactor: branchingFactor,
|
|
332
|
+
spine: frontier,
|
|
333
|
+
sideIndex: getRightmostIndex,
|
|
334
|
+
sideInsertionIndex: getRightInsertionIndex,
|
|
335
|
+
splitOffSide: splitOffRightSide,
|
|
336
|
+
balanceLeaves: balanceLeavesRight,
|
|
337
|
+
updateMax: updateRightMax,
|
|
338
|
+
mergeLeaves: mergeRightEntries
|
|
339
|
+
};
|
|
340
|
+
// Process all subtrees to the right of the tallest subtree
|
|
341
|
+
if (tallestIndex + 1 <= disjointEntryCount - 1) {
|
|
342
|
+
updateFrontier(rightContext, 0);
|
|
343
|
+
processSide(heights, nodes, tallestIndex + 1, disjointEntryCount, 1, rightContext);
|
|
344
|
+
}
|
|
345
|
+
var leftContext = {
|
|
346
|
+
branchingFactor: branchingFactor,
|
|
347
|
+
spine: frontier,
|
|
348
|
+
sideIndex: getLeftmostIndex,
|
|
349
|
+
sideInsertionIndex: getLeftmostIndex,
|
|
350
|
+
splitOffSide: splitOffLeftSide,
|
|
351
|
+
balanceLeaves: balanceLeavesLeft,
|
|
352
|
+
updateMax: parallelWalk_1.noop,
|
|
353
|
+
mergeLeaves: mergeLeftEntries
|
|
354
|
+
};
|
|
355
|
+
// Process all subtrees to the left of the current tree
|
|
356
|
+
if (tallestIndex - 1 >= 0) {
|
|
357
|
+
// Note we need to update the frontier here because the right-side processing may have grown the tree taller.
|
|
358
|
+
updateFrontier(leftContext, 0);
|
|
359
|
+
processSide(heights, nodes, tallestIndex - 1, -1, -1, leftContext);
|
|
360
|
+
}
|
|
361
|
+
var reconstructed = new constructor(undefined, cmp, maxNodeSize);
|
|
362
|
+
reconstructed._root = frontier[0];
|
|
363
|
+
// Return the resulting tree
|
|
364
|
+
return reconstructed;
|
|
365
|
+
}
|
|
366
|
+
exports.buildFromDecomposition = buildFromDecomposition;
|
|
367
|
+
/**
|
|
368
|
+
* Processes one side (left or right) of the disjoint subtree set during a reconstruction operation.
|
|
369
|
+
* Merges each subtree in the disjoint set from start to end (exclusive) into the given spine.
|
|
370
|
+
* @internal
|
|
371
|
+
*/
|
|
372
|
+
function processSide(heights, nodes, start, end, step, context) {
|
|
373
|
+
var spine = context.spine, sideIndex = context.sideIndex;
|
|
374
|
+
// Determine the depth of the first shared node on the frontier.
|
|
375
|
+
// Appending subtrees to the frontier must respect the copy-on-write semantics by cloning
|
|
376
|
+
// any shared nodes down to the insertion point. We track it by depth to avoid a log(n) walk of the
|
|
377
|
+
// frontier for each insertion as that would fundamentally change our asymptotics.
|
|
378
|
+
var isSharedFrontierDepth = 0;
|
|
379
|
+
var cur = spine[0];
|
|
380
|
+
// Find the first shared node on the frontier
|
|
381
|
+
while (!cur.isShared && isSharedFrontierDepth < spine.length - 1) {
|
|
382
|
+
isSharedFrontierDepth++;
|
|
383
|
+
cur = cur.children[sideIndex(cur)];
|
|
384
|
+
}
|
|
385
|
+
// This array holds the sum of sizes of nodes that have been inserted but not yet propagated upward.
|
|
386
|
+
// For example, if a subtree of size 5 is inserted at depth 2, then unflushedSizes[1] += 5.
|
|
387
|
+
// These sizes are added to the depth above the insertion point because the insertion updates the direct parent of the insertion.
|
|
388
|
+
// These sizes are flushed upward any time we need to insert at level higher than pending unflushed sizes.
|
|
389
|
+
// E.g. in our example, if we later insert at depth 0, we will add 5 to the node at depth 1 and the root at depth 0 before inserting.
|
|
390
|
+
// This scheme enables us to avoid a log(n) propagation of sizes for each insertion.
|
|
391
|
+
var unflushedSizes = new Array(spine.length).fill(0); // pre-fill to avoid "holey" array
|
|
392
|
+
for (var i = start; i != end; i += step) {
|
|
393
|
+
var currentHeight = spine.length - 1; // height is number of internal levels; 0 means leaf
|
|
394
|
+
var subtree = nodes[i];
|
|
395
|
+
var subtreeHeight = heights[i];
|
|
396
|
+
var isEntryInsertion = subtreeHeight === -1;
|
|
397
|
+
(0, b_tree_1.check)(subtreeHeight <= currentHeight, "Subtree taller than spine during reconstruction.");
|
|
398
|
+
// If subtree height is -1 (indicating underfilled leaf), then this indicates insertion into a leaf
|
|
399
|
+
// otherwise, it points to a node whose children have height === subtreeHeight
|
|
400
|
+
var insertionDepth = currentHeight - (subtreeHeight + 1);
|
|
401
|
+
// Ensure path is unshared before mutation
|
|
402
|
+
ensureNotShared(context, isSharedFrontierDepth, insertionDepth);
|
|
403
|
+
var insertionCount = // non-recursive
|
|
404
|
+
void 0; // non-recursive
|
|
405
|
+
var insertionSize = // recursive
|
|
406
|
+
void 0; // recursive
|
|
407
|
+
if (isEntryInsertion) {
|
|
408
|
+
(0, b_tree_1.check)(subtree.isShared !== true);
|
|
409
|
+
insertionCount = insertionSize = subtree.keys.length;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
insertionCount = 1;
|
|
413
|
+
insertionSize = subtree.size();
|
|
414
|
+
}
|
|
415
|
+
var cascadeEndDepth = findSplitCascadeEndDepth(context, insertionDepth, insertionCount);
|
|
416
|
+
// Calculate expansion depth (first ancestor with capacity)
|
|
417
|
+
var expansionDepth = Math.max(0, // -1 indicates we will cascade to new root
|
|
418
|
+
cascadeEndDepth);
|
|
419
|
+
// Update sizes on spine above the shared ancestor before we expand
|
|
420
|
+
updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, expansionDepth);
|
|
421
|
+
var newRoot = undefined;
|
|
422
|
+
var sizeChangeDepth = void 0;
|
|
423
|
+
if (isEntryInsertion) {
|
|
424
|
+
newRoot = splitUpwardsAndInsertEntries(context, insertionDepth, subtree);
|
|
425
|
+
// if we are inserting entries, we don't have to update a cached size on the leaf as they simply return count of keys
|
|
426
|
+
sizeChangeDepth = insertionDepth - 1;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
newRoot = splitUpwardsAndInsert(context, insertionDepth, subtree)[0];
|
|
430
|
+
sizeChangeDepth = insertionDepth;
|
|
431
|
+
}
|
|
432
|
+
if (newRoot) {
|
|
433
|
+
// Set the spine root to the highest up new node; the rest of the spine is updated below
|
|
434
|
+
spine[0] = newRoot;
|
|
435
|
+
unflushedSizes.push(0); // new root level, keep unflushed sizes in sync
|
|
436
|
+
sizeChangeDepth++; // account for the spine lengthening
|
|
437
|
+
}
|
|
438
|
+
isSharedFrontierDepth = sizeChangeDepth + 1;
|
|
439
|
+
unflushedSizes[sizeChangeDepth] += insertionSize;
|
|
440
|
+
// Finally, update the frontier from the highest new node downward
|
|
441
|
+
// Note that this is often the point where the new subtree is attached,
|
|
442
|
+
// but in the case of cascaded splits it may be higher up.
|
|
443
|
+
updateFrontier(context, expansionDepth);
|
|
444
|
+
(0, b_tree_1.check)(isSharedFrontierDepth === spine.length - 1 || spine[isSharedFrontierDepth].isShared === true, "Non-leaf subtrees must be shared.");
|
|
445
|
+
(0, b_tree_1.check)(unflushedSizes.length === spine.length, "Unflushed sizes length mismatch after root split.");
|
|
446
|
+
// Useful for debugging:
|
|
447
|
+
//updateSizeAndMax(context, unflushedSizes, spine.length - 1, 0);
|
|
448
|
+
//spine[0].checkValid(0, { _compare: cmp } as unknown as BTree<K, V>, 0);
|
|
449
|
+
}
|
|
450
|
+
// Finally, propagate any remaining unflushed sizes upward and update max keys
|
|
451
|
+
updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, 0);
|
|
452
|
+
}
|
|
453
|
+
;
|
|
454
|
+
/**
|
|
455
|
+
* Cascade splits upward if capacity needed, then append a subtree at a given depth on the chosen side.
|
|
456
|
+
* All un-propagated sizes must have already been applied to the spine up to the end of any cascading expansions.
|
|
457
|
+
* This method guarantees that the size of the inserted subtree will not propagate upward beyond the insertion point.
|
|
458
|
+
* Returns a new root if the root was split, otherwise undefined, and the node into which the subtree was inserted.
|
|
459
|
+
*/
|
|
460
|
+
function splitUpwardsAndInsert(context, insertionDepth, subtree) {
|
|
461
|
+
var spine = context.spine, branchingFactor = context.branchingFactor, sideIndex = context.sideIndex, sideInsertionIndex = context.sideInsertionIndex, splitOffSide = context.splitOffSide, updateMax = context.updateMax;
|
|
462
|
+
// We must take care to avoid accidental propagation upward of the size of the inserted subtree
|
|
463
|
+
// To do this, we first split nodes upward from the insertion point until we find a node with capacity
|
|
464
|
+
// or create a new root. Since all un-propagated sizes have already been applied to the spine up to this point,
|
|
465
|
+
// inserting at the end ensures no accidental propagation.
|
|
466
|
+
// Depth is -1 if the subtree is the same height as the current tree
|
|
467
|
+
if (insertionDepth >= 0) {
|
|
468
|
+
var carry = undefined;
|
|
469
|
+
// Determine initially where to insert after any splits
|
|
470
|
+
var insertTarget = spine[insertionDepth];
|
|
471
|
+
if (insertTarget.keys.length === branchingFactor) {
|
|
472
|
+
insertTarget = carry = splitOffSide(insertTarget);
|
|
473
|
+
}
|
|
474
|
+
var d = insertionDepth - 1;
|
|
475
|
+
while (carry && d >= 0) {
|
|
476
|
+
var parent = spine[d];
|
|
477
|
+
var sideChildIndex = sideIndex(parent);
|
|
478
|
+
// Refresh last key since child was split
|
|
479
|
+
updateMax(parent, parent.children[sideChildIndex].maxKey());
|
|
480
|
+
if (parent.keys.length < branchingFactor) {
|
|
481
|
+
// We have reached the end of the cascade
|
|
482
|
+
insertNoCount(parent, sideInsertionIndex(parent), carry);
|
|
483
|
+
carry = undefined;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// Splitting the parent here requires care to avoid incorrectly double counting sizes
|
|
487
|
+
// Example: a node is at max capacity 4, with children each of size 4 for 16 total.
|
|
488
|
+
// We split the node into two nodes of 2 children each, but this does *not* modify the size
|
|
489
|
+
// of its parent. Therefore when we insert the carry into the torn-off node, we must not
|
|
490
|
+
// increase its size or we will double-count the size of the carry subtree.
|
|
491
|
+
var tornOff = splitOffSide(parent);
|
|
492
|
+
insertNoCount(tornOff, sideInsertionIndex(tornOff), carry);
|
|
493
|
+
carry = tornOff;
|
|
494
|
+
}
|
|
495
|
+
d--;
|
|
496
|
+
}
|
|
497
|
+
var newRoot = undefined;
|
|
498
|
+
if (carry !== undefined) {
|
|
499
|
+
// Expansion reached the root, need a new root to hold carry
|
|
500
|
+
var oldRoot = spine[0];
|
|
501
|
+
newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size() + carry.size());
|
|
502
|
+
insertNoCount(newRoot, sideInsertionIndex(newRoot), carry);
|
|
503
|
+
}
|
|
504
|
+
// Finally, insert the subtree at the insertion point
|
|
505
|
+
insertNoCount(insertTarget, sideInsertionIndex(insertTarget), subtree);
|
|
506
|
+
return [newRoot, insertTarget];
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Insertion of subtree with equal height to current tree
|
|
510
|
+
var oldRoot = spine[0];
|
|
511
|
+
var newRoot = new b_tree_1.BNodeInternal([oldRoot], oldRoot.size());
|
|
512
|
+
insertNoCount(newRoot, sideInsertionIndex(newRoot), subtree);
|
|
513
|
+
return [newRoot, newRoot];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
;
|
|
517
|
+
/**
|
|
518
|
+
* Inserts an underfilled leaf (entryContainer), merging with its sibling if possible and splitting upward if not.
|
|
519
|
+
*/
|
|
520
|
+
function splitUpwardsAndInsertEntries(context, insertionDepth, entryContainer) {
|
|
521
|
+
var branchingFactor = context.branchingFactor, spine = context.spine, balanceLeaves = context.balanceLeaves, mergeLeaves = context.mergeLeaves;
|
|
522
|
+
var entryCount = entryContainer.keys.length;
|
|
523
|
+
var parent = spine[insertionDepth];
|
|
524
|
+
var parentSize = parent.keys.length;
|
|
525
|
+
if (parentSize + entryCount <= branchingFactor) {
|
|
526
|
+
// Sibling has capacity, just merge into it
|
|
527
|
+
mergeLeaves(parent, entryContainer);
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// As with the internal node splitUpwardsAndInsert method, this method also must make all structural changes
|
|
532
|
+
// to the tree before inserting any new content. This is to avoid accidental propagation of sizes upward.
|
|
533
|
+
var _a = splitUpwardsAndInsert(context, insertionDepth - 1, entryContainer), newRoot = _a[0], grandparent = _a[1];
|
|
534
|
+
var minSize = Math.floor(branchingFactor / 2);
|
|
535
|
+
var toTake = minSize - entryCount;
|
|
536
|
+
balanceLeaves(grandparent, entryContainer, toTake);
|
|
537
|
+
return newRoot;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Clone along the spine from [isSharedFrontierDepth to depthTo] inclusive so path is safe to mutate.
|
|
542
|
+
* Short-circuits if first shared node is deeper than depthTo (the insertion depth).
|
|
543
|
+
*/
|
|
544
|
+
function ensureNotShared(context, isSharedFrontierDepth, depthToInclusive) {
|
|
545
|
+
var spine = context.spine, sideIndex = context.sideIndex;
|
|
546
|
+
if (depthToInclusive < 0 /* new root case */)
|
|
547
|
+
return; // nothing to clone when root is a leaf; equal-height case will handle this
|
|
548
|
+
// Clone root if needed first (depth 0)
|
|
549
|
+
if (isSharedFrontierDepth === 0) {
|
|
550
|
+
var root = spine[0];
|
|
551
|
+
spine[0] = root.clone();
|
|
552
|
+
}
|
|
553
|
+
// Clone downward along the frontier to 'depthToInclusive'
|
|
554
|
+
for (var depth = Math.max(isSharedFrontierDepth, 1); depth <= depthToInclusive; depth++) {
|
|
555
|
+
var parent = spine[depth - 1];
|
|
556
|
+
var childIndex = sideIndex(parent);
|
|
557
|
+
var clone = parent.children[childIndex].clone();
|
|
558
|
+
parent.children[childIndex] = clone;
|
|
559
|
+
spine[depth] = clone;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
;
|
|
563
|
+
/**
|
|
564
|
+
* Propagates size updates and updates max keys for nodes in (isSharedFrontierDepth, depthTo)
|
|
565
|
+
*/
|
|
566
|
+
function updateSizeAndMax(context, unflushedSizes, isSharedFrontierDepth, depthUpToInclusive) {
|
|
567
|
+
var spine = context.spine, updateMax = context.updateMax;
|
|
568
|
+
// If isSharedFrontierDepth is <= depthUpToInclusive there is nothing to update because
|
|
569
|
+
// the insertion point is inside a shared node which will always have correct sizes
|
|
570
|
+
var maxKey = spine[isSharedFrontierDepth].maxKey();
|
|
571
|
+
var startDepth = isSharedFrontierDepth - 1;
|
|
572
|
+
for (var depth = startDepth; depth >= depthUpToInclusive; depth--) {
|
|
573
|
+
var sizeAtLevel = unflushedSizes[depth];
|
|
574
|
+
unflushedSizes[depth] = 0; // we are propagating it now
|
|
575
|
+
if (depth > 0) {
|
|
576
|
+
// propagate size upward, will be added lazily, either when a subtree is appended at or above that level or
|
|
577
|
+
// at the end of processing the entire side
|
|
578
|
+
unflushedSizes[depth - 1] += sizeAtLevel;
|
|
579
|
+
}
|
|
580
|
+
var node = spine[depth];
|
|
581
|
+
node._size += sizeAtLevel;
|
|
582
|
+
// No-op if left side, as max keys in parents are unchanged by appending to the beginning of a node
|
|
583
|
+
updateMax(node, maxKey);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
;
|
|
587
|
+
/**
|
|
588
|
+
* Update a spine (frontier) from a specific depth down, inclusive.
|
|
589
|
+
* Extends the frontier array if it is not already as long as the frontier.
|
|
590
|
+
*/
|
|
591
|
+
function updateFrontier(context, depthLastValid) {
|
|
592
|
+
var frontier = context.spine, sideIndex = context.sideIndex;
|
|
593
|
+
(0, b_tree_1.check)(frontier.length > depthLastValid, "updateFrontier: depthLastValid exceeds frontier height");
|
|
594
|
+
var startingAncestor = frontier[depthLastValid];
|
|
595
|
+
if (startingAncestor.isLeaf)
|
|
596
|
+
return;
|
|
597
|
+
var internal = startingAncestor;
|
|
598
|
+
var cur = internal.children[sideIndex(internal)];
|
|
599
|
+
var depth = depthLastValid + 1;
|
|
600
|
+
while (!cur.isLeaf) {
|
|
601
|
+
var ni = cur;
|
|
602
|
+
frontier[depth] = ni;
|
|
603
|
+
cur = ni.children[sideIndex(ni)];
|
|
604
|
+
depth++;
|
|
605
|
+
}
|
|
606
|
+
frontier[depth] = cur;
|
|
607
|
+
}
|
|
608
|
+
;
|
|
609
|
+
/**
|
|
610
|
+
* Find the first ancestor (starting at insertionDepth) with capacity.
|
|
611
|
+
*/
|
|
612
|
+
function findSplitCascadeEndDepth(context, insertionDepth, insertionCount) {
|
|
613
|
+
var spine = context.spine, branchingFactor = context.branchingFactor;
|
|
614
|
+
if (insertionDepth >= 0) {
|
|
615
|
+
var depth = insertionDepth;
|
|
616
|
+
if (spine[depth].keys.length + insertionCount <= branchingFactor) {
|
|
617
|
+
return depth;
|
|
618
|
+
}
|
|
619
|
+
depth--;
|
|
620
|
+
while (depth >= 0) {
|
|
621
|
+
if (spine[depth].keys.length < branchingFactor)
|
|
622
|
+
return depth;
|
|
623
|
+
depth--;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return -1; // no capacity, will need a new root
|
|
627
|
+
}
|
|
628
|
+
;
|
|
629
|
+
/**
|
|
630
|
+
* Inserts the child without updating cached size counts.
|
|
631
|
+
*/
|
|
632
|
+
function insertNoCount(parent, index, child) {
|
|
633
|
+
parent.children.splice(index, 0, child);
|
|
634
|
+
parent.keys.splice(index, 0, child.maxKey());
|
|
635
|
+
}
|
|
636
|
+
// ---- Side-specific delegates for merging subtrees into a frontier ----
|
|
637
|
+
function getLeftmostIndex() {
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
function getRightmostIndex(node) {
|
|
641
|
+
return node.children.length - 1;
|
|
642
|
+
}
|
|
643
|
+
function getRightInsertionIndex(node) {
|
|
644
|
+
return node.children.length;
|
|
645
|
+
}
|
|
646
|
+
function splitOffRightSide(node) {
|
|
647
|
+
return node.splitOffRightSide();
|
|
648
|
+
}
|
|
649
|
+
function splitOffLeftSide(node) {
|
|
650
|
+
return node.splitOffLeftSide();
|
|
651
|
+
}
|
|
652
|
+
function balanceLeavesRight(parent, underfilled, toTake) {
|
|
653
|
+
var siblingIndex = parent.children.length - 2;
|
|
654
|
+
var sibling = parent.children[siblingIndex];
|
|
655
|
+
var index = sibling.keys.length - toTake;
|
|
656
|
+
var movedKeys = sibling.keys.splice(index);
|
|
657
|
+
var movedValues = sibling.values.splice(index);
|
|
658
|
+
underfilled.keys.unshift.apply(underfilled.keys, movedKeys);
|
|
659
|
+
underfilled.values.unshift.apply(underfilled.values, movedValues);
|
|
660
|
+
parent.keys[siblingIndex] = sibling.maxKey();
|
|
661
|
+
}
|
|
662
|
+
function balanceLeavesLeft(parent, underfilled, toTake) {
|
|
663
|
+
var sibling = parent.children[1];
|
|
664
|
+
var movedKeys = sibling.keys.splice(0, toTake);
|
|
665
|
+
var movedValues = sibling.values.splice(0, toTake);
|
|
666
|
+
underfilled.keys.push.apply(underfilled.keys, movedKeys);
|
|
667
|
+
underfilled.values.push.apply(underfilled.values, movedValues);
|
|
668
|
+
parent.keys[0] = underfilled.maxKey();
|
|
669
|
+
}
|
|
670
|
+
function updateRightMax(node, maxBelow) {
|
|
671
|
+
node.keys[node.keys.length - 1] = maxBelow;
|
|
672
|
+
}
|
|
673
|
+
function mergeRightEntries(leaf, entries) {
|
|
674
|
+
leaf.keys.push.apply(leaf.keys, entries.keys);
|
|
675
|
+
leaf.values.push.apply(leaf.values, entries.values);
|
|
676
|
+
}
|
|
677
|
+
function mergeLeftEntries(leaf, entries) {
|
|
678
|
+
leaf.keys.unshift.apply(leaf.keys, entries.keys);
|
|
679
|
+
leaf.values.unshift.apply(leaf.values, entries.values);
|
|
680
|
+
}
|