Goal
I've got a DOM with about 70 elements on it (divs with some content) . I need to move and toggle the display of those divs quite a lot and also quite fast. The speed is one of the most important things. The trigger for moving and toggling these divs is a search query, kind of like Google Instant, except that all the DOM elements I move around and toggle are loaded the first time (so no more calls to the server).
Implementation
I've implemented this in the following way: alongside the DOM I pass in a JavaScript array of objects representing the divs along with their attributes like position, contents etcetera. This array acts like a mirror to the DOM. When the user starts typing I start looping through the array and calculating, per div/object, what needs to be done to it. I actually loop over this array a couple of times: I first check if I need to look at a div/object, then I look at the object, then whether I need to look at the contents, then I look at the contents.
One of the things I do in these loops is the setting of flags for DOM-manipulation. As I understand it, reading and manipulating and the DOM is one of the slower operations in JavaScript, as compared to the other stuff I'm doing (looping, reading and writing object attributes etc.). I also did some profiling, confirming this assumption. So at every corner I've tried to prevent "touching" the DOM to increase performance. At the end of my algorithm I loop once more, execute all the necessary DOM actions and reset the flags to signal they've been read. For cross-browser compatibility I use jQuery to actually do the DOM actions (selecting, moving, toggling). I do not use jQuery to loop over my array.
Problem
My problem now is that I think my code and data structure is a bit ugly. I have this rather
large multidimensional array with lots of attributes and flags. I repeatedly loop over it with functions calling functions calling functions. When running into problems I can (still) somewhat easily debug stuff, but it doesn't feel right.
Question
Is there a design pattern or common solution to this kind of problem? I suspect I could implement some sort of smart coupling between the array and the DOM where I would not have to explicitly set flags and execute DOM actions, but I've no idea how such a coupling should work or if it's even a good idea or just complicating things.
Are there any other data-structure or algorithmic principles I've overlooked when solving this problem?
Thanks!
Update
As requested I've added my code, it's about 700 lines. Note: I'm not polluting the global namespace, these functions are defined and used inside a closure.
/**
* Applies the filter (defined by the currentQuery and to the cats array)
*
* -checks whether matching is needed
* -if needed does the matching
* -checks whether DOM action is needed
* -if needed executes DOM action
*
* cats is an array of objects representing categories
* which themselves contain an array of objects representing links
* with some attributes
*
* cats = (array) array of categories through which to search
* currentQuery = (string) with which to find matches within the cats
* previousQuery = (string) with previously-typed-in query
*
* no return values, results in DOM action and manipulation of cats array
*/
function applyFilter(cats,currentQuery, previousQuery) {
cats = flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery);
cats = matchCats(cats,currentQuery);
cats = flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery);
cats = matchLinks(cats,currentQuery);
cats = flagIfDisplayToggleNeeded(cats);
if ( currentQuery.length > 0 ) {
cats = flagIfMoveNeeded(cats);
} else {
// move everything back to its original position
cats = flagMoveToOriginalPosition(cats);
}
// take action on the items that need a DOM action
cats = executeDomActions(cats);
}
/**
* Sets a flag on a category if it needs matching, parses and returns cats
*
* Loops through all categories and sets a boolean to signal whether they
* need matching.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/
function flagIfMatchingIsNeededForCats(cats,currentQuery,previousQuery) {
var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);
// check if matching is necessary for categories
for (var i = 0; i < cats.length; i++) {
cats[i].matchingNeeded = isMatchingNeededForCat(
cats[i].matches
,newQueryIsLonger
,currentQuery.length
,cats[i].noMatchFoundAtNumChars
);
}
return cats;
}
/**
* Whether the new query is longer than the previous one
*
* currentQuery = (string) the currently typed-in query
* previousQuery = (string) the query that was previously typed in
*
* returns (boolean) true/false
*/
function isNewQueryLonger(currentQuery, previousQuery) {
if (previousQuery == false) {
return true;
}
return currentQuery.length > previousQuery.length
}
/**
* Deduces if a category needs to be matched to the current query
*
* This function helps in improving performance. Matching is done using
* indexOf() which isn't slow of itself but preventing even fast processes
* is a good thing (most of the time). The function looks at the category,
* the current and previous query, then decides whether
* matching is needed.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a
* category when it switches from being matched to being not-matched. The
* number indicates the number of characters in the first query that did
* not match the category. This helps in performance because we don't need
* to recheck the categoryname if it doesn't match now and the new query is
* even longer.
*
* returns (boolean) true/false
*/
function isMatchingNeededForCat(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
if (typeof(currentlyMatched) == 'undefined') {
// this happens the first time we look at a category, for all
// categories this happens with an empty query and that matches with
// everything
currentlyMatched = true;
}
if (currentlyMatched && newQueryIsLonger) {
return true;
}
if (!currentlyMatched && !newQueryIsLonger) {
// if currentlyMatched == false, we always have a value for
// noMatchFoundAtNumChars
// matching is needed if the first "no-match" state was found
// at a number of characters equal to or bigger than
// queryLength
if ( queryLength < noMatchFoundAtNumChars ) {
return true;
}
}
return false;
}
/**
* Does matching on categories for all categories that need it.
*
* Sets noMatchFoundAtNumChars to a number if the category does not match.
* Sets noMatchFoundAtNumChars to false if the category matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchCats(cats,currentQuery) {
for (var i = 0; i < cats.length; i++) {
if (cats[i].matchingNeeded) {
cats[i].matches = categoryMatches(cats[i],currentQuery);
// set noMatchFoundAtNumChars
if (cats[i].matches) {
cats[i].noMatchFoundAtNumChars = false;
} else {
cats[i].noMatchFoundAtNumChars = currentQuery.length;
}
}
}
return cats;
}
/**
* Check if the category name matches the query
*
* A simple indexOf call to the string category_name
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function categoryMatches(category,query) {
catName = category.category_name.toLowerCase();
if (catName.indexOf(query) !== -1 ) {
return true;
}
return false;
}
/**
* Checks links to see whether they need matching
*
* Loops through all cats, selects the non-matching, for every link decides
* whether it needs matching
*
* cats = (array) an array with all the category-objects in it
* currentQuery = the currently typed-in query
* previousQuery = the query that was previously typed in
*
* returns (array) cats, possibly in a different state
*/
function flagIfMatchingIsNeededForLinks(cats,currentQuery,previousQuery) {
var newQueryIsLonger = isNewQueryLonger(currentQuery, previousQuery);
for (var i = 0; i < cats.length; i++) {
if (!cats[i].matches) { // only necessary when cat does not match
for (var k = 0; k < cats[i].links.length; k++) {
cats[i].links[k].matchingNeeded = isMatchingNeededForLink(
cats[i].links[k].matches
,newQueryIsLonger
,currentQuery.length
,cats[i].links[k].noMatchFoundAtNumChars
);
}
}
}
return cats;
}
/**
* Checks whether matching is needed for a specific link
*
* This function helps in improving performance. Matching is done using
* indexOf() for every (relevant) link property, this function helps decide
* whether that *needs* to be done. The function looks at some link
* properties, the current and previous query, then decides whether
* matching is needed for the link.
*
* currentlyMatched = (boolean) on whether the boolean was matched to the previous query
* newQueryIsLonger = (boolean) whether the new query is longer
* queryLength = (int) the length of the current query
* noMatchFoundAtNumChars = (int) this variable gets set (to an int) for a
* link when it switches from being matched to being not-matched. The
* number indicates the number of characters in the first query that did
* not match the link. This helps in performance because we don't need
* to recheck the link properties in certain circumstances.
*
* return (boolean) true/false
*/
function isMatchingNeededForLink(currentlyMatched, newQueryIsLonger ,queryLength ,noMatchFoundAtNumChars) {
if (typeof(currentlyMatched) == 'undefined') {
// this happens to a link the first time a cat does not match and
// we want to scan the links for matching
return true;
}
if (currentlyMatched && newQueryIsLonger) {
return true;
}
if (!currentlyMatched && !newQueryIsLonger) {
// if currentlyMatched == false, we always have a value for
// noMatchFoundAtNumChars
// matching is needed if the first "no-match" state was found
// at a number of characters equal to or bigger than
// queryLength
if ( queryLength < noMatchFoundAtNumChars ) {
return true;
}
}
return false;
}
/**
* Does matching on links for all links that need it.
*
* Sets noMatchFoundAtNumChars to a number if the link does not match.
* Sets noMatchFoundAtNumChars to false if the link matches once again.
*
* cats = (array) an array with all the category-objects in it
* currentQuery = (string) the currently typed-in query
*
* returns (array) cats, possibly in a different state
*/
function matchLinks(cats,currentQuery) {
for (var i = 0; i < cats.length; i++) {
// category does not match, check if links in the category match
if (!cats[i].matches) {
for (var k = 0; k < cats[i].links.length; k++) {
if (cats[i].links[k].matchingNeeded) {
cats[i].links[k].matches = linkMatches(cats[i].links[k],currentQuery);
}
// set noMatchFoundAtNumChars
if (cats[i].links[k].matches) {
cats[i].links[k].noMatchFoundAtNumChars = false;
} else {
cats[i].links[k].noMatchFoundAtNumChars = currentQuery.length;
}
}
}
}
return cats;
}
/**
* Check if any of the link attributes match the query
*
* Loops through all link properties, skips the irrelevant ones we use for filtering
*
* category = (object) a category object
* query = (string) the query
*
* return (boolean) true/false
*/
function linkMatches(link,query) {
for (var property in link) {
// just try to match certain properties
if (
!( // if it's *not* one of the following
property == 'title'
|| property == 'label'
|| property == 'url'
|| property == 'keywords'
|| property == 'col'
|| property == 'row'
)
){
continue;
}
// if it's an empty string there's no match
if( !link[property] ) {
continue;
}
var linkProperty = link[property].toLowerCase();
if (linkProperty.indexOf(query) !== -1){
return true;
}
}
return false;
}
/**
* Flags if toggling of display is needed for a category.
*
* Loops through all categories. If a category needs some DOM
* action (hiding/showing) it is flagged for action. This helps in
* performance because we prevent unnecessary calls to the DOM (which are
* slow).
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfDisplayToggleNeeded(cats) {
for (var i = 0; i < cats.length; i++) {
// this happens the first time we look at a category
if (typeof(cats[i].currentlyDisplayed) == 'undefined') {
cats[i].currentlyDisplayed = true;
}
var visibleLinks = 0;
// a cat that matches, all links need to be shown
if (cats[i].matches) {
visibleLinks = cats[i].links.length;
} else {
// a cat that does not match
for (var k = 0; k < cats[i].links.length; k++) {
if (cats[i].links[k].matches) {
visibleLinks++;
}
}
}
// hide/show categories if they have any visible links
if (!cats[i].currentlyDisplayed && visibleLinks > 0 ) {
cats[i].domActionNeeded = 'show';
} else if( cats[i].currentlyDisplayed && visibleLinks == 0 ){
cats[i].domActionNeeded = 'hide';
}
}
return cats;
}
/**
* Flags categories to be moved to other position.
*
* Loops through all categories and looks if they are distributed properly.
* If not it moves them to another position. It remembers the old position so
* it can get the categories back in their original position.
*
* cats = (array) an array with all the category-objects in it
*
* returns (array) cats, possibly in a different state
*/
function flagIfMoveNeeded(cats) {
var numCats, numColumns, displayedCats, i, moveToColumn, tmp;
numColumns = getNumColumns(cats);
numDisplayedCats = getNumDisplayedCats(cats);
columnDistribution = divideInPiles(numDisplayedCats, numColumns);
// optional performance gain: only move stuff when necessary
// think about this some more
// we convert the distribution in columns to a table so we get columns
// and positions
catDistributionTable = convertColumnToTableDistribution(columnDistribution);
// sort the categories, highest positions first
// catPositionComparison is a function to do the sorting with
// we could improve performance by doing this only once
cats = cats.sort(catPositionComparison);
for (i = 0; i < cats.length; i += 1) {
if( categoryWillBeDisplayed(cats[i]) ){
tmp = getNewPosition(catDistributionTable); // returns multiple variables
catDistributionTable = tmp.catDistributionTable;
cats[i].moveToColumn = tmp.moveToColumn;
cats[i].moveToPosition = tmp.moveToPosition;
} else {
cats[i].moveToColumn = false;
cats[i].moveToPosition = false;
}
}
return cats;
}
/**
* A comparison function to help the sorting in flagIfMoveNeeded()
*
* This function compares two categories and returns an integer value
* enabling the sort function to work.
*
* cat1 = (obj) a category
* cat2 = (obj) another category
*
* returns (int) signaling which category should come before the other
*/
function catPositionComparison(cat1, cat2) {
if (cat1.category_position > cat2.category_position) {
return 1; // cat1 > cat2
} else if (cat1.category_position < cat2.category_position) {
return -1; // cat1 < cat2
}
// the positions are equal, so now compare on column, if we need the
// performance we could skip this
if (cat1.category_column > cat2.category_column) {
return 1; // cat1 > cat2
} else if (cat1.category_column < cat2.category_column) {
return -1; // cat1 < cat2
}
return 0; // position and column are equal
}
/**
* Checks if a category will be displayed for the currentQuery
*
* cat = category (object)
*
* returns (boolean) true/false
*/
function categoryWillBeDisplayed(cat) {
if( (cat.currentlyDisplayed === true && cat.domActionNeeded !== 'hide')
||
(cat.currentlyDisplayed === false && cat.domActionNeeded === 'show')
){
return true;
} else {
return false;
}
}
/**
* Gets the number of unique columns in all categories
*
* Loops through all cats and saves the columnnumbers as keys, insuring
* uniqueness. Returns the number of
*
* cats = (array) of category objects
*
* returns (int) number of unique columns of all categories
*/
function getNumColumns(cats) {
var columnNumber, uniqueColumns, numUniqueColumns, i;
uniqueColumns = [];
for (i = 0; i < cats.length; i += 1) {
columnNumber = cats[i].category_column;
uniqueColumns[columnNumber] = true;
}
numUniqueColumns = 0;
for (i = 0; i < uniqueColumns.length; i += 1) {
if( uniqueColumns[i] === true ){
numUniqueColumns += 1
}
}
return numUniqueColumns;
}
/**
* Gets the number of categories that will be displayed for the current query
*
* cats = (array) of category objects
*
* returns (int) number of categories that will be displayed
*/
function getNumDisplayedCats(cats) {
var numDisplayedCats, i;
numDisplayedCats = 0;
for (i = 0; i < cats.length; i += 1) {
if( categoryWillBeDisplayed(cats[i]) ){
numDisplayedCats += 1;
}
}
return numDisplayedCats;
}
/**
* Evenly divides a number of items into piles
*
* Uses a recursive algorithm to divide x items as evenly as possible over
* y piles.
*
* items = (int) a number of items to be divided
* piles = (int) the number of piles to divide items into
*
* return an array with numbers representing the number of items in each pile
*/
function divideInPiles(items, piles) {
var averagePerPileRoundedUp, rest, pilesDivided;
pilesDivided = [];
if (piles === 0) {
return false;
}
averagePerPileRoundedUp = Math.ceil(items / piles);
pilesDivided.push(averagePerPileRoundedUp);
rest = items - averagePerPileRoundedUp;
if (piles > 1) {
pilesDivided = pilesDivided.concat(divideInPiles(rest, piles - 1)); // recursion
}
return pilesDivided;
}
/**
* Converts a column distribution to a table
*
* Receives a one-dimensional distribution array and converts it to a two-
* dimensional distribution array.
*
* columnDist (array) an array of ints, example [3,3,2]
*
* returns (array) two dimensional array, rows with "cells"
* example: [[true,true,true],[true,true,true],[true,true,false]]
* returns false on failure
*/
function convertColumnToTableDistribution(columnDist) {
'use strict';
var numRows, row, numCols, col, tableDist;
if (columnDist[0] === 'undefined') {
return false;
}
// the greatest number of items are always in the first column
numRows = columnDist[0];
numCols = columnDist.length;
tableDist = []; // we
for (row = 0; row < numRows; row += 1) {
tableDist.push([]); // add a row
// add "cells"
for (col = 0; col < numCols; col += 1) {
if (columnDist[col] > 0) {
// the column still contains items
tableDist[row].push(true);
columnDist[col] -= 1;
} else {
tableDist[row][col] = false;
}
}
}
return tableDist;
}
/**
* Returns the next column and position to place a category in.
*
* Loops through the table to find the first position that can be used. Rows
* and positions have indexes that start at zero, we add 1 in the return
* object.
*
* catDistributionTable = (array) of rows, with positions in them
*
* returns (object) with the mutated catDistributionTable, a column and a
* position
*/
function getNewPosition(catDistributionTable) {
var numRows, row, col, numCols, moveToColumn, moveToPosition;
numRows = catDistributionTable.length;
findposition:
for (row = 0; row < numRows; row += 1) {
numCols = catDistributionTable[row].length;
for ( col = 0; col < numCols; col += 1) {
if (catDistributionTable[row][col] === true) {
moveToColumn = col;
moveToPosition = row;
catDistributionTable[row][col] = false;
break findposition;
}
}
}
// zero-indexed to how it is in the DOM, starting with 1
moveToColumn += 1;
moveToPosition += 1;
return {
'catDistributionTable' : catDistributionTable
,'moveToColumn' : moveToColumn
,'moveToPosition' : moveToPosition
};
}
/**
* Sets the target position of a category to its original location
*
* Each category in the DOM has attributes defining their original position.
* After moving them around we might want to move them back to their original
* position, this function flags all categories to do just that.
*
* cats = (array) of category objects
*
* All of the possible return values
*/
function flagMoveToOriginalPosition(cats) {
for (i = 0; i < cats.length; i += 1) {
cats[i].moveToColumn = cats.category_column;
cats[i].moveToPosition = cats.category_position;
}
return cats;
}
/**
* Execute DOM actions for the items that need DOM actions
*
* Parses all categories, executes DOM actions on the categories that
* require a DOM action.
*
* cats = (array) an array with all the category-objects in it
*
* no return values
*/
function executeDomActions(cats) {
for (var i = 0; i < cats.length; i++) {
var category_id = cats[i].category_id;
// toggle display of columns
if (cats[i].domActionNeeded == 'show') {
showCategory(category_id);
cats[i].currentlyDisplayed = true;
}
if (cats[i].domActionNeeded == 'hide') {
hideCategory(category_id);
cats[i].currentlyDisplayed = false;
}
cats[i].domActionNeeded = false;
// for every currentlyDisplayed category move it to new location
// if necessary
if (cats[i].currentlyDisplayed && cats[i].moveToColumn !== false) {
cats[i] = moveCat(cats[i]);
}
}
return cats;
}
/**
* Show a certain category
*
* category_id = (int) the id of the category that needs to be shown
*
* no return values
*/
function showCategory(category_id) {
$('#' + category_id).show();
}
/**
* Hide a certain category
*
* category_id = (int) the id of the category that needs to be hidden
*
* no return values
*/
function hideCategory(category_id) {
$('#' + category_id).hide();
}
/**
* Moves a category to the position set in its attributes
*
* A category can have attributes defining the column and position (or row)
* this function moves the category to the correct column and position.
*
* cat = (object) category
*
* returns (object) category
*/
function moveCat(cat) {
var columnSelector, catSelector;
columnSelector = '#column' + cat.moveToColumn + ' .column_inner' + ' .hiddenblocks';
catSelector = '#' + cat.category_id;
$(columnSelector).prepend($(catSelector));
// reset target coordinates
cat.moveToColumn = false;
cat.moveToPosition = false;
return cat;
}
Well commented and formatted JavaScript, kudos sir!
First off, it seams like your use-case would be perfect for a SQL database query. Sending your query to a DB and getting the category IDs and locations back would be much simpler than your current implementation. I assume you do it all client side because you don't have access to a DB, your data is fairly static, or you're not confident in the real-time speed of your database.
To speed up your current implementation lower case and concatenate all your Link data properties into one property before hand.
function linkMatches(link,query) {
if (link["ConcatenatedLCasedProperties"].indexOf(query) !== -1){
return true;
}
return false;
}
EDIT Here's a faster/more efficient version of your divideInPiles function.
function divideInPiles(items, piles) {
var result = [];
var perPile = Math.floor(items/piles);
var leftOver = items % piles;
if(piles == 0)
return false;
for(var x=0; x<piles; x++)
result.push(perPile + (--leftOver >= 0 ? 1: 0));
return result;
}
I think that it really possible to use tree structure there. Also you can try to implement manipulate some graph algorithms. Also, think it could be reasonable to have some hidden div on each level of the tree, when you store most popular information, and you can just display it if necessary instead of manipulation with div content.
But, think that it needs to specify you task with more details. Some real cases could be really helpfull.
Since DOM-Operations are costly, you should aim to detach the elements from the tree, work on them and then re-attach them to the DOM. This is easily done with JQuery's .detach().
I do not know your data structure, but the fastest loops are plain loops with counters. Remember to store any possible length values in a variable, to not lookup the length in every loop.
Related
Javascript/ECMAScript 6 specific solution desired.
I want to generate a random sample from an array of objects using an array of weighted values for each object. The population list contains the actual members of the population - not the types of members. Once one is selected for a sample, it can't be selected again.
An analogous problem to the one I'm working on would be simulating a probable outcome for a chess tournament. Each player's rating would be their weight. A player can only place once (1st, 2nd, or 3rd place) per tournament.
To pick a likely list of the top 3 winners could look like:
let winners = wsample(chessPlayers, // population
playerRatings, // weights
3); // sample size
The weighted list may, or may not, be integer values. It could be floats like [0.2, 0.1, 0.7, 0.3], or it could be integers like [20, 10, 70, 30]. The weights do not have to add up to a value that represents 100%.
Peter below gave me a good reference on a general algorithm, however It's not specific to JS: https://stackoverflow.com/a/62459274/7915759 it may be a good point of reference.
Solutions to the problem that rely on generating a second population list with each member copied weight number of times may not be a practical solution. Each weight in the weights array could be very high numbers, or they could be fractional; basically, any non-negative value.
Some additional questions:
Is there already an accumulate() function available in JS?
Is there a bisect() type function in JS that does a binary search of sorted lists?
Are there any efficient and low memory footprint JS modules available with statistical functions that include solutions for the above?
The following implementation selects k out of n elements, without replacement, with weighted probabilities, in O(n + k log n) by keeping the accumulated weights of the remaining elements in a sum heap:
function sample_without_replacement<T>(population: T[], weights: number[], sampleSize: number) {
let size = 1;
while (size < weights.length) {
size = size << 1;
}
// construct a sum heap for the weights
const root = 1;
const w = [...new Array(size) as number[], ...weights, 0];
for (let index = size - 1; index >= 1; index--) {
const leftChild = index << 1;
const rightChild = leftChild + 1;
w[index] = (w[leftChild] || 0) + (w[rightChild] || 0);
}
// retrieves an element with weight-index r
// from the part of the heap rooted at index
const retrieve = (r: number, index: number): T => {
if (index >= size) {
w[index] = 0;
return population[index - size];
}
const leftChild = index << 1;
const rightChild = leftChild + 1;
try {
if (r <= w[leftChild]) {
return retrieve(r, leftChild);
} else {
return retrieve(r - w[leftChild], rightChild);
}
} finally {
w[index] = w[leftChild] + w[rightChild];
}
}
// and now retrieve sampleSize random elements without replacement
const result: T[] = [];
for (let k = 0; k < sampleSize; k++) {
result.push(retrieve(Math.random() * w[root], root));
}
return result;
}
The code is written in TypeScript. You can transpile it to whatever version of EcmaScript you need in the TypeScript playground.
Test code:
const n = 1E7;
const k = n / 2;
const population: number[] = [];
const weight: number[] = [];
for (let i = 0; i < n; i++) {
population[i] = i;
weight[i] = i;
}
console.log(`sampling ${k} of ${n} elments without replacement`);
const sample = sample_without_replacement(population, weight, k);
console.log(sample.slice(0, 100)); // logging everything takes forever on some consoles
console.log("Done")
Executed in Chrome, this samples 5 000 000 out of 10 000 000 entries in about 10 seconds.
This is one approach, but not the most efficient.
Its efficiency can be improved by using a binary indexed tree as its prefix sum.
The highest level function. It iterates k times, calling wchoice() each time. To remove the currently selected member from the population, I just set its weight to 0.
/**
* Produces a weighted sample from `population` of size `k` without replacement.
*
* #param {Object[]} population The population to select from.
* #param {number[]} weights The weighted values of the population.
* #param {number} k The size of the sample to return.
* #returns {[number[], Object[]]} An array of two arrays. The first holds the
* indices of the members in the sample, and
* the second holds the sample members.
*/
function wsample(population, weights, k) {
let sample = [];
let indices = [];
let index = 0;
let choice = null;
let acmwts = accumulate(weights);
for (let i=0; i < k; i++) {
[index, choice] = wchoice(population, acmwts, true);
sample.push(choice);
indices.push(index);
// The below updates the accumulated weights as if the member
// at `index` has a weight of 0, eliminating it from future draws.
// This portion could be optimized. See note below.
let ndecr = weights[index];
for (; index < acmwts.length; index++) {
acmwts[index] -= ndecr;
}
}
return [indices, sample];
}
The section of code above that updates the accumulated weights array is the point of inefficiency in the algorithm. Worst case it's O(n - ?) to update on every pass. Another solution here follows a similar algorithm to this, but uses a binary indexed tree to reduce the cost of updating the prefix sum to an O(log n) operation.
wsample() calls wchoice() which selects one member from the weighted list. wchoice() generates an array of cumulative weights, generates a random number from 0 to the total sum of the weights (last item in the cumulative weights list). Then finds its insertion point in the cumulative weights; which is the winner:
/**
* Randomly selects a member of `population` weighting the probability each
* will be selected using `weights`. `accumulated` indicates whether `weights`
* is pre-accumulated, in which case it will skip its accumulation step.
*
* #param {Object[]} population The population to select from.
* #param {number[]} weights The weights of the population.
* #param {boolean} [accumulated] true if weights are pre-accumulated.
* Treated as false if not provided.
* #returns {[number, Object]} An array with the selected member's index and
* the member itself.
*/
function wchoice(population, weights, accumulated) {
let acm = (accumulated) ? weights : accumulate(weights);
let rnd = Math.random() * acm[acm.length - 1];
let idx = bisect_left(acm, rnd);
return [idx, population[idx]];
}
Here's a JS implementation I adapted from the binary search algorithm from https://en.wikipedia.org/wiki/Binary_search_algorithm
/**
* Finds the left insertion point for `target` in array `arr`. Uses a binary
* search algorithm.
*
* #param {number[]} arr A sorted ascending array.
* #param {number} target The target value.
* #returns {number} The index in `arr` where `target` can be inserted to
* preserve the order of the array.
*/
function bisect_left(arr, target) {
let n = arr.length;
let l = 0;
let r = n - 1;
while (l <= r) {
let m = Math.floor((l + r) / 2);
if (arr[m] < target) {
l = m + 1;
} else if (arr[m] >= target) {
r = m - 1;
}
}
return l;
}
I wasn't able to find an accumulator function ready-made for JS, so I wrote a simple one myself.
/**
* Generates an array of accumulated values for `numbers`.
* e.g.: [1, 5, 2, 1, 5] --> [1, 6, 8, 9, 14]
*
* #param {number[]} numbers The numbers to accumulate.
* #returns {number[]} An array of accumulated values.
*/
function accumulate(numbers) {
let accm = [];
let total = 0;
for (let n of numbers) {
total += n;
accm.push(total)
}
return accm;
}
using the code below, I've created a grid of buttons, 5x5, with random 1-25 numbers assigned to each button. They are to be clicked in numerical order, each's background turns red when clicked in the correct order. I can't use a global variable for this prompt. Without a global variable, I can't figure out how to increment the correctNumbers function which checks whether the right number is clicked each time. I think I'm missing something, a js function or something that would enable an incrementing variable declared within the incrementing function. I'm not looking for the whole explanation, just tips on functions i might not know about, and whether or not what i'm trying to do just isn't logicly possible.
<div id="numbers" class="hidden"></div>
<div id="youWon" class="hidden">You Won!</div>
The relevant JS:
... /**
* Gives the numbers a random order
* the "Fisher-Yates shuffle" found at: https://www.frankmitchell.org/2015/01/fisher-yates/
* #param {*} array
*/
const shuffle = (array) => {
let i = 0,
j = 0,
temp = null
for (i = array.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1))
temp = array[i]
array[i] = array[j]
array[j] = temp
}
}
/**
* Generates an array of numbers 1-25
*/
const generateNums = () => {
document.getElementById("youWon").classList.toggle("hidden", "visible");
const numberArray = [];
for (let a = 1; a <= 25; a++) {
numberArray.push(a);
}
shuffle(numberArray);
let numEl = document.getElementById('numbers'); //write into html div id "numbers"
for (let b = 0; b <= 24; b++) { //loop to create button array
let newBtn = document.createElement('button'); //create buttons
newBtn.className = 'number'; //assign newBtns 'number' class
newBtn.innerText = numberArray[b]; //assign numbers to each button
numEl.appendChild(newBtn); //match with number elements in "numbers" array
newBtn.addEventListener("click", onNumberClick) //create function trigger
}
}
/**
* Creates a function to decide correct and incorrect clicks
* When a user clicks a number, if it is the next number in order, then it turns a different color for the remainder of the test
* If it is the wrong number, nothing happens
* #param {*} event
*/
const incrementNum = (correctNumber) => {
correctNumber++;
}
const onNumberClick = (event) => {
let correctNumber = 1; //start at 1
let numberVal = event.target; //apply it to clicks on the numbers
if (Number(numberVal.innerHTML) + 1 == incrementNum(correctNumber)) {
incrementNum(correctNumber);
numberVal.classList.add("red");
}
if (correctNumber == 26) {
document.getElementById("youWon").classList.toggle("visible"); //show win message if 25 is the last button and gets clicked
}
}
I would suggest that you count the number of elements in the DOM that have the class "red" and add 1... checking if the innerHTML is equal to that number to get the sequence right. So, instead of this:
if (Number(numberVal.innerHTML) + 1 == incrementNum(correctNumber)) {
incrementNum(correctNumber);
numberVal.classList.add("red");
}
You can have something like this:
if(Number(numberVal.innerHTML) == document.getElementsByClassName('red').length + 1) {
numberVal.classList.add("red");
}
I'm trying to solve this problem "Nested List Weight Sum": https://leetcode.com/problems/nested-list-weight-sum/description/
Problem:
Given the list [[1,1],2,[1,1]], return 10. (four 1's at depth 2, one 2
at depth 1)
This is my solution.
var depthSum = function (nestedList, sum=0, depth=1) {
nestedList.forEach((val) => {
if (Array.isArray(val)) {
depth = depth+1;
return depthSum(val, sum, depth);
} else {
sum += val * depth;
}
});
return sum;
};
Not sure what I'm missing. When I debug it, at some point I get the answer but return sum does not kick in and i get a different answer.
Can someone point me the bug?
You could use Array#reduce and omit the sum for every level by returning a sum for each level.
function depthSum(nestedList, level = 1) {
return nestedList.reduce((sum, val) =>
sum + (Array.isArray(val)
? depthSum(val, level + 1)
: level * val),
0);
};
console.log(depthSum([[1, 1], 2, [1, 1]]));
So one way to tackle it.
There's no sense in returning from inside your forEach, what you should instead do is add the total from the recursive call to your current total. And since you are doing that, you don't need to have the sum be a parameter to your depthSum function
var nestedList = [[1,1],2,[1,1]];
var depthSum = function(nestedList, depth = 1) {
var sum = 0;
nestedList.forEach((val) => {
if (Array.isArray(val)) {
sum += depthSum(val, depth + 1);
} else {
sum += val * depth;
}
});
return sum;
};
console.log(depthSum(nestedList))
As per the requirement of the code in Leetcode the following is the Working code.
var depthSum = function (nestedList, depth=1) {
var res = 0;
nestedList.forEach((val) => {
if (val.isInteger() === false) {
res += depthSum(val.getList(), depth + 1);
} else {
res += val.getInteger() * depth;
}
});
return res;
};
You can't use Array.isArray() because all the members will return false. and also you can't access the values or list directly. You need to access through their API. The input to the function is not simply an array. See the input type and the APIs present from the specification as follow:
* function NestedInteger() {
*
* Return true if this NestedInteger holds a single integer, rather than a nested list.
* #return {boolean}
* this.isInteger = function() {
* ...
* };
*
* Return the single integer that this NestedInteger holds, if it holds a single integer
* Return null if this NestedInteger holds a nested list
* #return {integer}
* this.getInteger = function() {
* ...
* };
*
* Set this NestedInteger to hold a single integer equal to value.
* #return {void}
* this.setInteger = function(value) {
* ...
* };
*
* Set this NestedInteger to hold a nested list and adds a nested integer elem to it.
* #return {void}
* this.add = function(elem) {
* ...
* };
*
* Return the nested list that this NestedInteger holds, if it holds a nested list
* Return null if this NestedInteger holds a single integer
* #return {NestedInteger[]}
* this.getList = function() {
* ...
* };
* };
*/
/**
* #param {NestedInteger[]} nestedList
* #return {number}
*/
I have a table and I want to sort it by one field. However, I need to carry another field, which will provide the needed data for swapping the table entries. It works, but when the field that we sort by has a value 10 (maybe this will be a problem for entries >=10), it interprets 10 as 1, thus sorting results for the dataset 10 8 5 9 to 9 8 5 10.
I can't figure out what's going on! Can you? :)
SOLUTION (by Vache)
The problem is that entries of array to be sorted are strings and not integers! I was collecting the entries of the array with jQuery and .text(). In addition I need to use parseInt() to convert string to an integer.
/**
* Will swap two table entries
* #param a - name of first player
* #param b - name of second player
*/
function swapEntries(a, b)
{
var editor = $("#" + a); //put your ids here
var viewer = $("#" + b);
editorContent = editor.clone();
viewerContent = viewer.clone();
editor.replaceWith(viewerContent);
viewer.replaceWith(editorContent);
}
/**
* Will swap two array cells
* #param ar - array
* #param a - first cell's index
* #param b - second cell's index
*/
function swap(ar, a, b)
{
var temp = ar[a];
ar[a] = ar[b];
ar[b] = temp;
}
/**
* Quicksort.
* #param a - The array to be sorted.
* #param first - The start of the sequence to be sorted.
* #param last - The end of the sequence to be sorted.
* #param names - Array of names.
*/
function quickSort( a, first, last, names )
{
var pivotElement;
if(first < last)
{
pivotElement = pivot(a, first, last, names);
quickSort(a, first, pivotElement-1, names);
quickSort(a, pivotElement+1, last, names);
}
}
/**
* Find and return the index of pivot element.
* #param a - The array.
* #param first - The start of the sequence.
* #param last - The end of the sequence.
* #param names - Array of names.
* #return - the pivot element.
*/
function pivot( a, first, last)
{
var p = first;
var pivotElement = a[first];
for(var i = first+1 ; i <= last ; i++)
{
if(a[i] > pivotElement)
{
p++;
swap(a, i, p);
swapEntries(names[i], names[p]);
swap(names, i, p);
}
}
swap(a, p, first);
swapEntries(names[p], names[first]);
swap(names, p, first);
return p;
}
Without seeing the arrays, your problem is probably that your numbers are actually strings.
With the way sorting is handled by JavaScript,
>>> "10" > "2"
false
>>> 10 > 2
true
Also worth noting, as soon as one of your operands is a number a conversion is made to have a number comparison. So,
>>> "10" > 2
true
>>> 10 > "2"
true
How can I select the first 5 random elements
<ul>
<li>First</li>
<li>Second</li>
<li>Third</li>
...
<li>N</li>
</ul>
I'm using this plugin:
alert($("li:random").text());
but it takes all random elements. I only want the first 5.
Is there another way to do the same thing?
Here's how to get 5 random elements from a jQuery selection, no need of plugins!
randomElements = jQuery("li").get().sort(function(){
return Math.round(Math.random())-0.5
}).slice(0,5)
At this point you have 5 DomElements that have been selected randomly from all the LIs that jQuery returned
You can then do whatever you like with them,
e.g change their color:
$(randomElements).css("color","red")
or display their combined text contents:
$(randomElements).text()
Using the Fisher-Yates shuffle I have created a small script for this purpose. This is done by first creating a randomly shuffled and sliced copy of the array of jQuery elements, and then filtering out all elements that do not exist in both arrays.
You can read about it at http://www.afekenholm.se/jquery-rand. Here's the script:
/**
* jQuery.rand v1.0
*
* Randomly filters any number of elements from a jQuery set.
*
* MIT License: #link http://www.afekenholm.se/license.txt
*
* #author: Alexander Wallin (http://www.afekenholm.se)
* #version: 1.0
* #url: http://www.afekenholm.se/jquery-rand
*/
(function($){
$.fn.rand = function(k){
var b = this,
n = b.size(),
k = k ? parseInt(k) : 1;
// Special cases
if (k > n) return b.pushStack(b);
else if (k == 1) return b.filter(":eq(" + Math.floor(Math.random()*n) + ")");
// Create a randomized copy of the set of elements,
// using Fisher-Yates sorting
r = b.get();
for (var i = 0; i < n - 1; i++) {
var swap = Math.floor(Math.random() * (n - i)) + i;
r[swap] = r.splice(i, 1, r[swap])[0];
}
r = r.slice(0, k);
// Finally, filter jQuery stack
return b.filter(function(i){
return $.inArray(b.get(i), r) > -1;
});
};
})(jQuery);
Get a random number index, 1-5, and get the child of the ul with that index. Like so:
var index = Math.floor(Math.random() * 5) + 1; // nth-child indices start at 1
alert($("ul:nth-child(" + index + ")").text());
jQuery.jQueryRandom = 0;
jQuery.extend(jQuery.expr[":"], {
random: function(a, i, m, r) {
if (i == 0) {
jQuery.jQueryRandom = Math.floor(Math.random() * r.length);
};
return i == jQuery.jQueryRandom;
}
});
$("li:lt(5):random").text()
Why not just do this, it seems pretty efficient:
jQuery('li:random').slice(0, 5);