<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged leetcode</title>
<link>https://coderlegion.com/tag/leetcode</link>
<description>Powered by Question2Answer</description>
<item>
<title>LeetCode #198. House Robber</title>
<link>https://coderlegion.com/27438/leetcode-198-house-robber</link>
<description>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.&lt;/p&gt;
&lt;p&gt;Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: nums = [1,2,3,1]&lt;br&gt;
Output: 4&lt;br&gt;
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).&lt;br&gt;
Total amount you can rob = 1 + 3 = 4.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: nums = [2,7,9,3,1]&lt;br&gt;
Output: 12&lt;br&gt;
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9) and rob house 5 (money = 1).&lt;br&gt;
Total amount you can rob = 2 + 9 + 1 = 12.&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= nums.length &amp;lt;= 100&lt;br&gt;
0 &amp;lt;= nums[i] &amp;lt;= 400&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The problem can be solved using dynamic programming.We create a new array dp and use it to store the maximum up to a certain index. The first index has the value stored at the first index in nums.The second one takes the maximum of the first 2 values in nums.&lt;br&gt;
We then iterate through the rest of the array and store in dp,the maximum of the previous value in dp and the sum of the current value in nums and the value before the previous value in dp. We return the last value in dp once we are done traversing.&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int rob(int[] nums) {
         if(nums.length==0) return 0;
         if(nums.length==1)return nums[0];
         if(nums.length==2)return Math.max(nums[0],nums[1]);
         int [] loot = new int[nums.length];
          loot[0]= nums[0];
          loot[1]= Math.max(nums[1],nums[0]);
          for(int i=2;i&amp;lt;loot.length;i++){
            loot[i]= Math.max(nums[i]+loot[i-2],loot[i-1]);
          }
          return loot[loot.length-1];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JavaScript&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var rob = function(nums) {
  if(nums.length==0) return 0;
  if(nums.length==1)return nums[0];
  if(nums.length==2)return Math.max(nums[0],nums[1]);
  const loot = new Array(nums.length);
  loot[0]= nums[0];
  loot[1]= Math.max(nums[1],nums[0]);
  for(let i=2;i&amp;lt;loot.length;i++){
   let previousloot= loot[i-1];
   let currentloot= nums[i]+loot[i-2];
   loot[i]= Math.max(currentloot,previousloot);
  }
  return loot[loot.length-1];
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C#&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Solution {
    public int Rob(int[] nums) {
     if(nums.Length==0) return 0;
     if(nums.Length==1)return nums[0];
     if(nums.Length==2)return Math.Max(nums[0],nums[1]);
     int [] loot = new int[nums.Length];
     loot[0]= nums[0];
     loot[1]= Math.Max(nums[1],nums[0]);
     for(int i=2;i&amp;lt;loot.Length;i++)loot[i]= Math.Max(nums[i]+loot[i-2],loot[i-1]);
     return loot[loot.Length-1];   
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int rob(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
     if(nums.size()==0) return 0;
     if(nums.size()==1)return nums.at(0);
     if(nums.size()==2)return std::max(nums.at(0),nums.at(1));
     vector&amp;lt;int&amp;gt; loot;
     loot.push_back(nums.at(0));
     loot.push_back(std::max(nums.at(1),nums.at(0)));
     for(int i=2;i&amp;lt;nums.size();i++){
      loot.push_back(std::max(nums.at(i)+loot.at(i-2),loot.at(i-1)));
     }
     return loot.at(loot.size()-1);  
    }
};
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27438/leetcode-198-house-robber</guid>
<pubDate>Wed, 16 Sep 2026 20:24:40 +0000</pubDate>
</item>
<item>
<title>1366. Rank Teams by Votes</title>
<link>https://coderlegion.com/24947/1366-rank-teams-by-votes</link>
<description>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;br&gt;
In a special ranking system, each voter gives a rank from highest to lowest to all teams participating in the competition.&lt;/p&gt;
&lt;p&gt;The ordering of teams is decided by who received the most position-one votes. If two or more teams tie in the first position, we consider the second position to resolve the conflict, if they tie again, we continue this process until the ties are resolved. If two or more teams are still tied after considering all positions, we rank them alphabetically based on their team letter.&lt;/p&gt;
&lt;p&gt;You are given an array of strings votes which is the votes of all voters in the ranking systems. Sort all teams according to the ranking system described above.&lt;/p&gt;
&lt;p&gt;Return a string of all teams sorted by the ranking system.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: votes = [&quot;ABC&quot;,&quot;ACB&quot;,&quot;ABC&quot;,&quot;ACB&quot;,&quot;ACB&quot;]&lt;br&gt;
Output: &quot;ACB&quot;&lt;br&gt;
Explanation:&lt;br&gt;
Team A was ranked first place by 5 voters. No other team was voted as first place, so team A is the first team.&lt;br&gt;
Team B was ranked second by 2 voters and ranked third by 3 voters.&lt;br&gt;
Team C was ranked second by 3 voters and ranked third by 2 voters.&lt;br&gt;
As most of the voters ranked C second, team C is the second team, and team B is the third.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: votes = [&quot;WXYZ&quot;,&quot;XYZW&quot;]&lt;br&gt;
Output: &quot;XWYZ&quot;&lt;br&gt;
Explanation:&lt;br&gt;
X is the winner due to the tie-breaking rule. X has the same votes as W for the first position, but X has one vote in the second position, while W does not have any votes in the second position.&lt;br&gt;
Example 3:&lt;/p&gt;
&lt;p&gt;Input: votes = [&quot;ZMNAGUEDSJYLBOPHRQICWFXTVK&quot;]&lt;br&gt;
Output: &quot;ZMNAGUEDSJYLBOPHRQICWFXTVK&quot;&lt;br&gt;
Explanation: Only one voter, so their votes are used for the ranking.&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= votes.length &amp;lt;= 1000&lt;br&gt;
1 &amp;lt;= votes[i].length &amp;lt;= 26&lt;br&gt;
votes[i].length == votes[j].length for 0 &amp;lt;= i, j &amp;lt; votes.length.&lt;br&gt;
votes[i][j] is an English uppercase letter.&lt;br&gt;
All characters of votes[i] are unique.&lt;br&gt;
All the characters that occur in votes[0] also occur in votes[j] where 1 &amp;lt;= j &amp;lt; votes.length.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;br&gt;
This problem can be solved using 2 data structures- a hashmap and a heap. We use the map to store the votes by each judge and the heap to sort the teams. We create a comparator to sort the teams and initialize the heap using it.&lt;br&gt;
We then traverse the votes array to calculate the votes and create the map.Once this is done we add the keys of the map which are the teams to the heap and pull the elements off one by one to get the final string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public String rankTeams(String[] votes) {
      HashMap&amp;lt;Character,int[]&amp;gt;map=new  HashMap&amp;lt;&amp;gt;();
      String answer=&quot;&quot;;
      for(int i=0;i&amp;lt;votes.length;i++){
       String vote=votes[i];
       for(int j=0;j&amp;lt;vote.length();j++){
        char team=vote.charAt(j);
        if(!map.containsKey(team)){
         int [] counts= new int[vote.length()];
         counts[j]++;
         map.put(team,counts);
        }else{
          int [] counts =map.get(team);
          counts[j]++;
          map.replace(team,counts);
        }
       } 
      }
      PriorityQueue&amp;lt;Character&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((a, b) -&amp;gt; {
        int[] va = map.get(a);
        int[] vb = map.get(b);
        for (int i = 0; i &amp;lt; va.length; i++) {
            if (va[i] != vb[i]) {
            return Integer.compare(vb[i], va[i]);
             }
        }
        return Character.compare(a, b);
      });  
      for (char team : map.keySet()) pq.offer(team);
      while(!pq.isEmpty()){
       char ch=pq.poll();
       answer+=ch; 
      }
      return answer;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24947/1366-rank-teams-by-votes</guid>
<pubDate>Wed, 19 Aug 2026 18:52:21 +0000</pubDate>
</item>
<item>
<title>768. Max Chunks To Make Sorted II</title>
<link>https://coderlegion.com/24400/768-max-chunks-to-make-sorted-ii</link>
<description>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;br&gt;
You are given an integer array arr.&lt;/p&gt;
&lt;p&gt;We split arr into some number of chunks (i.e., partitions), and individually sort each chunk. After concatenating them, the result should equal the sorted array.&lt;/p&gt;
&lt;p&gt;Return the largest number of chunks we can make to sort the array.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: arr = [5,4,3,2,1]&lt;br&gt;
Output: 1&lt;br&gt;
Explanation:&lt;br&gt;
Splitting into two or more chunks will not return the required result.&lt;br&gt;
For example, splitting into [5, 4], [3, 2, 1] will result in [4, 5, 1, 2, 3], which isn't sorted.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: arr = [2,1,3,4,4]&lt;br&gt;
Output: 4&lt;br&gt;
Explanation:&lt;br&gt;
We can split into two chunks, such as [2, 1], [3, 4, 4].&lt;br&gt;
However, splitting into [2, 1], [3], [4], [4] is the highest number of chunks possible.&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= arr.length &amp;lt;= 2000&lt;br&gt;
0 &amp;lt;= arr[i] &amp;lt;= 108&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This can be solved by traversing the array twice. The first time we put the elements in a min-heap/priority queue. This ensures that we have a list of the elements sorted in ascending order. Then we create 3 integer variables. One stores the sum of the sorted elements in the heap and is named sum. The second stores the total of the elements traversed in the array and is named total. The third stores the maximum number of chunks and is named answer.&lt;/p&gt;
&lt;p&gt;We then traverse the array for the second time. We pull the topmost element from the heap and add it to sum. We add the element at the current index in the array and add to total. If the values are equal, we increment answer. Once we have traversed the array, we return the value of answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int maxChunksToSorted(int[] arr) {
     PriorityQueue&amp;lt;Integer&amp;gt; pq=new PriorityQueue&amp;lt;&amp;gt;();
     int total=0;
     int sum=0;
     int answer=0;
     for(int i=0;i&amp;lt;arr.length;i++)pq.offer(arr[i]);
     for(int i=0;i&amp;lt;arr.length;i++){
      sum+=pq.poll();
      total+=arr[i];
      if(sum==total)answer++;  
     }
     return answer;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24400/768-max-chunks-to-make-sorted-ii</guid>
<pubDate>Mon, 10 Aug 2026 17:51:40 +0000</pubDate>
</item>
<item>
<title>Leetcode #324. Wiggle Sort II</title>
<link>https://coderlegion.com/24112/leetcode-324-wiggle-sort-ii</link>
<description>&lt;p&gt;&lt;em&gt;Greedy algorithms are usually given in leetcode style technical interviews.They involve taking the maximum or minimum.Below is a an example of using the greedy approach to solve a problem.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Given an integer array nums, reorder it such that nums[0] &amp;lt; nums[1] &amp;gt; nums[2] &amp;lt; nums[3]....&lt;/p&gt;
&lt;p&gt;You may assume the input array always has a valid answer.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: nums = [1,5,1,1,6,4]&lt;br&gt;
Output: [1,6,1,5,1,4]&lt;br&gt;
Explanation: [1,4,1,5,1,6] is also accepted.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: nums = [1,3,2,2,3,1]&lt;br&gt;
Output: [2,3,1,3,1,2]&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;.1 &amp;lt;= nums.length &amp;lt;= 5 * 104&lt;br&gt;
.0 &amp;lt;= nums[i] &amp;lt;= 5000&lt;br&gt;
.It is guaranteed that there will be an answer for the given input nums.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This algorithm can be solved using the greedy approach.We copy the contents of the original array into a new one and sort it.We create two pointers. One is placed at the end of the new array,the other in the middle of it.We iterate through the original from the start.If the index is even,we take the value from the second pointer.Otherwise,we take the value from the first one.This ensures that the biggest values are always placed at the odd indexes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public void wiggleSort(int[] nums) {
     int [] nums1= new int [nums.length];
     for(int i=0;i&amp;lt;nums.length;i++)nums1[i]=nums[i];
     Arrays.sort(nums1);
     int nums1Index=nums.length-1;
     int nums1Index1=(nums.length-1)/2;      
     for(int i=0;i&amp;lt;nums.length;i++){
      if(i%2==0)nums[i]=nums1[nums1Index1--];
      else nums[i]=nums1[nums1Index--];  
     }
     return;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C#&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Solution {
    public void WiggleSort(int[] nums) {
     int [] nums1= new int [nums.Length];
     for(int i=0;i&amp;lt;nums.Length;i++)nums1[i]=nums[i];
     Array.Sort(nums1);
     int nums1Index=nums.Length-1;
     int nums1Index1=(nums.Length-1)/2;      
     for(int i=0;i&amp;lt;nums.Length;i++){
      if(i%2==0)nums[i]=nums1[nums1Index1--];
      else nums[i]=nums1[nums1Index--];  
     }
     return;   
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    void wiggleSort(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
     vector&amp;lt;int&amp;gt; nums1;
     for(int i=0;i&amp;lt;nums.size();i++)nums1.push_back(nums[i]);
     std::sort(nums1.begin(),nums1.end());
     int nums1Index=nums.size()-1;
     int nums1Index1=(nums.size()-1)/2;      
     for(int i=0;i&amp;lt;nums.size();i++){
      if(i%2==0)nums[i]=nums1[nums1Index1--];
      else nums[i]=nums1[nums1Index--];  
     }
     return;   
    }
};
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24112/leetcode-324-wiggle-sort-ii</guid>
<pubDate>Tue, 04 Aug 2026 19:55:27 +0000</pubDate>
</item>
<item>
<title>Leetcode # 107. Binary Tree Level Order Traversal II</title>
<link>https://coderlegion.com/24110/leetcode-107-binary-tree-level-order-traversal-ii</link>
<description>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Given the root of a binary tree, return the bottom-up level order traversal of its nodes' values. (i.e., from left to right, level by level from leaf to root).&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: root = [3,9,20,null,null,15,7]&lt;br&gt;
Output: [[15,7],[9,20],[3]]&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: root = [1]&lt;br&gt;
Output: [[1]]&lt;br&gt;
Example 3:&lt;/p&gt;
&lt;p&gt;Input: root = []&lt;br&gt;
Output: []&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;.The number of nodes in the tree is in the range [0, 2000].&lt;br&gt;
.-1000 &amp;lt;= Node.val &amp;lt;= 1000&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using a recursive function that returns a hashmap, a hashmap of the entire tree from top to bottom was obtained and a list was built. This list was reversed and returned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
     public HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; getRestOfTree(int level,TreeNode t){
     int value= t.val;
     List&amp;lt;Integer&amp;gt; root= new ArrayList&amp;lt;&amp;gt;();
     root.add(value);
     HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; tree= new HashMap&amp;lt;&amp;gt;();
     tree.put(level,root);
     if(t.left ==null &amp;amp;&amp;amp; t.right== null){
       return tree;
     }else{
      int nextlevel =level+1;
      if(t.left==null){
         HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; rightSubtree= getRestOfTree(nextlevel,t.right);
         tree.putAll(rightSubtree);
         return tree;
      }else if(t.right==null){
          HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; leftSubtree= getRestOfTree(nextlevel,t.left);
         tree.putAll(leftSubtree);
         return tree;
      }else{
         HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; leftSubtree= getRestOfTree(nextlevel, t.left);
         HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; rightSubtree= getRestOfTree(nextlevel, t.right);
         HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; restSubtree = new HashMap&amp;lt;&amp;gt;();
         for(Integer key:leftSubtree.keySet()){
           if(rightSubtree.containsKey(key)){
             List&amp;lt;Integer&amp;gt; rightSubtreeList = rightSubtree.get(key);
             List&amp;lt;Integer&amp;gt; leftSubtreeList= leftSubtree.get(key);
             leftSubtreeList.addAll(rightSubtreeList);
             leftSubtree.replace(key,leftSubtreeList);
           }
         }
         for(Integer key:rightSubtree.keySet()){
            if(!leftSubtree.containsKey(key)){
               List&amp;lt;Integer&amp;gt; rightSubtreeList= rightSubtree.get(key);
               restSubtree.put(key,rightSubtreeList);
            }
         }
         tree.putAll(leftSubtree);
         tree.putAll(restSubtree);
         return tree;
      }
      }
   }
    public List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; levelOrderBottom(TreeNode root) {
        if(root==null)return new ArrayList&amp;lt;&amp;gt;();
        HashMap&amp;lt;Integer,List&amp;lt;Integer&amp;gt;&amp;gt; tree = getRestOfTree(1,root);
        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; values= new ArrayList&amp;lt;&amp;gt;();
        for(Integer key: tree.keySet()){
         List&amp;lt;Integer&amp;gt; nodes= tree.get(key);
         values.add(nodes);
        }
        Collections.reverse(values);
        return values;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24110/leetcode-107-binary-tree-level-order-traversal-ii</guid>
<pubDate>Tue, 04 Aug 2026 19:48:53 +0000</pubDate>
</item>
<item>
<title>I Froze on Iterative Postorder. The Fix Was Understanding the Stack</title>
<link>https://coderlegion.com/19164/i-froze-on-iterative-postorder-the-fix-was-understanding-the-stack</link>
<description>&lt;p&gt;I had written recursive preorder, inorder, and postorder more times than I could count, three lines and a base case each. Then an onsite asked for postorder without recursion, and I blanked, pushing nodes onto a stack and popping them in the wrong order for a solid ten minutes. The traversal that had felt automatic fell apart the moment the stack became mine to manage.&lt;/p&gt;
&lt;p&gt;What stung was realizing the recursion had been carrying a hidden stack the entire time. The frames were doing the work, and nobody ever made me look at them.&lt;/p&gt;
&lt;h3 id=&quot;what-finally-clicked&quot;&gt;What finally clicked&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The three recursive traversals were one walk, not three separate facts to memorize.&lt;/li&gt;
&lt;li&gt;Recursion was quietly leaning on a stack the language managed the whole time.&lt;/li&gt;
&lt;li&gt;Iterative postorder is the one that proves whether you understand that stack.&lt;/li&gt;
&lt;li&gt;The order you choose is a data direction question, not a matter of taste.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-the-understanding-was-thin&quot;&gt;Where the understanding was thin&lt;/h3&gt;
&lt;p&gt;Writing the recursive version of all three was automatic. Explaining what the call stack was doing while they ran was not, and that gap turned out to be the whole story.&lt;/p&gt;
&lt;p&gt;I could trace the output by hand but not the machine producing it. So when the iterative version asked me to be the machine, there was nothing underneath the memorized shapes to stand on. The recursion had been doing the bookkeeping, and that was easy to mistake for understanding it.&lt;/p&gt;
&lt;h3 id=&quot;what-the-recursion-was-doing-for-me&quot;&gt;What the recursion was doing for me&lt;/h3&gt;
&lt;p&gt;Every call on a child saves the current position and jumps in, then comes back when the child returns. That saved position is a stack frame. A recursive traversal is really a loop over a stack you never have to see, which is exactly why it feels effortless until you have to rebuild it.&lt;/p&gt;
&lt;p&gt;Deep trees punish the hidden version. A chain of 10,000 left only children overflows the call stack in most languages. The iterative rewrite moves that work onto a list in heap memory, so the skewed tree that crashes recursion runs fine.&lt;/p&gt;
&lt;h3 id=&quot;rebuilding-postorder-by-hand&quot;&gt;Rebuilding postorder by hand&lt;/h3&gt;
&lt;p&gt;Postorder is the hard conversion, because you reach a parent before its two children, but you have to process it after them. The cleanest way to hold that backwards is two stacks. The first produces a node, right, left order. The second reverses it into left, right, node, which is postorder.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def iterative_postorder(root):
    if not root:
        return
    stack1 = [root]
    stack2 = []
    while stack1:
        node = stack1.pop()
        stack2.append(node)
        if node.left:           # left first so right lands on top
            stack1.append(node.left)
        if node.right:
            stack1.append(node.right)
    while stack2:               # stack2 holds node, right, left
        process(stack2.pop())   # popping reverses it into postorder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trace it on a tiny tree with root &lt;code&gt;1&lt;/code&gt;, left child &lt;code&gt;2&lt;/code&gt;, and right child &lt;code&gt;3&lt;/code&gt;. Postorder should read 2, 3, 1.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stack1: [1]                      stack2: []
pop 1 -&amp;gt; stack2: [1]             push 2, 3 -&amp;gt; stack1: [2, 3]
pop 3 -&amp;gt; stack2: [1, 3]          3 is a leaf
pop 2 -&amp;gt; stack2: [1, 3, 2]       2 is a leaf
drain stack2 by popping:         process 2, then 3, then 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output 2, 3, 1 is postorder. Watching the second stack get drained in reverse is the moment the whole thing stopped feeling like a trick.&lt;/p&gt;
&lt;h3 id=&quot;why-the-order-was-never-the-hard-part&quot;&gt;Why the order was never the hard part&lt;/h3&gt;
&lt;p&gt;The last lesson took the longest to land. The problem was never the traversal. It was that the recursion got practiced and the stack underneath it got skipped.&lt;/p&gt;
&lt;p&gt;Once that gap closed, picking an order got simple, because the order is decided by one question. Does the parent need answers from its children, or do the children need data from the parent? Height, diameter, and subtree sums need the children first, so they are postorder. Path sums hand data downward, so they are preorder. Sorted output from a BST is inorder. Work defined per level is level order with a queue instead of a stack. Which way the data had to move decided the order every time, with no preference involved.&lt;/p&gt;
&lt;p&gt;That gap, between knowing the recursion and understanding the stack underneath, is what I kept circling back to, and it is a big part of why I built Codeintuition. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/SYV3exXvDpTl4oSnnQKn7&quot;&gt;The lesson that traces the stack frame by frame&lt;/a&gt; is the walkthrough I wish I had back then.&lt;/p&gt;
&lt;h3 id=&quot;the-habits-that-fixed-it&quot;&gt;The habits that fixed it&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Old habit &lt;/th&gt;&lt;th&gt;New habit&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Memorize three recursive shapes as separate facts &lt;/td&gt;&lt;td&gt;Treat them as one walk with the visit line moved&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reach for recursion and hope the stack held &lt;/td&gt;&lt;td&gt;Manage the stack by hand when depth is a risk&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Guess at iterative postorder under pressure &lt;/td&gt;&lt;td&gt;Build it from the two stack idea every time&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pick a traversal order by familiarity &lt;/td&gt;&lt;td&gt;Pick it by which way data moves, parent to child&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you are staring at the same wall, three things would have saved me a lot of time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Learn the stack, not the three shapes. Once you read the recursion as a loop over a stack, preorder and inorder stop being separate things to memorize.&lt;/li&gt;
&lt;li&gt;Write iterative postorder by hand before an interview forces you to. It is the one that exposes whether you understand the stack, since you meet the parent before its children and have to hold it back.&lt;/li&gt;
&lt;li&gt;Decide the order by data direction. Ask whether the parent needs its children's answers or the children need the parent's data, and the order picks itself.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I put the longer version on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/binary-tree-traversal&quot;&gt;my own blog&lt;/a&gt;, including the level order walkthrough and the full set of traces.&lt;/p&gt;
&lt;p&gt;What's a traversal or algorithm you thought you knew cold until an interview asked for the version you had never written by hand?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19164/i-froze-on-iterative-postorder-the-fix-was-understanding-the-stack</guid>
<pubDate>Thu, 28 May 2026 11:14:52 +0000</pubDate>
</item>
<item>
<title>I Solved 80 Tree Problems Before Binary Tree Recursion Clicked</title>
<link>https://coderlegion.com/19004/i-solved-80-tree-problems-before-binary-tree-recursion-clicked</link>
<description>&lt;p&gt;I'd solved around 80 tree problems by the time my second on site interview wrapped up. Most were mediums. Preorder, inorder, postorder, level order felt like second nature. Tree height and BST validation, written from scratch, half a dozen times each. But the medium I'd just bombed had a stateful postorder shape I didn't see in the moment, and 20 of the 35 minutes went to patching the wrong recursion.&lt;/p&gt;
&lt;p&gt;On the train home I traced the solution by hand. It clicked in 90 seconds. The problem wasn't tree knowledge. What was missing was the skill of predicting how a recursive function would flow through the call stack, frame by frame, before writing any code.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What changed for me&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mental simulation of the call stack is a separate skill from knowing traversal types.&lt;/li&gt;
&lt;li&gt;The six tree patterns split cleanly along one axis: which direction information flows.&lt;/li&gt;
&lt;li&gt;Stateful postorder problems look right at every node and wrong overall when you confuse the return value with the answer.&lt;/li&gt;
&lt;li&gt;BST validation looks structurally fine and is logically broken when you check only direct children.&lt;/li&gt;
&lt;li&gt;Practicing tree problems without simulating the call stack is what kept me stuck for months.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;where-the-recursion-kept-breaking&quot;&gt;Where the recursion kept breaking&lt;/h3&gt;
&lt;p&gt;The approach had been treating tree problems the same way you'd treat array problems. Read the statement, sketch one small example, write code, test against the example, debug. On arrays that loop works fine because the traversal is linear and the bugs show up immediately. On trees the loop fails silently. The recursion executes, the values look plausible at every node, the test case passes on a small input, and a deeper tree from the interview question produces the wrong answer.&lt;/p&gt;
&lt;p&gt;The symptom looked like a knowledge problem, so for a while the response was reading. Reread the postorder explanation. Try more medium problems. Next interview, freeze on something with the same shape. The reading wasn't the gap. The simulation skill was.&lt;/p&gt;
&lt;p&gt;The fix turned out to be simpler than expected. Before writing any code for a tree problem, ask one question: which direction does information have to flow to get the answer? Down through parameters? Up through return values? Sideways with a queue?&lt;/p&gt;
&lt;p&gt;That single question puts you into the right pattern category before you've written a line of code. It also forces you to predict what's going to happen on the call stack before running anything.&lt;/p&gt;
&lt;h3 id=&quot;what-i-started-doing-differently&quot;&gt;What I started doing differently&lt;/h3&gt;
&lt;p&gt;The protocol ran something like this. It was deliberately slower than the old read-and-code habit, and it was the part that closed the gap.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the problem. Sketch a small concrete tree on paper. Not the example from the problem statement, a tree you make up.&lt;/li&gt;
&lt;li&gt;Ask the direction question. Does the answer for a node depend on what's above it, what's below it, or what's beside it?&lt;/li&gt;
&lt;li&gt;Pick a pattern. Down means preorder. Up means postorder. Sideways means level order. Stateful versions of each split off by whether a global accumulator is involved.&lt;/li&gt;
&lt;li&gt;Write one sentence describing what &lt;code&gt;f(node)&lt;/code&gt; returns. If you can't write the sentence, you've misidentified the pattern.&lt;/li&gt;
&lt;li&gt;Walk through the call stack on the sketched tree. Predict the order of recursive calls. Predict what each call returns. Note where global state changes if any.&lt;/li&gt;
&lt;li&gt;Only then start coding.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The whole protocol took 5 minutes the first few times. It saved 20 in debugging. After running it on 30 problems, the first five steps collapsed into about 60 seconds of thinking before any code got written.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-diameter&quot;&gt;What that looked like on diameter&lt;/h3&gt;
&lt;p&gt;The first problem I ran this protocol on was tree diameter. The diameter of a binary tree is the longest path between any two nodes, measured in edges.&lt;/p&gt;
&lt;p&gt;Sketch a small tree. Root with a left subtree of depth 2 and a right subtree of depth 3.&lt;/p&gt;
&lt;p&gt;Direction question: does the diameter at the root depend on what's above it (impossible, it's the root), what's below it (yes, the depths of the subtrees), or sideways (no)?&lt;/p&gt;
&lt;p&gt;Information flows up. Postorder.&lt;/p&gt;
&lt;p&gt;Sentence describing what &lt;code&gt;f(node)&lt;/code&gt; returns: the height of the subtree rooted at &lt;code&gt;node&lt;/code&gt;. The diameter accumulates globally.&lt;/p&gt;
&lt;p&gt;That last detail is where stateful postorder lives. The return value is the local thing the parent needs (subtree height). The actual answer is a global thing updated at each node (the maximum of &lt;code&gt;left_height + right_height&lt;/code&gt; seen anywhere in the tree).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def diameter(root):
    longest = 0

    def height(node):
        nonlocal longest
        if not node:
            return 0
        left = height(node.left)
        right = height(node.right)
        longest = max(longest, left + right)
        return 1 + max(left, right)

    height(root)
    return longest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Walking the call stack:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;height(left_child)&lt;/code&gt; runs first. Recurses down to the leaves, returns heights from the bottom up. At each node, updates &lt;code&gt;longest&lt;/code&gt; with &lt;code&gt;left + right&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;height(right_child)&lt;/code&gt; runs next. Same behavior on the right subtree.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;height(root)&lt;/code&gt; runs last. Gets left subtree's height (2) and right subtree's height (3). Updates &lt;code&gt;longest&lt;/code&gt; with &lt;code&gt;2 + 3 = 5&lt;/code&gt;. Returns &lt;code&gt;1 + max(2, 3) = 4&lt;/code&gt; (which is the tree's total height, not the answer).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The diameter is &lt;code&gt;longest = 5&lt;/code&gt;. The return value of the top call is &lt;code&gt;4&lt;/code&gt; (the height) and it isn't the answer. That separation between return value and answer is the entire pattern.&lt;/p&gt;
&lt;p&gt;Earlier attempts at diameter kept trying to make the return value carry the answer directly. It doesn't work, because the diameter at a node combines information from two siblings, not from a single root-to-leaf path. The global accumulator is what handles that combination.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works&quot;&gt;Why this works&lt;/h3&gt;
&lt;p&gt;The mechanism isn't deep, once you see it. Tree recursion is two recursive calls per node. The call stack pushes the parent frame, pushes the left child's call chain, pops back to the parent, pushes the right child's call chain, pops back to the parent. Two branches per frame.&lt;/p&gt;
&lt;p&gt;What makes tree problems feel hard is that you have to predict this branching in your head before writing code. Once you can predict it, the choice between preorder, postorder, and level order falls out of the prediction itself. Down means the answer accumulates on the way down. Up means it accumulates on the way back. Sideways means the call stack is the wrong tool entirely and you should use a queue.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/CcYCZ_L78G0WUZuAl79GI&quot;&gt;maximum path sum lesson&lt;/a&gt; on Codeintuition was the one where this pattern finally became obvious to me, because it's stateful postorder with a meaner set of edge cases (negative values, single node paths, paths that pass through the root rather than ending at it). Watching the call stack trace through that problem frame by frame was what made the structure click for everything after it.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-in-the-same-spot&quot;&gt;What I'd tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;If you've solved 50+ tree problems and still freeze on unfamiliar ones, the gap is almost always the simulation skill, not the knowledge. More problems won't close it.&lt;/p&gt;
&lt;p&gt;The shortest practice that helped me: take a tree problem you've solved before. Cover the solution. Sketch a small tree. Predict the order of recursive calls and what each call returns. Open the solution in a debugger and step through. Note where your prediction diverged from what actually happened.&lt;/p&gt;
&lt;p&gt;Three rounds of that and the call stack stops feeling abstract. Five rounds and you can read a new tree problem, decide direction in 30 seconds, and start coding from a correct mental model.&lt;/p&gt;
&lt;p&gt;Worth doing on every pattern category at least once. Stateless preorder. Stateless postorder. Stateful postorder. Root to leaf path. The BST inorder pattern. Level order with a queue. Once each of those patterns has a call stack trace burned into your head, the rest of the variants are small adaptations.&lt;/p&gt;
&lt;p&gt;There's a longer version on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/binary-tree-recursion-interview&quot;&gt;my own blog&lt;/a&gt; that walks through the four BST-only patterns and the heap variants in more depth.&lt;/p&gt;
&lt;p&gt;What's a tree problem that finally made the call stack visible to you in your head, where reading the textbook explanation hadn't? For me it was diameter. The protocol I built around it is what made every subsequent stateful postorder problem feel mechanical.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19004/i-solved-80-tree-problems-before-binary-tree-recursion-clicked</guid>
<pubDate>Tue, 26 May 2026 16:09:31 +0000</pubDate>
</item>
<item>
<title>What 40 Binary Tree Problems Didn't Teach Me Before My FAANG Interview</title>
<link>https://coderlegion.com/18882/what-40-binary-tree-problems-didnt-teach-me-before-my-faang-interview</link>
<description>&lt;p&gt;The tree round on my first onsite was Root to Leaf Paths. Enumerate every root-to-leaf path in a binary tree. Looked easy. The recursion took about four minutes to write. Running it on the sample input came back with a list where every path ended at the leaf of the last branch the function had visited. The earlier paths had been contaminated. The interviewer watched the screen for two full minutes before the cause clicked: the shared list parameter never got cleaned up between recursive calls. That technique had shown up in practice twice before, but the conditions for when it was required had never internalised. What was missing was the framing that root-to-leaf problems are a class with their own boundary handling, separate from the other recursive tree patterns.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What changed afterwards:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tree problems aren't a single skill. They are six distinct mechanisms (preorder, postorder, root to leaf, level order, LCA, simultaneous traversal), each with its own boundary cases.&lt;/li&gt;
&lt;li&gt;Fifteen problems chosen by pattern coverage beat forty random problems sorted by acceptance rate.&lt;/li&gt;
&lt;li&gt;The single biggest mistake on root-to-leaf problems is forgetting to backtrack the shared accumulator after each recursive call.&lt;/li&gt;
&lt;li&gt;Practice in pattern order, not difficulty order. Each pattern's mechanism transfers to its variants but not to other patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;where-the-tree-prep-was-breaking&quot;&gt;Where the tree prep was breaking&lt;/h3&gt;
&lt;p&gt;The grinding approach is what most onlines recommend. Open LeetCode's tree tag. Sort by acceptance rate or popularity. Work through the top forty. After about a week the easy ones feel reflexive, after two weeks the mediums start landing more often than not, and by week three the assumption is that tree rounds will be fine.&lt;/p&gt;
&lt;p&gt;That assumption broke on the Root to Leaf Paths bug. The problem wasn't hard. It was a class of problem that had been practiced before. What was missing was the recognition that root-to-leaf is its own pattern with its own boundary behaviour, distinct from the postorder problems on the list and distinct from the preorder problems that felt most confident. Forty problems sat behind that interview, and there was no grouping that would have made the root-to-leaf class visible as a class.&lt;/p&gt;
&lt;p&gt;The deeper diagnosis was this. What was missing wasn't more problems. It was the grouping that would let problem 1 in a class prepare a candidate for problem 2 in the same class. Forty problems organised by popularity give no transfer. Two problems per pattern, organised by mechanism, give coverage of every interview-grade tree question that's likely to show up.&lt;/p&gt;
&lt;h3 id=&quot;what-started-working-differently&quot;&gt;What started working differently&lt;/h3&gt;
&lt;p&gt;The rebuild happened pattern by pattern. Each session, pick one of the six patterns. Work through the direct application first (the problem that teaches the mechanism cleanly). Then work through one variant (the problem that proves the mechanism transferred). Two problems per pattern, no more. If the mechanism hadn't clicked after problem two, the answer wasn't to add a third problem in the same pattern. The answer was to go back to problem one and trace it on paper until the mechanism became obvious from the structure of the recursion.&lt;/p&gt;
&lt;p&gt;The change in approach looked like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Before (grinding random problems)                      &lt;/th&gt;&lt;th&gt;After (two per pattern, in pattern order)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Sort LeetCode tree tag by frequency, do top 40         &lt;/td&gt;&lt;td&gt;Pick six patterns, do two problems each, in pattern dependency order&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Treat each problem as a separate puzzle to solve       &lt;/td&gt;&lt;td&gt;Treat each problem as an instance of one of six known mechanisms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practice difficulty order (easy, then medium, then hard) &lt;/td&gt;&lt;td&gt;Practice pattern order, with the direct application before each variant&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Recognise solutions you've already seen                &lt;/td&gt;&lt;td&gt;Recognise which pattern a new problem needs, then derive the solution&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Freeze on root-to-leaf backtracking under pressure     &lt;/td&gt;&lt;td&gt;Walk into root-to-leaf knowing the path parameter is shared and needs cleanup&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The key was that the six patterns gave the practice a vocabulary. Preorder. Postorder. Root to leaf. Level order. Lowest common ancestor. Simultaneous traversal. Every binary tree problem that came up, the first question became &quot;which of the six is this?&quot; That single question reorganised everything. Problems that previously felt random started feeling like instances of techniques already met.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;Three weeks into the rebuild, a friend ran a mock interview and the prompt was Root to Leaf Paths. The exact problem that had broken the onsite.&lt;/p&gt;
&lt;p&gt;The framing was different this time. Root to leaf problems share a single accumulator (the path list) across all recursive branches. That sharing is what makes them fast (no per-call allocation), and it is what creates the bug if the accumulator doesn't get cleaned up after each child returns. Recognise the pattern first, write the recursion with the cleanup built in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def root_to_leaf_paths(root):
    paths = []

    def walk(node, current):
        if node is None:
            return
        current.append(node.value)
        if node.left is None and node.right is None:
            paths.append(list(current))
        else:
            walk(node.left, current)
            walk(node.right, current)
        current.pop()

    walk(root, [])
    return paths
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things to notice in the code. First, &lt;code&gt;current&lt;/code&gt; is a single list that gets passed down through every recursive call. There's no copy at each call, which is what makes the algorithm O(n) plus the cost of producing the output. Second, the &lt;code&gt;current.pop()&lt;/code&gt; at the end of &lt;code&gt;walk&lt;/code&gt; is the cleanup. After the recursion returns from a child, the appended value gets removed so the sibling subtree sees the path as it was when the parent called it. Without that line, the bug from the original onsite shows up.&lt;/p&gt;
&lt;p&gt;The mechanism transfers to the variant. Equal paths asks whether any root-to-leaf path sums to a target. Same skeleton (descend, accumulate, check at leaf, backtrack), different terminal check. Once root to leaf paths is solid, equal paths is a five-minute extension. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/AeXH6FknJNkFz0Je1Rw5y&quot;&gt;root to leaf paths lesson&lt;/a&gt; walks the backtracking explicitly, which is the part that gets glossed over in most textbook reading.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-pattern-based-practice-&quot;&gt;Why this works (pattern based practice)&lt;/h3&gt;
&lt;p&gt;The reason pattern grouping holds up under interview pressure has a name in learning science: near transfer versus far transfer. Practicing forty random problems builds a lot of near transfer skill (the ability to handle problems that look like the ones already practiced) and almost no far transfer (the ability to reliably handle a new problem with a slightly different surface).&lt;/p&gt;
&lt;p&gt;Practicing two problems per pattern, in pattern order, inverts that ratio. The first problem teaches the mechanism. The second forces a variation that confirms the mechanism transferred, not the surface form. Past two problems per pattern, the marginal value drops fast, because the third is rehearsing the same mechanism with diminishing returns.&lt;/p&gt;
&lt;p&gt;The six patterns also explain why difficulty order is the wrong sequencing. A medium postorder problem can be easier than a hard preorder problem, if postorder is already understood and preorder hadn't been met yet. Difficulty is a function of &quot;has this mechanism been seen before?&quot; not a function of the problem's intrinsic complexity. Pattern order respects that dependency. Postorder builds on preorder's recursive structure. Root to leaf reuses postorder's return mechanism plus the backtracking subtlety. Level order is a paradigm shift to a queue-based approach. LCA introduces multi node reasoning. Simultaneous traversal scales LCA's reasoning to two trees.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-starting-tree-prep&quot;&gt;What I'd tell someone starting tree prep&lt;/h3&gt;
&lt;p&gt;If you're three weeks into tree practice and tree rounds still feel random, the shift to make is to stop grinding and start grouping.&lt;/p&gt;
&lt;p&gt;A protocol that worked for the rebuild:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Before each session, name the pattern you'll practice (preorder, postorder, root to leaf, level order, LCA, or simultaneous traversal). Pick one. Don't mix patterns within a session.&lt;/li&gt;
&lt;li&gt;Solve the direct application first (the problem that teaches the mechanism cleanly). For postorder, that's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/GXFycvfMF94E4A14c4kdA&quot;&gt;height of binary tree&lt;/a&gt;. For root to leaf, that's root to leaf paths. For LCA, that's the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/23LPmm1m-KZkwIIB1PG97&quot;&gt;canonical lowest common ancestor problem&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Trace the solution on paper after writing it. If you can't narrate why each step preserves the mechanism, the mechanism hasn't clicked. Go back to the recursion and walk it variable by variable.&lt;/li&gt;
&lt;li&gt;Solve the variant second. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/7D8VO_D2WFnGmlT79LtgS&quot;&gt;Diameter&lt;/a&gt; after height. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/ARsmRsSslpsO-t9Cf7yjG&quot;&gt;Equal paths&lt;/a&gt; after root to leaf paths. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-tree/j1X8mlH7RIoEQ4HQBinPy&quot;&gt;Distance between nodes&lt;/a&gt; after LCA. If the variant feels like a fresh problem instead of an extension, the first problem hadn't internalised.&lt;/li&gt;
&lt;li&gt;Move to the next pattern only after the current pattern's variant feels like a five-minute job. The transfer signal is &quot;the pattern got recognised first, then the solution wrote itself,&quot; not &quot;it took careful work to get there.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The protocol slowed things down at first. Two problems per session felt like less practice than four problems per session. The payoff was that the slower practice transferred. When the next mock interview surfaced a tree problem that hadn't been seen, the pattern recognition kicked in first and the recursion followed. That sequence was what was missing from the original forty-problem grind.&lt;/p&gt;
&lt;p&gt;The grouping is what got built into Codeintuition's tree course, because watching engineers grind through forty problems with no transfer was the failure mode that kept showing up across every prep cohort. Pattern-first practice changed how trees felt to walk into, and the course now teaches the same restructure that made the difference.&lt;/p&gt;
&lt;p&gt;Originally posted on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/binary-tree-interview-problems&quot;&gt;my own blog&lt;/a&gt;, with the full fifteen-problem list grouped by pattern and a recursion-bug catalogue.&lt;/p&gt;
&lt;p&gt;What's a tree problem that finally made a pattern click for you, where the textbook explanation hadn't?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18882/what-40-binary-tree-problems-didnt-teach-me-before-my-faang-interview</guid>
<pubDate>Mon, 25 May 2026 12:09:46 +0000</pubDate>
</item>
<item>
<title>What I Wish I'd Known About Binary Search Trees Before Interviews</title>
<link>https://coderlegion.com/18867/what-i-wish-id-known-about-binary-search-trees-before-interviews</link>
<description>&lt;p&gt;Three months of practicing binary search trees on paper. Search procedure memorised. Insertion procedure memorised. Three deletion cases memorised. Then came the onsite. The interviewer drew a tree on the whiteboard, asked for an insertion walkthrough, and the recall worked fine. Then she asked for a deletion of a node with two children, and the freeze lasted almost a full minute before the inorder successor steps came out without any real understanding of why they preserved the ordering. She let the explanation finish, then asked one followup: why does the inorder successor specifically work? There was no answer beyond &quot;because the textbook says so.&quot; That was the moment the gap became obvious. The months spent memorising procedures had skipped the part the interview was actually testing.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What changed afterwards:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BST search, insert, and delete are not three procedures. They are the same recursive walk with three different terminal conditions.&lt;/li&gt;
&lt;li&gt;The two child deletion case is the one interviewers probe because it is the only case where the invariant has to be explained out loud.&lt;/li&gt;
&lt;li&gt;Memorising the inorder successor trick doesn't transfer. Being able to say why the inorder successor specifically works does.&lt;/li&gt;
&lt;li&gt;Once the invariant becomes the unit of memory, every operation that preserves it becomes derivable on the spot.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;where-the-approach-was-breaking&quot;&gt;Where the approach was breaking&lt;/h3&gt;
&lt;p&gt;What had been getting practiced wasn't the data structure. It was a list of procedures the invariant could have handed over for free.&lt;/p&gt;
&lt;p&gt;The recall pattern looked like this. Open a BST problem. Recognise it as a BST problem. Try to recall the right procedure for whatever the question asked. Search? Walk down comparing target to current. Insert? Same walk, create a leaf at the bottom. Delete? Three cases, pick the matching one, run the steps. The recall worked fine for problems that matched the textbook exactly. It fell apart the moment the problem deviated, because there was no deeper rule available to derive a new procedure from.&lt;/p&gt;
&lt;p&gt;The specific failure on that first interview was treating the two child deletion case as a separate procedure rather than a question about the invariant. The procedure said &quot;find the inorder successor, copy its value, delete the original successor.&quot; Those steps executed fine. What didn't execute was the reason behind them, which was the actual signal the interviewer was checking.&lt;/p&gt;
&lt;p&gt;The gap is small in concept and load bearing in practice. If you can recite the steps but can't explain why each one preserves the rule, you will lose the thread whenever the interview adds a constraint that doesn't appear in the textbook version. And interviews almost always add a constraint.&lt;/p&gt;
&lt;h3 id=&quot;what-started-working-differently-1&quot;&gt;What started working differently&lt;/h3&gt;
&lt;p&gt;After that interview, the practice rebuild was small in shape and large in effect. Instead of &quot;memorise the procedure, then run it,&quot; the new approach was &quot;start from the invariant, then derive what has to happen to preserve it.&quot; Same problems, same operations, completely different relationship to the material.&lt;/p&gt;
&lt;p&gt;The contrast looked like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Before (procedure first)                            &lt;/th&gt;&lt;th&gt;After (invariant first)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Memorise search as a walk down comparing values     &lt;/td&gt;&lt;td&gt;Start from &lt;code&gt;left &amp;lt; node &amp;lt; right&lt;/code&gt; and derive the walk as the consequence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memorise insert as &quot;find null pointer, create leaf&quot; &lt;/td&gt;&lt;td&gt;Notice that insert is the same walk, only the terminal action changes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memorise three deletion cases                       &lt;/td&gt;&lt;td&gt;Ask &quot;what value can sit in this slot without breaking the invariant?&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Recall the inorder successor as a step              &lt;/td&gt;&lt;td&gt;Derive the inorder successor as the answer to the slot question&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Stall on novel problems                             &lt;/td&gt;&lt;td&gt;Answer novel problems by reasoning forward from the rule&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The shift was to remember fewer things, more carefully. There is exactly one rule for a BST: every node's left subtree contains only smaller values, every right subtree contains only larger values. Every operation either preserves the rule or is incorrect. That framing made the procedures fall out naturally, because the question &quot;what do I do here?&quot; reduced to &quot;what action keeps the rule intact?&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-1&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;A month later came a phone screen at a different company. The interviewer gave a tree with &lt;code&gt;[8, 3, 10, 1, 6, 14]&lt;/code&gt; and asked for a search for &lt;code&gt;6&lt;/code&gt; and then an insertion of &lt;code&gt;7&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Search started at the root. &lt;code&gt;6 &amp;lt; 8&lt;/code&gt;, so go left to &lt;code&gt;3&lt;/code&gt;. Then &lt;code&gt;6 &amp;gt; 3&lt;/code&gt;, so go right to &lt;code&gt;6&lt;/code&gt;. Found it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def search(node, target):
    if node is None or node.value == target:
        return node
    if target &amp;lt; node.value:
        return search(node.left, target)
    return search(node.right, target)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then inserting &lt;code&gt;7&lt;/code&gt; was the same walk. From the root, &lt;code&gt;7 &amp;lt; 8&lt;/code&gt;, go left. Then &lt;code&gt;7 &amp;gt; 3&lt;/code&gt;, go right to &lt;code&gt;6&lt;/code&gt;. Then &lt;code&gt;7 &amp;gt; 6&lt;/code&gt;, go right again. Right pointer is null. Create node &lt;code&gt;7&lt;/code&gt; there.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def insert(node, value):
    if node is None:
        return Node(value)
    if value &amp;lt; node.value:
        node.left = insert(node.left, value)
    elif value &amp;gt; node.value:
        node.right = insert(node.right, value)
    return node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What was different this time was the framing said out loud after the code finished. Search and insert are the same recursive walk. Search stops when the value matches or the pointer is null. Insert stops at a null pointer and writes a new leaf there. The walk is what the invariant requires. The terminal action is the only thing that changes between operations. The interviewer asked a few more questions and then moved on. The framing was the part that mattered, not the code.&lt;/p&gt;
&lt;p&gt;The same shift made deletion easier too. Instead of three cases, the new mental model was one question: what value can sit in this slot without breaking &lt;code&gt;left &amp;lt; node &amp;lt; right&lt;/code&gt;? For a leaf, the answer is no value. Drop the pointer. For a one child node, the answer is the child's value, because the child's subtree already satisfies the invariant against the parent. For a two child node, the answer is the smallest value larger than the deleted node (the inorder successor), which is what the slot requires after deletion. Three procedures collapsed into three answers to the same question.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-invariant-doing-the-heavy-lifting-&quot;&gt;Why this works (the invariant doing the heavy lifting)&lt;/h3&gt;
&lt;p&gt;The reason the invariant first framing holds up under interview pressure has a name in learning science: near transfer versus far transfer. Memorising a procedure builds a near transfer skill. The procedure applies to problems that look exactly like the one that got practiced. It doesn't apply to problems that look slightly different, because the procedure is tied to the specific form of the original.&lt;/p&gt;
&lt;p&gt;Deriving a procedure from a rule builds a far transfer skill. The rule applies to any problem that respects the rule, regardless of how the form changes. The rule is what generalises. The procedure is what gets generated for the specific case.&lt;/p&gt;
&lt;p&gt;BSTs make this concrete because there is exactly one rule and there are dozens of variant problems that ask you to apply it. BST validation. Conversion to sorted array. Conversion to sorted doubly linked list. Range queries. Kth smallest element. Lowest common ancestor in a BST. BST iterator. All derivable from &quot;left subtree smaller, right subtree larger.&quot; None easy to memorise as individual procedures, because there are too many of them and they shade into each other.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-search-tree/-qJIiGUt7_snRlnBS-g_6&quot;&gt;inorder successor lesson&lt;/a&gt; walks the two child deletion case from the invariant down, which is the framing that most first practice resources skip. The procedure becomes the easy part once the question is framed as &quot;what value belongs in this slot?&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-in-the-same-spot-1&quot;&gt;What I'd tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;If your BST practice is at the point where the procedures feel memorised but the mock interviews aren't going well, the shift to make is to stop quizzing yourself on the steps and start quizzing yourself on the rule.&lt;/p&gt;
&lt;p&gt;A practice protocol that worked for the rebuild:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Before solving any BST problem, write the invariant at the top of the page. &lt;code&gt;left &amp;lt; node &amp;lt; right&lt;/code&gt; for every node, applied recursively to every subtree. Look at it for ten seconds.&lt;/li&gt;
&lt;li&gt;Read the problem and ask: what action does the invariant require here? Not &quot;what procedure do I run.&quot; What does the rule say has to happen.&lt;/li&gt;
&lt;li&gt;Trace one example by hand on paper, narrating each step as &quot;this preserves the rule because...&quot; If that sentence won't finish, what's running is a memorised procedure, not a derivation from the rule.&lt;/li&gt;
&lt;li&gt;Code it up. If the code falls out naturally from the narration, you have it. If a procedure shows up that you can't justify, stop and go back to step 2.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The protocol slows things down at first. Adding a thirty second pause to write the invariant and narrate the reasoning feels expensive under time pressure. The payoff is that the slow practice transfers. When the interview adds a constraint, the procedure that would have been recalled is wrong, but the rule still applies and the new procedure derives in real time.&lt;/p&gt;
&lt;p&gt;That gap, between solving a BST problem and recognising why an operation works, is the thing that pushed me to build Codeintuition. Watching engineers grind through hundreds of tree problems without the invariant first framing was the pattern that wouldn't stop showing up. The tree courses now lead with the rule and let the procedures fall out, which is the ordering that would have saved months on the first attempt.&lt;/p&gt;
&lt;p&gt;Originally posted on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/binary-search-tree-explained&quot;&gt;my own blog&lt;/a&gt;, a slightly longer version with the inorder traversal proof and the balanced construction algorithm.&lt;/p&gt;
&lt;p&gt;What is a data structure that started making sense for you only after you stopped memorising the operations and started re-deriving them from the rule?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18867/what-i-wish-id-known-about-binary-search-trees-before-interviews</guid>
<pubDate>Mon, 25 May 2026 09:11:56 +0000</pubDate>
</item>
<item>
<title>Big O Notation: What I Wish I'd Known Before My First Interview</title>
<link>https://coderlegion.com/18747/big-o-notation-what-i-wish-id-known-before-my-first-interview</link>
<description>&lt;p&gt;The Big O table was memorised before the first onsite. Binary search logarithmic, merge sort linearithmic, hash insert constant amortised. All recitable. Then a senior engineer wrote a recursive function on the whiteboard, asked for an analysis, and almost a minute went by trying to match what was on the board to something on the cached table. The function was a naive recursive Fibonacci, seen before, implemented before. The freeze still happened, because nothing about the table told anyone how to count the recursive calls.&lt;/p&gt;
&lt;h4 id=&quot;what-i-figured-out-afterwards&quot;&gt;What I figured out afterwards&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;The table is the answer, not the method. Memorising it never builds the ability to derive complexity from code you haven't seen.&lt;/li&gt;
&lt;li&gt;The thing the table hides is the counting. How many times the dominant operation runs as a function of input size.&lt;/li&gt;
&lt;li&gt;Once four or five derivations have been traced by hand, the shape generalises. Unfamiliar code stops feeling opaque.&lt;/li&gt;
&lt;li&gt;Space complexity has the same derivation shape, plus the call stack that's easy to forget.&lt;/li&gt;
&lt;li&gt;The drill that actually builds the skill is deriving complexity on problems whose answer you don't already know.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-the-approach-was-breaking-1&quot;&gt;Where the approach was breaking&lt;/h3&gt;
&lt;p&gt;The Fibonacci moment exposed the gap. The function had been seen. The function had been implemented. What hadn't happened was counting what the recursion actually does.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def fib(n):
    if n &amp;lt; 2:
        return n
    return fib(n - 1) + fib(n - 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interviewer was patient. &quot;How many calls does &lt;code&gt;fib(5)&lt;/code&gt; make?&quot; The whiteboard got a tree. &lt;code&gt;fib(5)&lt;/code&gt; calls &lt;code&gt;fib(4)&lt;/code&gt; and &lt;code&gt;fib(3)&lt;/code&gt;. &lt;code&gt;fib(4)&lt;/code&gt; calls &lt;code&gt;fib(3)&lt;/code&gt; and &lt;code&gt;fib(2)&lt;/code&gt;. &lt;code&gt;fib(3)&lt;/code&gt; shows up twice. &lt;code&gt;fib(2)&lt;/code&gt; shows up three times. The tree grew faster than the page it was being drawn on. The answer wasn't in cached memory because no one had asked for the count before. The answer is &lt;code&gt;O(2ⁿ)&lt;/code&gt;, roughly, and the reason is that each call branches into two and the depth is about &lt;code&gt;n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Big O had been treated as a label to attach after solving. The interview was treating it as a derivation to perform on unfamiliar code. Those are different skills, and the wrong one had been practiced.&lt;/p&gt;
&lt;h3 id=&quot;what-started-working-differently-2&quot;&gt;What started working differently&lt;/h3&gt;
&lt;p&gt;The fix wasn't reading more articles. It was changing the drill. The new prompt was &quot;given this code you haven't seen before, what's the complexity, and how would you justify it&quot; instead of &quot;what's the complexity of merge sort.&quot; Once the prompt changed, the way unfamiliar code reads changed with it.&lt;/p&gt;
&lt;p&gt;Here's the shape that worked, side by side with the old habit:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Then &lt;/th&gt;&lt;th&gt;Now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Match the code to a memorised complexity. &lt;/td&gt;&lt;td&gt;Identify the dominant operation, the one that runs most.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Look up the answer when the match wasn't obvious. &lt;/td&gt;&lt;td&gt;Count how many times that operation runs, as a function of &lt;code&gt;n&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Move on once a label was attached. &lt;/td&gt;&lt;td&gt;Sum the work across levels (loops, recursion depth, branching factor).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Forget about space until reminded. &lt;/td&gt;&lt;td&gt;Track space the same way: auxiliary memory plus the call stack.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The change is a habit, not a piece of knowledge. The Big O table doesn't get more correct over time. The way unfamiliar code gets read does.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-2&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;A few weeks after the Fibonacci interview, the same function came back out for re-derivation from scratch. No looking up the answer. Just the dominant operation and counting.&lt;/p&gt;
&lt;p&gt;The dominant operation in &lt;code&gt;fib(n)&lt;/code&gt; is the recursive call. &lt;code&gt;fib(n)&lt;/code&gt; calls &lt;code&gt;fib(n-1)&lt;/code&gt; and &lt;code&gt;fib(n-2)&lt;/code&gt;. Treat both as roughly &lt;code&gt;fib(n-1)&lt;/code&gt; for the upper bound. So the call count &lt;code&gt;T(n) ≈ 2 × T(n-1)&lt;/code&gt;. That recurrence solves to &lt;code&gt;T(n) = 2ⁿ&lt;/code&gt;. The depth of the tree is about &lt;code&gt;n&lt;/code&gt;. The branching factor is 2. Total work is roughly the number of nodes in a binary tree of depth &lt;code&gt;n&lt;/code&gt;, which is &lt;code&gt;2ⁿ&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That's why naive Fibonacci is unusably slow even at moderate &lt;code&gt;n&lt;/code&gt;. &lt;code&gt;fib(40)&lt;/code&gt; makes more than a billion calls.&lt;/p&gt;
&lt;p&gt;Adding memoisation changes the shape entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def fib(n, memo={}):
    if n in memo:
        return memo[n]
    if n &amp;lt; 2:
        return n
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
    return memo[n]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now each subproblem &lt;code&gt;fib(k)&lt;/code&gt; for &lt;code&gt;k&lt;/code&gt; from 0 to &lt;code&gt;n&lt;/code&gt; is computed exactly once. There are &lt;code&gt;n&lt;/code&gt; distinct subproblems. Each one does constant work after its two subcalls return. Total: &lt;code&gt;O(n)&lt;/code&gt; time, &lt;code&gt;O(n)&lt;/code&gt; space from the memo plus the call stack.&lt;/p&gt;
&lt;p&gt;The complexity went from &lt;code&gt;O(2ⁿ)&lt;/code&gt; to &lt;code&gt;O(n)&lt;/code&gt; because the structure of the work changed, and the derivation made that visible in a way the table never did. (For the longer walkthrough of how memoisation becomes the gateway to dynamic programming, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/dynamic-programming-interview&quot;&gt;the DP interview blog&lt;/a&gt; covers the recurrence-to-table-to-bottom-up progression.)&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-science-behind-it-&quot;&gt;Why this works (the science behind it)&lt;/h3&gt;
&lt;p&gt;The learning science term for the swapped drill is &lt;em&gt;elaborative interrogation&lt;/em&gt;. Asking yourself why each operation contributes the work it does, rather than accepting the labelled answer at face value. The literature on it is consistent: forcing yourself to construct the reason produces a more durable, more transferable understanding than reading the reason already constructed.&lt;/p&gt;
&lt;p&gt;The other piece is &lt;em&gt;near transfer versus far transfer&lt;/em&gt;. The old drill was near transfer practice (the same complexity question on the same code already memorised). The interview was a far transfer test (different code, same skill). Near transfer practice doesn't reliably move far transfer scores. Once that lands, the right drill picks itself: derive complexity on code you haven't seen.&lt;/p&gt;
&lt;h3 id=&quot;what-to-tell-someone-in-the-same-spot&quot;&gt;What to tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;Three things, in the order they mattered.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Stop quizzing yourself on named algorithms.&lt;/strong&gt; &quot;What's the complexity of merge sort&quot; trains memory, not derivation. Quiz yourself on an unfamiliar function instead, and force yourself to count the dominant operation explicitly. If you have to count it out loud, that's the right intensity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trace at least one recursive call tree by hand per week.&lt;/strong&gt; The first few feel slow. The fifth or sixth is where the shape generalises. Branching factor times depth gives nodes, nodes times work-per-node gives total work. The arithmetic is the same on every problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track space the same way you track time.&lt;/strong&gt; Auxiliary memory plus call-stack frames. Most engineers in interviews get time right and space wrong, and the difference is purely habit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before this shift, a solution would read as &lt;em&gt;that looks fast&lt;/em&gt; and the analysis would stop there. After, the same solution reads as &lt;code&gt;O(n log n)&lt;/code&gt; because the divide step and the merge step can be pointed at independently. Adding a hash map to a nested loop visibly drops the complexity from quadratic to linear, with the derivation right there in the code. Unfamiliar code becomes analysable because the analysis is something you do, not something you recall.&lt;/p&gt;
&lt;p&gt;Originally posted on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/big-o-notation-explained&quot;&gt;my own blog&lt;/a&gt;, with the amortised analysis derivation for hash-map resize and the space-complexity traps skipped over in this retrospective.&lt;/p&gt;
&lt;p&gt;What's a piece of complexity analysis that finally clicked for you only after deriving it by hand, where the textbook label had never really stuck?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18747/big-o-notation-what-i-wish-id-known-before-my-first-interview</guid>
<pubDate>Sat, 23 May 2026 11:29:05 +0000</pubDate>
</item>
<item>
<title>BFS vs DFS? The Question That Fixed My Graph Solutions</title>
<link>https://coderlegion.com/18704/bfs-vs-dfs-the-question-that-fixed-my-graph-solutions</link>
<description>&lt;p&gt;For a long time I treated BFS and DFS as interchangeable. Two ways to visit every node, pick whichever came to mind first. Then an interview handed me a grid that wanted the minimum number of steps, and a recursive DFS felt faster to type, so that's what went down. The path that came back was valid and far too long. No error. Just an answer that passed the small cases and fell apart on the one that mattered, with no obvious reason why.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What finally clicked:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The two traversals hold different invariants. They don't just visit in a different order.&lt;/li&gt;
&lt;li&gt;The queue gives BFS its shortest path guarantee. The stack makes DFS commit to one branch.&lt;/li&gt;
&lt;li&gt;&quot;Minimum&quot; in an unweighted graph turns out to be a reliable signal for BFS.&lt;/li&gt;
&lt;li&gt;&quot;Detect&quot;, &quot;order&quot;, and &quot;all paths&quot; point to DFS.&lt;/li&gt;
&lt;li&gt;Once edges carry weights, neither applies, and Dijkstra's algorithm takes over.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-my-approach-was-breaking&quot;&gt;Where my approach was breaking&lt;/h3&gt;
&lt;p&gt;The mistake wasn't picking the slower traversal. It was reaching for whichever traversal got practiced most recently instead of reading what the problem actually asked for.&lt;/p&gt;
&lt;p&gt;A grid that wants the fewest steps is asking for a distance guarantee, and DFS doesn't offer one. It happily commits to a branch and walks 30 cells deep before it would ever consider the 8 cell route, then stops the moment it touches the goal, satisfied with a path that happens to be valid. The wrong choice there has nothing to do with DFS being worse. It comes from never asking what kind of answer the problem needed.&lt;/p&gt;
&lt;h3 id=&quot;the-one-question-that-changed-it&quot;&gt;The one question that changed it&lt;/h3&gt;
&lt;p&gt;Before writing any code now, the question is always the same: does this problem need a distance guarantee or a structural property? If the answer is distance, that's BFS. If it's structure, meaning cycles, ordering, or which nodes connect to which, that's DFS.&lt;/p&gt;
&lt;p&gt;It sounds almost too simple, and that's the point. The decision was never about which algorithm you can implement faster. It's about which guarantee the problem is quietly demanding.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-same-six-node-graph&quot;&gt;What that looked like on the same six node graph&lt;/h3&gt;
&lt;p&gt;The clearest way to feel the difference is to run both on one small graph and watch the order. Take six nodes with edges 0→1, 0→2, 1→3, 1→4, 2→4, 2→5, starting from node 0.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from collections import deque

graph = {0: [1, 2], 1: [3, 4], 2: [4, 5], 3: [], 4: [], 5: []}

def bfs(start):
    order, seen, q = [], {start}, deque([start])
    while q:
        node = q.popleft()
        order.append(node)
        for nxt in graph[node]:
            if nxt not in seen:
                seen.add(nxt)
                q.append(nxt)
    return order

def dfs(start):
    order, seen, stack = [], set(), [start]
    while stack:
        node = stack.pop()
        if node in seen:
            continue
        seen.add(node)
        order.append(node)
        for nxt in reversed(graph[node]):
            if nxt not in seen:
                stack.append(nxt)
    return order

print(bfs(0))  # [0, 1, 2, 3, 4, 5]
print(dfs(0))  # [0, 1, 3, 4, 2, 5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BFS visits in exact distance order: everything one hop from the source (1, 2) before anything two hops away (3, 4, 5). DFS visits 3 before it ever gets back to 2, even though 3 sits further from the source. It cares about depth, not distance. Swapping the queue for a stack is the only structural change between the two functions, and it produces the entire difference in behaviour.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-part-that-took-digging-&quot;&gt;Why this works (the part that took digging)&lt;/h3&gt;
&lt;p&gt;A queue is first in, first out, so the nodes you discover at distance &lt;code&gt;d&lt;/code&gt; always come out behind the remaining &lt;code&gt;d-1&lt;/code&gt; nodes and ahead of the &lt;code&gt;d+1&lt;/code&gt; ones. That ordering is the shortest path guarantee, with no explicit distance tracking needed. A stack is last in, first out, so the most recent branch always gets expanded next, which is depth first by construction.&lt;/p&gt;
&lt;p&gt;There's a memory split that follows from the same fact. BFS holds the full frontier, so its peak memory tracks the widest level of the graph. DFS holds only the current path, so its memory tracks the deepest one. Tracing both algorithms frame by frame in &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/graph&quot;&gt;the graph course&lt;/a&gt;, watching the queue and stack contents change at every step, is what makes the distance ordering stop feeling abstract.&lt;/p&gt;
&lt;h3 id=&quot;what-to-do-when-you-re-still-guessing&quot;&gt;What to do when you're still guessing&lt;/h3&gt;
&lt;p&gt;If you're staring at a graph problem and reaching for habit, run it through this instead:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;If the problem asks for &lt;/th&gt;&lt;th&gt;Reach for&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Shortest path or fewest moves, equal cost edges &lt;/td&gt;&lt;td&gt;BFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Level by level processing &lt;/td&gt;&lt;td&gt;BFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Nearest target &lt;/td&gt;&lt;td&gt;BFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cycle detection &lt;/td&gt;&lt;td&gt;DFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Topological order &lt;/td&gt;&lt;td&gt;DFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Connected components &lt;/td&gt;&lt;td&gt;DFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;All paths or backtracking &lt;/td&gt;&lt;td&gt;DFS&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Shortest path with weighted edges &lt;/td&gt;&lt;td&gt;Neither, use Dijkstra&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I originally posted this on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/bfs-vs-dfs&quot;&gt;my own blog&lt;/a&gt;, a slightly longer version with a directed cycle detection walkthrough.&lt;/p&gt;
&lt;p&gt;What's a graph problem that finally made the BFS versus DFS choice click for you, the one where guessing wrong actually cost you in an interview?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18704/bfs-vs-dfs-the-question-that-fixed-my-graph-solutions</guid>
<pubDate>Fri, 22 May 2026 20:01:47 +0000</pubDate>
</item>
<item>
<title>Backtracking Finally Clicked When I Fixed One Habit</title>
<link>https://coderlegion.com/18581/backtracking-finally-clicked-when-i-fixed-one-habit</link>
<description>&lt;p&gt;For a long stretch, backtracking was the topic I quietly dreaded. The recursion was easy enough to write. The code ran. It even returned answers. They were just wrong, and not in a way that threw an error you could chase. A Combination Sum solution would spit out duplicate-looking results with no obvious culprit line. The fix, when it came, had nothing to do with understanding recursion better. It was one habit that kept getting skipped.&lt;/p&gt;
&lt;p&gt;Here's what changed once backtracking stopped being a clever trick and became a fixed loop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What changed&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backtracking is a depth first walk over a state space tree, and the tree is the part worth drawing first.&lt;/li&gt;
&lt;li&gt;The loop is always the same: choose an option, check the constraint, undo the choice on the way back up.&lt;/li&gt;
&lt;li&gt;The undo step is the easiest to drop, and dropping it silently corrupts later branches.&lt;/li&gt;
&lt;li&gt;Pruning before you recurse, not after you reach a leaf, is where the speed lives.&lt;/li&gt;
&lt;li&gt;Once the same partial state repeats across branches, the whole thing becomes top down DP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-my-backtracking-kept-breaking&quot;&gt;Where my backtracking kept breaking&lt;/h3&gt;
&lt;p&gt;The mistake was always the same, and naming it took embarrassingly long. You build a partial solution by mutating a shared list, recurse, then move on to the next option without ever putting the list back the way you found it. So branch two starts with branch one's leftovers still inside it.&lt;/p&gt;
&lt;p&gt;What broke wasn't the recursion. It was that the path never got restored on the way back up.&lt;/p&gt;
&lt;p&gt;The reason it's so hard to catch: nothing crashes. A forgotten undo doesn't raise an exception. It quietly hands stale state to the next branch, and the answers look plausible until you check them by hand. That single omission costs more debugging time than any actual algorithmic concept.&lt;/p&gt;
&lt;p&gt;Picture it concretely on a small Combination Sum case, candidates &lt;code&gt;[2, 3]&lt;/code&gt; and target &lt;code&gt;5&lt;/code&gt;. The first branch explores &lt;code&gt;[2, 2, ...]&lt;/code&gt;, overshoots, and returns. If that branch left a stray &lt;code&gt;2&lt;/code&gt; in the shared list, the next branch starts from &lt;code&gt;[2]&lt;/code&gt; when it should start from &lt;code&gt;[]&lt;/code&gt;. Now the &lt;code&gt;[3, ...]&lt;/code&gt; branch is really exploring &lt;code&gt;[2, 3, ...]&lt;/code&gt;, and the result set fills with combinations that were never asked for. Reading the function top to bottom, every line looks reasonable. The bug only shows up when you trace the shared list across two sibling branches and notice it isn't the same length going in as coming out.&lt;/p&gt;
&lt;h3 id=&quot;what-i-started-doing-differently-1&quot;&gt;What I started doing differently&lt;/h3&gt;
&lt;p&gt;The shift was to stop writing backtracking freehand and write the same three line frame every time, then fill it in. Choose, check, undo, in that order, with the undo non negotiable. Here's the before and after, once it became a fixed habit:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Then &lt;/th&gt;&lt;th&gt;Now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Recursion written ad hoc, undo when remembered &lt;/td&gt;&lt;td&gt;Choose, check, undo written as a fixed frame first&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Constraints checked at the leaf &lt;/td&gt;&lt;td&gt;Constraint checked before recursing, so dead branches die early&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Debugged by re-reading the whole function &lt;/td&gt;&lt;td&gt;Trace one branch, confirm state is restored after the call&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Each problem treated as new &lt;/td&gt;&lt;td&gt;Slot the problem into one of three known shapes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &quot;check before recurse&quot; row mattered more than expected. Pruning at the leaf still gives correct answers, it's just slow, and on a tight time limit slow is the same as failing.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-combination-sum&quot;&gt;What that looked like on Combination Sum&lt;/h3&gt;
&lt;p&gt;Combination Sum is the problem that finally made it click for me. Given a list of candidates and a target, find every combination that sums to the target, where a number can be reused. It's a conditional enumeration problem, so you prune any branch whose running sum overshoots.&lt;/p&gt;
&lt;p&gt;Here's the frame, filled in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def combination_sum(candidates, target):
    result = []

    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(path[:])      # finished combination
            return
        if remaining &amp;lt; 0:
            return                      # overshot, prune this branch
        for i in range(start, len(candidates)):
            path.append(candidates[i])                       # choose
            backtrack(i, path, remaining - candidates[i])    # explore (reuse allowed)
            path.pop()                                       # undo

    backtrack(0, [], target)
    return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The line that used to go missing is &lt;code&gt;path.pop()&lt;/code&gt;. Without it, every combination after the first carries elements from the previous one. The &lt;code&gt;path[:]&lt;/code&gt; matters too, it copies the list into the result instead of storing a reference that later &lt;code&gt;pop&lt;/code&gt; calls would mutate out from under you. Two small lines, and they're the entire difference between a function that works and one that lies to you.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;start&lt;/code&gt; index is the other quiet detail. Passing &lt;code&gt;i&lt;/code&gt; rather than &lt;code&gt;i + 1&lt;/code&gt; into the recursive call is what allows a number to be reused. Pass &lt;code&gt;i + 1&lt;/code&gt; and you've written a different problem, combinations without repetition. Same frame, one parameter changes the meaning.&lt;/p&gt;
&lt;h3 id=&quot;why-undoing-state-is-the-whole-game&quot;&gt;Why undoing state is the whole game&lt;/h3&gt;
&lt;p&gt;With the frame in hand, the three problem shapes read as one idea seen from different angles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/backtracking/dIe_AzPs_9PsI92I4FLHc&quot;&gt;&lt;strong&gt;Unconditional enumeration&lt;/strong&gt;&lt;/a&gt;, like all subsets, has no constraint, so you walk the whole tree and every node is an answer.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/backtracking/UUkyIeusIihAiyH8Xz9P8&quot;&gt;&lt;strong&gt;Conditional enumeration&lt;/strong&gt;&lt;/a&gt;, like Combination Sum or valid parentheses, prunes branches that break a rule mid build.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/backtracking/rblzCNicLv5RvATRMpCWV&quot;&gt;&lt;strong&gt;Backtracking search&lt;/strong&gt;&lt;/a&gt;, like N-Queens or Sudoku, looks for configurations under a global constraint and prunes hardest.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All three run the same choose, check, undo loop. The undo is what lets one mutable structure stand in for every node of the tree without the nodes contaminating each other. That's the thing I ended up rebuilding my own approach around.&lt;/p&gt;
&lt;p&gt;It also reframes how the three shapes relate. They aren't three separate algorithms to learn. They're one loop with the constraint check dialled from off (subsets) to mid build (Combination Sum) to every level (N-Queens). Seeing them as one thing meant a new problem stopped being a blank page. The question became which shape it was and what the constraint was, and the frame handled the rest.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-stuck-on-this&quot;&gt;What I'd tell someone stuck on this&lt;/h3&gt;
&lt;p&gt;If you're losing time to backtracking that returns wrong answers, the fix is almost never more practice on harder problems. Grinding ten more search problems with the same blind spot just produces ten more silently wrong solutions. The thing standing between you and reliable backtracking is usually one missing line, not a missing concept. It's two habits. First, write the undo line at the same moment you write the choose line, as a pair, before anything else in the loop. Second, trace one full branch on paper and confirm the shared state looks identical before and after the recursive call.&lt;/p&gt;
&lt;p&gt;And if the recursion itself still feels unstable underneath all this, that's worth shoring up first, because backtracking is just recursion with the choose, check, undo wrapper on top. Going back through &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/recursion&quot;&gt;the recursion lessons&lt;/a&gt; before touching backtracking again is what makes the wrapper feel mechanical instead of magical.&lt;/p&gt;
&lt;p&gt;I originally posted this on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/backtracking-algorithm-explained&quot;&gt;my own blog&lt;/a&gt;, a slightly longer version with the N-Queens trace and a few more worked examples.&lt;/p&gt;
&lt;p&gt;What's a bug that taught you more than the algorithm itself? For a lot of people backtracking is the one where a missing undo quietly breaks everything.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18581/backtracking-finally-clicked-when-i-fixed-one-habit</guid>
<pubDate>Thu, 21 May 2026 16:14:14 +0000</pubDate>
</item>
<item>
<title>AVL Trees Explained: The Rotation Patterns Most People Memorise Wrong</title>
<link>https://coderlegion.com/18486/avl-trees-explained-the-rotation-patterns-most-people-memorise-wrong</link>
<description>&lt;p&gt;The first time an interviewer asked me to walk through an LR rotation on a whiteboard, the four-case table was memorised cold. LL, RR, LR, RL. The interviewer drew three nodes, asked which case it was, and the answer came out fast. Then they asked: &quot;predict what a single right rotation would do to that tree, and tell me why it fails.&quot; That part hadn't been drilled. The grade went out as &quot;knows the table, didn't internalise the invariant.&quot;&lt;/p&gt;
&lt;h3 id=&quot;what-i-figured-out&quot;&gt;What I figured out&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The four rotation cases are not four things. They're two atomic moves applied once or twice.&lt;/li&gt;
&lt;li&gt;The balance factor is the rule being protected, not a check. Every rotation is a local repair on the rule &lt;code&gt;|left_height - right_height| &amp;lt;= 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Insertion needs at most one rotation. Deletion can need &lt;code&gt;O(log n)&lt;/code&gt;. That asymmetry is the answer to every &quot;what's the tradeoff&quot; question.&lt;/li&gt;
&lt;li&gt;The double-rotation cases exist because a single rotation on an &quot;inside&quot; imbalance shifts the heavy side without reducing the depth.&lt;/li&gt;
&lt;li&gt;Knowing the two atomic moves is enough to derive both LR and RL cases. Memorising the table on top is the wrong study target.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-my-approach-was-breaking-1&quot;&gt;Where my approach was breaking&lt;/h3&gt;
&lt;p&gt;Three failure modes showed up on those rounds, and they show up in most engineers preparing for the same kind of question.&lt;/p&gt;
&lt;p&gt;The four-case table got memorised without anyone tracing why each fix works. The table is a lookup. Interviewers test for the derivation. When a tree shows up on the board and the question is &quot;what's the imbalance shape here,&quot; pattern matching against a remembered picture fails when the tree is rotated or relabelled.&lt;/p&gt;
&lt;p&gt;Insertion got drilled, deletion didn't. Insertion is the cleaner operation. Almost every textbook walks insertion. Few walk deletion with the upward rebalance loop. Knowing &quot;deletion is harder&quot; as a phrase is not the same as knowing what concretely makes it harder.&lt;/p&gt;
&lt;p&gt;Wrong rotations never got sketched. When an interviewer asks &quot;what happens if you apply a single right rotation to an LR case,&quot; the answer has to come from a drawing, not a memory. Engineers who've never traced a failed rotation can't answer this on the spot.&lt;/p&gt;
&lt;h3 id=&quot;what-i-started-doing-differently-2&quot;&gt;What I started doing differently&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Then &lt;/th&gt;&lt;th&gt;Now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Memorise LL / RR / LR / RL as four rotations &lt;/td&gt;&lt;td&gt;Internalise two atomic rotations and derive the four cases from balance factor signs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practice insertion only &lt;/td&gt;&lt;td&gt;Practice deletion with the upward loop, including the worst-case &lt;code&gt;O(log n)&lt;/code&gt; rotation chain&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Treat balance factor as a check &lt;/td&gt;&lt;td&gt;Treat balance factor as the invariant being protected, with rotations as the local repair&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Draw rotated trees from memory &lt;/td&gt;&lt;td&gt;Draw the pointer assignments explicitly and recompute heights before moving on&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Skip wrong-rotation traces &lt;/td&gt;&lt;td&gt;Sketch what each wrong rotation does on an LR case, so the trap is recognisable under pressure&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &quot;now&quot; column is what interviewers actually probe. The &quot;then&quot; column is what most prep material asks you to drill.&lt;/p&gt;
&lt;p&gt;The first rule that locks in is this. If you see a node with balance factor &lt;code&gt;+2&lt;/code&gt;, the next look is at the left child's balance factor, not at the rotation table. If the left child is &lt;code&gt;+1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;, single right rotate the parent. If the left child is &lt;code&gt;-1&lt;/code&gt;, left rotate the left child first, then right rotate the parent. The decision is two questions, not a memorised table.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-3&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;Six months later, a different round asked for AVL insertion implementation. The sequence used to motivate the walk-through was &lt;code&gt;[5, 3, 4]&lt;/code&gt;, which fires an LR rotation. Here's the primitive:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def right_rotate(z):
    y = z.left
    T3 = y.right
    y.right = z
    z.left = T3
    z.height = 1 + max(height(z.left), height(z.right))
    y.height = 1 + max(height(y.left), height(y.right))
    return y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the insertion function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def insert(node, key):
    if not node:
        return Node(key)
    if key &amp;lt; node.key:
        node.left = insert(node.left, key)
    else:
        node.right = insert(node.right, key)

    node.height = 1 + max(height(node.left), height(node.right))
    bf = balance_factor(node)

    if bf &amp;gt; 1 and key &amp;lt; node.left.key:
        return right_rotate(node)
    if bf &amp;lt; -1 and key &amp;gt; node.right.key:
        return left_rotate(node)
    if bf &amp;gt; 1 and key &amp;gt; node.left.key:
        node.left = left_rotate(node.left)
        return right_rotate(node)
    if bf &amp;lt; -1 and key &amp;lt; node.right.key:
        node.right = right_rotate(node.right)
        return left_rotate(node)

    return node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tracing &lt;code&gt;[5, 3, 4]&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Insert &lt;code&gt;5&lt;/code&gt;. Single node, balance factor &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Insert &lt;code&gt;3&lt;/code&gt;. Goes left of &lt;code&gt;5&lt;/code&gt;. Node &lt;code&gt;5&lt;/code&gt;'s balance factor is &lt;code&gt;+1&lt;/code&gt;. Within the rule.&lt;/li&gt;
&lt;li&gt;Insert &lt;code&gt;4&lt;/code&gt;. &lt;code&gt;4 &amp;lt; 5&lt;/code&gt;, go left. &lt;code&gt;4 &amp;gt; 3&lt;/code&gt;, go right of &lt;code&gt;3&lt;/code&gt;. Node &lt;code&gt;4&lt;/code&gt; lands as &lt;code&gt;3&lt;/code&gt;'s right child.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Walking back up: node &lt;code&gt;4&lt;/code&gt; is balance factor &lt;code&gt;0&lt;/code&gt;, node &lt;code&gt;3&lt;/code&gt; is &lt;code&gt;-1&lt;/code&gt;, node &lt;code&gt;5&lt;/code&gt; is &lt;code&gt;+2&lt;/code&gt;. Rule broken at node &lt;code&gt;5&lt;/code&gt;, with its left child right-heavy. That's LR.&lt;/p&gt;
&lt;p&gt;First rotation: left rotation on node &lt;code&gt;3&lt;/code&gt;. Node &lt;code&gt;4&lt;/code&gt; moves up, node &lt;code&gt;3&lt;/code&gt; becomes &lt;code&gt;4&lt;/code&gt;'s left child. Node &lt;code&gt;5&lt;/code&gt; still has balance factor &lt;code&gt;+2&lt;/code&gt;, but its left subtree is now left-heavy. That's LL.&lt;/p&gt;
&lt;p&gt;Second rotation: right rotation on node &lt;code&gt;5&lt;/code&gt;. Node &lt;code&gt;4&lt;/code&gt; becomes the root, node &lt;code&gt;3&lt;/code&gt; is &lt;code&gt;4&lt;/code&gt;'s left child, node &lt;code&gt;5&lt;/code&gt; is &lt;code&gt;4&lt;/code&gt;'s right child. All balance factors back to &lt;code&gt;0&lt;/code&gt;. Height is 1.&lt;/p&gt;
&lt;p&gt;The interviewer asked what a single right rotation would have done to the original tree. Sketched it out: node &lt;code&gt;3&lt;/code&gt; becomes the root, node &lt;code&gt;5&lt;/code&gt; is &lt;code&gt;3&lt;/code&gt;'s right child, but node &lt;code&gt;4&lt;/code&gt; ends up as &lt;code&gt;3&lt;/code&gt;'s right child too, with node &lt;code&gt;5&lt;/code&gt; displaced. The tree depth does not decrease. That's why the double rotation exists.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-height-theory-behind-it-&quot;&gt;Why this works (the height theory behind it)&lt;/h3&gt;
&lt;p&gt;The choice of &lt;code&gt;±1&lt;/code&gt; as the balance factor cap is not arbitrary. It's the tightest constraint that's still cheap to maintain locally.&lt;/p&gt;
&lt;p&gt;An AVL tree with &lt;code&gt;n&lt;/code&gt; nodes has height at most about &lt;code&gt;1.44 * log2(n)&lt;/code&gt;. For a million nodes that's roughly 29 levels. A perfectly balanced tree would be 20 levels. The constant factor is small enough that you get the lookup speed of a balanced tree without paying the cost of full rebalancing on every insert.&lt;/p&gt;
&lt;p&gt;Red-Black trees relax the constraint to allow heights up to about &lt;code&gt;2 log(n)&lt;/code&gt;. That's roughly 40 levels for a million nodes. In exchange, at most 2 rotations per insertion or deletion. Looser balance, fewer rotations, faster writes.&lt;/p&gt;
&lt;p&gt;When Adelson-Velsky and Landis named the AVL tree in 1962, the workload they had in mind was read-heavy. Lookups dominated. The strict balance was worth the rebalancing cost because every lookup was faster. Modern systems insert and delete more than they search-only, which is why Java's &lt;code&gt;TreeMap&lt;/code&gt;, C++'s &lt;code&gt;std::map&lt;/code&gt;, and Linux's &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.kernel.org/scheduler/sched-design-CFS.html&quot;&gt;CFS process scheduler&lt;/a&gt; all picked Red-Black instead. Different workload, different tradeoff. Neither tree is &quot;better.&quot;&lt;/p&gt;
&lt;p&gt;That's the answer the OS interviewer was looking for. AVL is read-optimised. Red-Black is tuned for mixed workloads. Picking between them is a workload question, not a theory question.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-in-the-same-spot-2&quot;&gt;What I'd tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;Three things to drill before either interview round:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Master the two atomic moves. Write &lt;code&gt;left_rotate&lt;/code&gt; and &lt;code&gt;right_rotate&lt;/code&gt; from memory, with the height updates in the right order. Three pointer reassignments, two height updates. Everything else builds on these.&lt;/li&gt;
&lt;li&gt;Trace deletion at least three times. Pick at least one trace where the rebalance propagates two levels up. That's the answer to every &quot;what's the tradeoff of stricter balance&quot; question.&lt;/li&gt;
&lt;li&gt;Sketch what a wrong rotation does on an LR case. Once you've drawn the broken tree, the LR / RL trap stops being a memory test.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Codeintuition's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/binary-search-tree&quot;&gt;Binary Search Tree course&lt;/a&gt; is structured this way. The balance invariant comes before the rotation code, and the rotation code comes before the four-case table. Doing it in that order is what made the table feel mechanical instead of memorised.&lt;/p&gt;
&lt;p&gt;I originally posted this on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/avl-tree-explained&quot;&gt;my own blog&lt;/a&gt;, a slightly longer version with the worked example for the original &lt;code&gt;[3, 2, 1, 4, 5]&lt;/code&gt; insertion sequence and the AVL vs Red-Black tradeoff matrix.&lt;/p&gt;
&lt;p&gt;What's the AVL or rotation question that finally made the rule click for you, where the textbook explanation had not?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18486/avl-trees-explained-the-rotation-patterns-most-people-memorise-wrong</guid>
<pubDate>Wed, 20 May 2026 19:57:13 +0000</pubDate>
</item>
<item>
<title>FAANG Coding Interviews Aren’t the Same: The DSA Patterns Each Company Actually Tests</title>
<link>https://coderlegion.com/17592/faang-coding-interviews-arent-the-same-the-dsa-patterns-each-company-actually-tests</link>
<description>&lt;p&gt;&lt;em&gt;After treating every FAANG company like the same interview, Amazon exposed the gap in about twenty minutes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'd built FAANG prep around a small set of pattern families that looked highest-frequency: predicate search, sliding window, hash table fundamentals, and a few graph traversals.&lt;/p&gt;
&lt;p&gt;The mental model was that these were the patterns &quot;every big tech round&quot; sampled from.&lt;/p&gt;
&lt;p&gt;The first Amazon round asked about Randomised Set design followed by a Queue Design follow up.&lt;/p&gt;
&lt;p&gt;Neither was in the deep set the plan covered.&lt;/p&gt;
&lt;p&gt;I solved the first one with effort and improvised the second one badly.&lt;/p&gt;
&lt;p&gt;The interview went south in the last 20 minutes.&lt;/p&gt;
&lt;p&gt;The thing missing wasn't a specific pattern.&lt;/p&gt;
&lt;p&gt;It was that Amazon's pattern footprint is meaningfully wider than Google's, and the prep allocation behind every hour was treating every FAANG company as if they sampled from the same bag.&lt;/p&gt;
&lt;p&gt;Here's what came out of mapping company tags across 450+ handpicked FAANG problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Seven patterns are universal across 6+ major companies. Skipping any of them is a structural hole.&lt;/li&gt;
&lt;li&gt;Amazon spans 11+ pattern families, the broadest in the data. &quot;Study three patterns deeply&quot; doesn't work there.&lt;/li&gt;
&lt;li&gt;Google has narrower coverage but a distinctive emphasis on &lt;code&gt;predicate search&lt;/code&gt; (binary search on the answer space).&lt;/li&gt;
&lt;li&gt;Meta wants hash table depth and design fluency in a tighter pattern band.&lt;/li&gt;
&lt;li&gt;Prefix Sum is the single most undertrained pattern relative to its 8-company tag count.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-the-old-approach-was-breaking&quot;&gt;Where the old approach was breaking&lt;/h3&gt;
&lt;p&gt;The old plan looked roughly like this:&lt;/p&gt;
&lt;p&gt;Pick five or six pattern families that look universal-ish, drill them to the point you can write the canonical solution from memory, then trust that volume on adjacent problems will generalise to whatever the interview throws.&lt;/p&gt;
&lt;p&gt;The implicit bet was that every FAANG company is a sample from roughly the same distribution, and the mean of the drilled patterns covers the mean of the question pool.&lt;/p&gt;
&lt;p&gt;The data didn't support the bet.&lt;/p&gt;
&lt;p&gt;The surprising part wasn't that companies differed.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;It was how much they differed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Across the company tags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google's distribution skewed toward predicate search and graph reasoning. Narrower set, harder reasoning per problem.&lt;/li&gt;
&lt;li&gt;Amazon's distribution was the flattest of any company in the set. Eleven plus pattern families, none dominant.&lt;/li&gt;
&lt;li&gt;Meta's distribution clustered tight: sliding window, prefix sum, counting, design.&lt;/li&gt;
&lt;li&gt;Microsoft had a quietly distinct cluster: 2D binary search and staircase search, multi-dimensional patterns most plans skip.&lt;/li&gt;
&lt;li&gt;Apple's distribution was the inverse of Amazon's: deep on fundamentals (especially counting), almost no advanced design.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mistake wasn't drilling the wrong patterns.&lt;/p&gt;
&lt;p&gt;It was applying the same allocation across companies that didn't share the same allocation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-shift-that-worked&quot;&gt;The shift that worked&lt;/h3&gt;
&lt;p&gt;The shift was small in description and large in practice.&lt;/p&gt;
&lt;p&gt;Instead of one prep plan, the move was to build a &lt;strong&gt;per-company allocation&lt;/strong&gt;, anchored to the universal patterns underneath.&lt;/p&gt;
&lt;p&gt;Concretely:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Then                                                      &lt;/th&gt;&lt;th&gt;Now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Drill 5–6 patterns deeply, hope coverage generalises      &lt;/td&gt;&lt;td&gt;Cover 7 universals first, then add 2–4 patterns specific to the target company&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Pick problems by data structure (Hash Table, Tree, Graph) &lt;/td&gt;&lt;td&gt;Pick problems by pattern, then filter by company tag&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Treat all FAANG rounds as the same problem distribution   &lt;/td&gt;&lt;td&gt;Allocate by the company’s actual tag profile&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practise at uniform intensity across pattern families     &lt;/td&gt;&lt;td&gt;Front load the universals, then go deep on company-specific gaps&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The 7-pattern universal layer never moved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LRU Cache&lt;/li&gt;
&lt;li&gt;Counting&lt;/li&gt;
&lt;li&gt;Backtracking&lt;/li&gt;
&lt;li&gt;Prefix Sum&lt;/li&gt;
&lt;li&gt;Binary Search&lt;/li&gt;
&lt;li&gt;Fixed Sliding Window&lt;/li&gt;
&lt;li&gt;Variable Sliding Window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What changed was what came after.&lt;/p&gt;
&lt;p&gt;For an Amazon round, the next four patterns to drill were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Randomised Set&lt;/li&gt;
&lt;li&gt;Queue Design&lt;/li&gt;
&lt;li&gt;2D Binary Search&lt;/li&gt;
&lt;li&gt;Staircase Search&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;plus dedicated time on the Bar Raiser follow up format.&lt;/p&gt;
&lt;p&gt;For Google, the next two patterns were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predicate Search&lt;/li&gt;
&lt;li&gt;Graph reasoning&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;drilled deeper.&lt;/p&gt;
&lt;p&gt;For Meta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Design depth (&lt;code&gt;LRU Cache&lt;/code&gt; from scratch, &lt;code&gt;Randomised Set&lt;/code&gt; from scratch)&lt;/li&gt;
&lt;li&gt;Maximum Predicate Search&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The total prep time stayed roughly the same.&lt;/p&gt;
&lt;p&gt;The allocation was different.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-4&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;The clearest example was &lt;code&gt;LRU Cache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The canonical solution had been in the toolkit for years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hash map + doubly linked list&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O(1)&lt;/code&gt; per operation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt; sentinels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I'd never done was rebuild it under a constraint shift.&lt;/p&gt;
&lt;p&gt;Which is exactly what the Amazon Bar Raiser round likes to test.&lt;/p&gt;
&lt;p&gt;The setup is the standard one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        self.head = ListNode()                    # most recently used end
        self.tail = ListNode()                    # least recently used end
        self.head.next = self.tail
        self.tail.prev = self.head

    def get(self, key):
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._add_to_front(node)
        return node.value

    def put(self, key, value):
        if key in self.map:
            self._remove(self.map[key])

        node = ListNode(key, value)
        self._add_to_front(node)
        self.map[key] = node

        if len(self.map) &amp;gt; self.capacity:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.key]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The constraint shift Amazon likes to ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Now each entry has a TTL. A read on an expired entry returns &lt;code&gt;-1&lt;/code&gt;. The cache still operates at expected &lt;code&gt;O(1)&lt;/code&gt; per call.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's the moment the canonical answer stops being enough.&lt;/p&gt;
&lt;p&gt;The candidate who memorised LRU starts redesigning from scratch.&lt;/p&gt;
&lt;p&gt;The candidate who built the original from the invariant asks one question first:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which invariant did the doubly linked list maintain, and does that invariant still hold under TTL?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The list maintained an ordering:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Nodes near &lt;code&gt;head&lt;/code&gt; were used more recently than nodes near &lt;code&gt;tail&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The eviction criterion changes from:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Least recently used”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Expired or least recently used.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The list ordering by recency is still useful for the second clause.&lt;/p&gt;
&lt;p&gt;The first clause needs a different signal.&lt;/p&gt;
&lt;p&gt;That's where &lt;strong&gt;lazy expiry&lt;/strong&gt; (timestamp checked on read) and &lt;strong&gt;eager expiry&lt;/strong&gt; (min heap on TTL) diverge.&lt;/p&gt;
&lt;p&gt;The canonical LRU sits in most candidates' heads.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The constraint flip doesn't.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once practising the follow up explicitly becomes part of the loop, on every design problem in the universal set, the conversion rate on Amazon-style rounds shifts noticeably.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-pattern-that-s-underrated-for-years&quot;&gt;The pattern that's underrated for years&lt;/h3&gt;
&lt;p&gt;Looking back at the tag data, the single biggest miscalibration in the old plan was &lt;strong&gt;Prefix Sum&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Eight companies tag it.&lt;/p&gt;
&lt;p&gt;That's the same as Backtracking.&lt;/p&gt;
&lt;p&gt;More than almost every classic searching variant except the basic one.&lt;/p&gt;
&lt;p&gt;Most prep plans treat it like a minor utility pattern because it doesn't have a flagship problem the way &lt;code&gt;LRU Cache&lt;/code&gt; does.&lt;/p&gt;
&lt;p&gt;In the data, it punches above its weight:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Subarray Sum Equals K&lt;/li&gt;
&lt;li&gt;Equilibrium Index&lt;/li&gt;
&lt;li&gt;Count Subarrays Divisible by K&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;especially in Meta and Amazon variants.&lt;/p&gt;
&lt;p&gt;The fix took less than a weekend.&lt;/p&gt;
&lt;p&gt;Prefix Sum's mechanic is small enough that two evenings of focused drilling cover it.&lt;/p&gt;
&lt;p&gt;The composite version (&lt;strong&gt;prefix sum + hash map of seen sums&lt;/strong&gt;) is the FAANG-shaped one, and it's the one most prep plans bury under a miscellaneous heading.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-works-for-someone-in-the-same-spot&quot;&gt;What works for someone in the same spot&lt;/h3&gt;
&lt;p&gt;Three rules from the retrospective:&lt;/p&gt;
&lt;h4 id=&quot;1-cover-the-seven-universals-first&quot;&gt;1. Cover the seven universals first&lt;/h4&gt;
&lt;p&gt;No exceptions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LRU Cache&lt;/li&gt;
&lt;li&gt;Counting&lt;/li&gt;
&lt;li&gt;Backtracking&lt;/li&gt;
&lt;li&gt;Prefix Sum&lt;/li&gt;
&lt;li&gt;Binary Search&lt;/li&gt;
&lt;li&gt;Fixed Sliding Window&lt;/li&gt;
&lt;li&gt;Variable Sliding Window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No matter the target company.&lt;/p&gt;
&lt;h4 id=&quot;2-specialise-second&quot;&gt;2. Specialise second&lt;/h4&gt;
&lt;p&gt;But specialise to the company’s actual tag profile.&lt;/p&gt;
&lt;p&gt;For Amazon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;breadth&lt;/li&gt;
&lt;li&gt;constraint-flip follow ups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Google:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;predicate search&lt;/li&gt;
&lt;li&gt;graph reasoning depth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Meta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;design depth&lt;/li&gt;
&lt;li&gt;sliding window fluency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Microsoft:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2D + staircase search&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For Apple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fundamentals depth&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-cut-what-doesn-t-match&quot;&gt;3. Cut what doesn't match&lt;/h4&gt;
&lt;p&gt;Targeting Google and not Amazon?&lt;/p&gt;
&lt;p&gt;Queue Design can wait.&lt;/p&gt;
&lt;p&gt;Targeting Meta and not Microsoft?&lt;/p&gt;
&lt;p&gt;Staircase Search can wait.&lt;/p&gt;
&lt;p&gt;Prep time is finite.&lt;/p&gt;
&lt;p&gt;The data tells you where the false-priority work lives.&lt;/p&gt;
&lt;p&gt;The biggest shift, though, wasn't any one pattern.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“FAANG prep” isn't one allocation problem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;It's five separate ones with a shared seven-pattern foundation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The company determines which 2–4 patterns sit on top of it.&lt;/p&gt;
&lt;p&gt;Every hour spent assuming the foundation was all that mattered was an hour that could've gone to the layer above.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally posted in a longer form on my &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/amazon-vs-google-coding-interview-dsa&quot;&gt;own blog&lt;/a&gt;, with the per-company FAQ and a fuller breakdown of what each company quietly deemphasises.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you've interviewed at more than one FAANG, what was a pattern that came up where the company’s reputation hadn't prepared you for it?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17592/faang-coding-interviews-arent-the-same-the-dsa-patterns-each-company-actually-tests</guid>
<pubDate>Tue, 12 May 2026 15:44:03 +0000</pubDate>
</item>
<item>
<title>What Amazon’s Bar Raiser Actually Tests in Coding Interviews</title>
<link>https://coderlegion.com/17439/what-amazons-bar-raiser-actually-tests-in-coding-interviews</link>
<description>&lt;p&gt;&lt;em&gt;I thought solving the problem was the round. The last ten minutes turned out to be the part that counted.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'd put close to four months into Amazon prep by the time my onsite landed. Two coding rounds went fine. The third started fine too.&lt;/p&gt;
&lt;p&gt;I solved the initial problem in about twenty minutes, walked through the test cases, and felt the round was already mostly won.&lt;/p&gt;
&lt;p&gt;Then the interviewer asked one follow up.&lt;/p&gt;
&lt;p&gt;The constraint they changed was small, the kind of thing I'd have called &quot;minor&quot; if I'd been describing it on paper.&lt;/p&gt;
&lt;p&gt;It exposed that I'd been preparing for the wrong half of the round, and I spent the next ten minutes recovering ground I should never have lost.&lt;/p&gt;
&lt;p&gt;That round was a Bar Raiser round.&lt;/p&gt;
&lt;p&gt;I didn't know it at the time (Amazon doesn't tell you), but the debrief came back with the kind of feedback only the Bar Raiser writes:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Strong initial solution, struggled to adapt under constraint change, did not demonstrate the structural reasoning we look for.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The phrasing is almost word for word from how Bar Raisers grade.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The follow up after the working solution is the round.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-i-figured-out-1&quot;&gt;What I figured out&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The Bar Raiser is one of the four or five interviewers on the loop, but you won't know which one. Treat every round as if it's them.&lt;/li&gt;
&lt;li&gt;Amazon's pattern coverage is wider than Google's or Meta's, so going deep on three families isn't enough.&lt;/li&gt;
&lt;li&gt;The grade often lives in the last ten minutes after your first working solution, not in the first solution itself.&lt;/li&gt;
&lt;li&gt;Adapting after a constraint flip is a specific skill that grinding more problems doesn't build.&lt;/li&gt;
&lt;li&gt;The fix is interleaved practice plus an explicit constraint flip rep on every problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-my-approach-was-breaking-2&quot;&gt;Where my approach was breaking&lt;/h3&gt;
&lt;p&gt;My prep model was the standard FAANG one.&lt;/p&gt;
&lt;p&gt;Pick the two or three pattern families that show up most for the company. Go deep on those. Time myself on company tagged problems. Repeat until recall got fast.&lt;/p&gt;
&lt;p&gt;For Google that worked well enough.&lt;/p&gt;
&lt;p&gt;Google leans heavily on predicate search, counting, and graph traversal. If you're solid on those three, the round usually lands in your zone. I'd interviewed at Google with that exact prep and it had been fine.&lt;/p&gt;
&lt;p&gt;Amazon's pattern set is wider.&lt;/p&gt;
&lt;p&gt;Across the problems Amazon actually tests, you'll see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;counting&lt;/li&gt;
&lt;li&gt;fixed and variable sliding window&lt;/li&gt;
&lt;li&gt;prefix sum&lt;/li&gt;
&lt;li&gt;LRU-style design&lt;/li&gt;
&lt;li&gt;randomized set design&lt;/li&gt;
&lt;li&gt;plain binary search&lt;/li&gt;
&lt;li&gt;2D and staircase binary search variants&lt;/li&gt;
&lt;li&gt;queue design&lt;/li&gt;
&lt;li&gt;backtracking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's roughly eleven distinct families against Google's seven or eight.&lt;/p&gt;
&lt;p&gt;The depth strategy that worked for Google had a worse hit rate at Amazon, and on the round that turned into the Bar Raiser slot, the problem landed in a family I'd given less time to.&lt;/p&gt;
&lt;p&gt;The deeper issue was something the wider pattern coverage just made more visible.&lt;/p&gt;
&lt;p&gt;Even on patterns I &lt;em&gt;had&lt;/em&gt; practised, my fluency was template recall, not invariant construction.&lt;/p&gt;
&lt;p&gt;As long as the problem looked enough like a textbook example, I produced the standard solution quickly.&lt;/p&gt;
&lt;p&gt;The moment the constraint shifted, I tried to retrieve a different template instead of modifying the one I had.&lt;/p&gt;
&lt;p&gt;The Bar Raiser format is built to expose that gap.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The follow up after the working solution is the round.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-started-doing-differently-3&quot;&gt;What I started doing differently&lt;/h3&gt;
&lt;p&gt;After the rejection landed, I went back through every coding round I'd done in mock interviews, including the ones I'd thought went well.&lt;/p&gt;
&lt;p&gt;Two patterns showed up across roughly half of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I'd produced a correct solution within the time budget, but I couldn't have explained which invariant the data structure I picked was maintaining. The hash map worked. I couldn't have said cleanly what would break if I'd used an array instead.&lt;/li&gt;
&lt;li&gt;When the mock interviewer changed a constraint at the end (which the better mocks always did), I either started from scratch or stalled. I almost never reached for the original solution and asked which part of it depended on the constraint that had moved.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both were habits I'd built by drilling for speed against template problems.&lt;/p&gt;
&lt;p&gt;Neither was a coding skill I lacked.&lt;/p&gt;
&lt;p&gt;They were prep habits.&lt;/p&gt;
&lt;p&gt;I shifted two things.&lt;/p&gt;
&lt;p&gt;First, every problem I solved got a &lt;strong&gt;constraint flip rep&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Whatever the original constraint was, I changed one piece of it and reimplemented.&lt;/p&gt;
&lt;p&gt;Second, I started reading my old solutions out loud as if explaining them to an interviewer, naming the invariant the structure maintained on every line.&lt;/p&gt;
&lt;p&gt;The first felt like more work for the same problem count.&lt;/p&gt;
&lt;p&gt;The second felt like wasted time.&lt;/p&gt;
&lt;p&gt;Both turned out to be the gap.&lt;/p&gt;
&lt;p&gt;Here's what changed in practice:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What I used to do                                   &lt;/th&gt;&lt;th&gt;What I started doing&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Solve the problem, check the answer, move on        &lt;/td&gt;&lt;td&gt;Solve the problem, then write down the constraint change that would invalidate the solution most&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Drill problems within one pattern family per week   &lt;/td&gt;&lt;td&gt;Rotate between three or four pattern families inside a single session&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Recall the optimal solution before writing any code &lt;/td&gt;&lt;td&gt;Get a correct &lt;code&gt;O(n²)&lt;/code&gt; working first, confirm edge cases, then improve&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Treat the follow up question in mocks as a bonus    &lt;/td&gt;&lt;td&gt;Treat the follow up as the main rep&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Explain the solution by walking through the code    &lt;/td&gt;&lt;td&gt;Explain the solution by naming the invariant first&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-word-search-follow-up&quot;&gt;What that looked like on the next Word Search follow up&lt;/h3&gt;
&lt;p&gt;A few weeks later I ran a mock that landed on Word Search.&lt;/p&gt;
&lt;p&gt;Standard backtracking on a grid:&lt;/p&gt;
&lt;p&gt;Given a board of characters and a target word, return whether the word can be formed by a path of adjacent cells, with no cell reused.&lt;/p&gt;
&lt;p&gt;The solution is a depth first search from each starting cell with a visited mark.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def exist(board, word):
    rows, cols = len(board), len(board[0])

    def dfs(r, c, idx):
        if idx == len(word):
            return True
        if r &amp;lt; 0 or r &amp;gt;= rows or c &amp;lt; 0 or c &amp;gt;= cols:
            return False
        if board[r][c] != word[idx]:
            return False

        tmp = board[r][c]
        board[r][c] = '#'

        found = (
            dfs(r+1, c, idx+1)
            or dfs(r-1, c, idx+1)
            or dfs(r, c+1, idx+1)
            or dfs(r, c-1, idx+1)
        )

        board[r][c] = tmp
        return found

    for r in range(rows):
        for c in range(cols):
            if dfs(r, c, 0):
                return True

    return False
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The invariant the in-place marking maintains:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;At any point in the recursion, the cells along the current path are marked &lt;code&gt;#&lt;/code&gt;, every other cell is restored.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Backtracking by restoring on the way out keeps the invariant true at depth zero, which is what makes:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“No cell reused along this path”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;hold.&lt;/p&gt;
&lt;p&gt;Then came the follow up.&lt;/p&gt;
&lt;p&gt;Instead of a single word, you now have a list of &lt;code&gt;k&lt;/code&gt; words to find on the same board.&lt;/p&gt;
&lt;p&gt;Same constraint.&lt;/p&gt;
&lt;p&gt;No cell reused along a single word's path.&lt;/p&gt;
&lt;p&gt;The old me would've started writing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for w in words:
    exist(board, w)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;taken the cost of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(k * rows * cols * 4^L)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and gone home.&lt;/p&gt;
&lt;p&gt;The interviewer would've asked the obvious question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Can you do better than the naive loop?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which is exactly what had happened in the real round.&lt;/p&gt;
&lt;p&gt;This time I asked a different question first:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which part of the original solution depended on the constraint that just changed?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The DFS was doing the same prefix walk repeatedly for every word sharing a prefix.&lt;/p&gt;
&lt;p&gt;So instead of changing the traversal:&lt;/p&gt;
&lt;p&gt;Change what the traversal checks against.&lt;/p&gt;
&lt;p&gt;Compose the DFS with a trie of all &lt;code&gt;k&lt;/code&gt; words.&lt;/p&gt;
&lt;p&gt;Every board walk now checks against all words simultaneously.&lt;/p&gt;
&lt;p&gt;The invariant doesn't change.&lt;/p&gt;
&lt;p&gt;The base case changes.&lt;/p&gt;
&lt;p&gt;The grid traversal stays.&lt;/p&gt;
&lt;p&gt;The constraint changed.&lt;/p&gt;
&lt;p&gt;Not the structure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def find_words(board, words):
    root = build_trie(words)
    rows, cols = len(board), len(board[0])
    found = set()

    def dfs(r, c, node):
        ch = board[r][c]

        if ch == '#' or ch not in node.children:
            return

        nxt = node.children[ch]

        if nxt.word:
            found.add(nxt.word)
            nxt.word = None

        board[r][c] = '#'

        for dr, dc in ((1,0), (-1,0), (0,1), (0,-1)):
            nr, nc = r + dr, c + dc

            if 0 &amp;lt;= nr &amp;lt; rows and 0 &amp;lt;= nc &amp;lt; cols:
                dfs(nr, nc, nxt)

        board[r][c] = ch

    for r in range(rows):
        for c in range(cols):
            dfs(r, c, root)

    return list(found)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I got there in roughly four minutes.&lt;/p&gt;
&lt;p&gt;Not because I'd drilled Word Search II as a separate template.&lt;/p&gt;
&lt;p&gt;Because I'd been practising one question repeatedly:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which part of the original depended on the constraint that changed?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The change was in what the DFS checked.&lt;/p&gt;
&lt;p&gt;Not in the DFS itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-interleaving-works-for-breadth&quot;&gt;Why interleaving works for breadth&lt;/h3&gt;
&lt;p&gt;The other half of the gap was simply pattern coverage.&lt;/p&gt;
&lt;p&gt;Three families deep wasn't enough for Amazon's surface area.&lt;/p&gt;
&lt;p&gt;But I also couldn't realistically go equally deep on eleven families in the time I had left.&lt;/p&gt;
&lt;p&gt;What I &lt;em&gt;could&lt;/em&gt; do was rotate between them.&lt;/p&gt;
&lt;p&gt;Research on interleaved practice consistently shows that mixing problem types improves your ability to identify which pattern applies to a new problem.&lt;/p&gt;
&lt;p&gt;Mixed practice feels harder.&lt;/p&gt;
&lt;p&gt;Blocked practice feels easier.&lt;/p&gt;
&lt;p&gt;That's exactly why it works.&lt;/p&gt;
&lt;p&gt;Blocked practice gives you:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“This week is sliding window.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Bar Raiser gives you:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Figure out what this is.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The added friction during practice was the same identification work the round required.&lt;/p&gt;
&lt;p&gt;Practically, that meant:&lt;/p&gt;
&lt;p&gt;A session of four problems became:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;four different families&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;not:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;four sliding window problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-in-the-same-spot-3&quot;&gt;What I'd tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;A few rules I wish I'd had four months earlier.&lt;/p&gt;
&lt;h4 id=&quot;1-get-a-working-solution-before-chasing-the-optimal-one&quot;&gt;1. Get a working solution before chasing the optimal one&lt;/h4&gt;
&lt;p&gt;Twenty-five minutes thinking before writing leaves no room for the follow up.&lt;/p&gt;
&lt;p&gt;A correct &lt;code&gt;O(n²)&lt;/code&gt; solution that gets improved beats a half-written &lt;code&gt;O(n)&lt;/code&gt; attempt every time.&lt;/p&gt;
&lt;h4 id=&quot;2-treat-the-follow-up-as-the-round&quot;&gt;2. Treat the follow up as the round&lt;/h4&gt;
&lt;p&gt;When the interviewer changes a constraint:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Ask which part of your solution depended on that constraint.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Modify only that part.&lt;/p&gt;
&lt;p&gt;The instinct to redesign from zero is expensive.&lt;/p&gt;
&lt;h4 id=&quot;3-practise-the-constraint-flip-explicitly&quot;&gt;3. Practise the constraint flip explicitly&lt;/h4&gt;
&lt;p&gt;Every problem gets a second pass with one constraint changed.&lt;/p&gt;
&lt;p&gt;Implement the modified version.&lt;/p&gt;
&lt;p&gt;That rep builds adaptation.&lt;/p&gt;
&lt;p&gt;And adaptation is what the round grades.&lt;/p&gt;
&lt;p&gt;If someone had explicitly taught me how to identify which part of a solution depends on a changing constraint, it would've shortened the loop considerably. The closest thing to the work I had to build for myself later was a pattern-identification lesson I eventually added inside Codeintuition.&lt;/p&gt;
&lt;p&gt;I originally posted a longer version on my &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/amazon-bar-raiser-coding-interview&quot;&gt;own blog&lt;/a&gt; with the per-pattern breakdown across all eleven families and how the rest of Amazon's interview loop factors into the Bar Raiser grade.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What’s a constraint flip from an interview that exposed a solution you thought you understood?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17439/what-amazons-bar-raiser-actually-tests-in-coding-interviews</guid>
<pubDate>Mon, 11 May 2026 14:47:38 +0000</pubDate>
</item>
<item>
<title>Arrays vs Linked Lists: The Real Difference Most Interview Prep Misses</title>
<link>https://coderlegion.com/17281/arrays-vs-linked-lists-the-real-difference-most-interview-prep-misses</link>
<description>&lt;p&gt;&lt;em&gt;It took an &lt;code&gt;LRU Cache&lt;/code&gt; interview for me to realise the complexity table wasn't answering the real question.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'd been picking between arrays and linked lists for years before realising the table wasn't the answer to the question.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;LRU Cache&lt;/code&gt; came up in an interview, and the table didn't help: both structures were on the page, neither alone solved the problem, and the silence ran nine minutes before the interviewer gave a hint.&lt;/p&gt;
&lt;h3 id=&quot;what-i-figured-out-2&quot;&gt;What I figured out&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Arrays and linked lists differ for one reason: &lt;strong&gt;contiguous memory vs scattered memory&lt;/strong&gt;. Every entry on the complexity table follows from that single fact.&lt;/li&gt;
&lt;li&gt;Two &lt;code&gt;O(n)&lt;/code&gt; operations aren't equal in practice. An array scan of a million integers runs roughly an order of magnitude faster than the equivalent linked list traversal because of CPU cache locality.&lt;/li&gt;
&lt;li&gt;The &quot;&lt;code&gt;O(1)&lt;/code&gt; insertion&quot; claim about linked lists comes with a hidden prerequisite: the algorithm has to already hold a pointer to the position. Find it first, and you've paid &lt;code&gt;O(n)&lt;/code&gt; before the insertion runs.&lt;/li&gt;
&lt;li&gt;Many interesting interview problems combine both structures rather than picking one. &lt;code&gt;LRU Cache&lt;/code&gt; is the canonical case.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-the-table-stopped-working&quot;&gt;Where the table stopped working&lt;/h3&gt;
&lt;p&gt;For a long time the table &lt;em&gt;was&lt;/em&gt; the decision tree.&lt;/p&gt;
&lt;p&gt;The problem mentioned &quot;insert&quot; or &quot;delete,&quot; reach for a linked list. The problem mentioned &quot;access by index,&quot; reach for an array.&lt;/p&gt;
&lt;p&gt;That worked on simple problems.&lt;/p&gt;
&lt;p&gt;It fell apart on anything that needed both behaviours.&lt;/p&gt;
&lt;p&gt;The specific moment was sitting in front of &lt;code&gt;LRU Cache&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The problem wants &lt;code&gt;get(key)&lt;/code&gt; and &lt;code&gt;put(key, value)&lt;/code&gt; both in &lt;code&gt;O(1)&lt;/code&gt; average time, with eviction of the least recently used entry when the cache is full.&lt;/p&gt;
&lt;p&gt;A hash map handles the lookup half, since &lt;code&gt;get&lt;/code&gt; has to be &lt;code&gt;O(1)&lt;/code&gt; and a hash map is the obvious answer.&lt;/p&gt;
&lt;p&gt;Then comes the recency tracking, which means some kind of ordering on the entries.&lt;/p&gt;
&lt;p&gt;An array of &lt;code&gt;(key, timestamp)&lt;/code&gt; pairs would mean updating timestamps on access, and the eviction step turns into a linear scan for the smallest timestamp.&lt;/p&gt;
&lt;p&gt;That breaks the &lt;code&gt;O(1)&lt;/code&gt; requirement.&lt;/p&gt;
&lt;p&gt;Switch to a linked list and inserting at the front on every access is &lt;code&gt;O(1)&lt;/code&gt; &lt;em&gt;given a pointer to the node&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To find the node from a key, the algorithm walks the list, which is &lt;code&gt;O(n)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The hash map gives &lt;code&gt;O(1)&lt;/code&gt; lookups but no reordering.&lt;/p&gt;
&lt;p&gt;The linked list gives &lt;code&gt;O(1)&lt;/code&gt; reordering but no fast lookup.&lt;/p&gt;
&lt;p&gt;The problem wasn't picking the wrong structure.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The problem was thinking the question had only two answers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-realisation-that-broke-the-table&quot;&gt;The realisation that broke the table&lt;/h3&gt;
&lt;p&gt;The fix sounds obvious in retrospect:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep both.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A hash map mapping keys to &lt;em&gt;linked list nodes&lt;/em&gt;, and a doubly linked list whose nodes store the values and recency order.&lt;/p&gt;
&lt;p&gt;The hash map gives &lt;code&gt;O(1)&lt;/code&gt; access to the exact node.&lt;/p&gt;
&lt;p&gt;Once the algorithm has the node, the linked list reorders without traversing in &lt;code&gt;O(1)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Eviction becomes removing the tail node, which is also &lt;code&gt;O(1)&lt;/code&gt; because a tail pointer is maintained.&lt;/p&gt;
&lt;p&gt;The two structures aren't competing.&lt;/p&gt;
&lt;p&gt;They're solving different sub-problems.&lt;/p&gt;
&lt;p&gt;The hash map handles constant-time access.&lt;/p&gt;
&lt;p&gt;The linked list handles pointer reassignment at known positions.&lt;/p&gt;
&lt;p&gt;The qualifier on linked list insertion:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;&lt;code&gt;O(1)&lt;/code&gt; only if you already hold the pointer&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;finally has a structure feeding it those pointers for free.&lt;/p&gt;
&lt;p&gt;That's the same property powering a handful of linked list patterns that previously read like tricks: reversal, splitting, merging, fast and slow pointers.&lt;/p&gt;
&lt;p&gt;That's the realisation that broke the table.&lt;/p&gt;
&lt;p&gt;The table had been answering:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“If you pick exactly one structure, which one wins?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The interesting problems were asking a different question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Which combination of memory layouts gives every operation in the algorithm the access pattern it needs?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-it-looks-like-in-code&quot;&gt;What it looks like in code&lt;/h3&gt;
&lt;p&gt;Here's the construction the technique walkthrough lands on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.lookup = {}                  # hash map: key -&amp;gt; Node
        self.head = Node(0, 0)           # sentinel
        self.tail = Node(0, 0)           # sentinel
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_front(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.lookup:
            return -1

        node = self.lookup[key]          # O(1) via hash map
        self._remove(node)               # O(1) via direct pointer
        self._add_front(node)            # O(1) splice at head

        return node.value

    def put(self, key, value):
        if key in self.lookup:
            self._remove(self.lookup[key])

        node = Node(key, value)
        self.lookup[key] = node
        self._add_front(node)

        if len(self.lookup) &amp;gt; self.capacity:
            lru = self.tail.prev         # O(1) via tail sentinel
            self._remove(lru)
            del self.lookup[lru.key]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every operation is &lt;code&gt;O(1)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The hash map carries the lookup load.&lt;/p&gt;
&lt;p&gt;The doubly linked list carries the ordering load.&lt;/p&gt;
&lt;p&gt;Each structure does the thing it's actually good at, and the combination handles a problem neither could solve alone.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-my-earlier-self&quot;&gt;What I'd tell my earlier self&lt;/h3&gt;
&lt;p&gt;The contrast between the old picking heuristic and the current one comes out cleanest side by side:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Then                                                                &lt;/th&gt;&lt;th&gt;Now&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Match keywords in the prompt (&quot;insert&quot; → linked list) to the table. &lt;/td&gt;&lt;td&gt;Identify every operation the algorithm performs and check whether each one already has the pointer or index it needs.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Treat arrays and linked lists as competing answers.                 &lt;/td&gt;&lt;td&gt;Treat them as primitives that combine.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Quote &lt;code&gt;O(1)&lt;/code&gt; insertion for linked lists without the qualifier.      &lt;/td&gt;&lt;td&gt;Read &quot;&lt;code&gt;O(1)&lt;/code&gt; insertion&quot; as &quot;&lt;code&gt;O(1)&lt;/code&gt; once you've paid for the pointer.&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Trust two &lt;code&gt;O(n)&lt;/code&gt; operations as equivalent in practice.              &lt;/td&gt;&lt;td&gt;Account for cache locality. Array scans often beat linked list traversals in real hardware.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And three rules that would've shortened the loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Identify the dominant operation first.&lt;/strong&gt; Before picking a structure, write down the access pattern the algorithm needs. &quot;Lookup by key, then mutate at that position&quot; is a different question than &quot;lookup by key, then iterate from there.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check the qualifier on every linked list insertion claim.&lt;/strong&gt; Does the algorithm already hold the pointer, or does it have to walk to find it? If it has to walk, the advantage is gone.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When two structures each solve half the problem, try combining them.&lt;/strong&gt; That's the move that handles &lt;code&gt;LRU Cache&lt;/code&gt;, caching problems, and most eviction-style systems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wrote a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/arrays-vs-linked-lists-difference&quot;&gt;longer version&lt;/a&gt; on Codeintuition that walks the same memory-layout reasoning through stacks, queues, and graph adjacency representations.&lt;/p&gt;
&lt;p&gt;If you want a deeper walkthrough of pointer-based mutation patterns, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/learning-paths/data-structures/singly-linked-list&quot;&gt;singly linked list course&lt;/a&gt; on codeintuition covers seven of them with worked examples.&lt;/p&gt;
&lt;p&gt;What was the moment a data structure choice finally clicked for you, where the textbook framing had been hiding the actual question?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17281/arrays-vs-linked-lists-the-real-difference-most-interview-prep-misses</guid>
<pubDate>Sun, 10 May 2026 13:06:23 +0000</pubDate>
</item>
<item>
<title>How to Know If You’re Ready for FAANG Interviews</title>
<link>https://coderlegion.com/17163/how-to-know-if-youre-ready-for-faang-interviews</link>
<description>&lt;p&gt;&lt;em&gt;After 280 LeetCode problems, I realised “feeling ready” wasn’t a metric. A diagnostic was.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I'd solved around 280 problems by the time the FAANG readiness question got serious, and the answer changed every week. Some weeks felt close after a clean batch of trees. Most weeks the next unfamiliar medium would freeze cold and the confidence would evaporate by Tuesday. The pattern repeated for months without resolving.&lt;/p&gt;
&lt;p&gt;It took longer than it should have to notice the question itself was the issue.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Do I feel ready&quot; wasn't a question with a stable answer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The threshold was real. The measurement of it was a mood read on a moving session.&lt;/p&gt;
&lt;h3 id=&quot;what-i-figured-out-3&quot;&gt;What I figured out&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Self assessed readiness drifts every session. A clean morning of trees and a frozen afternoon of graphs both produced wrong answers.&lt;/li&gt;
&lt;li&gt;The interview wasn't testing the same skill that practice on familiar problems trains. It was testing what holds up on shapes you haven't seen.&lt;/li&gt;
&lt;li&gt;Problem count flatlined as a useful signal somewhere around 100 problems. Past that, more volume on familiar shapes added count without moving the threshold.&lt;/li&gt;
&lt;li&gt;The fix was a measurable test, not more prep. An unseen medium, a hidden title, a real timer, repeated across three different families.&lt;/li&gt;
&lt;li&gt;The diagnostic was more useful than the score. &quot;Two passed, one failed on graphs&quot; was an action plan. &quot;Feeling 70 percent ready&quot; was anxiety.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quick disclosure before I get into this: I built Codeintuition, a structured learning platform for coding interviews. This post is the retrospective on what someone should have handed me earlier, not a pitch for the product.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-the-approach-was-breaking-2&quot;&gt;Where the approach was breaking&lt;/h3&gt;
&lt;p&gt;The first thing that broke was the belief that the count was the metric.&lt;/p&gt;
&lt;p&gt;The tracker said 280, on its way to 350. The tracker also didn't predict any individual interview performance. Open a medium you haven't seen, freeze for ten minutes, and the count number sits there grinning at you, useless.&lt;/p&gt;
&lt;p&gt;The second thing that broke was the topic completion checkmark.&lt;/p&gt;
&lt;p&gt;Sliding window finished in February. By June the recall on a sliding window medium you hadn't seen was unreliable. The completion checkmark didn't decay.&lt;/p&gt;
&lt;p&gt;Recall did.&lt;/p&gt;
&lt;p&gt;The third thing that broke was speed on familiar problems.&lt;/p&gt;
&lt;p&gt;Two Sum in two minutes feels like fluency. It's actually retrieval of a stored solution. The first time you hit a problem that &lt;em&gt;looks&lt;/em&gt; like Two Sum but has a different constraint structure, the speed disappears and you're back to staring.&lt;/p&gt;
&lt;p&gt;The fourth thing was peer comparison, which broke quietly because it had never worked in the first place.&lt;/p&gt;
&lt;p&gt;Someone else got into Google in six months. That said nothing about your readiness. Background, family coverage, how they'd practised, the level they interviewed for, none of it generalises.&lt;/p&gt;
&lt;p&gt;What was actually happening is that all four signals were measuring the wrong thing.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The interview gives you a problem you haven't seen. The four signals all measured behaviour on problems you had seen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They were diagnostically useless for the question they were being asked to answer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-diagnostic-that-ended-up-working&quot;&gt;The diagnostic that ended up working&lt;/h3&gt;
&lt;p&gt;The version that worked was small and strict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two hours. Three problems. Three different families. Real interview conditions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick a pattern family genuinely studied (sliding window, tree traversal, graph BFS, DP subsequence)&lt;/li&gt;
&lt;li&gt;Find a medium difficulty problem in that family that has &lt;strong&gt;never&lt;/strong&gt; been solved, hinted, or read about&lt;/li&gt;
&lt;li&gt;Cover the problem name so the title can't reveal the family&lt;/li&gt;
&lt;li&gt;Twenty minute timer, hard cap on failed code execution attempts, no peeking&lt;/li&gt;
&lt;li&gt;Identify the family from the constraints, build the solution from the family's invariant, trace it before running&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pass was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;solved within the timer&lt;/li&gt;
&lt;li&gt;fewer than two failed runs&lt;/li&gt;
&lt;li&gt;family identified from the constraints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anything else was data.&lt;/p&gt;
&lt;p&gt;Three passes across three families was a strong readiness signal. Recognition was firing on shapes you hadn't seen, the construction was following the family's invariant rather than recall, and the conditions were close enough to a real round that the result generalised.&lt;/p&gt;
&lt;p&gt;Two passes and one failure told you precisely which family to work on. No need for more random volume.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Depth in one specific place.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;All three failed was the most useful diagnostic of the three.&lt;/p&gt;
&lt;p&gt;It said the prep method had been building near transfer without far transfer, which is the gap described in the idea of Transfer of Learning.&lt;/p&gt;
&lt;p&gt;Method gap, not talent gap.&lt;/p&gt;
&lt;p&gt;The fix was correspondingly narrower than:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Grind more problems.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-third-pass&quot;&gt;What that looked like on the third pass&lt;/h3&gt;
&lt;p&gt;The pass that taught the most was a DP subsequence problem run cold.&lt;/p&gt;
&lt;p&gt;The family had been studied. The canonical examples had been worked through. It hadn't been ground the way sliding window or trees had been ground, so it was a fair test of recognition rather than recall.&lt;/p&gt;
&lt;p&gt;The constraints described a longest increasing subsequence variant.&lt;/p&gt;
&lt;p&gt;With the title hidden, the recognition still happened in the first three minutes because the constraints fit the DP subsequence shape:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A one dimensional input, not two.&lt;/li&gt;
&lt;li&gt;An output asking for the longest sub structure satisfying a condition.&lt;/li&gt;
&lt;li&gt;The absence of two pointer or sliding window triggers that would have applied if the answer had been contiguous.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The skeleton came from the family's invariant, not from a remembered problem:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; is the length of the longest valid subsequence ending at index &lt;code&gt;i&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def length_of_lis(nums):
    if not nums:
        return 0

    dp = [1] * len(nums)

    for i in range(1, len(nums)):
        for j in range(i):
            if nums[j] &amp;lt; nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)

    return max(dp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trace on a five element array by hand. Verify the recurrence on the trace. Then run.&lt;/p&gt;
&lt;p&gt;It passed.&lt;/p&gt;
&lt;p&gt;Five minutes still on the timer.&lt;/p&gt;
&lt;p&gt;The check that mattered wasn't getting it right.&lt;/p&gt;
&lt;p&gt;It was how:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Family identified from the constraints inside three minutes&lt;/li&gt;
&lt;li&gt;Skeleton written from the invariant rather than retrieval&lt;/li&gt;
&lt;li&gt;Verified by tracing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's what pattern recognition holding under pressure looks like in practice.&lt;/p&gt;
&lt;p&gt;If any of those three steps had broken, the pass would have been a memory pass, not a recognition pass, and the family wouldn't actually have been ready yet.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-protocol-the-earlier-version-of-me-needed&quot;&gt;The protocol the earlier version of me needed&lt;/h3&gt;
&lt;p&gt;The change that mattered most was switching what practice was for.&lt;/p&gt;
&lt;p&gt;Up to about month four practice was for a problem set. After running the diagnostic for the first time, practice changed shape to feed the diagnostic.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Before the diagnostic                &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Solve next problem on the list       &lt;/td&gt;&lt;td&gt;Pick a family, work through three problems with the title hidden&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Read editorial when stuck            &lt;/td&gt;&lt;td&gt;Read the foundational material on the family's invariant when stuck&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Track problem count                  &lt;/td&gt;&lt;td&gt;Track families where the diagnostic passes cleanly&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Do problems in order of difficulty   &lt;/td&gt;&lt;td&gt;Do problems in order of how shallow recognition was on the family&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Speed up by recognising problems     &lt;/td&gt;&lt;td&gt;Speed up by recognising the family from constraints with the title hidden&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;How many problems before I'm ready&quot; &lt;/td&gt;&lt;td&gt;&quot;How many families pass the diagnostic before I'm ready&quot;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three rules ended up doing most of the work in the after column.&lt;/p&gt;
&lt;h4 id=&quot;1-one-unseen-medium-per-family-per-week&quot;&gt;1. One unseen medium per family per week&lt;/h4&gt;
&lt;p&gt;Title hidden.&lt;/p&gt;
&lt;p&gt;Timer running.&lt;/p&gt;
&lt;p&gt;Not as a substitute for the rest of practice.&lt;/p&gt;
&lt;p&gt;As the diagnostic that anchored what to practise next.&lt;/p&gt;
&lt;h4 id=&quot;2-foundational-material-on-the-family-s-invariant-before-more-volume&quot;&gt;2. Foundational material on the family's invariant before more volume&lt;/h4&gt;
&lt;p&gt;When a family failed the diagnostic, the next thing to read wasn't a new problem.&lt;/p&gt;
&lt;p&gt;It was the page on what made the family applicable.&lt;/p&gt;
&lt;p&gt;Volume came after.&lt;/p&gt;
&lt;p&gt;That distinction between solving a problem and recognising a family from constraints is exactly what eventually shaped how I built Codeintuition.&lt;/p&gt;
&lt;h4 id=&quot;3-don-t-retest-with-the-same-problem&quot;&gt;3. Don't retest with the same problem&lt;/h4&gt;
&lt;p&gt;A retest on a problem already seen, even one that was failed, measured recall instead of reasoning.&lt;/p&gt;
&lt;p&gt;The whole point of the diagnostic was novel problem performance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-the-failure-modes-told-me&quot;&gt;What the failure modes told me&lt;/h3&gt;
&lt;p&gt;Most engineers running a version of this diagnostic don't pass three for three on the first attempt.&lt;/p&gt;
&lt;p&gt;The first attempt for me wasn't clean either.&lt;/p&gt;
&lt;p&gt;The diagnoses to read off the result:&lt;/p&gt;
&lt;h4 id=&quot;one-family-failed&quot;&gt;One family failed&lt;/h4&gt;
&lt;p&gt;The pattern was understood at a surface level but the identification triggers and the construction skeleton hadn't been internalised.&lt;/p&gt;
&lt;p&gt;Fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;revisit foundational material&lt;/li&gt;
&lt;li&gt;retest with a different unseen problem&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;two-families-failed&quot;&gt;Two families failed&lt;/h4&gt;
&lt;p&gt;Usually:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;One strong area with over practised volume, shallow gaps everywhere else.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Common for engineers who spent months on one topic because the work felt productive.&lt;/p&gt;
&lt;p&gt;Fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;broaden coverage&lt;/li&gt;
&lt;li&gt;spend focused time on the families where the depth is thin&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;all-three-failed&quot;&gt;All three failed&lt;/h4&gt;
&lt;p&gt;Method gap, not talent gap.&lt;/p&gt;
&lt;p&gt;Too much:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;solving familiar problems&lt;/li&gt;
&lt;li&gt;repetition&lt;/li&gt;
&lt;li&gt;memorisation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Not enough:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;recognition&lt;/li&gt;
&lt;li&gt;transfer&lt;/li&gt;
&lt;li&gt;unseen problems&lt;/li&gt;
&lt;li&gt;interview shaped conditions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fix is narrower than:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Work harder.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-wish-someone-had-handed-me-earlier&quot;&gt;What I wish someone had handed me earlier&lt;/h3&gt;
&lt;p&gt;The difference between the score and the diagnostic was the difference between an opaque feeling and an action plan.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Maybe 70 percent ready&quot; was anxiety.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Two passed, the DP subsequence family failed because the invariant for non contiguous selections wasn't internalised yet&quot; was what to do on Wednesday.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If family coverage is the bottleneck rather than conditions, the algorithms learning path on Codeintuition is one place to fill the gap before retesting.&lt;/p&gt;
&lt;p&gt;The diagnostic itself doesn't depend on it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The point is the test, not the source of the problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I originally wrote a longer version on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/am-i-ready-for-faang&quot;&gt;my own blog&lt;/a&gt; that includes the per family signal breakdowns and diagnosis trees for the most common failure modes.&lt;/p&gt;
&lt;p&gt;What was the moment that made the readiness question itself feel like the wrong question, and what did you start measuring instead?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17163/how-to-know-if-youre-ready-for-faang-interviews</guid>
<pubDate>Sat, 09 May 2026 12:57:37 +0000</pubDate>
</item>
<item>
<title>How to Recognize Which Algorithm a Coding Interview Problem Needs</title>
<link>https://coderlegion.com/16965/how-to-recognize-which-algorithm-a-coding-interview-problem-needs</link>
<description>&lt;p&gt;I'd solved around 300 problems by the time my third senior screen wrapped up. The medium that had just bombed the call was something I could explain back to myself in 90 seconds once it ended. The constraints were familiar. The technique was one anyone with my problem count had implemented before. The thing that wouldn't fire, in the moment, was the connection between the constraints and the technique.&lt;/p&gt;
&lt;p&gt;The instinct, when that happens, is to grind more problems. That instinct cost me about two months. The thing that actually moved the needle was something else.&lt;/p&gt;
&lt;h3 id=&quot;what-i-figured-out-4&quot;&gt;What I figured out&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Volume practice was building memory of specific problems. It wasn't building the skill that recognises which technique applies to a problem you haven't seen before.&lt;/li&gt;
&lt;li&gt;The default LeetCode loop trains around recognition, not at it. Reading the tag before attempting skips the part of the work that interviews actually test.&lt;/li&gt;
&lt;li&gt;The fix is small and unglamorous. Read the problem for two or three observable features before any code. Name the technique. Then code.&lt;/li&gt;
&lt;li&gt;Conditions matter as much as content. The LeetCode UI is friendlier than any phone screen, and it takes conscious effort to practise under conditions that resemble the real thing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quick disclosure before I get into this: I built Codeintuition, a structured learning platform for coding interviews. The story above is the version of this realisation that keeps showing up across engineers prepping for senior screens, almost verbatim. The closing link goes to the longer version on my own blog.&lt;/p&gt;
&lt;h3 id=&quot;where-the-approach-was-breaking-3&quot;&gt;Where the approach was breaking&lt;/h3&gt;
&lt;p&gt;For most of the prep arc, every stuck problem got handled the same way. Attempt for fifteen or twenty minutes, hit a wall, glance at the tag, read a solution, move on. The progression chart said the weekly count was climbing. The interview chart said the bar wasn't moving.&lt;/p&gt;
&lt;p&gt;Tracing what was actually being learned per problem made the picture clearer. The implementation got learned each time. The shape of the solution got learned. What didn't get learned was the answer to a question nobody was asking: &lt;strong&gt;why is this problem the kind of problem that takes this technique?&lt;/strong&gt; The tag and the solution skipped that step every single time.&lt;/p&gt;
&lt;p&gt;In a real interview, the tag is gone. So the thing that came for free in practice (which technique to use) becomes the thing the candidate has to figure out from scratch under a clock. The freeze makes sense in retrospect. Nothing in the practice loop had trained for it.&lt;/p&gt;
&lt;h3 id=&quot;what-started-working&quot;&gt;What started working&lt;/h3&gt;
&lt;p&gt;The shift was small. Before opening the editor, the rule was: spend 30 to 60 seconds reading just the problem statement, trying to name the technique and the reasons. No code, no guessing. The output of the pass is a sentence: &quot;this is a [technique] problem because [feature one], [feature two], [feature three].&quot; If you can't write that sentence, you don't start coding. Go back to the constraints with a checklist of features for each technique and try again.&lt;/p&gt;
&lt;p&gt;Concretely:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;What used to happen &lt;/th&gt;&lt;th&gt;What started happening&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Open problem, attempt for 20 minutes, glance at tag if stuck &lt;/td&gt;&lt;td&gt;Read constraints first, name the technique in 60 seconds, only then attempt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Read solutions to learn the implementation &lt;/td&gt;&lt;td&gt;Read solutions only after attempting, and look for which features in the statement point at the technique&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practise one technique in batches (15 sliding window problems back-to-back) &lt;/td&gt;&lt;td&gt;Mix techniques in each session so the constraints had to be read before knowing what to use&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Practise with the title and difficulty visible &lt;/td&gt;&lt;td&gt;Cover the title with a sticky note, ignore the difficulty tag&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The first time, this protocol feels slow. The second week, it feels slow but useful. By the fourth week, the features start firing on their own as the problem statement is read.&lt;/p&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-5&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;The problem that finally landed for me was Longest Increasing Subsequence. The &lt;code&gt;O(n^2)&lt;/code&gt; version was familiar. The patience-sorting &lt;code&gt;O(n log n)&lt;/code&gt; version was familiar too. What wasn't trustworthy was the read of when LIS was the right tool versus when something else was.&lt;/p&gt;
&lt;p&gt;The session with the new approach started with the constraints. Three features fell out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A single sequence, not two. That ruled out edit-distance-family techniques right away.&lt;/li&gt;
&lt;li&gt;The optimisation target was the length of an ordered subsequence. Order of the original sequence had to be preserved.&lt;/li&gt;
&lt;li&gt;The transition between elements was a comparison, not a sum or a count. &quot;Strictly greater than the previous kept element&quot; is a binary check.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once those three features were written down, the technique was obvious. The simpler &lt;code&gt;O(n^2)&lt;/code&gt; version was enough because the problem didn't require the faster one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def lis(nums):
    if not nums:
        return 0
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] &amp;lt; nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation wasn't new. The change was that the technique got reached for because the features matched, not because the exact problem had been memorised.&lt;/p&gt;
&lt;h4 id=&quot;a-second-problem-the-same-week&quot;&gt;A second problem the same week&lt;/h4&gt;
&lt;p&gt;The same week brought Russian Doll Envelopes, which usually trips people up because it doesn't look like an LIS problem on the surface. The problem asks for the maximum number of nested envelopes given pairs of width and height.&lt;/p&gt;
&lt;p&gt;Reading for features: a single sequence (after sorting), a length to maximise, a strict comparison between consecutive kept elements (one envelope must fit inside the next). That's LIS, run on the heights after sorting by width with a tiebreak rule. The reframe took two minutes once the read was for features instead of surface details.&lt;/p&gt;
&lt;p&gt;That was the moment the recognition drill paid for itself. Two surface-different problems, same three features, same technique. Far transfer.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-science-to-dig-into-later-&quot;&gt;Why this works (the science to dig into later)&lt;/h3&gt;
&lt;p&gt;The learning sciences have language for this. &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Transfer_of_learning&quot;&gt;Transfer of learning&lt;/a&gt; splits into near transfer (a problem that resembles one already practised) and far transfer (a problem that doesn't resemble anything practised, but whose structure can be read).&lt;/p&gt;
&lt;p&gt;Volume practice mostly trains near transfer. Whether far transfer is reliably teachable is a debate the research hasn't fully settled. What the evidence does support is the specific intervention this protocol is built around. Explicit instruction in &lt;strong&gt;when&lt;/strong&gt; and &lt;strong&gt;why&lt;/strong&gt; a method applies, not just &lt;strong&gt;how&lt;/strong&gt; to execute it, produces more transfer than practice alone.&lt;/p&gt;
&lt;p&gt;In retrospect, the &quot;how&quot; was everywhere on the internet. The &quot;when&quot; and &quot;why&quot; were the parts no resource in the practice loop bothered to teach explicitly.&lt;/p&gt;
&lt;h3 id=&quot;what-to-try-if-you-re-in-the-same-spot&quot;&gt;What to try if you're in the same spot&lt;/h3&gt;
&lt;p&gt;If problem 200 feels harder than problem 50, the bottleneck probably isn't volume. It's a recognition skill that volume alone doesn't build for everyone. A few things worth trying:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;For each technique you've practised, write down two or three observable features that signal it applies. Sliding window: contiguous range, condition across the window, optimisation on length. Two pointers: sorted array, search for a pair or triple, pointers move from the ends. Monotonic stack: per element answer, directional search, comparison. Keep the checklists short.&lt;/li&gt;
&lt;li&gt;On every new problem, run the 60 second feature pass before any code. Write a sentence: &quot;this is a [technique] problem because [features].&quot; If you can't write the sentence, don't code yet.&lt;/li&gt;
&lt;li&gt;Mix techniques across sessions. Three problems from three different techniques will train recognition in a way that fifteen problems from one technique won't.&lt;/li&gt;
&lt;li&gt;Cover the title and the difficulty before reading the constraints. Many problem names give away the technique, and the assistance is exactly the kind of help that disappears in an interview.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is a practice protocol, not a product. It runs against any problem bank.&lt;/p&gt;
&lt;h3 id=&quot;where-it-s-still-hard&quot;&gt;Where it's still hard&lt;/h3&gt;
&lt;p&gt;The recognition drill doesn't fix everything. There's a separate axis prep tends to skip, which is the conditions the practice runs under. The default LeetCode loop has the tag, the difficulty, the discussion thread, and no clock. None of those are present in a real interview. Even with recognition trained, the first time the tag disappears the read gets harder. So the last few weeks of any prep cycle, run every practice session under interview-shaped conditions: clock running, tag covered, discussion section closed.&lt;/p&gt;
&lt;p&gt;That's the version of the lesson worth keeping. It wasn't the problem count. It was what was happening on each problem, and what nobody had ever trained on purpose.&lt;/p&gt;
&lt;p&gt;I originally posted this on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/algorithmic-intuition-dsa&quot;&gt;my own blog&lt;/a&gt;, with the feature checklists for six more techniques and a longer worked example for variable sliding window.&lt;/p&gt;
&lt;p&gt;What's a problem that finally made a technique click for you, where the explanations you'd read before hadn't quite landed?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16965/how-to-recognize-which-algorithm-a-coding-interview-problem-needs</guid>
<pubDate>Thu, 07 May 2026 12:47:24 +0000</pubDate>
</item>
<item>
<title>How to Explain Algorithm Correctness in Coding Interviews</title>
<link>https://coderlegion.com/16892/how-to-explain-algorithm-correctness-in-coding-interviews</link>
<description>&lt;p&gt;I'd written the BFS in about eight minutes. The interviewer ran the example I'd traced and said &quot;ok, but how do you know this is right for any input?&quot; My instinct was to point at the code. Pointing wasn't an argument. The silence stretched, and the realisation landed: nobody had ever taught me how to answer that question.&lt;/p&gt;
&lt;p&gt;The interview ended without an offer. The post-mortem felt strange because the algorithm &lt;em&gt;was&lt;/em&gt; correct, and there was still no way to say &lt;em&gt;why&lt;/em&gt; in a form the interviewer could check.&lt;/p&gt;
&lt;h4 id=&quot;what-changed-for-me&quot;&gt;What changed for me&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Tracing one input is testing, not proving. Most engineers, including a past version of me, don't notice the difference.&lt;/li&gt;
&lt;li&gt;Interviewers want a loop invariant, the property that's true at the start of every iteration.&lt;/li&gt;
&lt;li&gt;The full method is two checks: state and verify the invariant, then check initialization, termination, and edge cases.&lt;/li&gt;
&lt;li&gt;Different patterns produce differently shaped invariants, but the two step structure stays constant.&lt;/li&gt;
&lt;li&gt;The skill is separate from problem solving. It needs its own practice before it becomes reflexive.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quick disclosure before I get into this: I built Codeintuition, a structured learning platform for coding interviews. This post is the retrospective version of the lesson that BFS interview should have taught me years earlier.&lt;/p&gt;
&lt;h3 id=&quot;where-my-approach-was-breaking-3&quot;&gt;Where my approach was breaking&lt;/h3&gt;
&lt;p&gt;The default habit looks reasonable from the outside. You write the algorithm. You walk through it on the example the interviewer suggested. You get the right answer. You stop. If they ask a follow up, you trace a slightly harder example. If they push harder, you start guessing about edge cases.&lt;/p&gt;
&lt;p&gt;None of that is an argument. It's a sequence of tests. Each test confirms one input. The interviewer's question was about every input.&lt;/p&gt;
&lt;p&gt;The gap is small in description and large in practice. A trace shows the algorithm worked once. A correctness argument has to give the interviewer a reason it works always. That bridge is where the interview lives, and most prep material skips it.&lt;/p&gt;
&lt;p&gt;The discrete math course you took in college covered induction proofs with formal notation. It's easy to dismiss as academic and move on. What that misses is that the &lt;em&gt;idea&lt;/em&gt; (a property that holds every iteration) is exactly what the interview wants. The interview just asks for it as 90 seconds of clear English instead of a paper proof.&lt;/p&gt;
&lt;h3 id=&quot;what-i-started-doing-differently-4&quot;&gt;What I started doing differently&lt;/h3&gt;
&lt;p&gt;The first habit was simple. Before tracing any input, write down what's supposed to be true at the start of each iteration. One sentence.&lt;/p&gt;
&lt;p&gt;The hard part is that for a few weeks, the sentence wouldn't always come. That alone is the diagnostic. If you can't state the invariant, you solved the problem by pattern matching, not by understanding what made the algorithm work.&lt;/p&gt;
&lt;p&gt;The second habit, after stating the invariant, is to check the boundaries separately. What happens before the loop runs. What happens when the loop exits. What happens on an empty input. What happens on a single element. What happens on the maximum size.&lt;/p&gt;
&lt;p&gt;The two step method is short. The change in interview outcomes is not.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Step &lt;/th&gt;&lt;th&gt;What I used to do &lt;/th&gt;&lt;th&gt;What works&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Before tracing &lt;/td&gt;&lt;td&gt;Pick an example and run it &lt;/td&gt;&lt;td&gt;State the invariant in one sentence, then trace 3 to 4 iterations&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;At the loop end &lt;/td&gt;&lt;td&gt;Say &quot;and that gives the right answer&quot; &lt;/td&gt;&lt;td&gt;Confirm the invariant plus the loop's exit condition together force the right output&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;On boundaries &lt;/td&gt;&lt;td&gt;Mention &quot;edge cases&quot; if asked &lt;/td&gt;&lt;td&gt;List initialization, termination, and at least three degenerate inputs explicitly&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-that-looked-like-on-the-next-problem-6&quot;&gt;What that looked like on the next problem&lt;/h3&gt;
&lt;p&gt;A few months later, the same kind of BFS question came up on a different graph problem. This time the invariant came first.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from collections import deque

def shortest_path(graph, start):
    distances = {start: 0}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for neighbor in graph[node]:
            if neighbor not in distances:
                distances[neighbor] = distances[node] + 1
                queue.append(neighbor)
    return distances
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The argument went something like this: the invariant is that when a node is dequeued, its recorded distance equals the shortest path from the start. The reason it holds is FIFO ordering on a graph with equal edge weights. Any node enqueued later has been reached by a path of the same length or longer, so the first time you see a node is via the shortest path to it.&lt;/p&gt;
&lt;p&gt;Then the boundaries. Initialization: only the start node sits in &lt;code&gt;distances&lt;/code&gt; with value 0, which is the shortest path from the start to itself. Termination: the loop exits when the queue is empty, meaning every reachable node has been dequeued with its correct distance. Edge cases: a graph with one node returns &lt;code&gt;{start: 0}&lt;/code&gt; immediately. A disconnected graph returns distances only for nodes reachable from the start. A self-loop is fine because the check &lt;code&gt;if neighbor not in distances&lt;/code&gt; runs before enqueueing.&lt;/p&gt;
&lt;p&gt;The interviewer didn't follow up with &quot;but how do you know it's right for any input?&quot; The argument had already covered that.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-the-bit-worth-digging-into-later-&quot;&gt;Why this works (the bit worth digging into later)&lt;/h3&gt;
&lt;p&gt;The two step method generalises because the structure is the same across every algorithm class. Only the &lt;em&gt;shape&lt;/em&gt; of the invariant changes. Sliding window tracks a running variable. Two pointers tracks a search range. Binary search tracks a range too, but a different one. BFS tracks a relationship between dequeue order and shortest path. DP tracks an entire table of subproblem solutions.&lt;/p&gt;
&lt;p&gt;The verification framework is invariant plus boundaries, in that order, regardless of pattern.&lt;/p&gt;
&lt;p&gt;This connects to a broader idea in &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Loop_invariant&quot;&gt;computer science formalism&lt;/a&gt;: the loop invariant is a logical statement that, combined with the loop's exit condition, gives you the postcondition you wanted to prove. The interview version drops the formal symbols and keeps the structure. Same skeleton, different vocabulary.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-tell-someone-in-the-same-spot-4&quot;&gt;What I'd tell someone in the same spot&lt;/h3&gt;
&lt;p&gt;Three concrete things matter most:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick a problem you've solved. Set a timer for 60 seconds. Try to state the loop invariant in one sentence. If the sentence doesn't come, that's the most important diagnostic in your prep, and more problems won't fix it.&lt;/li&gt;
&lt;li&gt;Take a correct algorithm. Write down an invariant that &lt;em&gt;sounds&lt;/em&gt; plausible but is subtly wrong. Find the iteration where it breaks. This trains the gap between invariants that describe what the code does and invariants that guarantee correctness.&lt;/li&gt;
&lt;li&gt;Pair correctness reasoning with mental dry running. The dry run gives you a trace. The invariant gives you the claim to verify against the trace. Practising them apart leaves a hole that interview pressure exposes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you want a worked walkthrough of the BFS distance invariant before solving a graph problem with it, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/courses/array/wAvrphLQKKOxZazp-fg6X&quot;&gt;the BFS lesson&lt;/a&gt; walks through why FIFO ordering plus equal edge weights forces the shortest path property to hold.&lt;/p&gt;
&lt;p&gt;I originally posted this on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/algorithm-correctness-proof&quot;&gt;my own blog&lt;/a&gt;, a slightly longer version that walks through the two step method on sliding window, binary search, and DP, with an FAQ on nested loops where each loop needs its own invariant.&lt;/p&gt;
&lt;p&gt;What's a problem where you understood the algorithm but couldn't say &lt;em&gt;why&lt;/em&gt; it was right when someone asked, and what was the missing sentence you wished you'd had?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16892/how-to-explain-algorithm-correctness-in-coding-interviews</guid>
<pubDate>Wed, 06 May 2026 19:47:14 +0000</pubDate>
</item>
<item>
<title>I Solved 200 LeetCode Problems and Still Froze in Interviews</title>
<link>https://coderlegion.com/16624/i-solved-200-leetcode-problems-and-still-froze-in-interviews</link>
<description>&lt;p&gt;A few years ago I solved 200 LeetCode problems and still froze on Mediums I hadn't seen. The breakthrough wasn't another hundred problems. It was a different loop.&lt;/p&gt;
&lt;p&gt;A problem asks for the longest substring with at most K distinct characters. You've solved sliding window before. Maximum sum subarray of size K, done. Longest substring without repeating characters, done. This third one stalls you. Twenty minutes pass. Discuss says sliding window. You'd already solved sliding window problems. The recognition didn't work on this one.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Practice volume builds memory of specific problems, not the recognition you have to do before you start coding. The skill that transfers to unfamiliar interview mediums is identifying which technique applies from what the problem looks like. That recognition is what most LeetCode-style prep doesn't train, and that gap is what this post is about.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;what-leetcode-actually-does-well&quot;&gt;What LeetCode actually does well&lt;/h3&gt;
&lt;p&gt;LeetCode is the default coding interview prep platform for a reason. The problem bank is the strongest available: 3,000+ problems across every common data structure, tagged by company and frequency. The company tagging is the most reliable public signal you can get for &quot;what does Amazon currently ask.&quot; Nothing else is close on that dimension.&lt;/p&gt;
&lt;p&gt;The free tier is generous, the discuss forum is open, and the weekly contests are free. If you already understand the underlying patterns and you need volume to drill them, LeetCode is the right platform. The contest system is genuinely underrated for building speed under time. Top voted discuss replies on popular problems also hold up. They often explain the reasoning more clearly than the official editorials.&lt;/p&gt;
&lt;p&gt;So the question isn't whether LeetCode is good. It is good. The real question is whether the thing holding you back is the kind of thing LeetCode's design actually fixes.&lt;/p&gt;
&lt;h3 id=&quot;the-gap-volume-doesn-t-close&quot;&gt;The gap volume doesn't close&lt;/h3&gt;
&lt;p&gt;The earlier sliding window problems taught implementation on those specific problems, not the rule for when sliding window applies. The connection between &quot;K distinct characters&quot; and &quot;variable sliding window&quot; was never made explicit anywhere in your prep. You learned an implementation by example, not a recognition rule. When the visible parts of the problem change, the implementation memory doesn't carry over.&lt;/p&gt;
&lt;p&gt;Learning research calls this near transfer versus far transfer. Near transfer means solving problems that look like ones you've already solved. Far transfer means reasoning through problems that look different but follow the same underlying pattern. The Wikipedia article on &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Transfer_of_learning&quot;&gt;transfer of learning&lt;/a&gt; covers it. The short version: practice volume produces near transfer reliably and far transfer unreliably. You can grind 500 problems and still not generalise across the differences in description if no one ever taught you which features signal which pattern.&lt;/p&gt;
&lt;p&gt;That isn't a talent gap. It's a gap in the method.&lt;/p&gt;
&lt;h3 id=&quot;what-recognition-actually-looks-like&quot;&gt;What recognition actually looks like&lt;/h3&gt;
&lt;p&gt;Recognition is reading a problem statement, naming the visible features, and matching them to a technique before any code is written. Every technique has a small set of trigger conditions, usually three to five. When all of them match, you've identified the technique.&lt;/p&gt;
&lt;p&gt;For variable sliding window, the trigger conditions are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The input is a contiguous range (a substring, a subarray, a window of consecutive elements).&lt;/li&gt;
&lt;li&gt;The optimisation target is the length of that range (longest, shortest, smallest valid).&lt;/li&gt;
&lt;li&gt;There is a condition you can check incrementally as the window expands or contracts (count of distinct characters, sum within bounds, set membership).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When all three apply, variable sliding window is the technique. K distinct characters hits all three. So does longest substring without repeating characters. So does minimum window substring. The triggers are the same. The descriptions differ.&lt;/p&gt;
&lt;p&gt;The skill is matching problems to triggers before coding. That's what doesn't get built by grinding problems sequentially.&lt;/p&gt;
&lt;h3 id=&quot;a-code-template-you-can-adapt&quot;&gt;A code template you can adapt&lt;/h3&gt;
&lt;p&gt;Here's the variable sliding window template in Python. Once you recognise the technique, the implementation has very few moving parts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def variable_window(arr, condition):
    left = 0
    state = init_state()
    best = 0

    for right in range(len(arr)):
        state = expand(state, arr[right])
        while not condition(state):
            state = shrink(state, arr[left])
            left += 1
        best = max(best, right - left + 1)

    return best
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For K distinct characters, &lt;code&gt;state&lt;/code&gt; is a hash map of character counts within the current window, &lt;code&gt;condition&lt;/code&gt; is &lt;code&gt;len(state) &amp;lt;= K&lt;/code&gt;, and &lt;code&gt;shrink&lt;/code&gt; decrements counts and removes keys whose count drops to zero. The recognition step (this is variable sliding window) is the decision that matters. The code follows.&lt;/p&gt;
&lt;p&gt;For longest substring without repeating characters, &lt;code&gt;state&lt;/code&gt; becomes a hash set and &lt;code&gt;condition&lt;/code&gt; is &quot;no duplicate.&quot; Same skeleton. Same invariant.&lt;/p&gt;
&lt;p&gt;That's the payoff of recognition trained explicitly. Write the template once. Adapt per problem in two or three lines. Stop staring at problems hoping the right approach surfaces.&lt;/p&gt;
&lt;h3 id=&quot;how-practice-and-interview-conditions-diverge&quot;&gt;How practice and interview conditions diverge&lt;/h3&gt;
&lt;p&gt;Recognition is half the gap. The other half is the conditions you practise under. LeetCode's practice environment shows you the problem name (which often hints at the technique), gives unlimited code executions, and doesn't penalise failed attempts. Real interviews give none of that.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Dimension &lt;/th&gt;&lt;th&gt;Practice (LeetCode default) &lt;/th&gt;&lt;th&gt;Real interview&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Problem name &lt;/td&gt;&lt;td&gt;Visible, often hints at category &lt;/td&gt;&lt;td&gt;Not given&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Code executions &lt;/td&gt;&lt;td&gt;Unlimited, no penalty &lt;/td&gt;&lt;td&gt;Limited, every failure costs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Hints / discuss &lt;/td&gt;&lt;td&gt;One click away &lt;/td&gt;&lt;td&gt;Not available&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Time limit &lt;/td&gt;&lt;td&gt;Self set, often skipped &lt;/td&gt;&lt;td&gt;20 to 45 minutes hard&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Reasoning &lt;/td&gt;&lt;td&gt;Internal &lt;/td&gt;&lt;td&gt;Spoken aloud&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The middle column is fine for the early learning phase. Open exploration helps when you're still building the model. The problem comes when those conditions are the only conditions you ever rehearse under, and the actual test wants something the rehearsal never asked for.&lt;/p&gt;
&lt;p&gt;Replicating interview pressure on your own takes one friend and a kitchen timer. The friend covers the problem name and reads the constraints aloud. You set the timer to 20 or 30 minutes. You don't open the IDE before stating the technique you intend to use. Two sessions a week against a friend who isn't shy about silence will do more for interview readiness than another fifty problems on the comfort setting.&lt;/p&gt;
&lt;h3 id=&quot;a-protocol-you-can-run-this-week&quot;&gt;A protocol you can run this week&lt;/h3&gt;
&lt;p&gt;If the K distinct characters story sounded like your last three failed problems, the move isn't more volume. It's a tighter loop on recognition first.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick one technique a week (variable sliding window, two pointers, prefix sum, monotonic stack, and so on).&lt;/li&gt;
&lt;li&gt;Write down the trigger conditions for that technique. Three to five features of the problem that, when they all apply, make this technique the right tool. Write them in your own words, not from a cheat sheet.&lt;/li&gt;
&lt;li&gt;Read five problems that use this technique without solving them. For each, name the triggers you can see in the problem statement before reading the constraints in detail.&lt;/li&gt;
&lt;li&gt;Solve three to five problems on this technique with the problem name and tag hidden. Force the recognition step.&lt;/li&gt;
&lt;li&gt;Once a week, do one timed pressure session: friend covers the title, you talk through your reasoning, no IDE until you've named the technique. If you can't name it inside five minutes, the problem rewinds to the recognition queue, not the implementation queue.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A pattern a week, eight to ten patterns covered, roughly ten weeks of focused work. That replaces the hundreds of mediums where the signal gets buried under details that don't matter.&lt;/p&gt;
&lt;p&gt;The longer write up of this is on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeintuition.io/blogs/codeintuition-vs-leetcode&quot;&gt;my own blog&lt;/a&gt;, with trigger conditions for prefix sum, two pointers, and monotonic stack alongside the variable sliding window walkthrough above, plus a worked example on minimum window substring that uses the same template.&lt;/p&gt;
&lt;p&gt;When did the recognition click for a pattern you used to grind without seeing, and what was the specific problem that broke it open?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Disclosure: I built Codeintuition, a structured learning platform for coding interviews. The post above is about the technique, not the platform.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16624/i-solved-200-leetcode-problems-and-still-froze-in-interviews</guid>
<pubDate>Mon, 04 May 2026 10:59:45 +0000</pubDate>
</item>
<item>
<title>LeetCode Milestone!!!</title>
<link>https://coderlegion.com/14519/leetcode-milestone</link>
<description>&lt;p&gt;1400+ leetcode solved!!!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14519/leetcode-milestone</guid>
<pubDate>Sat, 11 Apr 2026 17:07:12 +0000</pubDate>
</item>
<item>
<title>960. Delete Columns to Make Sorted III</title>
<link>https://coderlegion.com/13840/960-delete-columns-to-make-sorted-iii</link>
<description>&lt;p&gt;&lt;em&gt;In the last post, I discussed how recognizing patterns is important in solving coding challenges and discussed the Longest Increasing Subsequence. I shall talk about applying the basics in this problem to solve one variation of LIS. Follow me here, on Twitter, LinkedIn or Github for more.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;br&gt;
You are given an array of n strings strs, all of the same length.&lt;/p&gt;
&lt;p&gt;We may choose any deletion indices, and we delete all the characters in those indices for each string.&lt;/p&gt;
&lt;p&gt;For example, if we have strs = [&quot;abcdef&quot;,&quot;uvwxyz&quot;] and deletion indices {0, 2, 3}, then the final array after deletions is [&quot;bef&quot;, &quot;vyz&quot;].&lt;/p&gt;
&lt;p&gt;Suppose we chose a set of deletion indices answer such that after deletions, the final array has every string (row) in lexicographic order. (i.e., (strs[0][0] &amp;lt;= strs[0][1] &amp;lt;= ... &amp;lt;= strs[0][strs[0].length - 1]), and (strs[1][0] &amp;lt;= strs[1][1] &amp;lt;= ... &amp;lt;= strs[1][strs[1].length - 1]), and so on). Return the minimum possible value of answer.length.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: strs = [&quot;babca&quot;,&quot;bbazb&quot;]&lt;br&gt;
Output: 3&lt;br&gt;
Explanation: After deleting columns 0, 1, and 4, the final array is strs = [&quot;bc&quot;, &quot;az&quot;].&lt;br&gt;
Both these rows are individually in lexicographic order (ie. strs[0][0] &amp;lt;= strs[0][1] and strs[1][0] &amp;lt;= strs[1][1]).&lt;br&gt;
Note that strs[0] &amp;gt; strs[1] - the array strs is not necessarily in lexicographic order.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: strs = [&quot;edcba&quot;]&lt;br&gt;
Output: 4&lt;br&gt;
Explanation: If we delete less than 4 columns, the only row will not be lexicographically sorted.&lt;br&gt;
Example 3:&lt;/p&gt;
&lt;p&gt;Input: strs = [&quot;ghi&quot;,&quot;def&quot;,&quot;abc&quot;]&lt;br&gt;
Output: 0&lt;br&gt;
Explanation: All rows are already lexicographically sorted.&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;n == strs.length&lt;br&gt;
1 &amp;lt;= n &amp;lt;= 100&lt;br&gt;
1 &amp;lt;= strs[i].length &amp;lt;= 100&lt;br&gt;
strs[i] consists of lowercase English letters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We apply the same principle in LIS with the double for loops.The only difference is we check all the characters in one column are less than or equal to all the characters in the other. We do this as we are trying to maximize the number of columns to be deleted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int minDeletionSize(String[] strs) {
     int length=strs[0].length();      
     int [] dp=new  int[length];          
     Arrays.fill(dp,1);
     for(int col=1;col&amp;lt;length;col++){       
      for(int j=0;j&amp;lt;col;j++){
        int numOfRows=0; 
        for(int i=0;i&amp;lt;strs.length;i++){
         char curr=strs[i].charAt(col);   
         char prev=strs[i].charAt(j);
         if(prev&amp;gt;curr) break;
         else numOfRows++;  
         }
         if(numOfRows==strs.length &amp;amp;&amp;amp; dp[col]&amp;lt;=dp[j])dp[col]=dp[j]+1;          
        }
      }  
     Arrays.sort(dp);
     return length-dp[dp.length-1]; 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C#&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Solution {
    public int MinDeletionSize(string[] strs) {
      int length=strs[0].Length;      
     int [] dp=new  int[length];          
     Array.Fill(dp,1);
     for(int col=1;col&amp;lt;length;col++){       
      for(int j=0;j&amp;lt;col;j++){
        int numOfRows=0; 
        for(int i=0;i&amp;lt;strs.Length;i++){
         char curr=strs[i][col];   
         char prev=strs[i][j];
         if(prev&amp;gt;curr) break;
         else numOfRows++;  
         }
         if(numOfRows==strs.Length &amp;amp;&amp;amp; dp[col]&amp;lt;=dp[j])dp[col]=dp[j]+1;          
        }
      }  
     Array.Sort(dp);
     return length-dp[dp.Length-1];    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minDeletionSize(vector&amp;lt;string&amp;gt;&amp;amp; strs) {
     string first=strs[0];
     int length=first.size();      
     vector&amp;lt;int&amp;gt; dp;          
     for(int i=0;i&amp;lt;length;i++)dp.push_back(1);
     for(int col=1;col&amp;lt;length;col++){       
      for(int j=0;j&amp;lt;col;j++){
        int numOfRows=0; 
        for(int i=0;i&amp;lt;strs.size();i++){
         string str= strs[i];  
         char curr=str[col];   
         char prev=str[j];
         if(prev&amp;gt;curr) break;
         else numOfRows++;  
         }
         if(numOfRows==strs.size() &amp;amp;&amp;amp; dp[col]&amp;lt;=dp[j])dp[col]=dp[j]+1;          
        }
      }  
     std::sort(dp.begin(),dp.end());
     return length-dp[dp.size()-1];    
    }
};
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13840/960-delete-columns-to-make-sorted-iii</guid>
<pubDate>Wed, 01 Apr 2026 21:03:23 +0000</pubDate>
</item>
<item>
<title>300. Longest Increasing Subsequence</title>
<link>https://coderlegion.com/13148/300-longest-increasing-subsequence</link>
<description>&lt;p&gt;&lt;em&gt;A good way to master coding challenges is to recognize patterns. A good way to recognize patterns is to understand the basics. This may involve understanding one core problem and learning to apply it. Think of it as similar to learning to crawl before learning to walk and then learning to run. An example core problem is the Longest Increasing Subsequence. Variations of this problem are frequently asked. Follow me here, on Twitter, LinkedIn or Github for more.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Given an integer array nums, return the length of the longest strictly increasing subsequence.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: nums = [10,9,2,5,3,7,101,18]&lt;br&gt;
Output: 4&lt;br&gt;
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: nums = [0,1,0,3,2,3]&lt;br&gt;
Output: 4&lt;br&gt;
Example 3:&lt;/p&gt;
&lt;p&gt;Input: nums = [7,7,7,7,7,7,7]&lt;br&gt;
Output: 1&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= nums.length &amp;lt;= 2500&lt;br&gt;
-104 &amp;lt;= nums[i] &amp;lt;= 104&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To solve, we use dynamic programming. We build a 1-dimensional array lengths to store the maximum length of the subsequences at each index and initialize it so that each value is 1.&lt;br&gt;
Then we use two for loops. The first starts at the second index (index 1 in some programming languages) and iterates forward until the end of the array. We shall refer to it as i .&lt;br&gt;
The second starts at the first index (index 0 in some languages) and iterates forward until it reaches i. We shall refer to it as j. If the number found at j is less than that found at i and lengths[i] is less than or equal to lengths[j], we recalculate the value at lengths[i] as lengths[i]=lengths[j]+1.&lt;br&gt;
On finishing the double for loop,we sort lengths and return the last value which is the largest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Java&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int lengthOfLIS(int[] nums) {
     int [] lengths= new int[nums.length];
     for(int i=0;i&amp;lt;lengths.length;i++)lengths[i]=1;
     for(int i=1;i&amp;lt;nums.length;i++){
      for(int j=0;j&amp;lt;i;j++){
       if(nums[j]&amp;lt;nums[i] &amp;amp;&amp;amp; lengths[i]&amp;lt;=lengths[j]) lengths[i]=lengths[j]+1;
      }
     }
     Arrays.sort(lengths);
     return lengths[lengths.length-1];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C#&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Solution {
    public int LengthOfLIS(int[] nums) {
     int [] lengths= new int[nums.Length];
     int max=0;
     for(int i=0;i&amp;lt;lengths.Length;i++)lengths[i]=1;
     for(int i=1;i&amp;lt;nums.Length;i++){
      for(int j=0;j&amp;lt;i;j++){
       if(nums[j]&amp;lt;nums[i] &amp;amp;&amp;amp; lengths[i]&amp;lt;=lengths[j]) lengths[i]=lengths[j]+1;
      }
     }
     for(int i=0;i&amp;lt;lengths.Length;i++)max=Math.Max(max,lengths[i]);
     return max;   
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int lengthOfLIS(vector&amp;lt;int&amp;gt;&amp;amp; nums) {
     vector&amp;lt;int&amp;gt;lengths;
     int max=0;
     for(int i=0;i&amp;lt;nums.size();i++)lengths.push_back(1);
     for(int i=1;i&amp;lt;nums.size();i++){
      for(int j=0;j&amp;lt;i;j++){
       if(nums.at(j)&amp;lt;nums.at(i) &amp;amp;&amp;amp; lengths.at(i)&amp;lt;=lengths.at(j)) lengths.at(i)=lengths.at(j)+1;
      }
     }
     for(int i=0;i&amp;lt;lengths.size();i++)max=std::max(max,lengths.at(i)); 
     return max;   
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Javascript&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var lengthOfLIS = function(nums) {
 const lengths= [];
 let max=0;
 for(let i=0;i&amp;lt;nums.length;i++)lengths.push(1);
 for(let i=1;i&amp;lt;nums.length;i++){
  for(let j=0;j&amp;lt;i;j++){
   if(nums[j]&amp;lt;nums[i] &amp;amp;&amp;amp; lengths[i]&amp;lt;=lengths[j]) lengths[i]=lengths[j]+1;
  }
 }
 for(let i=0;i&amp;lt;lengths.length;i++)max=Math.max(lengths[i],max);
 return max;
};
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13148/300-longest-increasing-subsequence</guid>
<pubDate>Wed, 18 Mar 2026 18:26:42 +0000</pubDate>
</item>
<item>
<title>LeetCode #1458.Max Dot Product of Two Subsequences</title>
<link>https://coderlegion.com/11062/leetcode-1458-max-dot-product-of-two-subsequences</link>
<description>&lt;p&gt;&lt;em&gt;Many engineers find leetcode problems hard and not a reflection of the actual work to do. I do have some sympathy for this view. Nonetheless, I do think that these questions can help practice and build programming skills. Due to this and the fact that these prop up in interviews, I will post discussions of these from time to time.Follow me on twitter,github or Linkedin for these and more.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Given two arrays nums1 and nums2.&lt;/p&gt;
&lt;p&gt;Return the maximum dot product between non-empty subsequences of nums1 and nums2 with the same length.&lt;/p&gt;
&lt;p&gt;A subsequence of an array is a new array which is formed from the original array by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, [2,3,5] is a subsequence of [1,2,3,4,5] while [1,5,3] is not).&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: nums1 = [2,1,-2,5], nums2 = [3,0,-6]&lt;br&gt;
Output: 18&lt;br&gt;
Explanation: Take subsequence [2,-2] from nums1 and subsequence [3,-6] from nums2.&lt;br&gt;
Their dot product is (2&lt;em&gt;3 + (-2)&lt;/em&gt;(-6)) = 18.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: nums1 = [3,-2], nums2 = [2,-6,7]&lt;br&gt;
Output: 21&lt;br&gt;
Explanation: Take subsequence [3] from nums1 and subsequence [7] from nums2.&lt;br&gt;
Their dot product is (3*7) = 21.&lt;br&gt;
Example 3:&lt;/p&gt;
&lt;p&gt;Input: nums1 = [-1,-1], nums2 = [1,1]&lt;br&gt;
Output: -1&lt;br&gt;
Explanation: Take subsequence [-1] from nums1 and subsequence [1] from nums2.&lt;br&gt;
Their dot product is -1.&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= nums1.length, nums2.length &amp;lt;= 500&lt;br&gt;
-1000 &amp;lt;= nums1[i], nums2[i] &amp;lt;= 1000&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discussion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I was stumped by this question. I read the hints and somebody mentioned that it was similar to Longest Common Subsequence. That got me thinking. I had to use a 2-D dp array to get the result.&lt;/p&gt;
&lt;p&gt;I also had to keep track of the multiplication of each number from each array along with the values from the previous row,current column and the previous column and current row.For edge cases I had to calculate the values for the first row and the first column.I came up with the solution below which passed all 70 cases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
     int  [] [] dp=new int[nums1.length] [nums2.length];   
     int firstRowMax=nums1[0]*nums2[0];
     int answer=0;
     dp[0][0]=firstRowMax;
     for(int j=1;j&amp;lt;nums2.length;j++){
      int current=nums1[0]*nums2[j];
      firstRowMax=Math.max(firstRowMax,current);
      dp[0][j]=firstRowMax;
     }
     answer=firstRowMax;
     for(int i=1;i&amp;lt;nums1.length;i++){
      for(int j=0;j&amp;lt;nums2.length;j++){
        int current=nums1[i]*nums2[j];
        if(j==0) dp[i][j]=Math.max(dp[i-1][j],current);   
        else
         dp[i][j]=Math.max(dp[i-1][j-1]+current,Math.max(current,Math.max(dp[i-1][j],dp[i][j-1])));
        answer=Math.max(answer,dp[i][j]);
      }  
     }
     return answer;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11062/leetcode-1458-max-dot-product-of-two-subsequences</guid>
<pubDate>Wed, 04 Feb 2026 17:37:05 +0000</pubDate>
</item>
<item>
<title>From Struggle to Solution-An Approach to Solving a programming problem using #650.Leetcode's 2 keys</title>
<link>https://coderlegion.com/3891/from-struggle-solution-approach-solving-programming-problem-using-650-leetcodes-keys</link>
<description>&lt;p&gt;&lt;strong&gt;1.Introduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; I regularly practice leetcode if not always on a daily basis. The reasons that I do this are: to keep my coding skills sharp, to deepen my understanding of algorithms and data structures and to prepare for coding challenges and technical interviews. Recently, I solved Leetcode's #650-the 2 keys keyboard using dynamic programming and I have decided to share my approach to solving this problem.Below is a description of the problem.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;2 Keys Keyboard&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is only one character 'A' on the screen of a notepad. You can perform one of two operations on this notepad for each step:&lt;/p&gt;
&lt;p&gt;Copy All: You can copy all the characters present on the screen (a partial copy is not allowed).&lt;br&gt;
Paste: You can paste the characters which are copied last time.&lt;br&gt;
Given an integer n, return the minimum number of operations to get the character 'A' exactly n times on the screen.&lt;/p&gt;
&lt;p&gt;Example 1:&lt;/p&gt;
&lt;p&gt;Input: n = 3&lt;br&gt;
Output: 3&lt;br&gt;
Explanation: Initially, we have one character 'A'.&lt;br&gt;
In step 1, we use Copy All operation.&lt;br&gt;
In step 2, we use Paste operation to get 'AA'.&lt;br&gt;
In step 3, we use Paste operation to get 'AAA'.&lt;br&gt;
Example 2:&lt;/p&gt;
&lt;p&gt;Input: n = 1&lt;br&gt;
Output: 0&lt;/p&gt;
&lt;p&gt;Constraints:&lt;/p&gt;
&lt;p&gt;1 &amp;lt;= n &amp;lt;= 1000&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.First Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; I read the problem and the topics it was listed under. It was listed as Math and Dynamic Programming. My understanding was that there were 2 types of operations: a copy and paste and a paste. I was at first baffled and confused about where to start. I mean how could I minimize the number of operations to achieve this task? The fact that it was listed as dynamic programming gave me an idea-use a 1-d array to store the results of subproblems but I was still baffled.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.Attempts &amp;amp; Failures&lt;/strong&gt;&lt;br&gt;
  I read the hints. Only 1 was provided. It said &quot;How many characters may be there in the clipboard at the last step if n = 3? n = 7? n = 10? n = 24?&quot;. This confused me more. I decided to try something. I could not come up with anything so I decided to reread the problem. On doing so, I realized my first error. &lt;em&gt;2 types of operations were allowed: a copy all and a paste&lt;/em&gt;.This helped. I did some thinking and realized that for a final string of 6,I could do a copy and paste of 3 characters and ditto for a string of 4 characters. It was progress but unfortunately not enough. I decided to read the discussion. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2 observations.
If n is a prime=&amp;gt; ans=n
If n=2^k=&amp;gt; ans=2*k
Something to do with prime factorization, maybe Sieve method can play again, not very necessary.
Have a nice day.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was helpful but still I struggled.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. The Breakthrough&lt;/strong&gt;&lt;br&gt;
 I decided to give it a rest and to come back to it later. I was eating lunch and my mind thought about it. I had a realization!!! If the final string was 1000 characters, I could minimize by performing a copy and paste on a string of 500 problems. If it was prime, I could perform a copy and paste of 1 character n times. If it was a string of 27 characters, I could copy a string of 9 characters and paste it twice. So I found a solution. Create a 1-d array of length n+1.Iterate from the index 2 to the end of the array,if the current index is prime, record its number.Otherwise,get its greatest factor,go to that position in the array and calculate the current value by adding the dp value of the highest factor and the quotient of the current value/highest factor.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(isPrime(i) dp[i]=i;
else dp[i]=dp[highestFactor(i)]+(i/highestFactor(i)).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; With this in mind, I attempted a solution. I attempted to do this in Java. Here was my first solution.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int minSteps(int n) {
      int []dp= new int[n+1];
      for(int i=2;i&amp;lt;n+1;i++){
       if(isPrime(i))dp[i]=i;
       else {
        //System.out.println
        dp[i] =dp[highestDivisor(i)]+(i/highestDivisor(i));
       } 
      }  
      return dp[n];
    }

    public int highestDivisor(int n) {
        if (n &amp;lt;= 1) return -1; // No proper divisor exists for 0 or 1

        for (int i = n / 2; i &amp;gt;= 1; i--) {
            if (n % i == 0) {
                return i; // First divisor found from the top is the highest
            }
        }
        return -1; // Should never reach here for n &amp;gt; 1
    }

    public  boolean isPrime(int n) {
        if (n &amp;lt;= 1) return false; // 0 and 1 are not prime
        if (n &amp;lt;= 3) return true;  // 2 and 3 are prime
        if (n % 2 == 0 || n % 3 == 0) return false; // eliminate multiples of 2 and 3

        // Only check up to √n using 6k ± 1 optimization
        for (int i = 5; i * i &amp;lt;= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0)
                return false;
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; As part of my recent decision to try and get solutions in at least 4 languages (Java, c# ,c++ and javascript),I attempted to achieve solutions in c#,c++ and javascript. I first tried c# and the following solution was accepted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Solution {
    public int MinSteps(int n) {
     int []dp= new int[n+1];
      for(int i=2;i&amp;lt;n+1;i++){
       if(IsPrime(i))dp[i]=i;
       else dp[i] =dp[HighestDivisor(i)]+(i/HighestDivisor(i));        
      }  
      return dp[n];
    }

    public int HighestDivisor(int n) {
        if (n &amp;lt;= 1) return -1; // No proper divisor exists for 0 or 1

        for (int i = n / 2; i &amp;gt;= 1; i--) {
            if (n % i == 0) {
                return i; // First divisor found from the top is the highest
            }
        }
        return -1; // Should never reach here for n &amp;gt; 1
    }

    public  bool IsPrime(int n) {
        if (n &amp;lt;= 1) return false; // 0 and 1 are not prime
        if (n &amp;lt;= 3) return true;  // 2 and 3 are prime
        if (n % 2 == 0 || n % 3 == 0) return false; // eliminate multiples of 2 and 3

        // Only check up to √n using 6k ± 1 optimization
        for (int i = 5; i * i &amp;lt;= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0)
                return false;
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then attempted c++ and my solution was again accepted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
public:
    int minSteps(int n) {
      vector&amp;lt;int&amp;gt;dp;
      for(int i=0;i&amp;lt;n+1;i++)dp.push_back(0);
      for(int i=2;i&amp;lt;n+1;i++){
       if(isPrime(i))dp[i]=i;
       else dp[i] =dp[highestDivisor(i)]+(i/highestDivisor(i));        
      }  
      return dp[n];
    }

    int highestDivisor(int n) {
        if (n &amp;lt;= 1) return -1; // No proper divisor exists for 0 or 1

        for (int i = n / 2; i &amp;gt;= 1; i--) {
            if (n % i == 0) {
                return i; // First divisor found from the top is the highest
            }
        }
        return -1; // Should never reach here for n &amp;gt; 1
    }

    bool isPrime(int n) {
        if (n &amp;lt;= 1) return false; // 0 and 1 are not prime
        if (n &amp;lt;= 3) return true;  // 2 and 3 are prime
        if (n % 2 == 0 || n % 3 == 0) return false; // eliminate multiples of 2 and 3

        // Only check up to √n using 6k ± 1 optimization
        for (int i = 5; i * i &amp;lt;= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0)
                return false;
        }
        return true;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Feeling very proud, I decided to try in javascript, I tried the following but saw that it was rejected.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var minSteps = function(n) {
      const []dp;
      for(let i=2;i&amp;lt;n+1;i++){
       let highestDivisor=i;
       for(let j=i/2;j&amp;gt;1;j--){
        if(i%j==0){
          highestDivisor=j;
          break;  
        }
       } 
       if(highestDivisor==i)dp[i]=i; 
       else dp[i] =dp[highestDivisor]+(i/highestDivisor);        
      }  
      return dp[n];  
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; I got a runtime error and was flabbergasted. I decided to come back later and to first attempt an optimized java solution. I decided to come up with the following approach. Instead of determining if a number is prime to find the highest divisor. I used the following which was accepted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int minSteps(int n) {
      int []dp= new int[n+1];
      for(int i=2;i&amp;lt;n+1;i++){
       if(isPrime(i))dp[i]=i;
       else {
        //System.out.println
        dp[i] =dp[highestDivisor(i)]+(i/highestDivisor(i));
       } 
      }  
      return dp[n];
    }

    public int highestDivisor(int n) {
        if (n &amp;lt;= 1) return -1; // No proper divisor exists for 0 or 1

        for (int i = n / 2; i &amp;gt;= 1; i--) {
            if (n % i == 0) {
                return i; // First divisor found from the top is the highest
            }
        }
        return -1; // Should never reach here for n &amp;gt; 1
    }

    public  boolean isPrime(int n) {
        if (n &amp;lt;= 1) return false; // 0 and 1 are not prime
        if (n &amp;lt;= 3) return true;  // 2 and 3 are prime
        if (n % 2 == 0 || n % 3 == 0) return false; // eliminate multiples of 2 and 3

        // Only check up to √n using 6k ± 1 optimization
        for (int i = 5; i * i &amp;lt;= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0)
                return false;
        }
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; It was accepted and was slightly better than the original. I felt happy. I decided to retry the javascript solution. I attempted to debug using print statements and realized that j was initialized as 1.5.So,I made some changes. I used j=i-1 and reran and came up with this solution which was accepted!!! I felt I was on a roll!!!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var minSteps = function(n) {
      const dp=[];
      for(let i=0;i&amp;lt;n+1;i++)dp.push(0);
      for(let i=2;i&amp;lt;n+1;i++){
       let highestDivisor=i;
       for(let j=i-1;j&amp;gt;1;j--){
        if(i%j==0){
          highestDivisor=j;
          break;  
        }
       } 
       if(highestDivisor==i)dp[i]=i; 
       else dp[i] =dp[highestDivisor]+(i/highestDivisor);        
      }
      return dp[n];  
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. Lessons Learned&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; This experience has taught me/helped reinforce a number of lessons.&lt;br&gt;
1) Fully understand the problem a program/coding challenge is trying to solve by careful reading, asking the necessary questions. A problem cant be solved if you have misunderstood or dont understand it.&lt;/p&gt;
&lt;p&gt;2) Research how previous or similar problems were solved by other programmers.&lt;/p&gt;
&lt;p&gt;3) Develop a first draft solution to the problem.&lt;/p&gt;
&lt;p&gt;4) If stuck, take a break. Solutions can come when your mind is at rest.&lt;/p&gt;
&lt;p&gt;5) Optimize your solutions where possible. It can save time and memory. In production environments, this can make a noticeable difference.&lt;/p&gt;
&lt;p&gt;6) Different programming languages handle variables differently. Understand how.&lt;/p&gt;
&lt;p&gt;7) Debugging with print statements is an unsophisticated but simple way to troubleshoot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Wrap-Up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; I hope my post has been helpful, please feel free to like,comment and share. Please check out my github profile for this and more solutions.It is here : &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/HectorwTT&quot;&gt;https://github.com/HectorwTT&lt;/a&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/3891/from-struggle-solution-approach-solving-programming-problem-using-650-leetcodes-keys</guid>
<pubDate>Thu, 10 Jul 2025 18:41:52 +0000</pubDate>
</item>
</channel>
</rss>