Saturday, December 7, 2013

A lesson on reading comprehension

We're now officially in the final exam period of the semester. Before I write a reflection on the whole semester, I thought I'd first reflect on some of the skills that are not technical in nature but crucial for success in technical subjects that I've come to reinforce through CSC148.

Throughout all of the labs, exercises, and questions we've done in CS, I've realized that none of the exercises are particularly challenging in nature. All of the questions can be solved by applying a concept we learned in class or simply through logic. Their difficulty actually arise largely from a lack of understanding of what it is that the question is asking you to do. In the past when I've been told to read carefully, I've always shrugged it off considering it as a mistake only 'amateurs' can make. From partaking in the labs this semester, I've come to take the advice of reading carefully to heart. There were definitely more times than I'd like to admit where I just stared at a piece of instruction in a lab handout in perplexity, unable to decipher how I'm supposed to approach the problem without a piece of information, only to have my partner point out to me that a paragraph in the beginning of the lab already addressed the lack of that piece of information. I made my own life harder than it has to be.

In one of the exercises involving binary trees (when we were first introduced to trees), the exercise asked us to do some kind of recursion through a tree, and I was immediately overwhelmed having assumed that each mention of 'tree' in the exercise pointed to non-binary trees. Then a friend told me to read the beginning of the exercise, and of course, the exercise specified that we're only working with binary trees.

Reading technical material is different from reading a story. Whereas in a story, your eye recognizes different sentence structures and your brain can interpret the general meaning of passage without taking to heart the weight of each word, in technical readings, each word is crucial to the understanding of the text.

Lesson learned.

Friday, November 29, 2013

Sorting Fun

Sorting. A big area of computer science due to its usefulness in a number of algorithms. Sorting is the process by which the elements in a list are put in a determined order. In selecting the method to sort a list of items, it's important to consider the efficiency of the sort in handling the cardinality, as well as the conditions of the input elements. One sort could be more efficient for a larger-sized input where another sort could be more efficient in handling smaller-sized inputs. On a similar note, a sort may perform better or worse depending on the order that the elements of the list are already in.

In essence, two things occur in a sorting algorithm.

1 - Two elements are compared against each other.
2 - The elements are exchanged as necessary.

Here are two examples of sorts:

Quick Sort
Quick sort is a sorting algorithm that involves recursion. A 'pivot' value is selected and from there on, the list is broken up into two halves where all values bigger than the pivot exist on the right of the pivot, and all values smaller than the pivot exists on the left of the pivot. These sorted sublists are recombined and the sorting algorithm continues until the whole list is combined.

A note on efficiency:
The efficiency of quick sort depends on the pivot that you select, as well as the aforementioned 'condition of the input elements' (i.e. is this list sorted already). If a 'good' pivot is selected, this means the list is split at each step into halves that are approximately equal, then quick sort would perform in the efficient way that is hoped. In contrast, a 'bad' pivot would see the list being split up into uneven parts, in which more work would have to be done in the half that contains more values (in the worst case, you would get one half of size 1, and the other of size n-1). From this, it's easy to see that if a quick sort is run on a list that's already been sorted, if the pivot point that is chosen is the first value of the list, it would be the worst-case scenario in terms of efficiency using the quick sort.

Merge Sort
Merge sort is similar to quick sort in that it also relies on recursion and the splitting of the list into halves. However, merge sort begins by splitting the list into two halves that are of equal cardinality. This splitting continues down to the base case. Along every step of the way, the smaller element is taken between the two lists and joined together. The process continues until the entire list is sorted.

--

In class, we learned about the Big-O notation which goes into conveying the efficiency of the worst-case scenario. Initially I found the concept of the Big-O hard to grasp (because after all, there are so many different algorithms, and so many different ways to implement them, how is Big-O supposed to standardize the expression of efficiency?). From scouring about, I came to learn that essentially, the efficiency of any algorithm can be modelled as a function of its input size. The Big-O notation expresses the worst-case scenario of the algorithm by  extracting the part of the function that causes the function to grow the fastest.

Up until now, my exposure to computer science has mainly been in application. Big-O notation has been in essence the first time I've seen the integration of some real math (and this is only the very tip of the ice berg) with computer science. I hear that Big-O notation is something that is touched upon more in CSC165, but a lot more in depth in second year. It's also in second year that we begin getting introduced to other types of notation, such as Big-Omega for expressing the best-case scenario. It will be interesting to learn more about exactly how an algorithm is optimized to perform efficiently, and through mathematical means.

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)