977. Squares of a Sorted Array
Problem Description
You are given an integer array nums
that is sorted in non-decreasing order (from smallest to largest). Your task is to create a new array containing the squares of each number from the original array, and this new array must also be sorted in non-decreasing order.
For example, if you have nums = [-4, -1, 0, 3, 10]
, after squaring each element you get [16, 1, 0, 9, 100]
. The final answer should be these squares arranged in non-decreasing order: [0, 1, 9, 16, 100]
.
The challenge here is that negative numbers, when squared, become positive and can be larger than the squares of positive numbers. Since the original array contains both negative and positive numbers sorted in order, the smallest original numbers (which may be negative) could produce the largest squares. This means you can't simply square each element in place and keep the same order - you need to rearrange them.
The solution uses a two-pointer technique. Since the original array is sorted, the largest squares will come from either the leftmost elements (large negative numbers) or the rightmost elements (large positive numbers). By placing pointers at both ends and comparing the squares, we can build the result array by always taking the larger square and moving the corresponding pointer inward. The squares are collected from largest to smallest, so the final step reverses the array to get the required non-decreasing order.
Intuition
When we square numbers, negative values become positive. This creates an interesting pattern in a sorted array. Consider the array [-4, -1, 0, 3, 10]
. The squares are [16, 1, 0, 9, 100]
. Notice how the squared values form a "valley" shape - they decrease from the left side (negative numbers) toward the middle, then increase toward the right side (positive numbers).
The key insight is that in a sorted array, the largest squared values must come from the extremes - either from the most negative numbers on the left or the most positive numbers on the right. The smallest squared values will be somewhere in the middle, near zero.
This observation leads us to think about using two pointers. If we place one pointer at the beginning and one at the end of the array, we can always identify which element produces the next largest square by comparing nums[left]ยฒ
with nums[right]ยฒ
.
Why build from largest to smallest? Because we can deterministically find the largest square at each step by comparing the extremes. Finding the smallest square would be harder - it could be anywhere in the middle of the array, and we'd need to search for it.
By repeatedly taking the larger square between the two extremes and moving that pointer inward, we gradually work our way toward the smaller squares in the middle. This gives us all squares in descending order. A simple reversal at the end gives us the required ascending order.
This approach elegantly handles all cases - arrays with only negative numbers, only positive numbers, or a mix of both - because the logic of "largest squares come from extremes" always holds true for sorted arrays.
Learn more about Two Pointers and Sorting patterns.
Solution Approach
The solution implements a two-pointer technique to efficiently build the sorted squares array. Here's how the algorithm works:
Initialize the pointers and result array:
- Create an empty list
ans
to store the squared values - Set pointer
i
at index 0 (leftmost element) - Set pointer
j
at indexlen(nums) - 1
(rightmost element)
Main processing loop:
The algorithm continues while i <= j
, ensuring we process all elements:
-
Calculate squares of both extremes:
a = nums[i] * nums[i]
- square of the left elementb = nums[j] * nums[j]
- square of the right element
-
Compare and select the larger square:
- If
a > b
: The left element produces the larger square- Append
a
to the result array - Move the left pointer right:
i += 1
- Append
- Otherwise: The right element produces the larger (or equal) square
- Append
b
to the result array - Move the right pointer left:
j -= 1
- Append
- If
-
Build from largest to smallest: Each iteration adds the next largest square to
ans
. Since we're always choosing between the two extremes and moving inward, we're guaranteed to get all squares in descending order.
Final reversal:
- Return
ans[::-1]
to reverse the array from descending to ascending order
Time and Space Complexity:
- Time:
O(n)
where n is the length of the input array - we visit each element exactly once - Space:
O(n)
for the result array (not counting this as extra space since it's required for the output)
The elegance of this approach lies in its simplicity - by recognizing that the largest values must come from the extremes of a sorted array, we can build our result in a single pass without needing to sort the squared values.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [-4, -1, 0, 3, 10]
.
Initial Setup:
ans = []
(empty result array)i = 0
(pointing to -4)j = 4
(pointing to 10)
Iteration 1: i = 0, j = 4
- Calculate squares:
a = (-4)ยฒ = 16
,b = (10)ยฒ = 100
- Compare:
16 < 100
, so we takeb = 100
- Add 100 to ans:
ans = [100]
- Move right pointer left:
j = 3
Iteration 2: i = 0, j = 3
- Calculate squares:
a = (-4)ยฒ = 16
,b = (3)ยฒ = 9
- Compare:
16 > 9
, so we takea = 16
- Add 16 to ans:
ans = [100, 16]
- Move left pointer right:
i = 1
Iteration 3: i = 1, j = 3
- Calculate squares:
a = (-1)ยฒ = 1
,b = (3)ยฒ = 9
- Compare:
1 < 9
, so we takeb = 9
- Add 9 to ans:
ans = [100, 16, 9]
- Move right pointer left:
j = 2
Iteration 4: i = 1, j = 2
- Calculate squares:
a = (-1)ยฒ = 1
,b = (0)ยฒ = 0
- Compare:
1 > 0
, so we takea = 1
- Add 1 to ans:
ans = [100, 16, 9, 1]
- Move left pointer right:
i = 2
Iteration 5: i = 2, j = 2
- Both pointers point to the same element (0)
- Calculate squares:
a = (0)ยฒ = 0
,b = (0)ยฒ = 0
- Compare:
0 = 0
, so we takeb = 0
- Add 0 to ans:
ans = [100, 16, 9, 1, 0]
- Move right pointer left:
j = 1
Termination: i = 2, j = 1
(i > j, loop ends)
Final Step:
- Reverse the array:
ans[::-1] = [0, 1, 9, 16, 100]
The algorithm successfully builds the sorted squares array by always selecting the larger square from the extremes and working inward, then reversing to get ascending order.
Solution Implementation
1class Solution:
2 def sortedSquares(self, nums: List[int]) -> List[int]:
3 """
4 Given a sorted array of integers, return a sorted array of their squares.
5 Uses two-pointer technique to handle negative numbers efficiently.
6
7 Args:
8 nums: A sorted list of integers (may include negative numbers)
9
10 Returns:
11 A sorted list of squared values
12 """
13 result = []
14 left_pointer = 0
15 right_pointer = len(nums) - 1
16
17 # Use two pointers to compare squares from both ends
18 # Since the array is sorted, the largest squares will be at the extremes
19 while left_pointer <= right_pointer:
20 # Calculate the square of the left element
21 left_square = nums[left_pointer] * nums[left_pointer]
22 # Calculate the square of the right element
23 right_square = nums[right_pointer] * nums[right_pointer]
24
25 # Append the larger square and move the corresponding pointer
26 if left_square > right_square:
27 result.append(left_square)
28 left_pointer += 1
29 else:
30 result.append(right_square)
31 right_pointer -= 1
32
33 # Reverse the result since we've been appending in descending order
34 return result[::-1]
35
1class Solution {
2 public int[] sortedSquares(int[] nums) {
3 int n = nums.length;
4 int[] result = new int[n];
5
6 // Use two pointers approach: left pointer starts from beginning, right from end
7 int left = 0;
8 int right = n - 1;
9 int position = n - 1; // Fill the result array from right to left (largest to smallest)
10
11 // Process elements while left pointer hasn't crossed right pointer
12 while (left <= right) {
13 // Calculate squares of elements at both pointers
14 int leftSquare = nums[left] * nums[left];
15 int rightSquare = nums[right] * nums[right];
16
17 // Place the larger square at the current position in result array
18 if (leftSquare > rightSquare) {
19 result[position] = leftSquare;
20 left++; // Move left pointer forward
21 } else {
22 result[position] = rightSquare;
23 right--; // Move right pointer backward
24 }
25
26 position--; // Move to next position in result array (from right to left)
27 }
28
29 return result;
30 }
31}
32
1class Solution {
2public:
3 vector<int> sortedSquares(vector<int>& nums) {
4 int n = nums.size();
5 vector<int> result(n);
6
7 // Use two pointers approach: left pointer starts from beginning, right from end
8 int left = 0;
9 int right = n - 1;
10 int insertPosition = n - 1; // Fill result array from end to beginning
11
12 // Process elements while pointers haven't crossed
13 while (left <= right) {
14 // Calculate squares of elements at both pointers
15 int leftSquare = nums[left] * nums[left];
16 int rightSquare = nums[right] * nums[right];
17
18 // Place the larger square at current insert position
19 if (leftSquare > rightSquare) {
20 result[insertPosition] = leftSquare;
21 left++; // Move left pointer forward
22 } else {
23 result[insertPosition] = rightSquare;
24 right--; // Move right pointer backward
25 }
26
27 insertPosition--; // Move to next insertion position
28 }
29
30 return result;
31 }
32};
33
1/**
2 * Given an integer array nums sorted in non-decreasing order,
3 * return an array of the squares of each number sorted in non-decreasing order.
4 *
5 * @param nums - Integer array sorted in non-decreasing order
6 * @returns Array of squared values sorted in non-decreasing order
7 */
8function sortedSquares(nums: number[]): number[] {
9 const length: number = nums.length;
10 // Initialize result array with zeros
11 const result: number[] = Array(length).fill(0);
12
13 // Use two pointers approach: left pointer starts from beginning, right from end
14 let leftPointer: number = 0;
15 let rightPointer: number = length - 1;
16 // Fill result array from right to left (largest to smallest)
17 let resultIndex: number = length - 1;
18
19 // Process until pointers meet
20 while (leftPointer <= rightPointer) {
21 // Calculate squares of elements at both pointers
22 const leftSquare: number = nums[leftPointer] * nums[leftPointer];
23 const rightSquare: number = nums[rightPointer] * nums[rightPointer];
24
25 // Place the larger square at current result position
26 if (leftSquare > rightSquare) {
27 result[resultIndex] = leftSquare;
28 leftPointer++;
29 } else {
30 result[resultIndex] = rightSquare;
31 rightPointer--;
32 }
33
34 // Move to next position in result array (from right to left)
35 resultIndex--;
36 }
37
38 return result;
39}
40
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the array nums
. The algorithm uses a two-pointer approach that traverses the array exactly once. In each iteration, either pointer i
moves forward or pointer j
moves backward, and the loop continues until they meet. Since we visit each element exactly once and perform constant-time operations (multiplication and comparison) for each element, the total time complexity is linear. The final reversal operation ans[::-1]
also takes O(n)
time, resulting in an overall time complexity of O(n) + O(n) = O(n)
.
Space Complexity: O(1)
if we ignore the space consumption of the answer array ans
. The algorithm only uses two pointers i
and j
, and two temporary variables a
and b
to store the squared values, all of which require constant extra space. The answer array ans
stores n
elements, but as mentioned in the reference answer, this is typically not counted as extra space since it's required for the output. Therefore, the auxiliary space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle the Case When Pointers Meet
A common mistake is using while left_pointer < right_pointer
instead of while left_pointer <= right_pointer
. This would miss processing the middle element when the pointers converge.
Incorrect:
while left_pointer < right_pointer: # Missing the case when left_pointer == right_pointer # ... rest of the logic
Correct:
while left_pointer <= right_pointer: # Ensures the last element is processed # ... rest of the logic
2. Directly Comparing Original Values Instead of Squares
Some might mistakenly compare the absolute values or original values to determine which square is larger, forgetting that the comparison should be between the actual squared values.
Incorrect:
if abs(nums[left_pointer]) > abs(nums[right_pointer]):
result.append(nums[left_pointer] ** 2)
left_pointer += 1
Correct:
left_square = nums[left_pointer] * nums[left_pointer] right_square = nums[right_pointer] * nums[right_pointer] if left_square > right_square: result.append(left_square) left_pointer += 1
3. Building the Array in Ascending Order with Wrong Pointer Movement
Attempting to build the array directly in ascending order by taking the smaller square can lead to incorrect pointer movement logic and missed elements.
Incorrect Approach:
# Trying to build in ascending order if left_square < right_square: result.append(left_square) left_pointer += 1 # This won't correctly process all elements
The issue here is that you can't determine which pointer to move just by comparing squares when building in ascending order. The smaller square could come from either end depending on the values.
Solution: Stick to building in descending order (taking the larger square each time) and reverse at the end, as this guarantees correct pointer movement.
4. Not Handling Edge Cases
Failing to consider edge cases like:
- Empty array
- Array with single element
- Array with all negative numbers
- Array with all positive numbers
Robust Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
if not nums: # Handle empty array
return []
result = []
left_pointer = 0
right_pointer = len(nums) - 1
while left_pointer <= right_pointer:
left_square = nums[left_pointer] * nums[left_pointer]
right_square = nums[right_pointer] * nums[right_pointer]
if left_square > right_square:
result.append(left_square)
left_pointer += 1
else:
result.append(right_square)
right_pointer -= 1
return result[::-1]
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!