diff --git a/mangle.json b/mangle.json index 2b50a8f..fc409ca 100644 --- a/mangle.json +++ b/mangle.json @@ -2,14 +2,13 @@ "minify": { "mangle": { "properties": { - "regex": "^(_color|isLeaf)$" + "regex": "^_color$" } } }, "props": { "props": { - "$_color": "c", - "$isLeaf": "L" + "$_color": "c" } } } \ No newline at end of file diff --git a/package.json b/package.json index fd879b4..d88c7fb 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "build-gh-pages": "npm run build-docs", "ci:test": "npm run lint-config && npm run lint && npm run cover", "commit-msg": "commitlint --edit", - "cover": "c8 --all --src src --reporter=lcov npm test", + "cover": "NODE_ENV=cover c8 --all --src src --reporter=lcov npm test", "debug": "NODE_ENV=debug npm run test -- -st --fail-fast", "dev": "npm run lint-config-and-fix && npm run lint-and-fix && npm run cover -- -- -st --fail-fast", "install-hooks": "husky install", @@ -104,9 +104,18 @@ [ "@babel/preset-env", { - "targets": [ - "defaults", - "maintained node versions" + "targets": "current node" + } + ] + ], + "plugins": [ + [ + "transform-remove-console", + { + "exclude": [ + "log", + "error", + "warn" ] } ] @@ -114,23 +123,6 @@ "env": { "debug": { "presets": [ - [ - "@babel/preset-env", - { - "targets": "current node" - } - ], - "babel-preset-power-assert" - ] - }, - "test": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": "current node" - } - ], "babel-preset-power-assert" ], "plugins": [ @@ -138,6 +130,7 @@ "transform-remove-console", { "exclude": [ + "debug", "log", "error", "warn" @@ -146,36 +139,45 @@ ] ] }, - "development": { + "test": { "presets": [ "babel-preset-power-assert" - ], - "plugins": [ + ] + }, + "cover": { + "sourceMaps": "inline", + "presets": [ + "babel-preset-power-assert" + ] + }, + "development": { + "presets": [ [ - "transform-remove-console", + "@babel/preset-env", { - "exclude": [ - "log", - "error", - "warn" + "targets": [ + "defaults", + "maintained node versions" ] } - ] + ], + "babel-preset-power-assert" ] }, "production": { - "plugins": [ - "babel-plugin-unassert", + "presets": [ [ - "transform-remove-console", + "@babel/preset-env", { - "exclude": [ - "log", - "error", - "warn" + "targets": [ + "defaults", + "maintained node versions" ] } ] + ], + "plugins": [ + "babel-plugin-unassert" ] } } diff --git a/src/debug/_debug.js b/src/debug/_debug.js index 5dcbecf..75873a4 100644 --- a/src/debug/_debug.js +++ b/src/debug/_debug.js @@ -1,6 +1,5 @@ import assert from 'assert'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; import BLACK from '../color/BLACK.js'; /** @@ -14,15 +13,11 @@ const _debug = ({red, black}) => { * Recursively constructs a prettyprint string for the red-black tree rooted at * root. * - * @param {Node|Leaf} root - The root of the tree. + * @param {Node} root - The root of the tree. * @returns {string} */ const debug = (root) => { - assert(root instanceof Node || root instanceof Leaf); - if (root.isLeaf()) { - assert(root instanceof Leaf); - return black('L'); - } + if (root === null) return black('L'); assert(root instanceof Node); diff --git a/src/deletion/delete_case0.js b/src/deletion/delete_case0.js new file mode 100644 index 0000000..22e1ddb --- /dev/null +++ b/src/deletion/delete_case0.js @@ -0,0 +1,23 @@ +import assert from 'assert'; +import BLACK from '../color/BLACK.js'; +import Node from '../types/Node.js'; +import delete_case1 from './delete_case1.js'; + +/** + * Preconditions: + * - n is black + * - all root-leaf paths going through n have a black height of b - 1 + * - all other root-leaf paths have a black height of b + * + * @param {Node} n - The input node. + */ +const delete_case0 = (n) => { + assert(n instanceof Node); + assert(n._color === BLACK); + // 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); +}; + +export default delete_case0; diff --git a/src/deletion/delete_case1.js b/src/deletion/delete_case1.js index f1fbffa..3b08cb4 100644 --- a/src/deletion/delete_case1.js +++ b/src/deletion/delete_case1.js @@ -1,24 +1,52 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; +import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; +import rotate_left from '../rotate/rotate_left.js'; +import rotate_right from '../rotate/rotate_right.js'; +import sibling from '../family/sibling.js'; + import delete_case2 from './delete_case2.js'; +import delete_case3 from './delete_case3.js'; /** * Preconditions: * - n is black * - all root-leaf paths going through n have a black height of b - 1 * - all other root-leaf paths have a black height of b + * - n is not the root * - * @param {Node|Leaf} n - The input node. + * @param {Node} n - The input node. */ const delete_case1 = (n) => { - assert(n instanceof Node || n instanceof Leaf); + assert(n instanceof Node); assert(n._color === BLACK); - // If n is the root, there is nothing to do: - // - all paths go through n, and - // - n is black. - if (n.parent !== null) delete_case2(n); + assert(n.parent !== null); + + const s = sibling(n); + assert(s instanceof Node); + + /** + * If n's sibling is red, prepare for and go to case 4. + * + * B B + * / \ / \ + * >B R R B + * / \ / \ --> / \ / \ + * - - B B >B B = = + * / \ / \ / \ / \ + * = = = = - - = = + */ + if (s._color === RED) { + n.parent._color = RED; + s._color = BLACK; + if (n === n.parent.left) rotate_left(n.parent); + else rotate_right(n.parent); + delete_case3(n); + } + + // Otherwise, go to case 3. + else delete_case2(n); }; export default delete_case1; diff --git a/src/deletion/delete_case2.js b/src/deletion/delete_case2.js index f77604a..3e85560 100644 --- a/src/deletion/delete_case2.js +++ b/src/deletion/delete_case2.js @@ -2,13 +2,10 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; -import rotate_left from '../rotate/rotate_left.js'; -import rotate_right from '../rotate/rotate_right.js'; import sibling from '../family/sibling.js'; +import delete_case0 from './delete_case0.js'; import delete_case3 from './delete_case3.js'; -import delete_case4 from './delete_case4.js'; /** * Preconditions: @@ -16,36 +13,41 @@ import delete_case4 from './delete_case4.js'; * - all root-leaf paths going through n have a black height of b - 1 * - all other root-leaf paths have a black height of b * - n is not the root + * - n's sibling is black * - * @param {Node|Leaf} n - The input node. + * @param {Node} n - The input node. */ const delete_case2 = (n) => { - assert(n instanceof Node || n instanceof Leaf); + assert(n instanceof Node); assert(n._color === BLACK); assert(n.parent !== null); - const s = sibling(n); + assert(s instanceof Node); + assert(s._color === BLACK); /** - * If n's sibling is red, prepare for and go to case 4. + * If n's parent is black and n's sibling's children are black, then + * repaint n's sibling red. Now all root-leaf paths going through n's + * parent have a black height of b - 1. We recurse thus on n's parent. * - * B B + * B >B * / \ / \ - * >B R R B - * / \ / \ --> / \ / \ - * - - B B >B B = = - * / \ / \ / \ / \ - * = = = = - - = = + * >B B B R + * / \ / \ --> / \ / \ + * - - B B - - B B + * / \ / \ / \ / \ + * - - - - - - - - */ - if (s._color === RED) { - n.parent._color = RED; - s._color = BLACK; - if (n === n.parent.left) rotate_left(n.parent); - else rotate_right(n.parent); - delete_case4(n); + if ( + n.parent._color === BLACK && + (s.left === null || s.left._color === BLACK) && + (s.right === null || s.right._color === BLACK) + ) { + s._color = RED; + delete_case0(n.parent); } - // Otherwise, go to case 3. + // Otherwise, go to case 4. else delete_case3(n); }; diff --git a/src/deletion/delete_case3.js b/src/deletion/delete_case3.js index c8985ac..bbeb718 100644 --- a/src/deletion/delete_case3.js +++ b/src/deletion/delete_case3.js @@ -2,10 +2,8 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; import sibling from '../family/sibling.js'; -import delete_case1 from './delete_case1.js'; import delete_case4 from './delete_case4.js'; /** @@ -15,40 +13,48 @@ import delete_case4 from './delete_case4.js'; * - all other root-leaf paths have a black height of b * - n is not the root * - n's sibling is black + * - n's parent and n's sibling's children cannot all be black * - * @param {Node|Leaf} n - The input node. + * @param {Node} n - The input node. */ const delete_case3 = (n) => { - assert(n instanceof Node || n instanceof Leaf); + assert(n instanceof Node); assert(n._color === BLACK); assert(n.parent !== null); const s = sibling(n); assert(s instanceof Node); assert(s._color === BLACK); + assert( + n.parent._color === RED || + s.left?._color === RED || + s.right?._color === RED, + ); /** - * If n's parent is black and n's sibling's children are black, then - * repaint n's sibling red. Now all root-leaf paths going through n's - * parent have a black height of b - 1. We recurse thus on n's parent. + * If n's parent is red and n's sibling's children are black, then swap n's + * parent and n's sibling color. All root-leaf paths going through n have + * now a black height of b. All other root-leaf paths have their black + * height unchanged. Red-black properties are respected. We are done. * - * B >B + * R B * / \ / \ - * >B B B R - * / \ / \ --> / \ / \ - * - - B B - - B B - * / \ / \ / \ / \ - * - - - - - - - - + * >B B >B R + * / \ / \ --> / \ / \ + * - - B B - - B B + * / \ / \ / \ / \ + * - - - - - - - - */ if ( - n.parent._color === BLACK && - s.left._color === BLACK && - s.right._color === BLACK + // The parent color test is always true when coming from case 2 + n.parent._color === RED && + (s.left === null || s.left._color === BLACK) && + (s.right === null || s.right._color === BLACK) ) { s._color = RED; - delete_case1(n.parent); + n.parent._color = BLACK; } - // Otherwise, go to case 4. + // Otherwise, go to case 5. else delete_case4(n); }; diff --git a/src/deletion/delete_case4.js b/src/deletion/delete_case4.js index 713f57a..e57911d 100644 --- a/src/deletion/delete_case4.js +++ b/src/deletion/delete_case4.js @@ -2,7 +2,8 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; +import rotate_left from '../rotate/rotate_left.js'; +import rotate_right from '../rotate/rotate_right.js'; import sibling from '../family/sibling.js'; import delete_case5 from './delete_case5.js'; @@ -14,47 +15,59 @@ import delete_case5 from './delete_case5.js'; * - all other root-leaf paths have a black height of b * - n is not the root * - n's sibling is black - * - n's parent and n's sibling's children cannot all be black + * - at least one of n's sibling's children is red * - * @param {Node|Leaf} n - The input node. + * @param {Node} n - The input node. */ const delete_case4 = (n) => { - assert(n instanceof Node || n instanceof Leaf); + assert(n instanceof Node); assert(n._color === BLACK); assert(n.parent !== null); const s = sibling(n); assert(s instanceof Node); assert(s._color === BLACK); - assert( - n.parent._color === RED || s.left._color === RED || s.right._color === RED, - ); + assert(s.left?._color === RED || s.right?._color === RED); + + // The following statements just force the red n's sibling child to be on + // the left of the left of the parent, or right of the right, so case 6 + // will rotate correctly. /** - * If n's parent is red and n's sibling's children are black, then swap n's - * parent and n's sibling color. All root-leaf paths going through n have - * now a black height of b. All other root-leaf paths have their black - * height unchanged. Red-black properties are respected. We are done. - * - * R B + * ? ? * / \ / \ - * >B B >B R + * >B B >B B * / \ / \ --> / \ / \ - * - - B B - - B B - * / \ / \ / \ / \ - * - - - - - - - - + * - - R B - - = R + * / \ / \ / \ + * = = - - = B + * / \ + * - - */ - if ( - // The parent color test is always true when coming from case 2 - n.parent._color === RED && - s.left._color === BLACK && - s.right._color === BLACK + if (n === n.parent.left && (s.right === null || s.right._color === BLACK)) { + s._color = RED; + s.left._color = BLACK; + rotate_right(s); + } else if ( + n === n.parent.right && + (s.left === null || s.left._color === BLACK) ) { + /** + * ? ? + * / \ / \ + * B >B B >B + * / \ / \ --> / \ / \ + * B R - - R = - - + * / \ / \ / \ + * - - = = B = + * / \ + * - - + */ s._color = RED; - n.parent._color = BLACK; + s.right._color = BLACK; + rotate_left(s); } - // Otherwise, go to case 5. - else delete_case5(n); + delete_case5(n); }; export default delete_case4; diff --git a/src/deletion/delete_case5.js b/src/deletion/delete_case5.js index 1969d86..93e5eb2 100644 --- a/src/deletion/delete_case5.js +++ b/src/deletion/delete_case5.js @@ -2,13 +2,10 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; import rotate_left from '../rotate/rotate_left.js'; import rotate_right from '../rotate/rotate_right.js'; import sibling from '../family/sibling.js'; -import delete_case6 from './delete_case6.js'; - /** * Preconditions: * - n is black @@ -16,56 +13,52 @@ import delete_case6 from './delete_case6.js'; * - all other root-leaf paths have a black height of b * - n is not the root * - n's sibling is black - * - at least one of n's sibling's children is red + * - if n is a left child, the right child of n's sibling is red + * - if n is a right child, the left child of n's sibling is red * - * @param {Node|Leaf} n - The input node. + * @param {Node} n - The input node. */ const delete_case5 = (n) => { - assert(n instanceof Node || n instanceof Leaf); + assert(n instanceof Node); assert(n._color === BLACK); assert(n.parent !== null); const s = sibling(n); assert(s instanceof Node); assert(s._color === BLACK); - assert(s.left._color === RED || s.right._color === RED); - - // The following statements just force the red n's sibling child to be on - // the left of the left of the parent, or right of the right, so case 6 - // will rotate correctly. /** - * ? ? - * / \ / \ - * >B B >B B - * / \ / \ --> / \ / \ - * - - R B - - = R - * / \ / \ / \ - * = = - - = B - * / \ - * - - + * Increment the black height of all root-leaf paths going through n by + * rotating at n's parent. This decrements the black height of all + * root-leaft 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. + * + * ? ? + * / \ / \ + * >B B B B + * / \ / \ / \ / \ + * - - = R --> >B = = B + * / \ / \ / \ + * = B - - - - + * / \ + * - - */ - if (n === n.parent.left && s.right._color === BLACK) { - s._color = RED; - s.left._color = BLACK; - rotate_right(s); - } else if (n === n.parent.right && s.left._color === BLACK) { - /** - * ? ? - * / \ / \ - * B >B B >B - * / \ / \ --> / \ / \ - * B R - - R = - - - * / \ / \ / \ - * - - = = B = - * / \ - * - - - */ - s._color = RED; + + s._color = n.parent._color; + n.parent._color = BLACK; + + if (n === n.parent.left) { + assert(s.right._color === RED); s.right._color = BLACK; - rotate_left(s); + rotate_left(n.parent); } - delete_case6(n); + // Symmetric case + else { + assert(s.left._color === RED); + s.left._color = BLACK; + rotate_right(n.parent); + } }; export default delete_case5; diff --git a/src/deletion/delete_case6.js b/src/deletion/delete_case6.js deleted file mode 100644 index 9f5f2c4..0000000 --- a/src/deletion/delete_case6.js +++ /dev/null @@ -1,65 +0,0 @@ -import assert from 'assert'; -import BLACK from '../color/BLACK.js'; -import RED from '../color/RED.js'; -import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; -import rotate_left from '../rotate/rotate_left.js'; -import rotate_right from '../rotate/rotate_right.js'; -import sibling from '../family/sibling.js'; - -/** - * Preconditions: - * - n is black - * - all root-leaf paths going through n have a black height of b - 1 - * - all other root-leaf paths have a black height of b - * - n is not the root - * - n's sibling is black - * - if n is a left child, the right child of n's sibling is red - * - if n is a right child, the left child of n's sibling is red - * - * @param {Node|Leaf} n - The input node. - */ -const delete_case6 = (n) => { - assert(n instanceof Node || n instanceof Leaf); - assert(n._color === BLACK); - assert(n.parent !== null); - const s = sibling(n); - assert(s instanceof Node); - assert(s._color === BLACK); - - /** - * Increment the black height of all root-leaf paths going through n by - * rotating at n's parent. This decrements the black height of all - * root-leaft 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. - * - * ? ? - * / \ / \ - * >B B B B - * / \ / \ / \ / \ - * - - = R --> >B = = B - * / \ / \ / \ - * = B - - - - - * / \ - * - - - */ - - s._color = n.parent._color; - n.parent._color = BLACK; - - if (n === n.parent.left) { - assert(s.right._color === RED); - s.right._color = BLACK; - rotate_left(n.parent); - } - - // Symmetric case - else { - assert(s.left._color === RED); - s.left._color = BLACK; - rotate_right(n.parent); - } -}; - -export default delete_case6; diff --git a/src/deletion/delete_no_child.js b/src/deletion/delete_no_child.js new file mode 100644 index 0000000..7b00891 --- /dev/null +++ b/src/deletion/delete_no_child.js @@ -0,0 +1,49 @@ +import assert from 'assert'; +import BLACK from '../color/BLACK.js'; +import RED from '../color/RED.js'; +import Node from '../types/Node.js'; + +import replace_node from './replace_node.js'; +import delete_case1 from './delete_case1.js'; + +import prune from './prune.js'; + +/** + * Delete a node n that has no non-leaf child. + * + * Precondition: + * - n has no non-leaf child. + * - n is not the root + * + * @param {Node} n - The node to delete. + */ +const delete_no_child = (n) => { + assert(n instanceof Node); + assert(n.parent !== null); + assert(n.left === null); + assert(n.right === null); + + if (n._color === RED) { + prune(n); + return; + } + + assert(n._color === BLACK); + + // Mock leaf since there is no left child + // We use key = n.key to avoid mixing types, but this property is never + // accessed. + const leaf = new Node(BLACK, n.key); + + // Replace n with the mocked leaf + replace_node(n, leaf); + + // 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); + + // Delete mocked leaf + prune(leaf); +}; + +export default delete_no_child; diff --git a/src/deletion/delete_one_child.js b/src/deletion/delete_one_child.js index 382504e..5ab00f6 100644 --- a/src/deletion/delete_one_child.js +++ b/src/deletion/delete_one_child.js @@ -2,52 +2,39 @@ import assert from 'assert'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; import replace_node from './replace_node.js'; -import delete_case2 from './delete_case2.js'; /** - * Delete a node n that has at most a single non-leaf child. + * Delete a node n with one non-leaf left child and one leaf right + * child. * * Precondition: - * - n has at most one non-leaf child. + * - n has exactly one non-leaf child. * - n is not the root - * - if n has a non-leaf child, then it is its left child. + * - n's only non-leaf child is n's left child. * - hence, n's right child is a leaf + * - hence, n's left child is RED + * - hence, n is BLACK * * @param {Node} n - The node to delete. */ const delete_one_child = (n) => { assert(n instanceof Node); + assert(n._color === BLACK); assert(n.parent !== null); - // Precondition: n's right child is a leaf. - // The right child of n is always a LEAF because either n is a subtree - // predecessor or it is the only child of its parent by the red-black tree - // properties - assert(n.right instanceof Leaf); + assert(n.left instanceof Node); + assert(n.left._color === RED); + assert(n.right === null); const child = n.left; - - // Replace n with its left child - replace_node(n, child); - // If n is black, deleting it reduces the black-height of every path going // through it by 1. - if (n._color === BLACK) { - // We can easily fix this when its left child is an - // internal red node: change the color of the left child to black and - // replace n with it. - if (child._color === RED) child._color = BLACK; - // Otherwise, there are more things to fix. - else { - delete_case2(child); - } - } else { - // If n is red then its child can only be black. Replacing n with its - // child suffices. This is a NO-OP. - assert(child._color === BLACK); - } + // We can easily fix this when its only child is an + // internal RED node: change the color of the child to black and + // replace n with it. + replace_node(n, child); + child._color = BLACK; }; export default delete_one_child; diff --git a/src/deletion/prune.js b/src/deletion/prune.js new file mode 100644 index 0000000..74853ad --- /dev/null +++ b/src/deletion/prune.js @@ -0,0 +1,17 @@ +import assert from 'assert'; +import Node from '../types/Node.js'; + +/** + * Prune subtree rooted at input node. + * + * @param {Node} root - The root of the subtree to prune. + */ +const prune = (root) => { + assert(root instanceof Node); + assert(root.parent !== null); + + if (root === root.parent.left) root.parent.left = null; + else root.parent.right = null; +}; + +export default prune; diff --git a/src/deletion/replace_node.js b/src/deletion/replace_node.js index ae4c302..1f480f6 100644 --- a/src/deletion/replace_node.js +++ b/src/deletion/replace_node.js @@ -1,17 +1,16 @@ import assert from 'assert'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; /** * Replaces node A by node B. * * @param {Node} A - The node to replace. - * @param {Node|Leaf} B - The replacement node. + * @param {Node} B - The replacement node. */ const replace_node = (A, B) => { assert(A instanceof Node); - assert(B instanceof Node || B instanceof Leaf); - // We never apply delete_one_child on the root + assert(B instanceof Node); + // We never apply delete_one_child or delete_no_child on the root assert(A.parent !== null); if (A === A.parent.left) A.parent.left = B; diff --git a/src/family/predecessor.js b/src/family/predecessor.js index 27f8159..b62a3fe 100644 --- a/src/family/predecessor.js +++ b/src/family/predecessor.js @@ -12,7 +12,7 @@ const predecessor = (node) => { assert(node.left instanceof Node); let pred = node.left; - while (!pred.right.isLeaf()) { + while (pred.right !== null) { assert(pred.right instanceof Node); pred = pred.right; } diff --git a/src/family/sibling.js b/src/family/sibling.js index 4616602..7d40268 100644 --- a/src/family/sibling.js +++ b/src/family/sibling.js @@ -1,16 +1,15 @@ import assert from 'assert'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; /** * Computes the sibling of the input node. * - * @param {Node|Leaf} node - The input node. - * @returns {Node|Leaf} + * @param {Node} node - The input node. + * @returns {Node} */ const sibling = (node) => { - assert(node instanceof Node || node instanceof Leaf); - // We only use this function when node HAS a sibling. + assert(node instanceof Node); + // We only use this function when node HAS a non-leaf sibling. assert(node.parent !== null); return node === node.parent.left ? node.parent.right : node.parent.left; diff --git a/src/family/uncle.js b/src/family/uncle.js index 4bb1454..9698487 100644 --- a/src/family/uncle.js +++ b/src/family/uncle.js @@ -1,6 +1,5 @@ import assert from 'assert'; import Node from '../types/Node.js'; -import Leaf from '../types/Leaf.js'; import grandparent from './grandparent.js'; /** @@ -8,15 +7,13 @@ import grandparent from './grandparent.js'; * exist. * * @param {Node} node - The input node. - * @returns {Node|Leaf} + * @returns {Node} */ const uncle = (node) => { assert(node instanceof Node); const g = grandparent(node); assert(g !== null); - const u = node.parent === g.left ? g.right : g.left; - assert(u instanceof Node || u instanceof Leaf); - return u; + return node.parent === g.left ? g.right : g.left; }; export default uncle; diff --git a/src/index.js b/src/index.js index 0c764a6..e3d1f23 100644 --- a/src/index.js +++ b/src/index.js @@ -3,29 +3,30 @@ export {default as from} from './api/from.js'; export {default as BLACK} from './color/BLACK.js'; export {default as RED} from './color/RED.js'; export {default as _debug} from './debug/_debug.js'; +export {default as delete_case0} from './deletion/delete_case0.js'; export {default as delete_case1} from './deletion/delete_case1.js'; export {default as delete_case2} from './deletion/delete_case2.js'; export {default as delete_case3} from './deletion/delete_case3.js'; export {default as delete_case4} from './deletion/delete_case4.js'; export {default as delete_case5} from './deletion/delete_case5.js'; -export {default as delete_case6} from './deletion/delete_case6.js'; +export {default as delete_no_child} from './deletion/delete_no_child.js'; export {default as delete_one_child} from './deletion/delete_one_child.js'; +export {default as prune} from './deletion/prune.js'; export {default as replace_node} from './deletion/replace_node.js'; export {default as grandparent} from './family/grandparent.js'; export {default as predecessor} from './family/predecessor.js'; export {default as sibling} from './family/sibling.js'; export {default as uncle} from './family/uncle.js'; export {default as insert} from './insertion/insert.js'; +export {default as insert_case0} from './insertion/insert_case0.js'; export {default as insert_case1} from './insertion/insert_case1.js'; export {default as insert_case2} from './insertion/insert_case2.js'; export {default as insert_case3} from './insertion/insert_case3.js'; export {default as insert_case4} from './insertion/insert_case4.js'; -export {default as insert_case5} from './insertion/insert_case5.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 inordertraversal} from './traversal/inordertraversal.js'; export {default as rangetraversal} from './traversal/rangetraversal.js'; -export {default as Leaf} from './types/Leaf.js'; export {default as Node} from './types/Node.js'; export {default as RedBlackTree} from './types/RedBlackTree.js'; diff --git a/src/insertion/insert.js b/src/insertion/insert.js index 854f58d..f41d3c6 100644 --- a/src/insertion/insert.js +++ b/src/insertion/insert.js @@ -10,7 +10,7 @@ import Node from '../types/Node.js'; * search tree. * For our red-black tree, all that is left to do is fix the red-black tree * properties in case they have been violated by this insertion. This is fixed - * by {@link insert_case1}. + * by {@link insert_case0}. * * @param {Function} compare - The comparison function to use. * @param {Node} A - The root of the tree. @@ -22,7 +22,7 @@ const insert = (compare, A, B) => { if (compare(B.key, A.key) < 0) { const node = A.left; - if (node.isLeaf()) { + if (node === null) { A.left = B; break; } @@ -32,7 +32,7 @@ const insert = (compare, A, B) => { } else { const node = A.right; - if (node.isLeaf()) { + if (node === null) { A.right = B; break; } diff --git a/src/insertion/insert_case0.js b/src/insertion/insert_case0.js new file mode 100644 index 0000000..923093e --- /dev/null +++ b/src/insertion/insert_case0.js @@ -0,0 +1,30 @@ +import assert from 'assert'; +import Node from '../types/Node.js'; +import BLACK from '../color/BLACK.js'; +import RED from '../color/RED.js'; +import insert_case1 from './insert_case1.js'; + +/** + * Preconditions: + * - n is red. + * - n's children are BLACK + * + * @param {Node} n - The input node. + */ +const insert_case0 = (n) => { + assert(n instanceof Node); + assert(n._color === RED); + assert(n.left._color === BLACK); + assert(n.right._color === BLACK); + /** + * If n is the root of the tree, paint it black and we are done. + * + * >R + * / \ + * - - + */ + if (n.parent === null) n._color = BLACK; + else insert_case1(n); +}; + +export default insert_case0; diff --git a/src/insertion/insert_case1.js b/src/insertion/insert_case1.js index 1b0f2bf..56180a1 100644 --- a/src/insertion/insert_case1.js +++ b/src/insertion/insert_case1.js @@ -8,23 +8,29 @@ import insert_case2 from './insert_case2.js'; * Preconditions: * - n is red. * - n's children are BLACK + * - n is not the root of the tree. * * @param {Node} n - The input node. */ const insert_case1 = (n) => { assert(n instanceof Node); assert(n._color === RED); - assert(n.left._color === BLACK); - assert(n.right._color === BLACK); + assert(n.left === null || n.left._color === BLACK); + assert(n.right === null || n.right._color === BLACK); + assert(n.parent !== null); + /** - * If n is the root of the tree, paint it black and we are done. + * If the parent of n is black then we have nothing to do. * - * >R + * B + * / \ + * >R - * / \ * - - */ - if (n.parent === null) n._color = BLACK; - else insert_case2(n); + if (n.parent._color === BLACK) return; + + insert_case2(n); }; export default insert_case1; diff --git a/src/insertion/insert_case2.js b/src/insertion/insert_case2.js index 0f45433..22b522c 100644 --- a/src/insertion/insert_case2.js +++ b/src/insertion/insert_case2.js @@ -2,6 +2,9 @@ import assert from 'assert'; import Node from '../types/Node.js'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; +import uncle from '../family/uncle.js'; +import grandparent from '../family/grandparent.js'; +import insert_case0 from './insert_case0.js'; import insert_case3 from './insert_case3.js'; /** @@ -9,28 +12,40 @@ import insert_case3 from './insert_case3.js'; * - n is red. * - n's children are BLACK * - n is not the root of the tree. + * - n's parent is red. * * @param {Node} n - The input node. */ const insert_case2 = (n) => { assert(n instanceof Node); assert(n._color === RED); - assert(n.left._color === BLACK); - assert(n.right._color === BLACK); + assert(n.left === null || n.left._color === BLACK); + assert(n.right === null || n.right._color === BLACK); assert(n.parent !== null); + assert(n.parent._color === RED); + const u = uncle(n); /** - * If the parent of n is black then we have nothing to do. + * If n has a non-leaf uncle and this uncle is red then we simply + * repaint the parent and the uncle of n in black, the grandparent of + * n in red, then call insert_case0 on n's grandparent. * - * B - * / \ - * >R - - * / \ - * - - + * B >R + * / \ / \ + * R R B B + * / \ / \ --> / \ / \ + * >R - - - R - - - + * / \ / \ + * - - - - */ - if (n.parent._color === BLACK) return; - insert_case3(n); + if (u !== null && u._color === RED) { + n.parent._color = BLACK; + u._color = BLACK; + const g = grandparent(n); + g._color = RED; + insert_case0(g); + } else insert_case3(n); }; export default insert_case2; diff --git a/src/insertion/insert_case3.js b/src/insertion/insert_case3.js index 5d9b8bc..fbde812 100644 --- a/src/insertion/insert_case3.js +++ b/src/insertion/insert_case3.js @@ -2,9 +2,9 @@ import assert from 'assert'; import Node from '../types/Node.js'; import BLACK from '../color/BLACK.js'; import RED from '../color/RED.js'; -import uncle from '../family/uncle.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_case1 from './insert_case1.js'; import insert_case4 from './insert_case4.js'; /** @@ -13,39 +13,80 @@ import insert_case4 from './insert_case4.js'; * - n's children are BLACK * - n is not the root of the tree. * - n's parent is red. + * - n's uncle is black. + * + * Here we fix the input subtree to pass the preconditions of {@link insert_case4}. * * @param {Node} n - The input node. */ const insert_case3 = (n) => { assert(n instanceof Node); assert(n._color === RED); - assert(n.left._color === BLACK); - assert(n.right._color === BLACK); + assert(n.left === null || n.left._color === BLACK); + assert(n.right === null || n.right._color === BLACK); assert(n.parent !== null); assert(n.parent._color === RED); - const u = uncle(n); + const g = grandparent(n); /** - * If n has a non-leaf uncle and this uncle is red then we simply - * repaint the parent and the uncle of n in black, the grandparent of - * n in red, then call insert_case1 on n's grandparent. + * 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 >R + * B B * / \ / \ - * R R B B + * R B R B * / \ / \ --> / \ / \ - * >R - - - R - - - - * / \ / \ - * - - - - + * = >R - - >R = - - + * / \ / \ + * = = = = */ - if (u._color === RED) { - n.parent._color = BLACK; - u._color = BLACK; - const g = grandparent(n); - g._color = RED; - insert_case1(g); - } else insert_case4(n); + if (n === n.parent.right && n.parent === g.left) { + rotate_left(n.parent); + + /** + * Rotate_left can be the below because of already having *g = grandparent(n) + * + * saved_p=g.left, *saved_left_n=n.left; + * g.left=n; + * n.left=saved_p; + * saved_p.right=saved_left_n; + * + * and modify the parent's nodes properly + */ + + // 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) { + /** + * 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 + * parent of n. + * + * B B + * / \ / \ + * B R B R + * / \ / \ --> / \ / \ + * - - >R = - - = >R + * / \ / \ + * = = = = + */ + rotate_right(n.parent); + + /** + * Rotate_right can be the below to take advantage of already having *g = grandparent(n) + * + * saved_p=g.right, *saved_right_n=n.right; + * g.right=n; + * n.right=saved_p; + * saved_p.left=saved_right_n; + * + */ + + // n = n.right ; + } + + insert_case4(n); }; export default insert_case3; diff --git a/src/insertion/insert_case4.js b/src/insertion/insert_case4.js index 97505f0..14fff50 100644 --- a/src/insertion/insert_case4.js +++ b/src/insertion/insert_case4.js @@ -5,7 +5,6 @@ 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_case5 from './insert_case5.js'; /** * Preconditions: @@ -14,79 +13,58 @@ import insert_case5 from './insert_case5.js'; * - n is not the root of the tree. * - n's parent is red. * - n's uncle is black. - * - * Here we fix the input subtree to pass the preconditions of {@link insert_case5}. + * - the path from n to its grandparent makes a left-left or right-right. * * @param {Node} n - The input node. */ const insert_case4 = (n) => { assert(n instanceof Node); assert(n._color === RED); - assert(n.left._color === BLACK); - assert(n.right._color === BLACK); + assert(n.left === null || n.left._color === BLACK); + assert(n.right === null || n.right._color === BLACK); assert(n.parent !== null); assert(n.parent._color === RED); const g = grandparent(n); - /** - * 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_case5} 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); - - /** - * Rotate_left can be the below because of already having *g = grandparent(n) - * - * saved_p=g.left, *saved_left_n=n.left; - * g.left=n; - * n.left=saved_p; - * saved_p.right=saved_left_n; - * - * and modify the parent's nodes properly - */ + // Repaint n's parent black, n's grandparent red + n.parent._color = BLACK; + g._color = RED; - // 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) { + if (n === n.parent.left) { /** - * 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_case5} on the old - * parent of n. + * If the path from g to n makes a left-left, {@link rotate_right} at g. + * We are done. * - * B B + * R B * / \ / \ - * B R B R + * B B >R R * / \ / \ --> / \ / \ - * - - >R = - - = >R - * / \ / \ - * = = = = + * >R = - - = = = B + * / \ / \ + * = = - - */ - rotate_right(n.parent); - + assert(g.left instanceof Node); + assert(n === g.left.left); + assert(g.right === null || g.right._color === BLACK); + rotate_right(g); + } else { /** - * Rotate_right can be the below to take advantage of already having *g = grandparent(n) - * - * saved_p=g.right, *saved_right_n=n.right; - * g.right=n; - * n.right=saved_p; - * saved_p.left=saved_right_n; + * If the path from g to n makes a right-right, {@link rotate_left} at g. + * We are done. * + * R B + * / \ / \ + * B B R >R + * / \ / \ --> / \ / \ + * - - = >R B = = = + * / \ / \ + * = = - - */ - - // n = n.right ; + assert(g.right instanceof Node); + assert(n === g.right.right); + assert(g.left === null || g.left._color === BLACK); + rotate_left(g); } - - insert_case5(n); }; export default insert_case4; diff --git a/src/insertion/insert_case5.js b/src/insertion/insert_case5.js deleted file mode 100644 index 1aa439d..0000000 --- a/src/insertion/insert_case5.js +++ /dev/null @@ -1,70 +0,0 @@ -import assert from 'assert'; -import Node from '../types/Node.js'; -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'; - -/** - * Preconditions: - * - n is red. - * - n's children are BLACK - * - n is not the root of the tree. - * - n's parent is red. - * - n's uncle is black. - * - the path from n to its grandparent makes a left-left or right-right. - * - * @param {Node} n - The input node. - */ -const insert_case5 = (n) => { - assert(n instanceof Node); - assert(n._color === RED); - assert(n.left._color === BLACK); - assert(n.right._color === BLACK); - assert(n.parent !== null); - assert(n.parent._color === RED); - const g = grandparent(n); - - // Repaint n's parent black, n's grandparent red - n.parent._color = BLACK; - g._color = RED; - - if (n === n.parent.left) { - /** - * If the path from g to n makes a left-left, {@link rotate_right} at g. - * We are done. - * - * R B - * / \ / \ - * B B >R R - * / \ / \ --> / \ / \ - * >R = - - = = = B - * / \ / \ - * = = - - - */ - assert(g.left instanceof Node); - assert(n === g.left.left); - assert(g.right._color === BLACK); - rotate_right(g); - } else { - /** - * If the path from g to n makes a right-right, {@link rotate_left} at g. - * We are done. - * - * R B - * / \ / \ - * B B R >R - * / \ / \ --> / \ / \ - * - - = >R B = = = - * / \ / \ - * = = - - - */ - assert(g.right instanceof Node); - assert(n === g.right.right); - assert(g.left._color === BLACK); - rotate_left(g); - } -}; - -export default insert_case5; diff --git a/src/rotate/rotate_left.js b/src/rotate/rotate_left.js index 51bf7ea..c645022 100644 --- a/src/rotate/rotate_left.js +++ b/src/rotate/rotate_left.js @@ -33,9 +33,9 @@ const rotate_left = (A) => { B.left = a; B.right = b; - a.parent = B; - b.parent = B; - c.parent = A; + if (a !== null) a.parent = B; + if (b !== null) b.parent = B; + if (c !== null) c.parent = A; }; export default rotate_left; diff --git a/src/rotate/rotate_right.js b/src/rotate/rotate_right.js index 9ad8cdb..ff0ef26 100644 --- a/src/rotate/rotate_right.js +++ b/src/rotate/rotate_right.js @@ -33,9 +33,9 @@ const rotate_right = (B) => { A.left = b; A.right = c; - a.parent = B; - b.parent = A; - c.parent = A; + if (a !== null) a.parent = B; + if (b !== null) b.parent = A; + if (c !== null) c.parent = A; }; export default rotate_right; diff --git a/src/search/search.js b/src/search/search.js index a400739..37de5b2 100644 --- a/src/search/search.js +++ b/src/search/search.js @@ -18,14 +18,11 @@ const search = (compare, root, key) => { return root; } - const child = d < 0 ? root.left : root.right; + root = d < 0 ? root.left : root.right; - if (child.isLeaf()) { + if (root === null) { return null; } - - assert(child instanceof Node); - root = child; } }; diff --git a/src/traversal/inordertraversal.js b/src/traversal/inordertraversal.js index 398ab56..a0c3451 100644 --- a/src/traversal/inordertraversal.js +++ b/src/traversal/inordertraversal.js @@ -9,7 +9,7 @@ import Node from '../types/Node.js'; */ export default function* inordertraversal(node) { assert(node instanceof Node); - if (!node.left.isLeaf()) { + if (node.left !== null) { // Yield the nodes on the left recursively. Those nodes are all smaller // than (or equal to) the current node by the binary search tree // properties. @@ -20,7 +20,7 @@ export default function* inordertraversal(node) { // Yield the current node. yield node.key; - if (!node.right.isLeaf()) { + if (node.right !== null) { // Yield the nodes on the right recursively. Those nodes are all larger // than (or equal to) the current node by the binary search tree // properties. diff --git a/src/traversal/rangetraversal.js b/src/traversal/rangetraversal.js index d26307b..c4db2c7 100644 --- a/src/traversal/rangetraversal.js +++ b/src/traversal/rangetraversal.js @@ -15,27 +15,27 @@ export default function* rangetraversal(compare, root, left, right) { if (compare(root.key, left) < 0) { // If the root lies to the left of the interval, we can discard the // entire left subtree. - if (!root.right.isLeaf()) { + if (root.right !== null) { assert(root.right instanceof Node); yield* rangetraversal(compare, root.right, left, right); } } else if (compare(root.key, right) >= 0) { // If the root lies to the right of the interval, we can discard the // entire right subtree. - if (!root.left.isLeaf()) { + if (root.left !== null) { assert(root.left instanceof Node); yield* rangetraversal(compare, root.left, left, right); } } else { // Otherwise just recurse on both subtrees and yield the root in // between. - if (!root.left.isLeaf()) { + if (root.left !== null) { assert(root.left instanceof Node); yield* rangetraversal(compare, root.left, left, right); } yield root.key; - if (!root.right.isLeaf()) { + if (root.right !== null) { assert(root.right instanceof Node); yield* rangetraversal(compare, root.right, left, right); } diff --git a/src/types/Leaf.js b/src/types/Leaf.js deleted file mode 100644 index 3e8a0a1..0000000 --- a/src/types/Leaf.js +++ /dev/null @@ -1,27 +0,0 @@ -import assert from 'assert'; -import BLACK from '../color/BLACK.js'; -import Node from './Node.js'; - -/** - * A black leaf node. - * - * @class - * @param {Node} parent - The parent node in the tree. - */ -export default function Leaf(parent) { - assert(parent instanceof Node); - /** @constant {number} The color of the node. */ - this._color = BLACK; - /** @member {Node} The parent node. */ - this.parent = parent; -} - -/** - * Returns true if the Leaf object is a leaf. This - * always returns true. - * - * @returns {boolean} - */ -Leaf.prototype.isLeaf = function () { - return true; -}; diff --git a/src/types/Node.js b/src/types/Node.js index e4bc1aa..c74cd66 100644 --- a/src/types/Node.js +++ b/src/types/Node.js @@ -1,5 +1,3 @@ -import Leaf from './Leaf.js'; - /** * An internal node. This node can be red or black. * @@ -10,22 +8,12 @@ import Leaf from './Leaf.js'; export default function Node(color, key) { /** @member {number} The color of the node. */ this._color = color; - /** @member {Node|Leaf} The left child */ - this.left = new Leaf(this); - /** @member {Node|Leaf} The right child */ - this.right = new Leaf(this); + /** @member {Node} The left child */ + this.left = null; + /** @member {Node} The right child */ + this.right = null; /** @member {Node} The parent node. */ this.parent = null; /** @member {any} The key held by this node. */ this.key = key; } - -/** - * Returns true if the Node object is a leaf. This - * always returns false. - * - * @returns {boolean} - */ -Node.prototype.isLeaf = function () { - return false; -}; diff --git a/src/types/RedBlackTree.js b/src/types/RedBlackTree.js index e0ec5b0..e161abc 100644 --- a/src/types/RedBlackTree.js +++ b/src/types/RedBlackTree.js @@ -1,12 +1,12 @@ import assert from 'assert'; import Node from './Node.js'; -import Leaf from './Leaf.js'; 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_case2 from '../insertion/insert_case2.js'; +import insert_case1 from '../insertion/insert_case1.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'; @@ -22,6 +22,7 @@ export default class RedBlackTree { * @param {Function} compare - The comparison function for node keys. */ constructor(compare) { + assert(compare instanceof Function); /** @member {Function} The comparison function for node keys. */ this.compare = compare; /** @member {Node} The root of the tree. */ @@ -48,7 +49,7 @@ export default class RedBlackTree { } else { const node = new Node(RED, key); insert(this.compare, this.root, node); - insert_case2(node); + insert_case1(node); } } @@ -95,31 +96,31 @@ export default class RedBlackTree { * @param {Node} node - The input node to delete. */ _delete(node) { - if (!node.left.isLeaf()) { + assert(node instanceof Node); + if (node.left !== null) { // Replace node's key with predecessor's key const pred = predecessor(node); node.key = pred.key; // Delete predecessor node - // note: this node can only have one non-leaf child - // because the tree is a red-black tree - delete_one_child(pred); - } else if (!node.right.isLeaf()) { + // 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); + } else { + delete_one_child(pred); + } + } else if (node.right !== null) { // Replace node's key with successor's key - // If there is no left child, then there can only be one right - // child. + // NOTE: Since there is no left child, then there can only be one + // right child by the red-black tree invariant. const succ = node.right; - assert(succ instanceof Node); - assert(succ.left instanceof Leaf); - assert(succ.right instanceof Leaf); node.key = succ.key; // Delete successor node - // note: this node can only have one non-leaf child - // because the tree is a red-black tree - delete_one_child(succ); + delete_no_child(succ); } else if (node === this.root) { this.root = null; } else { - delete_one_child(node); + delete_no_child(node); } }