Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Trees

Trees.livemd

Trees

Introduction

Wikipeida reference

We want to build a binary tree, which is a fundamental data structure and one that works quite well as a recursive algorithm.

For this notebook, I want to think about how I would store a tree structure and how you could traverse it both depth first and breadth first.

(These are common tasks typical at university level CS or job interviews)

The Tree itself

From the Wikipedia entry we see that “In computer science, a binary tree is a tree data structure in which each node has at most two children, which are referred to as the left child and the right child.”

It suggests that we can store the tree as “Nodes and references” - binary trees are typically constructed by having a tree node structure which contains some data and references to its left child and its right child.

or

Binary trees can also be stored in breadth-first order as an implicit data structure in arrays, and if the tree is a complete binary tree, this method wastes no space. In this compact arrangement, if a node has an index i, its children are found at indices …

That is all very complicated, so lets have a go at storing a simple tree

Experiments

# A Tree is a collection of Nodes
defmodule Tree do
  defstruct nodes: []
end
defmodule Node do
  defstruct val: 0, left_child: nil, right_child: nil
end

Lets build a simple tree -

one = %Node{val: 1}
three = %Node{val: 3}
two = %Node{val: 2, left_child: one, right_child: three}

five = %Node{val: 5}
seven = %Node{val: 7}
six = %Node{val: 6, left_child: five, right_child: seven}

four = %Node{val: 4, left_child: two, right_child: six}

tree = %Tree{nodes: four}

The challenge now is given this structure can we find a way of going through it and getting the right order out?

defmodule TraverseTree do
  def run(tree) do
    tree.nodes
    |> depth_search([tree.nodes], [])
  end

  # We want to go all the way to the bottom of the left hand side adding to a result.
  # We need to keep track of the nodes we visted and add them to the accumulator if they are a leaf (last)
  # or both children have alredy been added

  # We exit when we have no more nodes to visit
  def depth_search(_node, [], acc) do
    acc
  end

  def depth_search(node, visited, acc) do
    IO.inspect(node.val)
    # Four actions
    # We find a leaf, add the leaf and return up
    # We find a node, with the left explores - add myself and explore the right hand
    # We find a node with both children unexplored - go down the left first
    # Find a node with both children explored - move up

    {parent, previous_visited} = get_previous_node(visited)

    new_node = get_next_node(node, parent, acc)
    new_visited = update_visited(node, visited, previous_visited, acc)
    new_acc = update_accumulator(node, acc)
    # Recurse the function with the new params
    depth_search(new_node, new_visited, new_acc)
  end

  def get_next_node(node, parent, acc) do
    if is_leaf(node) or has_visited_children(node, acc) do
      parent
    else
      if has_visited_left_child(node, acc) and !has_visited_right_child(node, acc) do
        node.right_child
      else
        node.left_child
      end
    end
  end

  def update_visited(node, visited, previous_visited, acc) do
    # We either move up the tree or down, if we move down we add the current node to the visited list
    # If we move up, we remove the current node
    if is_leaf(node) or has_visited_children(node, acc) do
      previous_visited
    else
      [node] ++ visited
    end
  end

  def update_accumulator(node, acc) do
    # Add the node if it is a leaf, or we have visted the left child but not the right child
    if is_leaf(node) or
         (has_visited_left_child(node, acc) and !has_visited_right_child(node, acc)) do
      acc ++ [node.val]
    else
      acc
    end
  end

  def get_previous_node(visited) do
    visited
    |> List.pop_at(0)
  end

  def has_visited_children(node, acc) do
    has_visited_left_child(node, acc) and has_visited_right_child(node, acc)
  end

  def has_visited_left_child(node, acc) do
    Enum.any?(acc, fn n -> n == node.left_child.val end)
  end

  def has_visited_right_child(node, acc) do
    Enum.any?(acc, fn n -> n == node.right_child.val end)
  end

  def is_leaf(node) do
    node.left_child == nil and node.right_child == nil
  end
end

TraverseTree.run(tree)

Lets try with a more complex example, we will add another layer to the Tree.

# Left side of Tree
one = %Node{val: 1}
three = %Node{val: 3}
two = %Node{val: 2, left_child: one, right_child: three}

five = %Node{val: 5}
seven = %Node{val: 7}
six = %Node{val: 6, left_child: five, right_child: seven}

four = %Node{val: 4, left_child: two, right_child: six}

# Right side

nine = %Node{val: 9}
eleven = %Node{val: 11}
ten = %Node{val: 10, left_child: nine, right_child: eleven}

thirteen = %Node{val: 13}
fifteen = %Node{val: 15}
fourteen = %Node{val: 14, left_child: thirteen, right_child: fifteen}

twelve = %Node{val: 12, left_child: ten, right_child: fourteen}

eight = %Node{val: 8, left_child: four, right_child: twelve}

tree = %Tree{nodes: eight}

TraverseTree.run(tree)

Great - we can see that the algorithm works and can traverse a bigger tree. However, there is a problem with the implementation. Not all binary trees are fully populated, so some nodes can have a left child with no right child or vice versa. Try running the above with setting a nodes child to nil - it will enter an infinite loop

Lets see what happens when we have a tree with a nil as a child.

b = %Node{val: "B"}

a = %Node{val: "A", left_child: b, right_child: nil}

TraverseTree.run(%Tree{nodes: a})

That seems to have worked - lets try a larger example and try and fix.

See diagram

defmodule TT do
  def run(tree) do
    tree.nodes
    |> depth_search([tree.nodes], [])
  end

  # We want to go all the way to the bottom of the left hand side adding to a result.
  # We need to keep track of the nodes we visted and add them to the accumulator if they are a leaf (last)
  # or both children have alredy been added

  # We exit when we have no more nodes to visit
  def depth_search(_node, [], acc) do
    acc
  end

  def depth_search(node, visited, acc) do
    IO.inspect(node.val)
    # Four actions
    # We find a leaf, add the leaf and return up
    # We find a node, with the left explores - add myself and explore the right hand
    # We find a node with both children unexplored - go down the left first
    # Find a node with both children explored - move up

    {parent, previous_visited} = get_previous_node(visited)

    new_node = get_next_node(node, parent, acc)

    new_visited = update_visited(node, visited, previous_visited, acc)

    new_acc = update_accumulator(node, acc)

    # Recurse the function with the new params
    depth_search(new_node, new_visited, new_acc)
  end

  def get_next_node(node, parent, acc) do
    if is_leaf(node) or has_visited_children(node, acc) do
      parent
    else
      if visited_left_but_not_right(node, acc) do
        node.right_child
      else
        node.left_child
      end
    end
  end

  def update_visited(node, visited, previous_visited, acc) do
    # We either move up the tree or down, if we move down we add the current node to the visited list
    # If we move up, we remove the current node
    if is_leaf(node) or has_visited_children(node, acc) do
      previous_visited
    else
      [node] ++ visited
    end
  end

  def update_accumulator(node, acc) do
    # Add the node if it is a leaf, or we have visted the left child but not the right child or if the right child is nil
    if is_leaf(node) or visited_left_but_not_right(node, acc) or
         (has_visited?(node.left_child, acc) && is_nil(node.right_child)) do
      acc ++ [node.val]
    else
      acc
    end
  end

  def get_previous_node(visited) do
    visited
    |> List.pop_at(0)
  end

  def has_visited_children(node, acc) do
    has_visited?(node.left_child, acc) and has_visited?(node.right_child, acc)
  end

  def visited_left_but_not_right(node, acc) do
    has_visited?(node.left_child, acc) and !has_visited?(node.right_child, acc)
  end

  def has_visited?(node, acc) do
    if is_nil(node) do
      true
    else
      Enum.any?(acc, fn n -> n == node.val end)
    end
  end

  def is_leaf(node) do
    node.left_child == nil and node.right_child == nil
  end
end

Now lests test with a tree that nodes with left_child is nil and right_child is nil.

(We have added a node “E” to the above diagram that is the right child of “D”)

a = %Node{val: "A"}
# Here is the nil Child
b = %Node{val: "B", left_child: a, right_child: nil}

e = %Node{val: "E"}
d = %Node{val: "D", left_child: nil, right_child: e}
c = %Node{val: "C", left_child: b, right_child: d}

TT.run(%Tree{nodes: c})