LeetCode Top 150 Flashcards

1
Q

45. Jump Game II, solve with O(n) time and O(1) space

You are given a 0-indexed array of integers nums of length n. You are initially positioned at nums[0].

Each element nums[i] represents the maximum length of a forward jump from index i. In other words, if you are at nums[i], you can jump to any nums[i + j] where:

0 <= j <= nums[i] and
i + j < n
Return the minimum number of jumps to reach nums[n - 1]. The test cases are generated such that you can reach nums[n - 1].

Example 1:

Input: nums = [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2. Jump 1 step from index 0 to 1, then 3 steps to the last index.
Example 2:

Input: nums = [2,3,0,1,4]
Output: 2

Constraints:

1 <= nums.length <= 104
0 <= nums[i] <= 1000
It’s guaranteed that you can reach nums[n - 1].

A

Intuition :
At any position ( i ), the maximum range we can reach is ( i + nums[i] ).
Using a greedy strategy, we try to jump as far as possible within our current range before incrementing the jump count.
Track the maximum range we can reach in the current jump window. When we exhaust this range, it means we must take another jump.
Algorithm :
Initialize Variables:

jumps to count the number of jumps taken.
current_end to track the farthest index we can reach in the current jump.
farthest to track the farthest index we can potentially reach overall.
Iterate Through the Array:

For every index ( i ), update the farthest reachable index: ( \text{farthest} = \max(\text{farthest}, i + \text{nums}[i]) ).
If ( i ) reaches current_end, increment the jumps counter and update current_end to farthest.
Stop Early:

If the current_end is greater than or equal to ( n - 1 ), we’ve reached the last index, so return jumps.
This approach ensures we take the minimum number of jumps while efficiently scanning the array.

class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        int jumps = 0, currentEnd = 0, farthest = 0;

        for (int i = 0; i < n - 1; i++) {
            farthest = Math.max(farthest, i + nums[i]);
            if (i == currentEnd) {
                jumps++;
                currentEnd = farthest;
            }
        }
        return jumps;
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

274. H-Index, solve with O(n) time and O(1) space

Given an array of integers citations where citations[i] is the number of citations a researcher received for their ith paper, return the researcher’s h-index.

According to the definition of h-index on Wikipedia: The h-index is defined as the maximum value of h such that the given researcher has published at least h papers that have each been cited at least h times.

Example 1:

Input: citations = [3,0,6,1,5]
Output: 3
Explanation: [3,0,6,1,5] means the researcher has 5 papers in total and each of them had received 3, 0, 6, 1, 5 citations respectively.
Since the researcher has 3 papers with at least 3 citations each and the remaining two with no more than 3 citations each, their h-index is 3.
Example 2:

Input: citations = [1,3,1]
Output: 1

Constraints:

n == citations.length
1 <= n <= 5000
0 <= citations[i] <= 1000

A

Intuition
The problem involves finding the H-Index, which is defined as the maximum value h such that the researcher has published at least h papers that have been cited at least h times each.

From the intuition behind the problem:

Sorting the citations array helps simplify the process of determining the H-Index.
By iterating through the sorted array, we can compare the number of citations to the number of papers that meet or exceed that threshold.

Approach
Sort the citations in ascending order.
For each index i in the sorted array:
The number of papers with citations greater than or equal to citations[i] is n - i, where n is the total number of papers.
If citations[i] is greater than or equal to n - i, then n - i is a valid candidate for the H-Index.
Return the maximum value that satisfies the above condition.

class Solution {
    public int hIndex(int[] citations) {
        Arrays.sort(citations);
        for(int i=0;i<citations.length;i++){
            if(citations[i]>= (citations.length-i)){
                return citations.length-i;
            }
        }
        return 0;
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

380. Insert Delete GetRandom O(1)

Implement the RandomizedSet class:

RandomizedSet() Initializes the RandomizedSet object.
bool insert(int val) Inserts an item val into the set if not present. Returns true if the item was not present, false otherwise.
bool remove(int val) Removes an item val from the set if present. Returns true if the item was present, false otherwise.
int getRandom() Returns a random element from the current set of elements (it’s guaranteed that at least one element exists when this method is called). Each element must have the same probability of being returned.
You must implement the functions of the class such that each function works in average O(1) time complexity.

Example 1:

Input
[“RandomizedSet”, “insert”, “remove”, “insert”, “getRandom”, “remove”, “insert”, “getRandom”]
[[], [1], [2], [2], [], [1], [2], []]
Output
[null, true, false, true, 2, true, false, 2]

Explanation
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // Inserts 1 to the set. Returns true as 1 was inserted successfully.
randomizedSet.remove(2); // Returns false as 2 does not exist in the set.
randomizedSet.insert(2); // Inserts 2 to the set, returns true. Set now contains [1,2].
randomizedSet.getRandom(); // getRandom() should return either 1 or 2 randomly.
randomizedSet.remove(1); // Removes 1 from the set, returns true. Set now contains [2].
randomizedSet.insert(2); // 2 was already in the set, so return false.
randomizedSet.getRandom(); // Since 2 is the only number in the set, getRandom() will always return 2.

Constraints:

-231 <= val <= 231 - 1
At most 2 * 105 calls will be made to insert, remove, and getRandom.
There will be at least one element in the data structure when getRandom is called.

A
class RandomizedSet {

    private Map<Integer, Integer> map = new HashMap<>();
    private List<Integer> list = new ArrayList<>();

    public RandomizedSet() {
        
    }
    
    public boolean insert(int val) {
        if (map.containsKey(val)) {
            return false;
        }

        list.add(val);
        map.put(val, list.size() - 1);

        return true;
    }
    
    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false;
        }  

        int index = map.get(val);
        int currElement = list.get(index);
        int last = list.get(list.size() - 1);

        list.set(index, last);
        list.set(list.size() - 1, currElement);
        map.put(list.get(index), index);

        list.remove(list.size() - 1);
        map.remove(val);

        return true;
    }
    
    public int getRandom() {
        Random r = new Random();
        return list.get(r.nextInt(list.size()));
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q
A
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

What is the two pointer approach in arrays?

A

The two pointer approach involves using two indices to traverse an array, often from opposite ends, to solve problems efficiently.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

True or False: The two pointer technique can only be applied to sorted arrays.

A

False

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Fill in the blank: The two pointer technique is commonly used to solve problems such as __________ and __________.

A

finding pairs that sum to a target, removing duplicates

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Which of the following scenarios can utilize the two pointer approach? (a) Reversing an array (b) Merging two sorted arrays (c) Finding the maximum element

A

Both (a) and (b) can utilize the two pointer approach.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

What is the time complexity of the two pointer approach in most cases?

A

O(n)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

238. Product of Array Except Self, solve with brute force appraoch.

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].

The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

You must write an algorithm that runs in O(n) time and without using the division operation.

Example 1:

Input: nums = [1,2,3,4]
Output: [24,12,8,6]
Example 2:

Input: nums = [-1,1,0,-3,3]
Output: [0,0,9,0,0]

Constraints:

2 <= nums.length <= 105
-30 <= nums[i] <= 30
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

A
  1. Brute Force
    So, the first and formost, the simplest method that comes to mind is, I can loop through the complete array using a pointer, say j, for every index i, and multiply all the elements at index j except when i == j. This would ensure that I skip the element at index i from being multiplied.

The Time Complexity for this solution would be O(n2).

Below is the Java Code for this approach.

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int ans[] = new int[n];
        
        for(int i = 0; i < n; i++) {
            int pro = 1;
            for(int j = 0; j < n; j++) {
                if(i == j) continue;
                pro *= nums[j];
            }
            ans[i] = pro;
        }
        
        return ans;
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

238. Product of Array Except Self, solve with O(n) time and O(n) space

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].

The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

You must write an algorithm that runs in O(n) time and without using the division operation.

Example 1:

Input: nums = [1,2,3,4]
Output: [24,12,8,6]
Example 2:

Input: nums = [-1,1,0,-3,3]
Output: [0,0,9,0,0]

Constraints:

2 <= nums.length <= 105
-30 <= nums[i] <= 30
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

A
  1. Finding Prefix Product and Suffix Product
    Similar to finding Prefix Sum Array, here we would intend to find the Prefix Product Array and Suffix Product Array for our original array, i.e. pre[i] = pre[i - 1] * a[i - 1] (yes, we multiply with a[i - 1] and not with a[i] on purpose) and similarly suff[i] = suff[i + 1] * a[i + 1].
    Now, at any index i our final answer ans[i] would be given by ans[i] = pre[i] * suff[i]. Why? Because the pre[i] * suff[i] contains product of every element before i and every element after i but not the element at index i (and that is the reson why we excluded a[i] in our prefix and suffix product).

The Time Complexity would be O(n), but we are now using Auxilary Space of O(n) (excluding the final answer array).

Below is the Java Code for this approach

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int pre[] = new int[n];
        int suff[] = new int[n];
        pre[0] = 1;
        suff[n - 1] = 1;
        
        for(int i = 1; i < n; i++) {
            pre[i] = pre[i - 1] * nums[i - 1];
        }
        for(int i = n - 2; i >= 0; i--) {
            suff[i] = suff[i + 1] * nums[i + 1];
        }
        
        int ans[] = new int[n];
        for(int i = 0; i < n; i++) {
            ans[i] = pre[i] * suff[i];
        }
        return ans;
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

238. Product of Array Except Self, solve with O(n) time and O(1) space

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].

The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

You must write an algorithm that runs in O(n) time and without using the division operation.

Example 1:

Input: nums = [1,2,3,4]
Output: [24,12,8,6]
Example 2:

Input: nums = [-1,1,0,-3,3]
Output: [0,0,9,0,0]

Constraints:

2 <= nums.length <= 105
-30 <= nums[i] <= 30
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.

A
  1. Directly store the product of prefix and suffix into the final answer array
    The logic is, we don’t actually need seperate array to store prefix product and suffix products, we can do all the approach discussed in method 3 directly onto our final answer array.

The Time Complexity would be O(n) but now, the Auxilary Space is O(1) (excluding the final answer array).

Below is the Java Code for the above algorithm.

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int ans[] = new int[n];
        Arrays.fill(ans, 1);
        int curr = 1;
        for(int i = 0; i < n; i++) {
            ans[i] *= curr;
            curr *= nums[i];
        }
        curr = 1;
        for(int i = n - 1; i >= 0; i--) {
            ans[i] *= curr;
            curr *= nums[i];
        }
        return ans;
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

t134. Gas Station, solve it in O(n) time and O(1) space

There are n gas stations along a circular route, where the amount of gas at the ith station is gas[i].

You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from the ith station to its next (i + 1)th station. You begin the journey with an empty tank at one of the gas stations.

Given two integer arrays gas and cost, return the starting gas station’s index if you can travel around the circuit once in the clockwise direction, otherwise return -1. If there exists a solution, it is guaranteed to be unique.

Example 1:

Input: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
Output: 3
Explanation:
Start at station 3 (index 3) and fill up with 4 unit of gas. Your tank = 0 + 4 = 4
Travel to station 4. Your tank = 4 - 1 + 5 = 8
Travel to station 0. Your tank = 8 - 2 + 1 = 7
Travel to station 1. Your tank = 7 - 3 + 2 = 6
Travel to station 2. Your tank = 6 - 4 + 3 = 5
Travel to station 3. The cost is 5. Your gas is just enough to travel back to station 3.
Therefore, return 3 as the starting index.
Example 2:

Input: gas = [2,3,4], cost = [3,4,3]
Output: -1
Explanation:
You can’t start at station 0 or 1, as there is not enough gas to travel to the next station.
Let’s start at station 2 and fill up with 4 unit of gas. Your tank = 0 + 4 = 4
Travel to station 0. Your tank = 4 - 3 + 2 = 3
Travel to station 1. Your tank = 3 - 3 + 3 = 3
You cannot travel back to station 2, as it requires 4 unit of gas but you only have 3.
Therefore, you can’t travel around the circuit once no matter where you start.

Constraints:

n == gas.length == cost.length
1 <= n <= 105
0 <= gas[i], cost[i] <= 104

A

Problem Breakdown:
You have gas stations arranged in a circle.
Each station has a certain amount of gas (gas[i]).
It costs a specific amount of gas (cost[i]) to travel from station i to station i+1.
You need to determine if there exists a starting gas station from which you can complete the entire circuit in a clockwise direction.

Key Observations:
If the total gas available is less than the total cost required, the journey is impossible.

If sum(gas) < sum(cost), return -1 immediately.
If the total gas is sufficient, there must be one unique starting station.

Even if you can’t start at some station, the journey might still be possible by starting at a later station.

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int tankCapacity = 0;
        int totalGas = 0;
        int totalCost = 0;
        int gasStationIndex = 0;

        for (int i = 0; i < n; i++) {
            totalGas += gas[i];
            totalCost += cost[i]; 
        }

        if (totalCost > totalGas) return -1;

        for (int i = 0; i < n; i++) {
            tankCapacity += gas[i] - cost[i];
            if (tankCapacity < 0) {
                tankCapacity = 0;
                gasStationIndex = i + 1;
            }
        }
        
        return gasStationIndex;
    }
}

totalGas and totalCost will track the total gas and total cost over the entire route.
tankCapacity tracks the current gas balance while iterating.
gasStationIndex stores the potential starting index.

first loop:
This loop calculates the total gas (totalGas) and total cost (totalCost).
If totalGas is less than totalCost, return -1 immediately because completing the circuit is impossible.

The second loop iterates through each station.
At each station i, calculate the net gain/loss of gas: tankCapacity += gas[i] - cost[i].
If capacity (current balance) drops below zero:
Reset the balance (total = 0).
Move to the next station (index = i + 1).
This means station i cannot be the starting point, so try station i+1.

Here’s a step-by-step breakdown of the solution:

Problem Breakdown:
You have gas stations arranged in a circle.
Each station has a certain amount of gas (gas[i]).
It costs a specific amount of gas (cost[i]) to travel from station i to station i+1.
You need to determine if there exists a starting gas station from which you can complete the entire circuit in a clockwise direction.
Key Observations:
If the total gas available is less than the total cost required, the journey is impossible.

If sum(gas) < sum(cost), return -1 immediately.
If the total gas is sufficient, there must be one unique starting station.

Even if you can’t start at some station, the journey might still be possible by starting at a later station.
Code Breakdown:
java
Copy code
public int canCompleteCircuit(int[] gas, int[] cost) {
int sGas = 0, sCost = 0, res = 0, total = 0;
sGas and sCost will track the total gas and total cost over the entire route.
total tracks the current gas balance while iterating.
res stores the potential starting index.
Step 1: Check if Completing the Circuit is Possible
java
Copy code
for (int i = 0; i < gas.length; i++) {
sGas += gas[i];
sCost += cost[i];
}
if (sGas < sCost) return -1;
This loop calculates the total gas (sGas) and total cost (sCost).
If sGas (total gas) is less than sCost (total cost), return -1 immediately because completing the circuit is impossible.
Step 2: Find the Starting Station
java
Copy code
for (int i = 0; i < gas.length; i++) {
total += gas[i] - cost[i];
if (total < 0) {
total = 0;
res = i + 1;
}
}
return res;
The second loop iterates through each station.
At each station i, calculate the net gain/loss of gas: total += gas[i] - cost[i].
If total (current balance) drops below zero:
Reset the balance (total = 0).
Move to the next station (res = i + 1).
This means station i cannot be the starting point, so try station i+1.

Why Does This Work?
If the total gas is sufficient, the only reason you fail at a certain station is that the gas you had up to that point wasn’t enough.
Resetting at i + 1 ensures that you only consider the segment that can possibly succeed.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

135. Candy, solve in O(n) time and O(n) space

There are n children standing in a line. Each child is assigned a rating value given in the integer array ratings.

You are giving candies to these children subjected to the following requirements:

Each child must have at least one candy.
Children with a higher rating get more candies than their neighbors.
Return the minimum number of candies you need to have to distribute the candies to the children.

Example 1:

Input: ratings = [1,0,2]
Output: 5
Explanation: You can allocate to the first, second and third child with 2, 1, 2 candies respectively.
Example 2:

Input: ratings = [1,2,2]
Output: 4
Explanation: You can allocate to the first, second and third child with 1, 2, 1 candies respectively.
The third child gets 1 candy because it satisfies the above two conditions.

Constraints:

n == ratings.length
1 <= n <= 2 * 104
0 <= ratings[i] <= 2 * 104

A

Candy Distribution Problem Solution Explained

Problem Summary
You need to distribute candies to children based on their ratings such that:
1. Each child gets at least one candy.
2. Children with a higher rating than their neighbors get more candies.

The goal is to calculate the minimum number of candies required to satisfy these conditions.

Solution Outline
The approach involves two passes through the ratings array:
1. Left to Right Pass – Ensures higher-rated children to the right receive more candies.
2. Right to Left Pass – Ensures higher-rated children to the left receive more candies.

By performing these two passes, we ensure that both conditions are met.

Code
```java
public class Solution {
public int candy(int[] ratings) {
int n = ratings.length; // Get the number of children
int[] candies = new int[n]; // Initialize an array to store the number of candies for each child

    // First pass: Check ratings from left to right
    for (int i = 1; i < n; i++) {
        if (ratings[i - 1] < ratings[i] && candies[i - 1] >= candies[i]) {
            candies[i] = candies[i - 1] + 1;
        }
    }
    
    // Second pass: Check ratings from right to left
    for (int i = n - 2; i >= 0; i--) {
        if (ratings[i + 1] < ratings[i] && candies[i + 1] >= candies[i]) {
            candies[i] = candies[i + 1] + 1;
        }
    }
    
    int totalCandies = 0;
    
    // Calculate the total number of candies needed
    for (int i = 0; i < n; i++) {
        totalCandies += candies[i] + 1;
    }
    
    return totalCandies;
} } ~~~

Step-by-Step Explanation
### 1. Initialization
```java
int n = ratings.length;
int[] candies = new int[n];
~~~
- n stores the number of children.
- candies array is initialized to keep track of the candies given to each child.

  1. First Pass (Left to Right)
    ```java
    for (int i = 1; i < n; i++) {
    if (ratings[i - 1] < ratings[i] && candies[i - 1] >= candies[i]) {
    candies[i] = candies[i - 1] + 1;
    }
    }
    ~~~
    - Traverse the array from left to right.
    - If the current child’s rating is higher than the previous child (ratings[i - 1] < ratings[i]), and the current child has fewer or equal candies, increment their candy count by one more than the previous child.
  1. Second Pass (Right to Left)
    ```java
    for (int i = n - 2; i >= 0; i–) {
    if (ratings[i + 1] < ratings[i] && candies[i + 1] >= candies[i]) {
    candies[i] = candies[i + 1] + 1;
    }
    }
    ~~~
    - Traverse the array from right to left.
    - If the current child’s rating is higher than the next child (ratings[i + 1] < ratings[i]), and the current child has fewer or equal candies, increment their candy count by one more than the next child.
  1. Calculate Total Candies
    ```java
    int totalCandies = 0;
    for (int i = 0; i < n; i++) {
    totalCandies += candies[i] + 1;
    }
    ~~~
    - Each child must have at least one candy, so candies[i] + 1 ensures that every child receives the minimum required candies.
    - Sum the candies to get the total number required.

Example Walkthrough
### Example 1
~~~
ratings = [1, 0, 2]
~~~
- Left to Right Pass:
- Start with [0, 0, 0]
- After pass: [1, 0, 1]
- Right to Left Pass:
- After pass: [2, 1, 2]
- Total Candies: 2 + 1 + 2 = 5

Example 2
~~~
ratings = [1, 2, 2]
~~~
- Left to Right Pass:
- Start with [0, 0, 0]
- After pass: [0, 1, 0]
- Right to Left Pass:
- No changes: [0, 1, 0]
- Total Candies: 1 + 2 + 1 = 4

Complexity Analysis
- Time Complexity: O(n) – Two linear passes through the ratings array.
- Space Complexity: O(n) – Additional array of size n for candies.

Key Points
- The solution guarantees that both conditions are satisfied by performing two passes.
- It ensures the minimum number of candies by distributing incrementally based on neighbors’ ratings.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

42. Trapping Rain Water, solve in O(n) time and O(n) space

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.

Input: height = [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6
Explanation: The above elevation map (black section) is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped.
Example 2:

Input: height = [4,2,0,3,2,5]
Output: 9

Constraints:

n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105

A

Let me explain this solution for the rain water trapping problem with a detailed breakdown of how it works.

First, let’s understand the core concept. The amount of water that can be trapped at any position depends on the minimum of the highest bars to its left and right, minus the height of the current position. This insight forms the foundation of our solution.

Let’s break down the solution step by step:

Understanding the Approach

The solution uses a concept called “boundaries” - for each position, we need to know:
1. The maximum height to its left
2. The maximum height to its right

This allows us to calculate how much water can be trapped at each position.

Implementation Details

Step 1: Creating Helper Arrays
```java
int[] left = new int[n];
int[] right = new int[n];
~~~
We create two arrays to store:
- left: Maximum height encountered from the left up to each position
- right: Maximum height encountered from the right up to each position

Step 2: Filling Left Max Heights
```java
left[0] = height[0];
for (int i = 1; i < n; i++) {
left[i] = Math.max(left[i - 1], height[i]);
}
~~~
For each position, we compare:
- The maximum height we’ve seen so far (left[i-1])
- The current height (height[i])
and store the larger value. This builds a running maximum from left to right.

Step 3: Filling Right Max Heights
```java
right[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i–) {
right[i] = Math.max(right[i + 1], height[i]);
}
~~~
Similar to Step 2, but working from right to left. For each position, we track the maximum height seen so far from the right side.

Step 4: Calculating Trapped Water
```java
int trappedWater = 0;
for (int i = 0; i < n; i++) {
int minHeight = Math.min(left[i], right[i]);
trappedWater += minHeight - height[i];
}
~~~
For each position:
1. Find the minimum of left and right boundaries (this determines how high water can rise)
2. Subtract the current height (this gives us the depth of water at this position)
3. Add this amount to our total

Example Walkthrough

Let’s take the first example: [0,1,0,2,1,0,1,3,2,1,2,1]

Consider position 2 (where height[2] = 0):
- Left maximum up to this point: max(0,1) = 1
- Right maximum after this point: max(2,1,0,1,3,2,1,2,1) = 3
- Minimum of these boundaries: min(1,3) = 1
- Water trapped = 1 - 0 = 1 unit

This same process repeats for each position, accumulating the total trapped water.

Time and Space Complexity
- Time Complexity: O(n) - we make three passes through the array
- Space Complexity: O(n) - we use two additional arrays of size n

This solution provides an efficient way to solve the problem by preprocessing the maximum heights from both directions, making it easier to calculate the trapped water at each position.

Would you like me to clarify any part of this explanation or walk through another example?

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

13. Roman to Integer, solve in O(n) time and O(1) space

Roman numerals are represented by seven different symbols: I, V, X, L, C, D and M.

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
For example, 2 is written as II in Roman numeral, just two ones added together. 12 is written as XII, which is simply X + II. The number 27 is written as XXVII, which is XX + V + II.

Roman numerals are usually written largest to smallest from left to right. However, the numeral for four is not IIII. Instead, the number four is written as IV. Because the one is before the five we subtract it making four. The same principle applies to the number nine, which is written as IX. There are six instances where subtraction is used:

I can be placed before V (5) and X (10) to make 4 and 9.
X can be placed before L (50) and C (100) to make 40 and 90.
C can be placed before D (500) and M (1000) to make 400 and 900.
Given a roman numeral, convert it to an integer.

Example 1:

Input: s = “III”
Output: 3
Explanation: III = 3.
Example 2:

Input: s = “LVIII”
Output: 58
Explanation: L = 50, V= 5, III = 3.
Example 3:

Input: s = “MCMXCIV”
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

Constraints:

1 <= s.length <= 15
s contains only the characters (‘I’, ‘V’, ‘X’, ‘L’, ‘C’, ‘D’, ‘M’).
It is guaranteed that s is a valid roman numeral in the range [1, 3999].

A
class Solution {
    public int romanToInt(String s) {
       int answer = 0;
       int previous = 0;
       int number = 0;

       for (int i = s.length() - 1; i >= 0; i--) {
            switch (s.charAt(i)) {
                case 'M' -> number = 1000;
                case 'D' -> number = 500;
                case 'C' -> number = 100;
                case 'L' -> number = 50;
                case 'X' -> number = 10;
                case 'V' -> number = 5;
                case 'I' -> number = 1;
            }

            if (number < previous) {
                answer -= number;
            } else {
                answer += number;
            }
            previous = number;
       } 

       return answer;
    }
}

Let me help you understand how this solution converts Roman numerals to integers by breaking it down step by step.

First, let’s understand the core insight: this solution processes the Roman numeral from right to left, which is particularly clever because it allows us to easily handle the subtraction cases (like IV or IX) by comparing each number with the previous one we processed.

Let’s walk through how it works using the example “MCMXCIV” (1994):

Step 1: Variable Setup
- answer: Stores our running total (final answer)
- number: Stores the current Roman numeral’s value
- prev: Stores the previous Roman numeral’s value
- We start from the rightmost character and work backwards

Step 2: Processing Right-to-Left
Let’s see how the values change as we process “MCMXCIV”:

Starting with “V”:
~~~
Character: ‘V’
number = 5
prev = 0 (initial value)
Since number > prev: answer += 5
answer = 5, prev = 5
~~~

Next character “I”:
~~~
Character: ‘I’
number = 1
prev = 5
Since number < prev: answer -= 1
answer = 4, prev = 1
~~~

Next character “C”:
~~~
Character: ‘C’
number = 100
prev = 1
Since number > prev: answer += 100
answer = 104, prev = 100
~~~

Next character “X”:
~~~
Character: ‘X’
number = 10
prev = 100
Since number < prev: answer -= 10
answer = 94, prev = 10
~~~

Next character “M”:
~~~
Character: ‘M’
number = 1000
prev = 10
Since number > prev: answer += 1000
answer = 1094, prev = 1000
~~~

Next character “C”:
~~~
Character: ‘C’
number = 100
prev = 1000
Since number < prev: answer -= 100
answer = 994, prev = 100
~~~

Final character “M”:
~~~
Character: ‘M’
number = 1000
prev = 100
Since number > prev: answer += 1000
Final answer = 1994
~~~

The key elements that make this solution work:

  1. Right-to-Left Processing: This approach simplifies the handling of subtraction cases. Instead of having to look ahead to see if we need to subtract, we can look at what we’ve already processed.
  2. Comparison Logic: If the current number is less than the previous number we saw, we subtract it (handling cases like IV). Otherwise, we add it. This elegantly handles both regular addition (like VI = 6) and subtraction cases (like IV = 4).
  3. Switch Statement: The switch statement provides a clean way to convert each Roman numeral character to its corresponding integer value. The modern arrow syntax (->) makes the code particularly readable.
  4. Single Pass: The algorithm only needs to traverse the string once, making it efficient with O(n) time complexity where n is the length of the input string.

This solution demonstrates how choosing the right perspective (right-to-left processing) can significantly simplify what might otherwise be a more complex problem requiring lookahead or multiple passes through the string.

Would you like me to elaborate on any particular aspect of the solution?

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

12. Integer to Roman, solve in O(1) time and O(1) space

Seven different symbols represent Roman numerals with the following values:

Symbol Value
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
Roman numerals are formed by appending the conversions of decimal place values from highest to lowest. Converting a decimal place value into a Roman numeral has the following rules:

If the value does not start with 4 or 9, select the symbol of the maximal value that can be subtracted from the input, append that symbol to the result, subtract its value, and convert the remainder to a Roman numeral.
If the value starts with 4 or 9 use the subtractive form representing one symbol subtracted from the following symbol, for example, 4 is 1 (I) less than 5 (V): IV and 9 is 1 (I) less than 10 (X): IX. Only the following subtractive forms are used: 4 (IV), 9 (IX), 40 (XL), 90 (XC), 400 (CD) and 900 (CM).
Only powers of 10 (I, X, C, M) can be appended consecutively at most 3 times to represent multiples of 10. You cannot append 5 (V), 50 (L), or 500 (D) multiple times. If you need to append a symbol 4 times use the subtractive form.
Given an integer, convert it to a Roman numeral.

Example 1:

Input: num = 3749

Output: “MMMDCCXLIX”

Explanation:

3000 = MMM as 1000 (M) + 1000 (M) + 1000 (M)
700 = DCC as 500 (D) + 100 (C) + 100 (C)
40 = XL as 10 (X) less of 50 (L)
9 = IX as 1 (I) less of 10 (X)
Note: 49 is not 1 (I) less of 50 (L) because the conversion is based on decimal places
Example 2:

Input: num = 58

Output: “LVIII”

Explanation:

50 = L
8 = VIII
Example 3:

Input: num = 1994

Output: “MCMXCIV”

Explanation:

1000 = M
900 = CM
90 = XC
4 = IV

Constraints:

1 <= num <= 3999

A
class Solution {
    public String intToRoman(int num) {
       String thousands[] = {"", "M", "MM", "MMM"};
       String hundreds[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}; 
       String tens[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
       String ones[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"};

       return thousands[num / 1000] + hundreds[(num % 1000) / 100] + tens[(num % 100) / 10] + ones[num % 10];
    }
}

Let me break down how this solution works step by step:

  1. Array Structure Setup

First, the solution creates four arrays that store pre-computed Roman numeral patterns:

```java
String M[] = {“”, “M”, “MM”, “MMM”} // Thousands place
String C[] = {“”, “C”, “CC”, “CCC”, “CD”, “D”, “DC”, “DCC”, “DCCC”, “CM”} // Hundreds place
String X[] = {“”, “X”, “XX”, “XXX”, “XL”, “L”, “LX”, “LXX”, “LXXX”, “XC”} // Tens place
String I[] = {“”, “I”, “II”, “III”, “IV”, “V”, “VI”, “VII”, “VIII”, “IX”} // Ones place
~~~

  1. Array Contents Explanation

Each array handles a specific decimal place:
- M array (thousands): Can only go up to 3000 (MMM) since constraint is num ≤ 3999
- C array (hundreds): Handles 100 to 900
- X array (tens): Handles 10 to 90
- I array (ones): Handles 1 to 9

  1. Number Splitting Logic

The solution splits the input number into its decimal places using division and modulo:

```java
M[num/1000] // Gets thousands digit
C[(num%1000)/100] // Gets hundreds digit
X[(num%100)/10] // Gets tens digit
I[num%10] // Gets ones digit
~~~

  1. Example Walkthrough

Let’s take num = 2743:

  1. num/1000 = 2
    • Looks up M[2] → “MM”
  2. (num%1000)/100 = 7
    • num%1000 = 743
    • 743/100 = 7
    • Looks up C[7] → “DCC”
  3. (num%100)/10 = 4
    • num%100 = 43
    • 43/10 = 4
    • Looks up X[4] → “XL”
  4. num%10 = 3
    • Looks up I[3] → “III”
  5. Concatenates: “MM” + “DCC” + “XL” + “III” = “MMDCCXLIII”
  6. Key Benefits
  7. No need for complex if-else conditions
  8. Pre-computed patterns handle all subtractive cases automatically
  9. O(1) time complexity since the number of operations is constant
  10. Clean and maintainable code structure

The solution is elegant because it transforms what could be a complex series of conditional statements into simple array lookups and basic arithmetic operations.

Ah, let’s understand why each array starts with an empty string “” as its first element (index 0).

The key is in how the arithmetic works. Let’s look at what happens when a particular place value is 0:

Take the number 1035 as an example:

  1. For thousands: 1035/1000 = 1
    • This correctly gets M[1] = “M”
  2. For hundreds: (1035%1000)/100 = 0
    • 1035%1000 = 35 (remainder after removing 1000)
    • 35/100 = 0
    • This gets C[0] = “” (empty string)
    • We want an empty string here because there are no hundreds!
  3. For tens: (1035%100)/10 = 3
    • 35%100 = 35
    • 35/10 = 3
    • Gets X[3] = “XXX”
  4. For ones: 1035%10 = 5
    • Gets I[5] = “V”

Final result: “M” + “” + “XXX” + “V” = “MXXXV”

If we didn’t have the empty string at index 0, we’d need additional logic to handle cases where a place value is zero. By including the empty string, we can simply concatenate the results without worrying about zero values - they automatically contribute nothing to the final string.

This is a clever way to handle zero values implicitly through array indexing rather than explicit conditional checks!

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

58. Length of Last Word in O(n) time and O(1) space

Given a string s consisting of words and spaces, return the length of the last word in the string.

A word is a maximal
substring
consisting of non-space characters only.

Example 1:

Input: s = “Hello World”
Output: 5
Explanation: The last word is “World” with length 5.
Example 2:

Input: s = “ fly me to the moon “
Output: 4
Explanation: The last word is “moon” with length 4.
Example 3:

Input: s = “luffy is still joyboy”
Output: 6
Explanation: The last word is “joyboy” with length 6.

Constraints:

1 <= s.length <= 104
s consists of only English letters and spaces ‘ ‘.
There will be at least one word in s.

A

Let me walk you through how this solution works to find the length of the last word in a string.

First, let’s understand the core method being used:
```java
public int lengthOfLastWord(String s) {
String[] split = s.split(“ “);
return split[split.length - 1].length();
}
~~~

The solution works by leveraging Java’s String manipulation capabilities in a clever way. Let’s break it down step by step:

  1. The split() method: When we call s.split(" "), Java breaks the input string into an array of substrings wherever it finds a space character. For example:
    • For “Hello World” → ["Hello", "World"]
    • For “ fly me to the moon “ → ["", "fly", "me", "to", "the", "moon"]
    • For “luffy is still joyboy” → ["luffy", "is", "still", "joyboy"]
  2. An important detail about split() is that it handles multiple consecutive spaces automatically. The method effectively removes empty strings that would result from consecutive spaces, so we don’t need to worry about the extra spaces in examples like the second one.
  3. Once we have our array of words, we access the last word using split[split.length - 1]. The array’s length minus 1 gives us the index of the last element. For example:
    • If split = ["Hello", "World"], then split.length is 2, so split[1] gives us “World”
    • If split = ["luffy", "is", "still", "joyboy"], then split[3] gives us “joyboy”
  4. Finally, we call length() on the last word to get its character count. The length() method returns the number of characters in the string.

This solution is particularly elegant because it handles all the edge cases mentioned in the constraints:
- It works for strings of any length between 1 and 10^4
- It handles multiple spaces between words
- It works correctly as long as there’s at least one word in the string
- It processes strings containing only English letters and spaces

However, it’s worth noting that while this solution is concise and readable, it does create a new array containing all words in the string. For very large strings, a more memory-efficient solution might scan the string from right to left and count characters until reaching a space. But for the given constraints (maximum length of 10^4), this solution is perfectly acceptable and much more maintainable.

Let me help you understand the time complexity of this solution by breaking down each operation:

  1. First, let’s look at the split() method. When Java’s String.split() runs, it needs to:
    • Scan through the entire input string once to identify where the spaces are
    • Create new substring objects for each word
    • Store these substrings in an array
    This operation has a time complexity of O(n), where n is the length of the input string, because it needs to examine each character exactly once.
  2. Next, we access the last element of the array with split[split.length - 1]. This is a constant time O(1) operation since we’re directly accessing an array element by its index.
  3. Finally, getting the length of the last word with length() is also an O(1) operation in Java, as String objects maintain their length as a field that can be accessed instantly.

Therefore, the overall time complexity is O(n), where n is the length of the input string. The dominant factor is the split() operation, while the other operations are constant time.

To put this in perspective, if we doubled the length of our input string, the time taken by the algorithm would roughly double as well. This linear relationship between input size and processing time makes the solution quite efficient for the given constraints (string length ≤ 10^4).

It’s worth noting that while we could write a solution that doesn’t use split() and instead scans from right to left (which would still be O(n)), the difference in actual performance would be minimal for the given constraints. The current solution prioritizes code readability and maintainability while still maintaining linear time complexity.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

14. Longest Common Prefix, solve in O(nlogn) time and O(1) space

Write a function to find the longest common prefix string amongst an array of strings.

If there is no common prefix, return an empty string “”.

Example 1:

Input: strs = [“flower”,”flow”,”flight”]
Output: “fl”
Example 2:

Input: strs = [“dog”,”racecar”,”car”]
Output: “”
Explanation: There is no common prefix among the input strings.

Constraints:

1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] consists of only lowercase English letters.

A
class Solution {
    public String longestCommonPrefix(String[] strs) {
        Arrays.sort(strs);

        String s1 = strs[0];
        String s2 = strs[strs.length - 1]; 

        int index = 0;
        while (index < s1.length() && index < s2.length()) {
            if (s1.charAt(index) == s2.charAt(index)) {
                index++;
            } else {
                break;
            }
        }

        return s1.substring(0, index);
    }
}

Let me explain how this solution cleverly finds the longest common prefix among an array of strings. The key insight is that if we sort the strings alphabetically, we only need to compare the first and last strings to find the common prefix.

Let’s break it down step by step:

First, consider what happens when we sort strings alphabetically:
```java
Arrays.sort(strs);
~~~
When strings are sorted, all strings with the same prefix will be grouped together. The first string (alphabetically smallest) and last string (alphabetically largest) in the sorted array will have the maximum possible difference. If these two strings share a common prefix, all strings between them must share that same prefix.

For example, with input [“flower”, “flow”, “flight”]:
After sorting: [“flight”, “flow”, “flower”]
- First string (s1) = “flight”
- Last string (s2) = “flower”

The code then compares these strings character by character:
```java
String s1 = strs[0]; // first string
String s2 = strs[strs.length - 1]; // last string
int index = 0;

while (index < s1.length() && index < s2.length()) {
if (s1.charAt(index) == s2.charAt(index)) {
index++;
} else {
break;
}
}
~~~

Let’s see how the comparison works in our example:
- index 0: ‘f’ == ‘f’ ✓ (increment index)
- index 1: ‘l’ == ‘l’ ✓ (increment index)
- index 2: ‘i’ != ‘o’ ✗ (break loop)

Finally, we extract the common prefix:
```java
return s1.substring(0, index);
~~~
This returns “fl”, which is indeed the longest common prefix.

The brilliance of this solution lies in its efficiency:
1. We don’t need to compare every string with every other string
2. We don’t need to check all characters in all strings
3. By using the sorted property, we only need to compare two strings
4. The solution handles edge cases naturally:
- If there’s no common prefix, we’ll break at index 0
- If one string is a prefix of all others, we’ll stop at its length
- If the array has only one string, it becomes both first and last string

Time complexity is O(S⋅log n + M) where:
- S is the length of the strings
- n is the number of strings
- M is the length of the common prefix
The space complexity is O(1) as we only store two string references and an index.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
20
Q

Why sorting is needed on finding the longest common prefix in the array? This question is based on the solution which uses sorting to find the longest common prefix.
Input: strs = [“flower”,”flow”,”flight”]

A

Let me explain why sorting is crucial for this solution to work correctly. I’ll break this down with examples to show why comparing just the first and last strings after sorting is sufficient.

Consider how strings are sorted alphabetically. When we sort strings, they get arranged based on character-by-character comparison from left to right. This creates an important property: any common prefix must be present in all strings between the first and last strings in the sorted array.

Let’s look at a detailed example:
~~~
Original array: [“flower”, “flow”, “flight”]
After sorting: [“flight”, “flow”, “flower”]
~~~

When these strings are sorted:
1. “flight” comes first because ‘i’ comes before ‘o’ in the alphabet
2. “flow” comes second because ‘o’ comes before ‘w’
3. “flower” comes last because it matches “flow” up to ‘w’ but has additional characters

Now, let’s understand why comparing just the first and last strings works:

  1. If there is a common prefix, it MUST be present in both the first and last strings:
    First: "flight"  -> starts with "fl"
    Last:  "flower"  -> starts with "fl"

    Any string that would come between them alphabetically (like “flow”) must also share this prefix. This is because to be sorted between these strings, it must start with the same characters that match in both the first and last strings.
  2. If the first and last strings don’t share a prefix at some position, no other string in the array can have a longer common prefix:
    "flight" -> third character is 'i'
    "flower" -> third character is 'o'

    Since ‘i’ comes before ‘o’ in the alphabet, any string with a different third character would either:
    • If the character comes before ‘i’: sort before “flight”
    • If the character comes after ‘o’: sort after “flower”
    • If the character is between ‘i’ and ‘o’: still not match one of the ends

Let’s see what would happen WITHOUT sorting:
~~~
Original array: [“flower”, “flow”, “flight”]
~~~
If we just compared the first and last strings without sorting:
~~~
First: “flower”
Last: “flight”
~~~
We would incorrectly find a common prefix of “fl”, but we wouldn’t be guaranteed that “flow” (the middle string) shares this prefix. We got lucky in this case because all strings do share “fl”, but in general, this approach wouldn’t work.

Consider this counterexample:
~~~
Unsorted: [“apple”, “zebra”, “appetite”]
~~~
Comparing just first and last without sorting would compare “apple” and “appetite”, finding “app” as a common prefix. But “zebra” in the middle doesn’t share this prefix at all.

After sorting:
~~~
Sorted: [“appetite”, “apple”, “zebra”]
~~~
Now comparing first and last strings correctly shows there is no common prefix, as “appetite” and “zebra” share no common starting characters.

This is why sorting is essential: it guarantees that if a prefix is common to the first and last strings, it must be common to all strings in between, because that’s the only way they could have been sorted into those positions. Without sorting, we lose this guarantee and can’t rely on just checking the endpoints of our array.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
21
Q

151. Reverse Words in a String, solve in O(n) time and O(n) space

Given an input string s, reverse the order of the words.

A word is defined as a sequence of non-space characters. The words in s will be separated by at least one space.

Return a string of the words in reverse order concatenated by a single space.

Note that s may contain leading or trailing spaces or multiple spaces between two words. The returned string should only have a single space separating the words. Do not include any extra spaces.

Example 1:

Input: s = “the sky is blue”
Output: “blue is sky the”
Example 2:

Input: s = “ hello world “
Output: “world hello”
Explanation: Your reversed string should not contain leading or trailing spaces.
Example 3:

Input: s = “a good example”
Output: “example good a”
Explanation: You need to reduce multiple spaces between two words to a single space in the reversed string.

Constraints:

1 <= s.length <= 104
s contains English letters (upper-case and lower-case), digits, and spaces ‘ ‘.
There is at least one word in s.

A
class Solution {
    public String reverseWords(String s) {
        String[] arr = s.trim().split("\\s+");
        
        int i = 0;
        int j = arr.length - 1;

        while (i < j) {
            String temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;

            i++;
            j--;
        }

        return String.join(" ", arr);
    }
}

Let me explain this solution step by step, breaking down how it reverses the words in a string while handling various edge cases.

Let’s start with understanding what the solution needs to accomplish:
1. Remove extra spaces (leading, trailing, and between words)
2. Reverse the order of words
3. Join the words back together with single spaces

Here’s how the solution works:

First, let’s look at the input preprocessing:
```java
String[] arr = s.trim().split(“\s+”);
~~~
- trim() removes leading and trailing spaces from the string. For example, “ hello world “ becomes “hello world”
- split("\\s+") splits the string into an array of words:
- \\s+ is a regular expression pattern that matches one or more whitespace characters
- This effectively handles multiple spaces between words
- For “hello world”, we get ["hello", "world"]

Next, the core reversal logic:
```java
int i = 0;
int j = arr.length - 1;
while (i < j) {
String temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j–;
}
~~~
This is implementing a two-pointer technique to reverse the array:
- i starts from the beginning (index 0)
- j starts from the end (last index)
- We swap elements at positions i and j using a temporary variable
- We move the pointers towards the center (i increases, j decreases)
- The process continues until the pointers meet in the middle

For example, with input ["the", "sky", "is", "blue"]:
1. First swap: “the” ↔ “blue” → ["blue", "sky", "is", "the"]
2. Second swap: “sky” ↔ “is” → ["blue", "is", "sky", "the"]

Finally, we join the words:
```java
return String.join(“ “, arr);
~~~
- String.join() concatenates all elements in the array
- The first argument “ “ specifies that we want a single space between elements
- This creates our final output string with proper spacing

Time Complexity: O(n), where n is the length of the input string
Space Complexity: O(n) to store the array of words

Regarding the follow-up question about solving it in-place with O(1) extra space: This Java solution doesn’t solve it in-place because Java strings are immutable. For an in-place solution, you would need a language with mutable strings (like C++) and would need to manipulate the characters directly within the string.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
22
Q

6. Zigzag Conversion, solve in O(n2) time and O(numRows) space.

The string “PAYPALISHIRING” is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)

P A H N
A P L S I I G
Y I R
And then read line by line: “PAHNAPLSIIGYIR”

Write the code that will take a string and make this conversion given a number of rows:

string convert(string s, int numRows);

Example 1:

Input: s = “PAYPALISHIRING”, numRows = 3
Output: “PAHNAPLSIIGYIR”
Example 2:

Input: s = “PAYPALISHIRING”, numRows = 4
Output: “PINALSIGYAHRPI”
Explanation:
P I N
A L S I G
Y A H R
P I
Example 3:

Input: s = “A”, numRows = 1
Output: “A”

Constraints:

1 <= s.length <= 1000
s consists of English letters (lower-case and upper-case), ‘,’ and ‘.’.
1 <= numRows <= 1000

A
class Solution {
    public String convert(String s, int numRows) {
        String[] ans = new String[numRows];
        for (int i = 0; i < numRows; i++) {
            ans[i] = "";
        } 

        int j = 0;
        while (j < s.length()) {
            for (int i = 0; i < numRows && j < s.length(); i++) {
                ans[i] += s.charAt(j++);
            }

            for (int i = numRows - 2; i > 0 && j < s.length(); i--) {
                ans[i] += s.charAt(j++);
            }
        }

        String result = "";
        for (int i = 0; i < numRows; i++) {
            result += ans[i];
        }

        return result;
    }
}

Let me explain how this zigzag conversion solution works step by step.

First, let’s understand what the zigzag pattern means. When we write characters in a zigzag with n rows, we move down vertically for n characters, then move up diagonally until we reach the top row, then repeat. This creates a wave-like pattern.

Here’s how the solution works:

  1. Initialize Storage

```java
String[] ans = new String[numRows];
for (int i = 0; i < numRows; i++) {
ans[i] = “”;
}
~~~

We create an array of strings, one for each row. Each string will store the characters that belong in that row. We initialize each string as empty.

  1. Main Processing Loop

The solution has two main parts that alternate:

Part A: Moving Down Vertically
```java
for (int i = 0; i < numRows && j < s.length(); i++) {
ans[i] += s.charAt(j++);
}
~~~

This part moves down vertically, adding one character to each row from top to bottom. For example, with “PAYPAL” and 3 rows:
~~~
P
A
Y
~~~

Part B: Moving Up Diagonally
```java
for (int i = numRows - 2; i > 0 && j < s.length(); i–) {
ans[i] += s.charAt(j++);
}
~~~

This part moves up diagonally, adding characters to the middle rows (excluding the top and bottom rows). Continuing our example:
~~~
P A
A P L
Y
~~~

  1. Combining Results

```java
String result = “”;
for (int i = 0; i < numRows; i++) {
result += ans[i];
}
~~~

Finally, we combine all rows into a single string by concatenating them from top to bottom.

Example Walkthrough

Let’s see how it processes “PAYPALISHIRING” with 3 rows:

  1. First vertical movement: “P”, “A”, “Y”
  2. Diagonal movement up: “P” in row 2
  3. Second vertical movement: “A”, “L”, “I”
  4. Diagonal movement up: “S” in row 2
  5. Third vertical movement: “H”, “I”, “R”
  6. Diagonal movement up: “I” in row 2
  7. Final vertical movement: “N”, “G”

This creates:
~~~
P A H N
A P L S I I G
Y I R
~~~

When we combine these rows, we get “PAHNAPLSIIGYIR”.

Special Cases

The solution elegantly handles special cases:
- When numRows = 1, only the vertical movement happens
- When the string is shorter than numRows, it still works correctly
- The zigzag pattern adjusts automatically for different numbers of rows

This solution is efficient with O(n) time complexity, where n is the length of the input string, as it processes each character exactly once.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
23
Q

28. Find the Index of the First Occurrence in a String, solve in O(n) ti

Given two strings needle and haystack, return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

Input: haystack = “sadbutsad”, needle = “sad”
Output: 0
Explanation: “sad” occurs at index 0 and 6.
The first occurrence is at index 0, so we return 0.
Example 2:

Input: haystack = “leetcode”, needle = “leeto”
Output: -1
Explanation: “leeto” did not occur in “leetcode”, so we return -1.

Constraints:

1 <= haystack.length, needle.length <= 104
haystack and needle consist of only lowercase English characters.

A

Let me break down this string matching solution step by step to help you understand how it works.

The given solution implements what’s known as a “sliding window” approach to find a substring (needle) within a larger string (haystack). Let’s examine how it works:

```java
class Solution {
public int strStr(String haystack, String needle) {
// Loop through possible windows in haystack
for (int i = 0, j = needle.length(); j <= haystack.length(); i++, j++) {
// Check if current window matches needle
if (haystack.substring(i, j).equals(needle)) {
return i;
}
}
return -1;
}
}
~~~

The key insight in this solution is that we can check for the presence of the needle by looking at substrings of the haystack that are the same length as the needle. Here’s how it works:

First, we set up two pointers: i and j. The pointer i marks the start of our current window, while j marks the end. The window size is always equal to the length of the needle we’re looking for.

For example, if we have:
- haystack = “sadbutsad”
- needle = “sad”

In the first iteration:
- i = 0
- j = 3 (length of “sad”)
- We check substring(0, 3) which gives us “sad”

If we don’t find a match, both pointers move forward by one position, maintaining the same window size:
- i = 1
- j = 4
- We check substring(1, 4) which gives us “adb”

The loop continues until either:
1. We find a matching substring, in which case we return the starting index i
2. We reach the end of the haystack without finding a match, in which case we return -1

The time complexity of this solution is O(n * m) where:
- n is the length of the haystack
- m is the length of the needle
This is because for each position in the haystack, we’re potentially creating and comparing a substring of length m.

The space complexity is O(m) due to the creation of substring objects during comparison.

While this solution is intuitive and works correctly, it’s worth noting that there are more efficient algorithms for string matching, such as the KMP (Knuth-Morris-Pratt) algorithm, which achieves O(n + m) time complexity. However, for most practical purposes and interview settings, this sliding window solution is perfectly acceptable and easier to understand and implement.

24
Q

125. Valid Palindrome, solve in O(n) time and O(1) space

A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.

Given a string s, return true if it is a palindrome, or false otherwise.

Example 1:

Input: s = “A man, a plan, a canal: Panama”
Output: true
Explanation: “amanaplanacanalpanama” is a palindrome.
Example 2:

Input: s = “race a car”
Output: false
Explanation: “raceacar” is not a palindrome.
Example 3:

Input: s = “ “
Output: true
Explanation: s is an empty string “” after removing non-alphanumeric characters.
Since an empty string reads the same forward and backward, it is a palindrome.

Constraints:

1 <= s.length <= 2 * 105
s consists only of printable ASCII characters.

A

Let me help you understand this palindrome checking solution by breaking it down step by step and explaining how it works. I’ll also suggest some improvements to make the code more robust.

First, let’s understand what the code is doing:

```java
class Solution {
public boolean isPalindrome(String s) {
// Convert input string to char array
char[] arr = s.toCharArray();
StringBuilder inputString = new StringBuilder();

    // First pass: Clean the string by keeping only alphanumeric characters
    for (int i = 0; i < arr.length; i++) {
        if(Character.isLetterOrDigit(arr[i])) {
            inputString.append(Character.toLowerCase(arr[i]));
        }
    }
    
    // Get length of cleaned string
    int length = inputString.length();
    int i = 0;
    int j = length - 1;
    
    // Check if it's a palindrome using two pointers
    while (i < length && j > 0) {
        if (inputString.charAt(i) != inputString.charAt(j)) {
            return false;
        }
        i++;
        j--;
    }
    return true;
} } ~~~

There’s actually a bug in this code - the while loop condition j > 0 should be i < j. Let me explain why and provide an improved version:

```java
// this solution is faster as compared to the above solution
class Solution {
public boolean isPalindrome(String s) {
// Convert input string to char array
char[] arr = s.toCharArray();
StringBuilder cleanString = new StringBuilder();

    // First pass: Clean the string by keeping only alphanumeric characters
    for (char c : arr) {
        if(Character.isLetterOrDigit(c)) {
            cleanString.append(Character.toLowerCase(c));
        }
    }
    
    // Use two pointers to check if it's a palindrome
    int left = 0;
    int right = cleanString.length() - 1;
    
    while (left < right) {
        if (cleanString.charAt(left) != cleanString.charAt(right)) {
            return false;
        }
        left++;
        right--;
    }
    return true;
} } ~~~

Let’s break down how this solution works:

  1. String Cleaning Phase:
    • The input string is first converted to a char array for easier processing
    • We create a StringBuilder to store our cleaned string
    • We iterate through each character, keeping only alphanumeric characters (letters and numbers)
    • All letters are converted to lowercase for case-insensitive comparison
  2. Palindrome Checking Phase:
    • We use two pointers: left (starting from beginning) and right (starting from end)
    • We compare characters from both ends, moving inward
    • If any pair of characters doesn’t match, we return false
    • If we complete the while loop without finding mismatches, we return true

The key improvements in the revised version:
1. Changed the while loop condition from j > 0 to left < right to properly handle all cases.
2. Used more descriptive variable names (left/right instead of i/j)
3. Used enhanced for loop for cleaner iteration over the char array
4. Renamed inputString to cleanString for better clarity

Time Complexity: O(n), where n is the length of the input string
Space Complexity: O(n) to store the cleaned string

The solution handles all the example cases correctly:
1. “A man, a plan, a canal: Panama” → “amanaplanacanalpanama” → true
2. “race a car” → “raceacar” → false
3. “ “ → “” → true (empty string is considered palindrome)

25
Q

392. Is Subsequence, solve in O(n) time and O(1) space

Given two strings s and t, return true if s is a subsequence of t, or false otherwise.

A subsequence of a string is a new string that is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (i.e., “ace” is a subsequence of “abcde” while “aec” is not).

Example 1:

Input: s = “abc”, t = “ahbgdc”
Output: true
Example 2:

Input: s = “axc”, t = “ahbgdc”
Output: false

Constraints:

0 <= s.length <= 100
0 <= t.length <= 104
s and t consist only of lowercase English letters.

Follow up: Suppose there are lots of incoming s, say s1, s2, …, sk where k >= 109, and you want to check one by one to see if t has its subsequence. In this scenario, how would you change your code?

A
class Solution {
    public boolean isSubsequence(String s, String t) {
        // Initialize pointers for both strings
        int first = 0;  // tracks position in string s (potential subsequence)
        int second = 0; // tracks position in string t (main string)
        
        // Continue until we reach the end of either string
        while (first < s.length() && second < t.length()) {
            // If characters match, move the first pointer forward
            if (s.charAt(first) == t.charAt(second)) {
                first++;
            }
            // Always move the second pointer forward
            second++;
        }
        
        // If we've processed all characters in s, it's a subsequence
        return first == s.length();
    }
}

This solution uses a two-pointer technique that’s particularly efficient for a single check. Let’s see how it works with an example:
For s = “abc” and t = “ahbgdc”:

Start: first = 0, second = 0

Compare ‘a’ with ‘a’: match found → first = 1, second = 1

Next: first = 1, second = 1

Compare ‘b’ with ‘h’: no match → first = 1, second = 2
Compare ‘b’ with ‘b’: match found → first = 2, second = 3

Continue: first = 2, second = 3

Compare ‘c’ with ‘g’: no match → first = 2, second = 4
Compare ‘c’ with ‘d’: no match → first = 2, second = 5
Compare ‘c’ with ‘c’: match found → first = 3, second = 6

26
Q

167. Two Sum II - Input Array Is Sorted, solve in O(n) time and O(1) spa

Given a 1-indexed array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Let these two numbers be numbers[index1] and numbers[index2] where 1 <= index1 < index2 <= numbers.length.

Return the indices of the two numbers, index1 and index2, added by one as an integer array [index1, index2] of length 2.

The tests are generated such that there is exactly one solution. You may not use the same element twice.

Your solution must use only constant extra space.

Example 1:

Input: numbers = [2,7,11,15], target = 9
Output: [1,2]
Explanation: The sum of 2 and 7 is 9. Therefore, index1 = 1, index2 = 2. We return [1, 2].
Example 2:

Input: numbers = [2,3,4], target = 6
Output: [1,3]
Explanation: The sum of 2 and 4 is 6. Therefore index1 = 1, index2 = 3. We return [1, 3].
Example 3:

Input: numbers = [-1,0], target = -1
Output: [1,2]
Explanation: The sum of -1 and 0 is -1. Therefore index1 = 1, index2 = 2. We return [1, 2].

Constraints:

2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers is sorted in non-decreasing order.
-1000 <= target <= 1000
The tests are generated such that there is exactly one solution.

A

Let me help you understand this solution for the Two Sum II problem, where we need to find two numbers in a sorted array that add up to a target value.

Let’s break down how this solution works:

  1. The Two-Pointer Approach
    ```java
    int low = 0;
    int high = numbers.length - 1;
    ~~~
    The solution uses two pointers:
    - low starts from the beginning of the array
    - high starts from the end of the array
  2. The Main Logic
    ```java
    while (low < high) {
    int sum = numbers[low] + numbers[high];
    if (sum == target) {
    return new int[]{low + 1, high + 1};
    } else if (sum < target) {
    low++;
    } else {
    high–;
    }
    }
    ~~~

Here’s how it works:
- Calculate the sum of numbers at low and high indices
- If the sum equals target, we’ve found our answer
- If sum is less than target, we need a larger sum, so increment low
- If sum is more than target, we need a smaller sum, so decrement high

The solution is efficient because:
1. It takes advantage of the array being sorted
2. Uses constant space O(1)
3. Has linear time complexity O(n)

Why This Works:
- Since the array is sorted, when we move low right, the sum increases
- When we move high left, the sum decreases
- This allows us to systematically search for the target sum

Example Walk-through:
~~~
numbers = [2,7,11,15], target = 9

Step 1: low = 0, high = 3
sum = 2 + 15 = 17 > 9
high–

Step 2: low = 0, high = 2
sum = 2 + 11 = 13 > 9
high–

Step 3: low = 0, high = 1
sum = 2 + 7 = 9 == 9
Return [low + 1, high + 1] = [1, 2]
~~~

Remember that the problem requires 1-indexed output, which is why we add 1 to both indices in the return statement.

Would you like me to explain any specific part in more detail?

27
Q

11. Container With Most Water, solve with O(n) time and O(1) space

You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).

Find two lines that together with the x-axis form a container, such that the container contains the most water.

Return the maximum amount of water a container can store.

Notice that you may not slant the container.

Example 1:

Input: height = [1,8,6,2,5,4,8,3,7]
Output: 49
Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.
Example 2:

Input: height = [1,1]
Output: 1

Constraints:

n == height.length
2 <= n <= 105
0 <= height[i] <= 104

A

Let me help you understand this problem and the solution thoroughly. This is the famous “Container With Most Water” problem, which is a great example of using the two-pointer technique.

Let’s break down the problem and solution step by step:

  1. Problem Understanding:
    • We have vertical lines of different heights on a 2D plane
    • We need to find two lines that, together with the x-axis, will form a container that holds the maximum amount of water
    • The amount of water is determined by the width between the lines multiplied by the height of the shorter line
  2. Why Two Pointers Work:
    The solution uses the two-pointer technique, which is particularly clever here because:
    • The width of the container is determined by the distance between the two lines
    • The height is limited by the shorter of the two lines
    • By starting with the maximum possible width (using pointers at both ends), we can systematically explore all potentially optimal solutions

Let’s analyze the Java solution with detailed comments:

```java
class Solution {
public int maxArea(int[] height) {
// Initialize two pointers at the ends of the array
int left = 0;
int right = height.length - 1;

    // Keep track of the maximum area found so far
    int maxArea = 0;
    
    // Continue until the pointers meet
    while (left < right) {
        // Calculate width between lines
        int w = right - left;
        
        // Height is limited by the shorter line
        int h = Math.min(height[left], height[right]);
        
        // Calculate current area
        int area = w * h;
        
        // Update maxArea if current area is larger
        maxArea = Math.max(maxArea, area);
        
        // Move the pointer that points to the shorter line
        // This is key: we always move the limiting factor
        if (height[left] < height[right]) {
            left++;
        } else if (height[left] > height[right]) {
            right--;
        } else {
            // If heights are equal, move both pointers
            left++;
            right--;
        }
    }
    
    return maxArea;
} } ~~~

The Key Insight:
The most important part of this solution is understanding why we move the pointer pointing to the shorter line. Here’s why:
- If we have two lines of different heights, the shorter line is always the limiting factor for the area
- Moving the pointer of the taller line can never give us a better solution because:
1. The width will decrease
2. The height will still be limited by the same shorter line (or become even shorter)
- Therefore, our only chance of finding a larger area is by moving the pointer of the shorter line, hoping to find a taller line that might compensate for the decreased width

Time Complexity: O(n) - we traverse the array once with two pointers
Space Complexity: O(1) - we only use a constant amount of extra space

Let’s trace the first example [1,8,6,2,5,4,8,3,7]:
1. Initial state: left=0 (height=1), right=8 (height=7)
- Area = 8 × 1 = 8
2. Move left pointer (height=1 is smaller)
3. New state: left=1 (height=8), right=8 (height=7)
- Area = 7 × 7 = 49
And so on…

The maximum area turns out to be 49, formed by the lines at index 1 (height=8) and index 8 (height=7), with width 7 and height 7.

Would you like me to explain any particular part of this solution in more detail?

28
Q

3Sum, solve in O(n2) time and O(1) space

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.

Notice that the solution set must not contain duplicate triplets.

Example 1:

Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]
Explanation:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
The distinct triplets are [-1,0,1] and [-1,-1,2].
Notice that the order of the output and the order of the triplets does not matter.
Example 2:

Input: nums = [0,1,1]
Output: []
Explanation: The only possible triplet does not sum up to 0.
Example 3:

Input: nums = [0,0,0]
Output: [[0,0,0]]
Explanation: The only possible triplet sums up to 0.

Constraints:

3 <= nums.length <= 3000
-105 <= nums[i] <= 105

A

I’ll help explain a solution to this three sum problem that runs in O(n²) time. The key insight is that we can avoid using three nested loops by sorting the array first and then using two pointers.

Let’s break down the solution step by step:

First, the overall strategy:
1. Sort the array (this takes O(n log n) time)
2. For each number, use two pointers to find pairs that sum to its negative
3. Skip duplicates to avoid counting the same triplet multiple times

Here’s the solution with detailed explanations:

```java
import java.util.*;

class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// Create a list to store our triplets
// We use ArrayList since we don't know how many triplets we'll find
List<List<Integer>> result = new ArrayList<>();</Integer></Integer>

    // First, sort the array to enable our two-pointer technique
    // and to make handling duplicates easier
			// Skip duplicates for our first number
    // Example: If we have [-1, -1, 2], once we've used the first -1,
    // we don't need to use the second -1 as our first number
    Arrays.sort(nums);
    
    // We'll iterate through the array, using each number as our first number
    // We stop at length-2 because we need room for two more numbers
    for (int i = 0; i < nums.length - 2; i++) {
        // Skip duplicates for the first position
        // If this number is the same as the previous one, we've already found
        // all possible triplets starting with this number
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;
        }
        
        // Initialize our two pointers
        // left starts right after i, right starts at the end
        int left = i + 1;
        int right = nums.length - 1;
        
        // Calculate what we need the other two numbers to sum to
        // Since we want the total sum to be 0, we need: 
        // nums[left] + nums[right] = -nums[i]
        int target = -nums[i];
        
        // Keep moving pointers until they meet
        while (left < right) {
            int currentSum = nums[left] + nums[right];
            
            if (currentSum == target) {
                // We found a valid triplet! Add it to our result
                // Arrays.asList creates a fixed-size list containing our three numbers
                result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                
                // Skip duplicates for left pointer
                while (left < right && nums[left] == nums[left + 1]) {
                    left++;
                }
                
                // Skip duplicates for right pointer
                while (left < right && nums[right] == nums[right - 1]) {
                    right--;
                }
                
                // Move both pointers inward to look for more triplets
                left++;
                right--;
            } else if (currentSum < target) {
                // Sum is too small, we need larger numbers
                // Since array is sorted, moving left pointer right gives us a larger number
                left++;
            } else {
                // Sum is too large, we need smaller numbers
                // Since array is sorted, moving right pointer left gives us a smaller number
                right--;
            }
        }
    }
    
    return result;
} } ~~~

Let’s understand why this works and how it achieves O(n²) time complexity:

  1. Sorting the array first is crucial because it:
    • Helps us avoid duplicates (similar numbers are adjacent)
    • Lets us use two pointers effectively (we know which direction to move based on the sum)
  2. The main loop runs n-2 times (we need room for two more numbers). For each iteration:
    • We fix one number (nums[i])
    • We look for two other numbers that sum to -nums[i]
    • This would give us a total sum of 0
  3. The two-pointer technique is what makes this O(n²) instead of O(n³):
    • Instead of using two more nested loops
    • We start one pointer right after i and one at the end
    • If the sum is too small, we move left pointer right
    • If the sum is too large, we move right pointer left
    • This works because the array is sorted!
  4. Handling duplicates is critical:
    • Skip duplicate values for the first number (nums[i])
    • Skip duplicate values for left and right pointers
    • This ensures no duplicate triplets in the result

Let’s analyze the time complexity:
- Sorting: O(n log n)
- Main loop: O(n)
- Two-pointer loop for each iteration: O(n)
- Total: O(n²)
- Overall complexity: O(n²) since O(n²) dominates O(n log n)

Space complexity is O(1) excluding the space needed for output, as we only use a constant amount of extra space for our pointers and variables.

This solution is much more efficient than the naive O(n³) approach of using three nested loops. The sorting step plus the two-pointer technique allows us to find all triplets in O(n²) time while still handling all the requirements, including avoiding duplicate triplets.

29
Q

209. Minimum Size Subarray Sum, solve in O(n) and O(nlogn) time and O(1)

Given an array of positive integers nums and a positive integer target, return the minimal length of a
subarray
whose sum is greater than or equal to target. If there is no such subarray, return 0 instead.

Example 1:

Input: target = 7, nums = [2,3,1,2,4,3]
Output: 2
Explanation: The subarray [4,3] has the minimal length under the problem constraint.
Example 2:

Input: target = 4, nums = [1,4,4]
Output: 1
Example 3:

Input: target = 11, nums = [1,1,1,1,1,1,1,1]
Output: 0

Constraints:

1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 104

Follow up: If you have figured out the O(n) solution, try coding another solution of which the time complexity is O(n log(n)).

A

I’ll help you solve this problem step by step. Let’s first understand what we’re trying to do and then implement both the O(n) and O(n log n) solutions.

Let’s start with the O(n) solution using the sliding window technique:

```java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// Edge case: if array is empty
if (nums == null || nums.length == 0) {
return 0;
}

    int minLength = Integer.MAX_VALUE;  // Track the minimal length
    int currentSum = 0;                 // Sum of current window
    int start = 0;                      // Start of sliding window
    
    // Iterate through the array using the end pointer of our window
    for (int end = 0; end < nums.length; end++) {
        currentSum += nums[end];  // Add the current number to our window sum
        
        // While our window sum is >= target, try to minimize the window
        while (currentSum >= target) {
            // Update minLength if current window is smaller
            minLength = Math.min(minLength, end - start + 1);
            
            // Remove elements from start of window
            currentSum -= nums[start];
            start++;
        }
    }
    
    // If we never found a valid subarray, return 0
    return minLength == Integer.MAX_VALUE ? 0 : minLength;
} } ~~~

Now let’s understand how this solution works:

  1. We use a sliding window approach with two pointers: start and end.
  2. We expand our window by moving the end pointer and adding elements.
  3. When our window sum becomes ≥ target, we try to minimize it by moving the start pointer.
  4. We keep track of the minimum length found so far.

Now for the O(n log n) solution using binary search:

```java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
// Edge case: if array is empty
if (nums == null || nums.length == 0) {
return 0;
}

    // Create prefix sum array
    int[] prefixSum = new int[nums.length + 1];
    for (int i = 1; i <= nums.length; i++) {
        prefixSum[i] = prefixSum[i-1] + nums[i-1];
    }
    
    int minLength = Integer.MAX_VALUE;
    
    // For each starting position, binary search for the ending position
    for (int start = 0; start < nums.length; start++) {
        // Binary search for the end position
        int targetSum = target + prefixSum[start];
        int end = binarySearch(prefixSum, start + 1, prefixSum.length, targetSum);
        
        if (end <= nums.length) {
            minLength = Math.min(minLength, end - start);
        }
    }
    
    return minLength == Integer.MAX_VALUE ? 0 : minLength;
}

// Binary search to find the first position where prefixSum[pos] >= target
private int binarySearch(int[] prefixSum, int left, int right, int target) {
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (prefixSum[mid] >= target) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
} } ~~~

Let’s understand the O(n log n) solution:

  1. We first create a prefix sum array where prefixSum[i] represents the sum of all elements from index 0 to i-1.
  2. For each possible starting position, we use binary search to find the smallest ending position that gives us a sum ≥ target.
  3. The binary search helps us find this ending position in O(log n) time.
  4. Since we do this for each starting position (n times) and each binary search takes O(log n), the total time complexity is O(n log n).

The trade-offs between these solutions:
- The O(n) solution is more space-efficient (O(1) space) and generally faster in practice.
- The O(n log n) solution uses O(n) space for the prefix sum array but might be useful in scenarios where we need to handle multiple queries on the same array.

The sliding window solution is generally preferred because:
1. It’s more intuitive and easier to understand
2. It uses less memory
3. It has better time complexity
4. It processes the array in a single pass

Let me help you understand the time complexity of the sliding window solution step by step.

First, let’s look at the key parts of the algorithm:

```java
for (int end = 0; end < nums.length; end++) {
currentSum += nums[end];

while (currentSum >= target) {
    minLength = Math.min(minLength, end - start + 1);
    currentSum -= nums[start];
    start++;
} } ~~~

At first glance, you might wonder if this is O(n²) because we have a while loop inside a for loop. However, it’s actually O(n), and here’s why:

Think about the movement of our two pointers, start and end. The end pointer iterates through the array once from left to right, and the start pointer also moves from left to right. Each element in the array will be:
1. Added to the sum exactly once (when end reaches it)
2. Removed from the sum at most once (when start passes it)

So even though we have a nested while loop, the start pointer never moves backwards and can only move forward n positions in total across all iterations of the outer loop. Each array element is processed at most twice - once when it enters the window (through the end pointer) and once when it exits the window (through the start pointer).

The while loop doesn’t run n times for each iteration of the for loop, which is a common misconception. Let me explain this with a concrete example to make it clearer.

Consider this array: [2, 3, 1, 2, 4, 3] with target = 7

Let’s track how the pointers move:

Step 1: [2] 3 1 2 4 3
start = 0, end = 0, sum = 2 (while loop doesn't run as 2 < 7)

Step 2: [2 3] 1 2 4 3
start = 0, end = 1, sum = 5 (while loop doesn't run as 5 < 7)

Step 3: [2 3 1] 2 4 3
start = 0, end = 2, sum = 6 (while loop doesn't run as 6 < 7)

Step 4: [2 3 1 2] 4 3
start = 0, end = 3, sum = 8 
while loop runs ONCE:
- removes 2 (start = 1)
- new sum = 6, exits while loop as 6 < 7

Step 5: [3 1 2 4] 3
start = 1, end = 4, sum = 10
while loop runs TWICE:
- removes 3 (start = 2)
- removes 1 (start = 3)
- new sum = 6, exits while loop

Step 6: [1 2 4 3]
start = 2, end = 5, sum = 9
while loop runs ONCE:
- removes 1 (start = 3)
- new sum = 8, still ≥ 7
- removes 2 (start = 4)
- new sum = 6, exits while loop

As you can see, even though we have a while loop inside a for loop, the start pointer only moves forward a total of 4 times throughout the entire process. It never processes the same element more than once. The total number of while loop iterations across the entire algorithm equals the number of times the start pointer moves, which cannot exceed n.

This is why we say the time complexity is O(n) - each element is added to the sum once (in the for loop) and removed at most once (in the while loop), giving us a total of at most 2n operations, which simplifies to O(n).

30
Q

3. Longest Substring Without Repeating Characters, solve in O(n) time an

Given a string s, find the length of the longest
substring
without repeating characters.

Example 1:

Input: s = “abcabcbb”
Output: 3
Explanation: The answer is “abc”, with the length of 3.
Example 2:

Input: s = “bbbbb”
Output: 1
Explanation: The answer is “b”, with the length of 1.
Example 3:

Input: s = “pwwkew”
Output: 3
Explanation: The answer is “wke”, with the length of 3.
Notice that the answer must be a substring, “pwke” is a subsequence and not a substring.

Constraints:

0 <= s.length <= 5 * 104
s consists of English letters, digits, symbols and spaces.

A

I’ll help you solve this problem step by step. Let’s first understand what we’re trying to do and then develop an efficient solution.

The problem asks us to find the longest substring (continuous sequence of characters) where no character repeats. Let’s solve this using the sliding window technique, which is particularly effective for substring problems.

Here’s a detailed solution with explanations:

```java
class Solution {
public int lengthOfLongestSubstring(String s) {
// If the string is empty, return 0
if (s == null || s.length() == 0) {
return 0;
}

    // Create a map to store character -> its most recent position
    // We use Integer array instead of HashMap for better performance
    // since we know the input consists of ASCII characters
    Integer[] charIndex = new Integer[128];
    
    int maxLength = 0;    // Track the maximum length found so far
    int start = 0;        // Start of current window
    
    // Iterate through each character in the string
    for (int end = 0; end < s.length(); end++) {
        char currentChar = s.charAt(end);
        
        // If we've seen this character before and it's in our current window,
        // move the start pointer to the position after its last occurrence
        if (charIndex[currentChar] != null && charIndex[currentChar] >= start) {
            start = charIndex[currentChar] + 1;
        }
        
        // Update the last seen position of current character
        charIndex[currentChar] = end;
        
        // Update maxLength if current window is longer
        maxLength = Math.max(maxLength, end - start + 1);
    }
    
    return maxLength;
} } ~~~

Let’s break down how this solution works:

  1. First, we handle the edge case where the input string is null or empty.
  2. Instead of using a HashMap, we use an Integer array of size 128 to store character positions. This works because the input consists of ASCII characters, and using an array is more efficient than a HashMap.
  3. We maintain two pointers:
    • start: marks the beginning of our current window
    • end: marks the end of our current window (the character we’re currently processing)
  4. As we iterate through the string:
    • For each character, we check if we’ve seen it before and if it’s within our current window
    • If yes, we move the start pointer to the position after the last occurrence of the current character
    • We update the last seen position of the current character
    • We calculate the current window size (end - start + 1) and update maxLength if necessary

Let’s see how it works with the example “abcabcbb”:

Step by step for "abcabcbb":
1. a -> window: [a], maxLength = 1
2. b -> window: [a,b], maxLength = 2
3. c -> window: [a,b,c], maxLength = 3
4. a -> window: [b,c,a], maxLength = 3 (start moves to after previous 'a')
5. b -> window: [c,a,b], maxLength = 3 (start moves to after previous 'b')
6. c -> window: [a,b,c], maxLength = 3 (start moves to after previous 'c')
7. b -> window: [c,b], maxLength = 3 (start moves to after previous 'b')
8. b -> window: [b], maxLength = 3 (start moves to after previous 'b')

The time complexity is O(n) where n is the length of the string, as we only need to traverse the string once. The space complexity is O(1) since we’re using a fixed-size array for character positions.

This solution is efficient both in terms of time and space complexity, and it handles all the given test cases correctly.

31
Q

36. Valid Sudoku, solve in O(1) time and O(1) space

Determine if a 9 x 9 Sudoku board is valid. Only the filled cells need to be validated according to the following rules:

Each row must contain the digits 1-9 without repetition.
Each column must contain the digits 1-9 without repetition.
Each of the nine 3 x 3 sub-boxes of the grid must contain the digits 1-9 without repetition.
Note:

A Sudoku board (partially filled) could be valid but is not necessarily solvable.
Only the filled cells need to be validated according to the mentioned rules.

Example 1:

Input: board =
[[“5”,”3”,”.”,”.”,”7”,”.”,”.”,”.”,”.”]
,[“6”,”.”,”.”,”1”,”9”,”5”,”.”,”.”,”.”]
,[”.”,”9”,”8”,”.”,”.”,”.”,”.”,”6”,”.”]
,[“8”,”.”,”.”,”.”,”6”,”.”,”.”,”.”,”3”]
,[“4”,”.”,”.”,”8”,”.”,”3”,”.”,”.”,”1”]
,[“7”,”.”,”.”,”.”,”2”,”.”,”.”,”.”,”6”]
,[”.”,”6”,”.”,”.”,”.”,”.”,”2”,”8”,”.”]
,[”.”,”.”,”.”,”4”,”1”,”9”,”.”,”.”,”5”]
,[”.”,”.”,”.”,”.”,”8”,”.”,”.”,”7”,”9”]]
Output: true
Example 2:

Input: board =
[[“8”,”3”,”.”,”.”,”7”,”.”,”.”,”.”,”.”]
,[“6”,”.”,”.”,”1”,”9”,”5”,”.”,”.”,”.”]
,[”.”,”9”,”8”,”.”,”.”,”.”,”.”,”6”,”.”]
,[“8”,”.”,”.”,”.”,”6”,”.”,”.”,”.”,”3”]
,[“4”,”.”,”.”,”8”,”.”,”3”,”.”,”.”,”1”]
,[“7”,”.”,”.”,”.”,”2”,”.”,”.”,”.”,”6”]
,[”.”,”6”,”.”,”.”,”.”,”.”,”2”,”8”,”.”]
,[”.”,”.”,”.”,”4”,”1”,”9”,”.”,”.”,”5”]
,[”.”,”.”,”.”,”.”,”8”,”.”,”.”,”7”,”9”]]
Output: false
Explanation: Same as Example 1, except with the 5 in the top left corner being modified to 8. Since there are two 8’s in the top left 3x3 sub-box, it is invalid.

Constraints:

board.length == 9
board[i].length == 9
board[i][j] is a digit 1-9 or ‘.’.

A

```java
class Solution {
public boolean isValidSudoku(char[][] board) {
// Initialize three 2D arrays to track numbers in rows, columns, and boxes
// The first dimension represents the row/column/box number (0-8)
// The second dimension represents the actual digit (1-9) where we store count
boolean[][] rows = new boolean[9][9];
boolean[][] cols = new boolean[9][9];
boolean[][] boxes = new boolean[9][9];

    // Iterate through each cell in the 9x9 board
    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            // Skip empty cells
            if (board[i][j] == '.') {
                continue;
            }
            
            // Convert char to array index (0-8)
            int num = board[i][j] - '1';
            
            // Calculate which 3x3 box we're in (0-8)
            // Formula: (row / 3) * 3 + (col / 3)
            int boxIndex = (i / 3) * 3 + j / 3;
            
            // Check if number already exists in current row, column, or box
            if (rows[i][num] || cols[j][num] || boxes[boxIndex][num]) {
                return false;
            }
            
            // Mark the number as seen in current row, column, and box
            rows[i][num] = true;
            cols[j][num] = true;
            boxes[boxIndex][num] = true;
        }
    }
    
    // If we've made it through all checks, the board is valid
    return true;
} }
Let me explain how this solution works:

1. **Data Structure Choice**: 
   - We use three 2D boolean arrays to track the presence of digits in each row, column, and 3x3 box.
   - Using boolean arrays instead of HashSets gives us O(1) access and uses less memory.

2. **Board Traversal**:
   - We make a single pass through the board, checking each cell once.
   - For each non-empty cell, we validate three rules simultaneously.

3. **Box Index Calculation**:
   - The formula `(i / 3) * 3 + j / 3` cleverly maps each cell to its corresponding 3x3 box number (0-8).
   - For example:
     - Top-left box: (0,0) → 0
     - Top-middle box: (0,3) → 1
     - Center box: (4,4) → 4

4. **Number Conversion**:
   - We convert the character digit to a 0-based index using `board[i][j] - '1'`
   - This maps '1' to 0, '2' to 1, etc., making array indexing simpler

5. **Validation Process**:
   - For each filled cell, we check if the number already exists in:
     - The current row
     - The current column
     - The current 3x3 box
   - If it exists in any of these, we return false immediately

Time Complexity: O(1) since we're always dealing with a 9x9 board
Space Complexity: O(1) since we use fixed-size arrays

I'll explain the box index calculation in detail, walking through how we map any position in a 9x9 Sudoku grid to its corresponding 3x3 box.

Let's start by visualizing the box numbering. Imagine we've divided our 9x9 grid into nine 3x3 boxes, numbered like this:

Box 0 | Box 1 | Box 2
Box 3 | Box 4 | Box 5
Box 6 | Box 7 | Box 8
~~~

The formula we use is: boxIndex = (row / 3) * 3 + (column / 3)

Let’s break this down into parts:

  1. First, row / 3 tells us which “row of boxes” we’re in:
    • For rows 0-2: 0/3 = 0 (first row of boxes)
    • For rows 3-5: 3/3 = 1 (second row of boxes)
    • For rows 6-8: 6/3 = 2 (third row of boxes)
  2. We multiply this by 3 (row / 3) * 3 to get the starting box number for that row:
    • First row of boxes: 0 * 3 = 0
    • Second row of boxes: 1 * 3 = 3
    • Third row of boxes: 2 * 3 = 6
  3. Finally, column / 3 tells us how many boxes to the right we need to move:
    • For columns 0-2: 0/3 = 0 (first box in row)
    • For columns 3-5: 3/3 = 1 (second box in row)
    • For columns 6-8: 6/3 = 2 (third box in row)

Let’s work through some examples:

  1. Position (4,4) - middle of the board:
    • row = 4, column = 4
    • (4 / 3) * 3 + (4 / 3)
    • (1) * 3 + (1)
    • 3 + 1 = 4
      So position (4,4) is in box 4 (the center box)
  2. Position (7,2) - bottom left area:
    • row = 7, column = 2
    • (7 / 3) * 3 + (2 / 3)
    • (2) * 3 + (0)
    • 6 + 0 = 6
      So position (7,2) is in box 6 (bottom left box)

The beauty of this formula is that it works for any position in the grid. When we use integer division (which Java does automatically with integers), the numbers naturally group into the correct boxes. The row / 3 part handles the vertical positioning, while the column / 3 part handles the horizontal positioning within that row of boxes.

32
Q

54. Spiral Matrix, solve in O(mn) time and O(1) space

Given an m x n matrix, return all elements of the matrix in spiral order.

Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [1,2,3,6,9,8,7,4,5]

Example 2:
Input: matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
Output: [1,2,3,4,8,12,11,10,9,5,6,7]

Constraints:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100

A

I’ll help you solve this problem of traversing a matrix in spiral order. Let’s break it down step by step and implement a solution in Java.

The key to solving this problem is to recognize that we need to traverse the matrix in four distinct directions: right, down, left, and up. We’ll keep track of our boundaries and adjust them as we move through the matrix.

Here’s an explained solution:

```java
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
// Initialize result list to store elements in spiral order
List<Integer> result = new ArrayList<>();</Integer></Integer>

    // Handle empty matrix case
    if (matrix == null || matrix.length == 0) {
        return result;
    }
    
    // Initialize boundary variables
    int rowStart = 0;                  // Top boundary
    int rowEnd = matrix.length - 1;    // Bottom boundary
    int colStart = 0;                  // Left boundary
    int colEnd = matrix[0].length - 1; // Right boundary
    
    // Continue traversing while boundaries haven't crossed
    while (rowStart <= rowEnd && colStart <= colEnd) {
        // Traverse right: move across top row
        for (int col = colStart; col <= colEnd; col++) {
            result.add(matrix[rowStart][col]);
        }
        rowStart++; // Move top boundary down
        
        // Traverse down: move along rightmost column
        for (int row = rowStart; row <= rowEnd; row++) {
            result.add(matrix[row][colEnd]);
        }
        colEnd--; // Move right boundary left
        
        // Check if we still have rows to traverse
        if (rowStart <= rowEnd) {
            // Traverse left: move across bottom row
            for (int col = colEnd; col >= colStart; col--) {
                result.add(matrix[rowEnd][col]);
            }
            rowEnd--; // Move bottom boundary up
        }
        
        // Check if we still have columns to traverse
        if (colStart <= colEnd) {
            // Traverse up: move along leftmost column
            for (int row = rowEnd; row >= rowStart; row--) {
                result.add(matrix[row][colStart]);
            }
            colStart++; // Move left boundary right
        }
    }
    
    return result;
} } ~~~

Let’s break down how this solution works:

  1. First, we create a list to store our result and handle the edge case of an empty matrix.
  2. We maintain four boundary variables:
    • rowStart: represents the topmost unprocessed row
    • rowEnd: represents the bottommost unprocessed row
    • colStart: represents the leftmost unprocessed column
    • colEnd: represents the rightmost unprocessed column
  3. The main logic occurs in a while loop that continues as long as we have valid boundaries (i.e., rowStart ≤ rowEnd and colStart ≤ colEnd).
  4. In each iteration, we perform four traversals:
    • Right: traverse the top row from colStart to colEnd
    • Down: traverse the rightmost column from rowStart to rowEnd
    • Left: traverse the bottom row from colEnd to colStart
    • Up: traverse the leftmost column from rowEnd to rowStart
  5. After each traversal, we update the corresponding boundary:
    • After going right, we increment rowStart
    • After going down, we decrement colEnd
    • After going left, we decrement rowEnd
    • After going up, we increment colStart
  6. The important part is the additional checks (if (rowStart <= rowEnd) and if (colStart <= colEnd)) before the left and up traversals. These prevent duplicate processing of elements when we have a matrix with unequal dimensions.

For the example input [[1,2,3],[4,5,6],[7,8,9]], the algorithm would:
1. First traverse right: [1,2,3]
2. Then down: [6,9]
3. Then left: [8,7]
4. Finally up: [4]
5. And lastly the center: [5]

The time complexity is O(m×n) where m and n are the dimensions of the matrix, as we visit each element exactly once. The space complexity is O(1) if we don’t count the space needed for the output array.

Would you like me to explain any part of this solution in more detail?

33
Q

48. Rotate Image, solve in O(n2) time and O(n2) space

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).

You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]]

Example 2:
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

Constraints:
n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000

A

Let me help you understand this solution step by step. However, I notice that this solution actually doesn’t follow the problem’s requirement of rotating the matrix in-place, as it creates a new matrix called result. Let me first explain how this solution works, and then I’ll suggest a better in-place solution.

Let’s analyze the given solution:

```java
public void rotate(int[][] matrix) {
// Create a new matrix of the same size
int[][] result = new int[matrix.length][matrix.length];

// first keeps track of rows from bottom to top
int first = matrix.length - 1;
// second keeps track of columns from left to right
int second = 0;

for (int i = 0; i < matrix.length; i++) {
    for (int j = 0; j < matrix.length; j++) {
        if (first >= 0 && second <= matrix.length) {
            // Copy elements from original matrix to result matrix
            result[i][j] = matrix[first][second];
            first--;
        }
    }
    // Reset first to bottom row and move second to next column
    first = matrix.length - 1;
    second++;
}

// Copy back the rotated matrix to original matrix
for (int i = 0; i < result.length; i++) {
    for (int j = 0; j < result.length; j++) {
        matrix[i][j] = result[i][j];
    }
} } ~~~

The key idea here is that when rotating a matrix 90 degrees clockwise:
- The first row becomes the last column
- The second row becomes the second-to-last column
- And so on…

For example, with matrix:
~~~
1 2 3
4 5 6
7 8 9
~~~

The transformation works like this:
- 7 (bottom-left) goes to top-left
- 4 (middle-left) goes to top-middle
- 1 (top-left) goes to top-right
And so on…

However, this solution has two main issues:
1. It uses extra space (O(n²)) by creating a new matrix
2. It doesn’t follow the problem requirement of modifying the matrix in-place

34
Q

48. Rotate Image, solve in O(n2) time and O(1) space

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).

You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

Example 1:
Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [[7,4,1],[8,5,2],[9,6,3]]

Example 2:
Input: matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
Output: [[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

Constraints:
n == matrix.length == matrix[i].length
1 <= n <= 20
-1000 <= matrix[i][j] <= 1000

A

Here’s a better in-place solution:

```java
public void rotate(int[][] matrix) {
int n = matrix.length;

// Step 1: Transpose the matrix
for (int i = 0; i < n; i++) {
    for (int j = i; j < n; j++) {
        // Swap matrix[i][j] with matrix[j][i]
        int temp = matrix[i][j];
        matrix[i][j] = matrix[j][i];
        matrix[j][i] = temp;
    }
}

// Step 2: Reverse each row
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n/2; j++) {
        // Swap matrix[i][j] with matrix[i][n-1-j]
        int temp = matrix[i][j];
        matrix[i][j] = matrix[i][n-1-j];
        matrix[i][n-1-j] = temp;
    }
} } ~~~

This improved solution rotates the matrix in two steps:
1. Transpose the matrix (swap elements across the main diagonal)
2. Reverse each row

For example:
~~~
1 2 3 1 4 7 7 4 1
4 5 6 -> 2 5 8 -> 8 5 2
7 8 9 3 6 9 9 6 3
~~~

This approach:
- Uses O(1) extra space
- Modifies the matrix in-place
- Has O(n²) time complexity
- Is more elegant and efficient than the original solution

35
Q

73. Set Matrix Zeroes, solve in O(n2) time and O(n + m) spacee

Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0’s.

You must do it in place.

Example 1:
Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
Output: [[1,0,1],[0,0,0],[1,0,1]]

Example 2:
Input: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
Output: [[0,0,0,0],[0,4,5,0],[0,3,1,0]]

Constraints:
m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-231 <= matrix[i][j] <= 231 - 1

A
class Solution {
    public void setZeroes(int[][] matrix) {
       int n = matrix.length;
       int m = matrix[0].length;
       int[] colZeroIndexes = new int[m];
       int[] rowZeroIndexes = new int[n];

       for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (matrix[i][j] == 0) {
                    colZeroIndexes[j] = 1;
                    rowZeroIndexes[i] = 1;
                }
            }
       }

       for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (colZeroIndexes[j] == 1) {
                    matrix[i][j] = 0;
                }

                if (rowZeroIndexes[i] == 1) {
                    matrix[i][j] = 0;
                }
            }

            
       }
    }
}

Let me break down how this solution works step by step:

  1. Setup and Initialization:
    ~~~
    int n = matrix.length; // number of rows
    int m = matrix[0].length; // number of columns
    int[] colZeroIndexes = new int[m]; // tracks which columns need to be zeroed
    int[] rowZeroIndexes = new int[n]; // tracks which rows need to be zeroed
    ~~~
  2. First Pass - Finding Zeros:
    ~~~
    for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
    if (matrix[i][j] == 0) {
    colZeroIndexes[j] = 1; // mark this column for zeroing
    rowZeroIndexes[i] = 1; // mark this row for zeroing
    }
    }
    }
    ~~~
    - This loop identifies all original zeros in the matrix
    - When it finds a zero, it marks both its row and column for later zeroing
    - Uses separate arrays to keep track of which rows and columns need to be zeroed
  3. Second Pass - Setting Zeros:
    ~~~
    for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
    if (colZeroIndexes[j] == 1 || rowZeroIndexes[i] == 1) {
    matrix[i][j] = 0;
    }
    }
    }
    ~~~
    - Goes through the matrix again
    - Sets element to zero if either its row or column was marked

Why this approach works:
1. It avoids the problem of modifying the matrix while we’re still scanning for zeros
2. It uses O(m + n) extra space to store which rows and columns need to be zeroed
3. It preserves the original state of the matrix during the identification phase

Example walkthrough with matrix = [[1,1,1],[1,0,1],[1,1,1]]:

  1. First pass:
    • Finds zero at position (1,1)
    • Sets rowZeroIndexes[1] = 1
    • Sets colZeroIndexes[1] = 1
  2. Second pass:
    • Any element in row 1 becomes 0
    • Any element in column 1 becomes 0
    • Result: [[1,0,1],[0,0,0],[1,0,1]]

This solution’s time complexity is O(m×n) and space complexity is O(m + n).

36
Q

73. Set Matrix Zeroes, solve in O(n2) time and O(1) space

Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0’s.

You must do it in place.

Example 1:
Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
Output: [[1,0,1],[0,0,0],[1,0,1]]

Example 2:
Input: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
Output: [[0,0,0,0],[0,4,5,0],[0,3,1,0]]

Constraints:
m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-231 <= matrix[i][j] <= 231 - 1

A

Ah, to solve this in constant space, we can use the first row and first column of the matrix itself as our markers instead of separate arrays. Here’s how we can do it:

```java
class Solution {
public void setZeroes(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;

    // Variable to track if first column needs to be zeroed
    boolean firstColHasZero = false;
    
    // Step 1: Check if first column has any zeros
    for (int i = 0; i < rows; i++) {
        if (matrix[i][0] == 0) {
            firstColHasZero = true;
            break;
        }
    }
    
    // Step 2: Use first row/col as markers and scan the rest of the matrix
    for (int i = 0; i < rows; i++) {
        for (int j = 1; j < cols; j++) {
            if (matrix[i][j] == 0) {
                matrix[i][0] = 0;  // mark the first cell of row
                matrix[0][j] = 0;  // mark the first cell of column
            }
        }
    }
    
    // Step 3: Use markers to zero out cells (skip first row and col)
    for (int i = rows-1; i >= 0; i--) {
        for (int j = cols-1; j >= 1; j--) {
            if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                matrix[i][j] = 0;
            }
        }
    }
    
    // Step 4: Check if first row needs to be zeroed
    if (matrix[0][0] == 0) {
        for (int j = 0; j < cols; j++) {
            matrix[0][j] = 0;
        }
    }
    
    // Step 5: Use tracked first column variable to zero out first column
    if (firstColHasZero) {
        for (int i = 0; i < rows; i++) {
            matrix[i][0] = 0;
        }
    }
} }
Let me break down this constant space solution:

1. **Key Idea**: Instead of using extra arrays, we use the first row and first column of the matrix itself to mark which rows and columns should be zeroed.

2. **Special Handling for First Column**:
   - We need a separate boolean `firstColHasZero` because the first column is used for marking rows
   - We check this first to preserve the information

3. **Marking Process**:
   - For any cell (i,j) that contains 0, we mark:
     - matrix[i][0] = 0 (first cell of that row)
     - matrix[0][j] = 0 (first cell of that column)
   - This preserves the information about which rows and columns need to be zeroed

4. **Processing Order**:
   - We process from bottom-right to top-left to avoid overwriting our markers
   - First row and first column are handled separately at the end
   - This ensures we don't lose information about which cells should be zeroed

5. **Time Complexity**: O(M×N)
   **Space Complexity**: O(1)

Let's see how it works with an example:

Initial matrix:
1 1 1
1 0 1
1 1 1

After marking (using first row/col):
1 0 1 <- first row used as marker for columns
1 0 1
1 1 1

first column used as marker for rows

Final result:
1 0 1
0 0 0
1 0 1
~~~

The key insight is that we’re using the matrix itself as storage space for our markers, which gets us down to constant space complexity. We just need to be careful about the order of operations to avoid overwriting information we still need.

37
Q

289. Game of Life, solve in O(mn) time and O(mn) space

According to Wikipedia’s article: “The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.”

The board is made up of an m x n grid of cells, where each cell has an initial state: live (represented by a 1) or dead (represented by a 0). Each cell interacts with its eight neighbors (horizontal, vertical, diagonal) using the following four rules (taken from the above Wikipedia article):

Any live cell with fewer than two live neighbors dies as if caused by under-population.
Any live cell with two or three live neighbors lives on to the next generation.
Any live cell with more than three live neighbors dies, as if by over-population.
Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
The next state of the board is determined by applying the above rules simultaneously to every cell in the current state of the m x n grid board. In this process, births and deaths occur simultaneously.

Given the current state of the board, update the board to reflect its next state.

Note that you do not need to return anything.

Example 1:
Input: board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
Output: [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
Example 2:
Input: board = [[1,1],[1,0]]
Output: [[1,1],[1,1]]

Constraints:
m == board.length
n == board[i].length
1 <= m, n <= 25
board[i][j] is 0 or 1.

A

The key challenge here is that we need to update all cells simultaneously, which means we can’t modify the original board directly while calculating the next state. Let’s write a solution with detailed explanations:

class Solution {
    public void gameOfLife(int[][] board) {
        // Get the dimensions of the board
        int rows = board.length;
        int cols = board[0].length;
        
        // Create a temporary board to store the next state
        // This helps us achieve simultaneous updates
        int[][] nextState = new int[rows][cols];
        
        // Iterate through each cell in the board
        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                // Count live neighbors for current cell
                int liveNeighbors = countLiveNeighbors(board, row, col);
                
                // Apply the rules of Game of Life
                if (board[row][col] == 1) {
                    // Rules 1, 2, and 3 for live cells
                    if (liveNeighbors < 2 || liveNeighbors > 3) {
                        // Cell dies due to under/over-population
                        nextState[row][col] = 0;
                    } else {
                        // Cell survives with 2 or 3 neighbors
                        nextState[row][col] = 1;
                    }
                } else {
                    // Rule 4 for dead cells
                    if (liveNeighbors == 3) {
                        // Cell becomes alive due to reproduction
                        nextState[row][col] = 1;
                    } else {
                        // Cell stays dead
                        nextState[row][col] = 0;
                    }
                }
            }
        }
        
        // Copy the next state back to the original board
        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                board[row][col] = nextState[row][col];
            }
        }
    }
    
    // Helper method to count live neighbors for a given cell
    private int countLiveNeighbors(int[][] board, int row, int col) {
        int count = 0;
        int rows = board.length;
        int cols = board[0].length;
        
        // Define the eight possible directions (including diagonals)
        int[][] directions = {
            {-1, -1}, {-1, 0}, {-1, 1},
            {0, -1},           {0, 1},
            {1, -1},  {1, 0},  {1, 1}
        };
        
        // Check each neighboring cell
        for (int[] dir : directions) {
            int newRow = row + dir[0];
            int newCol = col + dir[1];
            
            // Check if the neighbor is within bounds
            if (newRow >= 0 && newRow < rows && 
                newCol >= 0 && newCol < cols) {
                // Add 1 to count if neighbor is alive
                count += board[newRow][newCol];
            }
        }
        
        return count;
    }
}

Let’s break down how this solution works:

  1. Main Strategy: We use a temporary board (nextState) to store the next generation’s state while we calculate changes. This is crucial because all changes need to happen simultaneously.
  2. The gameOfLife Method:
    • First, we get the dimensions of the board
    • We create a temporary board of the same size
    • We iterate through each cell and apply the rules
    • Finally, we copy the next state back to the original board
  3. The countLiveNeighbors Method:
    • Uses a directions array to elegantly handle all eight possible neighbors
    • Checks bounds before counting each neighbor
    • Returns the total count of live neighbors
  4. Rules Implementation:
    • For live cells (value = 1):
      • Dies if fewer than 2 neighbors (underpopulation)
      • Lives on with 2 or 3 neighbors
      • Dies if more than 3 neighbors (overpopulation)
    • For dead cells (value = 0):
      • Becomes alive if exactly 3 neighbors
      • Stays dead otherwise

This solution has a time complexity of O(m×n) where m and n are the dimensions of the board. The space complexity is also O(m×n) due to the temporary board we create.

You could optimize the space complexity to O(1) by using bit manipulation (using different bits to store current and next states), but I’ve chosen this more readable approach as it makes the logic clearer and easier to understand.

38
Q

383. Ransom Note, solve in O(m + n) time and O(1) space

Given two strings ransomNote and magazine, return true if ransomNote can be constructed by using the letters from magazine and false otherwise.

Each letter in magazine can only be used once in ransomNote.

Example 1:
Input: ransomNote = “a”, magazine = “b”
Output: false

Example 2:
Input: ransomNote = “aa”, magazine = “ab”
Output: false

Example 3:
Input: ransomNote = “aa”, magazine = “aab”
Output: true

Constraints:
1 <= ransomNote.length, magazine.length <= 105
ransomNote and magazine consist of lowercase English letters.

A

Let me help you solve this problem step by step. We’ll create a solution that determines if we can construct a ransom note from the letters in a magazine.

Let’s think about the approach first:
1. We need to track the frequency of letters in the magazine - these are our available letters
2. Then we’ll check if we have enough of each letter to construct the ransom note
3. If at any point we don’t have enough of a letter, we return false
4. If we make it through all letters in the ransom note, we return true

Here’s a solution with detailed explanations:

```java
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
// Create an array to store character frequencies
// We use an array of size 26 since we only have lowercase letters
// Each index represents a letter (0 = ‘a’, 1 = ‘b’, etc.)
int[] letterFrequency = new int[26];

    // First, count all letters in the magazine
    // This builds our "inventory" of available letters
    for (char c : magazine.toCharArray()) {
        // Convert each character to its corresponding index (0-25)
        // by subtracting 'a' from it
        letterFrequency[c - 'a']++;
    }
    
    // Now check if we have enough letters for the ransom note
    for (char c : ransomNote.toCharArray()) {
        // Get the current count for this letter
        int index = c - 'a';
        
        // If we have no more of this letter available, we can't construct the note
        if (letterFrequency[index] <= 0) {
            return false;
        }
        
        // Use up one occurrence of this letter
        letterFrequency[index]--;
    }
    
    // If we made it through the entire ransom note, we have enough letters
    return true;
} } ~~~

Let’s break down how this solution works:

  1. We use an array of size 26 to store the frequency of each lowercase letter. This is more efficient than using a HashMap since we know we only have lowercase letters.
  2. The index in our array corresponds to each letter:
    • ‘a’ corresponds to index 0
    • ‘b’ corresponds to index 1
    • and so on…
  3. When we subtract ‘a’ from any lowercase letter, we get its corresponding index:
    • ‘a’ - ‘a’ = 0
    • ‘b’ - ‘a’ = 1
    • ‘z’ - ‘a’ = 25
  4. Time Complexity: O(m + n) where:
    • m is the length of magazine
    • n is the length of ransomNote
    • We need one pass through the magazine to count letters
    • One pass through the ransom note to check availability
  5. Space Complexity: O(1) because:
    • We use a fixed-size array of 26 elements
    • The space used doesn’t grow with input size
39
Q

205. Isomorphic Strings, solve in O(n) time and O(k) space

Given two strings s and t, determine if they are isomorphic.

Two strings s and t are isomorphic if the characters in s can be replaced to get t.

All occurrences of a character must be replaced with another character while preserving the order of characters. No two characters may map to the same character, but a character may map to itself.

Example 1:
Input: s = “egg”, t = “add”
Output: true

Explanation:
The strings s and t can be made identical by:
Mapping ‘e’ to ‘a’.
Mapping ‘g’ to ‘d’.
Example 2:

Input: s = “foo”, t = “bar”
Output: false

Explanation:
The strings s and t can not be made identical as ‘o’ needs to be mapped to both ‘a’ and ‘r’.

Example 3:
Input: s = “paper”, t = “title”
Output: true

Constraints:
1 <= s.length <= 5 * 104
t.length == s.length
s and t consist of any valid ascii character.

A
class Solution {
    public boolean isIsomorphic(String s, String t) {
        Map<String, String> map1 = new HashMap<>();
        Map<String, String> map2 = new HashMap<>();

        int i = 0;
        while (i < s.length()) {
            String first = String.valueOf(s.charAt(i));
            String second = String.valueOf(t.charAt(i));

            if (!map1.containsKey(first)) {
                map1.put(first, second);
            } else {
                if (!map1.get(first).equals(second)) {
                    return false;
                }
            }

            if (!map2.containsKey(second)) {
                map2.put(second, first);
            } else {
                if (!map2.get(second).equals(first)) {
                    return false;
                }
            }
            i++; 
        } 

        return true;
    }
}

Let me break down how this solution works by explaining the core concept and then walking through the implementation step by step.

The core concept behind checking if two strings are isomorphic is ensuring there’s a consistent one-to-one mapping between characters. This means each character in string s must map to exactly one unique character in string t, and vice versa. The solution uses two HashMaps to maintain and verify these mappings in both directions.

Let’s analyze the solution step by step:

  1. First, we create two HashMaps:
    ```java
    Map<String, String> map1 = new HashMap<>(); // Maps characters from s to t
    Map<String, String> map2 = new HashMap<>(); // Maps characters from t to s
    ~~~
    These maps serve different purposes:
    - map1 tracks how characters from string s map to characters in string t
    - map2 tracks how characters from string t map to characters in string s
    - Having both maps ensures we maintain a true one-to-one relationship
  2. The solution then iterates through both strings simultaneously:
    ```java
    int i = 0;
    while (i < s.length()) {
    String first = String.valueOf(s.charAt(i));
    String second = String.valueOf(t.charAt(i));
    ~~~
    We convert each character to a String to work with them more easily in the HashMap.
  3. For each character pair, we perform two checks:

First check (s → t mapping):
```java
if (!map1.containsKey(first)) {
map1.put(first, second);
} else {
if (!map1.get(first).equals(second)) {
return false;
}
}
~~~
This checks if we’ve seen the character from s before:
- If we haven’t (first time seeing it), we create a new mapping
- If we have, we verify it maps to the same character in t as before

Second check (t → s mapping):
```java
if (!map2.containsKey(second)) {
map2.put(second, first);
} else {
if (!map2.get(second).equals(first)) {
return false;
}
}
~~~
This performs the same check in reverse, ensuring characters from t consistently map back to characters in s.

Let’s see this in action with the example “egg” and “add”:

First iteration (i=0):
- first = “e”, second = “a”
- map1: {“e” → “a”}
- map2: {“a” → “e”}

Second iteration (i=1):
- first = “g”, second = “d”
- map1: {“e” → “a”, “g” → “d”}
- map2: {“a” → “e”, “d” → “g”}

Third iteration (i=2):
- first = “g”, second = “d”
- Checks map1: “g” should map to “d” ✓
- Checks map2: “d” should map to “g” ✓

The solution returns true because all mappings remain consistent.

For the counter-example “foo” and “bar”:
The second “o” in “foo” would try to map to “r” while map1 already has “o” mapping to “a”, causing the function to return false.

This approach ensures both the preservation of character order and the one-to-one mapping requirement of isomorphic strings. The time complexity is O(n) where n is the length of the strings, and the space complexity is O(k) where k is the number of unique characters in the strings.

40
Q

290. Word Pattern, solve in O(N) time and O(k) space

Given a pattern and a string s, find if s follows the same pattern.

Here follow means a full match, such that there is a bijection between a letter in pattern and a non-empty word in s. Specifically:

Each letter in pattern maps to exactly one unique word in s.
Each unique word in s maps to exactly one letter in pattern.
No two letters map to the same word, and no two words map to the same letter.

Example 1:
Input: pattern = “abba”, s = “dog cat cat dog”
Output: true

Explanation:
The bijection can be established as:
‘a’ maps to “dog”.
‘b’ maps to “cat”.

Example 2:
Input: pattern = “abba”, s = “dog cat cat fish”
Output: false

Example 3:
Input: pattern = “aaaa”, s = “dog cat cat dog”
Output: false

Constraints:
1 <= pattern.length <= 300
pattern contains only lower-case English letters.
1 <= s.length <= 3000
s contains only lowercase English letters and spaces ‘ ‘.
s does not contain any leading or trailing spaces.
All the words in s are separated by a single space.

A
class Solution {
    public boolean wordPattern(String pattern, String s) {
       String[] words = s.split(" ");
       Map<String, String> map = new HashMap<>();

       if (words.length != pattern.length()) {
            return false;
       }

       int i = 0; 
       while(i < pattern.length()) {
            String letter = String.valueOf(pattern.charAt(i));
            if (!map.containsKey(letter)) {
                if (map.containsValue(words[i])) {
                    return false;
                }
                map.put(letter, words[i]);
            } else {
                if (!map.get(letter).equals(words[i])) {
                    return false;
                }

            }

            i++;
       }

       return true;
    }
}

Let me walk you through how this solution works step by step. The problem requires us to verify if a pattern matches a string of words with specific bijection rules, meaning each pattern letter must map to exactly one unique word and vice versa.

First, let’s look at the initial setup:
```java
String[] words = s.split(“ “);
Map<String, String> map = new HashMap<>();
~~~
The solution begins by splitting the input string s into an array of words using spaces as delimiters. It also creates a HashMap that will store the mapping between pattern letters and words.

Then comes an important validation:
```java
if (words.length != pattern.length()) {
return false;
}
~~~
This check ensures that the number of words matches the length of the pattern. For example, if the pattern is “abba” but we have only three words, it’s impossible to have a valid mapping.

The main logic happens in the while loop that processes each character of the pattern alongside its corresponding word:
```java
while(i < pattern.length()) {
String letter = String.valueOf(pattern.charAt(i));
// … rest of the logic
}
~~~

For each pattern letter, the code handles two possible scenarios:

Scenario 1 - When we encounter a new pattern letter:
```java
if (!map.containsKey(letter)) {
if (map.containsValue(words[i])) {
return false;
}
map.put(letter, words[i]);
}
~~~
If we haven’t seen this pattern letter before, we first check if its corresponding word is already mapped to some other letter (using containsValue). This prevents violations of the bijection rule where two different letters map to the same word. For example, in pattern “aba” with words “cat dog cat”, when we reach the second ‘a’, we wouldn’t want to map it to “dog” since ‘b’ is already mapped to “dog”.

Scenario 2 - When we encounter a pattern letter we’ve seen before:
```java
else {
if (!map.get(letter).equals(words[i])) {
return false;
}
}
~~~
If we have seen this pattern letter before, we verify that the current word matches what we previously mapped this letter to. For example, if ‘a’ was mapped to “dog”, every subsequent ‘a’ must also correspond to “dog”.

Let’s see this in action with the example “abba” and “dog cat cat dog”:
1. First ‘a’: Maps ‘a’ → “dog”
2. First ‘b’: Maps ‘b’ → “cat”
3. Second ‘b’: Verifies that the word “cat” matches the existing mapping for ‘b’
4. Second ‘a’: Verifies that the word “dog” matches the existing mapping for ‘a’

The solution returns true only if all these checks pass, meaning we’ve found a valid bijection between pattern letters and words. This approach efficiently handles all the constraints while maintaining both the uniqueness of mappings and the consistency of existing mappings throughout the string.

41
Q

242. Valid Anagram, solve in O(n + m) time and O(n + m) space.

Given two strings s and t, return true if t is an
anagram of s, and false otherwise.

Example 1:
Input: s = “anagram”, t = “nagaram”
Output: true

Example 2:
Input: s = “rat”, t = “car”
Output: false

Constraints:
1 <= s.length, t.length <= 5 * 104
s and t consist of lowercase English letters.

Follow up: What if the inputs contain Unicode characters? How would you adapt your solution to such a case?

A
class Solution {
    public boolean isAnagram(String s, String t) {
        Map<Character, Integer> map = new HashMap<>();

        for (char ch : s.toCharArray()) {
            map.put(ch, map.getOrDefault(ch, 0) + 1);
        }

        for (char ch : t.toCharArray()) {
            map.put(ch, map.getOrDefault(ch, 0) - 1);
        }

        for (int val : map.values()) {
            if (val != 0) {
                return false;
            }
        }

        return true;
    }
}

The given solution uses a HashMap-based approach to determine if two strings are anagrams. Let’s understand what anagrams are first: two strings are anagrams if they contain exactly the same characters with the same frequencies, just in a different order.

Here’s how the solution works:

Map<Character, Integer> map = new HashMap<>();

First, we create a HashMap where:
- The key is a character (Character)
- The value is the frequency of that character (Integer)

for (char ch : s.toCharArray()) {
    map.put(ch, map.getOrDefault(ch, 0) + 1);
}

In this first pass through string s, we:
1. Convert the string to a character array and iterate through each character
2. For each character, we increment its count in the map
3. The getOrDefault() method returns the current count of the character if it exists, or 0 if it doesn’t
4. We add 1 to this count and store it back in the map

For example, if s = “anagram”:
- After first ‘a’: map = {‘a’ => 1}
- After ‘n’: map = {‘a’ => 1, ‘n’ => 1}
- After second ‘a’: map = {‘a’ => 2, ‘n’ => 1}
And so on…

for (char ch : t.toCharArray()) {
    map.put(ch, map.getOrDefault(ch, 0) - 1);
}

In this second pass through string t, we:
1. Iterate through each character in t
2. For each character, we decrement its count in the map
3. If the character doesn’t exist in the map, we start from 0 and decrement to -1

The clever part is that if t is truly an anagram of s:
- Characters present in both strings will end up with a count of 0
- Characters present in only one string will have non-zero counts

for (int val : map.values()) {
    if (val != 0) {
        return false;
    }
}
return true;

Finally, we check all values in the map:
- If any value is not 0, it means some character appeared more times in one string than the other
- If all values are 0, the strings are anagrams

Regarding the follow-up question about Unicode characters: This solution actually handles Unicode characters naturally because:
1. Java’s char type is 16-bit and can handle Unicode characters
2. The HashMap can use any character as a key, regardless of whether it’s ASCII or Unicode
3. The counting logic remains the same regardless of character type

The time complexity is O(n) where n is the length of the strings, as we make three passes: one through s, one through t, and one through the map values. The space complexity is O(k) where k is the number of unique characters in the strings.

42
Q

49. Group Anagrams, solve in O(nklogk) time and O(n*K) space

Given an array of strings strs, group the
anagrams together. You can return the answer in any order.

Example 1:
Input: strs = [“eat”,”tea”,”tan”,”ate”,”nat”,”bat”]
Output: [[“bat”],[“nat”,”tan”],[“ate”,”eat”,”tea”]]

Explanation:
There is no string in strs that can be rearranged to form “bat”.
The strings “nat” and “tan” are anagrams as they can be rearranged to form each other.
The strings “ate”, “eat”, and “tea” are anagrams as they can be rearranged to form each other.

Example 2:
Input: strs = [””]
Output: [[””]]

Example 3:
Input: strs = [“a”]
Output: [[“a”]]

Constraints:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i] consists of lowercase English letters.

A

The key insight is that two strings are anagrams if they have the same characters in the same frequency, just in a different order. We can use this to create a “signature” for each string that will be identical for all anagrams.

Here’s a solution with detailed explanations:

import java.util.*;

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // Create a HashMap to store groups of anagrams
        // Key: the sorted version of the string (our "signature")
        // Value: list of original strings that share this signature
        Map<String, List<String>> anagramGroups = new HashMap<>();
        
        // Process each string in the input array
        for (String str : strs) {
            // Convert the string to a char array so we can sort it
            char[] chars = str.toCharArray();
            // Sort the characters to create our signature
            Arrays.sort(chars);
            // Convert back to string to use as our HashMap key
            String sortedStr = new String(chars);
            
            // If we haven't seen this signature before, create a new list
            if (!anagramGroups.containsKey(sortedStr)) {
                anagramGroups.put(sortedStr, new ArrayList<>());
            }
            
            // Add the original string to its group
            anagramGroups.get(sortedStr).add(str);
        }
        
        // Convert the HashMap values to our result list
        return new ArrayList<>(anagramGroups.values());
    }
}

Let’s walk through how this solution works:

  1. First, we create a HashMap where:
    • The key is a sorted version of each string (our “signature”)
    • The value is a list of all original strings that share that signature
  2. For each string in our input array:
    • We convert it to a char array and sort it
    • This sorted version becomes our key
    • For example, both “eat” and “tea” will produce “aet” as their key
  3. For each string we process:
    • If we haven’t seen its signature before, we create a new ArrayList
    • We add the original string to the list associated with its signature
  4. Finally, we return all the values from our HashMap as a list of lists

Time Complexity: O(n * k * log k), where:
- n is the number of strings in the input array
- k is the maximum length of any string
- The log k factor comes from sorting each string

Space Complexity: O(n * k) to store all the strings in our HashMap

Let’s see how it processes Example 1:
~~~
Input: [“eat”,”tea”,”tan”,”ate”,”nat”,”bat”]

Processing:
“eat” -> sorted “aet” -> creates new list -> [“eat”]
“tea” -> sorted “aet” -> adds to existing -> [“eat”,”tea”]
“tan” -> sorted “ant” -> creates new list -> [“tan”]
“ate” -> sorted “aet” -> adds to existing -> [“eat”,”tea”,”ate”]
“nat” -> sorted “ant” -> adds to existing -> [“tan”,”nat”]
“bat” -> sorted “abt” -> creates new list -> [“bat”]

Output: [[“bat”],[“nat”,”tan”],[“ate”,”eat”,”tea”]]
~~~

This solution handles all the given test cases, including edge cases like empty strings and single characters, while meeting all the constraints.

43
Q

1. Two Sum, solve in O(n) time and O(N) space

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

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

Example 1:

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].
Example 2:

Input: nums = [3,2,4], target = 6
Output: [1,2]
Example 3:

Input: nums = [3,3], target = 6
Output: [0,1]

Constraints:

2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
Only one valid answer exists.

A

I’ll help you solve this problem using a HashMap approach in Java. Let’s break down the solution step by step.

public class Solution {
    public int[] twoSum(int[] nums, int target) {
        // Create a HashMap to store complements
        // Key: number from array, Value: its index
        HashMap<Integer, Integer> complementMap = new HashMap<>();
        
        // Iterate through the array once
        for (int i = 0; i < nums.length; i++) {
            // Calculate what number we need to find (complement)
            int complement = target - nums[i];
            
            // Check if we've already seen the complement before
            if (complementMap.containsKey(complement)) {
                // If found, return current index and complement's index
                return new int[] {complementMap.get(complement), i};
            }
            
            // If complement not found, store current number and its index
            complementMap.put(nums[i], i);
        }
        
        // No solution found (though problem guarantees one exists)
        return new int[] {};
    }
}

Let me explain how this solution works and why it’s efficient:

The key insight is that for any number x in our array, we’re looking for another number y such that x + y = target. This means y = target - x, which we call the “complement” of x.

Instead of using nested loops (which would give us O(n²) time complexity), we can use a HashMap to achieve O(n) time complexity. Here’s how:

  1. We create a HashMap where:
    • Keys are the numbers we’ve seen so far
    • Values are their indices in the array
  2. For each number in the array:
    • We calculate its complement (what number we need to add to it to reach the target)
    • We check if that complement is already in our HashMap
    • If it is, we’ve found our pair!
    • If not, we add the current number and its index to the HashMap

Let’s walk through Example 1: nums = [2,7,11,15], target = 9

  1. i = 0, nums[0] = 2
    • complement = 9 - 2 = 7
    • Map is empty, so add 2->0 to map
    • Map: {2:0}
  2. i = 1, nums[1] = 7
    • complement = 9 - 7 = 2
    • 2 exists in map!
    • Return [map.get(2), 1] which is [0,1]

Time Complexity: O(n) - we only need to traverse the array once
Space Complexity: O(n) - in worst case, we might need to store almost all elements in the HashMap

This approach is much more efficient than the brute force method of checking every pair of numbers, which would take O(n²) time.

44
Q

219. Contains Duplicate II, solve in O(N) time and O(N) space

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) <= k.

Example 1:

Input: nums = [1,2,3,1], k = 3
Output: true
Example 2:

Input: nums = [1,0,1,1], k = 1
Output: true
Example 3:

Input: nums = [1,2,3,1,2,3], k = 2
Output: false

Constraints:

1 <= nums.length <= 105
-109 <= nums[i] <= 109
0 <= k <= 105

A

We can solve this problem using a sliding window approach with a hash map to store the indices of elements seen so far. This ensures the solution is both correct and efficient.

import java.util.HashMap;

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        HashMap<Integer, Integer> map = new HashMap<>(); // Stores the last index of each element

        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(nums[i])) {
                // Check the distance between the current and previous indices
                if (i - map.get(nums[i]) <= k) {
                    return true;
                }
            }
            // Update the last index of the current element
            map.put(nums[i], i);
        }

        return false;
    }
}

Explanation of the Correct Solution:

  1. HashMap Usage:
    • The hash map stores the last index where each element was seen.
  2. Efficient Lookup:
    • For each element in the array, we check if it exists in the hash map. If it does, we compute the distance between the current index and the stored index.
  3. Sliding Window Behavior:
    • If the distance is less than or equal to k, the function returns true. Otherwise, it updates the hash map with the current index of the element.

Time Complexity:
- O(n): We traverse the array once, and all hash map operations (insert, lookup, update) are O(1) on average.

Space Complexity:
- O(min(n, k)): The hash map stores at most min(n, k) elements, where n is the array length, and k is the distance constraint.

Examples:

  1. Input: nums = [1, 2, 3, 1], k = 3
    • Output: true
    • Explanation: The indices (0, 3) satisfy the condition.
  2. Input: nums = [1, 0, 1, 1], k = 1
    • Output: true
    • Explanation: The indices (2, 3) satisfy the condition.
  3. Input: nums = [1, 2, 3, 1, 2, 3], k = 2
    • Output: false
    • Explanation: No indices satisfy the condition.
45
Q

128. Longest Consecutive Sequence, solve in O(N) time and O(N) space

Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.

You must write an algorithm that runs in O(n) time.

Example 1:

Input: nums = [100,4,200,1,3,2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.
Example 2:

Input: nums = [0,3,7,2,5,8,4,6,0,1]
Output: 9

Constraints:

0 <= nums.length <= 105
-109 <= nums[i] <= 109

A

The key insight here is that we need an O(n) solution, which means we can’t rely on sorting the array (which would be O(n log n)). Instead, we’ll use a HashSet for O(1) lookups and a clever strategy to find sequences.

Here’s the solution with detailed explanations:

class Solution {
    public int longestConsecutive(int[] nums) {
        // Edge case: if array is empty, return 0
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // Step 1: Add all numbers to a HashSet for O(1) lookup
        Set<Integer> numSet = new HashSet<>();
        for (int num : nums) {
            numSet.add(num);
        }
        
        int longestStreak = 0;
        
        // Step 2: Iterate through each number in the array
        for (int num : nums) {
            // Only start checking sequences from the smallest number in the sequence
            // This ensures we don't count the same sequence multiple times
            if (!numSet.contains(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;
                
                // Keep checking for consecutive numbers
                while (numSet.contains(currentNum + 1)) {
                    currentNum++;
                    currentStreak++;
                }
                
                // Update longest streak if current streak is longer
                longestStreak = Math.max(longestStreak, currentStreak);
            }
        }
        
        return longestStreak;
    }
}

Let’s understand how this solution works:

  1. First, we handle the edge case where the input array is null or empty.
  2. The core strategy uses a HashSet to store all numbers. This gives us O(1) lookup time to check if a number exists.
  3. The clever part comes in how we find sequences:
    • For each number n, we first check if (n-1) exists in the set
    • If (n-1) doesn’t exist, it means this number could be the start of a sequence
    • Only then do we start counting consecutive numbers from this point
  4. For example, with input [100,4,200,1,3,2]:
    • When we encounter 100, since 99 isn’t in the set, we check for 101, 102, etc.
    • When we encounter 4, since 3 is in the set, we skip it (we’ll count it when we start from 1)
    • When we encounter 1, since 0 isn’t in the set, we check for 2,3,4, finding a sequence of length 4

Time Complexity: O(n)
- Adding all numbers to HashSet: O(n)
- The while loop might seem like it could make this O(n²), but each number is only visited once as part of a sequence
- Total operations = n (adding to set) + n (outer loop) + n (total inner loop operations across all iterations)
- Therefore, overall complexity is O(n)

Space Complexity: O(n)
- We store all numbers in a HashSet

This solution is particularly elegant because:
1. It avoids sorting (which would make it O(n log n))
2. It only counts each sequence once by starting from the smallest number
3. It uses a HashSet for efficient lookups
4. It handles duplicates automatically (since HashSet only stores unique values)

46
Q

128. Longest Consecutive Sequence, solve in O(nlogn) time and O(1) space

Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.

You must write an algorithm that runs in O(n) time.

Example 1:

Input: nums = [100,4,200,1,3,2]
Output: 4
Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.
Example 2:

Input: nums = [0,3,7,2,5,8,4,6,0,1]
Output: 9

Constraints:

0 <= nums.length <= 105
-109 <= nums[i] <= 109

A

I’ll solve this problem using a sorting-based approach in Java and provide a detailed explanation of the solution.

class Solution {
    public int longestConsecutive(int[] nums) {
        // If the array is empty, return 0
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // Sort the array in ascending order
        Arrays.sort(nums);
        
        // Initialize variables to track the longest consecutive sequence
        int longestSequence = 1;  // Minimum sequence length is 1
        int currentSequence = 1;
        
        // Iterate through the sorted array starting from the second element
        for (int i = 1; i < nums.length; i++) {
            // Skip duplicate elements
            if (nums[i] == nums[i-1]) {
                continue;
            }
            
            // Check if current number is consecutive to previous number
            if (nums[i] == nums[i-1] + 1) {
                // If consecutive, increment current sequence
                currentSequence++;
            } else {
                // If not consecutive, reset current sequence
                // Update longest sequence if needed
                longestSequence = Math.max(longestSequence, currentSequence);
                currentSequence = 1;
            }
        }
        
        // Final check to update longest sequence after loop
        return Math.max(longestSequence, currentSequence);
    }
}

Let me break down the solution and explain the approach in depth:

Problem Understanding
The challenge is to find the length of the longest consecutive elements sequence in an unsorted array. A consecutive sequence means numbers that follow each other sequentially, like [1, 2, 3, 4].

Sorting-Based Solution Strategy
1. Sorting the Array:
- By sorting the array first, we can easily identify consecutive elements.
- This transforms the problem from finding scattered consecutive elements to identifying consecutive runs in a sorted sequence.

  1. Algorithm Steps:
    • First, handle the edge case of an empty or null array
    • Sort the input array in ascending order
    • Iterate through the sorted array
    • Track the current consecutive sequence length
    • Update the longest sequence found so far

Time and Space Complexity
- Time Complexity: O(n log n) due to sorting
- Note: The problem statement asks for O(n), which this solution doesn’t meet
- A true O(n) solution would use a HashSet approach
- Space Complexity: O(1) or O(log n) depending on the sorting algorithm

Key Implementation Details
- currentSequence tracks the length of the current consecutive run
- longestSequence stores the maximum consecutive sequence length
- Duplicate elements are skipped to prevent overcounting
- The final Math.max() ensures we capture the last potential longest sequence

Example Walkthrough
Let’s trace the example: [100, 4, 200, 1, 3, 2]

  1. After sorting: [1, 2, 3, 4, 100, 200]
  2. Consecutive sequence tracking:
    • [1, 2, 3, 4] forms a sequence of length 4
    • Other numbers break the sequence
  3. Returns 4 ✓

Possible Improvements
While this solution works, it doesn’t meet the O(n) time complexity requirement. For a true O(n) solution, you’d use a HashSet-based approach that allows O(1) lookups.

47
Q

228. Summary Ranges, solve in O(N) time and O(1) space

You are given a sorted unique integer array nums.

A range [a,b] is the set of all integers from a to b (inclusive).

Return the smallest sorted list of ranges that cover all the numbers in the array exactly. That is, each element of nums is covered by exactly one of the ranges, and there is no integer x such that x is in one of the ranges but not in nums.

Each range [a,b] in the list should be output as:

“a->b” if a != b
“a” if a == b

Example 1:

Input: nums = [0,1,2,4,5,7]
Output: [“0->2”,”4->5”,”7”]
Explanation: The ranges are:
[0,2] –> “0->2”
[4,5] –> “4->5”
[7,7] –> “7”
Example 2:

Input: nums = [0,2,3,4,6,8,9]
Output: [“0”,”2->4”,”6”,”8->9”]
Explanation: The ranges are:
[0,0] –> “0”
[2,4] –> “2->4”
[6,6] –> “6”
[8,9] –> “8->9”

Constraints:

0 <= nums.length <= 20
-231 <= nums[i] <= 231 - 1
All the values of nums are unique.
nums is sorted in ascending order.

A
class Solution {
    public List<String> summaryRanges(int[] nums) {
        // List to store the final ranges
        List<String> ranges = new ArrayList<>();
        
        // Handle empty array case
        if (nums == null || nums.length == 0) {
            return ranges;
        }
        
        // Start with the first number as the range start
        int start = nums[0];
        
        // Iterate through the array to find consecutive ranges
        for (int i = 1; i <= nums.length; i++) {
            // Check if we've reached the end of the array or found a break in consecutive numbers
            if (i == nums.length || nums[i] != nums[i-1] + 1) {
                // Create range string
                if (start == nums[i-1]) {
                    // Single number range
                    ranges.add(String.valueOf(start));
                } else {
                    // Multiple number range
                    ranges.add(start + "->" + nums[i-1]);
                }
                
                // If not at the end, set new range start
                if (i < nums.length) {
                    start = nums[i];
                }
            }
        }
        
        return ranges;
    }
}

Let me break down the solution and explain the key aspects:

Algorithm Explanation:

  1. Handling Edge Cases:
    • First, we check if the input array is null or empty. If so, we return an empty list.
    • This prevents any potential null pointer or index out of bounds exceptions.
  2. Range Tracking:
    • We use two key variables:
      • start: Tracks the beginning of the current range
      • i: Iterates through the array to find consecutive numbers
  3. Consecutive Number Detection:
    • We look for breaks in consecutive numbers by checking if nums[i] is not equal to nums[i-1] + 1
    • This helps us identify where a range ends
  4. Range String Creation:
    • If the start and end of a range are the same, we add just that number as a string
    • If the range spans multiple numbers, we create a string in the format “start->end”
  5. Time and Space Complexity:
    • Time Complexity: O(n), where n is the length of the input array
    • Space Complexity: O(1) extra space (not counting the output list)

Example Walkthrough:
Let’s trace the algorithm with the first example: [0,1,2,4,5,7]

  1. Start with start = 0
  2. First range: 0, 1, 2 are consecutive
    • When 4 is encountered, we add “0->2” to ranges
  3. Next range: 4, 5 are consecutive
    • When 7 is encountered, we add “4->5” to ranges
  4. Last range: 7 is a single number
    • Add “7” to ranges

Key Insights:
- The solution leverages the fact that the input is already sorted and contains unique numbers
- By iterating just once through the array, we efficiently create the range summary
- The algorithm handles both single-number and multi-number ranges seamlessly

Let me break down the time and space complexities for the solution in detail.

Time Complexity: O(n)
- The algorithm makes a single pass through the input array nums
- We iterate through the array exactly once from start to end
- Each iteration involves constant-time operations:
- Comparing adjacent elements
- Creating range strings
- Adding to the result list
- No nested loops or recursive calls
- The number of operations grows linearly with the input array’s length

Mental Model for Time Complexity:
Imagine you’re walking through a line of numbered stones. You’re creating signs to mark continuous sections of these stones. You only need to walk the line once, looking at each stone just once to decide how to label it. This is essentially O(n) - you touch each element exactly once.

Space Complexity: O(1) Extra Space
- The extra space used by the algorithm (excluding the output list) is constant
- We only use a few variables: start and loop index i
- The output list’s space depends on the input, so it’s not counted in extra space

Nuanced Space Complexity Explanation:
While the output list’s size depends on the input, its creation is part of the problem’s requirement. We’re interested in the additional memory used by the algorithm itself, which remains constant regardless of input size.

Visualization:
~~~
Input: [0,1,2,4,5,7]

Memory used:
- ‘start’ variable (current range start)
- ‘i’ loop index
- A few other constant-memory variables
= Constant Additional Memory
~~~

Comparison to Alternatives:
- A naive approach might use nested loops, increasing time complexity to O(n²)
- Some solutions might create temporary arrays or use additional data structures
- This solution is optimal, using minimal extra memory and making a single pass

48
Q

56. Merge Intervals, solve in O(nlogn) time and O(n) space

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

Example 1:

Input: intervals = [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].
Example 2:

Input: intervals = [[1,4],[4,5]]
Output: [[1,5]]
Explanation: Intervals [1,4] and [4,5] are considered overlapping.

Constraints:

1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104

A
import java.util.*;

class Solution {
    public int[][] merge(int[][] intervals) {
        // If the input is empty or has only one interval, return as is
        if (intervals == null || intervals.length <= 1) {
            return intervals;
        }
        
        // Sort intervals based on start time
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
        
        List<int[]> merged = new ArrayList<>();
        int[] currentInterval = intervals[0];
        merged.add(currentInterval);
        
        for (int[] interval : intervals) {
            // Get the current end time and next start time
            int currentEnd = currentInterval[1];
            int nextStart = interval[0];
            int nextEnd = interval[1];
            
            // If intervals overlap, update the end time if necessary
            if (currentEnd >= nextStart) {
                currentInterval[1] = Math.max(currentEnd, nextEnd);
            } else {
                // If no overlap, add the new interval to result
                currentInterval = interval;
                merged.add(currentInterval);
            }
        }
        
        // Convert List<int[]> to int[][]
        return merged.toArray(new int[merged.size()][]);
    }

    // Test the solution
    public static void main(String[] args) {
        Solution solution = new Solution();
        
        // Test case 1
        int[][] intervals1 = {{1,3}, {2,6}, {8,10}, {15,18}};
        int[][] result1 = solution.merge(intervals1);
        System.out.println("Test case 1:");
        printResult(result1);  // Expected: [[1,6], [8,10], [15,18]]
        
        // Test case 2
        int[][] intervals2 = {{1,4}, {4,5}};
        int[][] result2 = solution.merge(intervals2);
        System.out.println("\nTest case 2:");
        printResult(result2);  // Expected: [[1,5]]
    }
    
    private static void printResult(int[][] intervals) {
        System.out.print("[");
        for (int i = 0; i < intervals.length; i++) {
            System.out.print(Arrays.toString(intervals[i]));
            if (i < intervals.length - 1) System.out.print(", ");
        }
        System.out.println("]");
    }
}

Let me explain how this solution works:

  1. First, we handle edge cases:
    • If the input is null or has 1 or fewer intervals, we return it as is
  2. We sort the intervals based on start time:
    • This ensures we can process intervals in order
    • Makes it easier to find overlapping intervals
  3. Main merging logic:
    • Keep track of current interval and compare with next intervals
    • If intervals overlap (currentEnd >= nextStart), merge them by updating the end time
    • If no overlap, add the new interval to result list
  4. Time Complexity: O(n log n) due to sorting
    • Space Complexity: O(n) to store the result

The solution includes test cases for the examples you provided. The output will be:
~~~
Test case 1:
[[1,6], [8,10], [15,18]]

Test case 2:
[[1,5]]
~~~

49
Q
A
50
Q

57. Insert Interval, solve in O(N) time and O(N) space

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.

Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).

Return intervals after the insertion.

Note that you don’t need to modify intervals in-place. You can make a new array and return it.

Example 1:

Input: intervals = [[1,3],[6,9]], newInterval = [2,5]
Output: [[1,5],[6,9]]
Example 2:

Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]
Explanation: Because the new interval [4,8] overlaps with [3,5],[6,7],[8,10].

Constraints:

0 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 105
intervals is sorted by starti in ascending order.
newInterval.length == 2
0 <= start <= end <= 105

A

The key idea behind this solution is to handle the new interval’s insertion and potential merging in a single pass through the existing intervals. Let’s walk through the algorithm’s logic and reasoning:

Key Strategy
The algorithm uses a single iteration approach to handle three possible scenarios for each interval:
1. The new interval comes before the current interval
2. The current interval comes before the new interval
3. The intervals overlap and need to be merged

Detailed Walkthrough

public int[][] insert(int[][] intervals, int[] newInterval) {
    // Create a list to store the result intervals
    List<int[]> result = new ArrayList<>();
    
    // Iterate through all existing intervals
    for(int[] slot : intervals)
    {
        // Scenario 1: New interval is completely before current interval
        if(newInterval[1] < slot[0])
        {
            // Add the new interval to the result
            result.add(newInterval);
            // Update newInterval to be the current interval for next iterations
            newInterval = slot;
        } 
        
        // Scenario 2: Current interval is completely before new interval
        else if(slot[1] < newInterval[0])
        {
            // Simply add the current interval to the result
            result.add(slot);
        } 
        
        // Scenario 3: Intervals overlap - need to merge
        else {
            // Merge by taking the minimum start and maximum end
            newInterval[0] = Math.min(newInterval[0], slot[0]);
            newInterval[1] = Math.max(newInterval[1], slot[1]);
        }
    }
    
    // Add the last interval (which might be the merged interval)
    result.add(newInterval);
    
    // Convert result list to 2D array and return
    return result.toArray(new int[result.size()][]);
}

Scenario Breakdown

  1. New Interval Comes Before Current Interval
    java
    if(newInterval[1] < slot[0])

    • This means the new interval ends before the current interval starts
    • We add the new interval to the result
    • We update newInterval to be the current interval for future processing
  2. Current Interval Comes Before New Interval
    java
    else if(slot[1] < newInterval[0])

    • This means the current interval ends before the new interval starts
    • We simply add the current interval to the result
    • No merging is needed
  3. Intervals Overlap
    java
    else {
        newInterval[0] = Math.min(newInterval[0], slot[0]);
        newInterval[1] = Math.max(newInterval[1], slot[1]);
    }

    • If neither of the above conditions are true, the intervals must overlap
    • We merge by taking the minimum start and maximum end
    • This effectively expands the new interval to cover both its original range and the overlapping interval

Example Walkthrough

Let’s use the second example: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]

  1. [1,2] - comes before new interval, added to result
  2. [3,5] - overlaps, merge occurs, newInterval becomes [3,8]
  3. [6,7] - overlaps, merge continues, newInterval becomes [3,8]
  4. [8,10] - overlaps, final merge, newInterval becomes [3,10]
  5. [12,16] - comes after merged interval, added to result

Time and Space Complexity
- Time Complexity: O(n), where n is the number of intervals
- Space Complexity: O(n) to store the result list

Mental Model
Think of this algorithm like sorting and merging books on a shelf:
- Some books go in easily
- Some books might need to be expanded to cover multiple existing books
- At the end, you have a neatly organized shelf with no wasted space

Potential Gotchas
- The algorithm assumes input intervals are already sorted by start time
- It modifies the newInterval in-place during merging
- The last interval is always added after the loop to handle edge cases

Step-by-Step Walkthrough

Let’s go through the iteration process, explaining what happens at each step:

Initial Setup
- intervals: [[1,2],[3,5],[6,7],[8,10],[12,16]]
- newInterval: [4,8]
- result: Empty list (will store our final intervals)

Iteration 1: [1,2]
- Current interval is [1,2]
- Condition check:
1. Is [4,8] before [1,2]? No.
2. Is [1,2] before [4,8]? Yes! ([1,2] ends before [4,8] starts)
- Action: Add [1,2] to result
- Current state:
- result: [[1,2]]
- newInterval remains [4,8]

Iteration 2: [3,5]
- Current interval is [3,5]
- Condition check:
1. Is [4,8] before [3,5]? No.
2. Is [3,5] before [4,8]? No.
- This means OVERLAP!
- Merge logic:
- New start: min(4, 3) = 3
- New end: max(8, 5) = 8
- Action: Update newInterval to [3,8]
- Current state:
- result: [[1,2]]
- newInterval: [3,8]

Iteration 3: [6,7]
- Current interval is [6,7]
- Condition check:
1. Is [3,8] before [6,7]? No.
2. Is [6,7] before [3,8]? No.
- Another overlap!
- Merge logic:
- New start: min(3, 6) = 3
- New end: max(8, 7) = 8
- Action: Keep newInterval as [3,8]
- Current state:
- result: [[1,2]]
- newInterval: [3,8]

Iteration 4: [8,10]
- Current interval is [8,10]
- Condition check:
1. Is [3,8] before [8,10]? No.
2. Is [8,10] before [3,8]? No.
- Another overlap!
- Merge logic:
- New start: min(3, 8) = 3
- New end: max(8, 10) = 10
- Action: Update newInterval to [3,10]
- Current state:
- result: [[1,2]]
- newInterval: [3,10]

Iteration 5: [12,16]
- Current interval is [12,16]
- Condition check:
1. Is [3,10] before [12,16]? Yes! ([3,10] ends before [12,16] starts)
- Action:
1. Add [3,10] to result
2. Add [12,16] to result
- Final state:
- result: [[1,2],[3,10],[12,16]]

Visualization of Merging Process
~~~
Original: [1,2] [3,5] [6,7] [8,10] [12,16]
New Interval: [4,8]
Merged: [1,2] [3,10] [12,16]
~~~

Key Insights
1. The algorithm handles merging dynamically during a single pass
2. It keeps the intervals sorted
3. It minimizes the start and maximizes the end during overlaps
4. It doesn’t require a separate merging step

51
Q

20. Valid Parentheses, solve in O(N) and O(N) space

Given a string s containing just the characters ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[’ and ‘]’, determine if the input string is valid.

An input string is valid if:

Open brackets must be closed by the same type of brackets.
Open brackets must be closed in the correct order.
Every close bracket has a corresponding open bracket of the same type.

Example 1:

Input: s = “()”

Output: true

Example 2:

Input: s = “()[]{}”

Output: true

Example 3:

Input: s = “(]”

Output: false

Example 4:

Input: s = “([])”

Output: true

Constraints:

1 <= s.length <= 104
s consists of parentheses only ‘()[]{}’.

A

First, the core approach: Instead of storing opening brackets and checking if we find their matching closing brackets later, this solution does something clever - when it sees an opening bracket, it immediately pushes the expected closing bracket onto the stack. This makes the checking process much simpler.

Here’s how it processes each character:

// Convert the input string to a character array for easier processing
char[] brackets = s.toCharArray();
Stack<Character> stack = new Stack();

for (char bracket : brackets) {
    // When we see an opening bracket, push its corresponding closing bracket
    if (bracket == '(') {
        stack.push(')');
    } else if (bracket == '{') {
        stack.push('}');
    } else if (bracket == '[') {
        stack.push(']');
    }
    // If we see a closing bracket, it must match the most recent expected closing bracket
    else if (stack.isEmpty() || stack.pop() != bracket) {
        return false;
    }
}

// At the end, all brackets should be matched (stack empty)
return stack.isEmpty();

Let’s see this in action with the example “([])” to understand why it works:

  1. See ‘(‘ → Push ‘)’ onto stack
    Stack: [’)’]
  2. See ‘[’ → Push ‘]’ onto stack
    Stack: [’)’, ‘]’]
  3. See ‘]’ → Pop top of stack and compare
    • Expected ‘]’, got ‘]’ - matches!
      Stack: [’)’]
  4. See ‘)’ → Pop top of stack and compare
    • Expected ‘)’, got ‘)’ - matches!
      Stack: [] (empty)

The solution returns true because the stack is empty (all brackets matched).

The code handles invalid cases elegantly:
- If we get a closing bracket when the stack is empty (like “]”), it returns false
- If we get a closing bracket that doesn’t match the expected one (like “(]”), it returns false
- If we have leftover opening brackets (like “((“), the stack won’t be empty at the end, so it returns false

This solution is not only correct but also efficient, with O(n) time complexity where n is the length of the string, and O(n) space complexity in the worst case where all characters are opening brackets.

The most brilliant part is how it pushes the expected closing bracket instead of the opening bracket - this eliminates the need for a separate mapping of opening to closing brackets and simplifies the matching logic to a single comparison.

52
Q

452. Minimum Number of Arrows to Burst Balloons, solve in O(nlogn) time

There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D integer array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.

Arrows can be shot up directly vertically (in the positive y-direction) from different points along the x-axis. A balloon with xstart and xend is burst by an arrow shot at x if xstart <= x <= xend. There is no limit to the number of arrows that can be shot. A shot arrow keeps traveling up infinitely, bursting any balloons in its path.

Given the array points, return the minimum number of arrows that must be shot to burst all balloons.

Example 1:

Input: points = [[10,16],[2,8],[1,6],[7,12]]
Output: 2
Explanation: The balloons can be burst by 2 arrows:
- Shoot an arrow at x = 6, bursting the balloons [2,8] and [1,6].
- Shoot an arrow at x = 11, bursting the balloons [10,16] and [7,12].
Example 2:

Input: points = [[1,2],[3,4],[5,6],[7,8]]
Output: 4
Explanation: One arrow needs to be shot for each balloon for a total of 4 arrows.
Example 3:

Input: points = [[1,2],[2,3],[3,4],[4,5]]
Output: 2
Explanation: The balloons can be burst by 2 arrows:
- Shoot an arrow at x = 2, bursting the balloons [1,2] and [2,3].
- Shoot an arrow at x = 4, bursting the balloons [3,4] and [4,5].

Constraints:

1 <= points.length <= 105
points[i].length == 2
-231 <= xstart < xend <= 231 - 1

A

Let me help you solve this balloon burst problem step by step. This is an interesting optimization problem that can be solved using a greedy approach.

Let’s first understand why a greedy approach works here:
1. For any overlapping balloons, we want to shoot an arrow that can burst as many balloons as possible with a single shot
2. Since the arrows travel vertically upward, we only need to consider the x-coordinates
3. For any set of overlapping balloons, the optimal position to shoot would be in their overlapping region

Here’s the solution with detailed explanations:

import java.util.Arrays;

class Solution {
    public int findMinArrowShots(int[][] points) {
        // Edge case: if there are no balloons, we need 0 arrows
        if (points == null || points.length == 0) {
            return 0;
        }
        
        // Sort balloons by their end coordinates
        // This is crucial because we want to shoot arrows at the earliest possible
        // position that can burst multiple balloons
        Arrays.sort(points, (a, b) -> {
            // Handle potential integer overflow when comparing
            if (a[1] < b[1]) return -1;
            if (a[1] > b[1]) return 1;
            return 0;
        });
        
        int arrows = 1;  // We need at least one arrow
        int currentArrowPosition = points[0][1];  // Position of the first arrow
        
        // Iterate through all balloons
        for (int i = 1; i < points.length; i++) {
            // If the current balloon starts after our last arrow position
            // we need a new arrow
            if (points[i][0] > currentArrowPosition) {
                arrows++;
                currentArrowPosition = points[i][1];
            }
            // If the current balloon overlaps with our last arrow position
            // we don't need a new arrow, and we continue to the next balloon
        }
        
        return arrows;
    }
}

Let’s break down how this solution works:

  1. Sorting Strategy:
    • We sort the balloons by their end coordinates (xend)
    • This is crucial because it helps us make the optimal choice at each step
    • By considering balloons in order of their ending positions, we ensure we’re making the most efficient use of each arrow
  2. The Greedy Choice:
    • After sorting, we place our first arrow at the end coordinate of the first balloon
    • For each subsequent balloon, we check if it overlaps with our last arrow position
    • If there’s no overlap, we need a new arrow
  3. Overlap Detection:
    • A balloon overlaps with our arrow position if its start coordinate (points[i][0]) is less than or equal to our current arrow position
    • If there’s no overlap, the start coordinate will be greater than our arrow position
  4. Time Complexity: O(n log n) due to the sorting
    Space Complexity: O(1) as we only use a constant amount of extra space

Let’s see how it works with Example 1:
~~~
Input: [[10,16],[2,8],[1,6],[7,12]]

After sorting by end coordinates:
[[1,6],[2,8],[7,12],[10,16]]

  1. Place first arrow at x = 6 (end of first balloon)
  2. Check [2,8]: starts at 2 ≤ 6, so same arrow works
  3. Check [7,12]: starts at 7 > 6, need new arrow at x = 12
  4. Check [10,16]: starts at 10 ≤ 12, so same arrow works

Result: 2 arrows needed
~~~

This solution is optimal because:
1. It ensures we never use more arrows than necessary by always shooting at the earliest possible position that can burst multiple balloons
2. The sorting by end coordinates guarantees we consider balloons in an order that maximizes the number of balloons each arrow can burst

Let me explain why sorting by first position (start coordinates) could lead to suboptimal results. Let’s analyze this with a carefully constructed example.

Consider this array of balloons:
~~~
[[1,15], [2,4], [3,5], [4,6]]
~~~

Let’s visualize these balloons on a number line:
~~~
Balloon 1: 1 ———————15
Balloon 2: 2 –4
Balloon 3: 3 –5
Balloon 4: 4 –6
~~~

If we sort by start position (which is already done in this case), our greedy algorithm might make the wrong decision. Let’s see what happens:

  1. We start with [1,15]. Following a greedy approach, we might place our first arrow at position 15 (the end of the first balloon).
  2. This first arrow at position 15 bursts all balloons, giving us a result of 1 arrow.
  3. This seems good, but it’s actually not optimal!

The optimal solution requires 2 arrows:
- One arrow at position 4 to burst balloons [2,4], [3,5], and [4,6]
- Another arrow at position 15 to burst balloon [1,15]

Why is 2 arrows better than 1? Because this example was specially constructed to trick us - in a real-world scenario, there could be more balloons coming after these that overlap with [1,15] but not with the others. By taking care of the shorter balloons first (which we discover by sorting by end position), we ensure we make the optimal choice.

When we sort by end position instead:
~~~
[[2,4], [3,5], [4,6], [1,15]]
~~~

Our algorithm works correctly:
1. Place first arrow at x = 4 (end of first balloon)
- This bursts [2,4], [3,5], and [4,6]
2. Place second arrow at x = 15
- This bursts [1,15]

The key insight is that sorting by end position lets us make the optimal local decision at each step because:
1. We handle shorter balloons first
2. We know that any remaining balloons must extend beyond our current position
3. This guarantees we never miss an opportunity to burst multiple balloons with a single arrow

This example demonstrates why the greedy choice of sorting by end position works - it allows us to make locally optimal decisions that lead to a globally optimal solution. When sorting by start position, we might make choices that seem good initially but prevent us from achieving the optimal solution.

53
Q

141. Linked List Cycle, solve in O(N) time and O(1) space

Given head, the head of a linked list, determine if the linked list has a cycle in it.

There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail’s next pointer is connected to. Note that pos is not passed as a parameter.

Return true if there is a cycle in the linked list. Otherwise, return false.

Example 1:

Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed).
Example 2:

Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where the tail connects to the 0th node.
Example 3:

Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.

Constraints:

The number of the nodes in the list is in the range [0, 104].
-105 <= Node.val <= 105
pos is -1 or a valid index in the linked-list.

A

I’ll help you solve this classic linked list cycle detection problem using Floyd’s Cycle-Finding Algorithm (also known as the “tortoise and hare” algorithm). This is an elegant solution that uses O(1) memory as requested in the follow-up question.

Let me explain how the algorithm works before implementing it:

  1. The key insight is that if we have two pointers moving at different speeds through the linked list:
    • If there’s a cycle, the faster pointer will eventually catch up to the slower pointer
    • If there’s no cycle, the faster pointer will reach the end of the list
  2. We’ll use:
    • A slow pointer that moves one step at a time
    • A fast pointer that moves two steps at a time

Let’s implement this solution in Java:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // Handle edge cases: empty list or single node
        if (head == null || head.next == null) {
            return false;
        }
        
        // Initialize our two pointers
        ListNode slow = head;
        ListNode fast = head;
        
        // Continue until fast reaches end or finds cycle
        while (fast != null && fast.next != null) {
            // Move slow pointer one step
            slow = slow.next;
            
            // Move fast pointer two steps
            fast = fast.next.next;
            
            // If they meet, we've found a cycle
            if (slow == fast) {
                return true;
            }
        }
        
        // If we exit the loop, fast has reached the end, so no cycle
        return false;
    }
}

Let’s analyze how this solution works:

  1. First, we handle edge cases:
    • An empty list (head == null) cannot have a cycle
    • A single node list (head.next == null) cannot have a cycle
  2. We initialize both pointers to the head of the list:
    • The slow pointer (tortoise)
    • The fast pointer (hare)
  3. In each iteration:
    • The slow pointer moves one step (slow = slow.next)
    • The fast pointer moves two steps (fast = fast.next.next)
    • We check if they meet (slow == fast)
  4. The loop continues until either:
    • We find a cycle (pointers meet)
    • Fast reaches the end of the list (no cycle)

Time Complexity: O(n) where n is the number of nodes in the linked list
- In the worst case, we might need to traverse the entire list
- If there’s a cycle, we’ll find it before completing a full cycle

Space Complexity: O(1)
- We only use two pointers regardless of the list size
- This meets the follow-up challenge of using constant memory

This algorithm is particularly elegant because it solves the problem without requiring any extra data structures to track visited nodes, which would require O(n) space. Instead, it cleverly uses the relative speeds of the two pointers to detect cycles.

54
Q

155. Min Stack

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

Implement the MinStack class:

MinStack() initializes the stack object.
void push(int val) pushes the element val onto the stack.
void pop() removes the element on the top of the stack.
int top() gets the top element of the stack.
int getMin() retrieves the minimum element in the stack.
You must implement a solution with O(1) time complexity for each function.

Example 1:

Input
[“MinStack”,”push”,”push”,”push”,”getMin”,”pop”,”top”,”getMin”]
[[],[-2],[0],[-3],[],[],[],[]]

Output
[null,null,null,null,-3,null,0,-2]

Explanation
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // return -3
minStack.pop();
minStack.top(); // return 0
minStack.getMin(); // return -2

Constraints:

-231 <= val <= 231 - 1
Methods pop, top and getMin operations will always be called on non-empty stacks.
At most 3 * 104 calls will be made to push, pop, top, and getMin.

A

Let me help you implement a MinStack in Java. The key to achieving O(1) time complexity for all operations is to maintain two stacks - one for the actual elements and another for tracking minimums.

class MinStack {
    private Stack<Integer> stack;      // Main stack to store elements
    private Stack<Integer> minStack;   // Stack to track minimum elements
    
    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }
    
    public void push(int val) {
        stack.push(val);
        
        // If minStack is empty or new value is less than or equal to current minimum,
        // push it to minStack
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    public void pop() {
        // If the popped element is the current minimum,
        // remove it from minStack as well
        if (stack.peek().equals(minStack.peek())) {
            minStack.pop();
        }
        stack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}

Let me explain how this solution works:

  1. We use two stacks:
    • stack: stores all the elements
    • minStack: keeps track of minimum elements
  2. The operations work as follows:
    • push: Always push to main stack. Push to minStack if the value is less than or equal to current minimum.
    • pop: Remove from main stack. If the popped value equals current minimum, remove from minStack too.
    • top: Simply return the top element of main stack.
    • getMin: Return the top element of minStack.
  3. Time complexity:
    • All operations are O(1) since they only involve stack push/pop operations
    • Space complexity is O(n) where n is the number of elements in the stack
  4. Important implementation details:
    • We use .equals() instead of == when comparing Integer objects in pop()
    • We push duplicates of minimum values to minStack to handle cases where the same minimum appears multiple times
55
Q

21. Merge Two Sorted Lists

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

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

Return the head of the merged linked list.

Example 1:

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

Input: list1 = [], list2 = []
Output: []
Example 3:

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

Constraints:

The number of nodes in both lists is in the range [0, 50].
-100 <= Node.val <= 100
Both list1 and list2 are sorted in non-decreasing order.

A

I’ll help you solve this linked list merging problem. Let’s break it down step by step and create a solution that’s both efficient and easy to understand.

First, let’s create the basic structure for our linked list node, then implement the merging logic:

/**
 * Definition for singly-linked list node
 */
class ListNode {
    int val;
    ListNode next;
    
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { 
        this.val = val; 
        this.next = next;
    }
}

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        // Create a dummy node to serve as the head of our merged list
        // This eliminates the need for special cases when inserting the first node
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;
        
        // Continue while both lists have nodes to compare
        while (list1 != null && list2 != null) {
            if (list1.val <= list2.val) {
                // If list1's value is smaller, add it to our merged list
                current.next = list1;
                list1 = list1.next;
            } else {
                // If list2's value is smaller, add it to our merged list
                current.next = list2;
                list2 = list2.next;
            }
            // Move our pointer forward in the merged list
            current = current.next;
        }
        
        // If there are remaining nodes in list1, append them
        if (list1 != null) {
            current.next = list1;
        }
        
        // If there are remaining nodes in list2, append them
        if (list2 != null) {
            current.next = list2;
        }
        
        // Return the head of our merged list (skip the dummy node)
        return dummy.next;
    }
}

Let’s understand how this solution works:

  1. We start by creating a dummy node. This is a common technique in linked list problems because it simplifies our logic by eliminating the need to handle the first node as a special case.
  2. The merging process works like this:
    • We maintain a current pointer that keeps track of where we’re building our merged list
    • We compare values from both lists and take the smaller one each time
    • We move forward in the list we took the node from
    • We update our current pointer to point to the newly added node
  3. The while loop continues as long as we have nodes to compare in both lists. Once we run out of nodes in either list, we handle any remaining nodes.
  4. For remaining nodes, we can simply point our current.next to whichever list still has nodes. Since both input lists are already sorted, we know any remaining nodes are in the correct order.

Time Complexity: O(n + m) where n and m are the lengths of the input lists. We need to process every node exactly once.
Space Complexity: O(1) because we’re only rearranging pointers and not creating any new nodes (except the dummy node).

For example, let’s trace through merging [1,2,4] and [1,3,4]:
1. Compare 1 and 1: Take either, advance that list
2. Compare 2 and 1: Take 1, advance list2
3. Compare 2 and 3: Take 2, advance list1
4. Compare 4 and 3: Take 3, advance list2
5. Compare 4 and 4: Take either, advance that list
6. Take remaining node(s)

The result will be [1,1,2,3,4,4], exactly as required.