Skip to content

Commit 80cfb27

Browse files
make-github-pseudonymous-againAurélien Ooms
authored and
Aurélien Ooms
committed
🚧 progress: First draft without explicit leaves.
Everything went as planned except that we needed to mock the child of the deleted node when it is an implicit leaf. The losses: This change puts additional load on: - delete_case{3,4,5}, - insert_case3, and - rotate_{left,right} This load comes from nullchecks before color checks and parent assignments. These checks should only be needed at the lowest levels of the fixed path because of the red-black tree properties. They could perhaps be circumvented entirely by adding additional mocked leaves to the sibling of the child of the deleted node. This should somehow be amortized by the facts that there is at most one deletion per node created and that each node now costs less to create (because of the allowed null pointers, see gains). If property access is always preceded by a nullcheck and if an explicit preceding nullcheck is somehow optimized away by the compiler, then perhaps nothing needs to be done. We have to profile this. The gains: - all Leaf children are replaced by null (>1/6 space save) - all isLeaf() calls are replaced by nullchecks: really faster? Should be since I assume each method call must be preceded by a null check internally? Also we should rewrite delete_one_child to completely avoid this mocking in case the deleted node is red. I left a TODO note about this. This is progress on #104.
1 parent 3dc424d commit 80cfb27

24 files changed

+87
-92
lines changed

mangle.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
"minify": {
33
"mangle": {
44
"properties": {
5-
"regex": "^(_color|isLeaf)$"
5+
"regex": "^_color$"
66
}
77
}
88
},
99
"props": {
1010
"props": {
11-
"$_color": "c",
12-
"$isLeaf": "L"
11+
"$_color": "c"
1312
}
1413
}
1514
}

src/debug/_debug.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import assert from 'assert';
22
import Node from '../types/Node.js';
3-
import Leaf from '../types/Leaf.js';
43
import BLACK from '../color/BLACK.js';
54

65
/**
@@ -14,15 +13,11 @@ const _debug = ({red, black}) => {
1413
* Recursively constructs a prettyprint string for the red-black tree rooted at
1514
* <code>root</code>.
1615
*
17-
* @param {Node|Leaf} root - The root of the tree.
16+
* @param {Node} root - The root of the tree.
1817
* @returns {string}
1918
*/
2019
const debug = (root) => {
21-
assert(root instanceof Node || root instanceof Leaf);
22-
if (root.isLeaf()) {
23-
assert(root instanceof Leaf);
24-
return black('L');
25-
}
20+
if (root === null) return black('L');
2621

2722
assert(root instanceof Node);
2823

src/deletion/delete_case2.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const delete_case2 = (n) => {
2525
assert(n.parent !== null);
2626

2727
const s = sibling(n);
28+
assert(s instanceof Node);
2829

2930
/**
3031
* If n's sibling is red, prepare for and go to case 4.

src/deletion/delete_case3.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ const delete_case3 = (n) => {
3434
* B >B
3535
* / \ / \
3636
* >B B B R
37-
* / \ / \ --> / \ / \
38-
* - - B B - - B B
37+
* / \ / \ --> / \ / \
38+
* - - B B - - B B
3939
* / \ / \ / \ / \
4040
* - - - - - - - -
4141
*/
4242
if (
4343
n.parent._color === BLACK &&
44-
s.left._color === BLACK &&
45-
s.right._color === BLACK
44+
(s.left === null || s.left._color === BLACK) &&
45+
(s.right === null || s.right._color === BLACK)
4646
) {
4747
s._color = RED;
4848
delete_case1(n.parent);

src/deletion/delete_case4.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ const delete_case4 = (n) => {
2626
assert(s instanceof Node);
2727
assert(s._color === BLACK);
2828
assert(
29-
n.parent._color === RED || s.left._color === RED || s.right._color === RED,
29+
n.parent._color === RED ||
30+
s.left?._color === RED ||
31+
s.right?._color === RED,
3032
);
3133

3234
/**
@@ -46,8 +48,8 @@ const delete_case4 = (n) => {
4648
if (
4749
// The parent color test is always true when coming from case 2
4850
n.parent._color === RED &&
49-
s.left._color === BLACK &&
50-
s.right._color === BLACK
51+
(s.left === null || s.left._color === BLACK) &&
52+
(s.right === null || s.right._color === BLACK)
5153
) {
5254
s._color = RED;
5355
n.parent._color = BLACK;

src/deletion/delete_case5.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const delete_case5 = (n) => {
2727
const s = sibling(n);
2828
assert(s instanceof Node);
2929
assert(s._color === BLACK);
30-
assert(s.left._color === RED || s.right._color === RED);
30+
assert(s.left?._color === RED || s.right?._color === RED);
3131

3232
// The following statements just force the red n's sibling child to be on
3333
// the left of the left of the parent, or right of the right, so case 6
@@ -44,11 +44,14 @@ const delete_case5 = (n) => {
4444
* / \
4545
* - -
4646
*/
47-
if (n === n.parent.left && s.right._color === BLACK) {
47+
if (n === n.parent.left && (s.right === null || s.right._color === BLACK)) {
4848
s._color = RED;
4949
s.left._color = BLACK;
5050
rotate_right(s);
51-
} else if (n === n.parent.right && s.left._color === BLACK) {
51+
} else if (
52+
n === n.parent.right &&
53+
(s.left === null || s.left._color === BLACK)
54+
) {
5255
/**
5356
* ? ?
5457
* / \ / \

src/deletion/delete_mocked_leaf.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import assert from 'assert';
2+
import Leaf from '../types/Leaf.js';
3+
4+
/**
5+
* Delete leaf L.
6+
*
7+
* @param {Leaf} L - The leaf to delete.
8+
*/
9+
const delete_mocked_leaf = (L) => {
10+
assert(L instanceof Leaf);
11+
assert(L.parent !== null);
12+
13+
if (L === L.parent.left) L.parent.left = null;
14+
else L.parent.right = null;
15+
};
16+
17+
export default delete_mocked_leaf;

src/deletion/delete_one_child.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Leaf from '../types/Leaf.js';
77
import replace_node from './replace_node.js';
88
import delete_case2 from './delete_case2.js';
99

10+
import delete_mocked_leaf from './delete_mocked_leaf.js';
11+
1012
/**
1113
* Delete a node <code>n</code> that has at most a single non-leaf child.
1214
*
@@ -25,9 +27,12 @@ const delete_one_child = (n) => {
2527
// The right child of n is always a LEAF because either n is a subtree
2628
// predecessor or it is the only child of its parent by the red-black tree
2729
// properties
28-
assert(n.right instanceof Leaf);
30+
assert(n.right === null);
31+
32+
// Mock leaf if there is no left child
33+
const child = n.left === null ? new Leaf(null) : n.left;
2934

30-
const child = n.left;
35+
// TODO skip creating mocked leaf if n._color === RED
3136

3237
// Replace n with its left child
3338
replace_node(n, child);
@@ -48,6 +53,8 @@ const delete_one_child = (n) => {
4853
// child suffices. This is a NO-OP.
4954
assert(child._color === BLACK);
5055
}
56+
57+
if (child instanceof Leaf) delete_mocked_leaf(child);
5158
};
5259

5360
export default delete_one_child;

src/family/predecessor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const predecessor = (node) => {
1212
assert(node.left instanceof Node);
1313
let pred = node.left;
1414

15-
while (!pred.right.isLeaf()) {
15+
while (pred.right !== null) {
1616
assert(pred.right instanceof Node);
1717
pred = pred.right;
1818
}

src/family/sibling.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import Leaf from '../types/Leaf.js';
66
* Computes the sibling of the input node.
77
*
88
* @param {Node|Leaf} node - The input node.
9-
* @returns {Node|Leaf}
9+
* @returns {Node}
1010
*/
1111
const sibling = (node) => {
1212
assert(node instanceof Node || node instanceof Leaf);
13-
// We only use this function when node HAS a sibling.
13+
// We only use this function when node HAS a non-leaf sibling.
1414
assert(node.parent !== null);
1515

1616
return node === node.parent.left ? node.parent.right : node.parent.left;

src/family/uncle.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import assert from 'assert';
22
import Node from '../types/Node.js';
3-
import Leaf from '../types/Leaf.js';
43
import grandparent from './grandparent.js';
54

65
/**
76
* Computes the uncle of the input node when the grandparent is guaranteed to
87
* exist.
98
*
109
* @param {Node} node - The input node.
11-
* @returns {Node|Leaf}
10+
* @returns {Node}
1211
*/
1312
const uncle = (node) => {
1413
assert(node instanceof Node);
1514
const g = grandparent(node);
1615
assert(g !== null);
17-
const u = node.parent === g.left ? g.right : g.left;
18-
assert(u instanceof Node || u instanceof Leaf);
19-
return u;
16+
return node.parent === g.left ? g.right : g.left;
2017
};
2118

2219
export default uncle;

src/insertion/insert.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const insert = (compare, A, B) => {
2222
if (compare(B.key, A.key) < 0) {
2323
const node = A.left;
2424

25-
if (node.isLeaf()) {
25+
if (node === null) {
2626
A.left = B;
2727
break;
2828
}
@@ -32,7 +32,7 @@ const insert = (compare, A, B) => {
3232
} else {
3333
const node = A.right;
3434

35-
if (node.isLeaf()) {
35+
if (node === null) {
3636
A.right = B;
3737
break;
3838
}

src/insertion/insert_case2.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import insert_case3 from './insert_case3.js';
1515
const insert_case2 = (n) => {
1616
assert(n instanceof Node);
1717
assert(n._color === RED);
18-
assert(n.left._color === BLACK);
19-
assert(n.right._color === BLACK);
18+
assert(n.left === null || n.left._color === BLACK);
19+
assert(n.right === null || n.right._color === BLACK);
2020
assert(n.parent !== null);
2121

2222
/**

src/insertion/insert_case3.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import insert_case4 from './insert_case4.js';
1919
const insert_case3 = (n) => {
2020
assert(n instanceof Node);
2121
assert(n._color === RED);
22-
assert(n.left._color === BLACK);
23-
assert(n.right._color === BLACK);
22+
assert(n.left === null || n.left._color === BLACK);
23+
assert(n.right === null || n.right._color === BLACK);
2424
assert(n.parent !== null);
2525
assert(n.parent._color === RED);
2626
const u = uncle(n);
@@ -39,7 +39,7 @@ const insert_case3 = (n) => {
3939
* - - - -
4040
*/
4141

42-
if (u._color === RED) {
42+
if (u !== null && u._color === RED) {
4343
n.parent._color = BLACK;
4444
u._color = BLACK;
4545
const g = grandparent(n);

src/insertion/insert_case4.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import insert_case5 from './insert_case5.js';
2222
const insert_case4 = (n) => {
2323
assert(n instanceof Node);
2424
assert(n._color === RED);
25-
assert(n.left._color === BLACK);
26-
assert(n.right._color === BLACK);
25+
assert(n.left === null || n.left._color === BLACK);
26+
assert(n.right === null || n.right._color === BLACK);
2727
assert(n.parent !== null);
2828
assert(n.parent._color === RED);
2929
const g = grandparent(n);

src/insertion/insert_case5.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import grandparent from '../family/grandparent.js';
2020
const insert_case5 = (n) => {
2121
assert(n instanceof Node);
2222
assert(n._color === RED);
23-
assert(n.left._color === BLACK);
24-
assert(n.right._color === BLACK);
23+
assert(n.left === null || n.left._color === BLACK);
24+
assert(n.right === null || n.right._color === BLACK);
2525
assert(n.parent !== null);
2626
assert(n.parent._color === RED);
2727
const g = grandparent(n);
@@ -45,7 +45,7 @@ const insert_case5 = (n) => {
4545
*/
4646
assert(g.left instanceof Node);
4747
assert(n === g.left.left);
48-
assert(g.right._color === BLACK);
48+
assert(g.right === null || g.right._color === BLACK);
4949
rotate_right(g);
5050
} else {
5151
/**
@@ -62,7 +62,7 @@ const insert_case5 = (n) => {
6262
*/
6363
assert(g.right instanceof Node);
6464
assert(n === g.right.right);
65-
assert(g.left._color === BLACK);
65+
assert(g.left === null || g.left._color === BLACK);
6666
rotate_left(g);
6767
}
6868
};

src/rotate/rotate_left.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ const rotate_left = (A) => {
3333
B.left = a;
3434
B.right = b;
3535

36-
a.parent = B;
37-
b.parent = B;
38-
c.parent = A;
36+
if (a !== null) a.parent = B;
37+
if (b !== null) b.parent = B;
38+
if (c !== null) c.parent = A;
3939
};
4040

4141
export default rotate_left;

src/rotate/rotate_right.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ const rotate_right = (B) => {
3333
A.left = b;
3434
A.right = c;
3535

36-
a.parent = B;
37-
b.parent = A;
38-
c.parent = A;
36+
if (a !== null) a.parent = B;
37+
if (b !== null) b.parent = A;
38+
if (c !== null) c.parent = A;
3939
};
4040

4141
export default rotate_right;

src/search/search.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@ const search = (compare, root, key) => {
1818
return root;
1919
}
2020

21-
const child = d < 0 ? root.left : root.right;
21+
root = d < 0 ? root.left : root.right;
2222

23-
if (child.isLeaf()) {
23+
if (root === null) {
2424
return null;
2525
}
26-
27-
assert(child instanceof Node);
28-
root = child;
2926
}
3027
};
3128

src/traversal/inordertraversal.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Node from '../types/Node.js';
99
*/
1010
export default function* inordertraversal(node) {
1111
assert(node instanceof Node);
12-
if (!node.left.isLeaf()) {
12+
if (node.left !== null) {
1313
// Yield the nodes on the left recursively. Those nodes are all smaller
1414
// than (or equal to) the current node by the binary search tree
1515
// properties.
@@ -20,7 +20,7 @@ export default function* inordertraversal(node) {
2020
// Yield the current node.
2121
yield node.key;
2222

23-
if (!node.right.isLeaf()) {
23+
if (node.right !== null) {
2424
// Yield the nodes on the right recursively. Those nodes are all larger
2525
// than (or equal to) the current node by the binary search tree
2626
// properties.

src/traversal/rangetraversal.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,27 @@ export default function* rangetraversal(compare, root, left, right) {
1515
if (compare(root.key, left) < 0) {
1616
// If the root lies to the left of the interval, we can discard the
1717
// entire left subtree.
18-
if (!root.right.isLeaf()) {
18+
if (root.right !== null) {
1919
assert(root.right instanceof Node);
2020
yield* rangetraversal(compare, root.right, left, right);
2121
}
2222
} else if (compare(root.key, right) >= 0) {
2323
// If the root lies to the right of the interval, we can discard the
2424
// entire right subtree.
25-
if (!root.left.isLeaf()) {
25+
if (root.left !== null) {
2626
assert(root.left instanceof Node);
2727
yield* rangetraversal(compare, root.left, left, right);
2828
}
2929
} else {
3030
// Otherwise just recurse on both subtrees and yield the root in
3131
// between.
32-
if (!root.left.isLeaf()) {
32+
if (root.left !== null) {
3333
assert(root.left instanceof Node);
3434
yield* rangetraversal(compare, root.left, left, right);
3535
}
3636

3737
yield root.key;
38-
if (!root.right.isLeaf()) {
38+
if (root.right !== null) {
3939
assert(root.right instanceof Node);
4040
yield* rangetraversal(compare, root.right, left, right);
4141
}

0 commit comments

Comments
 (0)