Author: Dileep

  • Climbing Stairs

    Climbing Stairs is probably the first problem that forces you to actually think about optimization. For many people, it’s also their first real introduction to Dynamic Programming (DP). At its core, DP is just the idea of remembering work you’ve already done so you don’t repeat it. It’s a great problem to get your feet wet in that domain.

    The Problem

    The problem asks that you climb a staircase by either taking one step or two steps. How many distinct ways can we climb to the top given a stair case of size n?

    For example, if we are given a staircase of size 2 then there are only two ways to climb to the top.

    • 1 step + 1 step 
    • 2 steps

      Similarly if n was 3 then we would have 3 ways to climb the staircase:

      • 1 step + 1 step + 1 step
      • 2 steps + 1 step
      • 1 step + 2 steps

        Solution

        Lets try to solve this problem by brute force by hand first. If we try to write out solutions by hand we will start to notice an interesting pattern:

        If n was 4 we would have 5 ways to climb the stairs

        • 1 step + 1 step + 1 step + 1 step
        • 2 steps + 1 step + 1 step
        • 1 step + 2 steps + 1 step
        • 1 step + 1 step + 2 steps
        • 2 steps + 2 steps

          Similarly if we do n = 5 we would get 8 (I’ll skip writing out all the possible combinations). More importantly we would notice an interesting pattern emerge:

          If n = 1 then we get 1

          If n = 2 then we get 2

          If n = 3 then we get 3

          If n = 4 then we get 5

          If n = 5 then we get 8

          …. and so on

          Do you see the pattern? Each number is the sum of the two before it. That’s just the Fibonacci sequence. This whole problem reduces to computing Fibonacci numbers.

          Naive Solution

          Anyways, the naive solution is to code the sequence using recursion:

          public static int climbStairs(int n) {
            if (n == 1) {
                return 1;
            }
          
          
            if (n == 2) {
                return 2;
            }
          
          
            return climbStairs(n - 1) + climbStairs(n - 2);
          }
          

          We set up a base case for the first two numbers in the sequence and otherwise we would work our way backwards to find the solution we need. This would give us the correct solution but this would not be accepted by LeetCode. It would get rejected not because it’s incorrect but because it’s too slow. As N gets large the function becomes slower dramatically. 

          Naive Solution Time and Space Complexity

          How slow exactly? Let’s find out. If we graph out an execution of this method for n = 5 the recursions make a pretty binary tree for us:

          climbStairs(5)

          ├── climbStairs(4)

          │   ├── climbStairs(3)

          │   │   ├── climbStairs(2) → 2

          │   │   └── climbStairs(1) → 1

          │   └── climbStairs(2) → 2

          └── climbStairs(3)

              ├── climbStairs(2) → 2

              └── climbStairs(1) → 1

          If we count each node in this tree as an execution of our method then this means the cost to run this solution will grow exponentially as n gets bigger. In general that means that our algorithm has a time complexity of O(2^N). Since the computer walks through this tree depth first (it goes from the root to one leaf before it visits another leaf) the space complexity is O(N) since the path from the root to the leaf of the tree would be at most N. As a result at most N calls of the recursive function would be on the call stack (and thus memory) at any given time.

          Memoized Solution

          We can make this program much faster by taking advantage of a pattern we see in the tree above. We often repeat the same work throughout the tree. What if instead of redoing that we remembered what we had done last time and used that? The act of remembering previous calculations is the core technique of dynamic programming. We can store the results of previous calculations in a variable called memo and do the same calculations as we go.

          public static int climbStairs(int n) {
             List<Integer> memo = new ArrayList<>();
             // take care of our base cases of when n = 0, 1, or 2
             memo.add(0);
             memo.add(1);
             memo.add(2);
          
          
             while(n >= memo.size()) {
                 int currentSize = memo.size();
                 memo.add(memo.get(currentSize - 1) + memo.get(currentSize - 2));
             }
          
          
             return memo.get(n);
          }

          Like before we record some values for the base cases but instead of calling the function over and over we just look at the last two values we calculated to determine the next one. Since we only ever do calculation for a new number once this solution is much faster.

          Memoized Time and Space Complexity

          How much faster? Well if we are only ever calculating new numbers once then at most the number of times we would calculate numbers in the sequence is O(n). Similarly, since our code needs to store an array of size n our space complexity remains O(n).

          Optimal Space Solution

          We can do even better than this in terms of space though. Since we only really need to remember the last two values it doesn’t make sense to keep all the old values we calculated in an array. Instead we could just keep track of the last two values to be more efficient.

          public int climbStairs(int n) {
              int current = 2;
              int before = 1;
          
              if (n == 1) {
                  return before;
              }
          
              if (n == 2) {
                  return current;
              }
          
              int temp;
              for(int i = 3; i <= n; i++) {
                  temp = before;
                  before = current;
                  current = temp + before;
              }
          
              return current;
          }
          

          This lets us keep our O(n) speed but actually reduces our space usage to just O(1).

          Key Takeaways

          The solution starts off as a straightforward variation of generating the fibonacci sequence but forces the user to approach the same problem with a different mindset as they run against performance constraints. In many ways this program is a great introduction to dynamic programming and evolving past beginner programming challenges to more advanced problems. 

        1. How to Merge Two Sorted Lists in Java

          Merging two sorted lists is both a programming technique and a LeetCode problem! It’s a part of the blind 75 and is a foundational problem to understand since it sets us up for understanding important algorithms like merge-sort down the line.

          If you would prefer a video version of this explanation you can check it out at my youtube channel!

          The Problem

          According to the LeetCode website:

          You are given the heads of two sorted linked lists list1 and list2.

          Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

          Return the head of the merged linked list.

          Example 1:

          Input: list1 = [1,2,4], list2 = [1,3,4]

          Output: [1,1,2,3,4,4]

          Example 2:

          Input: list1 = [], list2 = []

          Output: []

          Example 3:

          Input: list1 = [], list2 = [0]

          Output: [0]

          Solution

          The algorithm itself is straightforward.Since we know that the provided linked lists are already sorted we can go through the elements in each list sequentially, compare them to each other, and progressively build the output as we read both. More specifically the algorithm would look like this:

          1. While we have data to read in both list1 and list2:
            1. Compare the current value we are on at list1 to the current value we are on at list2
              1. If the current value of list1 is less than that of list2
                1.  add the current value list1 to the output list
                2.  Mark the current value of list1 as read and get ready to read the next.
              2. Otherwise
                1.  add the current value of list2 to the output list
                2.  Mark the current value of list2 as read and get ready to read the next.
          2. If we still have data in list1 but not in list2 then append all the remaining data in list1 to the output.
          3. otherwise append all the remaining data in list2 to the output since we know that list2 is not empty.

          The logic seems straightforward but there are actually two different implementations for it: an iterative and a recursive approach. We will walk through both in this post. Let’s start with the iterative implementation since it is easier to understand.

          Iterative implementation

          The iterative approach maps to our pseudocode above quite nicely.

          public ListNode mergeTwoListsIterative(ListNode list1, ListNode list2) {
             // we initialize the output with a dummy node to make adding to it easier
             ListNode output = new ListNode();
             ListNode outputPointer = output;
          
          
             // go through the nodes of each list and compare them to each other
             while(list1 != null && list2 != null) {
                 if (list1.val < list2.val) {
                     outputPointer.next = list1;
                     list1 = list1.next;
                 } else {
                     outputPointer.next = list2;
                     list2 = list2.next;
                 }
                 outputPointer = outputPointer.next;
             }
          
          
             // flush the remaining data of whichever list is not empty to the output
             outputPointer.next = (list1 != null) ? list1 : list2;
          
          
             // send output but without the dummy node
             return output.next;
          }
          

          Recursive implementation

          The recursive implementation is the same idea but the main paradigm shift is that instead of tracking where we are using pointers we rely on the function’s call stack to keep track of the ordering for us. This results in less code but it can be harder to read if you are not comfortable with the concept of recursion. 

          If you look at our code below it is very much like our pseudocode but in reverse. We address our base cases if either of the lists are empty and otherwise compare and sort the nodes one by one.

          public ListNode mergeTwoListsRecursive(ListNode list1, ListNode list2) {
             if (list1 == null) {
                 return list2;
             }
          
          
             if (list2 == null) {
                 return list1;
             }
          
          
             if (list1.val < list2.val) {
                 list1.next = mergeTwoListsRecursive(list1.next, list2);
                 return list1;
             } else {
                 list2.next = mergeTwoListsRecursive(list1, list2.next);
                 return list2;
             }
          }
          

          Time and Space Complexity

          The time complexity is actually the same regardless of whether you implement this in the recursive or the iterative manner. Since we have to go through all the elements of each list once the time complexity is O(m + n). The space complexity is O(1) for the iterative solution. We only create pointers once in our code and reuse them for the rest of the program. The recursive solution has space complexity of O(m + n) because we can build up a call stack the size of both lists combined.

          Key Takeaways

          Hopefully this post was enlightening. We explored two ways to merge two sorted linked lists. We also walked through the space and time complexities of each solution. Although this is a simple problem it is foundational for understanding more complex algorithms like merge sort which are used in more advanced LeetCode problems. 

        2. How to Solve Valid Parentheses in Java

          In the ever present LeetCode grind that our industry puts us through I decided to solve the “Valid Parentheses” problem in LeetCode today. For a LeetCode problem it’s surprisingly practical and provides insight into how our IDEs know when we are missing a bracket within our code. Like other LeetCode problems it’s relatively straightforward once you pick the right data structure.

          Before we dive in if you are the type that prefers videos over reading do check out this walk through on my Youtube channel.

          The Problem

          We are given a string containing the following characters: ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[‘, and ‘]’.  We need to write a function that determines if the input string is valid. The problem defines ‘valid’ as:

          1. Open brackets must be closed by the same type of brackets.
          2. Open brackets have to be closed in the right order.
          3. Every close bracket has a corresponding bracket of the same type.

          Let’s work through some examples:

          Example 1:

          Input s  = “[()]”

          Output: true

          This makes sense since the string starts with “[“ and is closed by “]”. Also the inner “()” closes itself since the opening and closing brackets are next to each other.

          Example 2:

          Input s  = “([)]”

          Output: false

          This makes sense since the string starts with “(“ and is closed by “]”. Although both “[“ and “(“ characters have a matching pair in the string the ordering is wrong and they don’t close each other out properly.

          Solution

          The core challenge with this problem is keeping track of the ordering of the open brackets as they come in and checking that they get closed later down the line. If we had to brute force this we could repeatedly scan the string, remove matching pairs and work our way through the string. Normally I would like to write a brute force solution, but in this scenario the optimal solution is actually simpler and easier to understand. There is actually an ideal data structure for this. A stack! The idea is that we push any open brackets we see onto the stack and when we see a closed bracket we pop the stack to see if the corresponding bracket is there. If it is there then we know the string is valid. If not then we know it’s invalid. If we go through the entire string and we still have stuff left in our stack then we know the string isn’t valid as well.

          So what does the pseudocode for this look like?

          1. Initialize an empty stack to keep track of ordering of brackets 
          2. For each character in the input string:
            1. If it’s an open brace then push it onto the stack
            2. If it’s a closed brace
              1. If the stack is empty or if we pop the stack and it’s not the corresponding bracket then we return false.
          3. If the stack is empty the string is valid, otherwise it’s invalid

          Now let’s code it out.

          public boolean isValid(String s) {
             Stack<Character> stack = new Stack<>();
             for(int i = 0; i < s.length(); i++) {
                 char current = s.charAt(i);
                 switch (current) {
                     case '(':
                     case '{':
                     case '[':
                         stack.push(current);
                         break;
                     case ')':
                         if (stack.empty() || stack.pop() != '(') return false;
                         break;
                     case '}':
                         if (stack.empty() || stack.pop() != '{') return false;
                         break;
                     case ']':
                         if (stack.empty() || stack.pop() != '[') return false;
                         break;
                     default:
          		           // do this in case LeetCode ever throws us a bad input
                         throw new IllegalArgumentException();
                 }
             }
          
          
             return stack.isEmpty();
          }
          

          If you are a seasoned LeetCoder you may prefer to write this solution using a map. That works too and the code is a bit cleaner. However in my experience I have found that switch statements to be more explicit and readable for a small character set. However for those who do prefer maps here is what that solution looks like:

          public boolean isValid(String s) {
             Map<Character, Character> map = new HashMap<>();
             map.put(')', '(');
             map.put('}', '{');
             map.put(']', '[');
          
             Stack<Character> stack = new Stack<>();
          
             for (char c : s.toCharArray()) {
                 if (!map.containsKey(c)) {
                     stack.push(c);
                 } else (map.containsKey(c)) {
                     if (stack.isEmpty() || map.get(c) != stack.pop()) {
                         return false;
                     }
                 }
             }
             return stack.isEmpty();
          }
          

          Since we have to traverse the entire string at least once to know if it’s valid or not this solution has a time complexity of O(n). Also in the worst case scenario of all the characters being open brackets and stored on our stack then the space complexity of this solution is also O(n). This time complexity and space complexity holds true for both the switch statement and the map approach.

           

          Key Takeaways

          Although this problem is relatively straightforward I think it still teaches the critical skill of choosing the right data structure for the problem. Solving this problem without some stack-like data structure would be incredibly painful. Also, unlike most LeetCode problems it does actually have a use in the real world. Anyone who solves this can appreciate how their IDE’s bracket checking works. So I hope that the next time you see your IDE telling you that a bracket is missing you remember this problem.

        3. How to Solve Two Sum in Java

          Today we’ll be tackling the quintessential problem everyone starts with on LeetCode: Two Sum. It’s approachable but teaches a genuinely useful lesson: how to  pick the right data structure for a problem.

          The Problem

          The problem from LeetCode looks like this:

          Given an array of integer nums and an integer target, return the indices of the two numbers such that they add up to target.

          You may assume that each input would have exactly one solution, and you may not use the same element twice. You can return the answer in any order.

          Example 1:

          Input: nums [3, 2, 4] and target = 6

          Output: [1,2]

          Explanation: because nums[1] + nums[2] == 6 so we return [1, 2]

          Example 2:

          Input: nums [3,3] and target = 6

          Output: [0,1]

          Explanation: because nums[0] + nums[1] == 6 so we return [0,1]

          From the examples provided we can learn two new things about the problem. Firstly the numbers in the input are unsorted and that although we cannot use the same number twice a number can appear multiple times in the input.

          Brute Force

          I like to try to solve problems using a naive brute-force approach first. It often gives insight into how to give the optimal solution for the problem later. So what does a brute force algorithm for this look like? Well we can try computing the sum of every pair of numbers and seeing if they equal our target number. We can pseudocode an algorithm like below.

          1. For each number in the input
            1. Go through every other number in the input
              1. Add the current number and the other number up
              2. If their sum equals target then return their indexes
              3. Otherwise go to the next pair of numbers
          public int[] twoSum(int[] nums, int target) {
             // go through each number in the input nums
             for(int i = 0; i < nums.length; i++) {
                 // go through each other number in the input
                 for(int j = i + 1; j < nums.length; j++) {
                     // if the sum of each number is equal to target then return the indexes
                     if(nums[i] + nums[j] == target) {
                         return new int[]{i, j};
                     }
                     // Otherwise go to the next pair of numbers
                 }
             }
          
             // we should never reach this but if we do then we got a bad input
             throw new IllegalArgumentException();
          
          }

          With that coded up let’s take a look at the time and space complexity of our work. In terms of space we don’t allocate any new memory so the solution is  O(1) in space. When it comes to time complexity we are O(n2). The rationale for the time complexity is that the worst case scenario for this algorithm is that the solution is the last two numbers in the input array. In that situation we would have to loop through the entire array for each preceding number until we reach the last two.

          Key Insight for Optimization

          The key to coming up with an optimal solution actually comes from looking at the brute-force approach. If we analyze it we notice that we lose a lot of time checking each individual pair in the inner for loop. If we could do that more efficiently then the algorithm would become dramatically faster. 

          Turns out there is actually a data structure that could help with this! We could introduce a hashmap to look up numbers and indices efficiently. Using that we can avoid the extra for-loop and check immediately if we have a number that adds up to target or not. How do we know what key to look up? Well for any number and a target number we know that we need to find something equal to target - current number to have a solution. So we can look up that target - current value in our HashMap quickly.

          Optimal Solution

          So what does the optimal solution look like? The algorithm would look something like this. 

          1. We create a map that stores values to their indexes
          2. We go through each number in the input array
            1. We calculate the number we need to add our current number to get the target number
            2. We check if our map has that number we need
              1. If yes then return the indexes and we are done!
              2. Otherwise add the current number and index to our map and move on

          So lets try coding that out.

          public int[] twoSum(int[] nums, int target) {
             // We create a map that stores values to their indexes
             Map<Integer, Integer> valToIndex = new HashMap<>();
          
          
             // We go through each number in the input array
             for (int i = 0; i < nums.length; i++) {
                 // We calculate the number we need to add our current number to get the target number
                 int goal = target - nums[i];
                  //We check if our map has that number we need
                 if (valToIndex.containsKey(goal)) {
                     // If yes then return the indexes and we are done!
                     return new int[]{i, valToIndex.get(goal)};
                 }
          
          
                 // Otherwise add the current number and index to our map and move on
                 valToIndex.put(nums[i], i);
             }
          
          
             // we should never reach this but if we do then we got a bad input
             throw new IllegalArgumentException();
          }
          

          With this new approach we have actually improved our time complexity to O(n). This is because the look ups in the hashmap are O(1) and we only have to loop through all the numbers in the input array once in the worst case scenario that the two numbers we need are at the end of the input array. However this speed improvement did not come for free. Out space complexity actually got worse and became O(n). This is because we need to allocate a Map that could potentially store our entire input array!

          Key Takeaway

          I think the most valuable lesson the Two Sum problem teaches is that if you are repeatedly searching the same list of things over and over we can make that more efficient by using a HashMap lookup pattern. This technique can be used in other LeetCode problems like Four Sum and Three Sum. So hopefully this article prepares you for the next challenges ahead!

          Also if you want more of a walk through check out this Youtube Video of me solving the problem.