Tree data structure part 3/5: extracting useful information out of tree using Loop, Stack and Queue

*Prerequisite: Tree data structure part 2/5: balanced/unbalanced Binary Search Tree.

This tutorial is a part of the Data Structures and Algorithms class:

Extracting useful information out of trees

Motivations from real life applications and coding interview questions:

bst sample problems

... endless number of questions, is there a simple way to solve these questions? The short answer is yes, there's one pattern to solve them (all). It turns out to answer (most of) these questions, we need to visit all nodes + change the order of visiting according to required logic. So:

  1. To visit all nodes: use loop (for, while, ...) or recursion.
  2. To control the order of which node to visit first:

    • for loop: use (single/multiple) Stack/Queue or a combination of both.
    • for recursion: change the position of the recursion call.

Difference of Human vs. Computer when solving problems

human vs computer solver

Human thought Human action Equivalent computer execution
looking at data see the whole tree at a time look at one node at a time + apply logic to only current node + use loop/recursion to visit all nodes
node selection freely select nodes as asked use Stack or Queue or change position of recursive call to control the order of node selection

While to see all nodes, Queue to control the order of visit

Queue allows us to visit nodes layer by layer, closest layer first (Breadth First Search).

while loop and queue

lecture-11/while-loop-and-queue.js
// structure of a node
class Node {
  constructor(data) {
    this.data = data
    this.left = null
    this.right = null
  }
}

// Binary Tree
class BinaryTree {
  constructor(data) {
    this.root = data ? new Node(data) : null
  }
}

// create new binary tree
const binaryTree = new BinaryTree(6)
binaryTree.root.left = new Node(4)
binaryTree.root.left.left = new Node(1)
binaryTree.root.left.right = new Node(5)
binaryTree.root.right = new Node(8)
binaryTree.root.right.right = new Node(10)

console.log(JSON.stringify(binaryTree, null, 4))

const traverseWithWhileAndQueue = (root) => { // aka Breadth First Search
  const queue = [root]
  const result = []
  let loopCount = 1

  console.log('initial queue data: ', queue[0].data)

  while(queue.length > 0) {
    console.log('loop #:', loopCount)
    loopCount++

    const queueData = queue.map(e => e.data )
    console.log('queue data: ', queueData)

    const currentNode = queue.shift() // dequeue

    currentNode.left && queue.push(currentNode.left) // enqueue
    currentNode.right && queue.push(currentNode.right) // enqueue

    result.push(currentNode.data) // process current node data
    console.log('result: ', result)
    console.log('- - - - - - - - -')
  }
}

traverseWithWhileAndQueue(binaryTree.root)
$ node while-loop-and-queue.js
{
    "root": {
        "data": 6,
        "left": {
            "data": 4,
            "left": {
                "data": 1,
                "left": null,
                "right": null
            },
            "right": {
                "data": 5,
                "left": null,
                "right": null
            }
        },
        "right": {
            "data": 8,
            "left": null,
            "right": {
                "data": 10,
                "left": null,
                "right": null
            }
        }
    }
}
initial queue data:  6
loop #: 1
queue data:  [ 6 ]
result:  [ 6 ]
- - - - - - - - -
loop #: 2
queue data:  [ 4, 8 ]
result:  [ 6, 4 ]
- - - - - - - - -
loop #: 3
queue data:  [ 8, 1, 5 ]
result:  [ 6, 4, 8 ]
- - - - - - - - -
loop #: 4
queue data:  [ 1, 5, 10 ]
result:  [ 6, 4, 8, 1 ]
- - - - - - - - -
loop #: 5
queue data:  [ 5, 10 ]
result:  [ 6, 4, 8, 1, 5 ]
- - - - - - - - -
loop #: 6
queue data:  [ 10 ]
result:  [ 6, 4, 8, 1, 5, 10 ]
- - - - - - - - -

While to see all nodes, Stack to control the order of visit

Stack allows us to visit nodes from root to leaf first (Depth First Search).

while loop and stack

lecture-11/while-loop-and-stack.js
// structure of a node
class Node {
  constructor(data) {
    this.data = data
    this.left = null
    this.right = null
  }
}

// Binary Tree
class BinaryTree {
  constructor(data) {
    this.root = data ? new Node(data) : null
  }

  // search if data is in binary search tree, return node/false
  search(data) {
    let current = this.root
    while(current) {
      if(current.data === data) {
        return current
      } else if(current.data > data) {
        current = current.left
      } else {
        current = current.right
      }
    }
    return false
  }
}

// create new binary tree
const binaryTree = new BinaryTree(6)
binaryTree.root.left = new Node(4)
binaryTree.root.left.left = new Node(1)
binaryTree.root.left.right = new Node(5)
binaryTree.root.right = new Node(8)
binaryTree.root.right.right = new Node(10)

console.log(JSON.stringify(binaryTree, null, 4))

const traverseWithWhileAndStack = (root) => { // aka Depth First Search
  const stack = [root]
  const path = []
  const result = []
  const visitedNodes = new Set()
  let loopCount = 1

  console.log('initial stack data: ', stack[0].data)

  while(stack.length > 0) {
    console.log('loop #:', loopCount)
    loopCount++

    const stackData = stack.map(e => e.data )
    console.log('stack data: ', stackData)

    const currentNode = stack.pop()
    visitedNodes.add(currentNode.data)

    currentNode.left && stack.push(currentNode.left)
    currentNode.right && stack.push(currentNode.right)

    path.push(currentNode.data)

    // if node is a leaf, save a complete root -> leaf path
    // and remove leaf from `path` variable to make way for new path
    if(!currentNode.left && !currentNode.right) {
      console.log('reached a leaf, path:', path)
      result.push(JSON.parse(JSON.stringify(path)))
      path.pop()

      // if node is a parent node and both left + right child
      // have been visited, remove node from `path` variable
      // to make way for new path
      const parentNode = binaryTree.search(path[path.length - 1])
      if(parentNode) {
        const leftChildVisited =
          !(parentNode.left && !visitedNodes.has(parentNode.left.data))
        const rightChildVisited =
          !(parentNode.right && !visitedNodes.has(parentNode.right.data))
        
          if(leftChildVisited && rightChildVisited) {
          console.log('popped a parent node:', path.pop())
        }
      }
    } else {
      console.log('current path:', path)
    }

    if(path.length && path[path.length - 1])
    console.log('- - - - - - - - -')
  }
  console.log('result: ', result)
}

traverseWithWhileAndStack(binaryTree.root)
$ node while-loop-and-stack.js
{
    "root": {
        "data": 6,
        "left": {
            "data": 4,
            "left": {
                "data": 1,
                "left": null,
                "right": null
            },
            "right": {
                "data": 5,
                "left": null,
                "right": null
            }
        },
        "right": {
            "data": 8,
            "left": null,
            "right": {
                "data": 10,
                "left": null,
                "right": null
            }
        }
    }
}
initial stack data:  6
loop #: 1
stack data:  [ 6 ]
current path: [ 6 ]
- - - - - - - - -
loop #: 2
stack data:  [ 4, 8 ]
current path: [ 6, 8 ]
- - - - - - - - -
loop #: 3
stack data:  [ 4, 10 ]
reached a leaf, path: [ 6, 8, 10 ]
popped a parent node: 8
- - - - - - - - -
loop #: 4
stack data:  [ 4 ]
current path: [ 6, 4 ]
- - - - - - - - -
loop #: 5
stack data:  [ 1, 5 ]
reached a leaf, path: [ 6, 4, 5 ]
- - - - - - - - -
loop #: 6
stack data:  [ 1 ]
reached a leaf, path: [ 6, 4, 1 ]
popped a parent node: 4
- - - - - - - - -
result:  [ [ 6, 8, 10 ], [ 6, 4, 5 ], [ 6, 4, 1 ] ]

Real life interview questions

  1. What's the key difference between a human and a computer when solving a tree problem? From your own opinion, what's the main reason for this difference?
  2. Solve problem 1, 2, 5, 6, 7 in 7 popular tree problems.
  3. Solution for stack example will not work in case tree height is greater than 2. Add your modification to the solution so it will work with tree with any height.
  4. Which/which combination/how many of Stack(s) or Queue(s) did you use to solve the problems above? Can you think of any other data structures to replace Stack or Queue to solve the same problems?