Search Blogs

Showing results for "Combination"

Found 50 results

LeetCode 17: Letter Combinations of a Phone Number — Java Solution and Backtracking Explained

LeetCode 17: Letter Combinations of a Phone Number — Java Solution and Backtracking Explained

IntroductionHave you ever wondered how a phone number can represent multiple possible words? On a traditional telephone keypad, each digit from 2 to 9 is associated with three or four letters. For example, 2 represents a, b, and c, while 3 represents d, e, and f.Now imagine entering the digits "23". For every letter represented by 2, we can combine it with every letter represented by 3. This produces nine possible combinations, including "ad", "ae", "af", "bd", and "cf".This is the central idea behind LeetCode 17: Letter Combinations of a Phone Number. The challenge is to generate every possible combination without missing any valid result.In this article, we will explore a recursive Java solution, understand how the keypad mapping is built, and trace how the recursive calls generate the final list.Problem link: LeetCode 17 — Letter Combinations of a Phone NumberUnderstanding the ProblemYou are given a string digits containing numbers from 2 to 9. Each digit corresponds to a group of letters on a telephone keypad.Your task is to return all possible strings that can be formed by choosing exactly one letter for each digit.For example, if the input is "23":Digit 2 can contribute a, b, or c.Digit 3 can contribute d, e, or f.Each letter selected from the first group must be paired with every letter in the second group.The resulting combinations are:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]The order of the returned combinations does not matter.One important detail is that the digits 0 and 1 are not included in the input constraints because they do not map to letters in this problem.Approach: Recursion with Phone Keypad MappingThe key observation is that each digit represents a choice, and recursion explores every available choice at the current position.For example, when processing "23", we first select a letter from digit 2. After that, we move to digit 3 and try all its letters. Once we have selected one letter for each digit, we have completed a valid combination.Our solution uses three main components.1. Build the digit-to-letter mappingWe create a list of character lists called arr.Each position represents a digit, and the corresponding list contains its letters. For instance:arr.get(2) contains a, b, c.arr.get(3) contains d, e, f.arr.get(7) contains p, q, r, s.arr.get(9) contains w, x, y, z.We begin with two empty lists at indices 0 and 1, so the mapping for a digit can be accessed directly using its numeric value.The variable charnum starts at 97, the ASCII value of lowercase a. We convert consecutive numeric values into characters and advance the counter by three or four letters, depending on the digit.This lets us construct the entire keypad mapping programmatically rather than writing each group manually.2. Recursively build each combinationThe helper method sol() receives four parameters:arr: the phone keypad mapping.ind: the current position in the input string.dig: the original digit string.sb: the combination constructed so far, represented as a string.At each recursive call, we examine the current digit and iterate over its available letters.For every letter, we make another recursive call with that letter appended to the current combination. This continues until every digit has contributed one letter.3. Stop when a complete combination is formedThe base condition is:if (sb.length() == dig.length())When the constructed string has the same length as the input digit string, we have selected one letter for every digit. We add that combination to the result list and return from the current call.The recursive process then continues exploring the remaining choices.This is the essential stopping condition that prevents the recursion from continuing beyond the number of input digits.Java SolutionHere is the recursive solution, preserving your approach while making two small improvements:Use a StringBuilder when adding a letter instead of repeatedly creating intermediate strings.Handle an empty input and keep the result list local to the method, so repeated calls do not accidentally reuse results from a previous invocation.import java.util.ArrayList;import java.util.List;class Solution {public void sol(List<List<Character>> arr,int ind,String digits,StringBuilder sb,List<String> lis) {if (ind == digits.length()) {lis.add(sb.toString());return;}int digit = digits.charAt(ind) - '0';for (char ch : arr.get(digit)) {sb.append(ch);sol(arr, ind + 1, digits, sb, lis);sb.deleteCharAt(sb.length() - 1);}}public List<String> letterCombinations(String digits) {List<String> lis = new ArrayList<>();if (digits.isEmpty()) {return lis;}List<List<Character>> arr = new ArrayList<>();arr.add(new ArrayList<>());arr.add(new ArrayList<>());int charnum = 97;for (int i = 0; i < 8; i++) {List<Character> li = new ArrayList<>();int count = (i == 5 || i == 7) ? 4 : 3;for (int j = 0; j < count; j++) {li.add((char) (charnum + j));}arr.add(li);charnum += count;}sol(arr, 0, digits, new StringBuilder(), lis);return lis;}}Why use StringBuilder and remove the last character?Your original code uses sb + arr.get(...).get(i) to create a new string for each recursive call. This is valid Java, but repeated string concatenation creates additional string objects.The improved version modifies a single StringBuilder:append(ch) adds the chosen letter.The recursive call explores the combinations that begin with that letter.deleteCharAt(sb.length() - 1) removes the letter after returning, allowing the next letter to be tried.This pattern is called backtracking. We make a choice, explore its consequences, and undo the choice before exploring another branch.It is especially useful when generating permutations, combinations, subsets, and other possible arrangements.Dry Run: digits = "23"Let's trace the recursion to see exactly how the combinations are generated.Initially, the current string is empty, and the current index is 0.Current choiceNext digit choiceCombination producedadadaeaeafafbdbdbebebfbfcdcdcececfcfThe process follows this sequence:Select a from digit 2.Explore all choices from digit 3, producing ad, ae, and af.Return to the first digit and replace a with b.Explore digit 3 again, producing bd, be, and bf.Repeat with c, producing cd, ce, and cf.The final result contains nine combinations.Why nine? The first digit has three choices, and the second digit also has three choices. Therefore, the total number of combinations is 3 × 3 = 9.Complexity AnalysisLet n be the length of the input string, and let R be the total number of generated combinations.Every digit has either three or four possible letters. The maximum number of combinations is therefore 4^n.Time complexity: O(n × R)There are R complete combinations, each containing n characters. Constructing and storing all these strings requires O(n × R) total time. Since R can be as large as 4^n, the worst-case time complexity is O(n × 4^n).Auxiliary space complexity: O(n)The recursion depth is at most n, and the StringBuilder holds at most n characters. The keypad mapping contains a fixed number of letters, so its space usage is constant with respect to the input length.Output space: O(n × R)The result list stores R strings, each of length n. This output memory is separate from the auxiliary space used by the algorithm.Because the constraints limit the input to at most four digits, the number of generated combinations remains small.Common Mistakes to Avoid1. Forgetting to advance to the next digitEvery recursive call must process the next position. If the index is not incremented, recursion will keep processing the same digit.2. Using an incorrect base conditionThe recursion should stop after processing all digits. Checking whether the current string has the same length as the input works, but checking ind == digits.length() directly makes the relationship between the index and the stopping condition clearer.3. Forgetting to undo a choiceWhen using a shared StringBuilder, remember to remove the character after the recursive call. Otherwise, letters from one branch may incorrectly carry over into another.4. Reusing a result list across separate callsIn your original implementation, lis is a class-level field. If the same Solution instance is used for multiple calls, previous results can remain in the list. Creating the result list inside letterCombinations() avoids this issue.5. Ignoring empty inputThe provided constraints guarantee at least one digit, but returning an empty list for an empty input is a useful defensive behavior. It also avoids accidentally treating the empty string as a valid result.Interview Tip: Recognize the Backtracking PatternWhenever a problem asks you to generate every possible arrangement from several groups of choices, consider recursion or backtracking.A useful way to design the solution is to ask:What choices are available at the current position?What state should be passed to the next recursive call?When is a complete answer formed?What needs to be undone before exploring the next choice?For this problem, the choices are the letters associated with the current digit, the state is the current combination and digit index, and a complete answer is reached after processing all digits.The same reasoning can help with problems such as generating subsets, permutations, combination sums, and other recursive search problems.ConclusionLeetCode 17, Letter Combinations of a Phone Number, is a useful introduction to recursive backtracking in Java. The phone keypad gives us a set of choices for every digit, and recursion explores every possible sequence of those choices.The most important part is not the keypad mapping itself. It is learning how to explore one choice completely, return to the previous state, and then try the next choice without mixing results between branches.Once this pattern becomes familiar, many combination-generation problems become easier to approach and explain in coding interviews.Key takeaway: Build the keypad mapping, choose one letter per digit, stop when every digit has been processed, and backtrack to explore the remaining possibilities.

LeetCodeJavaBacktrackingRecursionStringArrayListDSAMediumCombination
LeetCode 39: Combination Sum – Java Backtracking Solution with Dry Run & Complexity

LeetCode 39: Combination Sum – Java Backtracking Solution with Dry Run & Complexity

IntroductionIf you are preparing for coding interviews or improving your Data Structures and Algorithms skills, LeetCode 39 Combination Sum is one of the most important backtracking problems to learn. This problem helps you understand how recursion explores multiple possibilities and how combinations are generated efficiently. It is a foundational problem that builds strong problem-solving skills and prepares you for many advanced recursion and backtracking questions.Why Should You Solve This Problem?Combination Sum is not just another coding question — it teaches you how to think recursively and break a complex problem into smaller decisions. By solving it, you learn how to manage recursive paths, avoid duplicate combinations, and build interview-level backtracking intuition. Once you understand this pattern, problems like subsets, permutations, N-Queens, and Sudoku Solver become much easier to approach.LeetCode Problem LinkProblem Name: Combination SumProblem Link: Combination SumProblem StatementGiven an array of distinct integers called candidates and a target integer target, you need to return all unique combinations where the chosen numbers sum to the target.Important rules:You can use the same number unlimited times.Only unique combinations should be returned.Order of combinations does not matter.ExampleExample 1Input:candidates = [2,3,6,7]target = 7Output:[[2,2,3],[7]]Explanation2 + 2 + 3 = 77 itself equals targetUnderstanding the Problem in Simple WordsWe are given some numbers.We need to:Pick numbers from the arrayAdd them togetherReach the target sumUse numbers multiple times if neededAvoid duplicate combinationsThis problem belongs to the Backtracking + Recursion category.Real-Life AnalogyImagine you have coins of different values.You want to make an exact payment.You can reuse coins multiple times.You need to find every possible valid coin combination.This is exactly what Combination Sum does.Intuition Behind the SolutionAt every index, we have two choices:Pick the current numberSkip the current numberSince numbers can be reused unlimited times, when we pick a number, we stay at the same index.This creates a recursion tree.We continue until:Target becomes 0 → valid answerTarget becomes negative → invalid pathArray ends → stop recursionWhy Backtracking Works HereBacktracking helps us:Explore all possible combinationsUndo previous decisionsTry another pathIt is useful whenever we need:All combinationsAll subsetsPath explorationRecursive searchingApproach 1: Backtracking Using Pick and SkipCore IdeaAt every element:Either take itOr move to next elementJava Code (Pick and Skip Method)class Solution {List<List<Integer>> result = new ArrayList<>();public void solve(int[] candidates, int index, int target, List<Integer> current) {if (target == 0) {result.add(new ArrayList<>(current));return;}if (index == candidates.length) {return;}if (candidates[index] <= target) {current.add(candidates[index]);solve(candidates, index, target - candidates[index], current);current.remove(current.size() - 1);}solve(candidates, index + 1, target, current);}public List<List<Integer>> combinationSum(int[] candidates, int target) {solve(candidates, 0, target, new ArrayList<>());return result;}}Approach 2: Backtracking Using Loop (Optimized)This is the cleaner and more optimized version.Your code belongs to this category.Java Code (Loop-Based Backtracking)class Solution {List<List<Integer>> result = new ArrayList<>();public void solve(int[] arr, int index, int target, List<Integer> current) {if (target == 0) {result.add(new ArrayList<>(current));return;}if (index == arr.length) {return;}for (int i = index; i < arr.length; i++) {if (arr[i] > target) {continue;}current.add(arr[i]);solve(arr, i, target - arr[i], current);current.remove(current.size() - 1);}}public List<List<Integer>> combinationSum(int[] candidates, int target) {solve(candidates, 0, target, new ArrayList<>());return result;}}Dry Run of the AlgorithmInputcandidates = [2,3,6,7]target = 7Step-by-Step ExecutionStart:solve([2,3,6,7], index=0, target=7, [])Pick 2[2]target = 5Pick 2 again:[2,2]target = 3Pick 2 again:[2,2,2]target = 1No valid choice possible.Backtrack.Try 3[2,2,3]target = 0Valid answer found.Add:[2,2,3]Try 7[7]target = 0Valid answer found.Add:[7]Final Output[[2,2,3],[7]]Recursion Tree Visualization[]/ | | \2 3 6 7/2/2/3Every branch explores a different combination.Time Complexity AnalysisTime ComplexityO(2^Target)More accurately:O(N^(Target/minValue))Where:N = Number of candidatesTarget = Required sumReason:Every number can be picked multiple times.This creates many recursive branches.Space ComplexityO(Target)Reason:Recursion stack stores elements.Maximum recursion depth depends on target.Why We Pass Same Index AgainNotice this line:solve(arr, i, target - arr[i], current);We pass i, not i+1.Why?Because we can reuse the same number unlimited times.If we used i+1, we would move forward and lose repetition.Why Duplicate Combinations Are Not CreatedWe start loop from current index.This guarantees:[2,3]and[3,2]are not both generated.Order remains controlled.Common Mistakes Beginners Make1. Using i+1 Instead of iWrong:solve(arr, i+1, target-arr[i], current)This prevents reuse.2. Forgetting Backtracking StepWrong:current.remove(current.size()-1)Without removing, recursion keeps incorrect values.3. Missing Target == 0 Base CaseThis is where valid answer is stored.Important Interview InsightCombination Sum is a foundational problem.It helps build understanding for:Combination Sum IISubsetsPermutationsN-QueensWord SearchSudoku SolverThis question is frequently asked in coding interviews.Pattern RecognitionUse Backtracking when problem says:Find all combinationsGenerate all subsetsFind all pathsUse recursionExplore possibilitiesOptimized Thinking StrategyWhenever you see:Target sumRepeated selectionMultiple combinationsThink:Backtracking + DFSEdge CasesCase 1candidates = [2]target = 1Output:[]No possible answer.Case 2candidates = [1]target = 3Output:[[1,1,1]]Interview Answer in One Line“We use backtracking to recursively try all candidate numbers while reducing the target and backtrack whenever a path becomes invalid.”Final Java Codeclass Solution {List<List<Integer>> result = new ArrayList<>();public void solve(int[] arr, int index, int target, List<Integer> current) {if (target == 0) {result.add(new ArrayList<>(current));return;}for (int i = index; i < arr.length; i++) {if (arr[i] > target) {continue;}current.add(arr[i]);solve(arr, i, target - arr[i], current);current.remove(current.size() - 1);}}public List<List<Integer>> combinationSum(int[] candidates, int target) {solve(candidates, 0, target, new ArrayList<>());return result;}}Key TakeawaysCombination Sum uses Backtracking.Reuse same element by passing same index.Target becomes smaller in recursion.Backtracking removes last element.Very important for interview preparation.Frequently Asked QuestionsIs Combination Sum DP or Backtracking?It is primarily solved using Backtracking.Dynamic Programming can also solve it but recursion is more common.Why is this Medium difficulty?Because:Requires recursion understandingRequires backtracking logicRequires duplicate preventionCan we sort the array?Yes.Sorting can help with pruning.ConclusionLeetCode 39 Combination Sum is one of the best problems to learn recursion and backtracking.Once you understand this pattern, many interview problems become easier.The loop-based recursive solution is clean, optimized, and interview-friendly.If you master this question, you gain strong understanding of recursive decision trees and combination generation.

LeetcodeMediumRecursionBacktrackingJava
LeetCode 1569: Number of Ways to Reorder Array to Get Same BST – Java Solution & Explanation

LeetCode 1569: Number of Ways to Reorder Array to Get Same BST – Java Solution & Explanation

IntroductionImagine inserting a sequence of numbers into a Binary Search Tree (BST). The first number becomes the root, smaller values move to the left subtree, and larger values move to the right subtree.Now consider an interesting question: Can the numbers be rearranged into a different order while producing exactly the same BST?The answer is yes.For example, the arrays [2, 1, 3] and [2, 3, 1] produce the same BST. The root remains 2, while 1 belongs to the left subtree and 3 belongs to the right subtree. The insertion order between those two subtrees can change without changing the final tree.This is the central idea behind LeetCode 1569 – Number of Ways to Reorder Array to Get Same BST.The challenge is that the number of possible reorderings can grow rapidly. Generating every permutation would be far too expensive. Instead, we can count valid arrangements mathematically using combinations, then combine those counts recursively for the left and right subtrees.There is one more problem: calculating combinations repeatedly using factorials can lead to unnecessary work and large intermediate numbers. Pascal’s Triangle provides a convenient way to precompute the required combination values.Let's build the solution from the beginning.Problem StatementProblem Link - Number of Ways to Reorder Array to Get Same BSTGiven an array nums containing a permutation of the integers from 1 to n, insert its elements in order into an initially empty BST.Return the number of different reorderings of nums that produce exactly the same BST as the original array.The original ordering itself is included in the total, but the answer must count only the other reorderings, so the original arrangement must be subtracted once.Since the answer may be large, return it modulo:1,000,000,007Example 1Input: nums = [2, 1, 3]Output: 1The original array creates this BST:2/ \1 3The array [2, 3, 1] creates the same tree.There are two valid arrangements in total, including the original one. After excluding the original ordering, the answer is 1.Example 2Input: nums = [3, 4, 5, 1, 2]Output: 5There are six valid arrangements in total, including the original sequence. Subtracting the original arrangement gives 5.Example 3Input: nums = [1, 2, 3]Output: 0The BST is a right-skewed tree. Every value must appear in increasing order to produce that same tree, so no alternative ordering exists.Step 1: The First Element Always Becomes the RootThe first observation comes directly from how a BST is constructed.Consider:nums = [3, 1, 2, 4, 5]The first element, 3, becomes the root.Every remaining value is divided into two groups:Values smaller than 3 go to the left subtree.Values greater than 3 go to the right subtree.The groups are:Left: [1, 2]Right: [4, 5]The resulting BST is:3/ \1 4\ \2 5The root must remain 3 in every valid reordering because the first inserted value determines the root.However, the left and right subtree elements do not necessarily need to appear in one fixed interleaving.For example, these sequences preserve the same relative order within each subtree:[3, 1, 2, 4, 5][3, 1, 4, 2, 5][3, 4, 1, 2, 5]The left-subtree sequence remains 1, 2, and the right-subtree sequence remains 4, 5.The elements from the two groups can be mixed together in different ways, provided that the relative insertion order within each group is preserved.This gives us the first major insight:Count the valid arrangements of the left subtree, count those of the right subtree, and then count the ways to interleave the two groups.Step 2: Why Do We Need Combinations (nCr)?Suppose a node has:L elements in its left subtree.R elements in its right subtree.After choosing the root, there are L + R remaining positions to fill.We need to choose which of those positions will contain the left-subtree elements. Once those positions are selected, the remaining positions automatically belong to the right subtree.The number of ways to choose L positions from L + R positions is: [(L+RL)][ \binom{L+R}{L} ][(LL+R​)] This is the familiar combinations formula, also written as nCr.The recursive count becomes: [ways=(L+RL)×waysLeft×waysRight][ ways = \binom{L+R}{L} \times waysLeft \times waysRight ][ways=(LL+R​)×waysLeft×waysRight] Here:waysLeft counts valid reorderings inside the left subtree.waysRight counts valid reorderings inside the right subtree.The combination term counts the possible interleavings of the two groups.All three factors are necessary.A small exampleConsider the tree formed by:nums = [2, 1, 3]There is one element on the left and one on the right.Therefore: [(21)=2][ \binom{2}{1}=2 ][(12​)=2] The two interleavings are:[1, 3][3, 1]Both produce the same BST when placed after root 2.Since each subtree contains only one element, each has exactly one valid arrangement.Thus: [ways=2×1×1=2][ ways = 2 \times 1 \times 1 = 2 ][ways=2×1×1=2] This count includes the original array. The final answer is: [2−1=1][ 2-1=1 ][2−1=1] The subtraction is performed only once, after the complete recursive count is calculated.Step 3: Calculating nCr EfficientlyNow we know how combinations fit into the solution. But how should the values of nCr be calculated?The mathematical formula is: [(nr)=n!r!(n−r)!][ \binom{n}{r}=\frac{n!}{r!(n-r)!} ][(rn​)=r!(n−r)!n!​] For example: [(42)=4!2!,2!=6][ \binom{4}{2}=\frac{4!}{2!,2!}=6 ][(24​)=2!,2!4!​=6] One approach is to calculate factorials repeatedly. However, the recursive solution needs combination values at many different subtree sizes. Recalculating factorials again and again adds unnecessary work.We can instead precompute all the required combinations using Pascal’s Triangle.The Pascal’s Triangle observationEvery value in Pascal’s Triangle is obtained by adding the two values directly above it: [(n−1r−1)+(n−1r)][\binom{n-1}{r-1} + \binom{n-1}{r} ][(r−1n−1​)+(rn−1​)] The boundary values are always 1: [(n0)=(nn)=1][ \binom{n}{0}=\binom{n}{n}=1 ][(0n​)=(nn​)=1] The first few rows look like this:11 11 2 11 3 3 11 4 6 4 11 5 10 10 5 1For example, the value 6 is calculated as:3 + 3 = 6It represents: [(42)=6][ \binom{4}{2}=6 ][(24​)=6] The same idea can be implemented using a two-dimensional list or array.Instead of calculating factorials whenever the recursive function needs a combination, we build the table once and look up the required value in constant time.How the submitted code builds the tableYour solution uses the list liss to store the rows of Pascal’s Triangle.for (int i = 0; i < nums.length; i++) {List<Integer> lis = new ArrayList<>();for (int j = 0; j < i + 1; j++) {if (i - 1 >= 0 && j - 1 >= 0 && j != i) {long sum = (liss.get(i - 1).get(j)+ liss.get(i - 1).get(j - 1));lis.add((int) (sum % mod));} else {lis.add(1);}}liss.add(new ArrayList<>(lis));}Each iteration builds one row.The first and last values of each row are 1. Every interior value is calculated by adding the two entries from the previous row.For instance, the row representing n = 4 contains:[1, 4, 6, 4, 1]So when the recursive function needs to choose positions for a left subtree of size 2 from a total of 4 elements, it can retrieve the value 6 directly.The table is built once before recursion begins.Step 4: Recursively Count the Left and Right SubtreesWith the combination table ready, the remaining work is recursive.Your sol() method starts by selecting the first element as the root:int root = nums.get(0);It then separates all remaining elements into two lists:List<Integer> left = new ArrayList<>();List<Integer> right = new ArrayList<>();for (int i = 1; i < n; i++) {if (nums.get(i) < root) {left.add(nums.get(i));} else {right.add(nums.get(i));}}Because the input contains distinct values, values smaller than the root belong to the left subtree, while larger values belong to the right subtree.For example:nums = [3, 4, 5, 1, 2]Root: 3Left: [1, 2]Right: [4, 5]Next, the function recursively counts the arrangements for both subtrees:long x = (sol(left)) % mod;long y = (sol(right)) % mod;The base case is:if (nums.size() <= 1) {return 1;}A subtree containing zero or one element has exactly one arrangement. There is nothing to reorder.Finally, the combination table is used to count the interleavings:long z = (liss.get(n - 1).get(left.size())) % mod;Here:n - 1 is the number of elements remaining after choosing the root.left.size() is the number of positions that must be assigned to the left subtree.z is the number of ways to choose those positions.The recursive result is then:return (int) ((((x * y) % mod) * z) % mod);In mathematical form: [ways=waysLeft×waysRight×(n−1leftSize)][ ways = waysLeft \times waysRight \times \binom{n-1}{leftSize} ][ways=waysLeft×waysRight×(leftSizen−1​)] The modulo operation keeps the intermediate result within manageable bounds.Complete Java SolutionThe following is the submitted solution, with formatting and explanatory comments added.class Solution {long mod = 1000000007;List<List<Integer>> liss = new ArrayList<>();public int sol(List<Integer> nums) {if (nums.size() <= 1) {return 1;}int root = nums.get(0);int n = nums.size();List<Integer> left = new ArrayList<>();List<Integer> right = new ArrayList<>();// Divide the remaining values by the current root.for (int i = 1; i < n; i++) {if (nums.get(i) < root) {left.add(nums.get(i));} else {right.add(nums.get(i));}}// Count valid arrangements of both subtrees.long x = sol(left) % mod;long y = sol(right) % mod;// Choose positions for the left-subtree elements.long z = liss.get(n - 1).get(left.size()) % mod;// Combine subtree arrangements and their interleavings.return (int) ((((x * y) % mod) * z) % mod);}public int numOfWays(int[] nums) {int n = nums.length;// Build Pascal's Triangle modulo 1,000,000,007.for (int i = 0; i < n; i++) {List<Integer> lis = new ArrayList<>();for (int j = 0; j <= i; j++) {if (i > 0 && j > 0 && j < i) {long sum = (long) liss.get(i - 1).get(j)+ liss.get(i - 1).get(j - 1);lis.add((int) (sum % mod));} else {lis.add(1);}}liss.add(lis);}List<Integer> num = new ArrayList<>();for (int value : nums) {num.add(value);}// Includes the original ordering.int ans = sol(num);// Exclude the original ordering.return ans - 1;}}Dry Run: How the Pieces Work TogetherConsider:nums = [2, 1, 3]1. Select the rootThe first element is 2.Root = 2Left = [1]Right = [3]2. Count the subtree arrangementsBoth subtrees contain one element.waysLeft = 1waysRight = 1The base case returns 1 for each subtree.3. Find the number of interleavingsThere are two remaining elements, and one belongs to the left subtree.The required combination is: [(21)=2][ \binom{2}{1}=2 ][(12​)=2] Pascal’s Triangle gives this value directly.4. Combine the results [ways=1×1×2=2][ ways = 1 \times 1 \times 2=2 ][ways=1×1×2=2] The two possible arrays are:[2, 1, 3][2, 3, 1]Both generate the same BST.5. Exclude the original orderingThe function initially counts all valid arrangements, including the input array.Therefore:Answer = 2 - 1 = 1The final output is 1, matching the expected result.Complexity AnalysisLet n be the length of nums.Building Pascal’s Triangle:There are (O(n^2)) entries, and each entry is calculated in constant time.Time: (O(n^2))Space: (O(n^2))Recursive BST counting:At each recursive call, the elements are divided into left and right lists. Across each level of the recursion, the elements processed total at most (n). In a highly unbalanced BST, the recursion can reach depth (n), and repeatedly scanning and copying the sublists can result in (O(n^2)) total work.Worst-case time: (O(n^2))Additional recursive and sublist space: up to (O(n^2)) across an unbalanced recursion, excluding the combination table.Therefore, the overall worst-case time complexity of this implementation is:O(n²)The Pascal’s Triangle table itself requires:O(n²) spaceThese bounds are suitable for the problem constraint of n <= 1000.Important Detail: Why Do We Subtract One?The recursive function counts all sequences that generate the same BST, including the original input sequence.However, the problem asks for the number of different reorderings, excluding the original ordering.That is why the final line is:return ans - 1;This subtraction should happen only once, after the full recursive count has been calculated. Subtracting one inside each recursive call would incorrectly exclude arrangements from individual subtrees.Interview TakeawayThis problem combines three ideas that are useful in more advanced algorithm questions:Recursive decomposition: The left and right subtrees can be solved independently.Combinatorics: The elements of the two subtrees can be interleaved in (\binom{L+R}{L}) ways.Dynamic programming: Pascal’s Triangle precomputes combination values so they can be reused instead of recalculated.The most important insight is that the left and right subtree sequences must each preserve their own relative order. Only their positions relative to one another can change.Once that restriction is understood, the counting formula follows naturally.ConclusionLeetCode 1569 looks difficult because it asks for the number of valid reorderings of an array, and the number of possible permutations grows extremely quickly.The solution becomes manageable when the problem is viewed through the structure of the BST. The first element fixes the root, smaller and larger elements form the left and right subtrees, and the two groups can be interleaved without changing the resulting tree.Recursion counts the valid arrangements inside each subtree, while combinations count the possible interleavings. Pascal’s Triangle makes those combination values available efficiently, and modular arithmetic keeps the calculations within the required range.The central formula to remember is:Ways =Left subtree ways× Right subtree ways× Ways to interleave both subtreesThis is the key to turning a seemingly enormous permutation problem into a structured recursive calculation.

LeetCodeJavaBinary Search TreeBSTRecursionCombinatoricsPascal TriangleDynamic ProgrammingDSAHard
LeetCode 22 Generate Parentheses | Backtracking Java Solution Explained

LeetCode 22 Generate Parentheses | Backtracking Java Solution Explained

IntroductionThe Generate Parentheses problem is one of the most important and frequently asked backtracking questions in coding interviews.At first glance, it may look like a simple string generation problem—but the real challenge is to generate only valid (well-formed) parentheses combinations.This problem is a perfect example of:Constraint-based recursionBacktracking with conditionsDecision tree pruningIn this article, we’ll break down the intuition, understand the constraints, and implement a clean and efficient solution.🔗 Problem LinkLeetCode: Generate ParenthesesProblem StatementGiven n pairs of parentheses:👉 Generate all combinations of well-formed parenthesesExamplesExample 1Input:n = 3Output:["((()))","(()())","(())()","()(())","()()()"]Example 2Input:n = 1Output:["()"]Key InsightWe are not generating all possible strings—we are generating only valid parentheses.Rules of Valid ParenthesesNumber of ( must equal number of )At any point:closing brackets should never exceed opening bracketsIntuition (Decision Making)At every step, we have two choices:Add "(" OR Add ")"But we cannot always take both choices.Valid ConditionsWhen can we add "("?If open > 0When can we add ")"?If close > open👉 This ensures the string always remains valid.Decision Tree (n = 3)👉 You can add your tree diagram here for better visualization.Conceptual FlowStart: ""→ "(" → "(("→ "(((" → "((()"→ ...Invalid paths like ")(" are never explored because of constraints.Approach: Backtracking with ConstraintsIdeaKeep track of:Remaining open bracketsRemaining close bracketsBuild string step by stepOnly take valid decisionsJava Codeimport java.util.*;class Solution {// List to store all valid combinationsList<String> lis = new ArrayList<>();public void solve(int open, int close, String curr) {// Base case: no brackets leftif (open == 0 && close == 0) {lis.add(curr); // valid combinationreturn;}// Choice 1: Add opening bracket "("// Allowed only if we still have opening brackets leftif (open > 0) {solve(open - 1, close, curr + "(");}// Choice 2: Add closing bracket ")"// Allowed only if closing brackets > opening bracketsif (open < close) {solve(open, close - 1, curr + ")");}}public List<String> generateParenthesis(int n) {solve(n, n, ""); // start recursionreturn lis;}}Step-by-Step Execution (n = 2)Start: ""→ "("→ "(("→ "(())"→ "()"→ "()()"Complexity AnalysisTime Complexity: O(4ⁿ / √n) (Catalan Number)Space Complexity: O(n) recursion stackWhy Catalan Number?The number of valid parentheses combinations is:Cn = (1 / (n+1)) * (2n choose n)Why This Approach WorksIt avoids invalid combinations earlyUses constraints to prune recursion treeGenerates only valid resultsEfficient compared to brute force❌ Naive Approach (Why It Fails)Generate all combinations of ( and ):Total combinations = 2^(2n)Then filter valid ones👉 Very inefficient → TLEKey TakeawaysThis is a constraint-based recursion problemAlways:Add "(" if availableAdd ")" only if validBacktracking avoids invalid pathsImportant pattern for interviewsCommon Interview VariationsValid parentheses checkingLongest valid parenthesesRemove invalid parenthesesBalanced expressionsConclusionThe Generate Parentheses problem is a must-know backtracking problem. It teaches how to apply constraints during recursion to efficiently generate valid combinations.Once mastered, this pattern becomes extremely useful in solving many advanced recursion problems.Frequently Asked Questions (FAQs)1. Why can’t we add ")" anytime?Because it may create invalid sequences like ")(".2. What is the key trick?Ensure:close > open3. Is recursion the best approach?Yes, it is the most intuitive and efficient method.

MediumLeetCodeJavaRecursion
Recursion in Java - Complete Guide With Examples and Practice Problems

Recursion in Java - Complete Guide With Examples and Practice Problems

IntroductionIf there is one topic in programming that confuses beginners more than anything else, it is recursion. Most people read the definition, nod their head, and then immediately freeze when they have to write recursive code themselves.The problem is not that recursion is genuinely hard. The problem is that most explanations start with code before building the right mental model. Once you have the right mental model, recursion clicks permanently and you start seeing it everywhere — in tree problems, graph problems, backtracking, dynamic programming, divide and conquer, and more.This guide covers everything from the ground up. What recursion is, how the call stack works, how to identify base cases and recursive cases, every type of recursion, common patterns, time and space complexity analysis, the most common mistakes, and the top LeetCode problems to practice.By the end of this article, recursion will not feel like magic anymore. It will feel like a natural tool you reach for confidently.What Is Recursion?Recursion is when a function calls itself to solve a smaller version of the same problem.That is the complete definition. But let us make it concrete.Imagine you want to count down from 5 to 1. One way is a loop. Another way is — print 5, then solve the exact same problem for counting down from 4 to 1. Then print 4, solve for 3. And so on until you reach the base — there is nothing left to count down.void countDown(int n) { if (n == 0) return; // stop here System.out.println(n); countDown(n - 1); // solve the smaller version}The function countDown calls itself with a smaller input each time. Eventually it reaches 0 and stops. That stopping condition is the most important part of any recursive function — the base case.The Two Parts Every Recursive Function Must HaveEvery correctly written recursive function has exactly two parts. Without both, the function either gives wrong answers or runs forever.Part 1: Base CaseThe base case is the condition under which the function stops calling itself and returns a direct answer. It is the smallest version of the problem that you can solve without any further recursion.Without a base case, recursion never stops and you get a StackOverflowError — Java's way of telling you the call stack ran out of memory.Part 2: Recursive CaseThe recursive case is where the function calls itself with a smaller or simpler input — moving closer to the base case with each call. If your recursive case does not make the problem smaller, you have an infinite loop.Think of it like a staircase. The base case is the ground floor. The recursive case is each step going down. Every step must genuinely bring you one level closer to the ground.How Recursion Works — The Call StackThis is the mental model that most explanations skip, and it is the reason recursion confuses people.Every time a function is called in Java, a new stack frame is created and pushed onto the call stack. This frame stores the function's local variables, parameters, and where to return to when the function finishes.When a recursive function calls itself, a new frame is pushed on top. When that call finishes, its frame is popped and execution returns to the previous frame.Let us trace countDown(3) through the call stack:countDown(3) called → frame pushed prints 3 calls countDown(2) → frame pushed prints 2 calls countDown(1) → frame pushed prints 1 calls countDown(0) → frame pushed n == 0, return → frame popped back in countDown(1), return → frame popped back in countDown(2), return → frame popped back in countDown(3), return → frame poppedOutput: 3, 2, 1The call stack grows as calls go deeper, then shrinks as calls return. This is why recursion uses O(n) space for n levels deep — each level occupies one stack frame in memory.Your First Real Recursive Function — FactorialFactorial is the classic first recursion example. n! = n × (n-1) × (n-2) × ... × 1Notice the pattern — n! = n × (n-1)!. The factorial of n is n times the factorial of n-1. That recursive structure makes it perfect for recursion.public int factorial(int n) { // base case if (n == 0 || n == 1) return 1; // recursive case return n * factorial(n - 1);}Dry Run — factorial(4)factorial(4)= 4 * factorial(3)= 4 * 3 * factorial(2)= 4 * 3 * 2 * factorial(1)= 4 * 3 * 2 * 1= 24The call stack builds up going in, then multiplications happen coming back out. This "coming back out" phase is called the return phase or unwinding of the stack.Time Complexity: O(n) — n recursive calls Space Complexity: O(n) — n frames on the call stackThe Two Phases of RecursionEvery recursive function has two phases and understanding both is critical.Phase 1: The Call Phase (Going In)This happens as the function keeps calling itself with smaller inputs. Things you do before the recursive call happen in this phase — in order from the outermost call to the innermost.Phase 2: The Return Phase (Coming Back Out)This happens as each call finishes and returns to its caller. Things you do after the recursive call happen in this phase — in reverse order, from the innermost call back to the outermost.This distinction explains why the output order can be surprising:void printBothPhases(int n) { if (n == 0) return; System.out.println("Going in: " + n); // call phase printBothPhases(n - 1); System.out.println("Coming out: " + n); // return phase}For printBothPhases(3):Going in: 3Going in: 2Going in: 1Coming out: 1Coming out: 2Coming out: 3This two-phase understanding is what makes problems like reversing a string or printing a linked list backwards via recursion feel natural.Types of RecursionRecursion is not one-size-fits-all. There are several distinct types and knowing which type applies to a problem shapes how you write the solution.1. Direct RecursionThe function calls itself directly. This is the most common type — what we have seen so far.void direct(int n) { if (n == 0) return; direct(n - 1); // calls itself}2. Indirect RecursionFunction A calls Function B which calls Function A. They form a cycle.void funcA(int n) { if (n <= 0) return; System.out.println("A: " + n); funcB(n - 1);}void funcB(int n) { if (n <= 0) return; System.out.println("B: " + n); funcA(n - 1);}Used in: state machines, mutual recursion in parsers, certain mathematical sequences.3. Tail RecursionThe recursive call is the last operation in the function. Nothing happens after the recursive call returns — no multiplication, no addition, nothing.// NOT tail recursive — multiplication happens after returnint factorial(int n) { if (n == 1) return 1; return n * factorial(n - 1); // multiply after return — not tail}// Tail recursive — recursive call is the last thingint factorialTail(int n, int accumulator) { if (n == 1) return accumulator; return factorialTail(n - 1, n * accumulator); // last operation}Why does tail recursion matter? In languages that support tail call optimization (like Scala, Kotlin, and many functional languages), tail recursive functions can be converted to iteration internally — no stack frame accumulation, O(1) space. Java does NOT perform tail call optimization, but understanding tail recursion is still important for interviews and functional programming concepts.4. Head RecursionThe recursive call happens first, before any other processing. All processing happens in the return phase.void headRecursion(int n) { if (n == 0) return; headRecursion(n - 1); // call first System.out.println(n); // process after}// Output: 1 2 3 4 5 (processes in reverse order of calls)5. Tree RecursionThe function makes more than one recursive call per invocation. This creates a tree of calls rather than a linear chain. Fibonacci is the classic example.int fibonacci(int n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); // TWO recursive calls}The call tree for fibonacci(4): fib(4) / \ fib(3) fib(2) / \ / \ fib(2) fib(1) fib(1) fib(0) / \ fib(1) fib(0)Time Complexity: O(2ⁿ) — exponential! Each call spawns two more. Space Complexity: O(n) — maximum depth of the call treeThis is why memoization (caching results) is so important for tree recursion — it converts O(2ⁿ) to O(n) by never recomputing the same subproblem twice.6. Mutual RecursionA specific form of indirect recursion where two functions call each other alternately to solve a problem. Different from indirect recursion in that the mutual calls are the core mechanism of the solution.// Check if a number is even or odd using mutual recursionboolean isEven(int n) { if (n == 0) return true; return isOdd(n - 1);}boolean isOdd(int n) { if (n == 0) return false; return isEven(n - 1);}Common Recursion Patterns in DSAThese are the patterns you will see over and over in interview problems. Recognizing them is more important than memorizing solutions.Pattern 1: Linear Recursion (Do Something, Recurse on Rest)Process the current element, then recurse on the remaining problem.// Sum of arrayint arraySum(int[] arr, int index) { if (index == arr.length) return 0; // base case return arr[index] + arraySum(arr, index + 1); // current + rest}Pattern 2: Divide and Conquer (Split Into Two Halves)Split the problem into two halves, solve each recursively, combine results.// Merge Sortvoid mergeSort(int[] arr, int left, int right) { if (left >= right) return; // base case — single element int mid = (left + right) / 2; mergeSort(arr, left, mid); // sort left half mergeSort(arr, mid + 1, right); // sort right half merge(arr, left, mid, right); // combine}Pattern 3: Backtracking (Try, Recurse, Undo)Try a choice, recurse to explore it, undo the choice when backtracking.// Generate all subsetsvoid subsets(int[] nums, int index, List<Integer> current, List<List<Integer>> result) { if (index == nums.length) { result.add(new ArrayList<>(current)); return; } // Choice 1: include nums[index] current.add(nums[index]); subsets(nums, index + 1, current, result); current.remove(current.size() - 1); // undo // Choice 2: exclude nums[index] subsets(nums, index + 1, current, result);}Pattern 4: Tree Recursion (Left, Right, Combine)Recurse on left subtree, recurse on right subtree, combine or process results.// Height of binary treeint height(TreeNode root) { if (root == null) return 0; // base case int leftHeight = height(root.left); // solve left int rightHeight = height(root.right); // solve right return 1 + Math.max(leftHeight, rightHeight); // combine}Pattern 5: Memoization (Cache Recursive Results)Store results of recursive calls so the same subproblem is never solved twice.Map<Integer, Integer> memo = new HashMap<>();int fibonacci(int n) { if (n <= 1) return n; if (memo.containsKey(n)) return memo.get(n); // return cached int result = fibonacci(n - 1) + fibonacci(n - 2); memo.put(n, result); // cache before returning return result;}This converts Fibonacci from O(2ⁿ) to O(n) time with O(n) space — a massive improvement.Recursion vs Iteration — When to Use WhichThis is one of the most common interview questions about recursion. Here is a clear breakdown:Use Recursion when:The problem has a naturally recursive structure (trees, graphs, divide and conquer)The solution is significantly cleaner and easier to understand recursivelyThe problem involves exploring multiple paths or choices (backtracking)The depth of recursion is manageable (not too deep to cause stack overflow)Use Iteration when:The problem is linear and a loop is equally clearMemory is a concern (iteration uses O(1) stack space vs O(n) for recursion)Performance is critical and function call overhead mattersJava's stack size limit could be hit (default around 500-1000 frames for deep recursion)The key rule: Every recursive solution can be converted to an iterative one (usually using an explicit stack). But recursive solutions for tree and graph problems are almost always cleaner to write and understand.Time and Space Complexity of Recursive FunctionsAnalyzing complexity for recursive functions requires a specific approach.The Recurrence Relation MethodExpress the time complexity as a recurrence relation and solve it.Factorial:T(n) = T(n-1) + O(1) = T(n-2) + O(1) + O(1) = T(1) + n×O(1) = O(n)Fibonacci (naive):T(n) = T(n-1) + T(n-2) + O(1) ≈ 2×T(n-1) = O(2ⁿ)Binary Search:T(n) = T(n/2) + O(1) = O(log n) [by Master Theorem]Merge Sort:T(n) = 2×T(n/2) + O(n) = O(n log n) [by Master Theorem]Space Complexity Rule for RecursionSpace complexity of a recursive function = maximum depth of the call stack × space per frameLinear recursion (factorial, sum): O(n) spaceBinary recursion (Fibonacci naive): O(n) space (maximum depth, not number of nodes)Divide and conquer (merge sort): O(log n) space (depth of recursion tree)Memoized Fibonacci: O(n) space (memo table + call stack)Classic Recursive Problems With SolutionsProblem 1: Reverse a StringString reverse(String s) { if (s.length() <= 1) return s; // base case // last char + reverse of everything before last char return s.charAt(s.length() - 1) + reverse(s.substring(0, s.length() - 1));}Dry run for "hello":reverse("hello") = 'o' + reverse("hell")reverse("hell") = 'l' + reverse("hel")reverse("hel") = 'l' + reverse("he")reverse("he") = 'e' + reverse("h")reverse("h") = "h"Unwinding: "h" → "he" → "leh" → "lleh" → "olleh" ✅Problem 2: Power Function (x^n)double power(double x, int n) { if (n == 0) return 1; // base case if (n < 0) return 1.0 / power(x, -n); // handle negative if (n % 2 == 0) { double half = power(x, n / 2); return half * half; // x^n = (x^(n/2))^2 } else { return x * power(x, n - 1); }}This is the fast power algorithm — O(log n) time instead of O(n).Problem 3: Fibonacci With Memoizationint[] memo = new int[100];Arrays.fill(memo, -1);int fib(int n) { if (n <= 1) return n; if (memo[n] != -1) return memo[n]; memo[n] = fib(n - 1) + fib(n - 2); return memo[n];}Time: O(n) — each value computed once Space: O(n) — memo array + call stackProblem 4: Tower of HanoiThe classic recursion teaching problem. Move n disks from source to destination using a helper rod.void hanoi(int n, char source, char destination, char helper) { if (n == 1) { System.out.println("Move disk 1 from " + source + " to " + destination); return; } // Move n-1 disks from source to helper hanoi(n - 1, source, helper, destination); // Move the largest disk from source to destination System.out.println("Move disk " + n + " from " + source + " to " + destination); // Move n-1 disks from helper to destination hanoi(n - 1, helper, destination, source);}Time Complexity: O(2ⁿ) — minimum moves required is 2ⁿ - 1 Space Complexity: O(n) — call stack depthProblem 5: Generate All Subsets (Power Set)void generateSubsets(int[] nums, int index, List<Integer> current, List<List<Integer>> result) { result.add(new ArrayList<>(current)); // add current subset for (int i = index; i < nums.length; i++) { current.add(nums[i]); // include generateSubsets(nums, i + 1, current, result); // recurse current.remove(current.size() - 1); // exclude (backtrack) }}For [1, 2, 3] — generates all 8 subsets: [], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]Time: O(2ⁿ) — 2ⁿ subsets Space: O(n) — recursion depthProblem 6: Binary Search Recursivelyint binarySearch(int[] arr, int target, int left, int right) { if (left > right) return -1; // base case — not found int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) return binarySearch(arr, target, mid + 1, right); else return binarySearch(arr, target, left, mid - 1);}Time: O(log n) — halving the search space each time Space: O(log n) — log n frames on the call stackRecursion on Trees — The Natural HabitatTrees are where recursion truly shines. Every tree problem becomes elegant with recursion because a tree is itself a recursive structure — each node's left and right children are trees themselves.// Maximum depth of binary treeint maxDepth(TreeNode root) { if (root == null) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));}// Check if tree is symmetricboolean isSymmetric(TreeNode left, TreeNode right) { if (left == null && right == null) return true; if (left == null || right == null) return false; return left.val == right.val && isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left);}// Path sum — does any root-to-leaf path sum to target?boolean hasPathSum(TreeNode root, int target) { if (root == null) return false; if (root.left == null && root.right == null) return root.val == target; return hasPathSum(root.left, target - root.val) || hasPathSum(root.right, target - root.val);}Notice the pattern in all three — base case handles null, recursive case handles left and right subtrees, result combines both.How to Think About Any Recursive Problem — Step by StepThis is the framework you should apply to every new recursive problem you encounter:Step 1 — Identify the base case What is the smallest input where you know the answer directly without any recursion? For arrays it is usually empty array or single element. For trees it is null node. For numbers it is 0 or 1.Step 2 — Trust the recursive call Assume your function already works correctly for smaller inputs. Do not trace through the entire recursion mentally — just trust it. This is the Leap of Faith and it is what makes recursion feel natural.Step 3 — Express the current problem in terms of smaller problems How does the answer for size n relate to the answer for size n-1 (or n/2, or subtrees)? This relationship is your recursive case.Step 4 — Make sure each call moves toward the base case The input must become strictly smaller with each call. If it does not, you have infinite recursion.Step 5 — Write the base case first, then the recursive case Always. Writing the recursive case first leads to bugs because you have not defined when to stop.Common Mistakes and How to Avoid ThemMistake 1: Missing or wrong base case The most common mistake. Missing the base case causes StackOverflowError. Wrong base case causes wrong answers.Always ask — what is the simplest possible input, and what should the function return for it? Write that case first.Mistake 2: Not moving toward the base case If you call factorial(n) inside factorial(n) without reducing n, you loop forever. Every recursive call must make the problem strictly smaller.Mistake 3: Trusting your brain to trace deep recursion Do not try to trace 10 levels of recursion in your head. Trust the recursive call, verify the base case, and check that each call reduces the problem. That is all you need.Mistake 4: Forgetting to return the recursive result// WRONG — result is computed but not returnedint sum(int n) { if (n == 0) return 0; sum(n - 1) + n; // computed but discarded!}// CORRECTint sum(int n) { if (n == 0) return 0; return sum(n - 1) + n;}Mistake 5: Modifying shared state without backtracking In backtracking problems, if you add something to a list before a recursive call, you must remove it after the call returns. Forgetting to backtrack leads to incorrect results and is one of the trickiest bugs to find.Mistake 6: Recomputing the same subproblems Naive Fibonacci computes fib(3) multiple times when computing fib(5). Use memoization whenever you notice overlapping subproblems in your recursion tree.Top LeetCode Problems on RecursionThese are organized by pattern — work through them in this order for maximum learning:Pure Recursion Basics:509. Fibonacci Number — Easy — start here, implement with and without memoization344. Reverse String — Easy — recursion on arrays206. Reverse Linked List — Easy — recursion on linked list50. Pow(x, n) — Medium — fast power with recursionTree Recursion (Most Important):104. Maximum Depth of Binary Tree — Easy — simplest tree recursion112. Path Sum — Easy — decision recursion on tree101. Symmetric Tree — Easy — mutual recursion on tree110. Balanced Binary Tree — Easy — bottom-up recursion236. Lowest Common Ancestor of a Binary Tree — Medium — classic tree recursion124. Binary Tree Maximum Path Sum — Hard — advanced tree recursionDivide and Conquer:148. Sort List — Medium — merge sort on linked list240. Search a 2D Matrix II — Medium — divide and conquerBacktracking:78. Subsets — Medium — generate all subsets46. Permutations — Medium — generate all permutations77. Combinations — Medium — generate combinations79. Word Search — Medium — backtracking on grid51. N-Queens — Hard — classic backtrackingMemoization / Dynamic Programming:70. Climbing Stairs — Easy — Fibonacci variant with memoization322. Coin Change — Medium — recursion with memoization to DP139. Word Break — Medium — memoized recursionRecursion Cheat Sheet// Linear recursion templatereturnType solve(input) { if (baseCase) return directAnswer; // process current return solve(smallerInput);}// Tree recursion templatereturnType solve(TreeNode root) { if (root == null) return baseValue; returnType left = solve(root.left); returnType right = solve(root.right); return combine(left, right, root.val);}// Backtracking templatevoid backtrack(choices, current, result) { if (goalReached) { result.add(copy of current); return; } for (choice : choices) { make(choice); // add to current backtrack(...); // recurse undo(choice); // remove from current }}// Memoization templateMap<Input, Output> memo = new HashMap<>();returnType solve(input) { if (baseCase) return directAnswer; if (memo.containsKey(input)) return memo.get(input); returnType result = solve(smallerInput); memo.put(input, result); return result;}FAQs — People Also AskQ1. What is recursion in Java with a simple example? Recursion is when a function calls itself to solve a smaller version of the same problem. A simple example is factorial — factorial(5) = 5 × factorial(4) = 5 × 4 × factorial(3) and so on until factorial(1) returns 1 directly.Q2. What is the difference between recursion and iteration? Iteration uses loops (for, while) and runs in O(1) space. Recursion uses function calls and uses O(n) stack space for n levels deep. Recursion is often cleaner for tree and graph problems. Iteration is better when memory is a concern or the problem is inherently linear.Q3. What causes StackOverflowError in Java recursion? StackOverflowError happens when recursion goes too deep — too many frames accumulate on the call stack before any of them return. This is caused by missing base case, wrong base case, or input too large for Java's default stack size limit.Q4. What is the difference between recursion and dynamic programming? Recursion solves a problem by breaking it into subproblems. Dynamic programming is recursion plus memoization — storing results of subproblems so they are never computed twice. DP converts exponential recursive solutions into polynomial ones by eliminating redundant computation.Q5. What is tail recursion and does Java support tail call optimization? Tail recursion is when the recursive call is the absolute last operation in the function. Java does NOT support tail call optimization — Java always creates a new stack frame for each call even if it is tail recursive. Languages like Scala and Kotlin (on the JVM) do support it with the tailrec keyword.Q6. How do you convert recursion to iteration? Every recursive solution can be converted to iterative using an explicit stack data structure. The call stack's behavior is replicated manually — push the initial call, loop while stack is not empty, pop, process, and push sub-calls. Tree traversals are a common example of this conversion.ConclusionRecursion is not magic. It is a systematic way of solving problems by expressing them in terms of smaller versions of themselves. Once you internalize the two parts (base case and recursive case), understand the call stack mentally, and learn to trust the recursive call rather than trace it completely, everything clicks.The learning path from here is clear — start with simple problems like Fibonacci and array sum. Move to tree problems where recursion is most natural. Then tackle backtracking. Finally add memoization to bridge into dynamic programming.Every hour you spend understanding recursion deeply pays dividends across the entire rest of your DSA journey. Trees, graphs, divide and conquer, backtracking, dynamic programming — all of them build on this foundation.

RecursionJavaBase CaseCall StackBacktrackingDynamic Programming
LeetCode 143 Reorder List - Java Solution Explained

LeetCode 143 Reorder List - Java Solution Explained

IntroductionLeetCode 143 Reorder List is one of those problems that looks simple when you read it but immediately makes you wonder — where do I even start? There is no single trick that solves it. Instead it combines three separate linked list techniques into one clean solution. Mastering this problem means you have genuinely understood linked lists at an intermediate level.You can find the problem here — LeetCode 143 Reorder List.This article walks through everything — what the problem wants, the intuition behind each step, all three techniques used, a detailed dry run, complexity analysis, and common mistakes beginners make.What Is the Problem Really Asking?You have a linked list: L0 → L1 → L2 → ... → LnYou need to reorder it to: L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → ...In plain English — take one node from the front, then one from the back, then one from the front, then one from the back, and keep alternating until all nodes are used.Example:Input: 1 → 2 → 3 → 4 → 5Output: 1 → 5 → 2 → 4 → 3Node 1 from front, Node 5 from back, Node 2 from front, Node 4 from back, Node 3 stays in middle.Real Life Analogy — Dealing Cards From Both EndsImagine you have a deck of cards laid out in a line face up: 1, 2, 3, 4, 5. Now you deal them by alternately picking from the left end and the right end of the line:Pick 1 from left → placePick 5 from right → place after 1Pick 2 from left → place after 5Pick 4 from right → place after 2Pick 3 (only one left) → place after 4Result: 1, 5, 2, 4, 3That is exactly what the problem wants. The challenge is doing this efficiently on a singly linked list where you cannot just index from the back.Why This Problem Is Hard for BeginnersIn an array you can just use two pointers — one at the start and one at the end — and swap/interleave easily. But a singly linked list only goes forward. You cannot go backwards. You cannot easily access the last element.This is why the problem requires a three-step approach that cleverly works around the limitations of a singly linked list.The Three Step ApproachEvery experienced developer solves this problem in exactly three steps:Step 1 — Find the middle of the linked list using the Fast & Slow Pointer techniqueStep 2 — Reverse the second half of the linked listStep 3 — Merge the two halves by alternating nodes from eachLet us understand each step deeply before looking at code.Step 1: Finding the Middle — Fast & Slow PointerThe Fast & Slow Pointer technique (also called Floyd's algorithm) uses two pointers moving at different speeds through the list:slow moves one step at a timefast moves two steps at a timeWhen fast reaches the end, slow is exactly at the middle. This works because fast covers twice the distance of slow in the same number of steps.ListNode fast = head;ListNode slow = head;while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next;}// slow is now at the middleFor 1 → 2 → 3 → 4 → 5:Start: slow=1, fast=1Step 1: slow=2, fast=3Step 2: slow=3, fast=5 (fast.next is null, stop)Middle is node 3For 1 → 2 → 3 → 4:Start: slow=1, fast=1Step 1: slow=2, fast=3Step 2: fast.next.next is null, stopslow=2, middle is node 2After finding the middle, we cut the list in two by setting slow.next = null. This disconnects the first half from the second half.Step 2: Reversing the Second HalfOnce we have the second half starting from slow.next, we reverse it. After reversal, what was the last node becomes the first — giving us easy access to the back elements of the original list.public ListNode reverse(ListNode head) { ListNode curr = head; ListNode prev = null; while (curr != null) { ListNode next = curr.next; // save next curr.next = prev; // reverse the link prev = curr; // move prev forward curr = next; // move curr forward } return prev; // prev is the new head}For second half 3 → 4 → 5 (from the first example):Reverse → 5 → 4 → 3Now we have:First half: 1 → 2 → 3 (but 3 is the end since we cut at slow)Wait — actually after cutting at slow=3: first half is 1 → 2 → 3, second half reversed is 5 → 4Let us be precise. For 1 → 2 → 3 → 4 → 5, slow stops at 3. slow.next = null cuts to give:First half: 1 → 2 → 3 → nullSecond half before reverse: 4 → 5Second half after reverse: 5 → 4Step 3: Merging Two HalvesNow we have two lists and we merge them by alternately taking one node from each:Take from first half, take from second half, take from first half, take from second half...ListNode orig = head; // pointer for first halfListNode newhead = second; // pointer for reversed second halfwhile (newhead != null) { ListNode temp1 = orig.next; // save next of first half ListNode temp2 = newhead.next; // save next of second half orig.next = newhead; // first → second newhead.next = temp1; // second → next of first orig = temp1; // advance first half pointer newhead = temp2; // advance second half pointer}Why do we loop on newhead != null and not orig != null? Because the second half is always equal to or shorter than the first half (we cut at middle). Once the second half is exhausted, the remaining first half nodes are already in the correct position.Complete Solutionclass Solution { public ListNode reverse(ListNode head) { ListNode curr = head; ListNode prev = null; while (curr != null) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return prev; } public void reorderList(ListNode head) { // Step 1: Find middle using fast & slow pointer ListNode fast = head; ListNode slow = head; while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } // Step 2: Reverse second half ListNode newhead = reverse(slow.next); slow.next = null; // cut the list into two halves // Step 3: Merge two halves alternately ListNode orig = head; while (newhead != null) { ListNode temp1 = orig.next; ListNode temp2 = newhead.next; orig.next = newhead; newhead.next = temp1; orig = temp1; newhead = temp2; } }}Complete Dry Run — head = [1, 2, 3, 4, 5]Step 1: Find MiddleList: 1 → 2 → 3 → 4 → 5Initial: slow=1, fast=1Iteration 1: slow=2, fast=3Iteration 2: fast.next=4, fast.next.next=5 → slow=3, fast=5fast.next is null → stopslow is at node 3Step 2: Cut and ReverseCut: slow.next = nullFirst half: 1 → 2 → 3 → nullSecond half: 4 → 5Reverse second half 4 → 5:prev=null, curr=4 → next=5, 4.next=null, prev=4, curr=5prev=4, curr=5 → next=null, 5.next=4, prev=5, curr=nullReturn prev=5Reversed second half: 5 → 4 → nullStep 3: Mergeorig=1, newhead=5Iteration 1:temp1 = orig.next = 2temp2 = newhead.next = 4orig.next = newhead → 1.next = 5newhead.next = temp1 → 5.next = 2orig = temp1 = 2newhead = temp2 = 4List so far: 1 → 5 → 2 → 3Iteration 2:temp1 = orig.next = 3temp2 = newhead.next = nullorig.next = newhead → 2.next = 4newhead.next = temp1 → 4.next = 3orig = temp1 = 3newhead = temp2 = nullList so far: 1 → 5 → 2 → 4 → 3newhead is null → loop endsFinal result: 1 → 5 → 2 → 4 → 3 ✅Dry Run — head = [1, 2, 3, 4]Step 1: Find MiddleInitial: slow=1, fast=1Iteration 1: slow=2, fast=3fast.next=4, fast.next.next=null → stopslow is at node 2Step 2: Cut and ReverseFirst half: 1 → 2 → nullSecond half: 3 → 4Reversed: 4 → 3 → nullStep 3: Mergeorig=1, newhead=4Iteration 1:temp1=2, temp2=31.next=4, 4.next=2orig=2, newhead=3List: 1 → 4 → 2 → 3Iteration 2:temp1=null (2.next was originally 3 but we cut at slow=2, so 2.next = null... wait)Actually after cutting at slow=2, first half is 1 → 2 → null, so orig when it becomes 2, orig.next = null.temp1 = orig.next = nulltemp2 = newhead.next = null2.next = 3, 3.next = nullorig = null, newhead = nullnewhead is null → stopFinal result: 1 → 4 → 2 → 3 ✅Why slow.next = null Must Come After Saving newheadThis is a subtle but critical ordering detail in the code. Look at this sequence:ListNode newhead = reverse(slow.next); // save reversed second half FIRSTslow.next = null; // THEN cut the listIf you cut first (slow.next = null) and then try to reverse, you lose the reference to the second half entirely because slow.next is already null. Always save the second half reference before cutting.Time and Space ComplexityTime Complexity: O(n) — each of the three steps (find middle, reverse, merge) makes a single pass through the list. Total is 3 passes = O(3n) = O(n).Space Complexity: O(1) — everything is done by rearranging pointers in place. No extra arrays, no recursion stack, no additional data structures. Just a handful of pointer variables.This is the optimal solution — linear time and constant space.Alternative Approach — Using ArrayList (Simpler but O(n) Space)If you find the three-step approach hard to implement under interview pressure, here is a simpler approach using extra space:public void reorderList(ListNode head) { // store all nodes in ArrayList for random access List<ListNode> nodes = new ArrayList<>(); ListNode curr = head; while (curr != null) { nodes.add(curr); curr = curr.next; } int left = 0; int right = nodes.size() - 1; while (left < right) { nodes.get(left).next = nodes.get(right); left++; if (left == right) break; // odd number of nodes nodes.get(right).next = nodes.get(left); right--; } nodes.get(left).next = null; // terminate the list}This is much easier to understand and code. Store all nodes in an ArrayList, use two pointers from both ends, and wire up the next pointers.Time Complexity: O(n) Space Complexity: O(n) — ArrayList stores all nodesThis is acceptable in most interviews. Mention the O(1) space approach as the optimal solution if asked.Common Mistakes to AvoidNot cutting the list before merging If you do not set slow.next = null after finding the middle, the first half still points into the second half. During merging, this creates cycles and infinite loops. Always cut before merging.Wrong loop condition for finding the middle The condition fast.next != null && fast.next.next != null ensures fast does not go out of bounds when jumping two steps. Using just fast != null && fast.next != null moves slow one step too far for even-length lists.Looping on orig instead of newhead The merge loop should run while newhead != null, not while orig != null. The second half is always shorter or equal to the first half. Once the second half is done, the remaining first half is already correctly placed.Forgetting to save both temp pointers before rewiring In the merge step, you must save both orig.next and newhead.next before changing any pointers. Changing orig.next first and then trying to access orig.next to save it gives you the wrong node.How This Problem Combines Multiple PatternsThis problem is special because it does not rely on a single technique. It is a combination of three fundamental linked list operations:Fast & Slow Pointer — you saw this concept in problems like finding the middle of a list and detecting cycles (LeetCode 141, 142).Reverse a Linked List — the most fundamental linked list operation, appears in LeetCode 206 and as a subtask in dozens of problems.Merge Two Lists — similar to merging two sorted lists (LeetCode 21) but here order is not sorted, it is alternating.Solving this problem proves you are comfortable with all three patterns individually and can combine them when needed.FAQs — People Also AskQ1. What is the most efficient approach for LeetCode 143 Reorder List? The three-step approach — find middle with fast/slow pointer, reverse second half, merge alternately — runs in O(n) time and O(1) space. It is the optimal solution. The ArrayList approach is O(n) time and O(n) space but simpler to code.Q2. Why use fast and slow pointer to find the middle? Because a singly linked list has no way to access elements by index. You cannot just do list[length/2]. The fast and slow pointer technique finds the middle in a single pass without knowing the length beforehand.Q3. Why reverse the second half instead of the first half? The problem wants front-to-back alternation. If you reverse the second half, its first node is the original last node — exactly what you need to interleave with the front of the first half. Reversing the first half would give the wrong order.Q4. What is the time complexity of LeetCode 143? O(n) time for three linear passes (find middle, reverse, merge). O(1) space since all operations are in-place pointer manipulations with no extra data structures.Q5. Is LeetCode 143 asked in coding interviews? Yes, frequently at companies like Amazon, Google, Facebook, and Microsoft. It is considered a benchmark problem for linked list mastery because it requires combining three separate techniques cleanly under pressure.Similar LeetCode Problems to Practice Next206. Reverse Linked List — Easy — foundation for step 2 of this problem876. Middle of the Linked List — Easy — fast & slow pointer isolated21. Merge Two Sorted Lists — Easy — merging technique foundation234. Palindrome Linked List — Easy — also uses find middle + reverse second half148. Sort List — Medium — merge sort on linked list, uses same split techniqueConclusionLeetCode 143 Reorder List is one of the best Medium linked list problems because it forces you to think in multiple steps and combine techniques rather than apply a single pattern. The fast/slow pointer finds the middle efficiently without knowing the length. Reversing the second half turns the "cannot go backwards" limitation of singly linked lists into a non-issue. And the alternating merge weaves everything together cleanly.Work through the dry runs carefully — especially the pointer saving step in the merge. Once you see why each step is necessary and how they connect, this problem will always feel approachable no matter when it shows up in an interview.

LeetCodeJavaLinked ListTwo PointerFast Slow PointerMedium
LeetCode 123 — Best Time to Buy and Sell Stock III | At Most Two Transactions Explained

LeetCode 123 — Best Time to Buy and Sell Stock III | At Most Two Transactions Explained

🚀 Try This Problem First!Before reading the solution, attempt it yourself on LeetCode — you will learn much more that way.🔗 Problem Link: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/What Is the Problem Asking?You have a list of stock prices, one for each day. You are allowed to buy and sell the stock, but with one important restriction — you can make at most two transactions in total. A transaction means one buy followed by one sell.You cannot hold two stocks at the same time, meaning you must sell whatever you are holding before you buy again.Your job is to find the maximum profit you can make by doing at most two transactions. If there is no way to make any profit, return 0.Example to build intuition: prices = [3, 3, 5, 0, 0, 3, 1, 4]If you buy on day 4 (price 0) and sell on day 6 (price 3), profit = 3. Then buy on day 7 (price 1) and sell on day 8 (price 4), profit = 3. Total profit = 6. That is the best you can do.Constraints:1 ≤ prices.length ≤ 10⁵0 ≤ prices[i] ≤ 10⁵Why Is This Harder Than the Previous Stock Problems?In LeetCode 121, you could only make one transaction — so you just found the best single buy-sell pair.In LeetCode 122, you could make unlimited transactions — so you greedily collected every upward price movement.Here, you are limited to exactly two transactions. Greedy does not work anymore because sometimes skipping a small profit early on allows you to make a much bigger combined profit across two trades. You need a smarter strategy.The Big Idea Behind the SolutionHere is a simple way to think about it.If you are allowed two transactions, you can imagine drawing a dividing line somewhere in the price array. The first transaction happens entirely on the left side of that line. The second transaction happens entirely on the right side.So the problem becomes:For every possible dividing position, find the best profit on the left side and the best profit on the right side.Add them together.The maximum sum across all dividing positions is your answer.The challenge is doing this efficiently without checking every position with a nested loop, which would be O(N²) and too slow.Approach 1 — Left-Right Prefix DPThe idea:Instead of trying every split point from scratch, precompute the answers in advance.Build a leftProfit array where leftProfit[i] stores the best single transaction profit you can make using prices from day 0 to day i.Build a rightProfit array where rightProfit[i] stores the best single transaction profit you can make using prices from day i to the last day.Then for every index i, the answer if you split at i is simply leftProfit[i] + rightProfit[i]. Take the maximum across all indices.How to build leftProfit:Go left to right. Keep track of the minimum price seen so far. At each day i, the best profit you could make ending at or before day i is either the same as the day before, or selling today at today's price minus the minimum price seen so far. Take whichever is larger.How to build rightProfit:Go right to left. Keep track of the maximum price seen so far from the right. At each day i, the best profit you could make starting at or after day i is either the same as the day after, or buying today at today's price and selling at the maximum price seen to the right. Take whichever is larger.Dry Run — prices = [3, 3, 5, 0, 0, 3, 1, 4]Building leftProfit left to right, tracking minimum price seen:Day 0 → min=3, leftProfit[0] = 0 (nothing to compare yet) Day 1 → min=3, best profit = 3-3 = 0, leftProfit[1] = max(0, 0) = 0 Day 2 → min=3, best profit = 5-3 = 2, leftProfit[2] = max(0, 2) = 2 Day 3 → min=0, best profit = 0-0 = 0, leftProfit[3] = max(2, 0) = 2 Day 4 → min=0, best profit = 0-0 = 0, leftProfit[4] = max(2, 0) = 2 Day 5 → min=0, best profit = 3-0 = 3, leftProfit[5] = max(2, 3) = 3 Day 6 → min=0, best profit = 1-0 = 1, leftProfit[6] = max(3, 1) = 3 Day 7 → min=0, best profit = 4-0 = 4, leftProfit[7] = max(3, 4) = 4leftProfit = [0, 0, 2, 2, 2, 3, 3, 4]Building rightProfit right to left, tracking maximum price seen:Day 7 → max=4, rightProfit[7] = 0 (nothing to compare yet) Day 6 → max=4, best profit = 4-1 = 3, rightProfit[6] = max(0, 3) = 3 Day 5 → max=4, best profit = 4-3 = 1, rightProfit[5] = max(3, 1) = 3 Day 4 → max=4, best profit = 4-0 = 4, rightProfit[4] = max(3, 4) = 4 Day 3 → max=4, best profit = 4-0 = 4, rightProfit[3] = max(4, 4) = 4 Day 2 → max=5, best profit = 5-5 = 0, rightProfit[2] = max(4, 0) = 4 Day 1 → max=5, best profit = 5-3 = 2, rightProfit[1] = max(4, 2) = 4 Day 0 → max=5, best profit = 5-3 = 2, rightProfit[0] = max(4, 2) = 4rightProfit = [4, 4, 4, 4, 4, 3, 3, 0]Combining at each index: Index 0 → 0 + 4 = 4 Index 1 → 0 + 4 = 4 Index 2 → 2 + 4 = 6 Index 3 → 2 + 4 = 6 Index 4 → 2 + 4 = 6 Index 5 → 3 + 3 = 6 Index 6 → 3 + 3 = 6 Index 7 → 4 + 0 = 4Maximum = 6 ✅Implementation code:class Solution { public int maxProfit(int[] prices) { int lefProf[] = new int[prices.length]; int rigProf[] = new int[prices.length]; int j = 1; int i = 0; int coL = 1; while (i < j && j < prices.length) { if (prices[i] > prices[j]) { i = j; if (coL - 1 != -1) { lefProf[coL] = lefProf[coL - 1]; } coL++; } else { int max = prices[j] - prices[i]; if (coL - 1 != -1) { lefProf[coL] = Math.max(lefProf[coL - 1], max); coL++; } else { lefProf[coL] = max; coL++; } } j++; } int ii = prices.length - 2; int jj = prices.length - 1; int coR = prices.length - 2; while (ii >= 0) { if (prices[ii] > prices[jj]) { jj = ii; if (coR + 1 < prices.length) { rigProf[coR] = rigProf[coR + 1]; } coR--; } else { int max = prices[jj] - prices[ii]; if (coR + 1 < prices.length) { rigProf[coR] = Math.max(rigProf[coR + 1], max); coR--; } else { rigProf[coR] = max; coR--; } } ii--; } int maxAns = 0; for (int k = 0; k < lefProf.length; k++) { maxAns = Math.max(maxAns, lefProf[k] + rigProf[k]); } return maxAns; }}The approach here is exactly right. You are building the left and right profit arrays using a two pointer scanning technique — the same idea as LeetCode 121 but applied twice, once going forward and once going backward. The manual counters coL and coR make it a bit harder to follow, but the logic underneath is solid.Here is the same approach written in a cleaner way so it is easier to read and debug:class Solution { public int maxProfit(int[] prices) { int n = prices.length; int[] leftProfit = new int[n]; int[] rightProfit = new int[n]; int minPrice = prices[0]; for (int i = 1; i < n; i++) { minPrice = Math.min(minPrice, prices[i]); leftProfit[i] = Math.max(leftProfit[i - 1], prices[i] - minPrice); } int maxPrice = prices[n - 1]; for (int i = n - 2; i >= 0; i--) { maxPrice = Math.max(maxPrice, prices[i]); rightProfit[i] = Math.max(rightProfit[i + 1], maxPrice - prices[i]); } int maxProfit = 0; for (int i = 0; i < n; i++) { maxProfit = Math.max(maxProfit, leftProfit[i] + rightProfit[i]); } return maxProfit; }}Time Complexity: O(N) — three separate linear passes through the array. Space Complexity: O(N) — two extra arrays of size N.Approach 2 — Four Variable State Machine DPThe idea:Instead of building arrays, what if you could track everything in just four variables and solve it in a single pass?Think about what is happening at any point in time as you go through the prices. You are always in one of these situations:You have bought your first stock and are currently holding it. You have sold your first stock and are sitting on that profit. You have bought your second stock (using the profit from the first sale) and are holding it. You have sold your second stock and collected your total profit.These four situations are your four states. For each one, you always want to know — what is the best possible profit I could have in this state up to today?Here is how each state updates as you look at a new price:buy1 — This represents the best outcome after buying the first stock. Buying costs money, so this is negative. As you see each new price, you ask: is it better to have bought on some earlier day, or to buy today? You keep whichever gives you the least cost, which means the highest value of negative price.sell1 — This represents the best profit after completing the first transaction. As you see each new price, you ask: is it better to have sold on some earlier day, or to sell today using the best buy1 we have? Selling today means adding today's price on top of buy1.buy2 — This represents the best outcome after buying the second stock. The key insight here is that you are not starting from zero — you already have the profit from the first transaction in your pocket. So buying the second stock costs today's price but you subtract it from sell1. As you see each new price, you ask: is it better to have bought the second stock earlier, or to buy today using the profit from sell1?sell2 — This is your final answer. It represents the best total profit after completing both transactions. As you see each new price, you ask: is it better to have completed both transactions earlier, or to sell the second stock today using the best buy2 we have?The beautiful thing is that all four updates happen in order on every single day, in one pass through the array. Each variable feeds into the next — buy1 feeds sell1, sell1 feeds buy2, buy2 feeds sell2.Dry Run — prices = [3, 3, 5, 0, 0, 3, 1, 4]We start with buy1 and buy2 as very negative (not yet bought anything) and sell1 and sell2 as 0 (no profit yet).Day 0, price = 3: buy1 = max(-∞, -3) = -3. Best cost to buy first stock is spending 3. sell1 = max(0, -3+3) = 0. Selling immediately gives no profit. buy2 = max(-∞, 0-3) = -3. Best cost to buy second stock after first sale is 3. sell2 = max(0, -3+3) = 0. No total profit yet.Day 1, price = 3: Nothing changes — same price as day 0.Day 2, price = 5: buy1 = max(-3, -5) = -3. Still best to have bought at price 3. sell1 = max(0, -3+5) = 2. Selling today at 5 after buying at 3 gives profit 2. buy2 = max(-3, 2-5) = -3. Buying second stock today costs too much relative to first profit. sell2 = max(0, -3+5) = 2. Completing both trades gives 2 so far.Day 3, price = 0: buy1 = max(-3, -0) = 0. Buying today at price 0 is the best possible first buy. sell1 = max(2, 0+0) = 2. Selling today at 0 gives nothing — keep the 2 from before. buy2 = max(-3, 2-0) = 2. Using the profit of 2 and buying today at 0 gives buy2 = 2. sell2 = max(2, 2+0) = 2. Selling second stock today at 0 gives nothing extra.Day 4, price = 0: Nothing changes — same price as day 3.Day 5, price = 3: buy1 = max(0, -3) = 0. Still best to have bought at price 0. sell1 = max(2, 0+3) = 3. Selling today at 3 after buying at 0 gives profit 3. buy2 = max(2, 3-3) = 2. Buying second stock today at 3 using sell1=3 gives 0. Keep 2. sell2 = max(2, 2+3) = 5. Selling second stock today at 3 after buy2=2 gives total 5.Day 6, price = 1: buy1 = max(0, -1) = 0. Still best to have bought at 0. sell1 = max(3, 0+1) = 3. Selling today at 1 gives less than 3 — no change. buy2 = max(2, 3-1) = 2. Buying second stock at 1 using sell1=3 gives 2 — same as before. sell2 = max(5, 2+1) = 5. No improvement yet.Day 7, price = 4: buy1 = max(0, -4) = 0. No change. sell1 = max(3, 0+4) = 4. Selling today at 4 after buying at 0 gives profit 4 — new best. buy2 = max(2, 4-4) = 2. No improvement. sell2 = max(5, 2+4) = 6. Selling second stock today at 4 with buy2=2 gives total 6 — new best.Output: 6 ✅class Solution { public int maxProfit(int[] prices) { int buy1 = Integer.MIN_VALUE; int sell1 = 0; int buy2 = Integer.MIN_VALUE; int sell2 = 0; for (int price : prices) { buy1 = Math.max(buy1, -price); sell1 = Math.max(sell1, buy1 + price); buy2 = Math.max(buy2, sell1 - price); sell2 = Math.max(sell2, buy2 + price); } return sell2; }}Time Complexity: O(N) — single pass through the array. Space Complexity: O(1) — only four integer variables, no extra arrays.Approach 3 — Generalizing to At Most K TransactionsOnce you understand Approach 2, there is a natural question — what if instead of two transactions, you were allowed K transactions? That is exactly LeetCode 188.Instead of four hardcoded variables, you use two arrays of size K. buy[j] tracks the best outcome after the j-th buy and sell[j] tracks the best outcome after the j-th sell. The logic is identical to Approach 2, just in a loop.For this problem set K = 2 and it gives the same answer:class Solution { public int maxProfit(int[] prices) { int k = 2; int[] buy = new int[k]; int[] sell = new int[k]; Arrays.fill(buy, Integer.MIN_VALUE); for (int price : prices) { for (int j = 0; j < k; j++) { buy[j] = Math.max(buy[j], (j == 0 ? 0 : sell[j - 1]) - price); sell[j] = Math.max(sell[j], buy[j] + price); } } return sell[k - 1]; }}Time Complexity: O(N × K) — for K=2 this is effectively O(N). Space Complexity: O(K) — two small arrays of size K.If you understand this, LeetCode 188 becomes a five minute problem.Comparing All Three ApproachesApproach 1 — Left-Right Prefix DP (Your Approach)You build two arrays — one scanning left to right, one scanning right to left — and combine them. This is the most visual and intuitive approach. It is easy to explain because you are essentially solving LeetCode 121 twice from opposite ends and adding the results. The downside is it uses O(N) extra space for the two arrays.Approach 2 — Four Variable State MachineYou solve the entire problem in a single pass using just four variables. No arrays, no extra memory. This is the most efficient and elegant solution. Once you understand what each variable represents, the code almost writes itself. This is what most interviewers are hoping to see.Approach 3 — General K-Transaction DPThis is Approach 2 extended to handle any number of transactions using arrays instead of hardcoded variables. Solving LeetCode 123 with this approach means you have already solved LeetCode 188 as a bonus.Mistakes That Catch Most PeopleTrying to use the greedy approach from LeetCode 122: Adding every upward price movement does not work when you are limited to two transactions. You need to pick the two best non-overlapping windows, not collect everything.Buying twice without selling in between: The state machine prevents this naturally — buy2 depends on sell1, so you can never enter the second buy before completing the first sell.Initializing buy variables to 0: Both buy1 and buy2 must start at Integer.MIN_VALUE. Setting them to 0 implies you bought a stock for free, which is wrong and will give inflated profits.Returning the wrong variable: Always return sell2, not buy2 or sell1. sell2 is the only variable that represents a completed two-transaction profit.Where This Fits in the Stock SeriesLeetCode 121 — One transaction only. Find the single best buy-sell pair. [ Blog is also avaliable on this - Read Now ]LeetCode 122 — Unlimited transactions. Collect every upward price movement greedily. [ Blog is also avaliable on this - Read Now ]LeetCode 188 — At most K transactions. Direct extension of Approach 3 above.LeetCode 309 — Unlimited transactions but with a cooldown day after every sale.LeetCode 714 — Unlimited transactions but with a fee charged on every transaction.Each problem adds one new constraint on top of the previous one. If you understand LeetCode 123 deeply — especially Approach 2 — every other problem in this series becomes a small modification of the same framework.Key Takeaways✅ When you are limited to two transactions, greedy fails. You need to find two non-overlapping windows that together give maximum profit.✅ The left-right prefix DP approach is the most intuitive — precompute the best single transaction from both ends, then combine at every split point.✅ The four variable state machine solves it in one pass with O(1) space. Each variable tracks the best outcome for one state — buy1 feeds sell1, sell1 feeds buy2, buy2 feeds sell2.✅ Always initialize buy variables to Integer.MIN_VALUE to represent "not yet bought anything."✅ Understanding Approach 3 here means LeetCode 188 is already solved — just change the hardcoded 2 to K.Happy Coding! This problem is the turning point in the stock series where DP becomes unavoidable — once you crack it, the rest of the series feels manageable. 🚀

LeetCodeDynamic ProgrammingHardJavaDSAPrefix DPArraysStock Series
LeetCode 2265: Count Nodes Equal to Average of Subtree – Java DFS Solution

LeetCode 2265: Count Nodes Equal to Average of Subtree – Java DFS Solution

IntroductionBinary tree problems often become interesting when the answer for a node depends on everything below that node.This problem is exactly that.For every node, the task is to calculate the average of all values in its subtree and check whether that average is equal to the value of the node itself.At first, this looks like a simple DFS problem. But there is an important optimization opportunity hidden inside it.To calculate the average of a subtree, two things are required:The sum of all nodes in the subtreeThe number of nodes in the subtreeThe first approach calculates these values directly. Then, by looking at the repeated work, the solution can be improved further.Question LinkLeetCode 2265 – Count Nodes Equal to Average of SubtreeUnderstanding the ProblemFor every node:Average = Subtree Sum / Number of NodesThe average is rounded down.If the calculated average is equal to the current node's value, that node is counted.Consider this tree: 4 / \ 8 5 / \ \ 0 1 6For node 5:Subtree = [5, 6]Sum = 5 + 6 = 11Count = 2Average = 11 / 2 = 5Since the average is 5, node 5 contributes to the answer.The same calculation has to be performed for every node.The Core ObservationFor each node, the required information is:Subtree Sum+Subtree Node CountOnce these two values are available, calculating the average is straightforward.The natural question is:How can the sum and count of every subtree be calculated efficiently?A recursive DFS is a natural fit because the information of a subtree can be obtained by processing its children first.Approach 1: Calculate Subtree Sum and Count SeparatelyThe first approach is the most direct way to think about the problem.For every node:Calculate the sum of its entire subtree.Calculate the number of nodes in that subtree.Calculate the average.Compare it with the current node.Repeat for the left and right children.A separate count() function can be used to count the nodes.This approach follows the problem statement very closely, which makes it a good starting point for understanding the solution.CodeHere is the complete approach, including the separate count() idea:class Solution { int co = 0; // Calculates the sum of all nodes in the subtree public int sumdfs(TreeNode roo) { if (roo == null) { return 0; } // Leaf node contributes its value to the sum if (roo.left == null && roo.right == null) { co++; return roo.val; } int left = sumdfs(roo.left); int right = sumdfs(roo.right); // Current node is also part of its subtree co++; return roo.val + left + right; } // Separate function to count the number of nodes // in the subtree. // // This was the initial idea, but it is commented out // because the sumdfs() traversal can already count nodes. // // public int count(TreeNode roo){ // if(roo == null){ // return 0; // } // if(roo.left == null && roo.right == null){ // return 1; // } // int left = count(roo.left); // int right = count(roo.right); // return 1 + left + right; // } int am = 0; public void solve(TreeNode root) { if (root == null) { return; } // Reset count for the current subtree co = 0; // Calculate the sum of the current subtree // and count its nodes at the same time. int tot = sumdfs(root); int cot = co; // The problem requires the average to be rounded down. if (Math.floor(tot / cot) == root.val) { am++; } // Check the left subtree solve(root.left); // Check the right subtree solve(root.right); } public int averageOfSubtree(TreeNode root) { solve(root); return am; }}How This Approach WorksThe interesting part is sumdfs().Initially, a separate count() function can be used:int cot = count(root);At the same time, another traversal calculates the sum.But there is no need to traverse the same subtree again just to count its nodes.The sumdfs() function is already visiting every node, so the counter can be increased during the same traversal.That is why the following idea works:co++;Whenever a node is visited, co increases by one.After sumdfs(root) finishes:int tot = sumdfs(root);int cot = co;Now:tot = sum of subtreecot = number of nodes in subtreeand the average can be calculated.Dry RunConsider: 4 / \ 8 5 / \ \ 0 1 6Start from node 4.Node 0sum = 0count = 1average = 0 / 1 = 0So node 0 is counted.Node 1sum = 1count = 1average = 1 / 1 = 1Node 1 is counted.Node 8Its subtree is: 8 / \ 0 1Therefore:sum = 8 + 0 + 1 = 9count = 3average = 9 / 3 = 3Since:8 != 3node 8 is not counted.Node 6sum = 6count = 1average = 6Node 6 is counted.Node 5Its subtree is: 5 \ 6So:sum = 5 + 6 = 11count = 2average = 11 / 2 = 5Node 5 is counted.Node 4The complete subtree is: 4 / \ 8 5 / \ \ 0 1 6Therefore:sum = 4 + 8 + 5 + 0 + 1 + 6 = 24count = 6average = 24 / 6 = 4Node 4 is counted.The final answer is:5An Important Complexity ObservationThe co variable removes the need for a separate count() traversal.However, there is still a bigger issue.Look at what happens when solve() processes the root.It calls:sumdfs(root);which visits the entire tree.Then solve() moves to the left child and calls:sumdfs(root.left);which visits that subtree again.Then the same thing happens for the right subtree.So the same nodes can be visited many times.For example, in a skewed tree:1 \ 2 \ 3 \ 4 \ 5The first call processes:1 → 2 → 3 → 4 → 5The second processes:2 → 3 → 4 → 5The third:3 → 4 → 5and so on.Therefore, although the co optimization is cleaner than using a separate count() traversal, the overall worst-case complexity can still reach:O(n²)This observation leads directly to the optimal solution.Approach 2: Calculate Everything During One Postorder TraversalInstead of recalculating every subtree, the better idea is:Calculate the subtree information once and return it to the parent.For every node, the DFS can return two values:sumcountSuppose a node has:left subtree → (leftSum, leftCount)right subtree → (rightSum, rightCount)Then the current node can calculate:sum = root.val + leftSum + rightSumand:count = 1 + leftCount + rightCountNow the average is immediately available.After checking the current node, the pair:(sum, count)is returned to its parent.This means every node is processed exactly once.Why Postorder Traversal?The traversal order is:Left → Right → RootThis is called postorder traversal.It fits perfectly because the parent needs information from its children before it can calculate its own subtree information.For example: 4 / \ 8 5The algorithm first calculates information for:85and then combines them at:4This is a very common pattern in tree problems.Optimal Java SolutionA small Pair class can be used to return both the subtree sum and node count together.class Solution { int ans = 0; class Pair { int sum; int count; Pair(int sum, int count) { this.sum = sum; this.count = count; } } public Pair dfs(TreeNode root) { // Empty subtree if (root == null) { return new Pair(0, 0); } // Get sum and count from left subtree Pair left = dfs(root.left); // Get sum and count from right subtree Pair right = dfs(root.right); // Calculate information for current subtree int sum = root.val + left.sum + right.sum; int count = 1 + left.count + right.count; // Calculate subtree average int average = sum / count; // Check if current node equals its subtree average if (average == root.val) { ans++; } // Return current subtree information to parent return new Pair(sum, count); } public int averageOfSubtree(TreeNode root) { dfs(root); return ans; }}Why This Solution Is OptimalConsider node 4.Instead of recalculating its subtree from scratch, the algorithm receives:From 8:sum = 9count = 3and:From 5:sum = 11count = 2Then:sum = 4 + 9 + 11 = 24and:count = 1 + 3 + 2 = 6Therefore:average = 24 / 6 = 4The information is calculated once and passed upward.No subtree needs to be recalculated.Complexity AnalysisApproach 1The sumdfs() traversal can be repeated for every node.Time ComplexityO(n²)in the worst case.Space ComplexityThe recursion stack depends on the height of the tree:O(h)Approach 2: One Postorder DFSEvery node is visited exactly once.At each node, only constant-time operations are performed.Time ComplexityO(n)Space ComplexityO(h)where h is the height of the tree.For a balanced tree:O(log n)For a skewed tree:O(n)A Small Java ImprovementThere is also a small simplification in the original code.The following expression:Math.floor(tot / cot)is unnecessary here.Because tot and cot are integers:tot / cotalready performs integer division.For example:11 / 2 = 5which is exactly the required rounded-down result because all node values are non-negative.Therefore, the comparison can simply be:if (tot / cot == root.val)The optimal solution uses:int average = sum / count;which is cleaner and easier to read.Approach ComparisonApproachMain IdeaTimeSpaceSeparate sum + countCalculate subtree information directlyO(n²) worst caseO(h)Global co counterCount nodes during sum traversalO(n²) worst caseO(h)One postorder DFSReturn sum + count to parentO(n)O(h)The second approach is a useful improvement over the first, but the third approach is the real optimization.What Can Be Learned From This Problem?This problem teaches an important binary-tree pattern:If a parent needs information about its entire subtree, calculate that information from the results returned by its children.Instead of repeatedly asking:"What is the sum of this subtree?""What is the count of this subtree?"the children calculate their information once and return it.The parent simply combines the results.This pattern appears in many tree problems involving:Subtree sumsSubtree sizesTree heightDiameterBalanced tree checkingMaximum path calculationsCounting nodes satisfying a conditionTree dynamic programmingA useful mental template is:Result dfs(Node root) { if (root == null) { return baseResult; } Result left = dfs(root.left); Result right = dfs(root.right); // Combine left + right + current node return result;}Once this pattern becomes familiar, many seemingly complicated tree problems become much easier to approach.Interview TipIf an interviewer asks for the straightforward solution first, it is perfectly reasonable to start with the direct recursive approach.But after getting it working, look for repeated subtree calculations.A strong follow-up thought process is:Am I visiting the same subtree multiple times? ↓Can I calculate its information once? ↓Can I return that information to the parent? ↓Postorder DFSThis is often the difference between an acceptable recursive solution and an optimal tree solution.ConclusionThe straightforward approach is a natural way to solve this problem: for each node, calculate the sum and number of nodes in its subtree, find the average, and check whether it matches the node's value.The first improvement is to notice that the same DFS used for calculating the sum can also maintain the node count using a shared counter instead of running a separate count() traversal.However, the bigger optimization comes from noticing that subtrees are still being recalculated.The optimal solution solves this by using one postorder DFS. Each node receives the sum and count from its children, calculates its own subtree information, checks the average, and passes the result to its parent.This changes the worst-case time complexity from:O(n²)to:O(n)The most valuable takeaway is the pattern behind the solution:When a tree problem asks for information about a subtree, try to calculate that information once and return it upward through postorder DFS.

LeetCodeJavaBinary TreeDFSRecursionPostorder TraversalMedium
LeetCode 784 Letter Case Permutation | Recursion & Backtracking Java Solution

LeetCode 784 Letter Case Permutation | Recursion & Backtracking Java Solution

IntroductionThe Letter Case Permutation problem is a classic example of recursion and backtracking, often asked in coding interviews and frequently searched by learners preparing for platforms like LeetCode.This problem helps in understanding:Decision-making at each stepRecursive branchingString manipulationIn this article, we’ll break down the intuition, visualize the decision process using your decision tree, and implement an efficient Java solution.🔗 Problem LinkLeetCode: Letter Case PermutationProblem StatementGiven a string s, you can transform each alphabet character into:LowercaseUppercaseDigits remain unchanged.👉 Return all possible strings formed by these transformations.ExamplesExample 1Input:s = "a1b2"Output:["a1b2","a1B2","A1b2","A1B2"]Example 2Input:s = "3z4"Output:["3z4","3Z4"]Key InsightAt each character:If it's a digit → only one choiceIf it's a letter → two choices:lowercase OR uppercaseSo total combinations:2^(number of letters)Intuition (Using Your Decision Tree)For input: "a1b2"Start from index 0: "" / \ "a" "A" | | "a1" "A1" / \ / \ "a1b" "a1B" "A1b" "A1B" | | | | "a1b2" "a1B2" "A1b2" "A1B2"Understanding the TreeAt 'a' → branch into 'a' and 'A''1' → no branching (digit)'b' → again branching'2' → no branching📌 Leaf nodes = final answersApproach: Recursion + BacktrackingIdeaTraverse the string character by characterIf digit → move forwardIf letter → branch into:lowercaseuppercaseJava Codeimport java.util.*;class Solution { // List to store all results List<String> lis = new ArrayList<>(); public void solve(String s, int ind, String ans) { // Base case: reached end of string if (ind == s.length()) { lis.add(ans); // store generated string return; } char ch = s.charAt(ind); // If character is a digit → only one option if (ch >= '0' && ch <= '9') { solve(s, ind + 1, ans + ch); } else { // Choice 1: convert to lowercase solve(s, ind + 1, ans + Character.toLowerCase(ch)); // Choice 2: convert to uppercase solve(s, ind + 1, ans + Character.toUpperCase(ch)); } } public List<String> letterCasePermutation(String s) { solve(s, 0, ""); // start recursion return lis; }}Step-by-Step ExecutionFor "a1b2":Start → ""'a' → "a", "A"'1' → "a1", "A1"'b' → "a1b", "a1B", "A1b", "A1B"'2' → final stringsComplexity AnalysisTime Complexity: O(2^n)(n = number of letters)Space Complexity: O(2^n)(for storing results)Why This Approach WorksRecursion explores all possibilitiesEach letter creates a branching pointDigits pass through unchangedBacktracking ensures all combinations are generatedKey TakeawaysThis is a binary decision recursion problemLetters → 2 choicesDigits → 1 choiceDecision tree directly maps to recursionPattern similar to:SubsetsPermutations with conditionsWhen This Problem Is AskedCommon in:Coding interviewsRecursion/backtracking roundsString manipulation problemsConclusionThe Letter Case Permutation problem is a perfect example of how recursion can be used to explore all possible combinations efficiently.Once the decision tree is clear, the implementation becomes straightforward. This pattern is widely used in many advanced problems, making it essential to master.Frequently Asked Questions (FAQs)1. Why don’t digits create branches?Because they have only one valid form.2. What is the main concept used?Recursion with decision-making (backtracking).3. Can this be solved iteratively?Yes, using BFS or iterative expansion, but recursion is more intuitive.

LeetCodeMediumJavaRecursion
LeetCode 1980: Find Unique Binary String – Multiple Ways to Generate a Missing Binary Combination

LeetCode 1980: Find Unique Binary String – Multiple Ways to Generate a Missing Binary Combination

Try the ProblemYou can solve the problem here:https://leetcode.com/problems/find-unique-binary-string/Problem DescriptionYou are given an array nums containing n unique binary strings, where each string has length n.Your task is to return any binary string of length n that does not appear in the array.Important ConditionsEach string consists only of '0' and '1'.Every string in the array is unique.The output must be a binary string of length n.If multiple valid answers exist, any one of them is acceptable.ExamplesExample 1Inputnums = ["01","10"]Output"11"ExplanationPossible binary strings of length 2:00011011Since "01" and "10" are already present, valid answers could be:00 or 11Example 2Inputnums = ["00","01"]Output"11"Another valid output could be:10Example 3Inputnums = ["111","011","001"]Output101Other valid answers include:000010100110Constraintsn == nums.length1 <= n <= 16nums[i].length == nnums[i] consists only of '0' and '1'All strings in nums are uniqueImportant ObservationThe total number of binary strings of length n is:2^nBut the array contains only:n stringsSince 2^n grows very quickly and n ≤ 16, there are many possible binary strings missing from the array. Our goal is simply to construct one of those missing strings.Thinking About the ProblemBefore jumping into coding, it's useful to think about different strategies that could help us generate a binary string that does not appear in the array.Possible Ways to Think About the ProblemWhen approaching this problem, several ideas may come to mind:Generate all possible binary strings of length n and check which one is missing.Store all strings in a HashSet or HashMap and construct a candidate string to verify whether it exists.Manipulate existing strings by flipping bits to create new combinations.Use a mathematical trick that guarantees the new string is different from every string in the list.Each of these approaches leads to a different solution strategy.In this article, we will walk through these approaches and understand how they work.Approach 1: Brute Force – Generate All Binary StringsIdeaThe simplest idea is to generate every possible binary string of length n and check whether it exists in the given array.Since there are:2^n possible binary stringsWe can generate them one by one and return the first string that does not appear in nums.StepsConvert numbers from 0 to (2^n - 1) into binary strings.Pad the binary string with leading zeros so its length becomes n.Check if that string exists in the array.If not, return it.Time ComplexityO(2^n * n)This works because n is at most 16, but it is still not the most elegant approach.Approach 2: HashMap + Bit Flipping (My Approach)IdeaWhile solving this problem, another idea is to store all given binary strings inside a HashMap for quick lookup.Then we can try to construct a new binary string by flipping bits from the existing strings.The intuition is simple:If the current character is '0', change it to '1'.If the current character is '1', change it to '0'.By flipping bits at different positions, we attempt to build a new binary combination.Once the string is constructed, we check whether it already exists in the map.If the generated string does not exist, we return it as our answer.Java Implementation (My Solution)class Solution { public String findDifferentBinaryString(String[] nums) { int len = nums[0].length(); // HashMap to store all given binary strings HashMap<String, Integer> mp = new HashMap<>(); for(int i = 0; i < nums.length; i++){ mp.put(nums[i], i); } int cou = 0; String ans = ""; for(int i = 0; i < nums.length; i++){ if(cou < len){ // Flip the current bit if(nums[i].charAt(cou) == '0'){ ans += '1'; cou++; } else{ ans += '0'; cou++; } }else{ // If generated string does not exist in map if(!mp.containsKey(ans)){ return ans; } // Reset and try building again ans = ""; cou = 0; } } return ans; }}Time ComplexityO(n²)Because we iterate through the array and perform string operations.Space ComplexityO(n)For storing the strings in the HashMap.Approach 3: Cantor’s Diagonalization (Optimal Solution)IdeaA clever mathematical observation allows us to construct a string that must differ from every string in the array.We build a new string such that:The first character differs from the first string.The second character differs from the second string.The third character differs from the third string.And so on.By ensuring that the generated string differs from each string at least at one position, it is guaranteed not to exist in the array.This technique is known as Cantor’s Diagonalization.Java Implementationclass Solution { public String findDifferentBinaryString(String[] nums) { int n = nums.length; StringBuilder result = new StringBuilder(); for(int i = 0; i < n; i++){ // Flip the diagonal bit if(nums[i].charAt(i) == '0'){ result.append('1'); } else{ result.append('0'); } } return result.toString(); }}Time ComplexityO(n)We only traverse the array once.Space ComplexityO(n)For storing the resulting string.Comparison of ApproachesApproachTime ComplexitySpace ComplexityNotesBrute ForceO(2^n * n)O(n)Simple but inefficientHashMap + Bit FlippingO(n²)O(n)Constructive approachCantor DiagonalizationO(n)O(n)Optimal and elegantKey TakeawaysThis problem highlights an interesting concept in algorithm design:Sometimes the best solution is not searching for the answer but constructing one directly.By understanding the structure of the input, we can generate a result that is guaranteed to be unique.ConclusionThe Find Unique Binary String problem can be solved using multiple strategies, ranging from brute force enumeration to clever mathematical construction.While brute force works due to the small constraint (n ≤ 16), more elegant solutions exist. Using hashing or constructive approaches improves efficiency and demonstrates deeper algorithmic thinking.Among all approaches, the Cantor Diagonalization technique provides the most efficient and mathematically guaranteed solution.Understanding problems like this helps strengthen skills in string manipulation, hashing, and constructive algorithms, which are commonly tested in coding interviews.

Binary StringsHashingCantor DiagonalizationLeetCodeMedium
LeetCode 110: Balanced Binary Tree – Java Optimized DFS Solution Explained

LeetCode 110: Balanced Binary Tree – Java Optimized DFS Solution Explained

IntroductionLeetCode 110 – Balanced Binary Tree is one of the most important binary tree interview problems.This problem teaches:Tree recursionHeight calculationDepth First Search (DFS)Bottom-up recursionOptimization techniquesIt is frequently asked in coding interviews because it checks whether you can:Traverse trees efficientlyAvoid repeated calculationsCombine recursion with conditionsOptimize brute force tree solutionsThis problem is also a foundation for advanced tree problems like:AVL TreesHeight-balanced treesDiameter of Binary TreeTree DP problemsProblem Link🔗 https://leetcode.com/problems/balanced-binary-tree/Problem StatementGiven the root of a binary tree:Return:trueif the tree is:height-balancedOtherwise return:falseWhat is a Balanced Binary Tree?A binary tree is balanced if:For every node,|height(left subtree) - height(right subtree)| <= 1Meaning:Left and right subtree heights should not differ by more than:1Example 1Inputroot = [3,9,20,null,null,15,7]Tree:3/ \9 20/ \15 7OutputtrueExplanation:Every node satisfies:height difference <= 1Example 2Inputroot = [1,2,2,3,3,null,null,4,4]Tree:1/ \2 2/ \3 3/ \4 4OutputfalseExplanation:Left subtree becomes much deeper than right subtree.Difference becomes greater than:1Key ObservationTo determine if tree is balanced:At every node we need:Height of left subtreeHeight of right subtreeCompare differenceThis naturally becomes a recursive DFS problem.Brute Force ApproachIntuitionFor every node:Calculate left heightCalculate right heightCompare differenceRecursively check childrenBrute Force Java Solutionclass Solution {public int height(TreeNode root) {if(root == null)return 0;return 1 + Math.max(height(root.left), height(root.right));}public boolean isBalanced(TreeNode root) {if(root == null)return true;int left = height(root.left);int right = height(root.right);if(Math.abs(left - right) > 1)return false;return isBalanced(root.left) && isBalanced(root.right);}}Problem with Brute ForceThe height function gets called repeatedly.For every node:Heights are recalculated again and again.This increases complexity significantly.Brute Force ComplexityTime ComplexityO(N²)because height calculation repeats.Space ComplexityO(H)for recursion stack.Optimized DFS ApproachInstead of:Calculating heights separatelyWe can:Calculate height and balance together.Core Optimization IdeaWhile calculating height:If subtree becomes unbalanced:Return negative value immediatelyThis avoids unnecessary computation.Optimized Java Solutionclass Solution {public int solve(TreeNode roo) {if(roo == null)return 0;int left = solve(roo.left);if(left < 0) {return -1;}int right = solve(roo.right);if(right < 0) {return -1;}if(Math.abs(left - right) > 1) {return -1000;}return 1 + Math.max(left, right);}public boolean isBalanced(TreeNode root) {int ans = solve(root);return ans < 0 ? false : true;}}Cleaner Optimized VersionWe can simplify negative returns using:-1consistently.Cleaner Java Solutionclass Solution {public int height(TreeNode root) {if(root == null)return 0;int left = height(root.left);if(left == -1)return -1;int right = height(root.right);if(right == -1)return -1;if(Math.abs(left - right) > 1)return -1;return 1 + Math.max(left, right);}public boolean isBalanced(TreeNode root) {return height(root) != -1;}}Why This WorksAt every node:Recursively calculate left heightRecursively calculate right heightIf difference > 1:Tree is unbalancedPropagate failure upward immediately.Dry RunInput1/ \2 2/ \3 3/ \4 4Step 1Start from leaf nodes:4 → height = 1Step 2Node:3gets:left = 1right = 1Difference:0Balanced.Height:2Step 3At node:2Left height:2Right height:0Difference:2Unbalanced.Return:-1Final ResultTree is:Not balancedReturn:falseTime Complexity AnalysisOptimized DFS SolutionTime ComplexityO(N)because every node is visited once.Space ComplexityO(H)where:H = height of treeWorst case:O(N)for skewed tree.Brute Force vs OptimizedApproachTime ComplexitySpace ComplexityBrute ForceO(N²)O(H)Optimized DFSO(N)O(H)Interview ExplanationIn interviews, explain:Instead of recalculating heights repeatedly, we combine height calculation and balance checking into a single DFS traversal.This demonstrates:Optimization skillsRecursive DFS understandingBottom-up tree processingCommon Mistakes1. Recalculating Heights RepeatedlyThis causes:O(N²)complexity.2. Forgetting Absolute DifferenceAlways use:Math.abs(left - right)3. Not Handling Null NodesBase case:if(root == null)return 0;is necessary.FAQsQ1. What is a balanced binary tree?A tree where left and right subtree heights differ by at most:1for every node.Q2. Why use DFS?Because height calculation naturally follows recursive depth traversal.Q3. Why return -1?It acts as a signal:Subtree already unbalancedQ4. Is this problem important for interviews?Very important.It is one of the most common tree optimization questions.ConclusionLeetCode 110 is an excellent binary tree optimization problem.It teaches:DFS traversalHeight calculationBottom-up recursionOptimization techniquesThe key insight is:Combine balance checking and height calculation in one DFS traversal.Once you understand this optimization pattern, many advanced tree problems become much easier.

LeetCodeBalanced Binary TreeJavaBinary TreeDFSTree HeightRecursionTree
Subsets Problem (LeetCode 78) Explained | Recursion, Iterative & Bit Manipulation

Subsets Problem (LeetCode 78) Explained | Recursion, Iterative & Bit Manipulation

IntroductionThe Subsets problem (LeetCode 78) is one of the most fundamental and frequently asked questions in coding interviews. It introduces the concept of generating a power set, which is a core idea in recursion, backtracking, and combinatorics.Mastering this problem helps in solving a wide range of advanced problems like combinations, permutations, and decision-based recursion.In this article, we will explore:Intuition behind subsetsRecursive (backtracking) approachIterative (loop-based) approachBit manipulation approachTime and space complexity analysisProblem StatementGiven an integer array nums of unique elements, return all possible subsets (the power set).Key PointsEach element can either be included or excludedNo duplicate subsetsReturn subsets in any orderExamplesExample 1Input:nums = [1, 2, 3]Output:[[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]Example 2Input:nums = [0]Output:[[], [0]]Key InsightFor each element, there are two choices:Include it OR Exclude itSo total subsets:2^nThis makes it a binary decision tree problem, very similar to:Permutation with SpacesBinary choices recursionBacktracking problemsApproach 1: Recursion + Backtracking (Most Important)IntuitionAt each index:Skip the elementInclude the elementBuild subsets step by step and backtrack.Java Code (With Explanation)import java.util.*;class Solution { List<List<Integer>> liss = new ArrayList<>(); void solve(int[] an, int ind, List<Integer> lis) { // Base case: reached end → one subset formed if (ind == an.length) { liss.add(new ArrayList<>(lis)); // store copy return; } // Choice 1: Do NOT include current element solve(an, ind + 1, lis); // Choice 2: Include current element lis.add(an[ind]); solve(an, ind + 1, lis); // Backtrack: remove last added element lis.remove(lis.size() - 1); } public List<List<Integer>> subsets(int[] nums) { List<Integer> lis = new ArrayList<>(); solve(nums, 0, lis); return liss; }}Dry Run (nums = [1,2])Start: [] → skip 1 → [] → skip 2 → [] → take 2 → [2] → take 1 → [1] → skip 2 → [1] → take 2 → [1,2]Final Output:[], [2], [1], [1,2]Approach 2: Iterative (Loop-Based)IntuitionStart with an empty subset:[ [] ]For each element:Add it to all existing subsetsCodeimport java.util.*;class Solution { public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); result.add(new ArrayList<>()); for (int num : nums) { int size = result.size(); for (int i = 0; i < size; i++) { List<Integer> temp = new ArrayList<>(result.get(i)); temp.add(num); result.add(temp); } } return result; }}How It WorksFor [1,2,3]:Start: [[]]Add 1 → [[], [1]]Add 2 → [[], [1], [2], [1,2]]Add 3 → [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]Approach 3: Bit ManipulationIntuitionEach subset can be represented using a binary number:For n = 3:000 → []001 → [1]010 → [2]011 → [1,2]...Codeimport java.util.*;class Solution { public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); int n = nums.length; int total = 1 << n; // 2^n for (int i = 0; i < total; i++) { List<Integer> subset = new ArrayList<>(); for (int j = 0; j < n; j++) { if ((i & (1 << j)) != 0) { subset.add(nums[j]); } } result.add(subset); } return result; }}Complexity AnalysisApproachTime ComplexitySpace ComplexityRecursionO(2^n)O(n) stackIterativeO(2^n)O(2^n)Bit ManipulationO(2^n)O(2^n)Why All Approaches Are O(2ⁿ)Because:Total subsets = 2ⁿEach subset takes up to O(n) to constructWhen to Use Which ApproachRecursion / Backtracking → Best for interviews (easy to explain)Iterative → Clean and beginner-friendlyBit Manipulation → Best for optimization & advanced understandingKey TakeawaysSubsets = power set problemEvery element → 2 choicesThink in terms of decision treesBacktracking = build + undo (add/remove)Common Interview VariationsSubsets with duplicatesCombination sumPermutationsK-sized subsetsConclusionThe Subsets problem is a foundational DSA concept that appears across many interview questions. Understanding all approaches—especially recursion and iterative expansion—gives a strong base for solving complex backtracking problems.If you master this pattern, you unlock a whole category of problems in recursion and combinatorics.Frequently Asked Questions (FAQs)1. Why are there 2ⁿ subsets?Because each element has 2 choices: include or exclude.2. Which approach is best for interviews?Recursion + backtracking is the most preferred.3. Is bit manipulation important?Yes, it helps in optimizing and understanding binary patterns.

LeetCodeMediumJavaRecursionBacktracking
LeetCode 2196: Create Binary Tree From Descriptions – Java HashMap & Tree Construction Solution

LeetCode 2196: Create Binary Tree From Descriptions – Java HashMap & Tree Construction Solution

IntroductionBinary tree construction problems are extremely common in coding interviews because they test multiple concepts together:Tree data structuresHashMap usageParent-child relationshipsGraph-style thinkingRoot identificationLeetCode 2196 is an excellent medium-level problem that focuses on constructing a binary tree from parent-child descriptions efficiently.In this article, we will deeply understand:How the tree is formedHow to identify the root nodeWhy HashMap is useful hereStep-by-step dry runTime and space complexityComplete optimized Java solutionProblem StatementYou are given a 2D array:descriptions[i] = [parent, child, isLeft]Where:parent is the parent nodechild is the child nodeisLeft == 1 means left childisLeft == 0 means right childYour task is to:Construct the binary treeReturn the root nodeHere is the Link to try it now -: Create Binary Tree From DescriptionsExampleInputdescriptions = [ [20,15,1], [20,17,0], [50,20,1], [50,80,0], [80,19,1]]Output[50,20,80,15,17,19]Visual Representation 50 / \ 20 80 / \ / 15 17 19Key ObservationEvery node except the root appears as a child exactly once.This means:Root node never appears in child positionsIf we store all child nodes,The node not present in the child set becomes the rootThis is the core trick of the problem.Approach OverviewWe solve the problem in 3 steps:Step 1: Find the Root NodeStore every child node in a HashSet.Then iterate through descriptions again:The parent that never appears as a child is the root.Step 2: Create Tree NodesUse a HashMap:value → TreeNodeThis prevents duplicate node creation.Step 3: Connect Parent and ChildBased on:isLeftattach nodes accordingly.Optimized Java Solutionclass Solution { public TreeNode createBinaryTree(int[][] descriptions) { HashSet<Integer> childSet = new HashSet<>(); // Store all child nodes for (int[] arr : descriptions) { childSet.add(arr[1]); } // Find root node int rootValue = -1; for (int[] arr : descriptions) { if (!childSet.contains(arr[0])) { rootValue = arr[0]; } } // Map value -> TreeNode HashMap<Integer, TreeNode> map = new HashMap<>(); for (int i = 0; i < descriptions.length; i++) { int parent = descriptions[i][0]; int child = descriptions[i][1]; int isLeft = descriptions[i][2]; // Create parent node if absent if (!map.containsKey(parent)) { map.put(parent, new TreeNode(parent)); } // Create child node if absent if (!map.containsKey(child)) { map.put(child, new TreeNode(child)); } // Connect nodes if (isLeft == 1) { map.get(parent).left = map.get(child); } else { map.get(parent).right = map.get(child); } } return map.get(rootValue); }}Step-by-Step ExplanationStep 1: Store All Child NodeschildSet.add(arr[1]);Every child is stored.Since root never becomes a child,it will not exist inside this set.Step 2: Identify Rootif (!childSet.contains(arr[0]))This finds the node that only acts as parent.That node becomes the root.Step 3: Create Unique Tree NodesHashMap<Integer, TreeNode> mapAvoids duplicate node creation.Each value maps to exactly one TreeNode.Step 4: Connect Parent and Childmap.get(parent).left = map.get(child);ormap.get(parent).right = map.get(child);depending on:isLeftDry RunInput[ [20,15,1], [20,17,0], [50,20,1], [50,80,0], [80,19,1]]Child Set15, 17, 20, 80, 19Root DetectionCheck parents:20 → exists in child set50 → NOT in child setSo:Root = 50Tree Construction50left → 20right → 8020left → 15right → 1780left → 19Final tree becomes: 50 / \ 20 80 / \ / 15 17 19Time ComplexityBuilding Child SetO(N)Finding RootO(N)Constructing TreeO(N)Total Time ComplexityO(N)Efficient for large constraints.Space ComplexityHashMap + HashSetO(N)Why This Problem is ImportantThis problem teaches:Tree constructionParent-child mappingRoot identificationEfficient object reuseHashMap optimizationIt is frequently asked in interviews because it combines multiple concepts in one clean implementation.Common Mistakes1. Creating Duplicate NodesWrong:new TreeNode(parent)every time.Always reuse nodes through HashMap.2. Incorrect Root DetectionRemember:Root never appears as child.3. Forgetting Left vs Right ChildAlways check:isLeftbefore attaching.Interview TipsIn interviews explain:We first identify the root using a child set, then use a HashMap to create and reuse TreeNode objects efficiently while connecting parent-child relationships.This demonstrates:Strong data structure understandingEfficient memory usageClean tree construction logicRelated ProblemsPractice these next:Construct Binary Tree from TraversalsValidate Binary Search TreeSerialize and Deserialize Binary TreeLowest Common AncestorBinary Tree Level Order TraversalConclusionLeetCode 2196 is a clean and elegant binary tree construction problem.The major insight is:The root node is the only node that never appears as a child.Combined with HashMap-based node reuse, we can construct the entire tree efficiently in linear time.This is an excellent interview problem for mastering tree construction patterns.

LeetCodeJavaTreeHashMapHashSetBinary TreeMedium
All Subsequences of a String (Power Set) | Recursion & Backtracking Java Solution

All Subsequences of a String (Power Set) | Recursion & Backtracking Java Solution

IntroductionThe Power Set problem for strings is a classic question in recursion and backtracking, frequently asked in coding interviews and platforms like GeeksforGeeks.In this problem, instead of numbers, we deal with strings and generate all possible subsequences (not substrings). This makes it slightly more interesting and practical for real-world applications like pattern matching, text processing, and combinatorics.In this article, we will cover:Intuition behind subsequencesRecursive (backtracking) approachSorting for lexicographical orderAlternative approachesComplexity analysisProblem StatementGiven a string s of length n, generate all non-empty subsequences of the string.RequirementsReturn only non-empty subsequencesOutput must be in lexicographically sorted orderExamplesExample 1Input:s = "abc"Output:a ab abc ac b bc cExample 2Input:s = "aa"Output:a a aaSubsequence vs Substring (Important)Substring: Continuous charactersSubsequence: Characters can be skippedExample for "abc":Subsequences → a, b, c, ab, ac, bc, abcKey InsightFor every character, we have two choices:Include it OR Exclude itSo total subsequences:2^nWe generate all and then remove the empty string.Approach 1: Recursion (Backtracking)IntuitionAt each index:Skip the characterInclude the characterBuild all combinations recursivelyJava Code (With Explanation)import java.util.*;class Solution { // List to store all subsequences List<String> a = new ArrayList<>(); void sub(String s, int ind, String curr) { // Base case: reached end of string if (ind == s.length()) { a.add(curr); // add current subsequence return; } // Choice 1: Exclude current character sub(s, ind + 1, curr); // Choice 2: Include current character sub(s, ind + 1, curr + s.charAt(ind)); } public List<String> AllPossibleStrings(String s) { // Start recursion sub(s, 0, ""); // Remove empty string (not allowed) a.remove(""); // Sort lexicographically Collections.sort(a); return a; }}Step-by-Step Dry Run (s = "abc")Start: ""→ Exclude 'a' → "" → Exclude 'b' → "" → Exclude 'c' → "" → Include 'c' → "c" → Include 'b' → "b" → Exclude 'c' → "b" → Include 'c' → "bc"→ Include 'a' → "a" → Exclude 'b' → "a" → Exclude 'c' → "a" → Include 'c' → "ac" → Include 'b' → "ab" → Exclude 'c' → "ab" → Include 'c' → "abc"Final Output (After Sorting)a ab abc ac b bc cApproach 2: Bit ManipulationIntuitionEach subsequence can be represented using binary numbers:0 → exclude1 → includeCodeimport java.util.*;class Solution { public List<String> AllPossibleStrings(String s) { List<String> result = new ArrayList<>(); int n = s.length(); int total = 1 << n; // 2^n for (int i = 1; i < total; i++) { // start from 1 to avoid empty StringBuilder sb = new StringBuilder(); for (int j = 0; j < n; j++) { if ((i & (1 << j)) != 0) { sb.append(s.charAt(j)); } } result.add(sb.toString()); } Collections.sort(result); return result; }}Approach 3: Iterative (Expanding List)IdeaStart with empty listFor each character:Add it to all existing subsequencesCodeimport java.util.*;class Solution { public List<String> AllPossibleStrings(String s) { List<String> result = new ArrayList<>(); result.add(""); for (char ch : s.toCharArray()) { int size = result.size(); for (int i = 0; i < size; i++) { result.add(result.get(i) + ch); } } result.remove(""); Collections.sort(result); return result; }}Complexity AnalysisTime Complexity: O(n × 2ⁿ)Space Complexity: O(n × 2ⁿ)Why?Total subsequences = 2ⁿEach subsequence takes O(n) to buildWhy Sorting is RequiredThe recursion generates subsequences in random order, so we sort them:Collections.sort(result);This ensures lexicographical order as required.Key TakeawaysThis is a power set problem for stringsEach character → 2 choicesRecursion = most intuitive approachBit manipulation = most optimized thinkingAlways remove empty string if requiredCommon Interview VariationsSubsets of arrayPermutations of stringCombination sumSubsequence with conditionsConclusionThe Power Set problem is a fundamental building block in recursion and combinatorics. Once you understand the include/exclude pattern, you can solve a wide range of problems efficiently.Mastering this will significantly improve your ability to tackle backtracking and decision tree problems.Frequently Asked Questions (FAQs)1. Why is the empty string removed?Because the problem requires only non-empty subsequences.2. Why is time complexity O(n × 2ⁿ)?Because there are 2ⁿ subsequences and each takes O(n) time to construct.3. Which approach is best?Recursion → best for understandingBit manipulation → best for optimization

GeeksforGeeksRecursionJavaBacktrackingMedium
LeetCode 2144: Minimum Cost of Buying Candies With Discount – Java Greedy Approach Explained

LeetCode 2144: Minimum Cost of Buying Candies With Discount – Java Greedy Approach Explained

IntroductionLeetCode 2144 – Minimum Cost of Buying Candies With Discount is a classic greedy + sorting problem.The question looks simple initially, but the key challenge is understanding:Which candies should be free?How can we minimize the total payment?Why sorting helps?This is a good beginner-friendly greedy interview problem that teaches how to maximize discounts strategically.Problem Link🔗 Minimum Cost of Buying Candies With DiscountProblem StatementA candy shop offers a discount:For every two candies bought, you can get one additional candy for free.Condition:The free candy’s price must be less than or equal to the cheaper purchased candy.You are given an array:cost[i]representing candy prices.Return the minimum total amount needed to buy all candies.Example 1Inputcost = [1,2,3]Output5ExplanationBuy:3 and 2Get:1 freeTotal:3 + 2 = 5Example 2Inputcost = [6,5,7,9,2,2]Output23IntuitionTo maximize savings:The most expensive candies should be grouped together.Why?Because every third candy becomes free.So we want:Expensive candies paidCheapest among the group freeKey Greedy ObservationIf we sort candies in descending order:9, 7, 6, 5, 2, 2Then:Pay 9Pay 7Get 6 freeThen:Pay 5Pay 2Get 2 freeThis produces maximum discount.Brute Force ApproachIdeaTry every grouping combination:Select 2 candiesChoose possible free candyCompute total minimumWhy Brute Force is BadThe number of combinations becomes huge.Time complexity becomes exponential.Not suitable for interviews.Optimized Greedy ApproachStepsSort candies in descending orderTraverse arraySkip every 3rd candyAdd remaining candies to answerJava Solutionclass Solution {public int minimumCost(int[] cost) {if(cost.length == 1) return cost[0];if(cost.length == 2) return cost[0] + cost[1];Arrays.sort(cost);int[] arr = new int[cost.length];int o = 0;for(int j = cost.length - 1; j >= 0; j--) {arr[o] = cost[j];o++;}int sum = 0;int c = 0;for(int i = 0; i < arr.length; i++) {c = i + 1;if(c % 3 == 0) continue;sum += arr[i];}return sum;}}Cleaner Optimized VersionYou can simplify the logic further:class Solution {public int minimumCost(int[] cost) {Arrays.sort(cost);int sum = 0;int count = 0;for(int i = cost.length - 1; i >= 0; i--) {count++;if(count % 3 == 0) continue;sum += cost[i];}return sum;}}Why This WorksAfter descending sorting:Most expensive candies appear firstEvery 3rd candy becomes free.This guarantees:Maximum discount possibleDry RunInputcost = [6,5,7,9,2,2]Step 1: Sort Descending9,7,6,5,2,2Step 2: TraverseGroup 19 -> pay7 -> pay6 -> freeTotal:16Group 25 -> pay2 -> pay2 -> freeTotal:16 + 7 = 23Final Answer23Time Complexity AnalysisSortingO(N log N)TraversalO(N)Total ComplexityO(N log N)Space ComplexityO(N)Because of extra reversed array.Optimized VersionO(1)Ignoring sorting space.Interview ExplanationIn interviews explain:To maximize savings, we should make the free candies as expensive as possible. Sorting in descending order ensures every third candy is the maximum eligible free candy.This demonstrates:Greedy thinkingSorting optimizationMathematical observation skillsCommon Mistakes1. Sorting AscendingAscending order gives smaller discounts.Descending order is necessary.2. Forgetting Every 3rd Candy is FreeCorrect pattern:Pay, Pay, Free3. Using Extra Arrays UnnecessarilyYou can directly traverse sorted array backward.4. Incorrect GroupingAlways group expensive candies together.FAQsQ1. Why descending sorting?To maximize free candy value.Q2. Why skip every third candy?Because the offer gives:1 free candy after buying 2Q3. Can this be solved without sorting?Not optimally.Sorting guarantees best grouping.Q4. Is this a greedy problem?Yes.This is a classic greedy sorting optimization problem.ConclusionLeetCode 2144 is an excellent beginner greedy problem.Main learning points:Sorting strategyGreedy groupingMaximizing discountsEfficient array traversalThe core insight is:Sort candies from largest to smallest and make every third candy free.Once this pattern becomes intuitive, many greedy interview problems become easier to solve.

LeetCodeJavaSortingArrayGreedy ApproachEasy
LeetCode 1254: Number of Closed Islands — Finding the Idea Before Writing the Code (Java)

LeetCode 1254: Number of Closed Islands — Finding the Idea Before Writing the Code (Java)

The Honest Beginning: I Understood the Question, but I Didn't Know How to Solve ItProblem: LeetCode 1254 — Number of Closed IslandsWhen I first read this question, I understood the basic meaning of the input. We have a grid containing 0s and 1s. Zero represents land, and one represents water.But understanding the input and knowing how to solve the problem are two completely different things.The question asks us to count closed islands. What exactly is an island here? How do we decide whether several zeroes belong to the same island? And how can we tell whether an island is closed?I didn't immediately think of DFS, recursion, or connected components. Honestly, I didn't even know which direction to take.So instead of jumping straight into a solution, let's start with something familiar and build the intuition from there.Step 1: Imagine Looking for Land on a MapImagine that you are looking at a small map made up of land and water.0 means land.1 means water.Suppose several pieces of land are touching each other horizontally or vertically. Even if that land occupies many grid cells, we consider it one connected island.For example, look at this grid:1 1 1 1 11 0 0 1 11 1 0 1 11 1 1 1 1There are three land cells, but they are not three separate islands.The two zeroes in the second row are connected. The zero directly below the second zero is also connected to them.Therefore, all three cells form one island.This gives us our first important observation:We should not count every zero individually. We need to discover groups of zeroes connected in four directions: up, down, left, and right.Notice that diagonal contact does not count. Two land cells touching only at their corners are not connected under this problem's rules.But we still haven't answered the most important question: how do we discover an entire group?Step 2: How Do We Explore All the Connected Land?Suppose we find a zero at position (1, 1).What should we do next?We can inspect its four neighboring cells:Left: (1, 0)Right: (1, 2)Up: (0, 1)Down: (2, 1)If a neighboring cell contains land, we should explore that cell too. Then we should inspect its neighbors, and continue until there is no more unvisited land connected to our starting point.Think about exploring a maze. You enter a room, check the available paths, enter another room, and continue exploring. Whenever you reach a dead end or a path you have already visited, you return and try another direction.That is very close to what recursion does.We can write a function that explores a cell and calls itself for the four neighboring cells. This technique is commonly called Depth-First Search (DFS).I initially found the recursive calls confusing because it felt as if the function would keep calling itself forever. The missing piece is that we need a way to remember which cells have already been explored.In this problem, we can use the grid itself.Whenever we visit a land cell, we change its value from 0 to 1. Since we have finished treating that cell as unvisited land, we won't explore it again.This prevents us from repeatedly visiting the same cells and getting stuck in an infinite recursive loop.Step 3: Exploring an Island Is Not Enough — Is It Closed?At this point, we can discover an island. But the problem doesn't ask us to count every island. It asks us to count only closed islands.Imagine two pieces of land.In the first case, land is surrounded by water on every side. It is completely enclosed.In the second case, some land reaches the edge of the map. This island is open because it is not completely surrounded by water.The important distinction is that an island touching the boundary of the grid cannot be closed.For example:1 1 1 1 11 0 0 1 11 1 0 1 11 1 1 1 1The island is surrounded by water, so it is closed.Now consider:0 0 1 1 11 0 0 1 11 1 1 1 1The land group touches the top edge of the grid. It is not closed, even though some of its cells have water around them.This gives us the second important observation:An island is closed only if none of its connected land cells touches the boundary of the grid.We now have the complete plan:Find an unvisited land cell.Explore every land cell connected to it.Determine whether that entire island touches the boundary.Count it only if it is closed.Continue looking for other unvisited islands.The remaining challenge is implementing this logic without losing information about whether the island is open or closed.Step 4: Translating the Idea into RecursionLet's design a recursive function called sol().It receives the grid and the current row and column. It returns a boolean:true means the explored land group is closed.false means the group touches the grid boundary and is not closed.The function needs to handle three situations.Case 1: We reach waterIf the current cell contains 1, we have reached water. This direction does not lead to additional land outside the island.We return true because this particular direction does not make the island open.Case 2: We reach the grid boundaryIf the current cell is land and lies on the outermost row or column, the island touches the boundary.That means the island is not closed, so this exploration must eventually return false.However, there is a subtle implementation detail here: we must still mark and explore the boundary-connected land correctly. If we stop immediately at the first boundary cell without exploring the rest of that connected group, some of the island's cells may remain unvisited. A later scan could then mistake part of that same open island for a separate closed island.We will address this in the final implementation.Case 3: We reach unvisited interior landThis is where recursion does the real work.We mark the current cell as visited by changing it to 1. Then we explore all four directions.The island is closed only if every direction confirms that it does not connect to the outside boundary.In boolean terms, the results must be combined using logical AND (&&).If even one path reaches boundary-connected land, the whole island is open.Step 5: Connecting the Recursive Function to the Main LoopWe need a way to start exploring islands.The simplest approach is to scan the grid using two nested loops.The outer loop moves through the rows, and the inner loop moves through the columns. Whenever we find an unvisited land cell, we call our recursive function.If the function confirms that the island is closed, we increment our answer.Why can we modify the grid instead of creating a separate visited matrix?Because the original problem only requires us to return the count. We don't need to preserve the original grid, so changing visited land into water is a convenient way to remember our progress.We also need to avoid counting the same island twice. Once an entire connected island has been explored and marked, its cells will no longer trigger another DFS call from the main loop.Java SolutionHere is the complete implementation. It follows the recursive DFS approach described above and handles boundary-connected islands correctly.class Solution {public boolean sol(int[][] grid, int row, int col) {// Reaching outside the grid means this island is open.if (row < 0 || row >= grid.length ||col < 0 || col >= grid[0].length) {return false;}// Water does not make the island open.if (grid[row][col] == 1) {return true;}// Mark this land cell as visited.grid[row][col] = 1;// Explore all four directions.boolean left = sol(grid, row, col - 1);boolean right = sol(grid, row, col + 1);boolean top = sol(grid, row - 1, col);boolean bottom = sol(grid, row + 1, col);return left && right && top && bottom;}public int closedIsland(int[][] grid) {int rows = grid.length;int cols = grid[0].length;int ans = 0;for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {if (grid[i][j] == 0) {if (sol(grid, i, j)) {ans++;}}}}return ans;}}What changed from my first attempt?My original code checked whether the current cell was water before checking whether it was on the boundary. It also returned immediately when isperim() detected a boundary land cell.That creates a subtle problem: the boundary cell remains unvisited, and the DFS does not continue through it to explore the rest of the connected island. Consequently, land connected through the boundary can be left behind and potentially counted incorrectly later.The revised code uses a different strategy:It checks whether the coordinates are outside the grid before accessing grid[row][col].It treats water as a successful direction.It marks every visited land cell, including boundary land.It explores all four directions before combining their results.This ensures that the whole connected island is visited, even when it is open.One more detail matters: we store the results of all four recursive calls in separate variables. This is intentional. Writing a short-circuit expression that skips later calls after the first false could leave some cells unexplored.Step 6: Dry RunConsider this grid:1 1 1 1 11 0 0 1 11 1 0 1 11 1 1 1 1The main loop scans the grid until it finds the first zero at (1, 1).First, we start DFS at (1, 1).The cell is land, so we mark it as visited. The function explores its left, right, top, and bottom neighbors.The right neighbor (1, 2) is also land. The DFS visits it and explores its neighbors.The cell (2, 2) is land as well, so the exploration continues there.Eventually, every connected land cell has been visited.Next, we check the four directions.All paths eventually reach water or the outside of the grid through no unvisited land. Because no connected land cell reaches the boundary, the recursive result is true.We increment the answer:ans = 1The main loop continues scanning. Since the three land cells have already been changed to 1, they are not counted again.There are no other land cells, so the final answer is:1This small example demonstrates the entire mechanism: one DFS call discovers the complete island, marks it visited, and returns whether it is closed.A More Important Edge Case: One Island Touching the BoundaryConsider this grid:0 0 1 1 11 0 0 1 11 1 1 1 1The connected land group touches the top boundary.When DFS starts from one of its land cells, it will eventually reach a coordinate outside the grid. That recursive call returns false.The false result propagates through the recursive calls, so the complete island is classified as open and is not counted.Because the DFS still explores all four directions and marks every land cell, the main loop cannot later count another portion of this same island as a closed island.The answer is 0.This is why visiting the complete connected group matters just as much as deciding whether it is closed.Complexity AnalysisLet m be the number of rows and n be the number of columns.Time complexity: O(m × n)Each cell is visited at most once because every visited land cell is changed from 0 to 1. The grid scan also examines every cell. Therefore, the total time complexity is O(m × n).Auxiliary space complexity: O(m × n) in the worst case.The recursive DFS can reach a depth proportional to the number of cells if a large island is connected throughout the grid. The recursion call stack can therefore use O(m × n) space in the worst case.The algorithm modifies the input grid directly and does not require a separate visited matrix.What I Learned from This ProblemThe biggest lesson was that I didn't need to know the term “connected component” before starting. I needed to understand what the problem was asking me to discover.First, I noticed that individual zeroes were not the answer; groups of connected zeroes were. That led to the idea of exploring neighbors.Then I realized that exploring one neighbor should lead to exploring its neighbors too. Recursion gave me a way to express that repeated process.Finally, I understood that a closed island was not just a group of land cells. It was a group that never reached the boundary.The difficult part was not writing four recursive calls. It was making sure the function explored the entire island and correctly communicated whether any part of it touched the boundary.That distinction is worth remembering in future DFS problems: discovering a connected group and evaluating a property of that group are two related, but separate, tasks.Final TakeawayLeetCode 1254 becomes easier to reason about once we stop thinking of it as a mysterious graph problem and start thinking of it as exploring connected land on a map.Use DFS to explore the entire island.Mark visited land so it cannot be counted twice.Treat a path that reaches outside the grid as evidence that the island is open.Count an island only when every explored direction confirms that it is closed.If you struggled to find this approach on your first attempt, that is a normal part of learning DSA. The goal is not to recognize every pattern immediately. It is to learn how to turn an unfamiliar problem into a sequence of smaller, understandable decisions.

LeetCodeJavaRecursionBacktrackingMatrixDFSMedium
LeetCode 41: First Missing Positive – O(n) Time and O(1) Space Java Solution

LeetCode 41: First Missing Positive – O(n) Time and O(1) Space Java Solution

IntroductionFinding the smallest positive integer missing from an unsorted array appears simple at first, but the problem becomes significantly more interesting because of its strict constraints.Problem Link -: First Missing PositiveGiven an unsorted integer array nums, the task is to return the smallest positive integer that does not appear in the array.For example:nums = [1,2,0]The positive integers begin with:1, 2, 3, 4, ...Both 1 and 2 are present, while 3 is missing.Therefore, the answer is:3The challenge is that the solution must run in:O(n) timeO(1) auxiliary spaceThis rules out several common approaches such as sorting and using a HashSet.Problem StatementGiven an unsorted integer array nums, return the smallest positive integer that is not present in the array.ExampleInput: nums = [3,4,-1,1]Output: 2The positive integers are:1, 2, 3, 4, ...1 exists in the array, but 2 does not.Therefore:Answer = 2Another example:Input: nums = [7,8,9,11,12]Output: 1Since 1 itself is missing, the answer is immediately:1Understanding the Important ObservationFor an array of length n, the answer can never be greater than:n + 1Consider:nums = [1,2,3]The first three positive integers are present:1, 2, 3Therefore, the smallest missing positive integer is:4Since n = 3:answer <= n + 1This observation is the key to the optimal solution.Numbers that are:<= 0or> ncannot directly determine the smallest missing positive number.The useful values are only:1 ... nThe Submitted HashSet ApproachA straightforward approach is to store every value in a HashSet and then search for the first missing positive integer.The basic idea is:Store all numbers ↓Check 1 ↓Check 2 ↓Check 3 ↓...The submitted implementation follows this idea:class Solution { public int firstMissingPositive(int[] nums) { int max = Integer.MIN_VALUE; HashSet<Integer> ms = new HashSet<>(); for (int a : nums) { max = Math.max(a, max); ms.add(a); } for (int i = 1; i <= max; i++) { if (!ms.contains(i)) { return i; } } if (max < 0) { return 1; } return max + 1; }}This approach is logically valid for finding the answer, but it does not satisfy the required constraints.Space ComplexityThe HashSet can store up to n elements.Therefore:O(n) auxiliary spaceThe problem requires:O(1) auxiliary spaceSo a different technique is required.Why Sorting Is Also Not EnoughSorting the array might appear to solve the problem easily.For example:[3,4,-1,1]After sorting:[-1,1,3,4]The missing positive integer can then be identified by scanning the array.However, sorting requires:O(n log n)time in the general case.The problem specifically requires:O(n)time.Therefore, sorting does not satisfy the required complexity either.The Core Idea: Put Every Number in Its Correct PositionThe optimal solution uses an in-place cyclic placement technique.The important relationship is:value 1 → index 0value 2 → index 1value 3 → index 2value 4 → index 3...In general:value x → index x - 1Consider:nums = [3,4,-1,1]The desired arrangement for useful values is:index: 0 1 2 3value: 1 2 3 4After placing the valid values into their corresponding positions, the array can become:[1, -1, 3, 4]Index 1 should contain:2but it contains -1.Therefore:answer = index + 1 = 1 + 1 = 2This transforms the problem from:Search for a missing number.into:Place every useful number at the position where it belongs, then find the first incorrect position.Which Numbers Should Be Placed?For an array of length n, only values satisfying:1 <= nums[i] <= nneed to be placed.Why?Suppose:n = 5The answer can only be one of:1,2,3,4,5,6A value such as:-4cannot be the answer.Similarly:10cannot prevent 1...6 from determining the answer.Therefore, values outside the range [1,n] can safely remain where they are.The Cyclic Placement ProcessFor every index i, check the current value:nums[i]If it belongs to the range:1 <= nums[i] <= nits correct index is:nums[i] - 1So the value should be swapped into:nums[nums[i] - 1]The process continues until the current position contains a value that either:is outside the useful range, oris already in its correct position.Why the Duplicate Check Is NecessaryConsider:nums = [1,1]The value 1 belongs at index 0.The first element is already correct.At index 1, the value is again 1.Trying to swap it repeatedly would cause an infinite loop because another 1 already occupies its correct position.Therefore, the swap condition must also ensure:nums[nums[i] - 1] != nums[i]This duplicate check is extremely important.Optimized Java Solutionclass Solution { public int firstMissingPositive(int[] nums) { int n = nums.length; for (int i = 0; i < n; i++) { while ( nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i] ) { int correctIndex = nums[i] - 1; int temp = nums[i]; nums[i] = nums[correctIndex]; nums[correctIndex] = temp; } } for (int i = 0; i < n; i++) { if (nums[i] != i + 1) { return i + 1; } } return n + 1; }}Dry RunConsider:nums = [3,4,-1,1]Array length:n = 4Initial Array[3, 4, -1, 1]At index 0:nums[0] = 3The correct index for 3 is:3 - 1 = 2Swap:[3, 4, -1, 1] ↓[-1, 4, 3, 1]The value at index 0 is now -1.Since -1 is outside [1,4], no further placement is required for this index.At index 1:nums[1] = 4Correct index:4 - 1 = 3Swap:[-1, 4, 3, 1] ↓[-1, 1, 3, 4]Now:nums[1] = 1The correct index of 1 is 0.Swap:[-1, 1, 3, 4] ↓[1, -1, 3, 4]Now index 1 contains -1, so placement stops.Final Arrangement[1, -1, 3, 4]The expected value at each index is:index 0 → 1 ✓index 1 → 2 ✗index 2 → 3 ✓index 3 → 4 ✓The first incorrect position is:index = 1Therefore:answer = index + 1 = 2Another ExampleConsider:nums = [1,2,0]The values 1 and 2 are already in their correct positions:[1,2,0]Expected arrangement:index 0 → 1 ✓index 1 → 2 ✓index 2 → 3 ✗The first incorrect position is index 2.Therefore:answer = 2 + 1 = 3Edge Case: All Positive Numbers Are PresentConsider:nums = [1,2,3]Every position contains the expected value:index 0 → 1index 1 → 2index 2 → 3No missing value exists between 1 and n.Therefore, the smallest missing positive integer is:n + 1So:answer = 4Edge Case: The Answer Is 1Consider:nums = [7,8,9,11,12]All values are greater than 1.After the placement phase, there is no 1 at index 0.Therefore:answer = 1Complexity AnalysisThe algorithm uses the array itself to store values in their correct positions.Time ComplexityAlthough the algorithm contains a nested while loop, the total number of swaps is bounded by O(n) because every successful swap places a useful value into its correct position.Therefore:Time Complexity: O(n)Space ComplexityNo additional data structure proportional to the input size is used.Only a few variables are required:Space Complexity: O(1)This satisfies the required constraints.Why the Algorithm WorksThe central invariant is:Whenever a value x lies in the range [1,n], the algorithm attempts to place it at index x-1.After the placement phase, every value that can occupy a meaningful position is either:at its correct indexorabsent from the arrayTherefore, scanning from left to right provides a direct answer.If:nums[i] != i + 1then i + 1 is missing.If every position is correct, then all values from:1 ... nexist, making:n + 1the smallest missing positive integer.Interview InsightThis problem is an important example of in-place array positioning.The key pattern is:Value x ↓Correct index = x - 1Whenever an array problem asks for a missing, duplicate, or misplaced number within a known range, it is worth checking whether the values themselves can be mapped directly to indices.This technique appears in several interview problems involving:Missing numbersDuplicate numbersCyclic sortIn-place rearrangementFrequency representation without extra memoryThe biggest clue in this problem is the combination of:Unsorted array+O(n) time+O(1) space+Positive integersThat combination strongly suggests an in-place index/value mapping strategy.ConclusionLeetCode 41, First Missing Positive, is a classic example of turning the array itself into auxiliary storage.A HashSet provides an easy solution but requires O(n) extra space. Sorting simplifies the search but requires O(n log n) time. The optimal solution instead uses the relationship:value x → index x - 1to rearrange useful values directly inside the input array.After this placement, the first index whose value does not match index + 1 identifies the smallest missing positive integer.The final complexity is:O(n) timeO(1) auxiliary spacemaking the approach suitable for the strict constraints and an important pattern to recognize in technical interviews.

LeetCodeJavaArraysCyclic SortHardHashSet
LeetCode 3483: Unique 3-Digit Even Numbers – Java Backtracking Solution

LeetCode 3483: Unique 3-Digit Even Numbers – Java Backtracking Solution

IntroductionWhat happens when a small collection of digits needs to be arranged into valid three-digit numbers?There are several conditions to satisfy at the same time:The number must contain exactly three digits.The first digit cannot be 0.The number must be even.A digit can only be used as many times as it appears in the input.Duplicate numbers should be counted only once.Because the input contains at most 10 digits, backtracking is a natural approach. Every possible arrangement can be generated, checked, and stored in a HashSet to ensure that only distinct numbers are counted.Question LinkLeetCode 3483 – Unique 3-Digit Even NumbersApproachThe idea is to build the number one digit at a time.A recursive function maintains:curr — the number currently being constructed.boo[] — tracks which positions of the input array have already been used.ms — a HashSet containing all valid three-digit numbers.At every recursion level, each unused digit is selected and appended to curr.Once three digits have been selected, three checks are performed:Leading zero012is not a valid three-digit number, so it is rejected.Even numberThe generated number must be divisible by 2.DistinctnessThe generated number is inserted into a HashSet, which automatically removes duplicates.Why Track Indices Instead of Digits?Consider:digits = [0, 2, 2]The digit 2 appears twice.Therefore, a valid number such as:220must be allowed.The boolean array tracks positions, not just digit values:Index: 0 1 2Digit: 0 2 2The two copies of 2 are therefore treated as two separate usable elements.The HashSet handles the other side of the problem: different index selections can produce the same number, but the final answer should count that number only once.Java Implementationclass Solution { // Stores every distinct valid three-digit number. HashSet<String> ms = new HashSet<>(); public void sol(int[] dig, String curr, boolean[] boo) { // Once three digits are selected, // check whether the generated number is valid. if (curr.length() == 3) { // A three-digit number cannot start with zero. if (curr.charAt(0) == '0') { return; } // Check whether the number is even. // HashSet automatically handles duplicates. if (ch(curr) && !ms.contains(curr)) { ms.add(curr); } return; } // Try every digit that has not been used yet. for (int i = 0; i < dig.length; i++) { // Skip the current digit if its array position // has already been used in this number. if (boo[i]) continue; String vl = String.valueOf(dig[i]); // Mark this position as used. boo[i] = true; // Add the digit to the current number // and continue building the number. sol(dig, curr + vl, boo); // Backtrack: make this position available // for another possible arrangement. boo[i] = false; } } // Checks whether the generated number is even. public boolean ch(String s) { int n = Integer.valueOf(s); return n % 2 == 0 ? true : false; } public int totalNumbers(int[] digits) { // Tracks which positions of the input array // are currently being used. boolean[] boo = new boolean[digits.length]; // Start generating numbers from an empty string. sol(digits, "", boo); // HashSet contains only distinct valid numbers. return ms.size(); }}Backtracking in ActionConsider:digits = [1, 2, 3, 4]The recursion starts with an empty string:""Choose 1:"1"Then choose 2:"12"Then choose 3:"123"The number has three digits, but 123 is odd, so it is rejected.Backtracking returns to:"12"and tries 4:"124"This number is three digits, does not start with zero, and is even.Therefore:124 → validThe recursion continues exploring other arrangements.Handling Duplicate DigitsConsider:digits = [0, 2, 2]The two 2s have different indices.Some recursion branches may therefore generate the same number:202from different copies of 2.Without a HashSet, these would be counted multiple times.With:HashSet<String> msonly one copy remains.The valid numbers are:202220Therefore:Answer = 2Dry RunConsider:digits = [1, 2, 3, 4]Some of the generated permutations include:123 → odd → reject124 → even → add132 → even → add134 → even → add142 → even → addThe recursion continues for all possible selections.Eventually, the set contains:124132134142214234312314324342412432So:ms.size() = 12The final answer is:12Why a HashSet Is UsefulThere are two separate concerns in this problem.Generating valid arrangementsBacktracking ensures that every possible selection of three positions is explored.Removing duplicatesThe HashSet ensures that identical numbers generated through different index choices are counted only once.This combination is especially useful when the input contains duplicate values.Complexity AnalysisLet n be the number of digits.At most 10 digits are given, and only three positions are selected.The number of possible index arrangements is:P(n, 3) = n × (n - 1) × (n - 2)Therefore, the number of generated arrangements is O(n³).For each completed arrangement, converting/checking the three-digit number takes constant time because the number always has exactly three digits.Time ComplexityO(n³)With n ≤ 10, this is very small in practice.Space ComplexityThe recursion depth is at most 3, while the HashSet stores the distinct valid numbers.So the auxiliary recursion space is O(1), excluding the result set.The result set contains at most a constant number of three-digit numbers because there are only 900 possible three-digit numbers.A Simpler ObservationThere is another way to think about the problem.A three-digit even number has the structure:Hundreds → Tens → UnitsThe units digit must be one of:0, 2, 4, 6, 8The hundreds digit cannot be zero.The tens digit can be any remaining available digit.This means the problem could also be solved by directly choosing:first digitsecond digitthird digitand checking whether the resulting number is valid.The backtracking solution generalizes this idea nicely because it systematically explores all possibilities.Interview TipWhen a problem asks to create numbers, strings, or arrangements from a small collection of elements, look for permutation/backtracking patterns.A useful checklist is:What is being built?↓How many elements are needed?↓Can an element be reused?↓How are duplicates handled?↓What makes a completed arrangement valid?For this problem:Build → 3-digit numberReuse → No, each copy onceDuplicates → HashSetValidity → No leading zero + evenThat immediately points toward a small backtracking solution.ConclusionLeetCode 3483 is a good introduction to combining backtracking with duplicate handling.The recursive function explores every possible three-digit arrangement while the boolean array ensures that each input position is used at most once. Once a number is completed, it is checked for the leading-zero and even-number conditions.Finally, a HashSet guarantees that duplicate numbers are counted only once.With at most 10 input digits and only three positions to fill, the brute-force search remains highly efficient and easy to understand.

LeetCodeJavaBacktrackingRecursionHashSetArraysPermutationsStringEasy
LeetCode 3550: Smallest Index With Digit Sum Equal to Index – Java Solution & Explanation

LeetCode 3550: Smallest Index With Digit Sum Equal to Index – Java Solution & Explanation

IntroductionLeetCode 3550, Smallest Index With Digit Sum Equal to Index, combines two simple ideas:Traversing an array using its indexFinding the sum of the digits of an integerFor every index i, the task is to check whether:sum of digits of nums[i] = iThe smallest valid index should be returned.If no index satisfies the condition, the answer is -1.Although the problem is easy, it is a good exercise for understanding how an array index can be compared with a property calculated from the corresponding value.Question LinkLeetCode 3550 – Smallest Index With Digit Sum Equal to IndexUnderstanding the ProblemConsider:nums = [1, 10, 11]Check every index from left to right.Index 0nums[0] = 1Digit sum:1Compare with index:1 != 0So index 0 does not work.Index 1nums[1] = 10Digit sum:1 + 0 = 1Now:digit sum = index1 = 1Therefore, index 1 satisfies the condition.Because the array is checked from left to right, this is automatically the smallest valid index.Answer = 1ApproachThe solution follows a simple left-to-right traversal.For every index i:Take nums[i].Calculate the sum of its digits.Compare that sum with i.Return i immediately when they are equal.If the complete array is checked without finding a match, return -1.The early return is important because the problem asks for the smallest index.There is no need to continue searching after finding the first valid index.Finding the Digit SumThe helper method so() calculates the sum of the digits.public int so(int n){ int su = 0; while(n != 0){ int dig = n % 10; n /= 10; su += dig; } return su;}Two operations are especially useful here.Extracting the last digitn % 10For example:123 % 10 = 3So the last digit is obtained.Removing the last digitn /= 10For example:123 / 10 = 12Since integer division is being used, the decimal part is discarded.Repeating these two operations gives every digit.For:123the process is:123 % 10 → 3123 / 10 → 1212 % 10 → 212 / 10 → 11 % 10 → 11 / 10 → 0Therefore:Digit Sum = 3 + 2 + 1 = 6Checking Each IndexThe main method traverses the array:for(int i = 0; i < nums.length; i++){For a single-digit number, the submitted solution directly compares the value with the index:if(nums[i] < 10){ if(i == nums[i]){ return i; }}For numbers containing multiple digits, it calculates their digit sum:else{ int su = so(nums[i]); if(su == i){ return i; }}This works correctly for the given constraints.Java SolutionHere is the submitted approach with clearer comments:class Solution { // Returns the sum of all digits of n public int so(int n){ int su = 0; while(n != 0){ // Extract the last digit int dig = n % 10; // Remove the last digit n /= 10; // Add the digit to the sum su += dig; } return su; } public int smallestIndex(int[] nums){ // Traverse from the smallest index for(int i = 0; i < nums.length; i++){ // Single-digit numbers can be compared directly if(nums[i] < 10){ if(i == nums[i]){ return i; } }else{ // Calculate digit sum for multi-digit numbers int su = so(nums[i]); if(su == i){ return i; } } } // No valid index was found return -1; }}Dry RunConsider:nums = [1, 3, 2]The indices are:0 1 2Index 0nums[0] = 1Digit sum:1Comparison:1 != 0Not valid.Index 1nums[1] = 3Digit sum:3Comparison:3 != 1Not valid.Index 2nums[2] = 2Digit sum:2Comparison:2 = 2A valid index is found.Therefore:Answer = 2The function immediately returns 2.Another ExampleConsider:nums = [1, 10, 11]IndexValueDigit SumMatch?011No1101Yes2112YesBoth indices 1 and 2 satisfy the condition.However, index 1 appears first.Therefore:Answer = 1This demonstrates why traversing from left to right and returning immediately is enough to find the smallest valid index.A Small SimplificationThe special case for single-digit numbers is not actually necessary.The digit-sum method can also handle a single-digit number.For example:so(7)returns:7So the main logic can simply calculate the digit sum for every element.A slightly cleaner version is:class Solution { public int digitSum(int n){ int sum = 0; while(n > 0){ sum += n % 10; n /= 10; } return sum; } public int smallestIndex(int[] nums){ for(int i = 0; i < nums.length; i++){ if(digitSum(nums[i]) == i){ return i; } } return -1; }}This version expresses the core idea directly:digit sum of nums[i] == iThere is no need to treat single-digit and multi-digit numbers differently.Why Does the First Match Give the Smallest Index?The array is traversed in increasing order:0 → 1 → 2 → 3 → ...The moment a valid index is found, every smaller index has already been checked.Therefore, the first match must be the smallest possible answer.This is a common pattern in array problems:When the problem asks for the smallest index satisfying a condition, scan from left to right and return the first match.Complexity AnalysisLet:n = length of the arrayd = number of digits in an elementEach array element is visited once, and calculating its digit sum takes O(d) time.Therefore:Time Complexity: O(n × d)Under the given constraints, nums[i] <= 1000, so each number contains at most 4 digits. This makes d effectively bounded by a small constant, giving:Effective Time Complexity: O(n)The digit-sum calculation uses only a few integer variables.Space Complexity: O(1)Interview TipWhen a problem asks for the digit sum of an integer, remember this standard pattern:while(n > 0){ sum += n % 10; n /= 10;}The two operations have very specific purposes:n % 10 → extract the last digitn / 10 → remove the last digitAlso, when a problem asks for the smallest index, checking elements from left to right often allows an immediate return as soon as the condition is satisfied.ConclusionLeetCode 3550 is a small problem, but it combines several useful programming fundamentals:Array traversalIndex-based conditionsDigit extractionInteger divisionEarly returnConstant-space problem solvingThe main condition is simple:digitSum(nums[i]) == iBy scanning the array from left to right, the first matching index is automatically the smallest one.The solution runs in O(n × d) time, which is effectively O(n) for the given constraints, and uses O(1) extra space.

LeetCodeJavaArraysMathEasyDSA
Count the Number of Good Subarrays – Apple OA DSA Problem & Java Solution

Count the Number of Good Subarrays – Apple OA DSA Problem & Java Solution

IntroductionThis is an interesting question asked in the Apple OA because the condition is slightly different from the usual subarray problems.Instead of asking whether a subarray itself satisfies some property, the task is to remove a subarray and check whether the elements left behind are strictly increasing.The challenge is to count every possible contiguous subarray whose removal leaves a strictly increasing sequence.With n as large as 10⁵, a direct brute-force solution becomes extremely expensive, making this a good problem for thinking about how the structure of the array can be exploited.Problem StatementGiven an integer array arr of size n, find the number of good subarrays.A subarray arr[l...r] is called good if, after removing all elements from index l through r, the remaining elements form a strictly increasing array.In other words, after removing:arr[l], arr[l+1], ..., arr[r]the elements before l and after r are joined together.The resulting array must satisfy:remaining[i] < remaining[i + 1]for every pair of adjacent elements.Constraints1 ≤ n ≤ 10⁵-10⁹ ≤ arr[i] ≤ 10⁹ExampleConsider:arr = [1, 2, 3, 4, 0, 5]There are 10 good subarrays.Some valid removals are:[0][0, 5][4, 0][4, 0, 5][3, 4, 0][3, 4, 0, 5][2, 3, 4, 0][2, 3, 4, 0, 5][1, 2, 3, 4, 0][1, 2, 3, 4]For example, removing:[3, 4, 0]from:[1, 2, 3, 4, 0, 5]leaves:[1, 2, 5]which is strictly increasing.Therefore, [3,4,0] is a good subarray.Similarly, removing:[0,5]leaves:[1,2,3,4]which is also strictly increasing.Hence:Answer = 10Key ObservationThe array:[1, 2, 3, 4, 0, 5]is already strictly increasing except around:4 → 0Removing a suitable contiguous section containing this problematic portion can reconnect two increasing parts.For example:[1, 2, 3, 4] [0, 5]Removing [0] gives:[1, 2, 3, 4, 5]Removing [4,0] gives:[1, 2, 3, 5]Removing [3,4,0] gives:[1, 2, 5]The important condition is therefore not just whether the removed part is valid. The remaining left and right portions must also connect correctly.Brute-Force Backtracking ApproachA straightforward way to explore the problem is to generate possible selections of indices using recursion.The recursive function maintains:currwhich contains the indices currently selected for removal.For every index, there are two choices:Include the index in the selected set.Do not include the index.Once a selection is created, the ch2() function removes those selected positions and checks whether the remaining array is strictly increasing.A HashSet is also used to avoid counting the same index selection more than once.Checking a CandidateThe ch2() function performs the validation.It first creates an array containing the elements that were not selected for removal.For example:Original:[1, 2, 3, 4, 0, 5]Selected indices:[2, 3, 4]Remaining:[1, 2, 5]Then the remaining array is scanned.If any adjacent pair violates:arr[i] < arr[i + 1]the candidate is rejected.Otherwise, it is considered good.Java SolutionThe following is the provided recursive implementation, with comments added to make the logic easier to follow.public class question {// Stores already counted selections.static int an = 0;static HashSet<List<Integer>> msl = new HashSet<>();// Checks whether removing the selected indices// leaves a strictly increasing array.public static boolean ch2(List<Integer> lis, int[] arr) {// No selected elements means nothing is removed.if (lis.size() == 0) {return false;}// If the entire array is removed,// this implementation does not count it.int[] dum = new int[arr.length - lis.size()];if (dum.length == 0) {return false;}// Store the selected indices.HashSet<Integer> ms = new HashSet<>();for (int a : lis) {ms.add(a);}int c = 0;// Construct the remaining array.for (int i = 0; i < arr.length; i++) {// Keep only indices that were not selected.if (!ms.contains(i)) {dum[c] = arr[i];c++;}}// Check whether the remaining array// is strictly increasing.for (int i = 0; i < dum.length - 1; i++) {if (dum[i] > dum[i + 1]) {return false;}}return true;}public static void main(String[] args) {int[] arr = {1, 2, 3, 4, 0, 5};List<Integer> lis = new ArrayList<>();if (arr.length == 1) {System.out.println(1);}// Explore both possibilities for the first index.sol2(arr, 0, lis, true);sol2(arr, 0, lis, false);System.out.println(an);}// Generates different selections using recursion.public static void sol2(int[] arr,int ind,List<Integer> lis,boolean boo) {// All indices have been processed.if (ind == arr.length) {// Check whether this selection is good// and has not already been counted.if (!msl.contains(lis) && ch2(lis, arr)) {an++;// Store a copy because the original list// continues changing during backtracking.msl.add(new ArrayList<>(lis));System.out.println(lis);}return;}// Check the current selection as well.if (!msl.contains(lis) && ch2(lis, arr)) {an++;msl.add(new ArrayList<>(lis));System.out.println(lis);}// Include the current index.if (boo) {lis.add(ind);}// Continue recursively.sol2(arr, ind + 1, lis, true);// Backtrack and remove the last selected index.if (lis.size() != 0) {lis.remove(lis.size() - 1);}// Explore the branch where the current index// is not selected.sol2(arr, ind + 1, lis, false);}}Dry RunFor:[1, 2, 3, 4, 0, 5]Suppose the selected indices are:[4]Index 4 contains 0.Removing it gives:[1, 2, 3, 4, 5]This is strictly increasing.Therefore:[0]is counted.Now consider:[3, 4]Removing indices 2 and 3 gives:[1, 2, 0, 5]Since:2 > 0the remaining array is not increasing.Therefore, this candidate is rejected.Another candidate:[2, 3, 4]removes:[3, 4, 0]and leaves:[1, 2, 5]which is strictly increasing.So this candidate is counted.The recursion continues until all possible selections have been explored.Important Detail: Subarray vs SubsetThere is an important distinction worth understanding when reviewing this implementation.The problem asks for a subarray, which means the removed elements must be contiguous.For example:[2, 3, 4]is a subarray.But:[2, 4]is not a subarray if the original array contains another element between them.The recursive code, however, chooses individual indices independently. Therefore, it explores subsets of indices rather than explicitly restricting the selection to contiguous ranges.This is an important conceptual difference.For a production or interview solution, the recursion should be changed to work directly with:left indexright indexso that only contiguous removals are considered.Why This Approach Is Not Suitable for n = 10⁵For every index, the recursion can make two choices:includeexcludeThis creates approximately:2ⁿpossible selections.Furthermore, each candidate is checked by constructing another array and scanning it.Therefore, the brute-force approach grows exponentially and cannot handle:n = 100000efficiently.This makes the problem much more interesting: the real challenge is finding the structure that allows the count to be obtained without examining every possible subarray individually.Toward an Optimized SolutionA useful way to analyze the problem is to split the remaining array into two parts.After removing arr[l...r], the remaining array is:arr[0...l-1] + arr[r+1...n-1]For this combined array to be strictly increasing, three things must be true:Left part must be increasingarr[0...l-1]must already be strictly increasing.Right part must be increasingarr[r+1...n-1]must already be strictly increasing.The two parts must connectIf both parts exist:arr[l-1] < arr[r+1]must hold.This observation removes the need to construct the remaining array for every candidate.The Main PatternThe problem can therefore be viewed as:Increasing Prefix|| removed subarray↓Increasing SuffixThe only difficult part is determining whether the last element of the prefix can connect to the first element of the suffix.This is the key observation that leads from brute force toward an efficient two-pointer/prefix-suffix based solution.Complexity of the Provided SolutionThe recursive generation can explore up to:O(2ⁿ)different index selections.For every candidate, ch2() may also scan the entire array.Therefore, the overall worst-case complexity is approximately:O(n × 2ⁿ)with substantial additional memory for the recursion and stored selections.This is only practical for very small arrays and should be considered a brute-force exploration, not a solution for the stated n = 10⁵ constraint.Interview TakeawayThis problem teaches an important DSA lesson:Before optimizing code, identify exactly what the problem is asking for.There is a significant difference between:subsetand:subarrayA subset can select arbitrary indices, while a subarray must be contiguous.Once the problem is represented correctly, the next question becomes:What must be true about the array remaining after removing [l...r]?That leads directly to the prefix/suffix observation:Left side increasing+Right side increasing+Left boundary < Right boundaryThis type of transformation is often much more valuable in an OA than trying to optimize an exponential recursion line by line.ConclusionThis Apple OA-style problem combines subarray reasoning, recursion, and array properties in an interesting way.The provided solution takes a brute-force route by recursively exploring possible index selections and checking whether the remaining elements form a strictly increasing sequence.Although this approach is useful for understanding the problem and verifying small test cases, the constraint of 10⁵ requires a much more efficient strategy.The major insight is to stop thinking about the removed portion itself and instead analyze the increasing prefix and increasing suffix that remain after the removal.That shift in perspective turns an exponential search problem into one that can be approached using prefix/suffix preprocessing and two-pointer techniques.

Apple OAArraysSubarraysRecursionBacktrackingHashSetBrute ForceJavaContignous Array
LeetCode 187 – Repeated DNA Sequences (Java Solution with Sliding Window and HashSet)

LeetCode 187 – Repeated DNA Sequences (Java Solution with Sliding Window and HashSet)

IntroductionIn this article, we will solve LeetCode 187: Repeated DNA Sequences using Java. This is a popular string problem that tests your understanding of the sliding window technique and efficient use of hash-based data structures.DNA sequences are composed of four characters:A (Adenine)C (Cytosine)G (Guanine)T (Thymine)The goal is to identify all 10-letter-long substrings that appear more than once in a given DNA string.You can try solving the problem directly on LeetCode here: https://leetcode.com/problems/repeated-dna-sequences/Problem StatementGiven a string s that represents a DNA sequence, return all the 10-letter-long substrings that occur more than once.Example 1Input: s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"Output: ["AAAAACCCCC", "CCCCCAAAAA"]Example 2Input: s = "AAAAAAAAAAAAA"Output: ["AAAAAAAAAA"]Key ObservationsWe only need substrings of fixed length 10.The maximum length of the string can be up to 10^5.A brute-force solution checking all substrings multiple times would be inefficient.This problem can be solved efficiently using a sliding window and hash-based data structures.Approach 1: Sliding Window with HashSet (Given Solution)IdeaUse two pointers (i and j) to maintain a sliding window.Build a substring of size 10 dynamically.Store previously seen substrings in a HashSet.If a substring is already present in the set:Check if it is already in the result list.If not, add it to the result list.Slide the window forward and continue.Java Code (Your Implementation)class Solution { public List<String> findRepeatedDnaSequences(String s) { HashSet<String> ms = new HashSet<>(); List<String> lis = new ArrayList<>(); int i = 0; int j = 0; String tes = ""; while (j < s.length()) { tes += s.charAt(j); if (j - i + 1 < 10) { j++; } else { if (j - i + 1 == 10) { if (ms.contains(tes)) { boolean fl = false; for (String a : lis) { if (a.equals(tes)) { fl = true; } } if (!fl) { lis.add(tes); } } else { ms.add(tes); } tes = tes.substring(1); i++; j++; } } } return lis; }}ExplanationThe variable tes maintains the current substring.ms stores all previously seen substrings of length 10.If a substring already exists in ms, we manually check whether it has already been added to the result list.This avoids duplicate entries in the final output.Time ComplexitySliding through the string: O(n)Checking duplicates in the result list: O(n) in the worst caseOverall worst-case complexity: O(n²)Space ComplexityHashSet storage: O(n)Limitation of Approach 1The manual duplicate check using a loop inside the result list introduces unnecessary overhead. This makes the solution less efficient.We can improve this by using another HashSet to automatically handle duplicates.Approach 2: Optimized Solution Using Two HashSetsIdeaUse one HashSet called seen to track all substrings of length 10.Use another HashSet called repeated to store substrings that appear more than once.Iterate from index 0 to s.length() - 10.Extract substring of length 10.If adding to seen fails, it means it has appeared before.Add it directly to repeated.This removes the need for a nested loop.Optimized Java Codeclass Solution { public List<String> findRepeatedDnaSequences(String s) { Set<String> seen = new HashSet<>(); Set<String> repeated = new HashSet<>(); for (int i = 0; i <= s.length() - 10; i++) { String substring = s.substring(i, i + 10); if (!seen.add(substring)) { repeated.add(substring); } } return new ArrayList<>(repeated); }}Why This Approach is BetterNo manual duplicate checking.Cleaner and more readable code.Uses HashSet properties efficiently.Each substring is processed only once.Time Complexity (Optimized)Single traversal of the string: O(n)Substring extraction of fixed length 10: O(1)Overall time complexity: O(n)Space ComplexityTwo HashSets storing substrings: O(n)ConclusionLeetCode 187 is a classic example of combining the sliding window technique with hash-based data structures.The first approach works but has unnecessary overhead due to manual duplicate checks.The second approach is more optimal, cleaner, and recommended for interviews.Always leverage the properties of HashSet to avoid redundant checks.This problem highlights the importance of choosing the right data structure to optimize performance.

JavaSliding WindowMedium
LeetCode 844: Backspace String Compare — Java Solution With All Approaches Explained

LeetCode 844: Backspace String Compare — Java Solution With All Approaches Explained

IntroductionLeetCode 844 Backspace String Compare is a fantastic problem that shows up frequently in coding interviews. It combines string manipulation, stack simulation, and even has a follow-up that challenges you to solve it in O(1) space — which is what separates a good candidate from a great one.Here is the Link of Question -: LeetCode 844In this article we cover a plain English explanation, real life analogy, 3 Java approaches including the O(1) space two pointer solution, dry runs, complexity analysis, common mistakes, and FAQs.What Is the Problem Really Asking?You are given two strings s and t. Both contain lowercase letters and # characters. Think of # as the backspace key on your keyboard — it deletes the character just before it. If there is nothing to delete, it does nothing.Process both strings through these backspace operations and check if the resulting strings are equal. Return true if equal, false otherwise.Quick Example:s = "ab#c" → # deletes b → becomes "ac"t = "ad#c" → # deletes d → becomes "ac"Both equal "ac" → return true ✅Real Life Analogy — The Keyboard TypoYou are typing a message. You type "ab", realize you made a typo, hit backspace, and type "c". Your friend types "ad", hits backspace, and types "c". Even though you both typed differently, the final message on screen is the same — "ac".That is exactly what this problem is about. Two people typing differently but ending up with the same result.Approach 1: StringBuilder as Stack (Optimal & Clean) ✅The IdeaThis is your own solution and the best O(n) approach. Process each string independently using a StringBuilder as a stack:Letter → append to StringBuilder (push)# → delete last character if StringBuilder is not empty (pop)Then simply compare the two resulting StringBuilders.public boolean backspaceCompare(String s, String t) { return process(s).equals(process(t));}private String process(String str) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c == '#') { if (sb.length() > 0) { sb.deleteCharAt(sb.length() - 1); } } else { sb.append(c); } } return sb.toString();}Notice how extracting a process() helper method makes the code cleaner and avoids repeating the same loop twice — a great habit to show in interviews!Dry Run — s = "ab##", t = "c#d#"Processing s = "ab##":a → append → "a"b → append → "ab"# → delete last → "a"# → delete last → ""Processing t = "c#d#":c → append → "c"# → delete last → ""d → append → "d"# → delete last → ""Both result in "" → return true ✅Time Complexity: O(n + m) — where n and m are lengths of s and t Space Complexity: O(n + m) — two StringBuilders storing processed stringsApproach 2: Stack Based Solution (Interview Classic)The IdeaSame logic as above but using explicit Stack<Character> objects. Great for explaining your thought process clearly in an interview even though StringBuilder is cleaner.public boolean backspaceCompare(String s, String t) { return processStack(s).equals(processStack(t));}private String processStack(String str) { Stack<Character> st = new Stack<>(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (c == '#') { if (!st.empty()) { st.pop(); } } else { st.push(c); } } StringBuilder sb = new StringBuilder(); while (!st.empty()) { sb.append(st.pop()); } return sb.reverse().toString();}Dry Run — s = "a#c", t = "b"Processing s = "a#c":a → push → stack: [a]# → pop → stack: []c → push → stack: [c]Result: "c"Processing t = "b":b → push → stack: [b]Result: "b""c" does not equal "b" → return false ✅Time Complexity: O(n + m) Space Complexity: O(n + m)Approach 3: Two Pointer — O(1) Space (Follow-Up Solution) 🔥This is the follow-up the problem asks about — can you solve it in O(n) time and O(1) space? This means no extra StringBuilder or Stack allowed.The IdeaInstead of building processed strings, traverse both strings from right to left simultaneously. Keep a count of pending backspaces. Skip characters that would be deleted and compare characters that survive.Why right to left? Because # affects characters to its left, so processing from the end lets us know upfront how many characters to skip.public boolean backspaceCompare(String s, String t) { int i = s.length() - 1; int j = t.length() - 1; int skipS = 0, skipT = 0; while (i >= 0 || j >= 0) { // Find next valid character in s while (i >= 0) { if (s.charAt(i) == '#') { skipS++; i--; } else if (skipS > 0) { skipS--; i--; } else { break; } } // Find next valid character in t while (j >= 0) { if (t.charAt(j) == '#') { skipT++; j--; } else if (skipT > 0) { skipT--; j--; } else { break; } } // Compare the valid characters if (i >= 0 && j >= 0) { if (s.charAt(i) != t.charAt(j)) { return false; } } else if (i >= 0 || j >= 0) { return false; // one string still has chars, other doesn't } i--; j--; } return true;}Dry Run — s = "ab#c", t = "ad#c"Starting from the right end of both strings:Round 1:s[3] = 'c' → valid, no skips → stopt[3] = 'c' → valid, no skips → stopCompare 'c' == 'c' ✅ → move both pointers leftRound 2:s[2] = '#' → skipS = 1, move lefts[1] = 'b' → skipS > 0, skipS = 0, move lefts[0] = 'a' → valid, stopt[2] = '#' → skipT = 1, move leftt[1] = 'd' → skipT > 0, skipT = 0, move leftt[0] = 'a' → valid, stopCompare 'a' == 'a' ✅ → move both pointers leftBoth pointers exhausted → return true ✅Time Complexity: O(n + m) — each character visited at most once Space Complexity: O(1) — only pointer and counter variables, no extra storage!Approach ComparisonThe StringBuilder approach is the easiest to write and explain. The Stack approach is slightly more verbose but shows clear intent. The Two Pointer approach is the hardest to code but the most impressive — it solves the follow-up and uses zero extra space.In an interview, start with the StringBuilder solution, explain it clearly, then mention the Two Pointer approach as the O(1) space optimization if asked.How This Fits the Stack Simulation PatternYou have now seen this same pattern across four problems:3174 Clear Digits — digit deletes closest left non-digit 2390 Removing Stars — star deletes closest left non-star 1047 Remove Adjacent Duplicates — character cancels matching top of stack 844 Backspace String Compare — # deletes closest left character, then compare two stringsAll four use the same StringBuilder-as-stack core. The only differences are the trigger character and what you do with the result. This is the power of pattern recognition in DSA.Common Mistakes to AvoidNot handling backspace on empty string When # appears but the StringBuilder is already empty, do nothing. Always guard with sb.length() > 0 before calling deleteCharAt. The problem explicitly states backspace on empty text keeps it empty.Using Stack and forgetting to handle # when stack is empty In the Stack approach, only pop if the stack is not empty. Pushing # onto the stack when it is empty is a common bug that gives wrong answers.In Two Pointer, comparing before both pointers find valid characters Make sure both inner while loops fully complete before comparing. Comparing too early is the most common mistake in the O(1) space solution.FAQs — People Also AskQ1. What is the best approach for LeetCode 844 in Java? For most interviews, the StringBuilder approach is the best — clean, readable, and O(n) time. If the interviewer asks for O(1) space, switch to the Two Pointer approach traversing from right to left.Q2. How does the O(1) space solution work for LeetCode 844? It uses two pointers starting from the end of both strings, keeping a skip counter to track pending backspaces. Characters that would be deleted are skipped, and only surviving characters are compared.Q3. What is the time complexity of LeetCode 844? All three approaches run in O(n + m) time where n and m are the lengths of the two strings. The Two Pointer approach achieves this with O(1) space instead of O(n + m).Q4. Why traverse from right to left in the Two Pointer approach? Because # affects characters to its left. Scanning from the right lets you know upfront how many characters to skip before you reach them, avoiding the need to store anything.Q5. Is LeetCode 844 asked in Google interviews? Yes, it is commonly used as a warmup or screening problem. The follow-up O(1) space solution is what makes it interesting for senior-level interviews.Similar LeetCode Problems to Practice Next1047. Remove All Adjacent Duplicates In String — Easy — same stack pattern2390. Removing Stars From a String — Medium — star as backspace3174. Clear Digits — Easy — digit as backspace1209. Remove All Adjacent Duplicates in String II — Medium — k adjacent duplicates678. Valid Parenthesis String — Medium — stack with wildcardsConclusionLeetCode 844 Backspace String Compare is a well-rounded problem that tests string manipulation, stack simulation, and space optimization all in one. The StringBuilder solution is your go-to for interviews. But always be ready to explain the Two Pointer O(1) space follow-up — that is what shows real depth of understanding.Check out these problems alongside 1047, 2390, and 3174 and you will have the entire stack simulation pattern locked down for any coding interview.

StringStackTwo PointerString Builder
What Is Dynamic Programming? Origin Story, Real-Life Uses, LeetCode Problems & Complete Beginner Guide

What Is Dynamic Programming? Origin Story, Real-Life Uses, LeetCode Problems & Complete Beginner Guide

Introduction — Why Dynamic Programming Feels Hard (And Why It Isn't)If you've ever stared at a LeetCode problem, read the solution, understood every single line, and still had absolutely no idea how someone arrived at it — welcome. You've just experienced the classic Dynamic Programming (DP) confusion.DP has a reputation. People treat it like some dark art reserved for competitive programmers or Google engineers. The truth? Dynamic Programming is one of the most logical, learnable, and satisfying techniques in all of computer science. Once it clicks, it really clicks.This guide will take you from zero to genuinely confident. We'll cover where DP came from, how it works, what patterns to learn, how to recognize DP problems, real-world places it shows up, LeetCode problems to practice, time complexity analysis, and the mistakes that trip up even experienced developers.Let's go.The Origin Story — Who Invented Dynamic Programming and Why?The term "Dynamic Programming" was coined by Richard Bellman in the early 1950s while working at RAND Corporation. Here's the funny part: the name was deliberately chosen to sound impressive and vague.Bellman was doing mathematical research that his employer — the US Secretary of Defense, Charles Wilson — would have found difficult to fund if described accurately. Wilson had a well-known distaste for the word "research." So Bellman invented a name that sounded suitably grand and mathematical: Dynamic Programming.In his autobiography, Bellman wrote that he picked the word "dynamic" because it had a precise technical meaning and was also impossible to use negatively. "Programming" referred to the mathematical sense — planning and decision-making — not computer programming.The underlying idea? Break a complex problem into overlapping subproblems, solve each subproblem once, and store the result so you never solve it twice.Bellman's foundational contribution was the Bellman Equation, which underpins not just algorithms but also economics, operations research, and modern reinforcement learning.So the next time DP feels frustrating, remember — even its inventor named it specifically to confuse people. You're in good company.What Is Dynamic Programming? (Simple Definition)Dynamic Programming is an algorithmic technique used to solve problems by:Breaking them down into smaller overlapping subproblemsSolving each subproblem only onceStoring the result (memoization or tabulation)Building up the final solution from those stored resultsThe key insight is overlapping subproblems + optimal substructure.Overlapping subproblems means the same smaller problems come up again and again. Instead of solving them every time (like plain recursion does), DP solves them once and caches the answer.Optimal substructure means the optimal solution to the whole problem can be built from optimal solutions to its subproblems.If a problem has both these properties — it's a DP problem.The Two Approaches to Dynamic Programming1. Top-Down with Memoization (Recursive + Cache)You write a recursive solution exactly as you would naturally, but add a cache (usually a dictionary or array) to store results you've already computed.fib(n):if n in cache: return cache[n]if n <= 1: return ncache[n] = fib(n-1) + fib(n-2)return cache[n]This is called memoization — remember what you computed so you don't repeat yourself.Pros: Natural to write, mirrors the recursive thinking, easy to reason about. Cons: Stack overhead from recursion, risk of stack overflow on large inputs.2. Bottom-Up with Tabulation (Iterative)You figure out the order in which subproblems need to be solved, then solve them iteratively from the smallest up, filling a table.fib(n):dp = [0, 1]for i from 2 to n:dp[i] = dp[i-1] + dp[i-2]return dp[n]This is called tabulation — fill a table, cell by cell, bottom to top.Pros: No recursion overhead, usually faster in practice, easier to optimize space. Cons: Requires thinking about the order of computation upfront.🧩 Dynamic Programming Template CodeBefore diving into how to recognize DP problems, here are ready-to-use Java templates for every major DP pattern. Think of these as your reusable blueprints — every DP problem you ever solve will fit into one of these structures. Just define your state, plug in your recurrence relation, and you are good to go.Template 1 — Top-Down (Memoization)import java.util.HashMap;import java.util.Map;public class TopDownDP {Map<Integer, Integer> memo = new HashMap<>();public int solve(int n) {// Base caseif (n <= 1) return n;// Check cacheif (memo.containsKey(n)) return memo.get(n);// Recurrence relation — change this part for your problemint result = solve(n - 1) + solve(n - 2);// Store in cachememo.put(n, result);return result;}}Template 2 — Bottom-Up (Tabulation)public class BottomUpDP {public int solve(int n) {// Create DP tableint[] dp = new int[n + 1];// Base casesdp[0] = 0;dp[1] = 1;// Fill the table bottom-upfor (int i = 2; i <= n; i++) {// Recurrence relation — change this part for your problemdp[i] = dp[i - 1] + dp[i - 2];}return dp[n];}}Template 3 — Bottom-Up with Space Optimizationpublic class SpaceOptimizedDP {public int solve(int n) {// Only keep last two values instead of full tableint prev2 = 0;int prev1 = 1;for (int i = 2; i <= n; i++) {// Recurrence relation — change this part for your problemint curr = prev1 + prev2;prev2 = prev1;prev1 = curr;}return prev1;}}Template 4 — 2D DP (Two Sequences or Grid)public class TwoDimensionalDP {public int solve(String s1, String s2) {int m = s1.length();int n = s2.length();// Create 2D DP tableint[][] dp = new int[m + 1][n + 1];// Base cases — first row and columnfor (int i = 0; i <= m; i++) dp[i][0] = i;for (int j = 0; j <= n; j++) dp[0][j] = j;// Fill table cell by cellfor (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {// Recurrence relation — change this part for your problemif (s1.charAt(i - 1) == s2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = 1 + Math.min(dp[i - 1][j],Math.min(dp[i][j - 1], dp[i - 1][j - 1]));}}}return dp[m][n];}}Template 5 — Knapsack Patternpublic class KnapsackDP {public int solve(int[] weights, int[] values, int capacity) {int n = weights.length;// dp[i][w] = max value using first i items with capacity wint[][] dp = new int[n + 1][capacity + 1];for (int i = 1; i <= n; i++) {for (int w = 0; w <= capacity; w++) {// Don't take item idp[i][w] = dp[i - 1][w];// Take item i if it fitsif (weights[i - 1] <= w) {dp[i][w] = Math.max(dp[i][w],values[i - 1] + dp[i - 1][w - weights[i - 1]]);}}}return dp[n][capacity];}}💡 How to use these templates:Step 1 — Identify which pattern your problem fits into. Step 2 — Define what dp[i] or dp[i][j] means in plain English before writing any code. Step 3 — Write your recurrence relation on paper first. Step 4 — Plug it into the matching template above. Step 5 — Handle your specific base cases carefully.🎥 Visual Learning Resource — Watch This Before Moving ForwardIf you prefer learning by watching before reading, this free full-length course by freeCodeCamp is one of the best Dynamic Programming resources on the internet. Watch it alongside this guide for maximum understanding.Credit: freeCodeCamp — a free, nonprofit coding education platform.How to Recognize a Dynamic Programming ProblemAsk yourself these four questions:1. Can I define the problem in terms of smaller versions of itself? If you can write a recursive formula (recurrence relation), DP might apply.2. Do the subproblems overlap? If a naive recursive solution would recompute the same thing many times, DP is the right tool.3. Is there an optimal substructure? Is the best answer to the big problem made up of best answers to smaller problems?4. Are you looking for a count, minimum, maximum, or yes/no answer? DP problems often ask: "What is the minimum cost?", "How many ways?", "Can we achieve X?"Red flag words in problem statements: minimum, maximum, shortest, longest, count the number of ways, can we reach, is it possible, fewest steps.The Core DP Patterns You Must LearnMastering DP is really about recognizing patterns. Here are the most important ones:Pattern 1 — 1D DP (Linear) Problems where the state depends on previous elements in a single sequence. Examples: Fibonacci, Climbing Stairs, House Robber.Pattern 2 — 2D DP (Grid / Two-sequence) Problems with two dimensions of state, often grids or two strings. Examples: Longest Common Subsequence, Edit Distance, Unique Paths.Pattern 3 — Interval DP You consider all possible intervals or subarrays and build solutions from them. Examples: Matrix Chain Multiplication, Burst Balloons, Palindrome Partitioning.Pattern 4 — Knapsack DP (0/1 and Unbounded) You decide whether to include or exclude items under a capacity constraint. Examples: 0/1 Knapsack, Coin Change, Partition Equal Subset Sum.Pattern 5 — DP on Trees State is defined per node; you combine results from children. Examples: Diameter of Binary Tree, House Robber III, Maximum Path Sum.Pattern 6 — DP on Subsets / Bitmask DP State includes a bitmask representing which elements have been chosen. Examples: Travelling Salesman Problem, Shortest Superstring.Pattern 7 — DP on Strings Matching, editing, or counting arrangements within strings. Examples: Longest Palindromic Subsequence, Regular Expression Matching, Wildcard Matching.Top LeetCode Problems to Practice Dynamic Programming (With Links)Here are the essential problems, organized by difficulty and pattern. Solve them in this order.Beginner — Warm UpProblemPatternLinkClimbing Stairs1D DPhttps://leetcode.com/problems/climbing-stairs/Fibonacci Number1D DPhttps://leetcode.com/problems/fibonacci-number/House Robber1D DPhttps://leetcode.com/problems/house-robber/Min Cost Climbing Stairs1D DPhttps://leetcode.com/problems/min-cost-climbing-stairs/Best Time to Buy and Sell Stock1D DPhttps://leetcode.com/problems/best-time-to-buy-and-sell-stock/Intermediate — Core PatternsProblemPatternLinkCoin ChangeKnapsackhttps://leetcode.com/problems/coin-change/Longest Increasing Subsequence1D DPhttps://leetcode.com/problems/longest-increasing-subsequence/Longest Common Subsequence2D DPhttps://leetcode.com/problems/longest-common-subsequence/0/1 Knapsack (via Subset Sum)Knapsackhttps://leetcode.com/problems/partition-equal-subset-sum/Unique Paths2D Grid DPhttps://leetcode.com/problems/unique-paths/Jump Game1D DP / Greedyhttps://leetcode.com/problems/jump-game/Word BreakString DPhttps://leetcode.com/problems/word-break/Decode Ways1D DPhttps://leetcode.com/problems/decode-ways/Edit Distance2D String DPhttps://leetcode.com/problems/edit-distance/Triangle2D DPhttps://leetcode.com/problems/triangle/Advanced — Interview LevelProblemPatternLinkBurst BalloonsInterval DPhttps://leetcode.com/problems/burst-balloons/Regular Expression MatchingString DPhttps://leetcode.com/problems/regular-expression-matching/Wildcard MatchingString DPhttps://leetcode.com/problems/wildcard-matching/Palindrome Partitioning IIInterval DPhttps://leetcode.com/problems/palindrome-partitioning-ii/Maximum Profit in Job SchedulingDP + Binary Searchhttps://leetcode.com/problems/maximum-profit-in-job-scheduling/Distinct Subsequences2D DPhttps://leetcode.com/problems/distinct-subsequences/Cherry Pickup3D DPhttps://leetcode.com/problems/cherry-pickup/Real-World Use Cases of Dynamic ProgrammingDP is not just for coding interviews. It is deeply embedded in the technology you use every day.1. Google Maps & Navigation (Shortest Path) The routing engines behind GPS apps use DP-based algorithms like Dijkstra and Bellman-Ford to find the shortest or fastest path between two points across millions of nodes.2. Spell Checkers & Autocorrect (Edit Distance) When your phone corrects "teh" to "the," it is computing Edit Distance — a classic DP problem — between what you typed and every word in the dictionary.3. DNA Sequence Alignment (Bioinformatics) Researchers use the Needleman-Wunsch and Smith-Waterman algorithms — both DP — to align DNA and protein sequences and find similarities between species or identify mutations.4. Video Compression (MPEG, H.264) Modern video codecs use DP to determine the most efficient way to encode video frames, deciding which frames to store as full images and which to store as differences from the previous frame.5. Financial Portfolio Optimization Investment algorithms use DP to find the optimal allocation of assets under risk constraints — essentially a variant of the knapsack problem.6. Natural Language Processing (NLP) The Viterbi algorithm — used in speech recognition, part-of-speech tagging, and machine translation — is a DP algorithm. Every time Siri or Google Assistant understands your sentence, DP played a role.7. Game AI (Chess, Checkers) Game trees and minimax algorithms with memoization use DP to evaluate board positions and find the best move without recomputing already-seen positions.8. Compiler Optimization Compilers use DP to decide the optimal order of operations and instruction scheduling to generate the most efficient machine code.9. Text Justification (Word Processors) Microsoft Word and LaTeX use DP to optimally break paragraphs into lines — minimizing raggedness and maximizing visual appeal.10. Resource Scheduling in Cloud Computing AWS, Google Cloud, and Azure use DP-based scheduling to assign computational tasks to servers in the most cost-efficient way possible.Time Complexity Analysis of Common DP ProblemsUnderstanding the time complexity of DP is critical for interviews and for building scalable systems.ProblemTime ComplexitySpace ComplexityNotesFibonacci (naive recursion)O(2ⁿ)O(n)Exponential — terribleFibonacci (DP)O(n)O(1) with optimizationLinear — excellentLongest Common SubsequenceO(m × n)O(m × n)m, n = lengths of two stringsEdit DistanceO(m × n)O(m × n)Can optimize space to O(n)0/1 KnapsackO(n × W)O(n × W)n = items, W = capacityCoin ChangeO(n × amount)O(amount)Classic tabulationLongest Increasing SubsequenceO(n²) or O(n log n)O(n)Binary search version is fasterMatrix Chain MultiplicationO(n³)O(n²)Interval DPTravelling Salesman (bitmask)O(2ⁿ × n²)O(2ⁿ × n)Still exponential but manageable for small nThe general rule: DP trades time for space. You use memory to avoid recomputation. The time complexity equals the number of unique states multiplied by the work done per state.How to Learn and Master Dynamic Programming — Step by StepHere is an honest, structured path to mastery:Step 1 — Get recursion absolutely solid first. DP is memoized recursion at its core. If you cannot write clean recursive solutions confidently, DP will remain confusing. Practice at least 20 pure recursion problems first.Step 2 — Start with the classics. Fibonacci → Climbing Stairs → House Robber → Coin Change. These teach you the core pattern of defining state and transition without overwhelming you.Step 3 — Learn to define state explicitly. Before writing any code, ask: "What does dp[i] represent?" Write it in plain English. "dp[i] = the minimum cost to reach step i." This single habit separates good DP thinkers from struggling ones.Step 4 — Write the recurrence relation before coding. On paper or in a comment. Example: dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). If you can write the recurrence, the code writes itself.Step 5 — Master one pattern at a time. Don't jump between knapsack and interval DP in the same week. Spend a few days on each pattern until it feels intuitive.Step 6 — Solve the same problem both ways. Top-down and bottom-up. This builds deep understanding of what DP is actually doing.Step 7 — Optimize space after getting correctness. Many 2D DP solutions can use a single row instead of a full matrix. Learn this optimization after you understand the full solution.Step 8 — Do timed practice under interview conditions. Give yourself 35 minutes per problem. Review what you got wrong. DP is a muscle — it builds with reps.Common Mistakes in Dynamic Programming (And How to Avoid Them)Mistake 1 — Jumping to code before defining state. The most common DP error. Always define what dp[i] or dp[i][j] means before writing a single line of code.Mistake 2 — Wrong base cases. A single wrong base case corrupts every answer built on top of it. Trace through your base cases manually on a tiny example before running code.Mistake 3 — Off-by-one errors in indexing. Whether your dp array is 0-indexed or 1-indexed must be 100% consistent throughout. This causes more bugs in DP than almost anything else.Mistake 4 — Confusing top-down with bottom-up state order. In bottom-up DP, you must ensure that when you compute dp[i], all values it depends on are already filled. If you compute in the wrong order, you get garbage answers.Mistake 5 — Memoizing in the wrong dimension. In 2D problems, some people cache only one dimension when the state actually requires two. Always identify all variables that affect the outcome.Mistake 6 — Using global mutable state in recursion. If you use a shared array and don't clear it between test cases, you'll get wrong answers on subsequent inputs. Always scope your cache correctly.Mistake 7 — Not considering the full state space. In problems like Knapsack, forgetting that the state is (item index, remaining capacity) — not just item index — leads to fundamentally wrong solutions.Mistake 8 — Giving up after not recognizing the pattern immediately. DP problems don't announce themselves. The skill is learning to ask "is there overlapping subproblems here?" on every problem. This takes time. Don't mistake unfamiliarity for inability.Frequently Asked Questions About Dynamic ProgrammingQ: Is Dynamic Programming the same as recursion? Not exactly. Recursion is a technique for breaking problems into smaller pieces. DP is recursion plus memoization — or iterative tabulation. All DP can be written recursively, but not all recursion is DP.Q: What is the difference between DP and Divide and Conquer? Divide and Conquer (like Merge Sort) breaks problems into non-overlapping subproblems. DP is used when subproblems overlap — meaning the same subproblem is solved multiple times in a naive approach.Q: How do I know when NOT to use DP? If the subproblems don't overlap (no repeated computation), greedy or divide-and-conquer may be better. If the problem has no optimal substructure, DP won't give a correct answer.Q: Do I need to memorize DP solutions for interviews? No. You need to recognize patterns and be able to derive the recurrence relation. Memorizing solutions without understanding them will fail you in interviews. Focus on the thinking process.Q: How long does it take to get good at DP? Most people start to feel genuinely comfortable after solving 40–60 varied DP problems with deliberate practice. The first 10 feel impossible. The next 20 feel hard. After 50, patterns start feeling obvious.Q: What programming language is best for DP? Any language works. Python is often used for learning because its dictionaries make memoization trivial. C++ is preferred in competitive programming for its speed. For interviews, use whatever language you're most comfortable in.Q: What is space optimization in DP? Many DP problems only look back one or two rows to compute the current row. In those cases, you can replace an n×m table with just two arrays (or even one), reducing space complexity from O(n×m) to O(m). This is called space optimization or rolling array technique.Q: Can DP be applied to graph problems? Absolutely. Shortest path algorithms like Bellman-Ford are DP. Longest path in a DAG is DP. DP on trees is a rich subfield. Anywhere you have states and transitions, DP can potentially apply.Q: Is Greedy a type of Dynamic Programming? Greedy is related but distinct. Greedy makes locally optimal choices without reconsidering. DP considers all choices and picks the globally optimal one. Some DP solutions reduce to greedy when the structure allows, but they are different techniques.Q: What resources should I use to learn DP? For structured learning: Neetcode.io (organized problem list), Striver's DP Series on YouTube, and the book "Introduction to Algorithms" (CLRS) for theoretical depth. For practice: LeetCode's Dynamic Programming study plan and Codeforces for competitive DP.Final Thoughts — Dynamic Programming Is a SuperpowerDynamic Programming is genuinely one of the most powerful ideas in computer science. It shows up in your GPS, your autocorrect, your streaming video, your bank's risk models, and the AI assistants you talk to daily.The path to mastering it is not memorization. It is developing the habit of asking: can I break this into smaller problems that overlap? And then learning to define state clearly, write the recurrence, and trust the process.Start with Climbing Stairs. Write dp[i] in plain English before every problem. Solve everything twice — top-down and bottom-up. Do 50 problems with genuine reflection, not just accepted solutions.The click moment will come. And when it does, you'll wonder why it ever felt hard.

Dynamic ProgrammingMemoizationTabulationJavaOrigin StoryRichard Bellman
LeetCode 450: Delete Node in a BST – Java Optimized Recursive Solution with Dry Run

LeetCode 450: Delete Node in a BST – Java Optimized Recursive Solution with Dry Run

IntroductionThe Delete Node in a BST problem is one of the most important Binary Search Tree interview questions because it combines:BST traversalTree restructuringRecursive thinkingNode replacement logicTree manipulationUnlike searching or insertion, deletion is slightly more complex because we must maintain BST properties after removing a node.This problem is frequently asked in coding interviews and online assessments.Problem Link🔗 LeetCode 450 – Delete Node in a BSTProblem StatementGiven:The root of a Binary Search TreeA key valueDelete the node containing the key while preserving BST properties.Return the updated BST root.BST Property ReminderIn a Binary Search Tree:Left subtree -> smaller valuesRight subtree -> greater valuesAfter deletion:Tree must still remain a valid BST.Example 1Inputroot = [5,3,6,2,4,null,7]key = 3Output[5,4,6,2,null,null,7]VisualizationBefore deletion: 5 / \ 3 6 / \ \ 2 4 7After deleting 3: 5 / \ 4 6 / \ 2 7Key Deletion CasesBST deletion has 3 important cases.Case 1: Node Has No ChildSimply remove the node.Case 2: Node Has One ChildReplace the node with its child.Case 3: Node Has Two ChildrenThis is the tricky part.We:Find inorder predecessor or successorReplace nodeReconnect subtrees properlyIntuitionSuppose we want to delete:3from: 5 / \ 3 6 / \ 2 4Since node 3 has:Left childRight childwe cannot directly delete it.Instead:Attach right subtree to rightmost node of left subtreeReturn left subtree as replacementThis preserves BST ordering.Brute Force ApproachIdeaStore inorder traversalRemove target nodeRebuild BSTWhy Brute Force is BadProblems:Extra memory usageRebuilding tree is expensiveUnnecessary traversalBrute Force ComplexityTime ComplexityO(N)Space ComplexityO(N)Optimized BST Deletion ApproachUse BST properties to:Search efficientlyModify only required nodesPreserve tree structureJava Solutionclass Solution { public TreeNode deleteNode(TreeNode root, int key) { if(root == null) return root; if(root.val == key) return solve(root); TreeNode originalRoot = root; while(root != null) { if(root.val > key) { if(root.left != null && root.left.val == key) { root.left = solve(root.left); } else { root = root.left; } } else { if(root.right != null && root.right.val == key) { root.right = solve(root.right); } else { root = root.right; } } } return originalRoot; } public TreeNode solve(TreeNode root) { if(root.left == null) return root.right; if(root.right == null) return root.left; TreeNode rightChild = root.right; TreeNode leftChild = asright(root.left); leftChild.right = rightChild; return root.left; } public TreeNode asright(TreeNode root) { if(root.right == null) return root; return asright(root.right); }}How This Solution WorksThe main logic happens inside:solve(root)This function deletes the node safely.Understanding solve()Case 1If:root.left == nullreturn right subtree.Case 2If:root.right == nullreturn left subtree.Case 3If both children exist:Save right subtreeFind rightmost node in left subtreeAttach right subtree thereReturn left subtreeWhy Rightmost Node?Because:Rightmost node of left subtreeis the:largest node smaller than rootThis maintains BST ordering perfectly.Dry RunInput 5 / \ 3 6 / \ \ 2 4 7key = 3Step 1Search node:3Step 2Node has:Left child = 2Right child = 4Step 3Find rightmost node in left subtree.Rightmost node:2Step 4Attach right subtree:2.right = 4Step 5Return left subtree:2Updated BST becomes valid.Time Complexity AnalysisBest CaseO(log N)Balanced BST.Worst CaseO(N)Skewed BST.Space ComplexityRecursive HelperO(H)where:H = tree heightAlternative Recursive ApproachAnother common method:Replace node with inorder successorDelete successor recursivelyThis approach is also interview friendly.Interview ExplanationIn interviews explain:When deleting a node with two children, we preserve BST properties by connecting the right subtree to the rightmost node of the left subtree.This demonstrates:BST restructuring knowledgeTree manipulation skillsRecursive reasoningPointer managementCommon Mistakes1. Forgetting BST PropertyDeletion should not break ordering.2. Losing SubtreesAlways reconnect children carefully.3. Incorrect Node ReplacementMany candidates replace node incorrectly.4. Not Handling Null CasesAlways check:root == nullproperly.FAQsQ1. Why is BST deletion difficult?Because tree structure must remain valid after removal.Q2. Why use rightmost node of left subtree?It is the largest smaller value.Perfect replacement candidate.Q3. Can we use inorder successor instead?Yes.Both predecessor and successor approaches work.Q4. What is deletion complexity?Balanced BST:O(log N)Worst case:O(N)Related BST ProblemsPractice these next:Insert into BSTSearch in BSTValidate BSTLowest Common Ancestor in BSTKth Smallest Element in BSTInorder Successor in BSTConclusionDelete Node in BST is one of the most important BST interview problems because it teaches:Tree restructuringRecursive manipulationPointer handlingBST property maintenanceThe key insight is:When deleting a node with two children, reconnect subtrees carefully so BST ordering remains valid.Mastering this problem makes advanced BST operations significantly easier.

BSTJavaBinary Search TreeLeetCodeTreeRecursionMedium
LeetCode 2033: Minimum Operations to Make a Uni-Value Grid | Java Solution Explained (Median Approach)

LeetCode 2033: Minimum Operations to Make a Uni-Value Grid | Java Solution Explained (Median Approach)

IntroductionIn this problem, we are given a 2D grid and an integer x.We can perform one operation where we either:Add x to any grid elementSubtract x from any grid elementOur goal is to make all elements in the grid equal using the minimum number of operations.If making all values equal is not possible, we return -1.This problem may initially look like a matrix manipulation question, but the actual logic is based on:Array transformationMedian propertyGreedy optimization# Problem LinkProblem StatementYou are given:A 2D integer grid of size m × nAn integer xYou can perform operations where you add or subtract x from any cell.A grid becomes uni-value when all elements become equal.Return the minimum number of operations needed.If impossible, return -1.ExampleExample 1Input:grid = [[2,4],[6,8]]x = 2Output:4Explanation:We can make every element equal to 4.2 → 4 (1 operation)6 → 4 (1 operation)8 → 4 (2 operations)Total = 4 operations.Key ObservationBefore solving the problem, we must understand one important rule.If we can add or subtract only x, then:All numbers must belong to the same remainder group when divided by x.Meaning:value % x must be same for every elementWhy?Because if two numbers have different remainders, they can never become equal using only +x or -x operations.IntuitionWe need to convert all numbers into a single target value.But what target value gives minimum operations?The answer is:MedianFor minimizing total absolute distance, median gives the optimal answer.Since every operation changes value by x, we can:Flatten grid into a listSort the listPick median as targetCalculate operations requiredWhy Median Works?Median minimizes:Sum of absolute differencesFor example:Numbers = [1, 2, 3, 10]If target = 2|1-2| + |2-2| + |3-2| + |10-2| = 10If target = 5|1-5| + |2-5| + |3-5| + |10-5| = 14Median gives minimum total distance.Approach 1: Brute ForceWe can try every possible number as target.For each target:Calculate operations requiredStore minimum answerTime ComplexityO(N²)Where N = total grid elementsThis is slow for large constraints.Approach 2: Optimal Median ApproachThis is the best approach.StepsStep 1: Flatten GridConvert 2D grid into 1D array.Step 2: Sort ArraySorting helps us find median.Step 3: Check ValidityAll values must have same remainder when divided by x.value % x must matchOtherwise return -1.Step 4: Pick MedianMedian minimizes operations.Step 5: Count OperationsFor every element:operations += abs(value - median) / xJava Solutionclass Solution {public int minOperations(int[][] grid, int x) {List<Integer> lis = new ArrayList<>();for (int i = 0; i < grid.length; i++) {for (int j = 0; j < grid[0].length; j++) {lis.add(grid[i][j]);}}Collections.sort(lis);int mid = lis.size() / 2;int remainder = lis.get(0) % x;for (int value : lis) {if (value % x != remainder) {return -1;}}int ans = 0;for (int value : lis) {int diff = Math.abs(lis.get(mid) - value);ans += diff / x;}return ans;}}Code ExplanationStep 1: Flatten Matrixlis.add(grid[i][j]);We convert grid into list.Step 2: Sort ListCollections.sort(lis);Sorting allows median selection.Step 3: Check Possibilityif(value % x != remainder)If remainders differ, answer becomes impossible.Step 4: Select Medianint mid = lis.size()/2;Median becomes target value.Step 5: Calculate Operationsans += diff/x;Difference divided by x gives operations.Dry RunInput:grid = [[2,4],[6,8]]x = 2Flatten:[2,4,6,8]Sort:[2,4,6,8]Median:6Operations:2 → 6 = 2 operations4 → 6 = 1 operation6 → 6 = 0 operation8 → 6 = 1 operationTotal:4 operationsTime ComplexityFlatten GridO(N)SortingO(N log N)Traverse ArrayO(N)Total ComplexityO(N log N)Where:N = m × nSpace ComplexityWe store all elements in list.O(N)Common Mistakes1. Forgetting Mod CheckMany people directly calculate operations.But without checking:value % xanswer may become invalid.2. Choosing Average Instead of MedianAverage does not minimize absolute distance.Median is required.3. Not Sorting Before Finding MedianMedian requires sorted array.4. Forgetting Division by xOperations are not direct difference.Correct formula:abs(target - value) / xEdge CasesCase 1All values already equal.Answer = 0Case 2Different modulo values.Return -1Case 3Single cell grid.No operation neededFAQsQ1. Why do we use median?Median minimizes total absolute difference.Q2. Why not average?Average minimizes squared distance, not absolute operations.Q3. Why modulo condition is important?Because we can only move by multiples of x.Q4. Can we solve without sorting?Sorting is easiest way to get median.Alternative median-finding algorithms exist but are unnecessary here.Interview InsightInterviewers ask this problem to test:Greedy thinkingMedian propertyMathematical observationArray flatteningOptimization logicConclusionLeetCode 2033 is a great problem that combines math with greedy logic.The most important learning points are:Flatten the gridValidate modulo conditionUse median as targetCalculate operations using difference divided by xThis approach is optimal and easy to implement.Once you understand why median works, this problem becomes very straightforward.

ArraySortingMathMedianMediumMatrixGridLeetCodeJava
LeetCode 1807: Evaluate the Bracket Pairs of a String – Java Solution & Explanation

LeetCode 1807: Evaluate the Bracket Pairs of a String – Java Solution & Explanation

IntroductionLeetCode 1807, Evaluate the Bracket Pairs of a String, is a string-processing problem that combines two common techniques:Using a HashMap for fast key-value lookupTraversing a string while keeping track of whether the current characters belong to a bracket pairThe string contains ordinary characters as well as bracketed keys such as:(name)(age)(city)A separate knowledge list provides the value associated with each known key.For every bracket pair:If the key exists in knowledge, replace the entire pair with its value.If the key is not present, replace the entire pair with ?.For example:s = "(name)is(age)yearsold"with:knowledge = [ ["name", "bob"], ["age", "two"]]becomes:bobistwoyearsoldThe main challenge is identifying exactly which characters belong to a bracketed key while keeping the characters outside brackets unchanged.Question LinkLeetCode 1807 – Evaluate the Bracket Pairs of a StringUnderstanding the ProblemConsider:s = "(name)is(age)yearsold"The bracket pairs are:(name)(age)The text outside the brackets is:isyearsoldSuppose:name → bobage → twoThen the transformation is:(name) → bob(age) → twoSo the final result becomes:bobistwoyearsoldThe characters outside brackets should not be interpreted as keys.For example:s = "(a)(a)aaa"with:a → yesproduces:yesyesaaaThe final aaa remains unchanged because those characters are not inside brackets.ApproachThe solution can be divided into two parts.Store the Knowledge in a HashMapThe knowledge array contains pairs such as:["name", "bob"]["age", "two"]These pairs are stored in a HashMap:HashMap<String, String> mp = new HashMap<>();for(int i = 0; i < knowledge.size(); i++){ mp.put( knowledge.get(i).get(0), knowledge.get(i).get(1) );}This gives direct access to a value using its key.For example:mp.get("name")returns:bobAnd:mp.containsKey("unknown")can be used to determine whether a key exists.Because each key appears at most once in knowledge, there is no need to handle duplicate key definitions.Detecting a Bracketed KeyThe solution uses a boolean variable:boolean bo = false;This represents whether the current position is inside a bracket pair.When the character immediately before the current character is '(':if(i > 0 && s.charAt(i - 1) == '('){ key = ""; bo = true;}the solution starts collecting a new key.For example:(name) ^Once n is reached, the previous character is '(', so the solution begins collecting:nnanamnameWhen ')' is encountered:if(s.charAt(i) == ')'){ bo = false;}the key is complete.The solution can then look it up in the map.Replacing the KeyWhen the closing bracket is found, the code checks whether the collected key exists:if(mp.containsKey(key)){ an += mp.get(key); key = "";}else{ an += "?"; key = "";}If the key is known:(name) → bobIf the key is unknown:(address) → ?This directly follows the problem statement.Keeping Characters Outside BracketsCharacters that are not part of a bracketed key are added directly to the result:if(!bo && (s.charAt(i) != ')' && s.charAt(i) != '(')){ an += s.charAt(i);}This is important because ordinary characters must remain unchanged.For:hi(name)the characters:hiare outside the brackets and are therefore copied directly.The key:nameis evaluated separately.Java SolutionHere is the submitted solution with comments explaining the important parts:class Solution { public String evaluate(String s, List<List<String>> knowledge) { // Store key -> value mappings HashMap<String, String> mp = new HashMap<>(); for(int i = 0; i < knowledge.size(); i++){ mp.put( knowledge.get(i).get(0), knowledge.get(i).get(1) ); } String key = ""; // true when we are currently reading a key boolean bo = false; String an = ""; for(int i = 0; i < s.length(); i++){ // The character after '(' starts a new key if(i > 0 && s.charAt(i - 1) == '('){ key = ""; bo = true; } // ')' means the current key is complete if(s.charAt(i) == ')'){ bo = false; } // Characters outside brackets are copied directly if(!bo && s.charAt(i) != ')' && s.charAt(i) != '('){ an += s.charAt(i); } // Characters inside brackets are collected as the key if(bo){ key += s.charAt(i); } // Evaluate the completed key if(key.length() > 0 && s.charAt(i) == ')'){ if(mp.containsKey(key)){ an += mp.get(key); key = ""; } else{ an += "?"; key = ""; } } } return an; }}Dry RunConsider:s = "(name)is(age)"and:knowledge = [ ["name", "bob"], ["age", "two"]]Initially:an = ""Reading (name)The scanner encounters '('.The next characters are collected as the key:nnanamnameWhen ')' is reached:key = "name"The map contains "name":name → bobSo:an = "bob"Reading isThese characters are outside brackets:isThey are copied directly:an = "bobis"Reading (age)The key is collected:aagageAt the closing bracket:key = "age"The map contains:age → twoSo:an = "bobistwo"The final result is:bobistwoHandling an Unknown KeyConsider:s = "hi(name)"with:knowledge = [["a", "b"]]The map contains only:a → bWhen the scanner reaches:(name)the collected key is:nameBut:mp.containsKey("name")is false.Therefore the entire bracket pair is replaced with:?The result becomes:hi?This is an important part of the problem because unknown keys should not remain in the output.Why a HashMap Works WellThe knowledge list can contain up to 10^5 entries.Searching through the entire list for every bracket pair would be unnecessarily expensive.Instead, the solution converts it into:key → valueusing a HashMap.Then a lookup is expected O(1).This makes the solution efficient even when the knowledge list is large.A Small Implementation ImprovementThe submitted solution uses:String an = "";and repeatedly performs:an += ...Java String objects are immutable, so repeatedly concatenating strings can create many intermediate objects.A StringBuilder is more appropriate when constructing a result character by character.The same approach can therefore be written as:class Solution { public String evaluate(String s, List<List<String>> knowledge) { HashMap<String, String> mp = new HashMap<>(); for(List<String> pair : knowledge){ mp.put(pair.get(0), pair.get(1)); } StringBuilder ans = new StringBuilder(); StringBuilder key = new StringBuilder(); boolean inside = false; for(int i = 0; i < s.length(); i++){ char ch = s.charAt(i); if(ch == '('){ inside = true; key.setLength(0); } else if(ch == ')'){ inside = false; String k = key.toString(); if(mp.containsKey(k)){ ans.append(mp.get(k)); } else{ ans.append('?'); } } else if(inside){ key.append(ch); } else{ ans.append(ch); } } return ans.toString(); }}This version expresses the parsing states more directly:'(' → start keyinside brackets → collect key')' → evaluate keyoutside brackets → copy characterComplexity AnalysisLet:n = length of sk = number of entries in knowledgeBuilding the HashMap requires:Time: O(k)The string is scanned once:Time: O(n)Therefore, the overall expected complexity is:Time Complexity: O(n + k)The HashMap stores all known keys and values.Space Complexity: O(k + n)The O(n) component accounts for the output and temporary string-building structures.Interview TipThis problem is a good example of a state-based string traversal.Instead of trying to manipulate the entire string repeatedly, scan it once and maintain a small amount of state:Outside brackets ↓ '(' ↓Inside bracket → collect key ↓ ')' ↓Look up key ↓Append value / '?' ↓Outside bracketsWhen a string contains delimiters such as:(...)[...]{...}a similar state-based approach can often simplify the implementation.The second important pattern is recognizing when a HashMap is appropriate: whenever many lookups need to be performed using a unique key.ConclusionLeetCode 1807 is primarily a string parsing + HashMap lookup problem.The solution first converts the knowledge list into a key-value map. It then scans the string and distinguishes between characters inside and outside brackets.For every bracketed key:known key → corresponding valueunknown key → ?The important implementation ideas are:Use a HashMap for fast key lookup.Track whether the scanner is inside a bracket pair.Collect characters between '(' and ')'.Preserve characters outside brackets.Return the first available result after processing the complete string.The submitted approach follows this logic correctly. For production-quality Java code, StringBuilder is preferable to repeated String concatenation when constructing the result.

LeetCodeJavaStringHashMapString ManipulationMediumDSA
LeetCode 3727 Maximum Alternating Sum of Squares – Java Solution & Explanation

LeetCode 3727 Maximum Alternating Sum of Squares – Java Solution & Explanation

IntroductionLeetCode 3727 – Maximum Alternating Sum of Squares is an array problem where the goal is to rearrange the elements to obtain the largest possible alternating score.At first, the formula looks straightforward:score = arr[0]2 - arr[1]2 + arr[2]2 - arr[3]2 + ...However, the challenge is deciding which squared values should occupy the positive positions and which should occupy the negative positions.A useful observation makes the problem easier: the values added to the score should be as large as possible, while the values subtracted should be as small as possible.This solution uses sorting to arrange the squared values, then alternates between taking the largest remaining value for a positive position and the smallest remaining value for a negative position.There is also a small debugging lesson from the submission process: a System.out.println() statement that appears harmless during development can add enough output overhead to contribute to a Time Limit Exceeded (TLE) result.Problem StatementProblem Link-: Maximum Alternating Sum of SquaresGiven an integer array nums, the elements can be rearranged in any order.The alternating score is defined as:score = arr[0]2 - arr[1]2 + arr[2]2 - arr[3]2 + ...Return the maximum possible alternating score after rearranging the array.Example 1Input: nums = [1, 2, 3]Output: 12One valid rearrangement is:[2, 1, 3]Its score is:2^2-1^2+3^2=4-1+9=12Example 2Input: nums = [1, -1, 2, -2, 3, -3]Output: 16One rearrangement that achieves the maximum is:[-3, -1, -2, 1, 3, 2]The score is:9-1+4-1+9-4=16Notice that negative numbers do not remain negative after squaring. For example, ((-3)^2=9), just like (3^2=9).Understanding the Greedy IdeaBefore looking at the code, consider the sign of each position in the alternating sum.PositionContributionIndex 0Add the squareIndex 1Subtract the squareIndex 2Add the squareIndex 3Subtract the squareIndex 4Add the squareThe even indices contribute positively, and the odd indices contribute negatively.To maximize the total, the positive positions should receive large squared values, while the negative positions should receive small squared values.For example, suppose the squared values are:[1, 4, 9]A good arrangement is:[9, 1, 4]The alternating score becomes:9-1+4=12This is better than:1-4+9=6The sorting step makes the largest and smallest remaining values easy to access. Two pointers can then select them in the required order.Java SolutionThe following implementation follows the submitted approach: square the elements, sort them, and build a new array by alternating between the largest and smallest remaining values.class Solution { public long maxAlternatingSum(int[] nums) { for (int i = 0; i < nums.length; i++) { nums[i] = nums[i] * nums[i]; } Arrays.sort(nums); int[] n = new int[nums.length]; int l = 0; int h = nums.length - 1; int v = 0; while (v < nums.length && l < h) { if (v % 2 == 0) { n[v] = nums[h]; h--; v++; } else { n[v] = nums[l]; l++; v++; } } if (h - l != 1) { n[v] = nums[l]; } long sum = 0; for (int i = 0; i < nums.length; i++) { if (i % 2 == 0) { sum += n[i]; } else { sum -= n[i]; } // System.out.println(n[i]); } return sum; }}Step-by-Step Explanation1. Square every elementfor (int i = 0; i < nums.length; i++) { nums[i] = nums[i] * nums[i];}The score uses squares, so each element is converted into its squared value.For example:Original: [1, -1, 2, -2, 3, -3]Squared: [1, 1, 4, 4, 9, 9]This also explains why the original signs no longer affect the ordering after squaring.2. Sort the squared valuesArrays.sort(nums);After sorting:[1, 1, 4, 4, 9, 9]Now the smallest value is at index 0, and the largest is at the last index.3. Use two pointersint l = 0;int h = nums.length - 1;l points to the smallest remaining value.h points to the largest remaining value.v represents the position currently being filled in the new array.At even positions, the code selects the largest remaining value:n[v] = nums[h];h--;At odd positions, it selects the smallest remaining value:n[v] = nums[l];l++;This creates the desired pattern of large positive contributions and small negative contributions.4. Calculate the alternating scoreif (i % 2 == 0) { sum += n[i];} else { sum -= n[i];}The final traversal adds values at even indices and subtracts values at odd indices.The result is stored in a long, which is important because the sum of squared values can exceed the range of a 32-bit integer.Dry RunConsider:nums = [1, 2, 3]After squaring and sorting:[1, 4, 9]Initially:l = 0h = 2v = 0The two pointers select values as follows:StepPositionSelectionValue10Largest remaining921Smallest remaining132Remaining value4The new array becomes:[9, 1, 4]Now calculate the alternating score:IndexValueOperationRunning Sum09+9911-1824+412Therefore:Output: 12The largest squared value contributes positively, while the smallest is subtracted.A Real Debugging Lesson: Why Did Removing System.out.println() Help?While testing a solution, it is common to add a print statement to inspect intermediate values:// System.out.println(n[i]);In this submission, the statement was inside the loop that processes every element.The interesting part is that the solution received a Time Limit Exceeded result while the print statement was active, but was accepted after the statement was commented out.Why can that happen?Console output has a costPrinting is not the same as assigning a value to a variable. A call to System.out.println() involves output handling, and online judges may capture or process that output.When the statement executes repeatedly, the accumulated overhead can become significant.For an array with many elements, a loop such as:for (int i = 0; i < nums.length; i++) { // Process the current element System.out.println(n[i]);}produces up to (n) lines of output.That work is unnecessary when the final submission only needs to return the answer.Why commenting it out can matterRemoving the print statement eliminates the repeated console-output operations. This can make a submission faster, particularly when the execution environment imposes overhead on captured output.However, a TLE is not automatically caused by printing. An inefficient algorithm, repeated string operations, or other unnecessary work can also cause it.The practical lesson: use print statements while debugging, but remove them before submitting unless the problem explicitly requires output through standard output.This is a useful reminder that competitive programming performance depends not only on the algorithm but also on the extra work performed inside its loops.Complexity AnalysisLet (n) be the length of nums.Time complexity: (O(nlog n))Squaring the elements takes (O(n)).Sorting takes (O(n\log n)).Rearranging the values takes (O(n)).Calculating the alternating sum takes (O(n)).Sorting dominates the total time complexity.Auxiliary space: (O(n))The implementation creates a new array of length (n). The sorting implementation may also use additional internal space.An Important Implementation NoteThe submitted solution works through a somewhat complicated final-element condition:if (h - l != 1) { n[v] = nums[l];}This handles the remaining element for certain pointer configurations, but the condition is harder to reason about than necessary.A cleaner implementation can avoid the extra array and special condition by accumulating the score directly while selecting values. It is also important to square into a long if the input constraints might permit values whose squares exceed the int range.For this problem's stated bounds, the square of an individual value fits in an int, but using long for the score protects the accumulated result.The broader lesson is to keep pointer conditions simple and test arrays of length one, odd lengths, and even lengths when implementing two-pointer rearrangements.Interview TakeawayFor array problems involving an alternating sum, identify which positions contribute positively and which contribute negatively before deciding how to sort the data.The key questions are:Which values should be maximized?Which values should be minimized?Can sorting make those values easy to select?Can two pointers build the required arrangement without unnecessary operations?This reasoning connects the problem to a broader set of greedy and sorting questions in which the order of elements determines the final score.ConclusionLeetCode 3727 – Maximum Alternating Sum of Squares combines array manipulation, sorting, two pointers, and greedy reasoning.The central idea is to maximize the values added to the score and minimize the values subtracted from it. Squaring the elements first makes the problem about arranging non-negative values, and sorting allows the largest and smallest remaining values to be selected efficiently.The debugging experience adds one more practical lesson: remove unnecessary console output from performance-sensitive loops before submitting a solution. A small development convenience can create measurable overhead, even when the underlying algorithm is sound.Together, the greedy observation and the debugging lesson make this problem useful both for algorithm practice and for building better submission habits.

LeetCodeJavaArraysSortingMedium
LeetCode 1021: Remove Outermost Parentheses — Java Solution Explained

LeetCode 1021: Remove Outermost Parentheses — Java Solution Explained

IntroductionLeetCode 1021 Remove Outermost Parentheses sounds intimidating with words like "primitive decomposition" but once you strip away the fancy terminology, it is a beautifully simple problem. It builds directly on the depth-counting technique from LeetCode 1614 and is a perfect example of how one clean insight can collapse a seemingly complex problem into just a few lines of code.Here is the Link of Question -: LeetCode 1021In this article we cover plain English explanation, what "primitive" really means, two Java approaches with detailed dry runs, complexity analysis, and everything you need to ace this in an interview.What Is the Problem Really Asking?Let us ignore the formal definition for a moment and think practically.A primitive string is the smallest self-contained valid bracket group. Think of it as a top-level unit — it opens, closes completely, and is not part of a bigger bracket group.For "(()())(())":"(()())" is one primitive — it opens and fully closes"(())" is another primitive — it opens and fully closes afterNow from each primitive, remove only the outermost opening and closing bracket, keep everything inside."(()())" → remove outer → "()()""(())" → remove outer → "()"Combined → "()()()"That is literally the whole problem!Real Life Analogy — Gift Boxes Inside a Shipping BoxImagine you ordered gifts online. They all arrive in one big shipping box. Inside that box are individual gift boxes. Inside some gift boxes are smaller boxes.The "primitive decomposition" is identifying each individual gift box inside the shipping box. Removing the outermost parentheses is like removing just the outer gift box wrapping but keeping everything inside it intact.You are not unpacking the inner boxes — just peeling off one layer from each top-level group.Understanding Primitive Decomposition VisuallyFor s = "(()())(())(()(()))":( ( ) ( ) ) ( ( ) ) ( ( ) ( ( ) ) )|_________| |_______| |_____________| primitive1 primitive2 primitive3Each group that starts at depth 0 and returns to depth 0 is one primitive. Remove the outermost ( and ) from each and concatenate what remains.The depth analogy from LeetCode 1614 is the key — a primitive starts when depth goes from 0 to 1 and ends when depth returns to 0.Approach 1: Counter With Substring (Your Solution) ✅The IdeaTrack depth with a counter. Use start and end pointers to mark the boundaries of each primitive. When depth hits 0, you have found a complete primitive — append everything between start+1 and end (skipping the outermost brackets) to the result.public String removeOuterParentheses(String s) { StringBuilder sb = new StringBuilder(); int c = 0; int start = 0; int end = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { c++; } else { c--; } if (c == 0) { // primitive found from index start to end (inclusive) sb.append(s.substring(start + 1, end)); // skip outermost ( and ) start = end + 1; // next primitive starts after current end } end++; } return sb.toString();}Dry Run — s = "(()())(())"Tracking c, start, end at each step:i=0, ( → c=1, end=1i=1, ( → c=2, end=2i=2, ) → c=1, end=3i=3, ( → c=2, end=4i=4, ) → c=1, end=5i=5, ) → c=0 → primitive found! append s.substring(1, 5) = "()()" → start=6, end=6i=6, ( → c=1, end=7i=7, ( → c=2, end=8i=8, ) → c=1, end=9i=9, ) → c=0 → primitive found! append s.substring(7, 9) = "()" → start=10, end=10Result: "()()" + "()" = "()()()" ✅Time Complexity: O(n) — single pass plus substring operations Space Complexity: O(n) — StringBuilder stores the resultApproach 2: Counter With Depth Filter (Cleaner & More Intuitive)The IdeaInstead of tracking start/end pointers, use depth directly to decide whether to include each character. The insight is:The outermost ( of each primitive is always encountered when c goes from 0 to 1 — skip itThe outermost ) of each primitive is always encountered when c goes from 1 to 0 — skip itEvery other character is inside some primitive — include itpublic String removeOuterParentheses(String s) { StringBuilder sb = new StringBuilder(); int depth = 0; for (char c : s.toCharArray()) { if (c == '(') { if (depth > 0) { sb.append(c); // not the outermost (, include it } depth++; } else { depth--; if (depth > 0) { sb.append(c); // not the outermost ), include it } } } return sb.toString();}This is arguably the most elegant solution. No pointers, no substring, just one rule — if depth is already greater than 0 when you see (, it is not the outer one. If depth is still greater than 0 after decrementing on ), it is not the outer one.Dry Run — s = "()()" (Edge Case)( → depth=0, skip it, depth becomes 1) → depth becomes 0, depth=0 so skip it( → depth=0, skip it, depth becomes 1) → depth becomes 0, depth=0 so skip itResult: "" ✅ Both are primitives "()" and "()", removing outer from each gives empty string.Dry Run — s = "(()())(())(()(()))"Let us trace just which characters get included vs skipped:For the first primitive "(()())":( at depth 0 → skip (outermost opener)( at depth 1 → include) at depth 2→1 → include( at depth 1 → include) at depth 2→1 → include) at depth 1→0 → skip (outermost closer)Inner content "()()" included ✅Same logic applies to each subsequent primitive. Final result: "()()()()(())" ✅Time Complexity: O(n) — single pass Space Complexity: O(n) — StringBuilder stores resultApproach 3: Stack Based (Verbose but Clear)The IdeaUse an explicit stack to track open brackets. When the stack becomes empty after a ), you have found a complete primitive. Collect all characters between the outermost brackets.public String removeOuterParentheses(String s) { StringBuilder sb = new StringBuilder(); Stack<Character> st = new Stack<>(); int start = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { st.push('('); } else { st.pop(); } if (st.empty()) { // primitive ends at i, extract inner content sb.append(s.substring(start + 1, i)); start = i + 1; } } return sb.toString();}This is the most verbose but makes the primitive detection very explicit — stack empty means primitive complete. Great for explaining your thought process in an interview before optimizing.Time Complexity: O(n) Space Complexity: O(n) — stack storageApproach ComparisonThe Stack approach is clearest for explaining primitive detection. The pointer-based counter approach (your solution) is direct and efficient. The depth-filter counter approach is the most elegant — no pointers, no substrings, just a single depth check per character. All three are O(n) time. The depth-filter approach wins on code clarity.How This Connects to the Full SeriesLooking at the bracket problem series you have been building:20 Valid Parentheses — is the string valid? 1614 Maximum Nesting Depth — how deep is the nesting? 1021 Remove Outermost Parentheses — identify and strip top-level groupsEach problem adds one more layer of understanding about bracket strings. The depth counter technique from 1614 directly powers the optimal solution here. This is pattern building at its best.Common Mistakes to AvoidOff-by-one in substring indices In your solution, s.substring(start+1, end) skips the outermost ( by using start+1, and end (not end+1) naturally excludes the outermost ) since end points to it. Getting these indices wrong by even one position gives completely wrong output.Appending the outermost brackets in the depth-filter approach The condition order matters — for (, check depth BEFORE incrementing. For ), check depth AFTER decrementing. Flipping these gives wrong results.Forgetting to update start after each primitive In pointer-based approaches, always set start = end + 1 (or start = i + 1) after appending each primitive, otherwise the next primitive's start index is wrong.FAQs — People Also AskQ1. What is a primitive valid parentheses string in LeetCode 1021? A primitive is the smallest self-contained valid bracket group that cannot be split into two smaller valid groups. It starts when depth goes from 0 to 1 and ends when depth returns to 0. For example in "(()())(())", the primitives are "(()())" and "(())".Q2. What is the most elegant solution for LeetCode 1021? The depth-filter counter approach — increment depth on (, decrement on ), and only append characters when depth is greater than 0 for ( and still greater than 0 after decrement for ). This skips outermost brackets naturally without tracking indices.Q3. What is the time complexity of LeetCode 1021? All approaches run in O(n) time with a single pass. Space complexity is O(n) for storing the output string in StringBuilder.Q4. How is LeetCode 1021 different from LeetCode 1614? LeetCode 1614 finds the maximum depth by tracking a counter. LeetCode 1021 uses the same counter technique but to identify primitive boundaries and strip their outermost characters. 1614 returns a number, 1021 returns a modified string.Q5. Is LeetCode 1021 asked in coding interviews? It appears occasionally as a follow-up to Valid Parentheses or Nesting Depth problems. The real skill it tests is understanding primitive decomposition — identifying self-contained units within a bracket string — which appears in real world parser and compiler design.Similar LeetCode Problems to Practice Next20. Valid Parentheses — Easy — validate bracket structure1614. Maximum Nesting Depth of Parentheses — Easy — find max depth1249. Minimum Remove to Make Valid Parentheses — Medium — remove minimum brackets394. Decode String — Medium — nested brackets with encoding32. Longest Valid Parentheses — Hard — longest valid substringConclusionLeetCode 1021 Remove Outermost Parentheses is a satisfying problem once you realize that "primitive decomposition" is just a fancy way of saying "find each top-level bracket group." The depth counter is the key — depth 0 to 1 marks the start of a primitive, depth 1 to 0 marks its end.The depth-filter approach is the cleanest solution — one pass, one counter, one condition. No substring, no pointers, just elegance. Master this alongside LeetCode 20 and 1614 and you will have a complete toolkit for any bracket string problem that comes your way.

StringStackEasyLeetCode
Stack Problems Explained: NGR, NGL, NSR, NSL — The Four-Problem Family You Must Master

Stack Problems Explained: NGR, NGL, NSR, NSL — The Four-Problem Family You Must Master

IntroductionAmong all the problems built around the Stack data structure, four stand out as a family — they appear repeatedly in coding interviews, competitive programming, and real-world software systems. These four are the Next Greater to the Right (NGR), Next Greater to the Left (NGL), Next Smaller to the Right (NSR), and Next Smaller to the Left (NSL).What makes them special is not just their individual solutions — it is the fact that all four are solved by a single elegant technique called the Monotonic Stack. Learn the pattern once, and you have all four in your toolkit permanently.This guide breaks down each problem with a full solution, step-by-step dry run, edge cases, and the exact reasoning behind every decision in the code. Whether you are preparing for a technical interview or simply want to deeply understand this pattern — you are in the right place.The Story That Makes This ClickBefore any code, let us understand this family of problems with one real-world story.Imagine you are standing in a queue at a cricket stadium. Everyone in the queue has a different height. You are standing somewhere in the middle. You look to your right and ask — who is the first person taller than me? That is your Next Greater Element to the Right (NGR).Now you look to your left — who is the first person taller than me on this side? That is your Next Greater to the Left (NGL).Now instead of taller, you ask shorter — who is the first shorter person to my right? That is Next Smaller to the Right (NSR).And shorter to your left? That is Next Smaller to the Left (NSL).Same queue. Same people. Four different questions. Four different answers. This is exactly what these four problems are about — and they all share the same solution pattern.What Is a Monotonic Stack?A monotonic stack is just a regular stack with one rule — elements inside it are always maintained in a specific order, either always increasing or always decreasing from bottom to top.You never enforce this rule explicitly. It happens naturally as you pop elements that violate the order before pushing a new one. This popping step is the key insight — the moment you pop an element, you have found its answer for the current element being processed.This one pattern solves all four problems. Only two small details change between them — the direction of traversal and the comparison condition inside the while loop.The Four Problems — Quick ReferenceProblemDirectionWhat You WantProblem LinksNGRTraverse Right to LeftFirst greater on right"Next Greater Element GFG"NGLTraverse Left to RightFirst greater on left"Previous Greater Element GFG"NSRTraverse Right to LeftFirst smaller on right"Next Smaller Element GFG"NSLTraverse Left to RightFirst smaller on left"Previous Smaller Element GFG"Problem 1 — Next Greater Element to Right (NGR)GFG Problem: Search "Next Greater Element" on GeeksForGeeks Difficulty: Medium | Accuracy: 32.95% | Submissions: 515K+The QuestionFor each element in the array, find the first element to its right that is strictly greater than it. If none exists, return -1.Input: [1, 3, 2, 4] Output: [3, 4, 4, -1]Input: [6, 8, 0, 1, 3] Output: [8, -1, 1, 3, -1]Real World ExampleThink of the stock market. You have daily closing prices: [1, 3, 2, 4]. For each day, you want to know — on which future day will the price first exceed today's price? Day 1 has price 1, first exceeded on Day 2 with price 3. Day 2 has price 3, first exceeded on Day 4 with price 4. Day 3 has price 2, also first exceeded on Day 4 with price 4. Day 4 has no future day, so -1. This is exactly NGR — and it is literally used in financial software to detect price breakout points.The IntuitionThe brute force is obvious — for every element, scan everything to its right and find the first greater one. That works but it is O(n²). For an array of 10⁶ elements that becomes 10¹² operations. It will time out on any large input.The stack insight is this — traverse right to left. As you move left, the stack always holds elements you have already seen on the right side. These are the candidates for being the next greater element. Before pushing the current element, pop all stack elements that are smaller than or equal to it. Why? Because the current element is blocking them — for any future element to the left, the current element will always be encountered first, so those smaller popped elements can never be an answer for anything. Whatever remains on top of the stack after popping is the answer for the current element.Step-by-Step Dry RunArray: [1, 3, 2, 4], traversing right to left.i=3, element is 4. Stack is empty. Answer for index 3 is -1. Push 4. Stack: [4]i=2, element is 2. Top of stack is 4, which is greater than 2. Answer for index 2 is 4. Push 2. Stack: [4, 2]i=1, element is 3. Top of stack is 2, which is not greater than 3. Pop 2. Top is now 4, which is greater than 3. Answer for index 1 is 4. Push 3. Stack: [4, 3]i=0, element is 1. Top of stack is 3, which is greater than 1. Answer for index 0 is 3. Push 1. Stack: [4, 3, 1]Answers collected right to left: [-1, 4, 4, 3] After Collections.reverse(): [3, 4, 4, -1] ✓The Code// NGR — Next Greater Element to Rightclass Solution {public ArrayList<Integer> nextLargerElement(int[] arr) {ArrayList<Integer> result = new ArrayList<>();Stack<Integer> st = new Stack<>();// Traverse from RIGHT to LEFTfor (int i = arr.length - 1; i >= 0; i--) {// Pop all elements smaller than or equal to current// They can never be the answer for any element to the leftwhile (!st.empty() && arr[i] >= st.peek()) {st.pop();}// Whatever is on top now is the next greater elementif (st.empty()) {result.add(-1);} else {result.add(st.peek());}// Push current — it is a candidate for elements to the leftst.push(arr[i]);}// Collected answers right to left, so reverse before returningCollections.reverse(result);return result;}}Edge CasesAll elements decreasing — Input: [5, 4, 3, 2, 1] Output: [-1, -1, -1, -1, -1] Every element has no greater element to its right. Traversing right to left, each new element is larger than everything already in the stack, so the stack gets cleared and the answer is always -1.All elements increasing — Input: [1, 2, 3, 4, 5] Output: [2, 3, 4, 5, -1] Each element's next greater is simply the next element in the array. The last element always gets -1 since nothing exists to its right.All elements equal — Input: [3, 3, 3, 3] Output: [-1, -1, -1, -1] Equal elements do not count as greater. The pop condition uses >= so equals get removed from the stack, ensuring duplicates never answer each other.Single element — Input: [7] Output: [-1] Nothing to the right, always -1.Why only 32.95% accuracy on GFG? Most people either forget to reverse the result at the end, use the wrong comparison in the while loop, or submit a brute force O(n²) solution that times out on large inputs.Problem 2 — Next Greater Element to Left / Previous Greater Element (NGL)GFG Problem: Search "Previous Greater Element" on GeeksForGeeks Difficulty: Medium | Accuracy: 68.93% | Submissions: 7K+The QuestionFor each element in the array, find the first element to its left that is strictly greater than it. If none exists, return -1.Input: [10, 4, 2, 20, 40, 12, 30] Output: [-1, 10, 4, -1, -1, 40, 40]Real World ExampleImagine you are a junior employee at a company. For each person in the office, you want to know — who is the first senior person sitting to their left who earns more? This is NGL. It is used in organizational hierarchy systems, salary band analysis tools, and even in database query optimizers to find the nearest dominant record on the left side.The IntuitionThis is the mirror image of NGR. Instead of traversing right to left, we traverse left to right. The stack holds elements we have already seen from the left side — these are candidates for being the previous greater element. For each new element, pop everything from the stack that is smaller than or equal to it. Whatever remains on top is the first greater element to its left. Then push the current element for future use.No reverse is needed here because we are already going left to right and building the result in order.Step-by-Step Dry RunArray: [10, 4, 2, 20, 40, 12, 30], traversing left to right.i=0, element is 10. Stack is empty. Answer is -1. Push 10. Stack: [10]i=1, element is 4. Top is 10, greater than 4. Answer is 10. Push 4. Stack: [10, 4]i=2, element is 2. Top is 4, greater than 2. Answer is 4. Push 2. Stack: [10, 4, 2]i=3, element is 20. Top is 2, not greater than 20. Pop 2. Top is 4, not greater. Pop 4. Top is 10, not greater. Pop 10. Stack is empty. Answer is -1. Push 20. Stack: [20]i=4, element is 40. Top is 20, not greater. Pop 20. Stack empty. Answer is -1. Push 40. Stack: [40]i=5, element is 12. Top is 40, greater than 12. Answer is 40. Push 12. Stack: [40, 12]i=6, element is 30. Top is 12, not greater than 30. Pop 12. Top is 40, greater than 30. Answer is 40. Push 30. Stack: [40, 30]Result: [-1, 10, 4, -1, -1, 40, 40] ✓ No reverse needed.The Code// NGL — Next Greater Element to Left (Previous Greater Element)class Solution {static ArrayList<Integer> preGreaterEle(int[] arr) {Stack<Integer> st = new Stack<>();ArrayList<Integer> result = new ArrayList<>();// Traverse LEFT to RIGHT — no reverse neededfor (int i = 0; i <= arr.length - 1; i++) {// Pop all elements smaller than or equal to currentwhile (!st.empty() && arr[i] >= st.peek()) {st.pop();}// Top of stack is the previous greater elementif (!st.empty() && st.peek() > arr[i]) {result.add(st.peek());} else {result.add(-1);}// Push current for future elementsst.push(arr[i]);}return result;}}Edge CasesStrictly increasing array — Input: [10, 20, 30, 40] Output: [-1, -1, -1, -1] Each new element is larger than everything before it, so the stack always gets fully cleared. No previous greater exists for any element.First element is always -1 — regardless of its value, the first element has nothing to its left. The stack is empty at i=0, so the answer is always -1 for index 0. This is guaranteed by the logic.Duplicate values — Input: [5, 5, 5] Output: [-1, -1, -1] Equal elements do not qualify as greater. The pop condition uses >= so duplicates get removed from the stack and never answer each other.Problem 3 — Next Smaller Element to Right (NSR)GFG Problem: Search "Next Smaller Element" on GeeksForGeeks Difficulty: Medium | Accuracy: 36.26% | Submissions: 225K+The QuestionFor each element in the array, find the first element to its right that is strictly smaller than it. If none exists, return -1.Input: [4, 8, 5, 2, 25] Output: [2, 5, 2, -1, -1]Input: [13, 7, 6, 12] Output: [7, 6, -1, -1]Real World ExampleYou work at a warehouse. Shelves have items of weights: [4, 8, 5, 2, 25] kg. For each item, the system needs to find the first lighter item sitting to its right on the shelf — this is used to optimize load balancing and shelf arrangement algorithms. Item of 4 kg — first lighter to the right is 2 kg. Item of 8 kg — first lighter is 5 kg. Item of 5 kg — first lighter is 2 kg. Items of 2 kg and 25 kg have no lighter item to their right, so -1.The IntuitionNSR is structurally identical to NGR — we traverse right to left and collect answers, then reverse. The only change is the pop condition. In NGR we popped elements smaller than or equal to current because we wanted greater. Here we want smaller, so we pop elements greater than or equal to current. After popping, whatever remains on top is the first smaller element to the right.Step-by-Step Dry RunArray: [4, 8, 5, 2, 25], traversing right to left.i=4, element is 25. Stack is empty. Answer is -1. Push 25. Stack: [25]i=3, element is 2. Top is 25, which is greater than or equal to 2. Pop 25. Stack is empty. Answer is -1. Push 2. Stack: [2]i=2, element is 5. Top is 2, which is less than 5. Answer is 2. Push 5. Stack: [2, 5]i=1, element is 8. Top is 5, which is less than 8. Answer is 5. Push 8. Stack: [2, 5, 8]i=0, element is 4. Top is 8, which is greater than or equal to 4. Pop 8. Top is 5, which is greater than or equal to 4. Pop 5. Top is 2, which is less than 4. Answer is 2. Push 4. Stack: [2, 4]Answers collected right to left: [-1, -1, 2, 5, 2] After Collections.reverse(): [2, 5, 2, -1, -1] ✓The Code// NSR — Next Smaller Element to Rightclass Solution {static ArrayList<Integer> nextSmallerEle(int[] arr) {Stack<Integer> st = new Stack<>();ArrayList<Integer> result = new ArrayList<>();// Traverse RIGHT to LEFTfor (int i = arr.length - 1; i >= 0; i--) {// Pop elements greater than or equal to current// Opposite of NGR — we want smaller, so clear the bigger oneswhile (!st.empty() && arr[i] <= st.peek()) {st.pop();}// Top is now the next smaller elementif (!st.empty() && st.peek() < arr[i]) {result.add(st.peek());} else {result.add(-1);}st.push(arr[i]);}Collections.reverse(result);return result;}}Edge CasesStrictly decreasing array — Input: [5, 4, 3, 2, 1] Output: [4, 3, 2, 1, -1] Each element's next smaller is simply the next element in the array. Last element is always -1.Strictly increasing array — Input: [1, 2, 3, 4, 5] Output: [-1, -1, -1, -1, -1] No element has a smaller element to its right since the array only grows.Last element is always -1 — nothing exists to its right regardless of its value.Single element — Input: [42] Output: [-1]Why 36.26% accuracy on GFG? The most common mistake is keeping the NGR pop condition (arr[i] >= st.peek()) and only changing the problem description in your head. The pop condition must flip to arr[i] <= st.peek() for NSR. Forgetting this gives completely wrong answers that look plausible, which makes the bug hard to spot.Problem 4 — Next Smaller Element to Left / Previous Smaller Element (NSL)GFG Problem: Search "Previous Smaller Element" on GeeksForGeeksThe QuestionFor each element in the array, find the first element to its left that is strictly smaller than it. If none exists, return -1.Input: [4, 8, 5, 2, 25] Output: [-1, 4, 4, -1, 2]Real World ExampleSame warehouse. Now the system looks left instead of right. For the item weighing 8 kg, the first lighter item to its left is 4 kg. For 25 kg, the first lighter to its left is 2 kg. For 4 kg, nothing lighter exists to its left so -1. For 2 kg, nothing lighter to its left so -1. This kind of lookback query appears in time-series analysis, price history tracking, and sensor data processing.The IntuitionNSL is the mirror of NSR, exactly as NGL was the mirror of NGR. We traverse left to right (no reverse needed). We maintain a stack of candidates from the left. For each element, pop all elements greater than or equal to it — they cannot be the answer since they are not smaller. Whatever remains on top is the first smaller element to the left. Push current and move on.Step-by-Step Dry RunArray: [4, 8, 5, 2, 25], traversing left to right.i=0, element is 4. Stack is empty. Answer is -1. Push 4. Stack: [4]i=1, element is 8. Top is 4, which is less than 8. Answer is 4. Push 8. Stack: [4, 8]i=2, element is 5. Top is 8, which is greater than or equal to 5. Pop 8. Top is 4, which is less than 5. Answer is 4. Push 5. Stack: [4, 5]i=3, element is 2. Top is 5, greater than or equal to 2. Pop 5. Top is 4, greater than or equal to 2. Pop 4. Stack is empty. Answer is -1. Push 2. Stack: [2]i=4, element is 25. Top is 2, which is less than 25. Answer is 2. Push 25. Stack: [2, 25]Result: [-1, 4, 4, -1, 2] ✓ No reverse needed.The Code// NSL — Next Smaller Element to Left (Previous Smaller Element)class Solution {static ArrayList<Integer> prevSmallerEle(int[] arr) {Stack<Integer> st = new Stack<>();ArrayList<Integer> result = new ArrayList<>();// Traverse LEFT to RIGHT — no reverse neededfor (int i = 0; i < arr.length; i++) {// Pop elements greater than or equal to currentwhile (!st.empty() && arr[i] <= st.peek()) {st.pop();}// Top is the previous smaller elementif (!st.empty() && st.peek() < arr[i]) {result.add(st.peek());} else {result.add(-1);}st.push(arr[i]);}return result;}}Edge CasesFirst element is always -1 — nothing exists to its left. Stack is empty at i=0 every time.All same elements — Input: [5, 5, 5, 5] Output: [-1, -1, -1, -1] Equal elements do not qualify as smaller. The condition arr[i] <= st.peek() ensures equals are popped and never answer each other.Single element — Input: [9] Output: [-1]The Master Cheat SheetThis is the one table to save and refer to whenever you encounter any of these four problems.VariantTraverse DirectionPop ConditionReverse Result?NGR — Next Greater RightRight to Leftarr[i] >= st.peek()YesNGL — Next Greater LeftLeft to Rightarr[i] >= st.peek()NoNSR — Next Smaller RightRight to Leftarr[i] <= st.peek()YesNSL — Next Smaller LeftLeft to Rightarr[i] <= st.peek()NoTwo rules to remember forever:Rule 1 — Direction. If you are looking to the right, traverse right to left and reverse at the end. If you are looking to the left, traverse left to right and no reverse is needed.Rule 2 — Pop Condition. If you want a greater element, pop when arr[i] >= st.peek() to clear out smaller useless candidates. If you want a smaller element, pop when arr[i] <= st.peek() to clear out bigger useless candidates.Mix these two rules and you derive all four variants instantly without memorizing anything separately.Common Mistakes to AvoidWrong pop condition — Using > instead of >= in the while loop. This causes duplicate values to wrongly answer each other. Always use >= for greater problems and <= for smaller problems inside the while loop.Forgetting to reverse — For right-to-left traversals (NGR and NSR), you collect answers from right to left. You must call Collections.reverse() before returning. Skipping this is the single most common reason for wrong answers on these problems.Not checking empty stack before peek — Always check !st.empty() before calling st.peek(). An empty stack peek throws EmptyStackException at runtime and will crash your solution.Wrong if-condition after the while loop — After the while loop, the if-condition must use strict comparison. For NGR use st.peek() > arr[i]. For NSR use st.peek() < arr[i]. These must be strict — no equals sign here.Confusing traversal direction with answer direction — You traverse right to left for NGR but the answer array is filled left to right. The reverse at the end handles this. Do not try to index directly into the result array to compensate — just use reverse.Time and Space ComplexityAll four problems run in O(n) time and use O(n) space.Even though there is a while loop nested inside the for loop, each element is pushed into the stack exactly once and popped from the stack at most once. So across the entire traversal, the total number of push and pop operations combined is at most 2n — which gives O(n) overall. This is the beauty of the monotonic stack.Why These Four Problems Matter Beyond GFGThese four patterns are not just textbook exercises. They appear as the hidden sub-problem inside some of the hardest stack questions:-Largest Rectangle in Histogram uses NSR and NSL to find the left and right boundaries of each bar.Trapping Rain Water uses NGR and NGL to determine the water level above each position.Stock Span Problem is literally NGL applied directly to stock prices.Sum of Subarray Minimums uses NSR and NSL together to count contributions of each element.Once you master these four patterns deeply, a whole family of hard problems that previously seemed unapproachable suddenly becomes a matter of recognizing the pattern and applying it.Also on This BlogIf you are building your stack foundation from scratch, check out the complete deep-dive here → Stack Data Structure in Java: The Complete Guide — covering everything from what a stack is, LIFO principle, all three implementations, every operation with code, and six practice problems.

MonotonicStackNextGreaterElementStackProblemsJavaGeeksForGeeksStackPattern
LeetCode 2913: Subarrays Distinct Element Sum of Squares I – Java Solutions & Explanation

LeetCode 2913: Subarrays Distinct Element Sum of Squares I – Java Solutions & Explanation

IntroductionSome array problems ask for the sum of elements, while others require counting unique values inside different portions of an array. LeetCode 2913 combines both ideas: every possible subarray must be considered, its distinct elements counted, and that count squared before being added to the final answer.The interesting part is that two solutions can perform the same calculations in slightly different ways. One approach stores the distinct count of each subarray and calculates the squares in a separate pass. The other calculates each square immediately after updating the set of distinct elements.The second approach avoids storing intermediate counts and makes the implementation more direct.In this article, both approaches are explained using Java, including the role of HashSet, a dry run of the example, complexity analysis, and the difference between the two implementations.Problem StatementIn LeetCode 2913 – Subarrays Distinct Element Sum of Squares I, an integer array nums is given.For every non-empty contiguous subarray, find the number of distinct elements, square that number, and add it to the total.A subarray is a contiguous sequence of elements from the original array.Mathematically, if (d(s)) represents the number of distinct values in a subarray (s), the required answer is:Formula: Sum of (distinct element count × distinct element count) for every non-empty subarrayExample 1Input: nums = [1, 2, 1]Output: 15The six possible subarrays are:SubarrayDistinct elementsDistinct count squared[1]{1}(1^2=1)[2]{2}(1^2=1)[1]{1}(1^2=1)[1, 2]{1, 2}(2^2=4)[2, 1]{1, 2}(2^2=4)[1, 2, 1]{1, 2}(2^2=4)Adding the contributions gives:1+1+1+4+4+4=15Notice that repeated values are counted only once within a subarray. For example, [1, 2, 1] has three elements but only two distinct values.Example 2Input: nums = [1, 1]Output: 3The possible subarrays are [1], [1], and [1, 1].Each has exactly one distinct value, so the answer is:1^2+1^2+1^2=3Approach 1: Store Distinct Counts and Calculate the Squares LaterThe first approach follows the problem statement directly.For every starting index, all subarrays beginning at that index are explored. A HashSet keeps track of the unique values encountered as the ending index moves forward.Whenever a new element is added, the size of the set represents the number of distinct values in the current subarray.Instead of calculating the square immediately, this approach stores each distinct count in a list. After all subarrays have been processed, another loop squares the stored counts and adds them to the answer.Java Codeclass Solution {public int sumCounts(List<Integer> nums) {List<Integer> counts = new ArrayList<>();for (int i = 0; i < nums.size(); i++) {HashSet<Integer> set = new HashSet<>();for (int j = i; j < nums.size(); j++) {set.add(nums.get(j));counts.add(set.size());}}int sum = 0;for (int count : counts) {sum += count * count;}return sum;}}How It WorksChoose a starting index i.Create an empty HashSet for that starting position.Move the ending index j forward, adding each element to the set.Store set.size() for every subarray.After processing all subarrays, square each stored count and add it to sum.This approach is easy to understand because it separates the two operations: finding distinct counts and calculating their squared contributions.However, it stores a value for every subarray, even though each count can be used immediately.Approach 2: Calculate Each Square DirectlyThe second approach simplifies the process by calculating each contribution as soon as the distinct count is known.Instead of storing the counts in another list, the code adds the square directly to the running total.This is the approach used in the submitted solution.Java Codeclass Solution {public int sumCounts(List<Integer> arr) {int sum = arr.size();for (int i = 0; i < arr.size(); i++) {HashSet<Integer> ms = new HashSet<>();ms.add(arr.get(i));for (int j = i + 1; j < arr.size(); j++) {ms.add(arr.get(j));sum += ms.size() * ms.size();}}return sum;}}Why Does sum Start at arr.size()?This line is an important detail:int sum = arr.size();Every individual element forms a subarray of length one.For a single-element subarray such as [5], the number of distinct values is always one, so its contribution is:1^2=1An array of length (n) has exactly (n) single-element subarrays. Their total contribution is therefore (n).That is why the code initializes sum to arr.size().The outer loop then handles the remaining subarrays, which have lengths of at least two.Why Use a HashSet?Consider the array:[1, 2, 1]As the ending index moves forward, the set changes like this:Current subarraySet contentsDistinct count[1, 2]{1, 2}2[1, 2, 1]{1, 2}2Adding the second occurrence of 1 does not increase the set size. A HashSet automatically ignores duplicate values, so ms.size() always provides the correct distinct count.The code then immediately adds:sum += ms.size() * ms.size();This removes the need for a separate list and a final traversal.Dry Run of the Improved ApproachLet's trace the submitted solution with:arr = [1, 2, 1]Initially:sum = 3The initial value accounts for the three single-element subarrays:[1], [2], [1]Each contributes one, giving a total of three.First starting index: i = 0The set begins with the first element:Set = {1}The inner loop starts at j = 1.jCurrent subarraySetDistinct countAdded contribution1[1, 2]{1, 2}2(2^2=4)2[1, 2, 1]{1, 2}2(2^2=4)Running total:3 + 4 + 4 = 11Second starting index: i = 1The set starts again with the element at index one:Set = {2}The inner loop continues from j = 2.jCurrent subarraySetDistinct countAdded contribution2[2, 1]{1, 2}2(2^2=4)Running total:11 + 4 = 15Third starting index: i = 2There are no elements after the starting index, so the inner loop does not execute.The contribution of [1] was already included in the initial value of sum.Final result:Output: 15All six subarrays have been accounted for.Comparing the Two ApproachesBoth approaches enumerate the same set of subarrays and use a HashSet to count distinct elements. The difference is how the contributions are accumulated.FeatureApproach 1Approach 2Counts distinct elementsHashSetHashSetStores counts separatelyYesNoCalculates squaresIn a second loopImmediatelyExtra storage for counts(O(n^2))None beyond the setOverall time complexity(O(n^2)) expected(O(n^2)) expectedImplementationTwo phasesOne accumulation phaseThe second approach is more space-efficient because it does not store a separate count for every subarray.It is important to distinguish this improvement from a change in asymptotic time complexity. Both approaches still examine a quadratic number of subarrays, so the direct-calculation version is not an (O(n)) solution.Complexity AnalysisLet (n) be the length of the input array.Time Complexity: (O(n^2)) expectedThere are (n(n+1)/2) non-empty subarrays.The nested loops enumerate these subarrays, and the HashSet performs insertions in expected constant time under normal hashing assumptions.Therefore, the overall expected time complexity is:O(n^2)Space ComplexityApproach 1: (O(n^2)) auxiliary space for the list of stored distinct counts, plus the set used during enumeration.Approach 2: (O(n)) auxiliary space in the worst case for the HashSet, since it can contain up to (n) distinct values. No separate list of counts is needed.With the given constraint (n\leq100), the straightforward nested-loop approach is suitable.Interview Tip: Avoid Storing Values You Can Use ImmediatelyA useful optimization habit is to ask whether an intermediate result needs to be saved.In the first approach, each distinct count is stored because the squares are calculated later. But the count is already available at the moment the subarray is processed.That makes this operation possible:sum += count * count;The general lesson is not that a separate list is always wrong. It is that intermediate storage should serve a purpose. If a value can be consumed immediately, doing so may simplify the code and reduce memory usage.For this problem, the main algorithm remains the same: enumerate every subarray, maintain its distinct values, and add the square of the current distinct count.ConclusionLeetCode 2913 is a useful exercise in subarray enumeration and distinct-element counting. The HashSet handles duplicate values, while nested loops ensure that every non-empty contiguous subarray is considered.The first approach stores the distinct count of each subarray and calculates the squares afterward. The second approach, used in the submitted solution, adds each squared count immediately.Both approaches have expected (O(n^2)) time complexity, but the second uses less auxiliary space and keeps the calculation in one place.For small constraints like those in this problem, a clear and correct brute-force solution is often the right choice. Understanding why the direct-calculation version is simpler is also a useful step toward writing more concise and memory-conscious Java code.

LeetCodeJavaArraysHashSetSubarraysBrute ForceEasy
LeetCode 682 Baseball Game - Java Solution Explained

LeetCode 682 Baseball Game - Java Solution Explained

IntroductionLeetCode 682 Baseball Game is one of the cleanest and most beginner-friendly stack simulation problems on LeetCode. It does not require any fancy algorithm or deep insight — it purely tests whether you can carefully read the rules and simulate them faithfully using the right data structure.But do not let "Easy" fool you. This problem is a great place to practice thinking about which data structure fits best and why. We will solve it three different ways — Stack, ArrayList, and Deque — so you can see the tradeoffs and pick the one that makes most sense to you.You can find the problem here — LeetCode 682 Baseball Game.What Is the Problem Really Asking?You are keeping score for a baseball game with four special rules. You process a list of operations one by one and maintain a record of scores. At the end, return the total sum of all scores in the record.The four operations are:A number (like "5" or "-2") — just add that number as a new score to the record."C" — the last score was invalid, remove it from the record."D" — add a new score that is double the most recent score."+" — add a new score that is the sum of the two most recent scores.That is it. Four rules, simulate them in order, sum up what is left.Real Life Analogy — A Scoreboard With CorrectionsImagine a scoreboard operator at a sports event. They write scores on a whiteboard as the game progresses:A player scores 5 points → write 5Another player scores 2 → write 2Referee says last score was invalid → erase the last number (C)Special bonus rule kicks in → double the last valid score (D)Two scores combine → add the last two scores as one entry (+)At the end, add up everything on the whiteboard. The stack is your whiteboard — you write on top and erase from the top.Why Stack Is the Natural FitAll four operations only ever look at or modify the most recently added scores. C removes the last one. D doubles the last one. + uses the last two. This "most recent first" access pattern is the definition of LIFO — Last In First Out — which is exactly what a Stack provides.Any time a problem says "the previous score" or "the last two scores," your brain should immediately think Stack.Approach 1: Stack (Your Solution) ✅The IdeaUse a Stack of integers. For each operation:Number → parse and pushC → pop the topD → peek the top, push doubled value+ → pop top two, push both back, push their sumpublic int calPoints(String[] operations) { Stack<Integer> st = new Stack<>(); for (int i = 0; i < operations.length; i++) { String op = operations[i]; if (op.equals("C")) { st.pop(); // remove last score } else if (op.equals("D")) { st.push(st.peek() * 2); // double of last score } else if (op.equals("+")) { int prev1 = st.pop(); // most recent score int prev2 = st.pop(); // second most recent score int sum = prev1 + prev2; st.push(prev2); // put them back st.push(prev1); st.push(sum); // push the new score } else { st.push(Integer.parseInt(op)); // regular number } } // sum all remaining scores int total = 0; while (!st.empty()) { total += st.pop(); } return total;}One small improvement over your original solution — using op.equals("C") instead of op.charAt(0) == 'C'. This is cleaner and handles edge cases better since negative numbers like "-2" also start with - not a digit, so charAt(0) comparisons can get tricky. Using equals is always safer for string operations.Why the + Operation Needs Pop-Push-PopThe trickiest part is the + operation. You need the two most recent scores. Stack only lets you see the top. So you pop the first, then the second, compute the sum, then push both back before pushing the sum. This restores the record correctly — the previous two scores stay, and the new sum score is added on top.Detailed Dry Run — ops = ["5","2","C","D","+"]Let us trace every step carefully:"5" → number, parse and push Stack: [5]"2" → number, parse and push Stack: [5, 2]"C" → remove last score, pop Stack: [5]"D" → double last score, peek=5, push 10 Stack: [5, 10]"+" → sum of last two:pop prev1 = 10pop prev2 = 5sum = 15push prev2=5, push prev1=10, push sum=15 Stack: [5, 10, 15]Sum all: 5 + 10 + 15 = 30 ✅Detailed Dry Run — ops = ["5","-2","4","C","D","9","+","+"]"5" → push 5. Stack: [5]"-2" → push -2. Stack: [5, -2]"4" → push 4. Stack: [5, -2, 4]"C" → pop 4. Stack: [5, -2]"D" → peek=-2, push -4. Stack: [5, -2, -4]"9" → push 9. Stack: [5, -2, -4, 9]"+" → prev1=9, prev2=-4, sum=5. Push -4, 9, 5. Stack: [5, -2, -4, 9, 5]"+" → prev1=5, prev2=9, sum=14. Push 9, 5, 14. Stack: [5, -2, -4, 9, 5, 14]Sum: 5 + (-2) + (-4) + 9 + 5 + 14 = 27 ✅Approach 2: ArrayList (Most Readable)The IdeaArrayList gives you index-based access which makes the + operation much cleaner — no need to pop and push back. Just access the last two elements directly using size()-1 and size()-2.public int calPoints(String[] operations) { ArrayList<Integer> record = new ArrayList<>(); for (String op : operations) { int n = record.size(); if (op.equals("C")) { record.remove(n - 1); // remove last element } else if (op.equals("D")) { record.add(record.get(n - 1) * 2); // double last } else if (op.equals("+")) { // sum of last two — no need to remove and re-add! record.add(record.get(n - 1) + record.get(n - 2)); } else { record.add(Integer.parseInt(op)); } } int total = 0; for (int score : record) { total += score; } return total;}See how the + operation becomes a single line with ArrayList? record.get(n-1) + record.get(n-2) directly accesses the last two elements without any pop-push gymnastics.Dry Run — ops = ["5","2","C","D","+"]"5" → add 5. List: [5]"2" → add 2. List: [5, 2]"C" → remove last. List: [5]"D" → 5×2=10, add 10. List: [5, 10]"+" → get(0)+get(1) = 5+10=15, add 15. List: [5, 10, 15]Sum: 30 ✅Time Complexity: O(n) — single pass through operations Space Complexity: O(n) — ArrayList stores at most n scoresThe one tradeoff — remove(n-1) on an ArrayList is O(1) for the last element (no shifting needed). And get() is O(1). So this is fully O(n) overall and arguably the cleanest solution to read and understand.Approach 3: Deque (ArrayDeque — Fastest in Java)The IdeaArrayDeque is faster than Stack in Java because Stack is synchronized (thread-safe overhead) and ArrayDeque is not. For single-threaded LeetCode problems, ArrayDeque is always the better choice over Stack.public int calPoints(String[] operations) { Deque<Integer> deque = new ArrayDeque<>(); for (String op : operations) { if (op.equals("C")) { deque.pollLast(); // remove last } else if (op.equals("D")) { deque.offerLast(deque.peekLast() * 2); // double last } else if (op.equals("+")) { int prev1 = deque.pollLast(); int prev2 = deque.pollLast(); int sum = prev1 + prev2; deque.offerLast(prev2); // restore deque.offerLast(prev1); // restore deque.offerLast(sum); // new score } else { deque.offerLast(Integer.parseInt(op)); } } int total = 0; for (int score : deque) { total += score; } return total;}The logic is identical to the Stack approach. The only difference is the method names — offerLast instead of push, pollLast instead of pop, peekLast instead of peek.Time Complexity: O(n) Space Complexity: O(n)For iterating a Deque to sum, you can use a for-each loop directly — no need to pop everything out like with Stack.Approach ComparisonApproachTimeSpaceBest ForStackO(n)O(n)Classic interview answer, clear LIFO intentArrayListO(n)O(n)Cleanest code, easiest to readArrayDequeO(n)O(n)Best performance, preferred in productionAll three approaches have identical time and space complexity. The difference is purely in code style and readability. In an interview, any of these is perfectly acceptable. Mention that ArrayDeque is preferred over Stack in Java for performance if you want to impress.Why op.equals() Is Better Than op.charAt(0)Your original solution uses operations[i].charAt(0) == 'C' to check operations. This works but has a subtle risk — the + character check with charAt(0) is fine, but imagine if a future test had a number starting with C or D (it will not in this problem but defensive coding is a good habit). More importantly, "-2".charAt(0) is '-' which is fine, but using equals is semantically clearer — you are comparing the whole string, not just the first character. This shows cleaner coding habits in interviews.Edge Cases to KnowNegative numbers like "-2" Integer.parseInt("-2") handles negatives perfectly. The D operation on -2 gives -4. The + operation works correctly with negatives too. No special handling needed."C" after a "+" that added a score The problem guarantees C always has at least one score to remove. So after + adds a score, a C removes just that one new score — the previous two scores that + used remain intact. This is correct behavior and our solution handles it automatically.All scores removed If all operations are numbers followed by C operations removing every score, the stack ends up empty and the sum is 0. Our while loop handles this correctly — it simply never executes and returns 0.Only one operation A single number like ["5"] → push 5, sum is 5. Works fine.Common Mistakes to AvoidIn the + operation, forgetting to push both numbers back When you pop prev1 and prev2 to compute the sum, you must push them back onto the stack before pushing the sum. If you only push the sum without restoring prev1 and prev2, those scores disappear from the record permanently — which is wrong. The + operation only adds a new score, it does not remove the previous ones.Using charAt(0) comparison for detecting numbers Negative numbers start with -, not a digit. If you check charAt(0) >= '0' && charAt(0) <= '9' to detect numbers, you will miss negatives. The safest approach is to check for C, D, and + explicitly using equals, and fall through to the else for everything else (which covers both positive and negative numbers).Calling st.peek() or st.pop() without checking empty The problem guarantees valid operations — C always has something to remove, + always has two previous scores, D always has one. But in real code and defensive interview solutions, adding empty checks shows good habits even when the constraints guarantee safety.FAQs — People Also AskQ1. Why is Stack a good choice for LeetCode 682 Baseball Game? Because all four operations only access the most recently added scores — the last score for C and D, the last two for +. This "most recent first" access pattern is exactly what LIFO (Last In First Out) provides. Stack's push, pop, and peek all run in O(1) making it perfectly efficient.Q2. What is the time complexity of LeetCode 682? O(n) time where n is the number of operations. Each operation performs a constant number of stack operations (at most 3 pushes/pops for the + case). Space complexity is O(n) for storing scores.Q3. Why does the + operation need to pop and push back in the Stack approach? Stack only gives direct access to the top element. To get the second most recent score, you must pop the first one, peek/pop the second, then push the first one back. The ArrayList approach avoids this by using index-based access directly.Q4. What is the difference between Stack and ArrayDeque in Java for this problem? Both work correctly. ArrayDeque is faster because Stack is a legacy class that extends Vector and is synchronized (thread-safe), adding unnecessary overhead for single-threaded use. ArrayDeque has no synchronization overhead. For LeetCode and interviews, either is acceptable but ArrayDeque is technically better.Q5. Is LeetCode 682 asked in coding interviews? It appears occasionally as a warmup or screening problem. Its main value is testing whether you can carefully simulate rules without making logical errors — a skill that matters in systems programming, game development, and any domain with complex state management.Similar LeetCode Problems to Practice Next71. Simplify Path — Medium — stack simulation with path operations1047. Remove All Adjacent Duplicates In String — Easy — stack simulation735. Asteroid Collision — Medium — stack simulation with conditions150. Evaluate Reverse Polish Notation — Medium — stack with arithmetic operations, very similar pattern227. Basic Calculator II — Medium — stack with operator precedenceConclusionLeetCode 682 Baseball Game is a perfect problem to build confidence with stack simulation. The four operations are clearly defined, the rules are unambiguous, and the stack maps naturally to every operation. Once you understand why pop-push-back is needed for + in the stack approach and how ArrayList simplifies that with index access, you have genuinely understood the tradeoffs between these data structures.If you are newer to stacks, start with the ArrayList solution for clarity. Once that clicks, rewrite it with Stack to understand the LIFO mechanics. Then try ArrayDeque to understand why it is preferred in modern Java code.

LeetCodeJavaStackArrayListDequeEasy
LeetCode 3751: Total Waviness of Numbers in Range I – Java Solution with Dry Run and Explanation

LeetCode 3751: Total Waviness of Numbers in Range I – Java Solution with Dry Run and Explanation

IntroductionLeetCode 3751 introduces an interesting digit-based pattern problem where we calculate the total waviness of numbers inside a given range.This problem combines:Digit traversalPattern recognitionPeaks and valleys logicString manipulationBrute force iterationThe problem is straightforward once we clearly understand how peaks and valleys work.Problem StatementYou are given two integers:num1 and num2representing the inclusive range:[num1, num2]For every number:A digit is called a peak if it is strictly greater than both neighbors.A digit is called a valley if it is strictly smaller than both neighbors.The first and last digits can never be peaks or valleys.Return the total waviness across all numbers in the range.ExampleInputnum1 = 120num2 = 130Output3ExplanationNumbers contributing to waviness:120 → 2 is a peak → waviness = 1121 → 2 is a peak → waviness = 1130 → 3 is a peak → waviness = 1Total:1 + 1 + 1 = 3Understanding Peaks and ValleysPeak ConditionA digit is a peak if:digit > left neighborANDdigit > right neighborExample:484Here:8 > 48 > 4So 8 is a peak.Valley ConditionA digit is a valley if:digit < left neighborANDdigit < right neighborExample:202Here:0 < 20 < 2So 0 is a valley.Key ObservationWe only need to check:index 1 to length-2because:First digit has no left neighborLast digit has no right neighborIntuitionThe simplest way:Iterate through every number in the rangeConvert number into charactersCheck each middle digitCount peaks and valleysAdd to final answerSince constraints are small:num2 <= 10^5a brute force approach works efficiently.Java Solutionclass Solution { public int totalWaviness(int num1, int num2) { int ans = 0; if(num1 == num2) { int temp = num1; String c = Integer.toString(temp); char[] arr = c.toCharArray(); for(int i = 1; i < arr.length - 1; i++) { if((arr[i] < arr[i - 1] && arr[i] < arr[i + 1]) || (arr[i] > arr[i - 1] && arr[i] > arr[i + 1])) { ans++; } } return ans; } for(int i = num1; i <= num2; i++) { String c = Integer.toString(i); char[] carr = c.toCharArray(); for(int j = 1; j < carr.length - 1; j++) { if((carr[j] < carr[j - 1] && carr[j] < carr[j + 1]) || (carr[j] > carr[j - 1] && carr[j] > carr[j + 1])) { ans++; } } } return ans; }}Step-by-Step ExplanationStep 1: Iterate Through Rangefor(int i = num1; i <= num2; i++)Process every number.Step 2: Convert Number to Character ArrayString c = Integer.toString(i);char[] carr = c.toCharArray();This makes digit comparison easier.Step 3: Check Middle Digitsfor(int j = 1; j < carr.length - 1; j++)Skip first and last digits.Step 4: Check Peak or Valley(carr[j] < carr[j-1] && carr[j] < carr[j+1])OR(carr[j] > carr[j-1] && carr[j] > carr[j+1])If true:ans++;Dry RunInputnum1 = 198num2 = 202Number 198Digits:1 9 8Check 9:9 > 19 > 8Peak found.Waviness:1Number 1991 9 99 is not strictly greater than right neighbor.No waviness.Number 2012 0 10 is smaller than both neighbors.Valley found.Number 2022 0 2Again:0 < 20 < 2Valley found.Final Answer198 → 1201 → 1202 → 1Total = 3Time Complexity AnalysisLet:N = num2 - num1 + 1D = number of digitsTime ComplexityO(N × D)At most:D = 6which is very small.Efficient for constraints.Space ComplexityO(D)due to character array conversion.Why Brute Force Works HereConstraints are small:num2 <= 100000So checking every number directly is acceptable.For larger constraints:Digit DP would be needed.But here:Simplicity is better.Common Mistakes1. Including First or Last DigitThese digits cannot be peaks or valleys.2. Using Non-Strict ComparisonWrong:>=<=Correct:><because definition says:strictly greaterstrictly smaller3. Forgetting Both ConditionsNeed to check:PeakValleyEdge CasesSingle Digit NumbersWaviness:0because fewer than 3 digits.Repeated DigitsExample:111No peak or valley.Alternating DigitsExample:4848Produces multiple waviness counts.Interview ExplanationIn interviews explain:Since the constraints are small, we can directly iterate through every number, convert it into digits, and count peaks and valleys by checking neighboring digits.This demonstrates:Observation skillsConstraint analysisClean implementationConclusionLeetCode 3751 is a clean implementation problem focused on:Digit traversalPattern recognitionPeaks and valleysBrute force optimizationThe key insight is:A digit contributes to waviness only if it is strictly greater or strictly smaller than both immediate neighbors.Once this condition is understood, the implementation becomes very straightforward.

LeetCodeJavaStringPeaks and ValleysBrute Force SolutionDigit ProblemsMediumArray
Left and Right Sum Differences

Left and Right Sum Differences

LeetCode 2574: Left and Right Sum Differences (Java)The Left and Right Sum Difference problem is a classic array manipulation challenge. It tests your ability to efficiently calculate prefix and suffix values—a skill essential for more advanced algorithms like "Product of Array Except Self."🔗 ResourcesProblem Link: LeetCode 2574 - Left and Right Sum Differences📝 Problem StatementYou are given a 0-indexed integer array nums. You need to return an array answer of the same length where:answer[i] = |leftSum[i] - rightSum[i]|leftSum[i] is the sum of elements to the left of index i.rightSum[i] is the sum of elements to the right of index i.💡 Approach 1: The Three-Array Method (Beginner Friendly)This approach is highly intuitive. We pre-calculate all left sums and all right sums in separate arrays before computing the final difference.The LogicPrefix Array: Fill pref[] by adding the previous element to the cumulative sum.Suffix Array: Fill suff[] by iterating backward from the end of the array.Result: Loop one last time to calculate Math.abs(pref[i] - suff[i]).Java Implementationpublic int[] leftRightDifference(int[] nums) {int n = nums.length;int[] pref = new int[n];int[] suff = new int[n];int[] ans = new int[n];// Calculate Left Sums (Prefix)for (int i = 1; i < n; i++) {pref[i] = pref[i - 1] + nums[i - 1];}// Calculate Right Sums (Suffix)for (int i = n - 2; i >= 0; i--) {suff[i] = suff[i + 1] + nums[i + 1];}// Combine resultsfor (int i = 0; i < n; i++) {ans[i] = Math.abs(pref[i] - suff[i]);}return ans;}Time Complexity: O(n)Space Complexity: O(n) (Uses extra space for pref and suff arrays).🚀 Approach 2: The Running Sum Method (Space Optimized)In technical interviews, you should aim for O(1) extra space. Instead of storing every suffix sum, we calculate the Total Sum first and derive the right sum using logic.The LogicWe use the mathematical property:Right Sum = Total Sum - Left Sum - Current ElementBy maintaining a single variable leftSum that updates as we iterate, we can calculate the result using only the output array.Java Implementationpublic int[] leftRightDifference(int[] nums) {int n = nums.length;int[] ans = new int[n];int totalSum = 0;int leftSum = 0;// 1. Get the total sum of all elementsfor (int num : nums) {totalSum += num;}// 2. Calculate rightSum and difference on the flyfor (int i = 0; i < n; i++) {int rightSum = totalSum - leftSum - nums[i];ans[i] = Math.abs(leftSum - rightSum);// Update leftSum for the next indexleftSum += nums[i];}return ans;}Time Complexity: O(n)Space Complexity: O(1) (Excluding the output array).📊 Summary ComparisonFeatureApproach 1Approach 2Space ComplexityO(n) (Higher)O(1) (Optimal)Logic TypeStorage-basedMathematicalUse CaseBeginnersInterviewsKey TakeawayWhile Approach 1 is easier to visualize, Approach 2 is more professional. It shows you can handle data efficiently without unnecessary memory allocation, which is critical when dealing with large-scale systems.

LeetCodePrefixSuffix
Longest Substring with K Unique Characters – Efficient Sliding Window Solution

Longest Substring with K Unique Characters – Efficient Sliding Window Solution

IntroductionFinding substrings with unique characters is a common problem in string manipulation and interview questions.GFG’s Longest Substring with K Unique Characters problem challenges you to find the length of the longest substring containing exactly K distinct characters.The focus is not just on brute-force solutions, but on using sliding window with a HashMap to efficiently track character frequencies and window boundaries.If you’d like to try solving the problem first, you can attempt it here: Try the problem on GeeksforGeeks: https://leetcode.com/problems/max-consecutive-ones-iii/Problem UnderstandingYou are given:A string s of lowercase lettersAn integer kYour task: Return the length of the longest substring containing exactly k distinct characters.Examples:Input: s = "aabacbebebe", k = 3Output: 7Explanation: "cbebebe" is the longest substring with 3 distinct characters.Input: s = "aaaa", k = 2Output: -1Explanation: No substring contains exactly 2 distinct characters.Input: s = "aabaaab", k = 2Output: 7Explanation: "aabaaab" has exactly 2 unique characters.A naive approach would try all possible substrings, count unique characters, and track the max length.Time Complexity: O(n²)Inefficient for large stringsKey Idea: Sliding Window + HashMapInstead of recomputing the unique characters for every substring:Use a HashMap to store character frequencies in the current windowUse two pointers (i and j) to maintain the windowExpand the window by moving j and add characters to the mapShrink the window from the left when the number of unique characters exceeds kUpdate the max length whenever the window has exactly k unique charactersThis ensures each character is processed only once, giving a linear solution.Approach (Step-by-Step)Initialize a HashMap mp to store character countsUse two pointers i (window start) and j (window end)Iterate over the string with jAdd s[j] to the map and update its countCheck the map size:If size < k → move j forwardIf size == k → update max length, move j forwardIf size > k → shrink window from i until map size <= kAfter iterating, return max (or -1 if no valid substring exists)Optimization:Using a HashMap avoids recalculating the number of unique characters for every substringSliding window ensures O(n) complexityImplementation (Java)class Solution {public int longestKSubstr(String s, int k) {HashMap<Character,Integer> mp = new HashMap<>();int i = 0, j = 0;int max = -1;while (j < s.length()) {// Add current character to mapmp.put(s.charAt(j), mp.getOrDefault(s.charAt(j), 0) + 1);if (mp.size() < k) {j++;}else if (mp.size() == k) {// Update max length when exactly k unique charactersmax = Math.max(max, j - i + 1);j++;}else {// Shrink window until map size <= kwhile (mp.size() > k) {mp.put(s.charAt(i), mp.get(s.charAt(i)) - 1);if (mp.get(s.charAt(i)) == 0) {mp.remove(s.charAt(i));}i++;}max = Math.max(max, j - i + 1);j++;}}return max;}}Dry Run ExampleInput:s = "aabacbebebe", k = 3Execution:Window [0-4] = "aaba" → unique = 2 → continueWindow [1-6] = "abacb" → unique = 3 → max = 5Window [4-10] = "cbebebe" → unique = 3 → max = 7Output:7Complexity AnalysisTime Complexity: O(n) → Each character enters and leaves the window onceSpace Complexity: O(k) → HashMap stores at most k unique charactersEdge Cases Consideredk greater than number of unique characters → return -1Entire string has exactly k unique charactersString with repeated characters onlyMinimum string length (1)Sliding Window Pattern ImportanceThis problem reinforces a common sliding window + hashmap pattern used in:Longest substring problems with constraintsCounting substrings with exactly K conditionsOptimizing brute-force approaches to linear solutionsConclusionBy combining sliding window with HashMap frequency tracking, we reduce an O(n²) problem to O(n).Once you master this approach, many string and substring problems with constraints on unique characters become much easier to solve efficiently.

SlidingWindowHashMapStringsGFG
Intersection of Two Arrays

Intersection of Two Arrays

LeetCode Problem 349Link of the Problem to try -: LinkGiven two integer arrays nums1 and nums2, return an array of their intersection. Each element in the result must be unique and you may return the result in any order. Example 1:Input: nums1 = [1,2,2,1], nums2 = [2,2]Output: [2]Example 2:Input: nums1 = [4,9,5], nums2 = [9,4,9,8,4]Output: [9,4]Explanation: [4,9] is also accepted. Constraints:1 <= nums1.length, nums2.length <= 10000 <= nums1[i], nums2[i] <= 1000Solution: Using HashMap and HashSet for IntersectionCore Objective: The primary goal of this problem is to identify common elements between two arrays and return them as a unique set. To achieve this efficiently, we utilize a combination of a HashMap for quick lookup and a HashSet to handle uniqueness in the result.Logic & Implementation:Populate Lookup Table: We begin by iterating through the first array (nums1) and storing its elements in a HashMap. This allows us to verify the existence of any number in $O(1)$ average time.Identify Intersection: Next, we iterate through the second array (nums2). For each element, we check if it exists within our HashMap.Handle Uniqueness: If a match is found, we add the element to a HashSet. This is a crucial step because the problem requires the output to contain unique elements, even if a common number appears multiple times in the input arrays.Final Conversion: Since the required return type is an array, we determine the size of our HashSet to initialize the result array. Using a for-each loop, we transfer the values from the Set into the array and return it as the final answer.Code Implementation:public int[] intersection(int[] nums1, int[] nums2) { // HashMap to store elements from the first array for lookup HashMap<Integer, Integer> mp = new HashMap<>(); // HashSet to store unique common elements HashSet<Integer> ms = new HashSet<>(); // Populate the map with elements from nums1 for (int i = 0; i < nums1.length; i++) { if (mp.containsKey(nums1[i])) { continue; } mp.put(nums1[i], i); } // Check for common elements in nums2 for (int i = 0; i < nums2.length; i++) { if (mp.containsKey(nums2[i])) { ms.add(nums2[i]); // HashSet ensures duplicates are not added } } // Convert the HashSet to the final integer array int[] ans = new int[ms.size()]; int co = 0; for (int i : ms) { ans[co] = i; co++; } return ans;}

LeetCodeArrayEasyHashMap
Maximum Twin Sum of a Linked List (LeetCode 2130) — Intuition, Dry Run & Optimal Approach

Maximum Twin Sum of a Linked List (LeetCode 2130) — Intuition, Dry Run & Optimal Approach

🔗 Try This ProblemPractice here:https://leetcode.com/problems/maximum-twin-sum-of-a-linked-list/📌 Problem OverviewYou are given a linked list of even length.Each node has a twin node defined as:Node at index i → Twin is at index (n - 1 - i)👉 Example (n = 4):Index 0 ↔ Index 3Index 1 ↔ Index 2The twin sum is:node[i] + node[n - 1 - i]🎯 Goal:Return the maximum twin sum among all such pairs.🧠 Understanding the ProblemLet’s take a simple example:Input: [4, 2, 2, 3]Pairs:4 + 3 = 72 + 2 = 4👉 So the answer is:7💡 Intuition — How to Think About This ProblemAt first glance, it feels like we need to access:First nodeLast nodeSecond nodeSecond last node👉 This naturally suggests:Two-directional accessBut linked lists don’t allow backward traversal🤔 Possible Thinking Patterns🔹 Idea 1: Use Extra SpaceStore values in an arrayUse two pointers from both ends✔ Works❌ But uses extra space O(n)🔹 Idea 2: Work Directly on Linked List (Better)We need a way to:Reach the middleAccess second half in reverse order👉 That leads to a powerful idea:“What if we reverse the second half of the list?”Now we can compare:First half (forward)Second half (also forward after reversal)🎥 Visual Intuition (Your Explanation Video)👉 In this section, focus only on:ConceptVisualizationThought process❌ Avoid code here✔ Build understanding⚙️ Optimized Approach (Step-by-Step)Step 1: Find MiddleUse two pointers:slow → moves 1 stepfast → moves 2 steps👉 When fast reaches end → slow is at middleStep 2: Reverse Second HalfStart from:slow.nextReverse the listStep 3: Compare Twin PairsNow:One pointer → start of listOne pointer → start of reversed halfCalculate:sum = left.val + right.valTrack maximum🧪 Dry RunExample:Input: [5, 4, 2, 1]Step 1: Find MiddleSlow → 4Fast → EndStep 2: Reverse Second HalfSecond half: 2 → 1Reversed: 1 → 2Step 3: Compare5 + 1 = 64 + 2 = 6✅ Maximum = 6💻 Optimized Code (Java)class Solution {public ListNode reverse(ListNode head){ListNode curr = head;ListNode prev = null;while(curr != null){ListNode next = curr.next;curr.next = prev;prev = curr;curr = next;}return prev;}public int pairSum(ListNode head) {ListNode slow = head;ListNode fast = head;// Find middlewhile(fast.next != null && fast.next.next != null){slow = slow.next;fast = fast.next.next;}// Reverse second halfListNode secondHalf = reverse(slow.next);// Compare pairsListNode firstHalf = head;int max = Integer.MIN_VALUE;while(secondHalf != null){int sum = firstHalf.val + secondHalf.val;max = Math.max(max, sum);firstHalf = firstHalf.next;secondHalf = secondHalf.next;}return max;}}⏱️ Complexity AnalysisTypeValueTime ComplexityO(n)Space ComplexityO(1)🧩 Key TakeawaysLinked list problems often require restructuring instead of extra spaceReversing half is a powerful trickFast & slow pointer is essential for mid-finding🏁 Final ThoughtsThis problem is a perfect example of:Combining multiple conceptsOptimizing spaceThinking beyond direct access👉 Once you understand this pattern, many linked list problems become easier.

LeetcodeMediumLinkedListFast and Slow Pointer
Contains Duplicate

Contains Duplicate

LeetCode Problem 217Link of the Problem to try -: LinkGiven an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.Example 1:Input: nums = [1,2,3,1]Output: trueExplanation:The element 1 occurs at the indices 0 and 3.Example 2:Input: nums = [1,2,3,4]Output: falseExplanation:All elements are distinct.Example 3:Input: nums = [1,1,1,3,3,4,3,2,4,2]Output: trueConstraints:1 <= nums.length <= 105-109 <= nums[i] <= 109Solution:1. The HashMap ApproachUsing a HashMap is a highly effective way to track occurrences. Although we are only checking for the existence of a value, the HashMap logic allows us to store the element as a "Key" and its frequency as a "Value."Logic:Iterate through the nums array.Before inserting an element, check if the HashMap already contains that key.If it exists, you've found your duplicate—return true.Otherwise, put the value into the map and continue.Code:public boolean containsDuplicate(int[] nums) {HashMap<Integer, Integer> map = new HashMap<>();for (int i = 0; i < nums.length; i++) {// Check if the current number is already a key in our mapif (map.containsKey(nums[i])) {return true;}// Map the number to its count (1)map.put(nums[i], 1);}return false;}2. The HashSet Approach (Optimized for Storage)While similar to the HashMap, the HashSet is often more appropriate for this specific problem because we only care if a number exists, not how many times it appears or what its index is.Logic:We initialize an empty HashSet.As we loop through the array, we check ms.contains(nums[i]).If the set already has the number, it's a duplicate.This approach is preferred over HashMap for this problem because it uses less memory and has cleaner syntax.Code:public boolean containsDuplicate(int[] nums) {HashSet<Integer> ms = new HashSet<>();for (int i = 0; i < nums.length; i++) {if (ms.contains(nums[i])) {return true;}ms.add(nums[i]);}return false;}3. The Sorting & Two-Pointer ApproachIf you want to avoid using extra memory (like a Set or Map), you can use the Two-Pointer method combined with Sorting.Logic:First, we sort the array. This ensures that any duplicate values are placed next to each other.We use two pointers: j (the previous element) and i (the current element).By moving these pointers across the array, we compare the values. If nums[i] == nums[j], a duplicate exists.Code:public boolean containsDuplicate(int[] nums) {// Step 1: Sort the arrayArrays.sort(nums);// Step 2: Use two pointers to compare adjacent elementsint j = 0;for (int i = 1; i < nums.length; i++) {if (nums[i] == nums[j]) {return true; // Duplicate found}j++; // Move the previous pointer forward}return false;}Performance SummaryApproachTime ComplexitySpace ComplexityRecommendationHashSetO(n)O(n)Best Overall – Fastest performance.HashMapO(n)O(n)Good, but HashSet is cleaner for this use case.Two PointerO(n \log n)O(1)Best for Memory – Use if space is limited.Final ThoughtsChoosing the right approach depends on whether you want to prioritize speed (HashSet) or memory efficiency (Two-Pointer). For most coding interviews, the HashSet solution is the "Gold Standard" due to its linear time complexity.

LeetCodeEasyHashMap
LeetCode 402: Remove K Digits — Java Solution Explained

LeetCode 402: Remove K Digits — Java Solution Explained

IntroductionLeetCode 402 Remove K Digits is one of those problems where the brute force solution feels obvious but completely falls apart at scale — and the optimal solution requires a genuinely clever insight that, once you see it, feels like magic.This problem sits at the intersection of two powerful techniques — Greedy thinking and Monotonic Stack. If you have already solved Next Greater Element and Asteroid Collision, you have all the building blocks you need. This is where those patterns level up.You can find the problem here — LeetCode 402 Remove K Digits.What Is the Problem Really Asking?You are given a number as a string and an integer k. Remove exactly k digits from the number such that the resulting number is as small as possible. Return it as a string without leading zeros.Example:num = "1432219", k = 3We want to remove 3 digits to make the number as small as possibleRemove 4, 3, 2 → remaining is "1219"Output: "1219"Simple goal — smallest possible number after exactly k removals.Real Life Analogy — Choosing the Best Price TagImagine you are reading a price tag digit by digit from left to right. Every time you see a digit that is bigger than the next one coming, you have a chance to remove it and make the price smaller. You have a limited number of removals — use them wisely on the biggest offenders from the left side, because leftmost digits have the most impact on the overall value.Removing a 9 from the front of a number shrinks it far more than removing a 9 from the end. This left-to-right priority with greedy removal is the entire insight of this problem.The Core Greedy InsightHere is the key question — which digit should we remove first to make the number as small as possible?Think about it this way. In the number "1432219", which digit is hurting us the most? It is 4 — because it is large and sits early in the number. After removing 4 we get "132219". Now 3 is the biggest early offender. And so on.More precisely — whenever you see a digit that is greater than the digit immediately after it, removing it makes the number smaller. This is because a larger digit sitting before a smaller digit inflates the overall value.This is the Greedy rule: scan left to right, and whenever the current digit on the stack is greater than the incoming digit, remove it (if we still have removals left).A Monotonic Increasing Stack enforces exactly this — it keeps digits in non-decreasing order, automatically kicking out any digit that is larger than what comes next.The Solution — Monotonic Stackpublic String removeKdigits(String num, int k) { Stack<Character> st = new Stack<>(); // build monotonic increasing stack for (int i = 0; i < num.length(); i++) { // while stack top is greater than current digit and we still have removals while (!st.empty() && k != 0 && st.peek() > num.charAt(i)) { st.pop(); // remove the larger digit — greedy choice k--; } st.push(num.charAt(i)); } // if k removals still remaining, remove from the end (largest digits are at top) while (k != 0) { st.pop(); k--; } // build result string from stack StringBuilder sb = new StringBuilder(); while (!st.empty()) { sb.append(st.pop()); } sb.reverse(); // stack gives reverse order // remove leading zeros while (sb.length() > 0 && sb.charAt(0) == '0') { sb.deleteCharAt(0); } // if nothing left, return "0" return sb.length() == 0 ? "0" : sb.toString();}Step-by-Step Dry Run — num = "1432219", k = 3Processing 1: Stack empty → push. Stack: [1]Processing 4: Stack top 1 < 4 → no pop. Push 4. Stack: [1, 4]Processing 3: Stack top 4 > 3 and k=3 → pop 4, k=2. Stack: [1] Stack top 1 < 3 → stop. Push 3. Stack: [1, 3]Processing 2: Stack top 3 > 2 and k=2 → pop 3, k=1. Stack: [1] Stack top 1 < 2 → stop. Push 2. Stack: [1, 2]Processing 2: Stack top 2 == 2 → not greater, stop. Push 2. Stack: [1, 2, 2]Processing 1: Stack top 2 > 1 and k=1 → pop 2, k=0. Stack: [1, 2] k=0, stop. Push 1. Stack: [1, 2, 1]Processing 9: k=0, no more removals. Push 9. Stack: [1, 2, 1, 9]k is now 0, skip the cleanup loop.Build result: pop all → "9121" → reverse → "1219" No leading zeros. Return "1219" ✅Step-by-Step Dry Run — num = "10200", k = 1Processing 1: stack empty → push. Stack: [1]Processing 0: Stack top 1 > 0 and k=1 → pop 1, k=0. Stack: [] Push 0. Stack: [0]Processing 2: k=0, no removals. Push. Stack: [0, 2]Processing 0: k=0. Push. Stack: [0, 2, 0]Processing 0: k=0. Push. Stack: [0, 2, 0, 0]k=0, skip cleanup.Build result: "0020" → reverse → "0200" Remove leading zero → "200" Return "200" ✅Step-by-Step Dry Run — num = "10", k = 2Processing 1: push. Stack: [1]Processing 0: Stack top 1 > 0 and k=2 → pop 1, k=1. Stack: [] Push 0. Stack: [0]k=1 remaining → cleanup loop → pop 0, k=0. Stack: []Build result: empty string. Remove leading zeros: nothing to remove. Length is 0 → return "0" ✅The Three Tricky Cases You Must HandleCase 1 — k is not fully used after the loop This happens with a non-decreasing number like "12345" with k=2. No element ever gets popped during the loop because no digit is greater than the next one. The stack ends up as [1,2,3,4,5] with k still = 2. The solution is to pop from the top of the stack — which holds the largest digits — until k hits 0.Case 2 — Leading zeros After removing digits, the remaining number might start with zeros. "10200" becomes "0200" after removing 1. We strip leading zeros with a while loop. But we must be careful not to strip the only zero — that is why we check length > 0 before each removal.Case 3 — Empty result If all digits are removed (like "10" with k=2), the StringBuilder ends up empty. We return "0" because an empty number is represented as zero.Why Remaining k Gets Removed From the EndAfter the main loop, if k is still greater than 0, why do we remove from the top of the stack (which corresponds to the end of the number)?Because the stack at this point holds a non-decreasing sequence. In a non-decreasing sequence, the largest values are at the end. Removing from the end removes the largest remaining digits — which is exactly the greedy choice to minimize the number.For example in "12345" with k=2: the stack is [1,2,3,4,5]. Pop top twice → remove 5 and 4 → [1,2,3] → result is "123". Correct!Time and Space ComplexityTime Complexity: O(n) — each digit is pushed onto the stack exactly once and popped at most once. Even with the while loop inside the for loop, total push and pop operations across the entire run never exceed 2n. The leading zero removal is also O(n) in the worst case. Overall stays linear.Space Complexity: O(n) — the stack holds at most n digits in the worst case when no removals happen during the main loop.Common Mistakes to AvoidNot handling remaining k after the loop This is the most common mistake. If the number is already non-decreasing, the loop never pops anything. Forgetting the cleanup loop gives the wrong answer for inputs like "12345" with k=2.Not removing leading zeros After removals, the result might start with zeros. "0200" should be returned as "200". Skipping this step gives wrong output.Returning empty string instead of "0" When all digits are removed, return "0" not "". An empty string is not a valid number representation.Using >= instead of > in the pop condition If you pop on equal digits too, you remove more than necessary and might get wrong results. Only pop when strictly greater — equal digits in sequence are fine to keep.How This Connects to the Monotonic Stack SeriesLooking at the stack problems you have been solving:496 Next Greater Element — monotonic decreasing stack, find first bigger to the right 735 Asteroid Collision — stack simulation, chain reactions 402 Remove K Digits — monotonic increasing stack, greedy removal for minimum numberThe direction of the monotonic stack flips based on what you are optimizing. For "next greater" you keep a decreasing stack. For "smallest number" you keep an increasing stack. Recognizing which direction to go is the skill that connects all these problems.FAQs — People Also AskQ1. Why does a Monotonic Stack give the smallest number in LeetCode 402? A monotonic increasing stack ensures that at every point, the digits we have kept so far are in the smallest possible order. Whenever a smaller digit arrives and the stack top is larger, removing that larger digit can only make the number smaller — this greedy choice is always optimal.Q2. What happens if k is not fully used after the main loop in LeetCode 402? The number is already in non-decreasing order so no removals happened during the loop. The remaining removals should be made from the end of the number (top of stack) since those are the largest values in a non-decreasing sequence.Q3. How are leading zeros handled in LeetCode 402? After building the result string, strip any leading zeros with a while loop. If stripping leaves an empty string, return "0" because the empty case represents zero.Q4. What is the time complexity of LeetCode 402? O(n) time because each digit is pushed and popped at most once across the entire algorithm. Space complexity is O(n) for the stack.Q5. Is LeetCode 402 asked in coding interviews? Yes, it is frequently asked at companies like Google, Amazon, and Microsoft. It tests greedy thinking combined with monotonic stack — two patterns that interviewers love because they require genuine insight rather than memorization.Similar LeetCode Problems to Practice Next496. Next Greater Element I — Easy — monotonic decreasing stack739. Daily Temperatures — Medium — next greater with distances316. Remove Duplicate Letters — Medium — similar greedy stack with constraints1081. Smallest Subsequence of Distinct Characters — Medium — same approach as 31684. Largest Rectangle in Histogram — Hard — advanced monotonic stackConclusionLeetCode 402 Remove K Digits is a beautiful problem that rewards clear thinking. The greedy insight — always remove a digit when a smaller digit comes after it — naturally leads to the monotonic stack solution. The three edge cases (remaining k, leading zeros, empty result) are what separate a buggy solution from a clean, accepted one.Work through all three dry runs carefully. Once you see how the stack stays increasing and how each pop directly corresponds to a greedy removal decision, this pattern will click permanently and carry you through harder stack problems ahead.

LeetCodeJavaStackMonotonic StackGreedyStringMedium
LeetCode 835: Image Overlap – Java Matrix Translation Solution

LeetCode 835: Image Overlap – Java Matrix Translation Solution

IntroductionWhat happens when one binary image is shifted over another and the goal is to find the position where the two images overlap the most?That is exactly the idea behind LeetCode 835: Image Overlap.Each image is represented as an n × n binary matrix containing only 0s and 1s. One image can be moved left, right, up, or down, but it cannot be rotated.After every possible translation, the number of positions containing 1 in both images is calculated. The maximum value among all translations is the answer.Since n is at most 30, a direct simulation of every possible translation is practical.Question LinkLeetCode 835 – Image OverlapUnderstanding the ProblemConsider two images:Image 11 1 00 1 00 1 0and:Image 20 0 00 1 10 0 1The first image can be translated.For example, shifting Image 1:Right → 1Down → 1can align several 1s from the two images.The objective is to find the translation that produces the largest number of overlapping 1s.Key ObservationA translation can be represented using two values:row offsetcolumn offsetFor example:row offset = 1column offset = 1means that a cell from Image 2 at:(i, j)is compared with Image 1 at:(i + 1, j + 1)The algorithm tries every possible row and column offset.For an n × n matrix, each offset ranges from:-(n - 1) to (n - 1)This covers every possible way the two images can overlap without requiring rotation.ApproachThe solution is divided into two parts.Generate Every TranslationThe largestOverlap() function generates every possible pair of row and column offsets.for(int i = -n + 1; i < n; i++)handles vertical movement.for(int j = -n + 1; j < n; j++)handles horizontal movement.For every pair:(rowOffset, columnOffset)the helper function ch() calculates the overlap.Count the Overlapping OnesFor every cell in img2, the translated position inside img1 is calculated:int tarrow = i + rowo;int tarcol = j + colo;The position is counted only when:It is still inside img1.img2[i][j] == 1.img1[tarrow][tarcol] == 1.Every such position contributes 1 to the overlap.Java Solutionclass Solution { // Calculates the overlap for one particular translation. public int ch(int[][] im1, int[][] im2, int rowo, int colo) { int max = 0; // Traverse every cell of img2. for (int i = 0; i < im2.length; i++) { for (int j = 0; j < im2.length; j++) { // Apply the row and column translation. int tarrow = i + rowo; int tarcol = j + colo; // Make sure the translated position // is still inside img1. if (tarrow >= 0 && tarcol >= 0 && tarrow < im1.length && tarcol < im1.length && im2[i][j] == 1 && im1[tarrow][tarcol] == 1) { // Both images contain 1 at this position. max++; } } } return max; } public int largestOverlap(int[][] img1, int[][] img2) { int n = img1.length; // Stores the maximum overlap found so far. int an = 0; // Try every possible row translation. for (int i = -n + 1; i < n; i++) { // Try every possible column translation. for (int j = -n + 1; j < n; j++) { // Calculate overlap for this translation. int max = 0; max = ch(img1, img2, i, j); // Keep the best overlap. an = Math.max(max, an); } } return an; }}Visual Dry RunConsider:img1 =1 1 00 1 00 1 0img2 =0 0 00 1 10 0 1One useful translation is:row offset = 1column offset = 1The idea is to move Image 2 relative to Image 1 and compare the positions containing 1.How offset move on matrix?The visual makes the important idea immediately clear: translation changes the positions being compared, while rotation is never performed.How the Translation WorksSuppose:rowo = 1colo = 1For a cell in img2:(i, j)the corresponding position in img1 becomes:(i + 1, j + 1)For example:img2[1][1]is compared with:img1[2][2]If both contain 1, the overlap count increases.The boundary check is important because some translations move cells outside the matrix.For example, a large positive offset could produce:tarrow >= im1.lengthIn that case, the translated cell is outside the image and must not be counted.Dry Run of the Helper FunctionSuppose the translation is:rowo = 1colo = 1For every (i, j) in img2, the code calculates:tarrow = i + rowo;tarcol = j + colo;Then it checks:img2[i][j] == 1and:img1[tarrow][tarcol] == 1Only when both are true does the overlap increase.Conceptually:Image 2 cell ↓Apply translation ↓Corresponding Image 1 cell ↓Are both values 1? ↓Yes → overlap++After checking every cell, ch() returns the overlap for that particular translation.Exploring All Possible TranslationsFor a matrix of size n, the row offset is explored from:-(n - 1)to:n - 1The same is done for the column offset.For example, when:n = 3the possible offsets are:-2, -1, 0, 1, 2Therefore, all combinations are tested:(-2,-2) (-2,-1) (-2,0) ...(-1,-2) (-1,-1) (-1,0) ...( 0,-2) ( 0,-1) ( 0,0) ......Each pair represents one possible translation.The largest overlap among all of them becomes the final answer.Why Boundary Checking Is NecessaryImagine shifting an image to the right.Some cells will move beyond the right edge of the matrix.Those cells effectively disappear.The same happens when moving:leftupdownTherefore, before accessing:im1[tarrow][tarcol]the solution verifies:tarrow >= 0tarcol >= 0tarrow < im1.lengthtarcol < im1.lengthThis prevents an ArrayIndexOutOfBoundsException and correctly handles pixels that move outside the image.Why Rotation Is Not NeededThe problem allows only translation.That means the relative structure of the 1s never changes.For example:1 00 1can move:← →↑ ↓but it cannot become:0 11 0through rotation.This is why simply testing row and column offsets is enough.Complexity AnalysisThere are:(2n - 1)possible row offsets and the same number of column offsets.Therefore, the number of translations is:(2n - 1)²For every translation, the entire n × n matrix is scanned.That takes:O(n²)So the overall complexity is:O((2n - 1)² × n²)which simplifies to:O(n⁴)Space ComplexityThe solution uses only a few variables apart from the input matrices.Therefore, the extra space is:O(1)Why O(n⁴) Is Acceptable HereAt first glance, O(n⁴) may look expensive.However, the constraint is:n ≤ 30This is a relatively small matrix.The number of translations is at most:(2 × 30 - 1)² = 59² = 3481and each translation checks at most:30 × 30 = 900cells.So the total amount of work remains manageable.This is a good example of an important DSA principle:The best algorithm depends not only on the Big-O notation, but also on the actual constraints.Interview TipWhen dealing with two matrices or grids and the problem allows one of them to move, a useful first question is:Can the movement be represented using coordinates or offsets?Here, every possible movement can be represented with:(row offset, column offset)Once that representation is identified, the problem becomes a straightforward simulation.A good mental pattern is:Movement ↓Represent movement as coordinates ↓Try every valid movement ↓Compare overlapping positions ↓Keep the maximumThis pattern can also appear in problems involving:Grid matchingPattern alignment2D simulationsImage processingCoordinate transformationsConclusionLeetCode 835 is a useful matrix simulation problem that demonstrates how a seemingly visual problem can be converted into simple coordinate calculations.The main idea is to represent every possible translation using a row offset and column offset. For each translation, every cell of the second image is mapped to its corresponding position in the first image, and overlapping 1s are counted.Because the matrix size is limited to 30 × 30, the O(n⁴) brute-force simulation is practical and keeps the implementation straightforward.The most important takeaway is the coordinate-based way of thinking: when an object moves on a grid, represent that movement as an offset and systematically test the valid positions.

LeetCodeJavaMatrixArraysSimulation2D ArraysMatrix TraversalMedium
LeetCode 3761 Minimum Absolute Distance Between Mirror Pairs | Java HashMap Solution

LeetCode 3761 Minimum Absolute Distance Between Mirror Pairs | Java HashMap Solution

IntroductionSome problems look simple at first—but hide a clever trick inside.LeetCode 3761 – Minimum Absolute Distance Between Mirror Pairs is one such problem. It combines:Number manipulation (digit reversal)HashingEfficient searchingIf approached naively, this problem can easily lead to O(n²) time complexity—which is not feasible for large inputs.In this article, we will walk through:Problem intuitionNaive approach (and why it fails)Optimized HashMap solutionStep-by-step explanationClean Java code with comments🔗 Problem LinkLeetCode: Minimum Absolute Distance Between Mirror PairsTo gain a deeper understanding of the problem, it is highly recommended that you review this similar problem Closest Equal Element Queries here is the link of the article. Both cases follow a nearly identical pattern, and studying the initial example will provide valuable context for the current task.Problem StatementYou are given an integer array nums.A mirror pair (i, j) satisfies:0 ≤ i < j < nums.lengthreverse(nums[i]) == nums[j]👉 Your task is to find:The minimum absolute distance between such pairsIf no mirror pair exists, return -1.ExamplesExample 1Input:nums = [12, 21, 45, 33, 54]Output:1Explanation:(0,1) → reverse(12) = 21 → distance = 1(2,4) → reverse(45) = 54 → distance = 2✔ Minimum = 1Example 2Input:nums = [120, 21]Output:1Example 3Input:nums = [21, 120]Output:-1Key InsightThe core idea is:Instead of checking every pair,store reversed values and match on the fly.❌ Naive Approach (Brute Force)IdeaCheck all pairs (i, j)Reverse nums[i]Compare with nums[j]ComplexityTime: O(n²) ❌Space: O(1)ProblemWith n ≤ 100000, this approach will definitely cause TLE.✅ Optimized Approach (HashMap)IntuitionWhile iterating through the array:Reverse the current numberCheck if this number was already seen as a reversed valueIf yes → we found a mirror pairKey TrickInstead of storing original numbers:👉 Store reversed values as keysThis allows instant lookup.Java Code (With Detailed Comments)import java.util.*;class Solution {// Function to reverse digits of a numberpublic int reverse(int m) {int rev = 0;while (m != 0) {int dig = m % 10; // extract last digitm = m / 10; // remove last digitrev = rev * 10 + dig; // build reversed number}return rev;}public int minMirrorPairDistance(int[] nums) {// Map to store reversed values and their indicesHashMap<Integer, Integer> mp = new HashMap<>();int min = Integer.MAX_VALUE;for (int i = 0; i < nums.length; i++) {// Check if current number exists in map// Meaning: some previous number had reverse equal to thisif (mp.containsKey(nums[i])) {// Calculate distanceint prevIndex = mp.get(nums[i]);min = Math.min(min, Math.abs(i - prevIndex));}// Reverse current numberint revVal = reverse(nums[i]);// Store reversed value with indexmp.put(revVal, i);}// If no pair found, return -1return min == Integer.MAX_VALUE ? -1 : min;}}Step-by-Step Dry RunInput:nums = [12, 21, 45, 33, 54]Execution:IndexValueReverseMap CheckAction01221not foundstore (21 → 0)12112founddistance = 124554not foundstore (54 → 2)33333not foundstore (33 → 3)45445founddistance = 2👉 Minimum = 1Complexity AnalysisTime ComplexityReversing number → O(digits) ≈ O(log n)Loop → O(n)👉 Overall: O(n)Space ComplexityHashMap stores at most n elements👉 O(n)Why This Approach WorksAvoids unnecessary pair comparisonsUses hashing for constant-time lookupProcesses array in a single passKey TakeawaysAlways think of hashing when matching conditions existReversing numbers can convert the problem into a lookup problemAvoid brute force when constraints are largeThis is a classic “store & check” patternCommon Interview PatternThis problem is similar to:Two Sum (hashing)Reverse pairsMatching transformationsConclusionThe Minimum Absolute Distance Between Mirror Pairs problem is a great example of how a simple optimization (using a HashMap) can reduce complexity from O(n²) → O(n).Understanding this pattern will help you solve many similar problems involving:TransformationsMatching conditionsEfficient lookupsFrequently Asked Questions (FAQs)1. Why store reversed value instead of original?Because we want to quickly check if a number matches the reverse of a previous number.2. What if multiple same reversed values exist?The map stores the latest index, ensuring minimum distance is considered.3. Can this be solved without HashMap?Yes, but it will result in inefficient O(n²) time.

LeetCodeArrayJavaMediumHashMap
Merge Sort Algorithm Explained | Java Implementation, Intuition & Complexity

Merge Sort Algorithm Explained | Java Implementation, Intuition & Complexity

IntroductionSorting is one of the most fundamental operations in computer science, and Merge Sort is among the most efficient and widely used sorting algorithms.It follows the Divide and Conquer approach, making it highly scalable and predictable even for large datasets.In this article, we will cover:Intuition behind Merge SortStep-by-step breakdownMultiple approachesJava implementation with commentsTime & space complexity analysis🔗 Problem LinkGeeksforGeeks: Merge SortProblem StatementGiven an array arr[] with starting index l and ending index r, sort the array using the Merge Sort algorithm.ExamplesExample 1Input:arr = [4, 1, 3, 9, 7]Output:[1, 3, 4, 7, 9]Example 2Input:arr = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]Output:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]Key InsightMerge Sort works by:Divide → Conquer → CombineDivide the array into two halvesRecursively sort each halfMerge both sorted halvesIntuition (Visual Understanding)For:[4, 1, 3, 9, 7]Step 1: Divide[4, 1, 3] [9, 7][4, 1] [3] [9] [7][4] [1]Step 2: Merge[4] [1] → [1, 4][1, 4] [3] → [1, 3, 4][9] [7] → [7, 9]Step 3: Final Merge[1, 3, 4] + [7, 9] → [1, 3, 4, 7, 9]Approach 1: Recursive Merge Sort (Top-Down)IdeaKeep dividing until single elements remainMerge sorted subarraysJava Codeclass Solution { // Function to merge two sorted halves void merge(int[] arr, int l, int mid, int h) { // Temporary array to store merged result int[] temp = new int[h - l + 1]; int i = l; // pointer for left half int j = mid + 1; // pointer for right half int k = 0; // pointer for temp array // Compare elements from both halves while (i <= mid && j <= h) { if (arr[i] <= arr[j]) { temp[k] = arr[i]; i++; } else { temp[k] = arr[j]; j++; } k++; } // Copy remaining elements from left half while (i <= mid) { temp[k] = arr[i]; i++; k++; } // Copy remaining elements from right half while (j <= h) { temp[k] = arr[j]; j++; k++; } // Copy sorted elements back to original array for (int m = 0; m < temp.length; m++) { arr[l + m] = temp[m]; } } // Recursive merge sort function void mergeSort(int arr[], int l, int h) { // Base case: single element if (l >= h) return; int mid = l + (h - l) / 2; // Sort left half mergeSort(arr, l, mid); // Sort right half mergeSort(arr, mid + 1, h); // Merge both halves merge(arr, l, mid, h); }}Approach 2: Iterative Merge Sort (Bottom-Up)IdeaStart with subarrays of size 1Merge pairsIncrease size graduallyCodeclass Solution { void merge(int[] arr, int l, int mid, int h) { int[] temp = new int[h - l + 1]; int i = l, j = mid + 1, k = 0; while (i <= mid && j <= h) { if (arr[i] <= arr[j]) temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } while (i <= mid) temp[k++] = arr[i++]; while (j <= h) temp[k++] = arr[j++]; for (int m = 0; m < temp.length; m++) { arr[l + m] = temp[m]; } } void mergeSort(int[] arr, int n) { for (int size = 1; size < n; size *= 2) { for (int l = 0; l < n - size; l += 2 * size) { int mid = l + size - 1; int h = Math.min(l + 2 * size - 1, n - 1); merge(arr, l, mid, h); } } }}Approach 3: Using Built-in Sorting (For Comparison)Arrays.sort(arr);👉 Internally uses optimized algorithms (TimSort in Java)Complexity AnalysisTime ComplexityCaseComplexityBestO(n log n)AverageO(n log n)WorstO(n log n)Space ComplexityO(n) (extra array for merging)Why Merge Sort is PowerfulStable sorting algorithmWorks efficiently on large datasetsPredictable performanceUsed in external sorting (large files)❌ Why Not Use Bubble/Selection Sort?AlgorithmTime ComplexityBubble SortO(n²)Selection SortO(n²)Merge SortO(n log n) ✅Key TakeawaysMerge Sort uses divide and conquerRecursion splits problem into smaller partsMerging is the key stepAlways O(n log n), regardless of inputWhen to Use Merge SortLarge datasetsLinked lists (very efficient)Stable sorting requiredExternal sortingConclusionMerge Sort is one of the most reliable and efficient sorting algorithms. Understanding its recursive structure and merging process is essential for mastering advanced algorithms.Once you grasp the divide-and-conquer pattern, it becomes easier to solve many complex problems.Frequently Asked Questions (FAQs)1. Is Merge Sort stable?Yes, it maintains the relative order of equal elements.2. Why is extra space required?Because we use a temporary array during merging.3. Can it be done in-place?Not efficiently; standard merge sort requires extra space.

GeekfOfGeeksMediumSortingMerge SortJava
LeetCode 3783 Mirror Distance of an Integer | Java Solution Explained

LeetCode 3783 Mirror Distance of an Integer | Java Solution Explained

IntroductionSome problems test complex algorithms, while others focus on fundamental concepts done right.LeetCode 3783 – Mirror Distance of an Integer falls into the second category.This problem is simple yet important because it builds understanding of:Digit manipulationReversing numbersMathematical operationsIn this article, we’ll break down the problem in a clean and intuitive way, along with an optimized Java solution.🔗 Problem LinkLeetCode: Mirror Distance of an IntegerProblem StatementYou are given an integer n.The mirror distance is defined as:| n - reverse(n) |Where:reverse(n) = number formed by reversing digits of n|x| = absolute value👉 Return the mirror distance.ExamplesExample 1Input:n = 25Output:27Explanation:reverse(25) = 52|25 - 52| = 27Example 2Input:n = 10Output:9Explanation:reverse(10) = 1|10 - 1| = 9Example 3Input:n = 7Output:0Key InsightThe problem consists of two simple steps:1. Reverse the number2. Take absolute differenceIntuitionLet’s take an example:n = 120Step 1: Reverse digits120 → 021 → 21👉 Leading zeros are ignored automatically.Step 2: Compute difference|120 - 21| = 99ApproachStep-by-StepExtract digits using % 10Build reversed numberUse Math.abs() for final resultJava Codeclass Solution { // Function to reverse a number public int reverse(int k) { int rev = 0; while (k != 0) { int dig = k % 10; // get last digit k = k / 10; // remove last digit rev = rev * 10 + dig; // build reversed number } return rev; } public int mirrorDistance(int n) { // Calculate mirror distance return Math.abs(n - reverse(n)); }}Dry RunInput:n = 25Execution:Reverse → 52Difference → |25 - 52| = 27Complexity AnalysisTime ComplexityReversing number → O(d)(d = number of digits)👉 Overall: O(log n)Space Complexity👉 O(1) (no extra space used)Why This WorksDigit extraction ensures correct reversalLeading zeros automatically removedAbsolute difference ensures positive resultEdge Cases to ConsiderSingle digit → result = 0Numbers ending with zero (e.g., 10 → 1)Large numbers (up to 10⁹)Key TakeawaysSimple math problems can test core logicDigit manipulation is a must-know skillAlways handle leading zeros carefullyUse built-in functions like Math.abs() effectivelyReal-World RelevanceConcepts used here are helpful in:Number transformationsPalindrome problemsReverse integer problemsMathematical algorithmsConclusionThe Mirror Distance of an Integer problem is a great example of combining basic operations to form a meaningful solution.While simple, it reinforces important programming fundamentals that are widely used in more complex problems.Frequently Asked Questions (FAQs)1. What happens to leading zeros in reverse?They are automatically removed when stored as an integer.2. Can this be solved using strings?Yes, but integer-based approach is more efficient.3. What is the best approach?Using arithmetic operations (% and /) is optimal.

EasyArrayReverse NumberLeetCodeJava
Search in Rotated Sorted Array II – Binary Search with Duplicates Explained (LeetCode 81)

Search in Rotated Sorted Array II – Binary Search with Duplicates Explained (LeetCode 81)

Try the QuestionBefore reading the explanation, try solving the problem yourself:👉 https://leetcode.com/problems/search-in-rotated-sorted-array-ii/Practicing the problem first helps develop stronger problem-solving intuition, especially for binary search variations.Problem StatementYou are given an integer array nums that is sorted in non-decreasing order.Example of a sorted array:[0,1,2,4,4,4,5,6,6,7]Before being passed to your function, the array may be rotated at some pivot index k.After rotation, the structure becomes:[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]Example:Original array[0,1,2,4,4,4,5,6,6,7]Rotated at index 5[4,5,6,6,7,0,1,2,4,4]You are also given an integer target.Your task is to determine:Return true if the target exists in the arrayReturn false if the target does not existThe goal is to minimize the number of operations, which suggests using Binary Search.Example WalkthroughExample 1Inputnums = [2,5,6,0,0,1,2]target = 0OutputtrueExplanation:0 exists in the arrayExample 2Inputnums = [2,5,6,0,0,1,2]target = 3OutputfalseExplanation:3 does not exist in the arrayUnderstanding the Core ChallengeThis problem is very similar to the classic problem:Search in Rotated Sorted Array (LeetCode 33).However, there is an important difference.Difference Between the Two ProblemsProblemArray ValuesRotated Sorted ArrayAll elements are distinctRotated Sorted Array IIArray may contain duplicatesDuplicates introduce ambiguity during binary search.Why Duplicates Make the Problem HarderIn the previous problem, we relied on this rule:If nums[left] <= nums[mid]→ left half is sortedBut duplicates can break this assumption.Example:nums = [1,0,1,1,1]If:left = 0mid = 2right = 4Then:nums[left] = 1nums[mid] = 1nums[right] = 1Here we cannot determine which half is sorted.This is the main complication introduced by duplicates.Key Idea to Handle DuplicatesWhen the values at left, mid, and right are the same, the algorithm cannot decide which half is sorted.To resolve this situation, we shrink the search space:left++right--This gradually removes duplicate values and allows binary search to continue.Modified Binary Search StrategyThe algorithm works as follows:Step 1Calculate the middle index.Step 2If the middle element equals the target:return trueStep 3If duplicates block decision-making:nums[left] == nums[mid] == nums[right]Then move both pointers inward:left++right--Step 4Otherwise, determine which half is sorted and apply normal binary search logic.Java Implementationclass Solution { public boolean search(int[] nums, int target) { int l = 0; int r = nums.length - 1; while (l <= r) { int mid = l + (r - l) / 2; if (nums[mid] == target) return true; // Handle duplicates if (nums[l] == nums[mid] && nums[mid] == nums[r]) { l++; r--; } // Left half sorted else if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } // Right half sorted else { if (nums[mid] < target && target <= nums[r]) { l = mid + 1; } else { r = mid - 1; } } } return false; }}Step-by-Step ExampleArray:[2,5,6,0,0,1,2]target = 0Iteration 1mid = 6value = 0Target found immediately.return trueTime ComplexityBest CaseO(log n)When duplicates do not interfere with binary search decisions.Worst CaseO(n)When many duplicate values force the algorithm to shrink the search space one element at a time.Example worst case:[1,1,1,1,1,1,1,1,1]Binary search cannot divide the array effectively.Space ComplexityO(1)The algorithm only uses a few variables and does not require extra memory.Follow-Up: How Do Duplicates Affect Runtime?Without duplicates, binary search always reduces the search space by half.Time Complexity → O(log n)With duplicates, we sometimes cannot determine which half is sorted.In such cases, we shrink the search space linearly:left++right--This may degrade performance to:Worst Case → O(n)However, in most practical cases the algorithm still performs close to logarithmic time.Key Takeaways✔ The array is sorted but rotated✔ Duplicates introduce ambiguity in binary search✔ Special handling is required when nums[left] == nums[mid] == nums[right]✔ The algorithm combines binary search with duplicate handling✔ Worst-case complexity may degrade to O(n)Final ThoughtsThis problem is a natural extension of the rotated sorted array search problem. It tests your ability to adapt binary search to more complex conditions.Understanding this pattern is valuable because similar techniques appear in many interview problems involving:Rotated arraysBinary search edge casesHandling duplicates in sorted data structuresMastering this approach strengthens both algorithmic thinking and interview preparation.

LeetCodeBinary SearchRotated Sorted ArrayJavaMedium
Permutation with Spaces Explained Using Recursion & Decision Tree | Java Solution GFG

Permutation with Spaces Explained Using Recursion & Decision Tree | Java Solution GFG

IntroductionThe Permutation with Spaces problem is a classic recursion question that helps build a strong understanding of decision-making and backtracking patterns.Instead of generating permutations by rearranging characters, this problem focuses on inserting spaces between characters in all possible ways.What makes this problem powerful is its decision tree structure, which you’ve already visualized perfectly. In this article, we will directly connect that intuition with code.Link of Problem: GeeksforGeeks – Permutation with SpacesProblem StatementGiven a string s, generate all possible strings by placing:Either a spaceOr no spacebetween every pair of characters.Return all results in sorted order.ExampleInput:s = "ABC"Output:A B CA BCAB CABCUnderstanding Your Decision Tree (Very Important)Two Choices at Each Step:❌ Do NOT add space before the character✔️ Add space before the characterMapping TreeFrom diagram:At B:"AB" → no space"A B" → spaceAt C:From "AB":"ABC""AB C"From "A B":"A BC""A B C"Final Output (Leaf Nodes)As shown in your diagram:ABC, AB C, A BC, A B C📌 This is exactly what recursion generates.Key InsightAt every index (except first), we have:2 choices → space OR no spaceSo total combinations:2^(n-1)Approach: Recursion + Decision MakingIdeaFix the first characterFor every next character:Add space + characterAdd character directlyContinue recursivelyJava Code with Detailed Commentsimport java.util.*;class Solution { // List to store all results ArrayList<String> lis = new ArrayList<>(); void solve(String s, int ind, String curr) { // Base case: // If index reaches end of string, // we have formed one valid permutation if (ind == s.length()) { lis.add(curr); // store the result return; } // Choice 1: Add SPACE before current character // Example: "A" → "A B" solve(s, ind + 1, curr + " " + s.charAt(ind)); // Choice 2: Do NOT add space // Example: "A" → "AB" solve(s, ind + 1, curr + s.charAt(ind)); } ArrayList<String> permutation(String s) { // Start with first character (no space before it) String curr = "" + s.charAt(0); // Start recursion from index 1 solve(s, 1, curr); // Sort results as required in problem Collections.sort(lis); return lis; }}Step-by-Step Execution (Using Your Tree)For "ABC":Start → "A"At "B":"AB""A B"At "C":"ABC", "AB C""A BC", "A B C"Exactly matches your decision tree leaf nodes ✅Complexity AnalysisTime Complexity: O(2ⁿ)Space Complexity: O(2ⁿ)Why This Approach WorksRecursion explores every possible choiceEach level = one characterEach branch = decision (space / no space)Leaf nodes = final answersKey TakeawaysThis is a binary decision recursion problemAlways identify:ChoicesBase conditionYour decision tree = direct blueprint of recursionSame pattern applies to:SubsetsBinary choices problemsConclusionThe Permutation with Spaces problem becomes extremely simple once the decision tree is understood—and your diagram already captures that perfectly.The recursion directly follows the same structure:Every branch = one decisionEvery leaf = one answerMaster this pattern, and you’ll find many recursion problems much easier to solve.

MediumGeeksforGeeksRecursionJava
LeetCode 234: Palindrome Linked List (Java) | Intuition, Dry Run & O(1) Space Solution

LeetCode 234: Palindrome Linked List (Java) | Intuition, Dry Run & O(1) Space Solution

🧩 Problem OverviewGiven the head of a singly linked list, determine whether it is a palindrome.A palindrome means the sequence reads the same forward and backward.Example:Input: [1,2,2,1] → Output: trueInput: [1,2] → Output: false🎯 Why This Problem MattersThis problem tests:Linked list traversalTwo-pointer technique (slow & fast)In-place reversalIt’s a must-know pattern for interviews.🧠 IntuitionA simple approach is to copy elements into an array and check for palindrome.But that uses extra space O(n).Optimal Idea:We can solve this in O(1) space by:Finding the middle of the linked listReversing the second halfComparing both halves🎥 Dry Run (Step-by-Step Explanation)👉 Watch the complete dry run and pointer movement below:This video explains how slow and fast pointers work and how the comparison is performed.⚙️ ApproachStep 1: Handle Edge CasesIf list has one node → return trueStep 2: Find MiddleUse slow and fast pointersFast moves 2 steps, slow moves 1Step 3: Reverse Second HalfReverse the list starting from the middleStep 4: Compare Both HalvesCompare values from:start of liststart of reversed half💻 Java Solutionclass Solution {public ListNode reverse(ListNode head) {ListNode curr = head;ListNode prev = null;while (curr != null) {ListNode next = curr.next;curr.next = prev;prev = curr;curr = next;}return prev;}public boolean isPalindrome(ListNode head) {if (head == null) return false;if (head.next == null) return true;ListNode slow = head;ListNode fast = head;// Find middlewhile (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;}// Reverse second halfListNode secondHalf = reverse(slow);// Compare both halvesListNode firstHalf = head;while (secondHalf != null) {if (secondHalf.val != firstHalf.val) {return false;}secondHalf = secondHalf.next;firstHalf = firstHalf.next;}return true;}}🔍 Dry Run (Quick Summary)Example:1 → 2 → 2 → 1Middle foundSecond half reversedCompare values one by oneResult → Palindrome ✅⏱️ Time and Space ComplexityTime Complexity: O(n)Space Complexity: O(1)⚠️ Edge CasesSingle nodeTwo nodesOdd length listEven length list💡 Key TakeawaysFast & slow pointer technique is essentialReversing linked list is a reusable patternHelps optimize space complexity🚀 Final ThoughtsThis is a classic interview problem combining multiple linked list techniques.Make sure you understand:Pointer movementReversal logicComparison step👉 For full clarity, don’t skip the video explanation above.

LinkedListFast and Slow PointerPalindromeEasy
LeetCode 769: Max Chunks To Make Sorted – Java Solution, Approach & Explanation

LeetCode 769: Max Chunks To Make Sorted – Java Solution, Approach & Explanation

IntroductionLeetCode 769, Max Chunks To Make Sorted, is an interesting array partitioning problem.The array is a permutation of numbers from 0 to n - 1. The goal is to divide the array into multiple contiguous chunks, sort each chunk independently, and then join all the sorted chunks together.The challenge is to find the maximum number of chunks for which the final concatenated array becomes completely sorted.For example:arr = [1,0,2,3,4]The array can be divided as:[1,0] [2] [3] [4]After sorting every chunk:[0,1] [2] [3] [4]The final array is:[0,1,2,3,4]Therefore, the answer is 4.The given solution approaches the problem using recursion and backtracking to generate possible partitions and then checks which partitions produce a sorted array.Question LinkLeetCode 769 – Max Chunks To Make SortedUnderstanding the ProblemThe important word in the problem is chunks.A chunk must contain consecutive elements from the original array.For example:[4,3,2,1,0]Possible partition:[4,3] [2,1,0]After sorting:[3,4] [0,1,2]Combining them gives:[3,4,0,1,2]which is not sorted.So this partition is invalid.The task is not simply to split the array into as many pieces as possible. Every chosen partition must satisfy the condition that sorting each piece independently produces the globally sorted array.Approach: Recursion + BacktrackingThe given solution tries every possible way of partitioning the array.At every index, it considers every possible ending position for the current chunk.For example:arr = [1,0,2]Starting from index 0, possible first chunks are:[1][1,0][1,0,2]For each choice, recursion continues from the next index.This generates different partition configurations such as:[1] [0] [2][1,0] [2][1] [0,2][1,0,2]Each complete partition is then checked to determine whether it produces a sorted array.Generating the ChunksThe recursive function is:public void sol(int[] arr, int ind, List<List<Integer>> chunk)Here:ind represents the current starting index.chunk stores the chunks selected so far.The loop:for(int i = ind; i < arr.length; i++){ List<Integer> lis = subar(arr, ind, i); chunk.add(lis); sol(arr, i + 1, chunk); chunk.remove(chunk.size() - 1);}tries every possible ending point for the current chunk.The important part is:sol(arr, i + 1, chunk);Once a chunk from ind to i has been selected, the next chunk must begin at i + 1.Creating a ChunkThe subar() method creates a list containing the elements between two indices.public List<Integer> subar(int[] arr, int st, int en){ List<Integer> lis = new ArrayList<>(); for(int i = st; i <= en; i++){ lis.add(arr[i]); } return lis;}For example:arr = [1,0,2,3]st = 0en = 1produces:[1,0]Checking a PartitionOnce a complete partition has been generated, the vali() method checks whether it is valid.First, every chunk is sorted:Collections.sort(a);Then all sorted chunks are concatenated into one list:res.add(a.get(i));Finally, the resulting array is checked to see whether it is globally sorted.for(int i = 0; i < res.size() - 1; i++){ if(res.get(i) > res.get(i + 1)){ return false; }}If no decreasing pair exists, the partition is valid.Java Solutionclass Solution { int an = 0; // Generates all possible partitions public void sol(int[] arr, int ind, List<List<Integer>> chunk) { // A complete partition has been created if(ind == arr.length){ // Check whether this partition produces // the completely sorted array if(vali(chunk)){ an = Math.max(an, chunk.size()); } return; } // Try every possible ending point // for the current chunk for(int i = ind; i < arr.length; i++){ // Create the current chunk List<Integer> lis = subar(arr, ind, i); // Choose the chunk chunk.add(lis); // Recursively create the remaining chunks sol(arr, i + 1, chunk); // Backtrack chunk.remove(chunk.size() - 1); } } // Creates a subarray from st to en public List<Integer> subar(int[] arr, int st, int en){ List<Integer> lis = new ArrayList<>(); for(int i = st; i <= en; i++){ lis.add(arr[i]); } return lis; } // Checks whether the selected partition is valid public boolean vali(List<List<Integer>> liss){ List<Integer> res = new ArrayList<>(); // Sort every chunk independently for(List<Integer> a : liss){ Collections.sort(a); // Add the sorted chunk to the result for(int i = 0; i < a.size(); i++){ res.add(a.get(i)); } } // Check whether the final array is sorted for(int i = 0; i < res.size() - 1; i++){ if(res.get(i) > res.get(i + 1)){ return false; } } return true; } public int maxChunksToSorted(int[] arr){ List<List<Integer>> liss = new ArrayList<>(); sol(arr, 0, liss); return an; }}Dry RunConsider:arr = [1,0,2,3,4]One of the partitions generated by recursion is:[1,0] [2] [3] [4]The chunks are individually sorted:[0,1] [2] [3] [4]After concatenation:[0,1,2,3,4]The resulting array is sorted, so the partition is valid.It contains:4 chunksThe recursion also examines partitions with fewer chunks, such as:[1,0,2,3,4][1,0] [2,3,4][1] [0,2,3,4][1,0] [2] [3,4]Among all valid partitions, the solution keeps the maximum number of chunks using:an = Math.max(an, chunk.size());Therefore:Answer = 4Why Does the Maximum Number of Chunks Matter?A partition with fewer chunks can still produce the sorted array.For example:[1,0,2,3,4]can be split as:[1,0] [2,3,4]After sorting:[0,1] [2,3,4]which produces:[0,1,2,3,4]So this is valid.But it is not the maximum because:[1,0] [2] [3] [4]also works and gives more chunks.Therefore, every valid partition cannot simply be accepted—the number of chunks must also be maximized.Complexity AnalysisThe number of ways to split an array of length n into contiguous chunks is:2^(n-1)because every gap between two elements can either contain a partition or not.For example, with:[a,b,c]there are two gaps:a | b | cEach gap has two choices, giving:2² = 4possible partitions.For every complete partition, the solution also sorts the chunks and constructs the resulting array.Therefore, the overall complexity is exponential.Time Complexity: Approximately O(2^n × n log n)Space Complexity: Approximately O(n × 2^n) in the worst case because many partition configurations and temporary lists are generated.Since the given constraint is only:n <= 10this brute-force approach is feasible.A Better ObservationAlthough recursion works for the small constraint, this problem has a much simpler O(n) greedy solution.The key observation comes from the fact that the array is a permutation of:0, 1, 2, ..., n-1Suppose the current chunk ends at index i.If the maximum value seen so far is exactly i, then the current elements contain exactly the values that should occupy positions 0 through i.Therefore, the chunk can safely end at this position.For example:arr = [1,0,2,3,4]Track the maximum:index value max 0 1 1 1 0 1 2 2 2 3 3 3 4 4 4Whenever:max == indexa new chunk can be created.This happens at:index 1index 2index 3index 4So the answer is:4The optimized implementation is:class Solution { public int maxChunksToSorted(int[] arr) { int max = 0; int chunks = 0; for(int i = 0; i < arr.length; i++){ max = Math.max(max, arr[i]); if(max == i){ chunks++; } } return chunks; }}This reduces the complexity to:Time Complexity: O(n)Space Complexity: O(1)Interview TipWhen a problem says the array is a permutation from 0 to n-1, that property is usually extremely important.Instead of immediately trying to generate all possibilities, look for a relationship between:the current indexthe values seen so farthe final sorted positionFor this problem, the condition:maximum value seen so far == current indexmeans that the current portion contains exactly the values needed for that prefix of the sorted array.That single observation turns an exponential backtracking solution into a linear greedy solution.ConclusionLeetCode 769 is a good example of how the same problem can be approached at different levels.The recursive solution explores every possible contiguous partition, sorts each chunk, and checks whether the final result is sorted. It is straightforward and works well under the small constraint n <= 10.However, the permutation property provides a much stronger observation. Whenever the maximum value seen so far equals the current index, a chunk can safely end there.That leads to a simple:O(n) timeO(1) spacegreedy solution.The main lesson is to first understand the brute-force structure, then look for properties in the input that can eliminate the need to explore every possibility.

LeetCodeJavaRecursionBacktrackingGreedyArraysSortingPermutationPartitioningMedium
Minimum Changes to Make Alternating Binary String – LeetCode 1758 Explained

Minimum Changes to Make Alternating Binary String – LeetCode 1758 Explained

Try This QuestionBefore reading the solution, try solving the problem yourself on LeetCode:👉 https://leetcode.com/problems/minimum-changes-to-make-alternating-binary-string/Problem StatementYou are given a binary string s consisting only of characters '0' and '1'.In one operation, you can change any '0' to '1' or '1' to '0'.A string is called alternating if no two adjacent characters are the same.Example of Alternating Strings01010101011010Example of Non-Alternating Strings0001001111101Your task is to return the minimum number of operations required to make the string alternating.Example WalkthroughExample 1Inputs = "0100"Possible fix:0101Only the last character needs to change, so the answer is:Output1Example 2Inputs = "10"The string is already alternating.Output0Example 3Inputs = "1111"Possible alternating strings:01011010Minimum operations needed = 2Output2Key ObservationAn alternating binary string can only have two possible patterns:Pattern 10101010101...Pattern 21010101010...So instead of trying many combinations, we only need to check:1️⃣ How many changes are required to convert s → "010101..."2️⃣ How many changes are required to convert s → "101010..."Then we return the minimum of the two.ApproachStep 1Generate two possible alternating strings:s1 = "010101..."s2 = "101010..."Both will be of the same length as the input string.Step 2Compare the original string with both patterns.Count mismatches.For example:s = 0100s1 = 0101Mismatch count = 1Step 3Repeat for the second pattern.Finally return:min(mismatch1, mismatch2)Intuition Behind the SolutionInstead of flipping characters randomly, we compare the string with the two valid alternating possibilities.Why only two?Because:Alternating strings must start with either 0 or 1After that, the pattern is fixed.So we simply compute which pattern requires fewer changes.This makes the solution efficient and simple.Java Implementationclass Solution { public int minOperations(String s) { int co1 = 0; int co2 = 0; String s1 = ""; String s2 = ""; for(int i = 0; i < s.length(); i++){ if(i % 2 == 0){ s1 += "0"; } else { s1 += "1"; } } for(int i = 0; i < s.length(); i++){ if(i % 2 == 0){ s2 += "1"; } else { s2 += "0"; } } for(int i = 0; i < s.length(); i++){ if(s.charAt(i) != s1.charAt(i)){ co1++; } } for(int i = 0; i < s.length(); i++){ if(s.charAt(i) != s2.charAt(i)){ co2++; } } return Math.min(co1, co2); }}Complexity AnalysisTime ComplexityO(n)We iterate through the string a few times.Space ComplexityO(n)Because we create two extra strings of size n.Optimized Approach (Better Interview Answer)We can avoid creating extra strings and calculate mismatches directly.Optimized Java Codeclass Solution { public int minOperations(String s) { int pattern1 = 0; int pattern2 = 0; for(int i = 0; i < s.length(); i++){ char expected1 = (i % 2 == 0) ? '0' : '1'; char expected2 = (i % 2 == 0) ? '1' : '0'; if(s.charAt(i) != expected1) pattern1++; if(s.charAt(i) != expected2) pattern2++; } return Math.min(pattern1, pattern2); }}Space Complexity NowO(1)No extra strings required.Why This Problem Is ImportantThis problem teaches important interview concepts:✔ Pattern observation✔ Greedy thinking✔ String manipulation✔ Optimization techniquesMany companies ask similar pattern-based string problems.Final ThoughtsThe trick in this problem is realizing that only two alternating patterns exist. Once you identify that, the problem becomes straightforward.Instead of trying multiple modifications, you simply compare and count mismatches.This leads to a clean and efficient O(n) solution.If you are preparing for coding interviews, practicing problems like this will improve your pattern recognition skills, which is a key skill for solving medium and hard problems later.Happy Coding 🚀

LeetCodeBinary StringGreedy AlgorithmJavaEasy
LeetCode 124: Binary Tree Maximum Path Sum – Java DFS Solution Explained

LeetCode 124: Binary Tree Maximum Path Sum – Java DFS Solution Explained

IntroductionLeetCode 124 – Binary Tree Maximum Path Sum is one of the most important and frequently asked hard-level binary tree interview problems.This problem teaches:Depth First Search (DFS)Bottom-up recursionTree Dynamic ProgrammingRecursive optimizationGlobal answer trackingIt is considered a classic interview problem because it combines:Tree traversalRecursive decision makingPath optimizationNegative value handlingMastering this problem helps in understanding advanced binary tree patterns used in:Diameter problemsTree DPMaximum path problemsGraph recursion problemsProblem Link🔗 https://leetcode.com/problems/binary-tree-maximum-path-sum/Problem StatementGiven the root of a binary tree:Return:Maximum path sum of any non-empty pathA path:Can start from any nodeCan end at any nodeMust follow parent-child connectionsCannot visit a node more than onceImportant:Path does NOT need to pass through rootExample 1Inputroot = [1,2,3]Tree: 1 / \ 2 3Output6Explanation:Best path:2 → 1 → 3Path sum:2 + 1 + 3 = 6Example 2Inputroot = [-10,9,20,null,null,15,7]Tree: -10 / \ 9 20 / \ 15 7Output42Explanation:Best path:15 → 20 → 7Sum:15 + 20 + 7 = 42Key ObservationAt every node:We have two important possibilities.Possibility 1Path continues upward to parent.In this case:We can choose only:ONE sidebecause path cannot split upward.Return value becomes:node + max(left, right)Possibility 2Current node becomes:Highest point of pathThen we can use:left + node + rightThis candidate updates global maximum answer.Core Recursive IdeaAt every node:Calculate left maximum contributionCalculate right maximum contributionIgnore negative pathsUpdate global answerReturn best single-side path upwardWhy Ignore Negative Paths?Negative paths reduce total sum.So:Math.max(path, 0)ensures:Negative contribution is discardedOnly profitable paths are consideredThis is the most important optimization.Your Java Solutionclass Solution { int max = Integer.MIN_VALUE; public int solve(TreeNode roo) { if(roo == null) return 0; int left = Math.max(solve(roo.left), 0); int right = Math.max(solve(roo.right), 0); max = Math.max(max, roo.val + left + right); return roo.val + Math.max(left, right); } public int maxPathSum(TreeNode root) { solve(root); return max; }}Why This WorksFor every node:We calculate:Best path passing through current nodewhich is:left + node + rightThis path may become global maximum.But while returning upward:We can only choose:one directionbecause paths cannot split.Dry RunInput -10 / \ 9 20 / \ 15 7Step 1Leaf nodes:9 → return 915 → return 157 → return 7Step 2At node:20Left:15Right:7Current best path:15 + 20 + 7 = 42Update:max = 42Return upward:20 + max(15,7)= 35Step 3At node:-10Left:9Right:35Candidate:9 + (-10) + 35 = 34Global maximum remains:42Final Answer42Recursive Visualizationsolve(-10) ├── solve(9) │ └── solve(20) ├── solve(15) └── solve(7)Global maximum gets updated during return phase.Brute Force ApproachA brute force approach would:Generate all possible pathsCalculate sumsTrack maximumBut number of paths becomes extremely large.This is inefficient.Brute Force ComplexityTime ComplexityCan become:O(N²)or worse depending on implementation.Optimized DFS ComplexityTime ComplexityO(N)because every node is visited once.Space ComplexityO(H)where:H = height of treeWorst case:O(N)for skewed tree.Important InsightTwo values exist at every node.1. Value Returned UpwardOnly one side allowed:node + max(left, right)2. Value Used for Global MaximumBoth sides allowed:left + node + rightThis distinction is the heart of the problem.Interview ExplanationIn interviews, explain:Every node acts as a potential highest point of a path. We compute the best path through that node while recursively returning the best single-side contribution upward.This demonstrates:Advanced DFS understandingTree DP conceptsRecursive optimizationHandling negative pathsCommon Mistakes1. Returning Both Sides UpwardIncorrect:left + node + rightA path cannot branch upward.2. Forgetting Negative Path HandlingAlways use:Math.max(value, 0)3. Assuming Path Must Pass RootThe path can exist entirely inside a subtree.4. Not Using Global VariableMaximum path may occur anywhere.FAQsQ1. Does path need to start from root?No.It can start and end anywhere.Q2. Why ignore negative sums?Negative paths reduce overall answer.Q3. Why can we return only one side?Because a path moving upward cannot split into two directions.Q4. Is this problem important for interviews?Extremely important.It is one of the most famous hard-level tree problems.Related ProblemsAfter mastering this problem, practice:Diameter of Binary TreeBalanced Binary TreeMaximum Depth of Binary TreeConclusionLeetCode 124 is one of the best problems for learning advanced binary tree recursion.It teaches:DFS optimizationTree dynamic programmingRecursive decision makingNegative path handlingGlobal answer trackingThe key insight is:Every node can become the highest point of a maximum path.Once this recursive pattern becomes clear, many advanced tree and graph problems become much easier to solve.

LeetCodeBinary Tree Maximum Path SumJavaBinary TreeDFSTreeRecursionDynamic Programming on TreesHard
LeetCode 1306: Jump Game III – Java DFS & Graph Traversal Solution Explained

LeetCode 1306: Jump Game III – Java DFS & Graph Traversal Solution Explained

IntroductionLeetCode 1306 – Jump Game III is an interesting graph traversal problem that combines:Depth First Search (DFS)Breadth First Search (BFS)RecursionVisited trackingCycle detectionAt first glance, this problem looks like an array problem.But internally, it behaves exactly like a graph traversal problem where:Each index acts like a nodeEach jump acts like an edgeThis problem is commonly asked in coding interviews because it tests:Recursive thinkingGraph traversal intuitionAvoiding infinite loopsState trackingProblem Link🔗 https://leetcode.com/problems/jump-game-iii/Problem StatementYou are given:An array arrA starting index startFrom index i, you can jump:i + arr[i]ori - arr[i]Your goal is to determine whether you can reach any index having value:0ExampleInputarr = [4,2,3,0,3,1,2]start = 5OutputtrueExplanationPossible path:5 → 4 → 1 → 3At index:3Value becomes:0So answer is:trueUnderstanding the ProblemThink of every index as a graph node.From each node:index iwe have two possible edges:i + arr[i]andi - arr[i]The goal is simply:Can we reach any node containing value 0?Brute Force IntuitionA naive recursive solution would:Try both forward and backward jumpsContinue recursivelyStop when we find zeroWhy Brute Force FailsWithout tracking visited indices, recursion may enter infinite loops.Example:1 → 3 → 1 → 3 → 1...This creates cycles.So we must track visited nodes.DFS IntuitionWe perform DFS traversal from the starting index.At every index:Check boundariesCheck if already visitedCheck if value is zeroExplore both possible jumpsKey DFS ObservationEach index should only be visited once.Why?Because revisiting creates cycles and unnecessary computation.So we use:HashSet<Integer> visitedorboolean[] visitedRecursive DFS ApproachSteps1. Boundary CheckIf index goes outside array:return false2. Visited CheckIf already visited:return false3. Found ZeroIf current index contains:0Return:true4. Explore Both DirectionsTry:start + arr[start]andstart - arr[start]Java DFS Solutionclass Solution { public boolean solve(HashSet<Integer> zeroIndexes, HashSet<Integer> visited, int start, int[] arr) { if(start >= arr.length || start < 0) return false; if(visited.contains(start)) return false; visited.add(start); if(zeroIndexes.contains(start)) return true; return solve(zeroIndexes, visited, start + arr[start], arr) || solve(zeroIndexes, visited, start - arr[start], arr); } public boolean canReach(int[] arr, int start) { HashSet<Integer> visited = new HashSet<>(); HashSet<Integer> zeroIndexes = new HashSet<>(); for(int i = 0; i < arr.length; i++) { if(arr[i] == 0) { zeroIndexes.add(i); } } return solve(zeroIndexes, visited, start, arr); }}Simpler Optimized DFS SolutionWe actually do not need a separate set for zero indexes.We can directly check:arr[start] == 0Cleaner Java DFS Solutionclass Solution { public boolean dfs(int[] arr, boolean[] visited, int start) { if(start < 0 || start >= arr.length) return false; if(visited[start]) return false; if(arr[start] == 0) return true; visited[start] = true; return dfs(arr, visited, start + arr[start]) || dfs(arr, visited, start - arr[start]); } public boolean canReach(int[] arr, int start) { return dfs(arr, new boolean[arr.length], start); }}Dry RunInputarr = [4,2,3,0,3,1,2]start = 5Step 1Current index:5Value:1Possible jumps:5 + 1 = 65 - 1 = 4Step 2Visit index:4Value:3Possible jumps:4 + 3 = 7 (invalid)4 - 3 = 1Step 3Visit index:1Value:2Possible jumps:1 + 2 = 31 - 2 = -1 (invalid)Step 4Visit index:3Value:0Return:trueBFS ApproachThis problem can also be solved using BFS.Instead of recursion:Use queueExplore neighbors level by levelJava BFS Solutionclass Solution { public boolean canReach(int[] arr, int start) { Queue<Integer> queue = new LinkedList<>(); boolean[] visited = new boolean[arr.length]; queue.offer(start); while(!queue.isEmpty()) { int index = queue.poll(); if(index < 0 || index >= arr.length) continue; if(visited[index]) continue; if(arr[index] == 0) return true; visited[index] = true; queue.offer(index + arr[index]); queue.offer(index - arr[index]); } return false; }}Time Complexity AnalysisDFS ComplexityTime ComplexityO(N)Each index is visited at most once.Space ComplexityO(N)Due to recursion stack and visited array.BFS ComplexityTime ComplexityO(N)Space ComplexityO(N)DFS vs BFSApproachAdvantagesDisadvantagesDFSSimple recursive logicRecursion stackBFSIterative solutionQueue managementInterview ExplanationIn interviews, explain:This problem behaves like graph traversal where each index acts as a node and jumps act as edges. We use DFS or BFS with visited tracking to avoid infinite cycles.This demonstrates strong graph intuition.Common Mistakes1. Forgetting Visited TrackingThis causes infinite recursion.2. Missing Boundary ChecksAlways check:start < 0 || start >= arr.length3. Revisiting NodesAvoid processing already visited indices.FAQsQ1. Is this an array problem or graph problem?Internally it is a graph traversal problem.Q2. Which is better: DFS or BFS?Both are valid.DFS is usually simpler for this problem.Q3. Why do we need visited tracking?To avoid infinite loops caused by cycles.Q4. Can this be solved greedily?No.Because multiple paths must be explored.ConclusionLeetCode 1306 is an excellent beginner-friendly graph traversal problem.It teaches:DFS traversalBFS traversalCycle detectionRecursive thinkingVisited state managementThe most important insight is:Treat every index as a graph node.Once you understand this idea, many graph and traversal interview problems become much easier.

LeetCodeMediumDFSBFSGraph TraversalJavaRecursion
LeetCode 235: Lowest Common Ancestor of a Binary Search Tree – Java BST Solution with Dry Run

LeetCode 235: Lowest Common Ancestor of a Binary Search Tree – Java BST Solution with Dry Run

IntroductionLeetCode 235 – Lowest Common Ancestor of a Binary Search Tree is one of the most important BST interview problems.This problem helps you understand:Binary Search Tree propertiesRecursive traversalTree navigationAncestor relationshipsOptimized searchingIt is frequently asked in coding interviews because it combines tree traversal with BST optimization.Problem Link🔗 https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/Problem StatementGiven:Root of a Binary Search TreeTwo nodes p and qReturn:The Lowest Common Ancestor (LCA) of p and qThe LCA is the lowest node in the tree that has both nodes as descendants.A node can also be a descendant of itself.Example 1Inputroot = [6,2,8,0,4,7,9,null,null,3,5]p = 2q = 8Output6Example 2Inputroot = [6,2,8,0,4,7,9,null,null,3,5]p = 2q = 4Output2Understanding the BST PropertyA Binary Search Tree follows:Left Subtree < RootRight Subtree > RootExample: 6 / \ 2 8 / \ / \ 0 4 7 9 / \ 3 5This property allows us to find the LCA efficiently.Key ObservationThere are only three possible cases:Case 1Both nodes are smaller than root.Move LeftCase 2Both nodes are greater than root.Move RightCase 3One node is on the left and the other is on the right.OROne node equals root.Then:Current root is the LCAIntuitionSuppose:p = 2q = 8root = 6Now:2 < 68 > 6This means:One node is in left subtreeOne node is in right subtreeSo:6 is the first common split pointHence:6 is the Lowest Common AncestorBrute Force ApproachIdeaFind path from root to pFind path from root to qCompare both pathsLast common node is the LCABrute Force ComplexityTime ComplexityO(N)Space ComplexityO(N)Extra path storage required.Optimized BST ApproachUsing BST properties:No need to store pathsNo need to traverse entire treeMove intelligentlyThis gives a much cleaner solution.Java Solutionclass Solution { public TreeNode solve(TreeNode root, TreeNode p, TreeNode q) { if(root == null) return root; if(root.val < p.val && root.val < q.val) { return solve(root.right, p, q); } else if(root.val > p.val && root.val > q.val) { return solve(root.left, p, q); } else { return root; } } public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root == null) return root; return solve(root, p, q); }}Cleaner Recursive Versionclass Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if(root.val > p.val && root.val > q.val) { return lowestCommonAncestor(root.left, p, q); } if(root.val < p.val && root.val < q.val) { return lowestCommonAncestor(root.right, p, q); } return root; }}Iterative ApproachWe can also solve this without recursion.Iterative Java Solutionclass Solution { public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { while(root != null) { if(root.val > p.val && root.val > q.val) { root = root.left; } else if(root.val < p.val && root.val < q.val) { root = root.right; } else { return root; } } return null; }}Dry RunInputroot = [6,2,8,0,4,7,9,null,null,3,5]p = 2q = 8Step 1Current root:6Check:2 < 68 > 6One node is on left.One node is on right.So:6 is the LCAFinal Output6Another Dry RunInputp = 2q = 4Step 1Current root:6Both nodes smaller than 6.Move left.Step 2Current root:2Now:p == rootSo:2 is the LCAWhy This WorksBST ordering helps us eliminate half the tree at every step.If:p and q both smallerthen LCA must exist in left subtree.If:p and q both greaterthen LCA must exist in right subtree.Otherwise:Current node is the split pointwhich becomes the Lowest Common Ancestor.Time Complexity AnalysisOptimized BST SolutionAverage Time ComplexityO(log N)Because BST halves search space.Worst Case Time ComplexityO(N)Occurs when BST becomes skewed.Space ComplexityRecursiveO(H)Where:H = height of treeIterativeO(1)No recursion stack used.Interview ExplanationIn interviews, explain:Since this is a BST, we can use node ordering to decide whether both nodes lie in the left subtree, right subtree, or on different sides. The first split point becomes the Lowest Common Ancestor.This demonstrates:BST understandingTree recursionSearch optimizationEfficient traversal logicCommon Mistakes1. Treating It Like a Normal Binary TreeThis problem becomes easier because it is a BST.Use BST properties.2. Forgetting Split Point LogicThe LCA occurs when:p <= root <= qor vice versa.3. Traversing Entire TreeUnnecessary.BST lets us eliminate half the tree.4. Confusing Ancestor DefinitionA node can be ancestor of itself.Important for cases like:p = 2q = 4Answer becomes:2FAQsQ1. Why is BST important here?BST ordering allows efficient searching.Without BST property, we need a completely different approach.Q2. Can this be solved iteratively?Yes.Iterative traversal is even more space-efficient.Q3. Is recursion necessary?No.Both recursive and iterative solutions work.Q4. What is the Lowest Common Ancestor?The deepest node that has both target nodes as descendants.ConclusionLeetCode 235 is an excellent BST problem for mastering:BST propertiesRecursive traversalTree navigationOptimized searchingAncestor-based reasoningThe main insight is:In a BST, the first node where paths to p and q split becomes the Lowest Common Ancestor.Once this concept becomes intuitive, many BST interview problems become much easier to solve.

LeetCodeBinary Search TreeBSTJavaTreeMedium
Ai Assistant Kas