1

I'm trying to build an AVL tree, but was not able to find a lot of code examples, only theory.

My code has the implementation for all rotations, but when the tree is 1 sided, I lose half the tree.

Here is my code:

function buildTree(dataSet) {
  let root = null
  
  function rotateRight(node) {
    return rotate(node, true)
  }

  function rotateLeft(node) {
    return rotate(node, false)
  }

  function rotate(node, right) {
    const inputNodeSide = right ? "left" : "right"
    const targetNodeSide = right ? "right" : "left"
    const targetNode = node[inputNodeSide]
    const targetNodeChild = targetNode[targetNodeSide]
    
    targetNode[targetNodeSide] = node
    node[inputNodeSide] = targetNodeChild
    return targetNode // as this is at the top
  }

  function createNode(data) {
    return {
      data,
      left: null,
      right: null,
      get balance() {
        return workOutHeight(this.left) -
          workOutHeight(this.right)
      },
      get height() {
        return workOutHeight(this)
      },
    }
  } // END createNode

  function workOutHeight(node) {
    if (null === node) {
      return -1
    }
    return Math.max(workOutHeight(node.left),
      workOutHeight(node.right)) + 1
  }

  function avl(node) {
    const balanced = node.balance
    if (2 === balanced) {
      if (0 > node.left.balance) {
        node.left = rotateLeft(node.left);
      }
      return rotateRight(node);
    } else if (-2 === balanced) {
      if (0 < node.right.balance) {
        node.right = rotateRight(node.right);
      }
      return rotateLeft(node);
    }
    return node
  }

  this.add = function(data, perent) {
    perent = perent || root;
    if (null === perent) {
      return root = createNode(data);
    } else if (data < perent.data) {
      if (null === perent.left) {
        return perent.left = createNode(data);
      }
      this.add(data, perent.left)
      avl(perent)
    } else if (data > perent.data) {
      if (null === perent.right) {
        return perent.right = createNode(data);
      }
      this.add(data, perent.right)
      avl(perent)
    }
  } // END addData

  this.tree = function() {
    return JSON.parse(JSON.stringify(root))
  }

  if (Array.isArray(dataSet)) {
    dataSet.forEach(val => this.add(val))
  }

} // END buildTree

console.log(new buildTree([2, 6, 9, 4, 7, 0]).tree())

As you can see when running the snippet, the resulting tree only has 2 nodes, while there should be 6. Where did I go wrong?

trincot
  • 317,000
  • 35
  • 244
  • 286
Brian
  • 1,026
  • 1
  • 15
  • 25

1 Answers1

0

The problem is in the this.add method.

It calls avl ignoring the node that is returned by that call. This returned node should replace the node that was passed as argument. As you don't know where this returned node should be attached to, you should return it as the return value for this.add, and that means you should redesign that whole function to deal with this "return" feature.

Here is how it could be adapted:

    this.add = function(data, perent=root) {
        if (!perent) {
            return createNode(data);
        } else if(data < perent.data) {
            perent.left = this.add(data, perent.left);
        } else if(data > perent.data) {
            perent.right = this.add(data, perent.right);
        }
        return avl(perent);
    }

Note that I removed the assignment to root here. I would suggest to move that to the place where you make the initial call to this.add:

    if (Array.isArray(dataSet)) {
        dataSet.forEach(val => root=this.add(val));
    }

And now it will work.

Redesign

As JavaScript has since ECMAScript 2015 the class syntax, I would actually make use of that, and create a Tree and Node class. Private members can be created with the # syntax.

Also, it is not efficient to calculate the height of a node every moment you need to calculate the balance factor of a node. The nice thing of AVL trees is that when you know the balance factors of the nodes that are being rotated, you can derive from that their new balance factors -- without needing the actual heights.

Here is all of that put in code, with in addition an input box, with which you can add nodes to the tree. The tree is displayed with simple indentation format (with the root at the left side).

class Tree {
    #root = null;
    static #config = { 
        "2": { "0": [ 1, -1], "1": [ 0,  0], "2": [-1,  0] },
       "-2": { "0": [-1,  1],"-1": [ 0,  0],"-2": [ 1,  0] },
        "1": {"-1": [ 0, -2], "0": [ 0, -1], "1": [-1, -1] },
       "-1": { "1": [ 0,  2], "0": [ 0,  1],"-1": [ 1,  1] },
    };
    
    static #Node = class {
        constructor(data) {
            this.data = data;
            this.left = this.right = null;
            this.balance = 0;
        }

        add(data) {
            const side = data < this.data ? "left" : "right";
            let delta = data < this.data ? -1 : 1;
            if (!this[side]) {
                this[side] = new Tree.#Node(data);
            } else {
                let balance = Math.abs(this[side].balance);
                this[side] = this[side].add(data);
                if (balance || !this[side].balance) delta = 0;
            }
            this.balance += delta;
            return this.avl();
        }

        avl() {
            const balanced = this.balance;
            if (-2 === balanced) {
                if (0 < this.left.balance) {
                    this.left = this.left.rotateLeft();
                }
                return this.rotateRight(); 
            } else if (2 === balanced) {
                if (0 > this.right.balance) {
                    this.right = this.right.rotateRight();
                }
                return this.rotateLeft(); 
            }
            return this;
        }
        
        rotateRight() {
            return this.rotate("left", "right");
        }
        
        rotateLeft() {
            return this.rotate("right", "left");
        }

        rotate(inputSide, targetSide) {
            const target = this[inputSide];
            const child = target[targetSide];
            target[targetSide] = this;
            this[inputSide] = child;
            // Use a lookup table to determine how the balance is impacted by rotation
            [this.balance, target.balance] = Tree.#config[this.balance][target.balance];
            return target;
        }

        toString(indent="\n") {
            return   (this.right?.toString(indent + "  ") ?? "")
                   + indent + this.data
                   + (this.left?.toString(indent + "  ") ?? "");
        }
    }

    constructor(...values) {
        this.add(...values);
    }
    
    add(...values) {
        for (const data of values) {
            this.#root = this.#root?.add(data) ?? new Tree.#Node(data);
        }
    }
      
    toString() {
        return this.#root?.toString() ?? "<empty>";
    }    
}

const tree = new Tree;

// I/O handling
document.querySelector("button").addEventListener("click", () => {
    const input = document.querySelector("input");
    tree.add(+input.value);
    input.value = "";
    input.focus();
    document.querySelector("pre").textContent = tree;
});
<input type="number"><button>Add</button><br>
<pre></pre>
trincot
  • 317,000
  • 35
  • 244
  • 286