Recursion

We'll look deeply at recursion, since it's the harder part of the 20 Questions assignment.

Plan

  1. Announcements
  2. Cloning
  3. Event Delegation
  4. Answer your recursion questions
  5. Explore Recursion

Announcements

  • Poll on Final Project partners:
    • Thanks for the 11 responses to the poll. I need to hear from the rest.
    • About 2/3 want me to assign partners, the rest to find their own partner
    • I'll plan to give people until LDOC to find a partner, and then I'll assign partners for anyone who doesn't have one.
    • Will that work?
  • Covid and Wellesley
  • Another speech on socializing

20 Questions Assignment

We'll look at the 20 questions Assignment

Observations

  1. You'll have to manage and work with trees
  2. You'll have to dynamically create pairs of clickable buttons
  3. You'll have to have a recursive function to show the tree
  4. Playing the game will involve tracing a single path through the tree from root to leaf. That task is not recursive.

Let's look at some of these issues.

Cloning

Cloning is a relatively easy way to construct a lot of HTML (DOM elements) as a copy or clone of some existing DOM elements (attached or not).

You can then modify the copy using some of the techniques we've learned, such as (1) .find(), (2) .attr(), (3) .text() and so forth.

The modified clone can then be added to the existing page.

Here's a complex bit of HTML:

<div id="template">
<div class="foo">
   <div class="bar">
      <p><span style="font-weight:bold">buttons!</span>
         <span data-msg="msg">lorem ipsum</span>
         <button data-kind="fred">fred</button>
         <button data-kind="george">george</button>
      </p>
   </div>
</div>
</div>

Here's what it looks like:

buttons! lorem ipsum

We will add copies of that template to a container (id of container). Here's the JavaScript code that will do the work:

$("#addToContainer").click(function () {
     var clone = $("#template .foo").clone();
     var num = Math.floor(100*Math.random());
     clone.find("[data-msg]").text(num);
     clone.appendTo("#container");
});

Let's see it in action. Here's the button and the container:

This is the container

The button makes it easy to add custom copies to the container, no matter how complicated the HTML.

We can hide the template using display:none in our CSS.

  • Cloning is optional in Quizzes and all subsequent assignments.
  • Most students, in fact, stick to what we learned in Chapter 11.
  • I personally find cloning easier and I use it in my solutions.
  • The choice is yours.

Delegation

Delegation is important in the Quizzes assignment and for later ones as well. We'll use it for the rest of the course.

Suppose we want to add behavior to our buttons. The following is one very good way. Note that these handlers are added when the page loads, before any of the buttons exists! I've used the .one() method from my bounds plugin to check that the container is found.

$("#container").one().on('click',
                   '[data-kind=fred]',
                   function (event) {
                      alert("Oh, Fred!");
                   });
$("#container").one().on('click',
                   '[data-kind=george]',
                   function (event) {
                      var num = $(event.target)
                                    .closest(".bar")
                                    .find("[data-msg]")
                                    .text();
                      alert("Heavens, George! number is "+num);
                   });

Recursion

Trees are important in Computer Science. They are best defined recursively and therefore are an excellent match for recursive algorithms. It's essential that you are comfortable with such algorithms and data structures.

Recursive Definition:

A binary tree is either:

  • a leaf (a node with no children), OR
  • a node with two children, each of which is a binary tree

Your Recursion Questions

I'll answer your Recursion questions

Recursive Tree Traversal

I've loaded into this page a tree in JSON as the value of treejs. We'll take a look at it. Here's a screenshot of it all opened up:

treejs in the JS console

Here's a differently drawn figure:

treejs as a normal tree

Note that an important feature of recursion is determining the base case(s). In this tree, the base case is when the child node is a string as opposed to another JS object.

Here's how we can define that:

function isLeaf(node) {
    return( typeof node === 'string' );
}

Tree Size

The size of a tree (number of nodes, counting the leaves) can be defined recursively. How?

In English:

  • a leaf is of size 1
  • a non-leaf is of size 1 plus the size of each child

Here's code to compute that:

function treeSize(node) {
    if( isLeaf(node) ) {
        return 1;
    } else {
        return (1 
                + treeSize(node.Y) 
                + treeSize(node.N));
    }
}

We'll refer back to that definition later in class, so keep it handy.

Notes: this function is very short and simple to write. It looks almost magical. Furthermore, any non-recursive solution would be much more complicated. So, recursion is very important in computer science.

treeSize of Subtrees

We can try all of the following:

treeSize(treejs);
treeSize(treejs.Y);
treeSize(treejs.N);
treeSize(treejs.Y.Y);
treeSize(treejs.Y.N);

Try some variants

Verbose Tree Size

Understanding how treeSize() works can be confusing. Here's a very chatty version of it:

function treeSizeChatty(node,level) {
    if( isLeaf(node) ) {
        return 1;
    } else {
        var i = indent(level);
        console.log(i+'Computing size of node '+node.Q);
        let y = treeSizeChatty(node.Y,level+1);
        let n = treeSizeChatty(node.N,level+1);
        let total = 1+y+n;
        // template literal
        console.log(i+`Size is 1+${y}+${n} or ${total}`)
        return total;
    }
}

Try it out, like this: treeSizeChatty(treejs,0)

You can also try it on subtrees, like treejs.Y and treejs.N

BreakOut #1

In your breakout session, discuss the following questions. The idea is not to code them, but to start gaining some intuition via some thought experiments.

Here are some thought questions. What does the function compute in each of these cases?

  1. What happens if we return zero for a leaf instead of 1?
  2. What happens if, on non-leaves, we return zero plus the sum of the sizes of the children?
  3. What happens if, for leaves, we return the leaf instead of 1?
  4. What happens if, for leaves, we return a one-element array of the leaf, and for non-leaves, we return the concatenation of the arrays returned for each child?
  1. Since leaves don't count, the function ends up counting just the number of non-leaves. We could call this function `notleafcount`
  2. Since we don't count 1 for each non-leaf, the function ends up counting just the leaves. We could call this function `leafcount`
  3. If we return the leaf (a string), and we use "+" in the recursive case, we end up concatenating together all the leaves, albeit with some "1" characters thrown in. Not a very useful function, though if we remove the "1" characters, it becomes almost useful.
  4. We end up returning an array containing all the leaves. We've "flattened" the tree, which might be a good name for this function.

Flattening

The example of flattening is interesting enough to spend a few minutes on. (Note that this is not the technique you should use in 20 Questions. We're just trying to learn recursion right now. In 20 Questions, you will be returning structure from the recursion, but you will not be trying to flatten the tree.)

function flatten(node) {
    if( isLeaf(node) ) {
        return [ node ];
    } else {
        var lchild = flatten(node.Y);
        var rchild = flatten(node.N);
        return lchild.concat(rchild);
    }
}

Try it! We could also name this function leavesOnly.

Do you remember Divide-Conquer-Glue from CS 111? Recursion is all about DCG. In the flatten case, the glue is concat, but we've also used +. The divide is to go from a node to its children, which are, by definition, smaller.

How could we include the questions as well?

Breakout #2

For the rest of class, you'll be in breakout rooms. You can

  • do some or all of the following exercises
  • work on Quizzes

Exercise: treeHeight

Write a function to find the height of the tree (the length of the longest path from the root to a leaf). It might be helpful to know that there is a Math.max function in JavaScript. Look back at the largestInTree function in the reading.

I suggest you first write it out in English and then try to code it using pencil and paper; don't try to do live coding in class, unless you have VSC up and running.

function treeHeight(node) {
    if( isLeaf(node) ) {
        return 1;
    } else {
        return 1 + Math.max(treeHeight(node.Y),
                            treeHeight(node.N));
    }
}

Exercise: longestAnswer

Write a function to find the longest answer (by number of characters in the string). Look back at the largestInTree function in the reading.

function longestInTree(node) {
    if( isLeaf(node) ) {
        return node;
    } else {
        let ymax = longestInTree(node.Y);
        let nmax = longestInTree(node.N);
        // return (ymax.length > nmax.length ? ymax : nmax );
        if ( ymax.length > nmax.length ) {
            return ymax;
        } else {
            return nmax;
        }
    }
}

Exercise: allAnswers

Write a function to return a string comprising all the answers in the tree. The strategy on the recursive step is to get the string of answers from each child and then concatenate those two strings together.

function allAnswers(node) {
    if( isLeaf(node) ) {
        // the node itself is the answer.
        // newline just for readability
        return node+"\n";
    } else {
        var yc = allAnswers(node.Y);
        var nc = allAnswers(node.N);
        return yc+nc;
    }
}

This is just our flatten function! Did you figure that out?

Exercise: allAnswersArray

The last solution was okay, but strings aren't the nicest data structure, though we could split the string, if we glued pieces together with an appropriate delimiter.

Instead, let's do a variant in which we return an array of all the answers.

You should know that there is a concat method on JavaScript arrays. Like this:

var evens = [2, 4, 6, 8];
var odds = [1, 3, 5, 7];
var all = odds.concat(evens);
function allAnswersArray(node) {
    if( isLeaf(node) ) {
        // the node itself is the answer.
        // must return an array, though
        return [ node ];
    } else {
        var yc = allAnswersArray(node.Y);
        var nc = allAnswersArray(node.N);
        return yc.concat(nc);
    }
}

Exercise: allQuestions

Write a function to return an array comprising all the questions in the tree.

The strategy on the recursive step is to get all the questions from both children and then concatenate those two strings together along with the question from this node.

function allQuestionsArray(node) {
    if( isLeaf(node) ) {
        return [];  // leaves don't have a question
    } else {
        var self = [ node.Q ];
        var yc = allQuestionsArray(node.Y);
        var nc = allQuestionsArray(node.N);
        return self.concat(yc,nc)
    }
}

Recursion with a "driver" function

Often we want to do something with the result of recursive function. That's sometimes an entirely separate, unrelated function, or sometimes is thought of as a "driver" function. Here's an example of the latter.

Suppose we want to get a list of the answers in a tree and show them on the page. Here's a function that does that:

function display_answer_list(where) {
    var answers = allAnswersArray(treejs);
    var dst = $(where).one();
    dst.empty();
    answers.forEach(function (ans) {
        $("<li>").text(ans).appendTo(dst);
    })
}

Try it with #answer-list:

    Note that the function above is an example of a driver function: a function that invokes a recursive function with the correct first argument and deals with the result.

    Choose

    Recall that one definition of the binomial coefficient (the N Choose K function) is a recursive function:

    function choose(n,k) {
        if( k==0 || k==n ) {
            return 1;
        } else {
            return choose(n-1,k-1)+choose(n-1,k);
        }
    }
    
    function test_choose(n) {
        for (var k = 0; k <= n; k++ ) {
             console.log(n+" choose "+k+" is "+choose(n,k));
        }
    }
    

    Try this on a few reasonable values where you can check using the alternative formula:

    choose(n,k) = n!/(k!(n-k)!)

    Notes:

    The base cases are k==0 and k==n. Every recursive function needs base cases and needs to always reach a base case.

    The recursive case is all others, and breaks down to the sum of two recursive calls, both of which are closer to the base cases than this call. Thus, we make progress.

    Summary

    Recursion

    • Very important for handling recursive data structures, such as trees (including web page documents).
    • Write a function to solve the "generic" case of a single node (leaf or internal node) and trust that the recursive calls will do the right thing.