From Brute Force to Optimal Solution
In this blog post, we'll explore different approaches to solve a common interview question: finding the length of the longest subarray with a given sum K in an array of positive integers. We'll start with a brute force approach and gradually optimize our solution, discussing the time and space complexity of each method.
Problem Statement
Given an array of positive integers and a target sum K, we need to find the length of the longest subarray that sums up to K.
For example:
Input: arr = [2, 3, 5, 1, 9], K = 10
Output: 3
Explanation: The longest subarray with sum 10 is [2, 3, 5]
Let's dive into different approaches to solve this problem.
Approach 1: Brute Force (Using Three Loops)
This is the most straightforward approach where we check all possible subarrays.
Implementation:
function getLongestSubarray(a, k) {
let n = a.length;
let len = 0;
for (let i = 0; i < n; i++) {
for (let j = i; j < n; j++) {
let s = 0;
for (let K = i; K <= j; K++) {
s += a[K];
}
if (s === k)
len = Math.max(len, j - i + 1);
}
}
return len;
}
Time Complexity: O(N³)
- We are using three nested loops, each running approximately N times.
Space Complexity: O(1)
- We are not using any extra space.
Approach 2: Optimized Brute Force (Using Two Loops)
We can optimize the previous approach by removing one loop and calculating the sum while iterating.
Implementation:
function getLongestSubarray(a, k) {
let n = a.length;
let len = 0;
for (let i = 0; i < n; i++) {
let s = 0;
for (let j = i; j < n; j++) {
s += a[j];
if (s === k)
len = Math.max(len, j - i + 1);
}
}
return len;
}
Time Complexity: O(N²)
- We are using two nested loops, each running approximately N times.
Space Complexity: O(1)
- We are not using any extra space.
Approach 3: Using Hashing
This approach uses a hash map to store cumulative sums and their indices, allowing us to find the longest subarray more efficiently.
Implementation:
function getLongestSubarray(a, k) {
let n = a.length;
let preSumMap = new Map();
let sum = 0;
let maxLen = 0;
for (let i = 0; i < n; i++) {
sum += a[i];
if (sum === k) {
maxLen = Math.max(maxLen, i + 1);
}
let rem = sum - k;
if (preSumMap.has(rem)) {
let len = i - preSumMap.get(rem);
maxLen = Math.max(maxLen, len);
}
if (!preSumMap.has(sum)) {
preSumMap.set(sum, i);
}
}
return maxLen;
}
Time Complexity: O(N) or O(N log N)
O(N) if using an unordered map (hash table)
O(N log N) if using an ordered map (tree-based)
Space Complexity: O(N)
- We are using a map data structure that can store up to N elements.
Approach 4: Optimal Solution (Using Two Pointers)
This is the most efficient approach, using the sliding window technique with two pointers.
Implementation:
function getLongestSubarray(a, k) {
let n = a.length;
let left = 0, right = 0;
let sum = a[0];
let maxLen = 0;
while (right < n) {
while (left <= right && sum > k) {
sum -= a[left];
left++;
}
if (sum === k) {
maxLen = Math.max(maxLen, right - left + 1);
}
right++;
if (right < n) sum += a[right];
}
return maxLen;
}
Time Complexity: O(2N) ≈ O(N)
- The right pointer moves N times, and the left pointer can move up to N times in total.
Space Complexity: O(1)
- We are not using any extra space.
Conclusion
We've explored four different approaches to solve the "Longest Subarray with given Sum K" problem, ranging from a simple brute force method to an optimal two-pointer solution. Here's a quick comparison:
Brute Force (Three Loops): Simple but inefficient (O(N³) time)
Optimized Brute Force (Two Loops): Slightly better but still slow for large inputs (O(N²) time)
Hashing Approach: Good balance of efficiency and simplicity (O(N) time, O(N) space)
Two Pointer Approach: Most efficient for this problem (O(N) time, O(1) space)
The two-pointer approach (Approach 4) is the most optimal solution for this problem, especially when dealing with positive integers. It achieves linear time complexity while using constant extra space.
In coding interviews, it's often valuable to start with a simple solution and then optimize it step by step. This demonstrates your problem-solving skills and your ability to analyze and improve algorithms.
Remember to practice these different approaches to become comfortable with various problem-solving techniques. Each method has its use cases, and understanding when to apply each is key to becoming a proficient programmer.