Saturday, October 19, 2013

Week 6 - Toughest week thus far

Last week in class we had our formal introduction to recursion. At the beginning of the course, our prof showed us a sneak peak into the wonderful (with this week's experience I'd attest as mostly horrendous) world of recursion:

def sum_list(L: list) -> float:
    """
    Return sum of the numbers in possibly nested list L
    
    >>> sum_list([1, 2, 3])
    6
    >>> sum_list([1, [2, 3, [4]], 5])
    15
    """
    
    return sum([sum_list(x) if isinstance(x, list) else x for x in L])

That was the first time I'd come across recursion, and after some in depth tracing, I thought I'd figured it out. Through Exercise 4 this week, I've come to experience in full both the wonders and aforementioned absolute horrors of recursion.

In our exercise this week, we were asked to construct a list representation of a binary tree from a preorder list traversal and inorder list traversal (serving as the parameters to the function).

For instance, given:

preorder traversal [10, 6, 8, 12, 11, 15] and
inorder traversal [8, 6, 12, 10, 11, 15]

Our function is to take these two lists to construct:

[10, [6, [8, None, None], [12, None, None]], [11, None, [15, None, None]]]

Illustrated as follows:




From this, it's evident that preorder traversal traverses the binary tree in the sequence of node, left subtree, right subtree, and inorder traversal traverses the tree in the sequence of left subtree, node, right subtree.

There are a number of inferences that I made first considering this problem.
  1. The first value in preorder is the parent node of the tree, thus in inorder, all the values that preceeds the first value described in preorder will be on the left of the parent node, or in other words, they will be in the left subtree.
  2. You can guess that the point at which recursion should stop (even before having identified exactly how recursion should occur) is when you get to the leaves, at which point, the [1] and [2] values are None (i.e. these nodes have empty subtrees). That's a hint for the base case.
  3. The same kind of recursion occurs for the left subtree as well as the right subtree. There needs to be some way to connect the results of recursion from the left subtree and from the right subtree together in the end.
At the core, to the seasoned programmer, this is really all there is to it. However, since it was my first time working on recursion, this problem confused me profusely.

My first attempt yielded the following result:

def make_tree(preorder, inorder):
    
    if inorder.index(preorder[0]) == 0:
        nested_list = [preorder[0], None, None]
        return nested_list
        
    else:
        tree_list = [preorder[0], make_tree(preorder[1:], inorder), make_tree(preorder[1:], inorder)]
    
    return tree_list

>>> [10, [6, [8, None, None], [8, None, None]], [6, [8, None, None], [8, None, None]]]

As compared to the correct result:
[10, [6, [8, None, None], [12, None, None]], [11, None, [15, None, None]]]

Alright, so I got the first few values correct, but needless to say, this was incorrect. My idea here was that I noticed on the outmost layer that the first value was the value at [0] of preorder, and the second and third value were nested trees, so I knew I had to have recursion for those two parts. From what I knew of recursion, some 'work' or change has to be made to descend into the next level of recursion, and it logically made sense that the new preorder list in my next level of make_tree would take on the values after the value at [0] (since it was already written down). I didn't know what to do with inorder though, so I just left it there to test.

For the base case, based on #2 of the inferences I made above, I knew the Nonetypes had to be incorporated for those leaf values, but (in retrospect) I was still thinking in my iterative ways, so I assumed the base case to be [preorder[0], None, None]. I saw that the point at which the first element in the preorder list becomes the first element in the inorder list is the point that we've reached the leaves of the tree, hence inorder.index(preorder[0]) == 0.

Because I wasn't 100% sure about how to approach this problem, I put together what I knew and without a full understanding of what I wrote, derived my first attempt.

After having exhausted all of my available brainpower, it was time to get help. After much consultation with a friend that's a much more experienced programmer, I slowly gained intuition of how to choose a correct base case. Attempt 2 is as follows:

def make_tree(preorder,inorder):
        
    if len(preorder) == 0 and len(inorder) == 0:
        return None
    
    elif len(preorder) == 0:
        return [inorder[0],None,None]
    
    elif len(inorder) == 0:
        return [preorder[0],None,None]
    
    else:

            left = make_tree(preorder[1:inorder.index(preorder[0])], inorder[:inorder.index(preorder[0])])

            right = make_tree(preorder[inorder.index(preorder[0]) + 1:], inorder[inorder.index(preorder[0]) + 1:])
            
    return [preorder[0], left, right]

After weeding out the unnecessary elif statements, I finally arrived at the correct code. 

def make_tree(preorder, inorder):
        
    if len(preorder) == 0 and len(inorder) == 0:
        return None
        
    else:
        left = make_tree(preorder[1:inorder.index(preorder[0]) + 1],
                         inorder[:inorder.index(preorder[0])])
                
        right = make_tree(preorder[inorder.index(preorder[0]) + 1:],
                          inorder[inorder.index(preorder[0]) + 1:])
           
    return [preorder[0], left, right]

This exercise (part a and b) took me a few hours to complete, and the biggest challenge for me was in trusting that recursion will work. This is where the magical part comes in. If you read the code, it's very intuitive. It makes sense that the same process through each of the subtrees would generate a nested result, that [preorder[0], left, right] gives exactly what it looks like will give. The difficulty in recursion lies in my inability to accept this simplicity. To understand a concept, I must see each step and understand it thoroughly, and so in completely this exercise and other recursion exercises I came across, I always had to trace through the whole code to the base case before I would believe it works.

The biggest challenge for me in the recursion I encountered in this exercise is in knowing how to incorporate the right side recursion of a tree or list with the left side. I seem to always get the left side to 'work', but I can't get my head wrapped around how I should go about the right side.

I thoroughly hope an understanding of recursion will come with practise. For now, I am just ecstatic that I got the desired result for this exercise.


Wednesday, October 16, 2013

1/2 CS Tests Done!

I've just finished my first test in university, and it was for CS.

I just wanted to make a note of some of my thoughts on the test.

Overall, it was quite straightforward. There was nothing new that we didn't cover in class. Time management is of course crucial for any test, and that's definitely something I need to work on for CS, because I'm not used to having to work under pressure for programming. I think gaining more familiarity with Python syntax overall will help quite a bit (for instance, I didn't know how min() evaluates a list of tuples, and assumed that it looks at each integer and outputs the lowest number, which is incorrect), but more importantly, I need to work on more exercises to familiarize myself with the type of questions you can come across for each concept we covered.

Then there are the silly mistakes. For one of the questions, I replaced what should've been a parameter inputted by the user with a value. Got tricked by the docstring example. Oh man.

On the bright side, I have a much better grasp of recursion now. The thing that didn't click with me in the beginning with recursion was how the function returns after it has reached its base case. In actuality, it's just like evaluating a mathematical function, you replace f(x), with the value it evaluates to. The return from say f(x) is what replaces the 'bigger' f(x) at the level above. The problem becomes very simple when you realize this.

I remember at the beginning of the course when we first did sum of a nested list. Danny told us not to trace through the steps, but to think on the surface of how the problem can be broken down and just go for it. I came across the same piece of advice when I was looking online on how to approach recursion problems online. In a way, you just need to have the general intuition that the problem will be broken up into smaller bits when you call the recursive function with a parameter that breaks up the initial input, and eventually reach a base case that will start returning the result at each step, where the function then evaluates at each step. You just need to trust it. And in that way, recursion is quite magical.

Well, that marked the beginning of test crunch, and now we speed towards the end of the semester. It's great that I have a much better idea of how I should study for tests now. Here's to doing much better on my next test!

Monday, October 14, 2013

'I' > 'you' = True

While reading about strings today, I found out that you can actually compare strings to one another in Python. Not just to check if two strings are equal, but you can actually compare them with < > operators. Alright, in actuality, it just compares the alphabetical order of the two strings but when you don't know that, man, it is weird.

# Let's see if bananas are 'greater than' Zebras or not.
>>> 'banana' < 'Zebra'
False
>>> 'banana' > 'Zebra'
True

Sunday, October 13, 2013

In-class concepts: Object-Oriented Programming and Recursion


Object-Oriented Programming is a form of programming where any data type can represented by objects. You can modify these objects by doing certain actions to the object that are particular to this object (in programming terminology, you can call methods defined for a class). Because I don't have experience in a language that is non-OO, unfortunately (or fortunately), OOP has been simply the way I know programming as. Of course, there are both advantages and disadvantages to using OOP as opposed to a different paradigm depending on the situation, so my lack of insight into other styles of programming definitely does not represent my intention to turn a blind eye to the other styles.

From what I've read, there are several features that are unique to OOP. 

OOP allows methods to be structured around each data type or concept you want to represent. In this way, it's convenient to call upon these enclosed methods, 'packages', to different parts of your script. It also simplifies the way of making changes to your code, as you could inherit the parts you need from code that you've already written in one object to another, and make then the necessary changes or additions that pertains to this new object you're presenting. OOP allows developers and users to use code in a way that makes sense intuitively by abstracting the exact implementation of the methods. So if you want to 'pet a cat', for example, you don't need to know the details of how exactly which muscle will be used in generating which kind of movement to pet the cat, you just pet the cat. cat = Cat.pet()

In general, OOP allows for a very 'human' way to tackle a problem in programming.

Recursion is a technique for solving a problem by breaking it up into smaller and smaller problems of the same nature. The solutions of these smaller problems then piece together in the end to bring about the final solution. In Python, you use recursion by calling, say, function_a() within itself, modifying the parameter by a little so to chop up the problem every time the function recurses until you get to a base case. Once a value is derived at the smallest part of the problem, it then gets substituted back into every level the function has been called until the problem is finally solved at large. 

Although recursion is not always the preferred way to solve a problem (due to the recursive nature of recursion, if base cases are not handled correctly, it could result in some unwanted results), recursion is still very powerful in that it allows one to reach a solution to a complex problem in just a couple lines of code.

Wednesday, October 9, 2013

CS every day

When you immerse yourself in a subject for a period of time, you begin to see all the concepts being manifested in your everyday life. It's an interesting phenomenon that I acknowledged it with my lab partner last week, and he laughed in agreement.

I remember back in grade 11 biology when we were learning about plant anatomy. For a few weeks following one of our big tests, I couldn't look at any living thing around me without first seeing it at the  microscopic level; I knew why this particular flower looked the way it did, why celery leaves withered, how this tree is alive and how old it must be now.

The same has been the case with computer science this year. The first few times they occurred, I'd note them, marvel for a second how relevant computer science could be in modelling real life situations, and then dismiss them. But recently, I thought it might actually be interesting to document these instances. 

Here's an example from just last week: Two of my friends and I were hanging out. I had on a maroon Harvard hoodie, my friend a gray Harvard sweatshirt, and my second friend a maroon University of Toronto Hoodie. In that same week, we had been dabbling in inheritance in class.

Here are the class headings for the inheritance for fun!

class Sweater

class UTHoodie(Sweater)

class HarvardHoodie(UTHoodie)