Based on a LinkedIn post originally published on 7 January 2026
During a technical interview, I was asked to solve a deceptively simple C++ problem:
Given a sorted array of integers in ascending order, produce a new array containing the square of each value. The output must also be sorted in ascending order and contain only unique values.
At first glance, the solution appears obvious:
Iterate through the input.
Square every value.
Sort the resulting array.
Remove duplicates.
That approach would produce the correct result. However, it would also ignore the most useful information provided by the problem: the input is already sorted.
The interesting part of the exercise is recognising that this is not really a sorting problem. It is a two-pointer problem.
Why squaring destroys the original order
Consider this input:
-7, -4, -1, 0, 3, 5It is already sorted in ascending order.
Squaring the values in their existing sequence produces:
49, 16, 1, 0, 9, 25The result is no longer sorted.
This happens because the magnitude of a negative number increases as we move towards the beginning of the input array. After squaring, a large negative value may become larger than every positive value.
The largest remaining square must therefore come from one of the two ends of the input:
the negative value with the greatest absolute magnitude on the left; or
the largest positive value on the right.
That observation leads naturally to a two-pointer solution.
The two-pointer approach
We place one pointer at the beginning of the input and another at the end.
At every step:
Square the value at the left pointer.
Square the value at the right pointer.
Compare the two squares.
Add the larger square to an intermediate result.
Move the pointer associated with the selected value.
If both squares are equal, move both pointers.
Because the largest value is selected first, the intermediate result is produced in descending order.
Once every input value has been considered, the result can be reversed to obtain ascending order.
Duplicates can be removed while the descending result is being constructed. Because equal squared values appear next to one another during the merge, no separate set or additional deduplication pass is required.
A C++17 implementation
#include <cstddef>
#include <vector>
auto sortedUniqueSquares(const std::vector<int>& input)
-> std::vector<long long>
{
if (input.empty()) {
return {};
}
std::vector<long long> descending;
descending.reserve(input.size());
std::ptrdiff_t left{0};
std::ptrdiff_t right{
static_cast<std::ptrdiff_t>(input.size()) - 1
};
while (left <= right) {
const auto leftValue{
static_cast<long long>(input[left])
};
const auto rightValue{
static_cast<long long>(input[right])
};
const auto leftSquare{leftValue * leftValue};
const auto rightSquare{rightValue * rightValue};
long long selected{};
if (leftSquare > rightSquare) {
selected = leftSquare;
++left;
} else if (rightSquare > leftSquare) {
selected = rightSquare;
--right;
} else {
selected = leftSquare;
++left;
--right;
}
if (descending.empty() ||
descending.back() != selected) {
descending.push_back(selected);
}
}
return {
descending.rbegin(),
descending.rend()
};
}Walking through an example
Suppose the input is:
-7, -4, -4, -1, 0, 3, 5The initial comparison is between -7 and 5:
(-7)² = 49
5² = 25The algorithm selects 49 and advances the left pointer.
The next comparison is between -4 and 5:
(-4)² = 16
5² = 25It selects 25 and moves the right pointer.
The process continues, producing an intermediate descending sequence:
49, 25, 16, 9, 1, 0The repeated -4 does not create another 16 because duplicates are discarded as the intermediate result is constructed.
Reversing the sequence produces the required output:
0, 1, 9, 16, 25, 49Complexity
Let n be the number of input values.
The two pointers move towards each other, and every input element is considered at most once.
The time complexity is therefore:
O(n)The output and intermediate storage require:
O(n)A solution that squares every value and then applies a general-purpose sort would require:
O(n log n)The difference may not matter for a tiny interview example, but recognising and using existing invariants is an important engineering habit.
Edge cases worth discussing
A good implementation should account for more than the simplest example.
Empty input
An empty input should return an empty result immediately.
One value
A single value should produce a single squared value.
Duplicate values
Repeated input values may produce repeated squares and must be removed.
For example:
-3, -3, 3, 3must produce:
9Different values with identical squares
A negative and positive version of the same value also produce identical squares:
-4, 4The result must contain only one 16.
Zero
Zero sits at the point where the ordering behaviour changes, but it requires no special branch in the two-pointer algorithm.
Integer overflow
Squaring a large int directly as an int may overflow before the result is assigned to a wider type.
For that reason, each input value is converted to long long before multiplication:
const auto value{
static_cast<long long>(input[index])
};
const auto square{value * value};The order of these operations matters. Casting only after multiplication would be too late.
Pointer underflow
Container sizes and indexes are frequently represented using unsigned types. Decrementing an unsigned index below zero causes it to wrap to a very large value.
The implementation uses std::ptrdiff_t for the two moving indexes so that the right pointer can safely move below zero when the final element has been processed.
What the question really tests
The code itself is not especially large. The value of the exercise lies in the reasoning behind it.
It tests whether a candidate can:
recognise that squaring changes the ordering around zero;
use the fact that the input is already sorted;
identify a two-pointer or merge-style solution;
compare algorithmic complexity;
handle duplicates without unnecessary data structures;
consider overflow and index safety;
and communicate the reasoning clearly.
This is why I appreciated the question.
It did not depend on obscure syntax or a memorised trick. It tested whether the candidate could identify the important properties of the data and design an efficient, readable solution around them.
The connection to real engineering
Production engineering problems are rarely solved by applying the most obvious generic operation.
Good solutions often begin by identifying what is already known:
Is the data ordered?
Is a range bounded?
Are values unique?
Is processing sequential?
Can earlier work be reused?
Which invariants remain true as the system changes?
Understanding those properties can eliminate unnecessary work and lead to simpler designs.
The broader lesson is not merely that two pointers are faster than sorting in this particular exercise. It is that engineers should make deliberate use of the structure already present in a problem.
Clear reasoning under constraints is more valuable than cleverness—and much closer to the work required in real systems.
This article is based on my original ideas, experience, analysis and conclusions. Artificial intelligence tools were subsequently used as editorial and research assistants to review grammar and wording, improve structure and presentation, organise some arguments into clearer logical sections, and help review references to legal, regulatory and technical concepts.
Where relevant, factual and regulatory references were checked against the sources cited in the article. AI assistance does not replace professional legal, regulatory, financial or technical advice, and the final selection, interpretation, opinions and conclusions presented here remain my own.
Comments
Post a Comment