How to sort and search in JavaScript large set - javascript

The array "scores" tells the total points for each person involved in a contest. So for example:
User A: 100 points
User B: 90 points
User C: 90 points
User D: 80 points
User E: 75 points
User F: 60 points
According to above scores we will have this ranking:
User A: #1
User B: #2
User C: #2
User D: #3
User E: #4
User F: #5
This ranking method follows the Dense Ranking method.
Then we have a user named alice. If she gets 55 points, she will rank at position #6 (according to ranking above).
If she scores 90 points, she will rank at position #2. And so on.
I actually have an array containing different "sessions" for alice. So having for example:
[55, 90]
This means that first time will alice be ranked at position #6. While second time she will be ranked at position #2.
I coded this, and it works. However, this does not seem to be very efficient. For large datasets, with half million entries in the scores-array, it times out. This is the code:
const getPosition = (element, scores) => {
scores.push(element);
scores.sort(function (a,b) { return b-a; });
return scores.indexOf(element)+1;
}
function climbingLeaderboard(scores, alice) {
var uniqueSet = new Set(scores);
scores = [...uniqueSet];
var positions = [];
let aliceIndex = 0;
while(aliceIndex < alice.length){
positions.push(getPosition(alice[aliceIndex], scores));
aliceIndex++;
}
return positions;
}
function main() {
const scores = [100, 90, 90, 80, 75, 60];
const alice = [50, 65, 77, 90, 102];
let result = climbingLeaderboard(scores, alice);
console.log(result.join("\n") + "\n");
}
I guess the "sort"-function and/or searching for the element in the array with indexOf is the problem. But I could not find a way to make these two operations more efficient.

Change your getPosition function to below and try. Just removed your sort function and doing the full array search with a condition.
const getPosition = (element, scores) => {
let length = scores.length;
let rank = 1;
for(let i=0; i<length; i++) {
if(scores[i] > element) {
rank++;
}
}
return rank;
}
const scores = [100, 90, 90, 80, 75, 60];
const alice = [50, 65, 77, 90, 102];
console.log(getPosition(77, scores));

Here's another take... Maybe not the most efficient method, but believe it does the trick. This in effect is an implementation of Jonas Wilms' comment to the question.
This is somewhat of a brute force solution, as it walks the scores array for each of alice's scores. A more efficient means would involve sorting alice's scores from highest to lowest (but keeping track of the original order in order to organize the results in the proper order), and then walking both the scores array and alice's array simultaneously.
Note that the solution below runs the test case from the question, in addition to running a test case against an array of 1M scores which is populated with random scores in the range from 99,999 to 0.
function climbingLeaderboard(scores, alice) {
scores.sort( (a, b) => b - a );
aliceRank = [];
for ( let aliceScore of alice ) {
let scoreIndex = 0;
let rank = 0;
while ( scoreIndex < scores.length ) {
if ( scoreIndex === 0 || scores[ scoreIndex - 1 ] !== scores[ scoreIndex ] ) {
rank++;
}
if ( scores[ scoreIndex ] <= aliceScore ) {
aliceRank.push( rank++ );
break;
}
scoreIndex++;
}
if ( scoreIndex === scores.length ) {
aliceRank.push( ++rank );
}
}
return aliceRank;
}
function main() {
const scores = [100, 90, 90, 80, 75, 60];
const alice = [50, 65, 77, 90, 102];
let result = climbingLeaderboard(scores, alice);
console.log(result);
console.log( 'Generating array of 1M scores' );
let scores2 = new Array( 1000000 );
for ( let i = 0; i < scores2.length; i++ ) {
scores2[ i ] = Math.floor( 100000 * Math.random() );
}
alice2 = [50000, 65000, 77000, 90000, 102000, -1];
let result2 = climbingLeaderboard(scores2, alice2);
console.log( `First of the 1M scores is ${scores2[0]} and last score is ${scores2[999999]}` );
console.log( result2 );
}
main();
Hope this helps.

this approach assumes that in case of equal scores Alice should be place first in the score bracket
meaning if she scores 90 then she will be ranked 2nd, behind 100 but above the rest of the 90s
function calculatePositions(scores, aliceScores) {
let positions = [];
const uniqueScoresSet = new Set([...scores]);
const uniqueScoresArray = Array.from(uniqueScoresSet);
aliceScores.forEach((aliceScore) => {
let position = uniqueScoresArray.findIndex((score) => aliceScore >= score);
position = position === -1 ? scores.length : position + 1;
positions.push(position);
});
return positions;
}
function main() {
const scores = [100, 90, 90, 80, 75, 60];
const alice = [50, 65, 77, 90, 102];
let result = calculatePositions(scores, alice);
console.log(result.join("\n") + "\n");
}
this approach assumes that in case of equal scores Alice should be place last in the score bracket
meaning if she scores 90 then she will be ranked 4th, behind 100 and the two other 90s.
function calculatePositions(scores, aliceScores) {
let positions = [];
aliceScores.forEach((aliceScore) => {
let position = scores.findIndex((score) => aliceScore > score);
position = position === -1 ? scores.length : position + 1;
positions.push(position);
});
return positions;
}
function main() {
const scores = [100, 90, 90, 80, 75, 60];
const alice = [50, 65, 77, 90, 102];
let result = calculatePositions(scores, alice);
console.log(result.join("\n") + "\n");
}

Merged your function into one. returning rank as an object in the format { mark : rank}
{
102: 1,
50: 50,
65: 35,
77: 23,
90: 10
}
function climbingLeaderboard(scores, alice) {
scores = [...new Set(scores)];
let length = scores.length;
const rank = alice.reduce((obj, key) => {
obj[key] = 1;
return obj;
}, {});
for (let i = 0; i < length; i++) {
alice.forEach((item) => {
if (scores[i] > item) {
rank[item]++;
}
});
}
return rank;
}
const scores = [];
for (i = 0; i < 500000; i++) {
scores[i] = Math.floor(Math.random() * 100);
}
const alice = [50, 65, 77, 90, 102];
let result = climbingLeaderboard(scores, alice);
console.log(result);

Related

Is there a way to create dynamic ranges given an array of values, and counting how many belong to each range

So i have a frontend with a Piechart and want to show the percentage of the ages in my customers (a database table). I've stored the age of each customer so i have an array like this.
const ages = [12,42,23,42,12,65,75,12,43,54,12,53,24,23,54,64,76,12,42];
Given these values, i want to end up having somenthing like this
const data = {
labels: ['12-20', '21-40', '41-60', '61-76']
dataSet: [4, 6, 2, 5] // This is the amount of ages between each range. The sum of these must be equivalent of the length of the array
}
This is what i've tried so far
const ages = [12, 42, 53, 12, 32, 12, 52, 66, 76, 87, 23, 12, 43, 12, 43, 54, 65].sort((a, b) => a - b);
const minAge = Math.min(...ages);
const maxAge = Math.max(...ages);
const q1 = ages[Math.floor(ages.length / 4)];
const q2 = ages[Math.floor(ages.length / 2)];
const q3 = ages[Math.floor(ages.length * 3 / 4)];
let firstRangeCount = 0;
let secondRangeCount = 0;
let thirdRangeCount = 0;
let fourthRangeCount = 0;
for (const age of ages) {
if (age) {
if (age <= q1) {
firstRangeCount++;
} else if (age <= q2) {
secondRangeCount++;
} else if (age <= q3) {
thirdRangeCount++;
} else {
fourthRangeCount++;
}
}
}
const data = {
labels: [
`${minAge} - ${q1}`,
`${q1} - ${q2}`,
`${q2} - ${q3}`,
`${q3} - ${maxAge}`,
],
datasets: {
label: 'Ages',
data: [firstRangeCount, secondRangeCount, thirdRangeCount, fourthRangeCount],
}
}
But the problem with this solution that it isnt dynamic. If the ages array contains less data, 4 ranges wouldn´t be appropiated.
How can i make this ranges "dynamic"?. I've read some about interquartile range but it didn´t help me much
Try this (Descriptive comments has been added in the below code snippet) :
// Input array
const ages = [12,42,23,42,12,65,75,12,43,54,12,53,24,23,54,64,76,12,42];
// data object with range array
const data = {
labels: ['12-20', '21-40', '41-60', '61-76'],
dataSet: []
}
// declare a variable which will contain the count of occurance based on the range
const obj = {};
// logic to find out the counts and pushed into an range array in an object.
data.labels.forEach(label => {
const splittedLabel = label.split('-')
const filteredAges = ages.filter(e => e >= splittedLabel[0] && e <= splittedLabel[1]);
obj[label] = [];
obj[label].push(...filteredAges);
});
// getting the length
const dataSet = Object.values(obj).map(arr => arr.length);
data.dataSet = dataSet;
// Result
console.log(data);

JS Array Interview Question - Split Array

I was given this problem at one of my interviews and was told I have 20 minutes to solve it. This is the answer I came up with ( 2 versions ). Can you let me know which version you prefer and why, and if you have a better idea of how to solve it (less complex, less memory usage, etc.) Please share.
Problem: You have an array of random numbers that range from 0 to 100 elements.
Write a function that will split this array into several arrays, each containing elements in the following range: (0-10],(10-20],(20-30], etc up to a 100].
Write a function that outputs these arrays in a form of a simple graph, where each delimiter represents a single value in the array.
Array = [10, 12, 71, 52, 51, 1, 5, 22, 21, 6, 95, 11, 3, 64, 45, 55,
65, 42, 99, 4];
Desired outcome:
5 Elements in array: ***** - 1,5,6,3,4
3 Elements in array: *** - 10,12,11
2 Elements in array: ** - 22,21
No Elements in array.
2 Elements in array: ** - 45,42
3 Elements in array: *** - 52,51,55
2 Elements in array: ** - 64,65
1 Elements in array: * - 71
No Elements in array.
2 Elements in array: ** - 95,99
// Version 1
arr = [10, 12, 71, 52, 51, 1, 5, 22, 21, 6, 95, 11, 3, 64, 45, 55, 65, 42, 99, 4];
const splitArray = (inputArray, range) => {
const newArray = [];
do {
let tempArray = [];
tempArray = inputArray.filter((item) => {
if (item >= range && item < range + 10) return item;
});
range += 10;
newArray.push(tempArray);
} while (range + 10 <= 100);
return newArray;
};
const printArrays = (array, delimiter) => {
let toPrint = "";
for (index in array) {
let stars = array[index].length;
let string = "";
for (let i = stars; i > 0; i--) {
string += delimiter;
}
toPrint += stars
? `${stars} Elements in array: ${string} - ${array[index]} \n`
: "No Elements in array. \n";
}
return toPrint;
};
console.log(printArrays(splitArray(arr, 0), "*"));
// Version 2
arr = [10, 12, 71, 52, 51, 1, 5, 22, 21, 6, 95, 11, 3, 64, 45, 55, 65, 42, 99, 4];
const getArrays = (inputArray) => {
const newArray = [];
let min = 0;
let max = 10;
do {
const tempArray = [];
for (i in arr) {
let val = arr[i];
val >= min && val < max ? tempArray.push(val) : "";
}
min += 10;
max += 10;
newArray.push(tempArray);
} while (max <= 100);
return newArray;
};
const printArrays = (array, delimiter) => {
for (index in array) {
let stars = array[index].length;
let string = "";
for (let i = stars; i > 0; i--) {
string += delimiter;
}
console.log(
stars ? `${stars} Elements in array: ${string} - ${array[index]}` : "No Elements in array."
);
}
};
printArrays(getArrays(arr), "^");
Both approaches have moderate issues.
The first approach does
let tempArray = [];
tempArray = inputArray.filter((item) => {
if (item >= range && item < range + 10) return item;
});
Better to just declare the tempArray as the filtered array to begin with.
const tempArray = inputArray.filter(...
Also, return item is suspicious inside a filter - all the filter callback cares about is whether its return value is truthy or falsey. Returning the array item when you actually want to indicate that the value should be included in the output is a common mistake. It happens not to be a problem here because 0 isn't a possibility, but it's still confusing. A better choice would be to do
const tempArray = inputArray.filter(
item => item >= range && item < range + 10
);
(and maybe rename range to startOfRange)
Both of your approaches are also iterating through the entire input array multiple times (once for each range), which seems a bit wasteful - better to iterate through the input once.
Your second approach uses for (i in arr), and both approaches are doing for (index in array). This is a bad idea, and since you don't actually care about the index you're iterating over, it'd make sense to use for..of loops instead.
I think a better looking approach that iterates through the input just once would be:
const arr = [10, 12, 71, 52, 51, 1, 5, 22, 21, 6, 95, 11, 3, 64, 45, 55, 65, 42, 99, 4];
const getArrays = (inputArray) => {
const grouped = {};
for (let i = 0; i < 100; i += 10) {
grouped[i] = [];
}
for (const item of inputArray) {
const rangeProp = Math.floor(item / 10) * 10;
grouped[rangeProp].push(item);
}
return Object.values(grouped);
};
const printArrays = (groupedArrays, delimiter) => {
for (const array of groupedArrays) {
const stars = delimiter.repeat(array.length);
console.log(
stars
? `${array.length} Elements in array: ${stars} - ${array.join(',')}`
: "No Elements in array."
);
}
};
printArrays(getArrays(arr), "*");
I will do that this way :
This approach is simple: it retrieves the values one by one and adds them to the array corresponding to their range.
const arr = [10, 12, 71, 52, 51, 1, 5, 22, 21, 6, 95, 11, 3, 64, 45, 55, 65, 42, 99, 4];
let ranges = arr.reduce((a,x)=>
{
let range = (x/10)|0 // get range start value 0 to 9
a[range] ??= [] // create the array of if it does not already exist
a[range].push(x)
return a
},{})
console.log('ranges=', ranges ) // so that noobs can visualize this result
for (let r = 0; r < 10; r++ )
{
if (!ranges[r])
document.write('No Elements in array.<br>')
else
{
let count = ranges[r].length
document.write(`${count} Elements in array: ${'*'.repeat(count)} - ${ranges[r].join(',')}<br>`)
}
}
.as-console-wrapper {max-height: 100% !important; width:20%; top: 0;
margin-left: 80%; }
.as-console-row::after {display: none !important;}
range = (x/10)|0 // get range start value 0 to 9
example in case of x = 25 -> 25/10 give 2.5 and 2.5 | 0 give 2 -> integer part value of 2.5
| is the OR boolean operator, work only on integers values so it return an interger
??= is Logical nullish assignment

Find upper and lower boundaries in array

I am trying to get the upper and lower boundaries of a numeric value in an array.
const boundaries = [15, 30, 45, 60, 75, 90];
const age = 22;
For the above example, the outcome should be:
[15, 30]
If for example the value is a boundary, it would become the lower value in the outcome array. If it is the max boundary or above, it should become the max value.
Example outcomes:
15 => [15, 30]
22 => [15, 30]
30 => [30, 45]
90 => [90]
I tried mapping through the array and if the age is higher => return boundary. Then filter out the boundaries and calculate the indexes, but this doesn't feel like the correct way to accomplish this.
const boundaries = [15, 30, 45, 60, 75, 90];
const age = 22;
// get all lower values
const allLower = boundaries.map((b) => age > b ? b : null).filter(x => x);
const lower = allLower[allLower.length - 1]; // get lowest
const upper = boundaries[boundaries.indexOf(lower) + 1]; // get next
const result = [lower, upper]; // form result
console.log(result);
Is there a shorter / better / more reliable way to do this?
Why do you use the indices for this? What if the boundaries array is not sorted? Wouldn't it be easier to filter the lists to allLower and allUpper (containing the values below and above the threshold), and then use min and max on the resulting arrays?
Sample code:
const boundaries = [15, 30, 45, 60, 75, 90];
const age = 22;
const allLower = boundaries.filter(x => x < age);
const allUpper = boundaries.filter(x => x > age);
const lowerBound = Math.max(...allLower);
const upperBound = Math.min(...allUpper);
Looks like a good use case for reduce:
const boundaries = [15, 30, 45, 60, 75, 90];
for (let search of [1, 22, 30, 90, 100]) {
let [low, upr] = boundaries.reduce(([low, upr], x) =>
[
x <= search ? Math.max(low, x) : low,
x > search ? Math.min(upr, x) : upr,
],
[-Infinity, +Infinity]
)
console.log(low, '<=', search, '<', upr)
}
This doesn't require boundaries to be sorted. If they always are, you might consider binary search to locate the lower bound.
Looks like a plain for-loop might help you out ;)
function getBounds(age) {
for (let i = 0; i < boundaries.length; i++) {
if (boundaries[i] <= age && (boundaries[i + 1] ?? Infinity) > age) {
return boundaries.slice(i, i + 2);
}
}
}
You could check the previous value and next value and filter the array.
const
getLowerUpper = (array, pivot) => array
.filter((v, i, { [i - 1]: prev, [i + 1]: next }) =>
v <= pivot && next > pivot ||
prev <= pivot && v >= pivot ||
prev === undefined && next > pivot ||
prev < pivot && next === undefined
),
boundaries = [15, 30, 45, 60, 75, 90];
console.log(...getLowerUpper(boundaries, 22)); // between
console.log(...getLowerUpper(boundaries, 30)); // direct and next
console.log(...getLowerUpper(boundaries, 10)); // lowest
console.log(...getLowerUpper(boundaries, 15)); // direct and next
console.log(...getLowerUpper(boundaries, 90)); // highest
console.log(...getLowerUpper(boundaries, 100)); // highest
You can reduce the array, something like this:
const boundaries = [15, 30, 45, 60, 75, 90];
const getResult = (array, target) => {
if (target < array[0] || target > array[array.length - 1]) {
return [];
}
return array.reduce((a, c) => {
if (c <= target) {
a[0] = c;
} else if (c > target && (!a[1] || c < a[a.length - 1])) {
a[a.length] = c;
}
return a;
}, []);
}
console.log(getResult(boundaries, 22));
console.log(getResult(boundaries, 15));
console.log(getResult(boundaries, 30));
console.log(getResult(boundaries, 90));
console.log(getResult(boundaries, 14));
console.log(getResult(boundaries, 91));

Finding max value in array (array.find)

Im learning Javascipt and actually im on episode with array methods.
My imaginary exercise relies on found the Max/Min value in array by array.find method.
Acutally I did smth like that, but script returned me "Undefined".
Please help. :)
const scores = [10, 20, 30, 22, 25, 109, 90];
const maxScore = scores.find(score => {
let max = 0;
for (let i=1; i < scores.length; i++){
if(score[i] > max){
max = score[i];
};
};
return max;
});
console.log(maxScore);
P.S. I know about "Math.max.apply", but I have to do it by array.find and simple loop.
You could take a closure over an index for looping from the end and a temporary max value which is at start undefined and gets the first value from the first element.
Then loop while the value at temp index is smaller than score, store this value in max, repeat.
At the end return the result if index plus one is equal to the temp index.
This approach takes a single loop. find iterates from start of the array and the inner loop from the end of the array if both indices cross, the result is found.
const
scores = [100, 20, 30, 22, 25, 109, 90],
maxScore = scores.find(
((j, max) => (score, i, array) => {
if (max === undefined) {
max = score;
j = array.length;
}
if (score < max) return;
while (array[j - 1] < score) max = array[--j];
return i + 1 === j;
})
()
);
console.log(maxScore);
The simplest way to do it, without using any Array methods, can be written as:
const maxScore = (scores) => {
let score = 0;
for ( let i = 0; i < scores.length; i++ ) {
if(scores[i] > score) {
score = scores[i]
}
}
return score;
}
From MDN:
The find() method returns the value of the first element
in the provided array that satisfies the provided testing function.
Lets redefine our simple function again,
const maxScore = scores => {
let score = Number.NEGATIVE_INFINITY;
scores.forEach(element => {
let acc = scores.find(number => number > score);
if(!isNaN(acc)) {
score = acc;
}
})
return score;
}
find works on each array element. So take the max outside the find method & log max. Besides there were two typos
const scores = [10, 20, 30, 22, 25, 109, 90];
let max = 0;
const maxScore = scores.find((score) => {
for (let i = 1; i < scores.length; i++) {
if (scores[i] > max) {
max = scores[i];
};
};
return max;
});
console.log(max)
Try this:
const scores = [10, 20, 30, 22, 25, 109, 90];
let max = 0;
scores.find(score => { if(score > max) max = score });
console.log(max);
Your current code is looping the scores array whilst its already looping it, JavaScripts .find, essentially loops the array.
const scores = [10, 20, 30, 22, 25, 109, 90];
scores.reduce(function(a,b) { return a > b ? a : b });
// 109

Return indexes of greatest value in a 3 dimensional array using JavaScript

I have an array of this:
[34, 12, 56]
[100,125,19]
[30,50,69]
125 has been the highest value, it will return the index [1,1] format. Meaning 125 which is the highest value will return row 1 column 1
I was able to get the index in an array using this code
var a = [0, 21, 22, 7, 12];
var indexOfMaxValue = a.reduce((iMax, x, i, arr) => x > arr[iMax] ? i :
iMax, 0);
document.write("indexOfMaxValue = " + indexOfMaxValue); // prints
"indexOfMaxValue = 2"
Here's my approach. It flattens out all the arrays into more managable one, finds the max number and its index, and then calculates it's position using some math. Using a single array makes this calculation much easier.
const arr = [[34, 12, 56], [100,125,19], [30,50,69]];
const arr2 = [0, 21, 22, 7, 12];
function findHighest(arr) {
// Get the number of columns
const cols = arr.length;
// Flatten out the arrays
const tempArr = arr.flatMap(el => el);
// Get the max number from the array
const max = Math.max.apply(null, tempArr);
// Find its index
const indexMax = tempArr.findIndex(el => el === max);
// Find the remainder (modulo) when you divide the index
// by the number of columns
const mod = indexMax % cols;
// Return the final array output
return [Math.floor(indexMax / cols), mod];
}
console.log(findHighest(arr))
console.log(findHighest(arr2))
This will give the expected output but not sure is it good way to solve this:
var arr = [
[34, 12, 56],
[100, 125, 19],
[30, 50, 69]
];
var maxValue, maxIndex;
arr.forEach((arr1, i) => {
arr1.forEach((value, j) => {
if (i == 0 && j == 0) {
maxValue = value;
maxIndex = [i, j]
} else {
if (maxValue < value) {
maxValue = value;
maxIndex = [i, j];
}
}
});
});
console.log("Max Number Index", maxIndex);
If you mean 2d solution, try this. Should work for dynamic length arrays
This should be extendable with a new forEach for a new dimension
[100,125,19],
[30,50,69]];
maxIndex = [-1, -1];
maxElem = 0;
input.forEach(function(arr, row) {
console.error(row);
arr.forEach(function(e, col) {
if( maxElem <= e ) {
maxElem = e;
maxIndex = [row, col];
}
})
})
console.log(maxIndex)

Categories