Yesterday, I was struggling with a question asked by a company in their pre-screen round. The problem is that you will be given an array of integers. Your task is to move all the elements that have the value of zero into the middle of the array. To make it easier, center = floor(array.length/2).
I already had a solution, but it only works if you have only one zero element in your array.
I hope to receive better answers from you all.
Thank you.
/**
*
* #param {*} arr: a list of integers
* #return: updated list of integers with all zero element moved to the middle
* #example: [4,0,1,1,3] => [4,1,0,1,3]
*/
const zeroesToMid = (arr) => {
const mid = Math.floor(arr.length/2)
let result
for(let i = 0;i < arr.length;i++){
if(arr[i] === 0) {
let firstHalf = arr.splice(0,i)
let secondHalf = arr.splice(i, arr.length)
result = firstHalf.concat(secondHalf)
result.splice(mid,0,0)
}
}
return result
}
Some comments about your attempt:
In each iteration you restart with a result based on arr. So actually you lose the result from every previous iteration.
splice will move several array items: so this is not very efficient, yet you call it several times in each iteration. Moreover, you have at least one conceptual error here:
let firstHalf = arr.splice(0,i)
let secondHalf = arr.splice(i, arr.length)
The first splice will actually remove those entries from arr (making the array shorter), and so the second call to splice will not have the desired result. Maybe you confused splice with slice here (but not later in your code).
If the previous two errors were corrected, then there still is this: after a zero has been moved from index i to a forward position, there is now another value at arr[i]. But as the next iteration of the loop will have incremented i, you will never look at that value, which might have been zero too.
concat creates a new array and you call it in each iteration, making it even more inefficient.
Solution
Better than splicing and concatenating is to copy values without affecting the array size.
If it is necessary that the non-zero values keep their original order, then I would propose the following algorithm:
In a first iteration you could just count the number of zeros. From this you can derive at which range they should be grouped in the result.
In a second iteration you would copy a certain number op non-zero values to the left side of the array. Similarly you would do that at the right side as will. Finally, just fill the center section with zeroes.
So here is an implementation:
function moveZeroes(arr) {
// count zeroes
let numZeroes = 0;
for (let i = 0; i < arr.length; i++) numZeroes += !arr[i];
// Determine target range for those zeroes:
let first = (arr.length - numZeroes) >> 1;
let last = first + numZeroes - 1;
// Move some non-zero values to the left of the array
for (let i = 0, j = 0; i < first; i++, j++) {
while (!arr[j]) j++; // Find next non-zero value
arr[i] = arr[j]; // Move it to right
}
// Move other non-zero values to the right of the array
for (let i = arr.length - 1, j = i; i > last; i--, j--) {
while (!arr[j]) j--; // Find next non-zero value
arr[i] = arr[j]; // Move it to right
}
// Fill the middle section with zeroes:
for (let i = first; i <= last; i++) arr[i] = 0;
return arr;
}
// Demo
console.log(moveZeroes([0, 1, 2, 3, 4, 5, 0, 6, 0, 0]));
NB: If it is not necessary that the non-zero values keep their original order, then you could reduce the number of assignments even more, as you then only need to move the non-zero values that occur in the region where the zeroes should come. The other non-zero values can just stay where they are.
Possible approach:
Problem statement doesn't specify, whether all zeros should be centered exactly (that requires counting zeros first), so we can just squeeze left and right parts, shifting non-zero elements to the corresponding end.
For example, left half might be treated like this sketch
cz = 0;
for(let i = 0; i <= mid;i++) {
if (A[i])
A[i-cz] = A[i];
else
cz++;
}
for(let i = mid - cz + 1; i <= mid;i++) {
A[i] = 0;
}
Now increment mid if cz>0 (so we have already filled central position with zero) and do the same for the right half in reverse direction (backward for-loop, A[i+cz] = A[i]).
Result:
[0, 1, 2, 3, 4, 5, 0, 6, 0, 0] => [1, 2, 3, 4, 0, 0, 0, 0, 5, 6]
As always, trincot does a spot-on analysis of what's wrong with your code.
I have what I think is a simpler solution to the problem though. We can use Array.prototype.filter to select the non-zero entries and then split that in half, then creating an output array by combining the first half with an appropriate number of zeros and then the second half. It might look like this:
const moveZeroes = (
ns,
nonZeroes = ns .filter (n => n !== 0),
len = nonZeroes .length >> 1
) => [
... nonZeroes .slice (0, len),
... Array (ns .length - nonZeroes .length) .fill (0),
... nonZeroes .slice (len)
]
const xs = [0, 1, 2, 3, 4, 5, 0, 6, 0, 0]
console .log (... moveZeroes (xs))
console .log (... xs)
First off, nonZeroes .length >> 1 takes advantage of bit-manipulation to create an expression that is simpler than Math .floor (nonZeroes .length / 2). It's like to be marginally faster too. But they have equivalent behavior.
Notice in the output that the original array did not change. This is very much intentional. I simply much prefer working with immutable data. But if you really want to change the original, we can just use Object.assign, to fold these new values back into the original array:
const moveZeroes = (
ns,
nonZeroes = ns .filter (n => n !== 0),
len = nonZeroes .length >> 1
) => Object .assign (ns, [
... nonZeroes .slice (0, len),
... Array (ns .length - nonZeroes .length) .fill (0),
... nonZeroes .slice (len)
])
const xs = [0, 1, 2, 3, 4, 5, 0, 6, 0, 0]
console .log (... moveZeroes (xs))
console .log (... xs)
Finally, I prefer the style demonstrated here of using only expressions and not statements. But if you don't like the defaulted parameters for extra variables, this could easily be rewritten as follows:
const moveZeroes = (ns) => {
const nonZeroes = ns .filter (n => n !== 0)
const len = nonZeroes .length >> 1
return [
... nonZeroes .slice (0, len),
... Array (ns .length - nonZeroes .length) .fill (0),
... nonZeroes .slice (len)
]
}
and again, we can use Object .assign if we want to.
The big trick here is that we're not actually moving the values, we're extracting some of them, and then simply sticking back together three separate arrays.
here we have to take new array to return the value so it takes space complexity of O(n)
const fun = (arr)=>{
new_array = []
let zero_count = 0
for(let i in arr)
if(!arr[i])
zero_count += 1
let last_index = -1
for(let i = 0; i<arr.length;i++){
if (new_array.length >= Math.floor((arr.length - zero_count)/2)){
last_index = i
break;
}
if(arr[i]) new_array.push(arr[i]);
}
for (let i = 0; i<zero_count;i++)
new_array.push(0)
if (last_index != -1)
for(let i =last_index;i<arr.length;i++)
if(arr[i]) new_array.push(arr[i]);
return new_array
}
console.log(fun([0,1,0,0,2,3,4,0,0]))
resultant output is [1,2,0,0,0,0,0,3,4]
Time Complexity is O(n),
Space Complexity is O(n)
Related
Below is the leetcode question:
"problem is when use sort concat any other method on nums1, it is bot modifying nums1 unless I'm emptying it and pushing new data. Below code is working but what if I dont want to create nums11 and perform every method on nums1."
Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively.
Merge nums1 and nums2 into a single array sorted in non-decreasing order.
The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n.
var merge = function(nums1, m, nums2, n) {
let nums11 = [];
for(let i = 0; i<m; i++){
nums11.push(nums1[i])
}
nums1.length = 0;
nums11 = nums11.concat(nums2);
nums11.sort((a,b) => { return a-b});
for(let i = 0; i<nums11.length;i++){
nums1.push(nums11[i])
}
};
You can maybe try something like this.
for(let i = 0; i<n ; i++){
nums1[i+m-1] = nums2[i]; // adding nums2 elements to the elements with value 0 in nums1
}
nums1.sort((a,b) => {return a-b});
return nums1;
As for the reason it's not getting modified, it could be because of the constraints in the question. It won't let you add more elements to nums1 because it is already the size it needs to be for the solution to fit in it.
If you check the for loop carefully we are pushing the elements into the nums1 array. Remove the line nums1.length=0; and instead of pushing elements into the nums1. Try to change the element on ith index. That will solve the issue.
Like this
nums1[i]=(nums11[i])
This is the complete code.
function merge(nums1, m, nums2) {
let nums11 = [];
for(let i = 0; i<m; i++){
nums11.push(nums1[i])
}
nums11 = nums11.concat(nums2);
nums11.sort((a,b) => { return a-b});
for(let i = 0; i<nums11.length;i++){
nums1[i]=(nums11[i])
}
};
The code in the question makes the length of num1 array to 12.
I hope it will solve the issue.
The requirement of the question is to modify the array in-place and also not use a built sorting method.
Consider the following approach:
Have two pointers i and j that point to the last index (excluding the empty space) of the arrays nums1 and nums2 respectively.
Have a pointer k that points the last index of nums1, this is where we would insert values during the iteration.
Iterate until all elements of the nums2 array have been exhausted.
Put the largest of the two elements at the kth index.
function merge(nums1, m, nums2, n) {
let i = m - 1,
j = n - 1,
k = m + n - 1;
while (j >= 0) {
if (nums1[i] >= nums2[j]) {
nums1[k] = nums1[i];
i -= 1;
} else {
nums1[k] = nums2[j];
j -= 1;
}
k -= 1;
}
}
const nums1 = [1, 4, 8, 11, 0, 0, 0, 0, 0, 0];
const nums2 = [2, 3, 4, 7, 8, 10];
merge(nums1, nums1.length - nums2.length, nums2, nums2.length);
console.log(nums1);
I am working through this solution that I saw in this discussion section of leetcode and am unable to grasp part of the logic. The name of the game is to move all zeros to the end of a given array in place while maintaining the order of the other numbers. The increment operator inside the index of j is where I am lost because wouldn't that place the non-zero number to the right?
var moveZeroes = function(nums) {
let j = 0
for(let i = 0; i < nums.length; i++) {
if(nums[i] !== 0) {
//storing the index we are iterating on
let n = nums[i]
//changing the index in place to 0
nums[i] = 0
//console.log(nums);
//
nums[j++] = n
console.log(nums);
}
}
return nums;
};
console.log(moveZeroes([0,1,0,3,12]));
Remember that j++ is post-increment, so the initial value of j is used to index nums and then j is incremented. If it was ++j then j would be incremented first, then used.
Simply Remove All Zeros. Add Removed Zeros to the end.
var moveZeroes = (nums) =>
(
nums.toString().replaceAll("0,", "") +
",0".repeat(("" + nums).replace(/[^0]/g, "").length)
)
.split(",")
.map(Number);
console.log(moveZeroes([0, 1, 0, 0, 3, 0, 12])); //[ 1, 3, 12, 0, 0, 0, 0 ]
NOTE:
(""+nums).replace(/[^0]/g,'').length: number of 0 in the Array
nums.toString(): To convert array to string we use or we could use the trick of concatenating with an empty string like ""+nums
split(','): To convert string into an array
map(Number): To convert an array of strings to numbers.
Update
OP wants to mutate the array, "Solution A" deals with immutable arrays and "Sulution B" nutates the array.
Solution A (Immuatable)
Try using Array.filter() to separate zeroes (ex. num === 0) and numbers that are not zeroes (ex. num !== 0) into two new and separate arrays. Then use either Array.concat() or the spread operator to merge them into a single array.
// log() is an optional utility function that formats console logs
const log = data => console.log(`[${data}]`);
// This input array has zeroes everywhere
const numbers = [0, 1, 0, 0, 3, 0, 12];
const moveZeroes = array => {
// With the given array called "numbers"
// left = [1, 3, 13]
const left = array.filter(num => num !== 0);
// right = [0, 0, 0, 0]
const right = array.filter(num => num === 0);
// Then arrays "left" and "right" are merged
return [].concat(left, right)
};
log(moveZeroes(numbers));
Solution B (Muatable)
Array.reverse() the array, track the index with Array.entries(), and run it through a for...of loop. If a number isn't a zero, Array.splice() it out then Array.unshift() it to the beginning of the array. Note, .reverse(), .splice(), and .unshift() are mutating Array methods.
// log() is an optional utility function that formats console logs
const log = data => console.log(`[${data}]`);
// This input array has zeroes everywhere
const numbers = [0, 1, 0, 0, 3, 0, 12];
const moveZeroes = array => {
/* .reverse() the array, track index with
.entries(), and loop thru with a for...of
loop */
for (let [idx, num] of array.reverse().entries()) {
/* if a number isn't zero, remove it with
.splice(), then .unshift() it into the
beginning of array */
if (num !== 0) {
array.splice(idx, 1);
array.unshift(num);
}
}
return array;
};
log(moveZeroes(numbers));
I am going through Codility questions and I am on "CountNonDivisible" question. I tried with the brute way it worked and it's not efficient at all.
I found the answers with no explanations, so if someone could take some time and walk me through this answer it would be highly appreciated.
function solution(A) {
const lenOfA = A.length
const counters = Array(lenOfA*2 + 1).fill(0)
for(let j = 0; j<lenOfA; j++) counters[A[j]]++;
return A.map(number=> {
let nonDivisor = lenOfA
for(let i = 1; i*i <= number; i++) {
if(number % i !== 0) continue;
nonDivisor -= counters[i];
if(i*i !== number) nonDivisor -= counters[number/i]
}
return nonDivisor
})
}
This is the question
Task description
You are given an array A consisting of N integers.
For each number A[i] such that 0 ≤ i < N, we want to count the number
of elements of the array that are not the divisors of A[i]. We say
that these elements are non-divisors.
For example, consider integer N = 5 and array A such that:
A[0] = 3
A[1] = 1
A[2] = 2
A[3] = 3
A[4] = 6
For the following elements:
A[0] = 3, the non-divisors are: 2, 6,
A[1] = 1, the non-divisors are: 3, 2, 3, 6,
A[2] = 2, the non-divisors are: 3, 3, 6,
A[3] = 3, the non-divisors are: 2, 6,
A[4] = 6, there aren't any non-divisors.
Write a function:
function solution(A);
that, given an array A consisting of N integers, returns a sequence of
integers representing the amount of non-divisors.
Result array should be returned as an array of integers.
For example, given:
A[0] = 3
A[1] = 1
A[2] = 2
A[3] = 3
A[4] = 6
the function should return [2, 4, 3, 2, 0], as explained above.
Write an efficient algorithm for the following assumptions:
N is an integer within the range [1..50,000];
each element of array A is an integer within the range [1..2 * N].
Here is a way I can explain the above solution.
From the challenge description, it states each element of the array are within the range [1... 2*N] where N is the length of the array; this means that no element in the array can be bigger than 2*N.
So an array of counters is created with length 2*N + 1(max index equal to max possible value in array), and every element in it is initialized to 0, except the elements which actually exists in the given array, those are set to one.
Now we want to go through all the elements in the given array, assuming every number is a nondivisor and subtracting the assumed nondivisors by the number of divisors we have in our array of counters, this will give us the actual number of nondivisors. During the loop, when an element that doesn't exist in our array is a divisor, 0 is subtracted(remember our initialized values), and when we encounter a divisor that is also in our array, we subtract by 1(remember our initialized values). This is done for every element in the array to get each of their nondivisor counts.
The solution you posted makes use of map, which is just a concise way of transforming arrays. A simple for loop can also be used an will be easier to understand. Here is a for loop variation of the solution above
function solution(A) {
const lenOfA = A.length
const counters = Array(lenOfA*2 + 1).fill(0)
for(let i = 0; i<lenOfA; i++) counters[A[i]]++;
const arrayOfNondivisors = [];
for(let i = 0; i < A.length; i++) {
let nonDivisor = lenOfA
for(let j = 1; j*j <= A[i]; j++) {
if(A[i] % j !== 0) continue;
nonDivisor -= counters[j];
if(j*j !== A[i]) nonDivisor -= counters[A[i]/j]
}
arrayOfNondivisors.push(nonDivisor);
}
return arrayOfNondivisors;
}
I am trying to get better at understanding recursion so that I can get better at implementing dynamic programming principles. I am aware this problem can be solved using Kadane's algorithm; however, I would like to solve it using recursion.
Problem statement:
Given an array of integers, find the subset of non-adjacent elements with the maximum sum. Calculate the sum of that subset.
I have written the following partial solution:
const maxSubsetSum = (arr) => {
let max = -Infinity
const helper = (arr, len) => {
if (len < 0) return max
let pointer = len
let sum = 0
while (pointer >= 0) {
sum += arr[pointer]
pointer -= 2
}
return max = Math.max(sum, helper(arr, len - 1))
}
return helper(arr, arr.length - 1)
}
If I had this data:
console.log(maxSubsetSum([3, 5, -7, 8, 10])) //15
//Our subsets are [3,-7,10], [3,8], [3,10], [5,8], [5,10] and [-7,10].
My algorithm calculates 13. I know it's because when I start my algorithm my (n - 2) values are calculated, but I am not accounting for other subsets that are (n-3) or more that still validate the problem statement's condition. I can't figure out the logic to account for the other values, please guide me on how I can accomplish that.
The code is combining recursion (the call to helper inside helper) with iteration (the while loop inside helper). You should only be using recursion.
For each element of the array, there are two choices:
Skip the current element. In this case, the sum is not changed, and the next element can be used. So the recursive call is
sum1 = helper(arr, len - 1, sum)
Use the current element. In this case, the current element is added to the sum, and the next element must be skipped. So the recursive call is
sum2 = helper(arr, len - 2, sum + arr[len])
So the code looks like something this:
const maxSubsetSum = (arr) => {
const helper = (arr, len, sum) => {
if (len < 0) return sum
let sum1 = helper(arr, len - 1, sum)
let sum2 = helper(arr, len - 2, sum + arr[len])
return Math.max(sum1, sum2)
}
return helper(arr, arr.length - 1, 0)
}
Your thinking is right in that you need to recurse from (n-2) once you start with a current index. But you don't seem to understand that you don't need to run through your array to get sum and then recurse.
So the right way is to
either include the current item and recurse on the remaining n-2 items or
not include the current item and recurse on the remaining n-1 items
Lets look at those two choices:
Choice 1:
You chose to include the item at the current index. Then you recurse on the remaining n-2 items. So your maximum could be item itself without adding to any of remaining n-2 items or add to some items from n-2 items.
So Math.max( arr[idx], arr[idx] + recurse(idx-2)) is the maximum for this choice. If recurse(idx-2) gives you -Infinity, you just consider the item at the current index.
Choice 2:
You didn't choose to include the item at the current index. So just recurse on the remaining n-1 items - recurse(n-1)
The final maximum is maximum from those two choices.
Code is :
const maxSubsetSum = (arr) => {
let min = -Infinity
const helper = (arr, idx) => {
if ( idx < 0 ) return min
let inc = helper(arr, idx-2)
let notInc = helper(arr, idx-1)
inc = inc == min ? arr[idx] : Math.max(arr[idx], arr[idx] + inc)
return Math.max( inc, notInc )
}
return helper(arr, arr.length - 1)
}
console.log(maxSubsetSum([-3, -5, -7, -8, 10]))
console.log(maxSubsetSum([-3, -5, -7, -8, -10]))
console.log(maxSubsetSum([-3, 5, 7, -8, 10]))
console.log(maxSubsetSum([3, 5, 7, 8, 10]))
Output :
10
-3
17
20
For the case where all items are negative:
In this case you can say that there are no items to combine together to get a maximum sum. If that is the requirement the result should be zero. In that case just return 0 by having 0 as the default result. Code in that case is :
const maxSubsetSum = (arr) => {
const helper = (arr, idx) => {
if ( idx < 0 ) return 0
let inc = arr[idx] + helper(arr, idx-2)
let notInc = helper(arr, idx-1)
return Math.max( inc, notInc )
}
return helper(arr, arr.length - 1)
}
With memoization:
You could memoize this solution for the indices you visited during recursion. There is only one state i.e. the index so your memo is one dimensional. Code with memo is :
const maxSubsetSum = (arr) => {
let min = -Infinity
let memo = new Array(arr.length).fill(min)
const helper = (arr, idx) => {
if ( idx < 0 ) return min
if ( memo[idx] !== min) return memo[idx]
let inc = helper(arr, idx-2)
let notInc = helper(arr, idx-1)
inc = inc == min ? arr[idx] : Math.max(arr[idx], arr[idx] + inc)
memo[idx] = Math.max( inc, notInc )
return memo[idx]
}
return helper(arr, arr.length - 1)
}
A basic version is simple enough with the obvious recursion. We either include the current value in our sum or we don't. If we do, we need to skip the next value, and then recur on the remaining values. If we don't then we need to recur on all the values after the current one. We choose the larger of these two results. That translates almost directly to code:
const maxSubsetSum = ([n, ...ns]) =>
n == undefined // empty array
? 0
: Math .max (n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
Update
That was missing a case, where our highest sum is just the number itself. That's fixed here (and in the snippets below)
const maxSubsetSum = ([n, ...ns]) =>
n == undefined // empty array
? 0
: Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15
But, as you note in your comments, we really might want to memoize this for performance reasons. There are several ways we could choose to do this. One option would be to turn the array we're testing in one invocation of our function into something we can use as a key in an Object or a Map. It might look like this:
const maxSubsetSum = (ns) => {
const memo = {}
const mss = ([n, ...ns]) => {
const key = `${n},${ns.join(',')}`
return n == undefined
? 0
: key in memo
? memo [key]
: memo [key] = Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns))
}
return mss(ns)
}
console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15
We could also do this with a helper function that acted on the index and memoized using the index for a key. It would be about the same level of complexity.
This is a bit ugly, however, and perhaps we can do better.
There's one issue with this sort of memoization: it only lasts for the current run. It I'm going to memoize a function, I would rather it holds that cache for any calls for the same data. That means memoization in the definition of the function. I usually do this with a reusable external memoize helper, something like this:
const memoize = (keyGen) => (fn) => {
const cache = {}
return (...args) => {
const key = keyGen (...args)
return cache[key] || (cache[key] = fn (...args))
}
}
const maxSubsetSum = memoize (ns => ns .join (',')) (([n, ...ns]) =>
n == undefined
? 0
: Math .max (n, n + maxSubsetSum (ns .slice (1)), maxSubsetSum (ns)))
console.log (maxSubsetSum ([3, 5, -7, 8, 10])) //15
memoize takes a function that uses your arguments to generate a String key, and returns a function that accepts your function and returns a memoized version of it. It runs by calling the key generation on your input, checks whether that key is in the cache. If it is, we simply return it. If not, we call your function, store the result under that key and return it.
For this version, the key generated is simply the string created by joining the array values with ','. There are probably other equally-good options.
Note that we cannot do
const recursiveFunction = (...args) => /* some recursive body */
const memomizedFunction = memoize (someKeyGen) (recursiveFunction)
because the recursive calls in memoizedFunction would then be to the non-memoized recursiveFunction. Instead, we always have to use it like this:
const memomizedFunction = memoize (someKeyGen) ((...args) => /* some recursive body */)
But that's a small price to pay for the convenience of being able to simply wrap up the function definition with a key-generator to memoize a function.
This code was accepted:
function maxSubsetSum(A) {
return A.reduce((_, x, i) =>
A[i] = Math.max(A[i], A[i-1] | 0, A[i] + (A[i-2] | 0)));
}
But trying to recurse that far, (I tried submitting Scott Sauyet's last memoised example), I believe results in run-time errors since we potentially pass the recursion limit.
For fun, here's bottom-up that gets filled top-down :)
function f(A, i=0){
if (i > A.length - 3)
return A[i] = Math.max(A[i] | 0, A[i+1] | 0);
// Fill the table
f(A, i + 1);
return A[i] = Math.max(A[i], A[i] + A[i+2], A[i+1]);
}
var As = [
[3, 7, 4, 6, 5], // 13
[2, 1, 5, 8, 4], // 11
[3, 5, -7, 8, 10] // 15
];
for (let A of As){
console.log('' + A);
console.log(f(A));
}
I am trying to return an array of indexes of values that add up to a given target. I am trying to solve it the fastest way I can!
Examples:
sumOfTwo([1, 2, 4, 4], 8) // => [2, 3]
sumOfTwo([1, 2, 3, 9], 8) // => []
So first I tried a simple brute-force. (Time complexity: O(n^2) )
function sumOfTwo(arr, target) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] + arr[j] === target) {
return [i, j];
}
}
}
return [];
}
Then I tried: (Time complexity: sorting O(n log n) + for loop O(n))
function sumOfTwo(arr, target) {
const sortedArr = arr.sort();
let idxFromBack = arr.length - 1;
for (let [idx, val] of sortedArr.entries()) {
if (val + arr[idxFromBack] > target) {
idxFromBack--;
}
if (val + arr[idxFromBack] === target) {
return [idx, idxFromBack];
}
}
return [];
}
Then I came with this solution that I don't even know the time complexity.
function sumOfTwo(arr, target) {
const complements = [];
for (let [idx, val] of arr.entries()) {
if (complements.reduce((acc, v) => (acc || v.value === val), false)) {
return [complements.find(v => v.value === target - val).index, idx];
}
complements.push({index: idx, value: target - val});
}
return [];
}
I know that I am using a for-loop but I don't know the complexity of the build-in high order functions .reduce() and .find(). I tried a couple of searches but I couldn't find anything.
If anyone can help me would be great! Please include Big-O notation if possible.
Repl.it: https://repl.it/#abranhe/sumOfTwo
Please also include the time complexity of the last solution.
The minimum time complexity of .reduce is O(n), because it must iterate through all elements once (assuming an error isn't thrown), but it can be unbounded (since you can write any code you want inside the callback).
For your
// Loop, O(n), n = length of arr:
for (let [idx, val] of arr.entries()) {
// .reduce, O(n), n = length of complements:
if (complements.reduce((acc, v) => (acc || v.value === val), false)) {
// If test succeeds, .find, O(n), n = length of complements:
return [complements.find(v => v.value === target - val).index, idx];
}
complements.push({index: idx, value: target - val});
}
the time complexity is, worst case, O(n^2). The reduce runs in O(n) time, and you run a reduce for every entry in arr, making it O(n^2).
(The .find is also an O(n) operation, but O(n) + O(n) = O(n))
Your code that sorts the array beforehand has the right idea for decreasing complexity, but it has a couple flaws.
First, you should sort numerically ((a, b) => a - b)); .sort() with no arguments will sort lexiographically (eg, [1, 11, 2] is not desirable).
Second, just decrementing idxFromBack isn't enough: for example, sumOfTwo([1, 3, 8, 9, 9], 9) will not see that 1 and 8 are a match. Perhaps the best strategy here would be to oscillate with while instead: from a idxFromBack, iterate backwards until a match is found or the sum is too small, and also iterate forwards until a match is found or the sum is too large.
You can also improve the performance of this code by sorting not with .sort((a, b) => a - b), which has complexity of O(n log n), but with radix sort or counting sort instead (both of which have complexity of O(n + k), where k is a constant). The optimal algorithm will depend on the general shape and variance of the input.
An even better, altogether different O(n) strategy would be to use a Map or object. When iterating over the array, put the value which would result in a match for the current item into a key of the object (where the value is the current index), and just look to see if the current value being iterated over exists in the object initially:
const sumOfTwo = (arr, target) => {
const obj = {};
for (const [i, num] of arr.entries()) {
if (obj.hasOwnProperty(String(num))) {
return [obj[num], i];
}
const matchForThis = target - num;
obj[matchForThis] = i;
}
return [];
};
console.log(
sumOfTwo([1, 2, 4, 4], 8), // => [2, 3]
sumOfTwo([1, 2, 8, 9], 9), // 1 + 8 = 9; [0, 2]
sumOfTwo([1, 2, 3, 9], 8) // => []
);
As a supplementary answer, here is the algorithm of the find method in the language spec:
When the find method is called, the following steps are taken:
Let O be ? ToObject(this value).
Let len be ? LengthOfArrayLike(O).
If IsCallable(predicate) is false, throw a TypeError exception.
Let k be 0.
Repeat, while k < len,
a. Let Pk be ! ToString(𝔽(k)).
b. Let kValue be ? Get(O, Pk).
c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
d. If testResult is true, return kValue.
e. Set k to k + 1.
Return undefined.
Note the "repeat, while k < len" in step 5. Since time complexity in general measures the worst case scenario (aka the upper bound), we can assume that the searched element is not present in the collection.
The number of iterations made during step 5 then is equal to len which directly depends on the number of elements in the collection. And what time complexity has a direct correlation to the number of elements? Exactly, the linear O(n).
For a visual-ish demonstration, run the following snippet. Apart from some stray dots, the improvized graph should show a linear progression (takes a little while to display in Stack snippets, but you can watch it live in the devtools console):
const iter = 1e7;
const incr = 2e5;
const test = new Array(iter).fill(0);
const steps = Math.ceil(iter / incr);
for (let i = 0; i < steps; i += 1) {
const sub = test.slice(0, i * incr + incr);
const s = Date.now();
const find = sub.find((v) => v === 1);
const e = Date.now();
const d = e - s;
console.log("\u200A".repeat(Math.floor(d/3))+"*");
}