diff --git a/package.json b/package.json index 109392b..eab8e1f 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "devDependencies": { "@aureooms/js-compare": "2.0.0", "@aureooms/js-itertools": "5.1.0", + "@aureooms/js-pseudo-random": "^2.0.0", "@aureooms/js-random": "2.0.0", "@babel/core": "7.13.14", "@babel/preset-env": "7.13.12", @@ -145,7 +146,7 @@ ] }, "cover": { - "sourceMaps": "inline", + "sourceMaps": "both", "presets": [ "babel-preset-power-assert" ] diff --git a/src/deletion/delete_case0.js b/src/deletion/delete_case0.js index 22e1ddb..9418d7a 100644 --- a/src/deletion/delete_case0.js +++ b/src/deletion/delete_case0.js @@ -10,6 +10,7 @@ import delete_case1 from './delete_case1.js'; * - all other root-leaf paths have a black height of b * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case0 = (n) => { assert(n instanceof Node); @@ -17,7 +18,7 @@ const delete_case0 = (n) => { // If n is the root, there is nothing to do: // - all paths go through n, and // - n is black. - if (n.parent !== null) delete_case1(n); + return n.parent === null ? n : delete_case1(n); }; export default delete_case0; diff --git a/src/deletion/delete_case1.js b/src/deletion/delete_case1.js index 06167e3..24c2000 100644 --- a/src/deletion/delete_case1.js +++ b/src/deletion/delete_case1.js @@ -17,6 +17,7 @@ import delete_case3 from './delete_case3.js'; * - n is not the root * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case1 = (n) => { assert(n instanceof Node); @@ -28,25 +29,25 @@ const delete_case1 = (n) => { if (s._color === BLACK) { // If n's sibling is BLACK, go to case 2. - delete_case2(n); - } else { - /** - * Otherwise, prepare for and go to case 3. - * - * B B - * / \ / \ - * >B R R B - * / \ / \ --> / \ / \ - * - - B B >B B = = - * / \ / \ / \ / \ - * = = = = - - = = - */ - n.parent._color = RED; - s._color = BLACK; - if (n === n.parent.left) rotate_left(n.parent); - else rotate_right(n.parent); - delete_case3(n); + return delete_case2(n); } + + /** + * Otherwise, prepare for and go to case 3. + * + * B *B + * / \ / \ + * >B R R B + * / \ / \ --> / \ / \ + * - - B B >B B = = + * / \ / \ / \ / \ + * = = = = - - = = + */ + n.parent._color = RED; + s._color = BLACK; + if (n === n.parent.left) rotate_left(n.parent); + else rotate_right(n.parent); + return delete_case3(n).parent; }; export default delete_case1; diff --git a/src/deletion/delete_case2.js b/src/deletion/delete_case2.js index 64bd5ed..df78148 100644 --- a/src/deletion/delete_case2.js +++ b/src/deletion/delete_case2.js @@ -16,6 +16,7 @@ import delete_case3 from './delete_case3.js'; * - n's sibling is black * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case2 = (n) => { assert(n instanceof Node); @@ -44,11 +45,11 @@ const delete_case2 = (n) => { (s.right === null || s.right._color === BLACK) ) { s._color = RED; - delete_case0(n.parent); + return delete_case0(n.parent); } // Otherwise, go to case 3. - else delete_case3(n); + return delete_case3(n); }; export default delete_case2; diff --git a/src/deletion/delete_case3.js b/src/deletion/delete_case3.js index dc02142..59203ac 100644 --- a/src/deletion/delete_case3.js +++ b/src/deletion/delete_case3.js @@ -16,6 +16,7 @@ import delete_case4 from './delete_case4.js'; * - n's parent and n's sibling's children cannot all be black * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case3 = (n) => { assert(n instanceof Node); @@ -52,10 +53,11 @@ const delete_case3 = (n) => { ) { s._color = RED; n.parent._color = BLACK; + return n.parent; } // Otherwise, go to case 4. - else delete_case4(n); + return delete_case4(n); }; export default delete_case3; diff --git a/src/deletion/delete_case4.js b/src/deletion/delete_case4.js index 285347c..d59e510 100644 --- a/src/deletion/delete_case4.js +++ b/src/deletion/delete_case4.js @@ -18,6 +18,7 @@ import delete_case5 from './delete_case5.js'; * - at least one of n's sibling's children is red * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case4 = (n) => { assert(n instanceof Node); @@ -32,18 +33,18 @@ const delete_case4 = (n) => { // the left of the left of the parent, or right of the right, so case 5 // will rotate correctly. - /** - * ? ? - * / \ / \ - * >B B >B B - * / \ / \ --> / \ / \ - * - - R B - - = R - * / \ / \ / \ - * = = - - = B - * / \ - * - - - */ if (n === n.parent.left && (s.right === null || s.right._color === BLACK)) { + /** + * ? ? + * / \ / \ + * >B B >B B + * / \ / \ --> / \ / \ + * - - R B - - = R + * / \ / \ / \ + * = = - - = B + * / \ + * - - + */ s._color = RED; s.left._color = BLACK; rotate_right(s); @@ -67,7 +68,9 @@ const delete_case4 = (n) => { rotate_left(s); } - delete_case5(n); + // TODO we could merge case 4 and 5 without too much trouble. + // It would allow to avoid checking n's direction twice. + return delete_case5(n); }; export default delete_case4; diff --git a/src/deletion/delete_case5.js b/src/deletion/delete_case5.js index 93e5eb2..e2d525a 100644 --- a/src/deletion/delete_case5.js +++ b/src/deletion/delete_case5.js @@ -17,6 +17,7 @@ import sibling from '../family/sibling.js'; * - if n is a right child, the left child of n's sibling is red * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const delete_case5 = (n) => { assert(n instanceof Node); @@ -28,8 +29,9 @@ const delete_case5 = (n) => { /** * Increment the black height of all root-leaf paths going through n by + * swapping the colors of n's parent and n's sibling and * rotating at n's parent. This decrements the black height of all - * root-leaft paths going through n's sibling's right child. + * root-leaf paths going through n's sibling's right child. * We can repaint n's sibling's right child in black to fix this. * We are done. * @@ -44,6 +46,7 @@ const delete_case5 = (n) => { * - - */ + // Swap the color of the parent and the sibling. s._color = n.parent._color; n.parent._color = BLACK; @@ -51,14 +54,15 @@ const delete_case5 = (n) => { assert(s.right._color === RED); s.right._color = BLACK; rotate_left(n.parent); + return s; } // Symmetric case - else { - assert(s.left._color === RED); - s.left._color = BLACK; - rotate_right(n.parent); - } + + assert(s.left._color === RED); + s.left._color = BLACK; + rotate_right(n.parent); + return s; }; export default delete_case5; diff --git a/src/deletion/delete_no_child.js b/src/deletion/delete_no_child.js index 0dd0820..0355eb0 100644 --- a/src/deletion/delete_no_child.js +++ b/src/deletion/delete_no_child.js @@ -16,6 +16,7 @@ import prune from './prune.js'; * - n is not the root * * @param {Node} n - The node to delete. + * @return {Node} The root of the modified subtree. */ const delete_no_child = (n) => { assert(n instanceof Node); @@ -26,7 +27,7 @@ const delete_no_child = (n) => { if (n._color !== BLACK) { assert(n._color === RED); prune(n); - return; + return n.parent; } // Mock leaf since there is no left child @@ -39,10 +40,11 @@ const delete_no_child = (n) => { // If n is black, deleting it reduces the black-height of every path going // through it by 1. The leaf is black, so there are more things to fix. - delete_case1(leaf); + const subtree = delete_case1(leaf); // Delete mocked leaf prune(leaf); + return subtree; }; export default delete_no_child; diff --git a/src/deletion/replace_node.js b/src/deletion/replace_node.js index 1f480f6..f862080 100644 --- a/src/deletion/replace_node.js +++ b/src/deletion/replace_node.js @@ -10,7 +10,7 @@ import Node from '../types/Node.js'; const replace_node = (A, B) => { assert(A instanceof Node); assert(B instanceof Node); - // We never apply delete_one_child or delete_no_child on the root + // We never apply replace_node on the root assert(A.parent !== null); if (A === A.parent.left) A.parent.left = B; diff --git a/src/index.js b/src/index.js index e3d1f23..f5637dc 100644 --- a/src/index.js +++ b/src/index.js @@ -26,6 +26,9 @@ export {default as insert_case4} from './insertion/insert_case4.js'; export {default as rotate_left} from './rotate/rotate_left.js'; export {default as rotate_right} from './rotate/rotate_right.js'; export {default as search} from './search/search.js'; +export {default as swap_color} from './swap/swap_color.js'; +export {default as swap_left} from './swap/swap_left.js'; +export {default as swap_non_adjacent} from './swap/swap_non_adjacent.js'; export {default as inordertraversal} from './traversal/inordertraversal.js'; export {default as rangetraversal} from './traversal/rangetraversal.js'; export {default as Node} from './types/Node.js'; diff --git a/src/insertion/insert_case0.js b/src/insertion/insert_case0.js index 923093e..a6049c0 100644 --- a/src/insertion/insert_case0.js +++ b/src/insertion/insert_case0.js @@ -10,6 +10,7 @@ import insert_case1 from './insert_case1.js'; * - n's children are BLACK * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const insert_case0 = (n) => { assert(n instanceof Node); @@ -23,8 +24,12 @@ const insert_case0 = (n) => { * / \ * - - */ - if (n.parent === null) n._color = BLACK; - else insert_case1(n); + if (n.parent === null) { + n._color = BLACK; + return n; + } + + return insert_case1(n); }; export default insert_case0; diff --git a/src/insertion/insert_case1.js b/src/insertion/insert_case1.js index 56180a1..86fd546 100644 --- a/src/insertion/insert_case1.js +++ b/src/insertion/insert_case1.js @@ -11,6 +11,7 @@ import insert_case2 from './insert_case2.js'; * - n is not the root of the tree. * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const insert_case1 = (n) => { assert(n instanceof Node); @@ -28,9 +29,7 @@ const insert_case1 = (n) => { * / \ * - - */ - if (n.parent._color === BLACK) return; - - insert_case2(n); + return n.parent._color === BLACK ? n : insert_case2(n); }; export default insert_case1; diff --git a/src/insertion/insert_case2.js b/src/insertion/insert_case2.js index 5de6a5d..b1bd649 100644 --- a/src/insertion/insert_case2.js +++ b/src/insertion/insert_case2.js @@ -15,6 +15,7 @@ import insert_case3 from './insert_case3.js'; * - n's parent is red. * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const insert_case2 = (n) => { assert(n instanceof Node); @@ -45,8 +46,10 @@ const insert_case2 = (n) => { u._color = BLACK; const g = grandparent(n); g._color = RED; - insert_case0(g); - } else insert_case3(n); + return insert_case0(g); + } + + return insert_case3(n); }; export default insert_case2; diff --git a/src/insertion/insert_case3.js b/src/insertion/insert_case3.js index fbde812..ddccbaa 100644 --- a/src/insertion/insert_case3.js +++ b/src/insertion/insert_case3.js @@ -4,7 +4,6 @@ import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import rotate_left from '../rotate/rotate_left.js'; import rotate_right from '../rotate/rotate_right.js'; -import grandparent from '../family/grandparent.js'; import insert_case4 from './insert_case4.js'; /** @@ -18,6 +17,7 @@ import insert_case4 from './insert_case4.js'; * Here we fix the input subtree to pass the preconditions of {@link insert_case4}. * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const insert_case3 = (n) => { assert(n instanceof Node); @@ -26,24 +26,24 @@ const insert_case3 = (n) => { assert(n.right === null || n.right._color === BLACK); assert(n.parent !== null); assert(n.parent._color === RED); - const g = grandparent(n); + const p = n.parent; + const g = p.parent; - /** - * If the path from g to n makes a left-right, change it to a left-left - * with {@link rotate_left}. Then call {@link insert_case4} on the old - * parent of n. - * - * B B - * / \ / \ - * R B R B - * / \ / \ --> / \ / \ - * = >R - - >R = - - - * / \ / \ - * = = = = - */ - - if (n === n.parent.right && n.parent === g.left) { - rotate_left(n.parent); + if (n === p.right && p === g.left) { + /** + * If the path from g to n makes a left-right, change it to a left-left + * with {@link rotate_left}. Then call {@link insert_case4} on the old + * parent of n. + * + * B B + * / \ / \ + * R B >R B + * / \ / \ --> / \ / \ + * = >R - - R = - - + * / \ / \ + * = = = = + */ + rotate_left(p); /** * Rotate_left can be the below because of already having *g = grandparent(n) @@ -57,7 +57,10 @@ const insert_case3 = (n) => { */ // n = n.left; /!\ need to fix rotate, so that we can safely reference a node - } else if (n === n.parent.left && n.parent === g.right) { + return insert_case4(p); + } + + if (n === p.left && p === g.right) { /** * If the path from g to n makes a right-left, change it to a right-right * with {@link rotate_right}. Then call {@link insert_case4} on the old @@ -65,13 +68,13 @@ const insert_case3 = (n) => { * * B B * / \ / \ - * B R B R + * B R B >R * / \ / \ --> / \ / \ - * - - >R = - - = >R + * - - >R = - - = R * / \ / \ * = = = = */ - rotate_right(n.parent); + rotate_right(p); /** * Rotate_right can be the below to take advantage of already having *g = grandparent(n) @@ -84,9 +87,10 @@ const insert_case3 = (n) => { */ // n = n.right ; + return insert_case4(p); } - insert_case4(n); + return insert_case4(n); }; export default insert_case3; diff --git a/src/insertion/insert_case4.js b/src/insertion/insert_case4.js index 14fff50..ace3358 100644 --- a/src/insertion/insert_case4.js +++ b/src/insertion/insert_case4.js @@ -16,6 +16,7 @@ import grandparent from '../family/grandparent.js'; * - the path from n to its grandparent makes a left-left or right-right. * * @param {Node} n - The input node. + * @return {Node} The root of the modified subtree. */ const insert_case4 = (n) => { assert(n instanceof Node); @@ -65,6 +66,8 @@ const insert_case4 = (n) => { assert(g.left === null || g.left._color === BLACK); rotate_left(g); } + + return n.parent; }; export default insert_case4; diff --git a/src/rotate/rotate_left.js b/src/rotate/rotate_left.js index c645022..ab721c8 100644 --- a/src/rotate/rotate_left.js +++ b/src/rotate/rotate_left.js @@ -3,13 +3,14 @@ import Node from '../types/Node.js'; /** * Rotate tree left. (see https://en.wikipedia.org/wiki/Tree_rotation) - * /!\ This swaps the references to A and B. * + * p p + * | | * A B * / \ / \ - * a B -> A c + * x B -> A y * / \ / \ - * b c a b + * b y x b * * * @param {Node} A - The root of the tree. @@ -20,22 +21,21 @@ const rotate_left = (A) => { assert(A instanceof Node); const B = A.right; assert(B instanceof Node); - const a = A.left; - const b = B.left; - const c = B.right; - [A.key, B.key] = [B.key, A.key]; - [A._color, B._color] = [B._color, A._color]; + const p = A.parent; + if (p !== null) { + if (A === p.left) p.left = B; + else p.right = B; + } - A.left = B; - A.right = c; + B.parent = p; + A.parent = B; - B.left = a; - B.right = b; + const b = B.left; + A.right = b; + B.left = A; - if (a !== null) a.parent = B; - if (b !== null) b.parent = B; - if (c !== null) c.parent = A; + if (b !== null) b.parent = A; }; export default rotate_left; diff --git a/src/rotate/rotate_right.js b/src/rotate/rotate_right.js index ff0ef26..0c5735d 100644 --- a/src/rotate/rotate_right.js +++ b/src/rotate/rotate_right.js @@ -3,13 +3,14 @@ import Node from '../types/Node.js'; /** * Rotate tree right. (see https://en.wikipedia.org/wiki/Tree_rotation) - * /!\ This swaps the references to A and B. * + * p p + * | | * B A * / \ / \ - * A c -> a B + * A y -> x B * / \ / \ - * a b b c + * x b b j * * * @param {Node} B - The root of the tree. @@ -20,22 +21,21 @@ const rotate_right = (B) => { assert(B instanceof Node); const A = B.left; assert(A instanceof Node); - const a = A.left; - const b = A.right; - const c = B.right; - [A.key, B.key] = [B.key, A.key]; - [A._color, B._color] = [B._color, A._color]; + const p = B.parent; + if (p !== null) { + if (B === p.left) p.left = A; + else p.right = A; + } - B.left = a; - B.right = A; + A.parent = p; + B.parent = A; - A.left = b; - A.right = c; + const b = A.right; + B.left = b; + A.right = B; - if (a !== null) a.parent = B; - if (b !== null) b.parent = A; - if (c !== null) c.parent = A; + if (b !== null) b.parent = B; }; export default rotate_right; diff --git a/src/swap/swap_color.js b/src/swap/swap_color.js new file mode 100644 index 0000000..a80264f --- /dev/null +++ b/src/swap/swap_color.js @@ -0,0 +1,22 @@ +import assert from 'assert'; +import Node from '../types/Node.js'; + +/** + * Swap colors of two arbitrary nodes. + * + * -A +B -> +A -B + * + * @param {Node} A - The first node. + * @param {Node} B - The second node. + */ + +const swap_color = (A, B) => { + assert(A instanceof Node); + assert(B instanceof Node); + + const color = A._color; + A._color = B._color; + B._color = color; +}; + +export default swap_color; diff --git a/src/swap/swap_left.js b/src/swap/swap_left.js new file mode 100644 index 0000000..a820088 --- /dev/null +++ b/src/swap/swap_left.js @@ -0,0 +1,51 @@ +import assert from 'assert'; +import Node from '../types/Node.js'; +import replace_node from '../deletion/replace_node.js'; +import swap_color from './swap_color.js'; + +/** + * Swap pointers and colors of a node and its left child. + * + * p p + * | | + * -A -B + * / \ / \ + * +B c -> +A c + * / \ / \ + * a b a b + * + * @param {Node} A - The node. + * @return {Node} The node B. + */ + +const swap_left = (A) => { + assert(A instanceof Node); + const B = A.left; + assert(B instanceof Node); + const a = B.left; + const b = B.right; + const c = A.right; + + if (A.parent === null) { + B.parent = null; + } else { + replace_node(A, B); + } + + A.parent = B; + + A.right = b; + if (b !== null) b.parent = A; + B.right = c; + if (c !== null) c.parent = B; + + A.left = a; + if (a !== null) a.parent = A; + B.left = A; + + swap_color(A, B); + + return B; +}; + +export default swap_left; diff --git a/src/swap/swap_non_adjacent.js b/src/swap/swap_non_adjacent.js new file mode 100644 index 0000000..e99e574 --- /dev/null +++ b/src/swap/swap_non_adjacent.js @@ -0,0 +1,53 @@ +import assert from 'assert'; +import Node from '../types/Node.js'; +import swap_color from './swap_color.js'; + +/** + * Swap pointers and colors of two NON-ADJACENT nodes. + * + * p q q p + * | | | | + * -A +B +A -B + * / \ / \ / \ / \ + * u v x y -> x y u v + * + * @param {Node} A - The first node. + * @param {Node} B - The second node. + */ + +const swap_non_adjacent = (A, B) => { + assert(A instanceof Node); + assert(B instanceof Node); + const p = A.parent; + const u = A.left; + const v = A.right; + const q = B.parent; + const x = B.left; + const y = B.right; + + if (p !== null) { + if (A === p.left) p.left = B; + else p.right = B; + } + + if (q !== null) { + if (B === q.right) q.right = A; + else q.left = A; + } + + A.parent = q; + A.left = x; + A.right = y; + B.parent = p; + B.left = u; + B.right = v; + + if (x !== null) x.parent = A; + if (y !== null) y.parent = A; + if (u !== null) u.parent = B; + if (v !== null) v.parent = B; + + swap_color(A, B); +}; + +export default swap_non_adjacent; diff --git a/src/types/RedBlackTree.js b/src/types/RedBlackTree.js index e161abc..241a678 100644 --- a/src/types/RedBlackTree.js +++ b/src/types/RedBlackTree.js @@ -4,12 +4,15 @@ import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import predecessor from '../family/predecessor.js'; import insert from '../insertion/insert.js'; -import insert_case1 from '../insertion/insert_case1.js'; +import insert_case2 from '../insertion/insert_case2.js'; import delete_one_child from '../deletion/delete_one_child.js'; import delete_no_child from '../deletion/delete_no_child.js'; import search from '../search/search.js'; import inordertraversal from '../traversal/inordertraversal.js'; import rangetraversal from '../traversal/rangetraversal.js'; +import replace_node from '../deletion/replace_node.js'; +import swap_non_adjacent from '../swap/swap_non_adjacent.js'; +import swap_left from '../swap/swap_left.js'; /** * A RedBlackTree with key-only nodes. @@ -42,15 +45,25 @@ export default class RedBlackTree { * Adds a key to the tree. * * @param {any} key - The key to add. + * @return {Node} The newly added node. */ add(key) { if (this.root === null) { this.root = new Node(BLACK, key); - } else { - const node = new Node(RED, key); - insert(this.compare, this.root, node); - insert_case1(node); + return this.root; + } + + const node = new Node(RED, key); + insert(this.compare, this.root, node); + assert(node.parent !== null); + if (node.parent._color !== BLACK) { + const subtree = insert_case2(node); + if (subtree.parent === null) { + this.root = subtree; + } } + + return node; } /** @@ -98,29 +111,80 @@ export default class RedBlackTree { _delete(node) { assert(node instanceof Node); if (node.left !== null) { - // Replace node's key with predecessor's key + // Swap node with its predecessor const pred = predecessor(node); - node.key = pred.key; // Delete predecessor node - // NOTE: this node can only have one non-leaf (left) child because - // of red-black tree invariant. - if (pred.left === null) { - delete_no_child(pred); + // NOTE: this node can have at most one non-leaf (left) child + // because of red-black tree invariant. + assert(pred.right === null); + if (pred === node.left) { + swap_left(node); } else { - delete_one_child(pred); + swap_non_adjacent(node, pred); + } + + assert(node.right === null); + if (node.left === null) { + const subtree = delete_no_child(node); + if (subtree.parent === null) { + this.root = subtree; + } else if (pred.parent === null) { + assert(node === this.root); + this.root = pred; + } + } else { + delete_one_child(node); + if (pred.parent === null) { + assert(node === this.root); + this.root = pred; + } } } else if (node.right !== null) { - // Replace node's key with successor's key - // NOTE: Since there is no left child, then there can only be one - // right child by the red-black tree invariant. + /** + * Swap node with its successor. + * + * NOTE: Since pred is a leaf, there can only by one node in the + * right subtree, succ, which is necessarily red, hence + * node is black. + * + * The configuration: + * + * (A) (B) (C) + * + * p p p + * | | | + * node (BLACK) succ (BLACK) succ (BLACK) + * / \ / \ / \ + * - succ (RED) -> - node (RED) -> - - + * / \ / \ + * - - - - + * + * NOTE: We take a shortcut and go directly from (A) to (C) + */ + assert(node.left === null); const succ = node.right; - node.key = succ.key; - // Delete successor node - delete_no_child(succ); + assert(succ._color === RED); + succ._color = BLACK; + if (node === this.root) { + assert(node.parent === null); + succ.parent = null; + this.root = succ; + } else { + replace_node(node, succ); + } } else if (node === this.root) { + assert(node.parent === null); + assert(node._color === BLACK); + assert(node.left === null); + assert(node.right === null); this.root = null; } else { - delete_no_child(node); + assert(node.left === null); + assert(node.right === null); + const subtree = delete_no_child(node); + if (subtree.parent === null) { + this.root = subtree; + } } } diff --git a/test/fixtures.js b/test/fixtures.js index bccac61..75e7658 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -4,3 +4,22 @@ export const increasing = (a, b) => _inc(a, b); increasing.step = 1; export const decreasing = (a, b) => _dec(a, b); decreasing.step = -1; + +import {_fisheryates, _shuffle} from '@aureooms/js-random'; +const _randint = (random) => (i, j) => i + Math.floor(random() * (j - i)); +import {splitmix64, nextFloat64} from '@aureooms/js-pseudo-random'; + +export const entropy = (seed) => { + const prng = splitmix64(seed); + const random = () => nextFloat64(prng); + const randint = _randint(random); + const sample = _fisheryates(randint); + const shuffle = _shuffle(sample); + return { + prng, + random, + randint, + sample, + shuffle, + }; +}; diff --git a/test/src/RedBlackTree/refs.js b/test/src/RedBlackTree/refs.js new file mode 100644 index 0000000..1cdf43e --- /dev/null +++ b/test/src/RedBlackTree/refs.js @@ -0,0 +1,62 @@ +import test from 'ava'; + +import {increasing, entropy} from '../../fixtures.js'; + +import {list, range, zip} from '@aureooms/js-itertools'; + +import {empty} from '../../../src/index.js'; + +const seed = [0, 17]; +const {shuffle} = entropy(seed); + +const randomItems = (n) => { + const items = list(range(n)); + shuffle(items, 0, n); + return items; +}; + +const macro = (t, items) => { + const refs = []; + const tree = empty(increasing); + for (const item of items) { + const ref = tree.add(item); + refs.push(ref); + } + + for (const [item, ref] of zip(items, refs)) { + t.is(ref.key, item); + if (ref.parent === null) { + t.is(ref, tree.root); + } else { + t.true(ref === ref.parent.left || ref === ref.parent.right); + } + } + + for (const [item, ref] of zip(items, refs)) { + t.is(ref.key, item); + if (ref.parent === null) { + t.is(ref, tree.root); + } else { + t.true(ref === ref.parent.left || ref === ref.parent.right); + } + + tree.remove(item); + } + + t.true(tree.isEmpty()); +}; + +macro.title = (title, items) => + title || + 'Test references with ' + + (items.length > 10 ? `${items.length} elements` : JSON.stringify(items)); + +test(macro, randomItems(2)); +test(macro, randomItems(3)); +test(macro, randomItems(4)); +test(macro, randomItems(5)); +test(macro, randomItems(10)); +test(macro, randomItems(20)); +test(macro, randomItems(100)); +test(macro, randomItems(1000)); +test(macro, randomItems(10000)); diff --git a/test/src/RedBlackTree/remove.js b/test/src/RedBlackTree/remove.js index ac5a6e4..f389f0a 100644 --- a/test/src/RedBlackTree/remove.js +++ b/test/src/RedBlackTree/remove.js @@ -1,10 +1,11 @@ import test from 'ava'; -import {increasing} from '../../fixtures.js'; +import {increasing, entropy} from '../../fixtures.js'; import {list, range, sorted, head, iter, exhaust} from '@aureooms/js-itertools'; -import {shuffle} from '@aureooms/js-random'; +const seed = [0, 17]; +const {shuffle} = entropy(seed); import {RedBlackTree, _debug} from '../../../src/index.js'; @@ -13,13 +14,8 @@ import {bgRed as red, bgBlack as black} from 'chalk'; const debug = _debug({red, black}); -test('RedBlackTree::remove', (t) => { - const n = 10000; - const reference = list(range(n)); - shuffle(reference, 0, n); - - // Const reference = [3,0,2,4,1]; - // const n = reference.length ; +const macro = (t, reference) => { + const n = reference.length; const tree = RedBlackTree.from(increasing, reference); t.deepEqual( @@ -28,8 +24,6 @@ test('RedBlackTree::remove', (t) => { 'tree contains n items', ); - // Console.log(reference); - const m = (n / 2) | 0; for (const i of range(m)) { const x = reference[i]; @@ -70,7 +64,24 @@ test('RedBlackTree::remove', (t) => { } t.deepEqual(list(tree), [], 'tree is empty'); -}); +}; + +macro.title = (title, reference) => + title || + 'Test RedBlackTree::remove with ' + + (reference.length > 10 + ? `${reference.length} elements` + : JSON.stringify(reference)); + +const n = 10000; +const huge_list = list(range(n)); +shuffle(huge_list, 0, n); +test(macro, huge_list); + +test(macro, [5, 3, 2, 6, 7, 8, 4, 1]); +test(macro, [5, 3, 2, 6, 7, 8, 4]); +test(macro, [5, 3, 2, 6, 7, 8]); +test(macro, [5, 3, 2, 6, 7]); test('delete root with right child', (t) => { const tree = new RedBlackTree(increasing); diff --git a/test/src/regression/deletion.js b/test/src/regression/deletion.js new file mode 100644 index 0000000..5dcb1a3 --- /dev/null +++ b/test/src/regression/deletion.js @@ -0,0 +1,12 @@ +import test from 'ava'; + +import {list} from '@aureooms/js-itertools'; +import {increasing} from '../../fixtures.js'; +import {RedBlackTree} from '../../../src/index.js'; + +test('Edge case of small list', (t) => { + const reference = [5, 3, 2, 6, 7, 8]; + const tree = RedBlackTree.from(increasing, reference); + t.true(tree.remove(3)); + t.deepEqual(list(tree), [2, 5, 6, 7, 8]); +}); diff --git a/test/src/regression/root-deletion.js b/test/src/regression/root-deletion.js new file mode 100644 index 0000000..d111225 --- /dev/null +++ b/test/src/regression/root-deletion.js @@ -0,0 +1,17 @@ +import test from 'ava'; + +import {increasing} from '../../fixtures.js'; +import {empty} from '../../../src/index.js'; + +test('root + pred', (t) => { + const tree = empty(increasing); + t.true(tree.isEmpty()); + tree.add(0); + t.false(tree.isEmpty()); + tree.add(-1); + t.false(tree.isEmpty()); + tree.remove(0); + t.false(tree.isEmpty()); + tree.remove(-1); + t.true(tree.isEmpty()); +}); diff --git a/test/src/regression/root-is-always-black.js b/test/src/regression/root-is-always-black.js new file mode 100644 index 0000000..8ebdf0b --- /dev/null +++ b/test/src/regression/root-is-always-black.js @@ -0,0 +1,17 @@ +import test from 'ava'; + +import {increasing} from '../../fixtures.js'; +import {empty} from '../../../src/index.js'; + +test('root + succ', (t) => { + const tree = empty(increasing); + t.true(tree.isEmpty()); + tree.add(0); + t.false(tree.isEmpty()); + tree.add(1); + t.false(tree.isEmpty()); + tree.remove(0); + t.false(tree.isEmpty()); + tree.remove(1); + t.true(tree.isEmpty()); +}); diff --git a/yarn.lock b/yarn.lock index 57308c7..f5cbaf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,11 +27,23 @@ "@aureooms/js-collections-deque" "^6.0.1" "@aureooms/js-error" "^5.0.2" +"@aureooms/js-pseudo-random@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aureooms/js-pseudo-random/-/js-pseudo-random-2.0.0.tgz#84e396cdf7d2c5d05c54c2e05f0aa4f69e107d4c" + integrity sha512-PdTQCoeo9npuLONdZme469xEbLyjWU5YjCl6DVa0NkiFDglx5Jka+Ojv6oNXkfdgjo/P/soqTDDK7ZKJDuo7dA== + dependencies: + "@aureooms/js-uint64" "^3.0.1" + "@aureooms/js-random@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@aureooms/js-random/-/js-random-2.0.0.tgz#f62c6954ed79bd9a520197125ee3cfbe14b00d72" integrity sha1-9ixpVO15vZpSAZcSXuPPvhSwDXI= +"@aureooms/js-uint64@^3.0.1": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@aureooms/js-uint64/-/js-uint64-3.1.0.tgz#abd5eba5392dc0503415c383b1deb3d09b5711b6" + integrity sha512-q38EIajp2Iiv+lLJjgFH9LVe5GnWnmhQPUOScPN4fmWywRJJ11qb5i5j1NbtmgRcs6EpymOk6bSRc3Jbh0OENw== + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"