Explain how JavaScript implements a binary search tree?

How does JavaScript implement a binary search tree? The following article will introduce you the method of implementing binary search tree using JavaScript. There is a certain reference value, friends in need can refer to it, I hope to help everyone.

JavaScript

One of the most commonly used and discussed data structures in computer science is the binary search tree . This is usually the first data structure introduced with a non-linear interpolation algorithm. A binary search tree is similar to a double-linked list, with each node containing some data and two pointers to other nodes; they differ in the way these nodes are related to each other. The pointers of the binary search tree nodes are commonly called “left” and “right” to indicate the subtree related to the current value. A simple JavaScript implementation of such a node is as follows:

var node = {
    value: 125,
    left: null,
    right: null
};

As can be seen from the name, the binary search tree is organized into a tree structure of layers. The first item becomes the root node, and each additional value is added to the tree as an ancestor of that root. However, the values ​​on the nodes of the binary search tree are unique, and they are sorted according to the values ​​they contain: the value of the left subtree as the node is always smaller than the value of the node, and the value in the right subtree is greater than the value of the node. In this way, finding the value in the binary search tree becomes very simple, as long as the value you are looking for is smaller than the node being processed, it will go to the left, and if the value is larger, it will move to the right. There must be no duplicates in a binary search tree, because duplicates break this relationship. The following figure represents a simple binary search tree.

search tree

The figure above shows a binary search tree with a root value of 8. When the value 3 is added, it becomes the left child of the root, because 3 is less than 8. When the value 1 is added, it becomes the left child of 3 because 1 is less than 8 (so left) and then 1 is less than 3 (and then left). When the value 10 is added, it becomes the right child node because 10 is greater than 8. Use this process to continue processing values ​​6, 4, 7, 14, and 13. The depth of this binary search tree is 3, which means that the node furthest from the root is three nodes.

Binary search trees end in a naturally sorted order, so they can be used to quickly find data because you can eliminate the possibility of each step immediately. You can search faster by limiting the number of nodes you need to find. Suppose you want to find the value 6 in the tree above. Starting at the root, determine that 6 is less than 8, so go to the left child of the root. Since 6 is greater than 3, you will go to the right node. You will find the correct value. So you only need to visit three rather than nine nodes to find this value.

To implement a binary search tree in JavaScript, the first step is to define the basic interface:

function BinarySearchTree () {
    this._root = null;
}

BinarySearchTree.prototype = {

    // restore constructor
    constructor: BinarySearchTree,

    add: function (value) {
    },

    contains: function (value) {
    },

    remove: function (value) {
    },

    size: function () {
    },

    toArray: function () {
    },

    toString: function () {
    }

};

Basic connection is similar to other data structures, there are methods to add and delete values. I also added some convenience methods size(), toArray()and toString(), they are useful for JavaScript.

To master the method uses a binary search tree, the best from the contains()start method. contains()The method accepts a value as a parameter and returns if the value exists in the tree true, otherwise it returns false. This method follows a basic binary search algorithm to determine if the value exists:

BinarySearchTree.prototype = {

    // more code

    contains: function (value) {
        var found = false,
            current = this._root

        // make sure there's a node to search
        while (! found && current) {

            // if the value is less than the current node's, go left
            if (value <current.value) {
                current = current.left;

            // if the value is greater than the current node's, go right
            } else if (value> current.value) {
                current = current.right;

            // values ​​are equal, found it!
            } else {
                found = true;
            }
        }

        // only proceed if the node was found
        return found;
    },

    // more code

};

The search starts at the root of the tree. If no data is added, there may be no roots, so you must check. Traversing the tree follows the simple algorithm discussed earlier: if the value to be found is less than the current node, it moves to the left, and if the value is greater, it moves to the right. Cover every time currentthe pointer, until I find the value (in this case foundset true) or there are no more nodes in that direction (in this case, the value is not in the tree).

In the contains()method of use it can also be used to insert new values in the tree. The main difference is that you are looking for where to put the new value, rather than looking for the value in the tree:

BinarySearchTree.prototype = {

    // more code

    add: function (value) {
        // create a new item object, place data in
        var node = {
                value: value,
                left: null,
                right: null
            },

            // used to traverse the structure
            current;

        // special case: no items in the tree yet
        if (this._root === null) {
            this._root = node;
        } else {
            current = this._root;

            while (true) {

                // if the new value is less than this node's value, go left
                if (value <current.value) {

                    // if there's no left, then the new node belongs there
                    if (current.left === null) {
                        current.left = node;
                        break;
                    } else {
                        current = current.left;
                    }

                // if the new value is greater than this node's value, go right
                } else if (value> current.value) {

                    // if there's no right, then the new node belongs there
                    if (current.right === null) {
                        current.right = node;
                        break;
                    } else {
                        current = current.right;
                    }       

                // if the new value is equal to the current one, just ignore
                } else {
                    break;
                }
            }
        }
    },

    // more code

};

When adding values ​​to a binary search tree, the special case is when there is no root. In this case, simply setting the root to the new value makes the job easy. In other cases, the basic algorithm and contains()substantially the same as used in the algorithm: new value less than the current node to the left, to the right if the value is greater. The main difference is that when you can’t move on, this is where the new value is. So if you need to move to the left without a left node, the new value will become the left node (same as the right node). Because there are no duplicates, if a node with the same value is found, the operation stops.

In continuing the discussion size()before the method, I would like to discuss in depth the tree traversal. In order to calculate the size of the binary search tree, each node in the tree must be visited. Binary search trees usually have different types of traversal methods, the most commonly used is an ordered traversal. Ordered traversal is performed on each node by processing the left subtree, then the node itself, and then the right subtree. Since the binary search tree is sorted this way, from left to right, the result is that the nodes are processed in the correct sort order. For the size()method, the order of node traversal does not really matter, but its toArray()method is very important. Since both methods require the implementation of traversal, I decided to add a can of generic traverse()method:

BinarySearchTree.prototype = {

    // more code

    traverse: function (process) {

        // helper function
        function inOrder (node) {
            if (node) {

                // traverse the left subtree
                if (node.left! == null) {
                    inOrder (node.left);
                }            

                // call the process method on this node
                process.call (this, node);

                // traverse the right subtree
                if (node.right! == null) {
                    inOrder (node.right);
                }
            }
        }

        // start with the root
        inOrder (this._root);
    },

    // more code

};

This method accepts one parameter process, which is a function that should be run on each node in the tree. This defines a method named inOrder()auxiliary function recursively traverse the tree. Note that if the current node exists, the recursion moves only left and right (to avoid multiple processing null). Then traverse()method begins sequentially traversed from the root, process()the function of each processing node. You can then use this method to achieve size(), toArray(), toString():

BinarySearchTree.prototype = {

    // more code

    size: function () {
        var length = 0;

        this.traverse (function (node) {
            length ++;
        });

        return length;
    },

    toArray: function () {
        var result = [];

        this.traverse (function (node) {
            result.push (node.value);
        });

        return result;
    },

    toString: function () {
        return this.toArray (). toString ();
    },

    // more code

};

size()And toArray()calls the traverse()method and pass in a function to run on each node. In the use of size()the case, the function simply increments the variable length, and toArray()using the function values of the nodes to add to the array. toString()In the method invocation toArray()before the return of the array to a string and returns.

When deleting a node, you need to determine if it is the root node. The root node is handled similarly to other nodes, but the obvious exception is that the root node needs to be set to a different value at the end. For simplicity, this will be considered a special case in JavaScript code.

The first step in deleting a node is to determine if the node exists:

BinarySearchTree.prototype = {

    // more code here

    remove: function (value) {

        var found = false,
            parent = null,
            current = this._root,
            childCount,
            replacement,
            replacementParent;

        // make sure there's a node to search
        while (! found && current) {

            // if the value is less than the current node's, go left
            if (value <current.value) {
                parent = current;
                current = current.left;

            // if the value is greater than the current node's, go right
            } else if (value> current.value) {
                parent = current;
                current = current.right;

            // values ​​are equal, found it!
            } else {
                found = true;
            }
        }

        // only proceed if the node was found
        if (found) {
            // continue
        }

    },
    // more code here

};

remove()The first part of the method uses binary search to locate the node to be deleted. If the value is less than the current node, it moves to the left, and if the value is greater than the current node, it moves to the right. When traversing also keeps track of parentthe node, because ultimately you need to remove the node from its parent. When foundequal true, the currentvalue of the node to be removed.

There are three conditions to be aware of when deleting nodes:

  1. Leaf node
  2. Node with only one child
  3. Node with two children

Removing content other than leaf nodes from a binary search tree means that values ​​must be moved to properly sort the tree. The first two are relatively simple to implement, removing only one leaf node, deleting a node with a child node and replacing it with its child nodes. The last case is a bit complicated for later access.

Before you learn how to delete a node, you need to know how many child nodes exist on the node. Once you know it, you must determine if the node is the root node, leaving a fairly simple decision tree:

BinarySearchTree.prototype = {

    // more code here

    remove: function (value) {

        var found = false,
            parent = null,
            current = this._root,
            childCount,
            replacement,
            replacementParent;

        // find the node (removed for space)

        // only proceed if the node was found
        if (found) {

            // figure out how many children
            childCount = (current.left! == null? 1: 0) + 
                         (current.right! == null? 1: 0);

            // special case: the value is at the root
            if (current === this._root) {
                switch (childCount) {

                    // no children, just erase the root
                    case 0:
                        this._root = null;
                        break;

                    // one child, use one as the root
                    case 1:
                        this._root = (current.right === null? 
                                      current.left: current.right);
                        break;

                    // two children, little work to do
                    case 2:

                        // TODO

                    // no default

                }        

            // non-root values
            } else {

                switch (childCount) {

                    // no children, just remove it from the parent
                    case 0:
                        // if the current value is less than its 
                        // parent's, null out the left pointer
                        if (current.value <parent.value) {
                            parent.left = null;

                        // if the current value is greater than its
                        // parent's, null out the right pointer
                        } else {
                            parent.right = null;
                        }
                        break;

                    // one child, just reassign to parent
                    case 1:
                        // if the current value is less than its 
                        // parent's, reset the left pointer
                        if (current.value <parent.value) {
                            parent.left = (current.left === null? 
                                           current.right: current.left);

                        // if the current value is greater than its 
                        // parent's, reset the right pointer
                        } else {
                            parent.right = (current.left === null? 
                                            current.right: current.left);
                        }
                        break;    

                    // two children, a bit more complicated
                    case 2:

                        // TODO          

                    // no default

                }

            }

        }

    },

    // more code here

};

When dealing with the root node, this is a simple process that covers it. For non-root nodes, must be removed in accordance with the set value of the node parentcorresponding pointer: if the value is less than the deleted parent node, the leftpointer must be reset null (for node has no children), or the delete node leftpointer; if removed value is greater than the parent must be righta pointer reset nullor deleted node rightpointer.

As mentioned earlier, deleting a node with two children is the most complicated operation. Consider the following representation of a binary search tree.

binary search tree

The root is 8 and the left child is 3. What happens if 3 is deleted? There are two possibilities: 1 (the child on the left of 3, called the ordered predecessor) or 4 (the leftmost child on the right subtree, called the ordered successor) can replace 3.

Either of these two options is appropriate. To find the ordered predecessor, that is, the value before the value is deleted, check the left subtree of the node to be deleted and select the rightmost child node; find the ordered successor, the value that appears immediately after the value is deleted, and reverse the process And check the leftmost right subtree. Each of them requires another traversal of the tree to complete the operation:

BinarySearchTree.prototype = {

    // more code here

    remove: function (value) {

        var found = false,
            parent = null,
            current = this._root,
            childCount,
            replacement,
            replacementParent;

        // find the node (removed for space)

        // only proceed if the node was found
        if (found) {

            // figure out how many children
            childCount = (current.left! == null? 1: 0) + 
                         (current.right! == null? 1: 0);

            // special case: the value is at the root
            if (current === this._root) {
                switch (childCount) {

                    // other cases removed to save space

                    // two children, little work to do
                    case 2:

                        // new root will be the old root's left child
                        //...maybe
                        replacement = this._root.left;

                        // find the right-most leaf node to be 
                        // the real new root
                        while (replacement.right! == null) {
                            replacementParent = replacement;
                            replacement = replacement.right;
                        }

                        // it's not the first node on the left
                        if (replacementParent! == null) {

                            // remove the new root from it's 
                            // previous position
                            replacementParent.right = replacement.left;

                            // give the new root all of the old 
                            // root's children
                            replacement.right = this._root.right;
                            replacement.left = this._root.left;
                        } else {

                            // just assign the children
                            replacement.right = this._root.right;
                        }

                        // officially assign new root
                        this._root = replacement;

                    // no default

                }        

            // non-root values
            } else {

                switch (childCount) {

                    // other cases removed to save space 

                    // two children, a bit more complicated
                    case 2:

                        // reset pointers for new traversal
                        replacement = current.left;
                        replacementParent = current;

                        // find the right-most node
                        while (replacement.right! == null) {
                            replacementParent = replacement;
                            replacement = replacement.right;
                        }

                        replacementParent.right = replacement.left;

                        // assign children to the replacement
                        replacement.right = current.right;
                        replacement.left = current.left;

                        // place the replacement in the right spot
                        if (current.value <parent.value) {
                            parent.left = replacement;
                        } else {
                            parent.right = replacement;
                        }          

                    // no default

                }

            }

        }

    },

    // more code here

};

The code for root and non-root nodes with two children is almost the same. This implementation always looks for an ordered precursor by looking at the left subtree and looking for the rightmost child node. Traversal using whilecycle replacementand replacementParentvariable done. replacementAlternatively eventually become a node in currentthe node, thus by its parent rightto replace a pointer is provided leftto remove it from the current pointer position. For the root node, when the replacementimmediate child of the root node, the replacementParentwill null, and therefore replacementthe rightpointer is set to only the root rightpointer. The final step is to assign the replacement node to the correct location. For the root node, the new root replacement setting; for non-root nodes, are assigned to replace the original parentproper position.

A note on this implementation: Always replacing nodes with ordered precursors can lead to an unbalanced tree, most of which will be on one side of the tree. Imbalanced trees mean that search efficiency is low, so they should be of interest in practical scenarios. In a binary search tree implementation, it is necessary to determine whether to use an ordered predecessor or an ordered successor to keep the tree properly balanced (commonly known as a self-balancing binary search tree).

Source Code

Github: https://github.com/humanwhocodes/computer-science-in-javascript

#javascript #binary-search-tree

Explain how JavaScript implements a binary search tree?
1 Likes31.40 GEEK