First let me point out a few details.
Use const
intead of let
unless you are going to modify the value after initialization. You use it for the ofNumber
variable, but there are more that deserve it.
But actually there's often no need to define a variable at all if it is used only once. Similarily, storing a return value to a variable and immediately returning that variable is redundant, just return the value returned by the function directly.
function operation(max) {
return Math.floor(max / 10);
}
But you could also stay consistent with the ofNumber
callback like this:
const operation = (max) => Math.floor(max / 10);
Another thing is that we usualy use for
loop in theese cases.
for (let ops = x; ops > 0; --ops) {...}
Now, let's analyse the big-O time complexity of your algorithm.
Adding a comment above each statement in your code. n=nums.length
.
function minSum(nums, x) {
// O(1)
if (nums.length === 0) {
return false;
}
function operation(max) {
// O(1)
let redcuedMax = Math.floor(max / 10);
return redcuedMax
}
let ops = x;
// O(x * inner)
while (ops > 0) {
// O(n)
let max = Math.max(...nums);
// O(1)
const ofNumber = (element) => element >= max ;
// O(n)
let maxIndex = nums.findIndex(ofNumber)
// O(1)
let operated = operation(max);
// O(1)
nums[maxIndex] = operated;
// O(1)
ops--
}
// O(n)
return nums.reduce((prev,next) => prev + next, 0)
}
That makes for O(x * n)
in time. O(1)
in space of course, because you are never making any copies of the array.
How can we optimize this?
Well first thing i see is that there are 2 O(n)
operations in the loop body.
Maybe we can find the maximum's element index in O(n)
.And if we do, we can access the maximum element in O(1)
. This optimization will be less efective if the input is sorted or almost sorted in descendant order, because the second O(n)
operation is basically O(1)
for such sorted input.
Another thing is that after the loop, there is another O(n)
operation. Maybe we can keep track of the sum (updating it in O(1)
time) ever since the first time we needed to scan the entire array. Although, this optimization is x
times less significant, for small x
it may help.
Of course, the most significant improvement can only arise from changing the entire algorithm's big-O complexity from O(x * n)
to something with slower rate of change. Even if it costs us increased memory complexity to say O(n)
.
To do that we have to leave the code for now and let's think about the problem itself.
You wrote:
As a result, you have to perform the operation on the elements with the highest values first.
Good. But is there more? How many of the highest elements will you actually need?
At most x
, right? Either the highest element divided by 10 remains the highest element, in which case you continue with that one, or the next highest element will become the current highest. So maybe we dont want to track just 1 highest element, but x
of them. This may raise our memory complexity to O(min(x,n))
, but that would be still a good tradeoff.
Well, and I think I will break off at this point. I don't wanna write it for you. I hope I gave you enough hints to come up with a faster solution on your own. Just one more thing to say, don't be afraid to use your own specialized loops in such optimizations even if it means your code will grow. It's always trade-offs. Time, space, readability/code size, ... you improve one, you loose on the other... well sometimes not if you got it very wrong on the first shot :D (not saying this is the case:)).
EDIT: I found this article (https://www.google.com/amp/s/www.geeksforgeeks.org/k-largestor-smallest-elements-in-an-array/amp/) which shows several ways to find the x largest elements of an array, some of them seem to be faster then actually sorting the entire array in O(n * log(n))
.