Category: Computer Science

  • Where discrete mathematics meets an interview question

    Last week, I was sitting behind my desk at work, surfing hacker news and and at the top of the website floated an article by the co founder of “Daily Coding Program”, a small tech start up that e-mails daily newsletters with a programming question.  The article shared some of their insights over the past year and how they bootstrapped the company and eventually grew to $2,000 in monthly revenue.

    Although I was interested in reading the blog post, I ended skipping over most of it and navigated to their front page, where I scrolled up and down and read more about the company’s product.  When I reached the end of the front page, I was confronted with a sample interview question:

    There’s a staircase with N steps, and you can climb 1 or 2 steps at a time. Given N, write a function that returns the number of unique ways you can climb the staircase. The order of the steps matters.

    This question, I had initially thought, could be solved using some techniques that I recently learned from taking my discrete mathematics course. But to confirm whether or not I could apply permutations and combinations, I hollered at my colleague, repeating the question out loud and asking him how this problem could be solved. And as I started reading the question out loud, “There’s a staircase with N steps, and you can climb …” he interrupted me, finishing my sentence with, “1 or 2 steps at a time.”

    Apparently, while preparing for interviews with Google and Microsoft, he had stumbled across this same practice problem.

    So after letting him interrupt me, I asked him if this particular problem could be solved with permutations or combinations. In other words, do these two mathematical concepts apply to the problem. He confidently answered, “No — dynamic programming.” He then proceeded to step me through his solution, scribbling down his work on a white piece of 8.5 x 11 that sat on my desk.

    Fast forward to now.

    I’m at home, starting the next lesson for my discrete mathematics course, the lesson title: “Counting using Recurrence Relations” and “Solutions to Recurrence Relations.” I’m skimming the chapter, building a mental model of what the entire chapters entail, an overview.  And when I reached the end of the first chapter, where exercise problems live, I couldn’t help but form a little grin.

    To my surprise, the following chapters cover topics relating to the interview question that I had just read. In fact, the exercise in the chapter is phrased almost identically:

    Sal climbs stairs by taking either one, two, or three steps at a time. Determine a recursive formula for the number of different ways Sal can climb a flight of n steps.

    So, I find it always nice and serendipitous when what you are learning, either in school or on your own, can be applied to real life examples. Albeit, this application was only for an interview question.

    But I’ll still consider that as a victory.

     

  • Lessons learned coding the quicksort algorithm in assembly (i.e. MIPS)

    About six months ago, I enrolled myself in a computer organization (i.e. CS1410) course offered by University of Northern Iowa and I’m taking it remotely from Seattle, where I work full time as software engineer at Amazon (Web Services).

    I’ve completed about two thirds of the course, which consists of sixteen homework assignments and three proctored exams, my most recent homework assignment requiring me to code in MIPS, a low level programming language known as assembly. More specifically, I’m tasked with implementing the quicksort, a recursive algorithm, to sort a sequence of integers.  This homework assignment targets teaching two important computer science concepts: the run-time stack and calling conventions.

    Normally, I complete one homework assignment per week. However, this homework assignment was extremely challenging, taking roughly two and a half weeks to complete. The first couple days I dedicated to drilling the quicksort algorithm into my head, ensuring that I could visualize how the program actually sorts elements in the sequence, reading article after article (and sections from the books that have been collecting dust on my bookshelf); the remainder of the time I spent deep diving into writing the assembly code, typing code and executing in a MIPS simulator.  I cannot explain the number of times I grew frustrated, banging my head into the keyboard, because of program crashing.  At one point, I was stuck — for three days straight. None of my troubleshooting skills pinpointed me to the root cause.  After three days of staring at the code, I finally discovered the problem: I was corrupting the run-time stack.  After modifying one single line, updating the instruction to subtract 24 instead of adding 24 to the stack pointer (i.e. $sp register), the quicksort program ran flawlessly.

    All in all, I found the homework assignment as challenging but rewarding.

     

     

     

     

     

  • A brief introduction to cache organization

    As a software programmer, I always had a vague understanding of how an operating system fetches data from memory.  At an abstract level, I understood that a processor requests data from the memory controller, sending a message (with the memory address encoded) across the bus.

    But I learned recently that in addition to the system’s physical memory—the same memory I used to squeeze into the motherboard when installing RAM—there are multiple hardware caches, completely abstracted from the programmer.

    These caches sit between the processor and main memory, called: L1 cache and L2 cache and L3 cache.  Each cache differs: in cost, in size, and in distance from the CPU. The lower the digit, the higher cost, the smaller in size, and the closer it sits to CPU.  For example, if we compare L1 and L2 cache, L1 costs more, holds less data, and sits closer to the processor.

    When the processor wants to retrieve data from memory, it sends a request first lands in L1 cache’s world.  If L1 has that memory page cached, it immediately sends that data back to the processor, preventing the request from unnecessarily flowing towards the memory controller.  This pattern of checking local cache and forwarding requests repeats until the request eventually reaches the memory controller, where data is actually stored.

    The further we allow the CPU’s request to travel down the bus, we penalize the CPU, forcing it to wait, like a car at a stop sign, for longer cycles. For example, the CPU waits 4 cycles for L1 cache, 12 cycles for L2, 36 cycles for L3, and—wait for it—62 cycles when accessing main memory.  Therefore, we strive to design systems that cache as much data as possible and as close to the CPU, increasing overall system performance.

    We break down a cache into the following components:

    • Blocks
    • Lines
    • Sets
    • Tag
    sets, lines, blocks
    Cache organized into sets, lines, and blocks

    As you can see from the image above, we organize our cache sets (S), lines (L), and blocks (B).  One block of data represents 8 bits (1 byte) and every block of data is represented by a physical memory address. For example, the memory address 0x0000 may store 010101010 and 0x0001 may store 01110111 another.  We group these blocks together into a line, which store sequential blocks.  A line may store two or four or eight or sixteen bytes—it all depends on how we design the system.  Finally, each line belongs to a set, a bucket that stores one or more lines.  Like the number of bytes a line stores, a set can store one or two or three or forty—again, it all depends on our design.

    Together, the total number of sets, number of lines, and number of bytes determine the cache’s size, calculated with the following formula: cache size = S x E x B.

    In the next post, I’ll cover how a cache processes a memory address, determining whether it retrieves memory from cache or forwards the request to the next cache (or memory controller).

  • Defusing a Binary Bomb (phase 1)

    http://blog.itsmemattchung.com/2017/02/28/csapp-defusing-the-bomb-phase-1/

    I password protected the original post (email me for the password if you are interested in reading it).  When I posted the original link on reddit/r/compsci, multiple commenters suggested that I delete the article to avoid students from cheating (which was not my intention).  I then sent an e-mail to the professors (at CMU) and they kindly replied, asking me to remove the post:

    Matt,

    Thanks so much for your kind words. It’s great to hear that the book is helpful to you. While every student gets a slightly different bomb, the solution strategies for each phase are very similar. So it would be good if you could remove those posts.
    Thanks!
    Dave
  • Protected: Defusing a Binary Bomb (phase 1)

    This content is password-protected. To view it, please enter the password below.

  • How does the modulus operator work?

    As a programmer, I’ve written a line or two of code that includes the modulus operator (i.e. “return x % 2“).  But, never have I paused to think: “How does the underlying system carry out this operation?” In this post, I limit “underneath the hood” to the lowest level (human readable) programming language: assembly.

    So, I’ll take a program (written in C language) and dump it into assembly instructions. Then, I’ll explain each instruction in detail.

    My background in assembly programming

    Up until a few weeks ago, I had never studied assembly—I did flip threw a page or two of an assembly book, when a colleague mentioned, about five years ago, that his uncle programmed in assembly—and I certainly underestimated the role that assembly plays in my career.

    Sure—the days of writing pure assembly language evaporated decades ago, but the treasure lies in understanding how programs, written in higher level languages (e.g “C, Perl, PHP, Python”), ultimately boil down to assembly instructions.

    Modulus program

    So, I contrived a trivial modulus program written in C—let’s dissect it:

    // modulus.c
    
    int modulus(int x, int y){
        return x % y;
    }

    Converting C code into assembly

    Before a program can be executed by an operating system, we must first convert the program into machine code—we call this compiling.  Before we run our program (modulus.c) through the compiler, we need to discuss two arguments that we’ll pass to the compiler, in order to alter the default behavior.  The first argument, -0g, disables the compiler from optimizing the assembly instructions.  By default, the compiler (we’re using gcc) intelligently optimizes code—one way is by reducing the number of instructions.  The second argument, -S, instructs the compiler to stop at  just before it creates the machine code (unreadable by humans), and, instead, directs the compiler to create a file (modulus.s) containing the assembly instructions.

    # gcc -Og -S modulus.c
    

    The command above outputs a file, modulus.s, with the contents (unrelated lines removed):

    modulus:
    movl	%edi, %eax
    cltd
    idivl	%esi
    movl	%edx, %eax
    ret

    Let’s step through and explain each of the assembly instructions, one-by-one.

    mov

    When we want our CPU to perform any action, such as adding, substracting, multiplying, or dividing numbers, we need to first move bytes of data (an intermediate value, or from memory) to a register.  We move data to registers with the mov command, which is capable of moving data from:

    • register to memory
    • memory to register
    • register to register
    • immediate value to memory
    • immediate value to register

    The assembly above first moves data from one register (%edi) to another register (%eax).  This is necessary since subsequent instructions, such as cltd, rely on data being present in the %eax register.

    cltd

    cltd stands for “convert long to double long.”  But before we dig into why we need this instruction, we must detour and briefly explain the next instruction in line, idivl.

    When we send an idivl (divide long) instruction, the processor calcutes the numbers and store the quotient in one register, and stores the remainder in another.  These two values stored in the register are half the size; if the dividend is 32 bits, the cpu stores (1) 16-bit value in one register, and the other 16-bit value in another.

    Therefore, if we expect a 32-bit quotient and 32-bit remainder, then the dividend (which is, presumably, 32-bits) must be doubled—to 64-bits.  In our modulus program, we set a return value of type int.

    idivl

    idivil divides the numerator (stored in %eax) by the argument (or denominator) — the argument that we passed to the instruction.  In our assembly example above, idivl divides the value stored in %rax, by the value in %esi—x and y, respectively.

    movl

    At the end of a sequence, assembly (by default) returns whatever value is stored in the register %rax.  Therefore, the final instruction moves the remainder, not the quotient, from %edx to %rax.

    Wrapping up

    I hope I was able to share a thing or two on how a higher level program ultimately breaks down into simple assembly instructions.

    [1] I was banging my head against the wall until I found a easy to understand reason why we must convert long to double long: http://www.programmingforums.org/post12771.html

  • Calculating page table entry size

    How many page table entries are required given a virtual address size and page size?

    I was confronted with this question when reading Computer Systems from a programmer’s perspective on virtual memory management (VMM) which forced me to reread the chapter, over and over. It wasn’t until I forced myself to close the book, step away from the problem for a day, was I able to fully grasp what was being asked:

    Page table entries problem
    Page table entries problem

    Let’s narrow the scope and focus on a single instance, where n is 16 and P is 4K;  the question can be easily rephrased into something that makes more sense (to me): “How many 4k pages can fit into 16 bit memory address”?

    First, how any bits are required, for a 4K block size, to represent a single page? Well, 212 is 4096 so … that’s sufficient. Therefore, we can rephrase the abstract question that we can solve mathematically: “How many 12 bit pages can fit into a 16 bit virtual address size?”

    So, the problem now boils down simple division— 216 / 212,which can be expressed as 216-12: 24, which is sixteen (16). That’s 16 page table entries that required for a virtual address of size of 16 combined with a page size of 4k.

    The same approach can be taken for all the remaining practice problems. Convert the page (e.g. 4k, 8k) size into bits and divide that into the virtual address size (e.g. 16, 32)

  • A simple C problem

    I’m slowly working through “Computer Systems from a programmer’s perspective” [1] , struggling every bit of the way. I stopped beating myself up about it, though.

    The code snippets below are in C. The first function, inplace_swap, swaps two integers. The code itself looks lacks intent but I promise you, it swaps two integers.

    The second function, reverses an array. Coming from the Python world, I wondered why in the world we need to pass incount as an additional argument. I discovered the additional argument is absolutely necessary. C functions will always collapse array arguments into a pointer. The length of pointer, which is nothing more than a memory address, cannot be calculated [2].

    #include <stdio.h>
    
    void inplace_swap(int *x, int *y){
        *y = *x ^ *y;
        *x = *x ^ *y;
        *y = *x ^ *y;
    }
    
    void reverse_array(int a[], int cnt){
        int first, last;
        for (first = 0, last = cnt - 1; first <= last; first++, last--){
            inplace_swap(&a[first], &a[last]);
        }
    
    }
    

    This function appears to work just fine, at first. It reverses even numbered array flawlessly, however, fails to reverse an odd numbered array.

    void print_array(int a[], int cnt){
        int i;
        for (i=0; i < cnt; i++){
            printf(" %d", a[i]);
        }
        printf("\n");
    }
    
    int main(){
        int a[] = {1,2,3,4};
        int a_length = sizeof(a)/sizeof(int);
        printf ("Reversing 1 2 3 4 : ");
        reverse_array(a, a_length);
        print_array(a, a_length);
    }
    
    $ ./reverse_array
    Reversing 1 2 3 4 :  4 3 2 1
    
    int b[] = {1,2,3,4,5};
    int b_length = sizeof(b)/sizeof(int);
    printf ("Reversing 1 2 3 4 5:");
    reverse_array(b, b_length);
    print_array(b, b_length);
    
    $ ./reverse_array
    Reversing 1 2 3 4 5: 5 4 0 2 1
    

    3 practice problems

    The author reveals there is a bug and poses 3 questions:

    1. For an array of odd length cnt = 2k + 1, what are the values of variables first and last in the final iteration of function reverse_array?
    2. Why does this call to function xor_swap set the array element to 0?
    3. What simple modification to the code for reverse_array would eliminate this problem?

    In an odd length array, the variables first and last values are the same. This explains why the middle array element is set to 0. Any number XOR’d with itself will always produce 0. Here are a few examples of XOR’ing two binary values of the same value:

    0001 ^ 0001 = 0000
    0111 ^ 0111 = 0000
    

    I grew frustrated with the last question. For ten minutes, I held back my impulse to flip to the back of the book and reveal the solution. Instead, I slapped a piece of a piece of paper and a pen onto my desk, and stepped through the each iteration. There’s something about putting the pen to paper, that activates a different part of the brain.

    1 2 3 4 5
    5 2 3 4 1
    5 4 3 2 1
    
    1 2 3 4 5 6 7
    7 2 3 4 5 6 1
    7 6 3 4 5 2 1
    7 6 5 4 3 2 1
    

    The little fix

    In an array with odd number of elements, the middle item is always in its final position. We should never call inplace_swapfor the middle item. Instead, we change the condition in the for loop from first <= last to first < last.

    Footnotes

    [1] My colleague Richard – who graduated from Carnegie Mellon – recommended this book. Its the only book he read from to back, twice.
    [2] http://stackoverflow.com/questions/492384/how-to-find-the-sizeofa-pointer-pointing-to-an-array
  • Teaching as a form of learning: Binary Search Trees (BST)

    Last week, an e-mail was sent out to an internal mailing list [1], looking for volunteers to lecture on data structures & algorithms topics, but nobody replied. I jumped at the opportunity. I volunteered to lecture on binary search trees, without any previous knowledge on the topic. Why in the world would I volunteer to teach something that I know nothing about ? Well, one of the best ways to learn something is attempt to teach it. I love teaching as much as I love learning; teaching forces you to deeply understand the subject, internalize the material, and requires distilling the concepts into digestible bits and pieces. Here’s Ken Thompson’s advice [2].

    But I love teaching: the hard work of the first class, the fun of the second class. Then the misery of the third.

    —Ken Thompson

    In the post, I’m covering what a binary search tree is, its structure, and the basic operations: find, insert, and delete. We also cover the three methods for traversing the tree. At the end of this post are practice problems to help drive the concepts home.

    Next is an example problem that glues the concepts together.

    Example: Badging in & out of the office

    Picture an office requiring employees to badge in and out as they enter and exit the premise. We want to keep track of the employees in the building. We also want to be check if a specific employee is currently in the building. Let’s implement a binary search tree to maintain this dataset.

    As employees badge in, we insert the Employee object (using the employee ID as the key) into the three. When employees badge out, we remove them from the tree. Occassionally, we search the database asking “Is Employee X currently in this building?”

    Tree structure and performance

    What do binary search trees offer over other data structures [3] ?

    The binary tree data structure elegantly combines the flexibility and benefits of two existing data structures: linked lists and arrays. Linked lists efficiently add and delete nodes, by updating pointers, but inefficiently search. Linked lists require iterating over every node (i.e “from the head of the list”) until the desired node is found. Arrays, on the other hand, are perfect for searching – when taking advantage of the index – but are unsuitable for insertions/deletions. When inserting at a[i], all indexes greater than i must be shifted up; when an item is deleted at a[i], all indexes greater than i must be shifted down.

    This table compares the average runtime complexity of the three data structures.

    Data Structure Search Insert Delete
    Arrays O(1) O(N) O(N)
    Linked Lists O(N) O(1) O(1)
    BSTs O(logn) O(logn) O(logn)

    Binary search trees are suitable for specific problems. Binary search trees can be used to solve problems such as runway reservations [3]. Before we dive into the structure and implement the methods, let’s cover some terminology.

    Tree Terminology

    Here’s a visual representation of a binary search tree.

        15
       /  \
      7    21
     / \
    5  10
    

    A tree consists of nodes that are connected by edges. Those lines above represent the edges.

    • Path – Walking node to node, along the edges, creates a sequence known as the path.
    • Root – The node at the very top of the tree. There is only one root in the tree.
    • Parent – Every node, except for the root, has an edge connecting to the node above it.
    • Traversechecking or performing an operation on the node
    • Leaf – a node that contains no children
    • Level – The number of generations from the root node (level 0). For example, the root’s childrens are at level 1.

    Structure & Operations

    The structure of a tree is straight forward. It contains a reference to its root node, which is an instance of the Node class. TheNode class contains two references: leftChild, rightChild. The leftChild references a node whos key (i.e “used for comparing”) is less than the current node’s key, and rightChild references a node with a key greater than the current node’s key. This ordering restriction is a property of BSTs, but not all trees.

    Here’s some sample code of the data structure:

     public class BinaryTree<E> {
         Node root;
    
         private class Node<E> {
             int key;
             E data;
             private Node leftChild;
             private Node rightChild;
    
             @Override
             public String toString(){
                 String myString = "<Node [" + this.key + "]>";
                 return myString;
             }
         }
    }
    

    Searching

    Searching for the node is quite easy. Our method, find, accepts a key parameter. We start at the root. If the key we are searcing for is less than the node’s key, continue walking along the edge connecting to the leftChild. If the key is greater than the node’s key, continue walking along the edge connecting to the rightChild. When the key we are searching for is equal to that of the node, return the node.

    public Node<E> find(int key){
        Node<E> currentNode = this.root;
        while(currentNode !=null){
            if (currentNode.key == key){
                return currentNode;
            }
    
            if (key < currentNode.key){
                currentNode = currentNode.leftChild;
            } else {
                currentNode = currentNode.rightChild;
            }
        }
    
        return currentNode;
    }
    

    Inserting

    Inserting a node is a little more involved, but not by much. We apply an algorithm similar to find but we check our base condition is when the leftChild or rightChild is null.

    public void insert(int key, E data){
        Node<E> newNode = new Node<E>();
        newNode.key = key;
        newNode.data = data;
        if(root == null){
            root = newNode;
            return;
        }else{
            Node<E> currentNode = root;
            Node<E> parent;
            while(true){
                parent = currentNode;
                if(key < currentNode.key){
                    currentNode = currentNode.leftChild;
                    if (currentNode == null){
                        parent.leftChild = newNode;
                        return;
                    }
                }else{
                    currentNode = currentNode.rightChild;
                    if (currentNode == null){
                        parent.rightChild = newNode;
                        return;
                    }
                }
    
            }
        }
    }
    

    Traversing

    There are three ways to traverse a binary tree:
    • Pre Order
    • In Order
    • Post Order

    The most common of the three is in order. In our example code, we only print the currentNode.

    public void inorder(Node<E> localRoot){
    
        if(localRoot !=null){
            Node<E> currentNode = localRoot;
            inorder(currentNode.leftChild);
            System.out.println(currentNode);
            inorder(currentNode.rightChild);
        }
    
    
    }
    
    public void preorder(Node<E> localRoot){
        if (localRoot != null){
            Node<E> currentNode = localRoot;
            System.out.println(currentNode);
            preorder(currentNode.leftChild);
            preorder(currentNode.rigthChild);
        }
    }
    
    public void postOrder(Node<E> localRoot){
        if (localRoot != null){
            Node<E> currentNode = localRoot;
            postOrder(currentNode.leftChild);
            postOrder(currentNode.rightChild);
            System.out.println(currentNode);
        }
    }
    

    Deleting

    There are three cases we must consider when deleting a node:
    • node with zero children
    • node with single child
    • node with two children

    Let’s consider the first case whend deleting a node with zero children.

     Before               After
    
        15                  15
       /  \                /  \
      7    21             7    21
     / \                   \
    5  10                   10
    

    Delete with zero children

    We maintain a reference to the deleted node’s parent. We “delete” the node by updating the parent’s pointer (leftChild or rightChild) to null. The only edge case we consider is if the deleted node is the root. In thtat case, we just update root to null.

    public boolean delete(int key){
        Node<E> currentNode = root;
        Node<E> parent = currentNode;
        boolean isLeftChild = false;
    
        while (currentNode.key != key){
            parent = currentNode;
            if (key < currentNode.key){
                isLeftChild = true;
                currentNode = currentNode.leftChild;
            } else {
                currentNode = currentNode.rightChild;
            }
            // node not found
            if (currentNode == null){
                return false;
            }
        }
        // Case 1: No children
        if (currentNode.leftChild == null && currentNode.rightChild == null){
            if (currentNode == root){
                root = null;
                return true;
            }
            else if (isLeftChild){
                parent.leftChild = null;
                return true;
            } else {
                parent.rightChild = null;
                return true;
            }
        }
        return false;
    }
    

    Delete node with single child

    We just covered how to update the tree when a node has no children. Deleting a node, with a single child, is more involved, but just a little more. There’s four cases for the deleted node:

    • it’s the leftChild of its parent and has a leftChild
    • it’s the leftChild of its parent and has a rightChild
    • it’s the rightChild of its parent and has a leftChild
    • it’s the rightChild of its parent and has a rightChild

    In the first two diagrams, we are deleting node 7. In the last two, we are deleting node 21.

         Left child with single left child
    
        15                15
       /  \              /  \
      7   21            5   21
     /
    5
    
         Left Child with a single right child
    
        15                15
       /  \              /  \
      7   21            9   21
       \
        9
    
         Right child with single left child
    
        15                15
       /  \              /  \
      7    21           7    18
          /
         18
    
         Right child with single right child
    
        15                15
       /  \              /  \
      7    21           7    25
             \
              25
    
    if (currentNode.leftChild != null && currentNode.rightChild == null){
        if(currentNode == root){
            root = currentNode.leftChild;
        }
        else if(isLeftChild){
            parent.leftChild = currentNode.leftChild;
        } else {
            parent.rightChild = currentNode.leftChild;
        }
    }
    else if(currentNode.rightChild != null && currentNode.leftChild == null){
        if (currentNode == root){
            root = currentNode.rightChild;
        }
        else if(isLeftChild){
            parent.leftChild = currentNode.rightChild;
        }
        else {
            parent.rightChild = currentNode.rightChild;
        }
    }
    
        15                15
       /  \              /  \
      7   21            8   21
     / \               / \
    5   9             5   9
       /
      8
    
        15
       /  \
      7   21
     / \
    5   9
         \
         12
        /  \
       10   14
    

    When deleting a node, we replace the node with its inorder successor. The successor is the minimum node, within the deleted node’s right child subtree. Take a moment and think about that. Why the right subtree, and not the left?

    Would the next largest number ever be to the left of the current node? No. By definition, all nodes to the left are less thanand all node to the right are greater than. Therefore, the smallest number (greater than the current node’s key) will be the minimum node, to the right.

    Once we identify the successor, we update the successor’s parent leftChild pointer with the successor’s rightChild pointer (this may be null). We finalize the connections by updating the successor’s rightChild pointer to the delete node’s rightChild.

    public Node<E> getSuccesor(Node<E> toBeDeleted){
    Node<E> parentSuccessor = toBeDeleted;
    Node<E> successor = toBeDeleted;
    Node<E> currentNode = toBeDeleted.rightChild;
    while(currentNode.leftChild != null){
        parentSuccessor = successor;
        successor = currentNode;
        currentNode = currentNode.leftChild;
    }
    if(toBeDeleted.rightChild != successor){
        parentSuccessor.leftChild = successor.rightChild;
        successor.rightChild = toBeDeleted.rightChild;
    }
    return successor;
    

    Drawbacks

    The binary search tree sounds like the dream, so why use an alternative data structure?

    My manager shared a story of an interview candidate who inserted a sequence of sorted items into a binary search tree. Pause for a moment and visualize the data structure. What do you get?

    A linked list!

    This increases the height of the tree and we lose advantage of the O(logn) performance. Binary Search tree is suited best forrandomized keys, not sorted.

    Example

    public class Employee {
        private int id;
        private String firstname;
        private String lastname;
    
        public Employee (int employeeid, String firstname, String lastname){
            this.id = employeeid;
            this.firstname = firstname;
            this.lastname = lastname;
        }
    
        @Override
        public String toString(){
            // < 05: Joe Schmoe >
            String myString = "< " + this.id + " " + this.firstname + " >";
            return myString;
        }
    
        public static void main(String[] args){
            Employee jboyd = new Employee(15, "jess", "boyd");
            Employee mstreiter = new Employee(7, "myles", "streiter");
            Employee mdavis = new Employee(21, "matt", "davis");
            Employee mmadsen= new Employee(5, "martin", "madsen");
            Employee mchung = new Employee(19, "matt", "chung");
    
            BinaryTree<Employee> employees = new BinaryTree<Employee>();
            employees.insert(jboyd.id, jboyd);
            employees.insert(mstreiter.id, mstreiter);
            employees.insert(mdavis.id, mdavis);
            employees.insert(mmadsen.id, mmadsen);
    
            // Is myles in the building?
            // returns true
            System.out.println(employees.find(mstreiter.id));
    
            // Jess clocks out
            employes.delete(jboyd.id);
    
            // Is Matt in the building?
            // returns false
            System.out.println(employees.find(mchung.id));
    
        }
    }
    

    Exercises

    1. Write a function that verifies that a binary search tree is valid.
        15
       /  \
      7   21
     / \
    5   17
    

    Acknowledgements

    Thanks to Jess Boyd for correcting my initial draft, Matt Davis for pointing out the drawbacks of BSTs, and Martin Madsen for techniques to make the post more comprehensible.

    Footnotes

    [1] In a previous post, I shared how I am teaching myself data structures and algorithms. In the breakroom work, a colleague overheard a group forming to study together – the weekend bravehearts. I was lucky enough to catch the group at the head end. We meet Friday after work, and Saturday mornings. If you live in the greater Seattle area and wanted to join, e-mail me and I’ll bring you along.
    [2] This is a quote pulled from interview, from the book “Coders at work”, with Ken Thompson
    [3] (1, 2) MIT’s lecturer presents a scheduling problem and compare the advantages and disadvatages of previously discussed datastructures (e.g “linked lists”, “arrays”, “heaps”). I recommend watching it:https://www.youtube.com/watch?v=9Jry5-82I68
  • Python progression path – quiz

    Python progression path – From apprentice to guru is a popular StackOverflow post. To categorize whether a person should take his beginner/intermediate course, one of the commentors posted this question:

    python progression quick

    I can better answer this question after reading Fluent Python. Example 1 and Example 2 deal with immutable and mutable data types – respectively. Let’s address each example individually.

    Example 1

    The id built in method returns the object’s memory address. We’ll use this to inspect the memory address for x and ythroughout the examples.

    x and y point to the same memory address when they are first assigned.

    >>> x = 42
    >>> y = x
    >>> id(x)
    4298172632
    >>> id(y)
    4298172632
    

    Because the value of x is immutable (e.g INT) and cannot be modified, by definition, a new memory address is allocated when x is modified. But, y‘s memory remains unmodified:

    >>> x = x + 1
    >>> id(x)
    4298172608
    >>> id(y)
    4298172632
    >>>
    

    Example 2

    Like Example 1, the memory address starts off the same:

    >>> x = [1,2,3]
    >>> y = x
    >>> id(x)
    4299948256
    >>> id(y)
    4299948256
    

    Now that we are dealing with mutable data types, x can be modified in place and the memory address does not change:

    >>> id(x)
    4299948256
    >>> id(y)
    4299948256
    

    Mutable default arguments

    This quiz seems trivial but it isn’t. Understanding this will prevent a novice mistake of using mutable default arguments.

    >>> def update(x, y=[]):
    ...     y.append(x)
    ...     return y
    ...
    >>> list1 = ['a', 'b', 'c']
    >>> list2 = [1, 2, 3]
    >>> update('d', list1)
    ['a', 'b', 'c', 'd']
    >>> update(4, list2)
    [1, 2, 3, 4]
    >>> list1
    ['a', 'b', 'c', 'd']
    >>> list2
    [1, 2, 3, 4]
    >>> update('a')
    ['a']
    >>> list3 = update('b')
    >>> list3
    ['a', 'b']
    >>> list4 = update('hello')
    >>> list4
    ['a', 'b', 'hello']