How to efficiently group items based on a comparison function? - javascript

I have a list of items and a comparison function f(item1, item2) which returns a boolean.
I want to generate groups out of these items so that all items in a same group satisfy the condition f(itemi, itemj) === true.
An item can be included in several groups. There is no mimimum size for a group.
I am trying to write an efficient algorithm in javascript (or other language) for that. I thought it would be pretty easy but I am still on it after a day or so...
Any pointer would be highly appreciated!
(the max size of my items array is 1000, if it helps)

OK, so here's how I did it in the end. I am still unsure this is completely correct but it gives good results so far.
There are two phases, first one is about creating groups that match the condition. Second one is about removing any duplicates or contained groups.
Here's the code for the first part, second part if quite trivial:
for(index = 0; index < products.length; index++) {
existingGroups = [];
seedProduct = products[index];
for(secondIndex = index + 1; secondIndex < products.length; secondIndex++) {
candidateProduct = products[secondIndex];
if(biCondition(seedProduct, candidateProduct)) {
groupFound = false;
existingGroups.forEach(function(existingGroup) {
// check if product belongs to this group
isPartOfGroup = true;
existingGroup.forEach(function(product) {
isPartOfGroup = isPartOfGroup && biCondition(product, candidateProduct);
});
if(isPartOfGroup) {
existingGroup.push(candidateProduct);
groupFound = true;
}
});
if(!groupFound) {
existingGroups.push([candidateProduct]);
}
}
}
// add the product to the groups
existingGroups.forEach(function(group) {
group.push(seedProduct);
if(group.length > minSize) {
groups.push(group);
}
});
}
Instead of items I use products, which is my real use case.
The bicondition tests for f(item1, item2) && f(item2, item1). To speed up and avoid duplication of calculus, I created a matrix of all condition results and I use this matrix in the biCondition function.

Related

How to sort pushed array by id?

I have two arrays. First one which comes from response is divided by pagination. When i remove items from second array, they should be placed back sorted by id in first array, instead they go to bottom of array and i have to scroll down to find certain element. This is my code for pushing elements from array vm.feeds to vm.rationList:
function addAll() {
var mList = JSON.parse(JSON.stringify(vm.feeds))
for (var i = 0; i < mList.length; i++) {
mList[i].is_selected = false;
vm.rationList.push(mList[i]);
}
vm.feeds = [];
vm.rationListSafe = vm.rationList;
if(vm.feeds.length == 0){
vm.currentPageMaster++;
vm.isPage = true;
vm.disableScroll = true;
getFeedsByTeam(vm.selectedTeam);
}
}
Second part is how i remove elements from array vm.rationList and push them back to first array - vm.feeds:
function removeAll() {
for (var i = 0; i < vm.rationList.length; i++) {
vm.feeds.push(vm.rationList[i])
}
vm.rationList = []
}
In removeAll() i have to add additional check which will sort them by ID on push. Any idea for this?
The Array.push method add item to the end of your list.
To put an element at a given position in an array, you may prefer using Array.splice.
First argument is the index, second is the number of items to delete (in your case, 0), and a third argument would be the item to add at the given index.
With that, you can replace the push in removeAll by
vm.feeds.splice(correctIndex, 0, vm.rationList[i]);
Question is : what is correctIndex ?
Well, for that, there might be many solutions depending on your architecture, but a simple one could be to loop on vm.feeds to find the first item with an id that is not lower than the one you want to add:
var correctIndex = 0;
for(var item of vm.feeds) {
if (item.id >= vm.rationList[i].id) {break;}
else {correctIndex++;}
}
vm.feeds.sort(function(a, b) {
return parseFloat(a.id) - parseFloat(b.id);
});
This is the solution I found and it works

How do i push an array[i] to another array

Basically i have to create a quiz with 3category. each with 5questions.
I would have to push the selected category-questions into this new array from the array with all the questions. I am unable to do so.
pushSelectedQuestion() {
for (var i = 0; i < this.getNumberOfQuestion; i++) {
if (usercategory == questionPool[i].category) {
mcqSelected.push(questionPool[i])
return mcqSelected;
}
}
}
usercategory = input from user.
if user chooses category 1.
if (1 == questionPool[1].category) (if it matches the category) then it will be pushed.
This is the part which i cant do
Well, from the information you've provided, there's one main issue here - the return statement is definitely shortcutting the loop - so even if you have everything else right, you'll only ever get the first matching question. The rest will have been cut out by the return statement, which stops the function and returns the value.
pushSelectedQuestion() {
for (var i = 0; i < this.getNumberOfQuestion; i++) {
if (usercategory == questionPool[i].category) {
mcqSelected.push(questionPool[i])
// the below line is causing this loop to end after the first time through the list.
// Remove it and then put a console.log(mcqSelected);
// here instead to see the value during each iteration of the loop.
return mcqSelected;
}
}
}
There are a lot of ways to accomplish what you want to do here though. For example, you could just use the javascript Array.filter method like so
let selectedQuestions = questionPool.filter(question => question.category == userCategory)
Maybe I am not understanding your question correctly, but can't you use nested arrays. If the questions are categorized beforehand that is.

Javascript Nested Loop Pushing to Array

I am relatively new to programming and am having some issues with a project I am working on.
msg.newCG2 = [];
for(i=0;i<msg.newCG.length;i++){
for(j=0;j<msg.campaignGroup.length;i++){
if(msg.campaignGroup[j].col10 === msg.newCG[j]){
msg.groupTotals = msg.groupTotals + msg.campaignGroup[j].col11;
}
msg.newCG2.push(msg.newCG[i], msg.groupTotals)
}
}
Basically, for each one of the "IDs" (integers) in msg.newCG, I want to look for each ID in msg.campaignGroup and sum up the totals for all listings with the same ID, from msg.campaignGroup.col11 - then push the ID and the totals to a new array - msg.newCG2.
When I run the code, the first item sent through processes, but grinds to a halt because of memory. I assume this is because of an error in my code.
Where did this code go wrong? I am sure that there are better ways to do this as a whole, but I am curious where I went wrong.
There is a typo in your second for loop and the push needs to happen inside the outer loop.
msg.newCG2 = [];
for(i=0;i<msg.newCG.length;i++){
for(j=0;j<msg.campaignGroup.length;j++){
if(msg.campaignGroup[j].col10 === msg.newCG[i]){
msg.groupTotals = msg.groupTotals + msg.campaignGroup[j].col11;
}
}
msg.newCG2.push(msg.newCG[i], msg.groupTotals)
}
How about:
msg.newCG2 = [];
for (i=0; i < msg.newCG.length; i++) {
var groupTotal = 0;
for (j=0; j < msg.campaignGroup.length; j++) {
if (msg.campaignGroup[j].col10 === msg.newCG[i]){
groupTotal = groupTotal + msg.campaignGroup[j].col11
}
}
msg.newCG2.push(groupTotal)
}
Rather than looping 1.2M times, it would be more efficient to use a single-pass over the 4000 campaign groups, grouping by id to create an array of totals for all ids -- I like using the reduce() function for this:
var cgMap = msg.campaignGroups.reduce(function(arr, grp) {
var grpid = grp.col10;
var count = grp.col11;
var total = arr[grpid] || 0;
arr[grpid] = total + count;
},
[]);
I know, the reduce(...) function is not the easiest to grok, but it takes the second arg (the empty array) and passes it, along with each campaign group object in turn, to that inline function. The result should be a simple array of group totals (from col11), indexed by the group id (from col10).
Now, it's just a matter of returning the totals for those 300 ids found in msg.newCG -- and this map() function does that for us:
var cgOut = msg.newCG.map(function(gid) {
return cgMap[gid]; // lookup the total by group id
}
);
I've made some assumptions here, like the group ids are not terribly large integers, and are rather closely spaced (not too sparse). From the original code, I was not able to determine the format of the data you are wanting to return in msg.newCG2. The final push() function would append 2 integers onto the array -- the output group id and the total for that group. Having pairs of group ids and totals interleaved in a flat array is not a very useful data structure. Perhaps you meant to place the total value into an array, indexed by the group id? If so, you could re-write that line as:
msg.newCG2[msg.newCG[i]] = msg.groupTotals;

Returning duplicates in multidimensional Javascript array

I have searched high and low, not only on StackOverflow, but many other places elsewhere on the web. I've tried what seems like everything, but something is fundamentally flawed with my logic. I apologize for introducing another "Duplicates in Array" question, but I am stuck and nothing seems to be working as expected.
Anyway, I have a multi-dimensional JavaScript array, only 2 levels deep.
var array = [[Part #, Description, Qty:],
[Part #, Description, Qty:],
[Part #, Description, Qty:]]; //etc
What I need to do is create a function that searches array and returns any duplicate "Part #" lines. When they are returned, I would like to have the entire inner array returned, complete with description and qty.
The trick with this is that the Part #'s that would qualify as 'duplicate' would end differently (specifically the last 4 characters), so using String.prototype.substr makes sense (to me).
I know there are duplicates in the array in the way that I am looking for, so I know that if I had the solution, it would return those Part #'s.
Here is what I have tried so far that gets me the closest to a solution:
function findDuplicateResults(arr) {
var result = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i][0].substr(0,5) === arr[++i][0].substr(0,5)) {
result.push(arr[i]);
}
}
return console.log(result);
}
My thinking is that if the element in the array(with substr(0,5) is equal to the next one in line, push that to the result array. I would need the other duplicate in there too. The point of the code is to show only dupes with substr(0,5).
I have tried using Higher Order Functions such as map, forEach, reduce, and filter (filter being the one that boggles my mind as to why it doesn't do what I want), but I have only been able to return [] or the entire array that way. The logic that I use for said Higher Order Functions remains the same (which is probably the problem here).
I am expecting that my if condition is where the most of the problem is. Any pointers or solutions are greatly appreciated.
There is a mistake in your code. When you use ++i, you are changing the value of i, so it is going to skip one item in the next iteration.
Regarding the logic, you are only comparing one item to the next item, when you should really be comparing each item to all items:
function findDuplicateResults(arr) {
var result = [];
for (var i = 0; i <= arr.length - 1; i++) {
for (var k = 0; k <= arr.length - 1; k++) {
if (i !== k && arr[i][0].substr(0,5) === arr[k][0].substr(0,5)) {
result.push(arr[i]);
}
}
}
return result;
}
Although, the 'substr' could be dropped, and 'for' loop could be replaced by a higher order function:
function findDuplicateResults(arr) {
return arr.filter(function(item1){
return arr.filter(function(item2){
return item1[0] === item2[0];
}).length > 1;
});
}

alternatives for excessive for() looping in javascript

Situation
I'm currently writing a javascript widget that displays a random quote into a html element. the quotes are stored in a javascript array as well as how many times they've been displayed into the html element. A quote to be displayed cannot be the same quote as was previously displayed. Furthermore the chance for a quote to be selected is based on it's previous occurences in the html element. ( less occurrences should result in a higher chance compared to the other quotes to be selected for display.
Current solution
I've currently made it work ( with my severely lacking javascript knowledge ) by using a lot of looping through various arrays. while this currently works ( !! ) I find this solution rather expensive for what I want to achieve.
What I'm looking for
Alternative methods of removing an array element from an array, currently looping through the entire array to find the element I want removed and copy all other elements into a new array
Alternative method of calculating and selecting a element from an array based on it's occurence
Anything else you notice I should / could do different while still enforcing the stated business rules under Situation
The Code
var quoteElement = $("div#Quotes > q"),
quotes = [[" AAAAAAAAAAAA ", 1],
[" BBBBBBBBBBBB ", 1],
[" CCCCCCCCCCCC ", 1],
[" DDDDDDDDDDDD ", 1]],
fadeTimer = 600,
displayNewQuote = function () {
var currentQuote = quoteElement.text();
var eligibleQuotes = new Array();
var exclusionFound = false;
for (var i = 0; i < quotes.length; i++) {
var iteratedQuote = quotes[i];
if (exclusionFound === false) {
if (currentQuote == iteratedQuote[0].toString())
exclusionFound = true;
else
eligibleQuotes.push(iteratedQuote);
} else
eligibleQuotes.push(iteratedQuote);
}
eligibleQuotes.sort( function (current, next) {
return current[1] - next[1];
} );
var calculatePoint = eligibleQuotes[0][1];
var occurenceRelation = new Array();
var relationSum = 0;
for (var i = 0; i < eligibleQuotes.length; i++) {
if (i == 0)
occurenceRelation[i] = 1 / ((calculatePoint / calculatePoint) + (calculatePoint / eligibleQuotes[i+1][1]));
else
occurenceRelation[i] = occurenceRelation[0] * (calculatePoint / eligibleQuotes[i][1]);
relationSum = relationSum + (occurenceRelation[i] * 100);
}
var generatedNumber = Math.floor(relationSum * Math.random());
var newQuote;
for (var i = 0; i < occurenceRelation.length; i++) {
if (occurenceRelation[i] <= generatedNumber) {
newQuote = eligibleQuotes[i][0].toString();
i = occurenceRelation.length;
}
}
for (var i = 0; i < quotes.length; i++) {
var iteratedQuote = quotes[i][0].toString();
if (iteratedQuote == newQuote) {
quotes[i][1]++;
i = quotes.length;
}
}
quoteElement.stop(true, true)
.fadeOut(fadeTimer);
setTimeout( function () {
quoteElement.html(newQuote)
.fadeIn(fadeTimer);
}, fadeTimer);
}
if (quotes.length > 1)
setInterval(displayNewQuote, 10000);
Alternatives considered
Always chose the array element with the lowest occurence.
Decided against this as this would / could possibly reveal a too obvious pattern in the animation
combine several for loops to reduce the workload
Decided against this as this would make the code to esoteric, I'd probably wouldn't understand the code anymore next week
jsFiddle reference
http://jsfiddle.net/P5rk3/
Update
Rewrote my function with the techniques mentioned, while I fear that these techniques still loop through the entire array to find it's requirements, at least my code looks cleaner : )
References used after reading the answers here:
http://www.tutorialspoint.com/javascript/array_map.htm
http://www.tutorialspoint.com/javascript/array_filter.htm
http://api.jquery.com/jQuery.each/
I suggest array functions that are mostly supported (and easily added if not):
[].splice(index, howManyToDelete); // you can alternatively add extra parameters to slot into the place of deletion
[].indexOf(elementToSearchFor);
[].filter(function(){});
Other useful functions include forEach and map.
I agree that combining all the work into one giant loop is ugly (and not always possible), and you gain little by doing it, so readability is definitely the winner. Although you shouldn't need too many loops with these array functions.
The answer that you want:
Create an integer array that stores the number of uses of every quote. Also, a global variable Tot with the total number of quotes already used (i.e., the sum of that integer array). Find also Mean, as Tot / number of quotes.
Chose a random number between 0 and Tot - 1.
For each quote, add Mean * 2 - the number of uses(*1). When you get that that value has exceeded the random number generated, select that quote.
In case that quote is the one currently displayed, either select the next or the previous quote or just repeat the process.
The real answer:
Use a random quote, at the very maximum repeat if the quote is duplicated. The data usages are going to be lost when the user reloads/leaves the page. And, no matter how cleverly have you chosen them, most users do not care.
(*1) Check for limits, i.e. that the first or last quota will be eligible with this formula.
Alternative methods of removing an array element from an array
With ES5's Array.filter() method:
Array.prototype.without = function(v) {
return this.filter(function(x) {
return v !== x;
});
};
given an array a, a.without(v) will return a copy of a without the element v in it.
less occurrences should result in a higher chance compared to the other quotes to be selected for display
You shouldn't mess with chance - as my mathematician other-half says, "chance doesn't have a memory".
What you're suggesting is akin to the idea that numbers in the lottery that haven't come up yet must be "overdue" and therefore more likely to appear. It simply isn't true.
You can write functions that explicitly define what you're trying to do with the loop.
Your first loop is a filter.
Your second loop is a map + some side effect.
I don't know about the other loops, they're weird :P
A filter is something like:
function filter(array, condition) {
var i = 0, new_array = [];
for (; i < array.length; i += 1) {
if (condition(array[i], i)) {
new_array.push(array[i]);
}
}
return new_array;
}
var numbers = [1,2,3,4,5,6,7,8,9];
var even_numbers = filter(numbers, function (number, index) {
return number % 2 === 0;
});
alert(even_numbers); // [2,4,6,8]
You can't avoid the loop, but you can add more semantics to the code by making a function that explains what you're doing.
If, for some reason, you are not comfortable with splice or filter methods, there is a nice (outdated, but still working) method by John Resig: http://ejohn.org/blog/javascript-array-remove/

Categories