Time Complexity: 3Sum algorithm under cubic time? - javascript

How can I make use of binary search for improving my algorithms time complexity?
I'm reviewing time complexity for some interviews & I'm having trouble making my algorithm more time efficient. This is my brute force solution for the 3-Sum problem: how many triples sum to exactly 0? Background: I don't have a CS degree.
//BRUTE FORCE SOLUTION: N^3
var threeSum = function(list){
var count = 0;
//checking each triple
for(var i = 0; i < list.length; i++){
for(var j = i+1; j < list.length; j++){
for(var k = j+1; k < list.length; k++){
if(list[i] + list[j] + list[k] === 0){count++;}
}
}
}
return count;
};
//binary search code
var binarySearch = function(target, array){
var lo = 0;
var hi = array.length - 1;
//base case
while(lo <= hi){
var mid = Math.floor( lo + (hi - lo) / 2 );
if(target === array[mid]) return mid;
if(target < array[mid]){
hi = mid - 1;
}
if(target > array[mid]){
lo = mid + 1;
}
}
// value not found
return -1;
}
I was reviewing an algorithms course online from Princeton & the professor noted that this algorithm could be made more efficient with use of a binary search algorithm.
According to the professor we would:
sort the list
for each pair of numbers array[ i ] & array[ j ] binary search for -(array[ i ] + array[ j ])
However, I'm having trouble understanding how binary search comes in to solve the problem. Here is a slide from the lecture, which I'm still trying to understand, but maybe useful to others:
I'm sure there a several efficient solutions out there: feel free to chime in with your implementation as it may help me and other future readers. Thanks

However, I'm having trouble understanding how binary search comes in to solve the problem.
This is how the n^2 log(n) algorithm works:
Sort the list in O(nlogn) time
Find all pairs of numbers (i,j), which is O(n^2) runtime.
Then, for each pair (i,j), it finds a number k where k = sum - j - i. This is constant time O(1)
The algorithm checks to see if each k exists, since the tuple (i,j,k) would sum to sum. To do this, do a binary search which takes log(n) time.
The final runtime would be O(nlogn) + O(logn * n^2) = O(n^2logn)
An alternative (and faster) solution would be to replace the sorting portion with a hashtable. Then, lookup of value k would take O(1) time instead of logn

The problem that the binary search approach is trying to solve is reducing the complexity of a cubic algorithm (this is your brute force algorithm) into a ~ N^2 log N algorithm.
As other commenters pointed out we know that when the following statement: list[i] + list[j] + list[k] == 0 is true than we found a 3SUM result. This is the same as saying that -(list[i] + list[j]) == list[k]. So the goal of the algorithm is to check for each i index and j index pair that there is a corresponding k index which satisfies the previous equation. Binary search can find those k indices in ~log N time. Hence the overall order of growth being ~N^2 log N (the outer for loops correspond to the N^2 part).
As for the implementation in javascript I would do it like this:
var threesum = function(list) {
list.sort(function(a,b){return a - b});
console.log(list);
var cnt = 0;
for(var i=0; i<list.length; i++) {
for(var j=i+1; j<list.length; j++) {
var k = bsearch(-(list[i]+list[j]), list);
if (k!= null && k > j) {
console.log("[i=%d], [j=%d], [k=%d]", i,j,k);
cnt++;
}
}
}
return cnt;
};
var bsearch = function(key, a) {
var lo = 0;
var hi = a.length-1;
while (lo <= hi) {
var mid = lo + Math.floor((hi - lo) / 2);
if (a[mid] < key) {
lo = mid + 1;
} else if (a[mid] > key) {
hi = mid - 1;
} else {
return mid;
}
}
return null;
};
threesum([41, 48, 31, 32, 34, 38, 1, -9, 12, 13, 99, 5, -65, 8, 3, -3])

The algorithm basically works in the following way:
Sort the array (worst-case O(n ^ 2), depending upon sorting algorithm)
Generate all pairs of numbers - takes O(n ^ 2)
for each pair (i , j), there might exist k, such that 0 = i + j + k.kis simply-(i + j), thus we can easily look it up by binary search inO(log n). Cases wherei < k < j` doesn't hold are avoided to exclude duplicates.
Thus total time-complexity is O(n ^ 2 log n).

const threeSum =(array,target)=>{
let result =[]
array = array.sort((a,b)=> a-b)
for(let i=0; i < array.length-2; i++){
let left = i+1;
let right = array.length -1;
while(left < right){
let sum = array[i]+ array[left]+ array[right];
if(sum === target){
result.push([array[i],array[left], array[right]]);
left++;
right--
}else if(sum < target){
//sum is lower than target so increment left pointer
left++;
}else if(sum > target){
//sum is greater than target so increment right pointer
right--;
}
}
}
//return the list
return result;
}
let a = [12, 3, 1, 2, -6, 5, -8, 6];
console.log(threeSum(a, 0));
Time Complexity: O(n^2)
Space Complexity: O(1)

Related

Iterating using a quadratic sequence in Javascript

I have a quadratic sequence in which I need to use to loop over, however, I am not sure how to execute this kind of logic... I want to use the quadratic sequence to count up to a given number <= n. The problem is that I have to give my quadratic sequence for loop some number to count and iterate up to as well... I know it's possible to iterate by 2s, 3s, 4s, etc. by doing things like i += 2, is it possible to use a quadratic expression as well? Is there a method using Javascript in which will allow me to iterate through quadratic sequences? I hope this is clear, thanks!
Here's my code:
The quadratic sequence is the sequence being console logged.
The first 14 terms are: 1,3,6,10,15,21,28,36,45,55,66,78,91,105
for (let i = 1; i <= 14; i++) {
let a = i;
let nthTerm = a - 1;
let b = 1 / 2;
let quadraticSequence = b * a ** 2 + b * a;
console.log(`${nthTerm} ${quadraticSequence}`);
const num = 93;
// use quadratic sequence to count up to a number <= 93;
for (i = 0; i <= num; i++quadraticSequence) {
console.log(i);
}
}
The result should console.log a count 0 > 105. I know the second for loop is not correct, but I'm not quite sure how to approach a proper attempt.
So instead of counting like, 1,2,3,4,5,6,7,8,9... It should count 1,3,6,10,15,21,28,36,45,55... Ultimately, I am trying to count up to a given number <= n (i.e. 93) through the quadratic sequence instead of the usually, 1,2,3,4,5, sequence.
let num = 93;
for (let i = 1, j = 2; i < 100; i += j, j += 1) {
console.log(i);
for (let k = 0; k <= num; k += i) {
console.log(k);
}
}
This will produce the sequence 1, 3, 6, 10, 15, 21, 28, 36, ... and then use each of them in a seperate for loop as step size.
An alternative interpretation would be using the 0th element in the quadratic series as the first step size and the next element as the next step size, etc.
for (let i = 1, j = 2, k = 0; k < 1000; k += i, i += j, j += 1) {
console.log(`Current, step: ${k}, ${i}`);
}
This should do that

javascript for loop to achieve particular output?

How would I get a standard for loop to output in pairs or other groups (like three's of four's) with the output shifting up one after the last digit of the group?
for(var i = 0: i < 8; i++){
console.log(i)
}
so instead of the output being; 0,1,2,3,4,5,6,7
In pairs it would be; 0,1,1,2,2,3,3,4
or if it went up in groups of four; 0,1,2,3,1,2,3,4
I did try doing something like this, but instead of going up in two's every time I need the loop to output the first 2 digits move up one then output the next two ect...
for(var i = 0: i < 8; i+= 2){
console.log(i)
}
Hope that makes sense
For each case you would need to come up with the right formula based on i:
so instead of the output being; 0,1,2,3,4,5,6,7 In pairs it would be; 0,1,1,2,2,3,3,4
for (let i = 1; i < 9; i++) {
console.log(i >> 1); // this bit shift is integer division by 2
}
or if it went up in groups of four; 0,1,2,3,1,2,3,4
for (let i = 0; i < 8; i++) {
// Perform division by 4 and add remainder to that integer quotient
console.log((i >> 2) + (i % 4));
}
You could work with a variable inside the loop to determine the index. This way you can specify how many times you want the loop to run:
for(let index = 0; index < 8; index++) {
const currentIndex = index - (index >> 1);
console.log(currentIndex);
}
It also makes it easy to implement it as immutable:
const array = new Array(8).fill(0).map((entry, index) => index - (index >> 1));
console.log(array);
I think a function like below where we specify the total n and the chunksize after which you want to increase a single step might work for us :-
function getByChunkSteps(n,chunkSize){
let step = -1;
let output = [];
for(let index = 0;index < n;index++){
if(index%chunkSize===0){
step+=1;
}
output.push((index%chunkSize)+step);
}
return output;
}
console.log(getByChunkSteps(10,2));
console.log(getByChunkSteps(8,4));
console.log(getByChunkSteps(9,3));

What is the Big O for the following functions:

I need help with the following problems on determining what the Big O is of each function.
For problem one, I've tried O(log(n)) and O(n). I figured the function was linear or in other words, for N elements we will require N iterations.
For problem two, I've tried O(n^2). I figured for this kind of order, the worst case time (iterations) is the square of the number of inputs. The time grows exponentially related to the number of inputs.
For problem three, I've tried O(n^2) and O(1).
Problem One:
function foo(array){
let sum = 0;
let product = 1;
for (let i = 0; i < array.length; i++){
sum += array[i]
}
for(let i = 0; i < array.length; i++){
product *= array[i];
}
consle.log(sum + ", " + product);
}
Problem Two:
function printUnorderedParis(array){
for(let i = 0; i < array.length; i++){
for(let j = i + j; j < array.length; j++){
console.log(array[i] + ", " + array[j]);
}
}
}
Problem Three:
function printUnorderedPairs(arrayA, arrayB){
for(let i = 0; i < arrayA.length; i++){
for(let j = 0; i < arrayB.length; j++){
for(let k = 0; k < 100000; k++){
console.log(arrayA[i] + ", " + arrayB[j]);
}
}
}
}
I expected my initial thoughts to be right, but maybe I'm having a hard time grasping Big O.
You're correct that it's O(n). You have two loops, they each perform array.length iterations. You could even combine them into a single loop to make it more obvious.
for (let i = 0; i < array.length; i++) {
sum += array[i];
product *= array[i];
}
You're correct, it's O(n^2). The nested loops perform array.length * array.length iterations.
EDIT -- see my comment above asking whether this problem is copied correctly.
This is also O(n^2). The third level of nested loop doesn't change the complexity, because it performs a fixed number of iterations. Since this doesn't depend on the size of the input, it's treated as a constant. So as far as Big-O is concerned, this is equivalent to Problem 2.
Well, you kind of answered your questions, but here we go:
In the first problem, you have two for loops, each of them iterating over the entire array. For a general array of size n, you'll have O(2n) or simply O(n) since we can let go of constants. There isn't any reasons this would be O(log(n)).
For the second one, I think there is a mistake. The statement j = i + j is not valid, and you'll get Uncaught ReferenceError: j is not defined. However, let's say the statement is actually let j = i. Then, we have:
i, iterating over the entire array, starting from the first element and going all the way to the last one
j, starting from i and going all the way to the last element
With this information, we know that for i = 0, j will iterate from 0 to n (n being the array's length), so n steps. For i=1, j will go from 1 to n, so n-1 steps. Generalizing, we are going to have a sum: n + (n - 1) + (n - 2) + ... + 1 + 0 = n * (n + 1) / 2 = 1/2 * (n^2 + n). So, the complexity is O(1/2 * (n^2 + n) = O(n^2 + n) = O(n). So you were correct.
For the third problem, the answer is O(n^2) and not O(1). The reasoning is very close to the one I made for the second one. Basically, the inner k will be executed 100000 times for every j iteration, but the number of iteration does not depend of n (the size of the array).
It's easy to see that:
for i = 0, j will go from 0 to n (last value for which j body will be executed being j = n - 1).
for j = 0, we will to 100k iterations
for j = 1, another 100k iterations
...
for j = n - 1, another 100k iterations
The entire j loop will make n * 100000 = 100000n iterations.
For i = 1, the same behaviour:
for j = 0, we will to 100k iterations
for j = 1, another 100k iterations
...
for j = n - 1, another 100k iterations,
getting another 100000n iterations.
In the end, we end up with 100000n + 100000n + ... + 100000n (n times) = sum(i = 0, n, 100000n) = n * 100000n = 100000 * n^2. So, the big O is O(100000 * n^2) = O(n^2).
Cheers!

Javascript collision explanation for b-tree (Birthday Paradox)?

I want to create a b-tree for storing some data in local storage. I was thinking of using this to find the index of an ID in a sorted list.
If I index an array normally (i.e. to append like array[20032] = 123, what's the big-O of that in Javascript arrays?).
function sortedIndex(array, value) {
var low = 0,
high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
if (array[mid] < value) low = mid + 1;
else high = mid;
}
return low;
}
When I test this with random numbers, I get some collision and it exits before 10k.
for (i = 0; i < 10000; i++) {
var r = Math.random();
array[sortedIndex(array,r)] = r;
}
This exits after a certain time (I'm assuming because of a collision).
I'm thinking it's a birthday paradox kind of thing because the collisions seem to be more likely when the list is already populated (see graph link) (but no exception is thrown...).
I wanted to see the final length of the array after many iterations, I get distribution of final lengths that look like this:
sortedList = []
listLengths = []
for (j = 0; j < 100; j++) {
for (i = 0; i < 10000; i++) {
var r = Math.random();
sortedList[sortedIndex(sortedList,r)] = r;
}
listLengths.push(sortedList.length);
}
graph of final lengths of sorted array after 1-100 iterations of appending attempts
I honestly don't want to deal with this and would also appreciate some pointers on efficient localStorage libraries.
The problem is that you're not shifting all the old elements up when you insert a new element in the array. So you'll extend the array by 1 when the new item is higher than anything else, but just overwrite an existing element when it's less than or equal to the maximum element.
array.splice will insert and move everything over to make room.
array = [];
listLengths = [];
for (j = 0; j < 100; j++) {
for (i = 0; i < 100; i++) {
var r = Math.random();
array.splice(sortedIndex(array, r), 0, r);
}
listLengths.push(array.length);
}
console.log(listLengths);
function sortedIndex(array, value) {
var low = 0,
high = array.length;
while (low < high) {
var mid = (low + high) >>> 1;
if (array[mid] < value) low = mid + 1;
else high = mid;
}
return low;
}

add element to ordered array in CoffeeScript

I have a sorted array, which I add elements to as the server gives them to me. The trouble I'm having is determining where to place my new element and then placing it in the same loop
in javascript this would look like this
for(var i = 0; i < array.length; ++i){
if( element_to_add < array[i]){
array.splice(i,0,element_to_add);
break;
}
}
The problem is that in coffee script I dont have access to the counter, so I cant tell it to splice my array at the desired index.
How can I add an element to a sorted array in CoffeeScript?
The default for loop returns the index as well:
a = [1, 2, 3]
item = 2
for elem, index in a
if elem >= item
a.splice index, 0, item
break
You may want to do a binary search instead.
If you are using Underscore.js (very recommended for these kind of array manipulations), _.sortedIndex, which returns the index at which a value should be inserted into an array to keep it ordered, can come very handy:
sortedInsert = (arr, val) ->
arr.splice (_.sortedIndex arr, val), 0, val
arr
If you're not using Underscore, making your own sortedIndex is not that hard either; is's basically a binary search (if you want to keep its complexity as O(log n)):
sortedIndex = (arr, val) ->
low = 0
high = arr.length
while low < high
mid = Math.floor (low + high) / 2
if arr[mid] < val then low = mid + 1 else high = mid
low
If I understood you correctly, why not save the position, like so,
var pos=-1;
for(var i = 0; i < array.length; ++i){
if( element_to_add < array[i]){
pos=i; break;
}
}
if(pos<0)
array.push(element_to_add);
else array.splice(pos,0,element_to_add);

Categories

Resources