4
votes

Fenêtre coulissante sur Array en JavaScript

J'ai besoin d'une fenêtre coulissante sur un tableau en JavaScript.

Par exemple, une fenêtre glissante de taille 3 sur [1,2,3,4,5,6,7,8,9] calcule la séquence [[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7],[6,7,8],[7,8,9]] .

Voici ma tentative, car je n'ai pas trouvé de solution prête à l'emploi:

function window(a, sz) {
  return a.map((_, i, ary) => ary.slice(i, i + sz)).slice(0, -sz + 1);
}

Il renvoie un tableau de fenêtres qui peuvent être mappées pour obtenir les fenêtres individuelles.

Quelle est la meilleure solution?


4 commentaires

«meilleure solution» mieux dans quel sens? De plus, si le code fonctionne, la question est mieux adaptée à codereview.stackexchange.com


Je vote pour fermer cette question comme hors sujet car la question appartient à codereview.stackexchange.com


J'ai besoin d'une solution courte et conforme aux meilleures pratiques pour ce problème courant qui peut servir de référence pour les applications futures.


Pour référence croisée: stackoverflow.com/questions/52219405/...


4 Réponses :


3
votes

L'ajout aux objets JavaScript natifs via leur prototype n'est pas une bonne idée. Cela peut casser des choses de manière inattendue et entraînera beaucoup de frustration pour vous et toute autre personne utilisant votre code. Il est préférable de créer simplement votre propre fonction dans ce cas.

Pour obtenir la fonctionnalité souhaitée, vous pouvez simplement passer le tableau à votre fonction et y accéder à partir de là. Effectuez les appels de méthode souhaités sur le tableau à partir de votre fonction. Suivant le principe de KISS, il n'y a pas besoin de quelque chose de plus sophistiqué ici.

Souvenez-vous également que Array.map est appelé pour chaque élément du tableau. Ce n'est pas vraiment ce dont vous avez besoin ici. Si le but est d'obtenir une fenêtre glissante de taille n et que vous voulez que chacune des fenêtres soit ajoutée à un nouveau tableau, vous pouvez utiliser une fonction comme celle-ci:

var myArray = [1, 2, 3, 4, 5, 6, 7, 8];

function getWindows(arr, n){
if (n > arr.length)
{
   return arr;
}
var result = [];
let lastWindow = i < arr.length/n - n;
for (let i = 0; i < lastWindow; i += n)
{
   result.push(arr.slice(i, i + n));
}

return result;

}

Donc, ici, nous allons obtenir un tableau de fenêtres, qui sont également des tableaux. L'appel de console.log(getWindows(myArray, 3)) , donne cette sortie:

0: Tableau (3) [1, 2, 3] -

1: tableau (3) [4, 5, 6]

2: tableau (3) [7, 8, 9]


2 commentaires

J'ai changé l'exemple car il confondait le problème de la fenêtre coulissante avec ma commodité de l'appeler en tant que fonction Array.prototype.


Votre exemple de sortie montre des tranches , pas des fenêtres coulissantes. J'ai mis à jour la question pour clarification. De plus, votre exemple de code semble produire uniquement des tableaux vides.



1
votes

À l'aide de JS ES6, vous pouvez effectuer les opérations suivantes:

class SlidingWindow{
  constructor(windowSize) {
    this.deque = []; // for storing the indexex of the 'k' elements in the input 
    this.windowSize = windowSize;
  }

  customCompute(input, processWindow, addOutput) {
    let output = [];

    if(!input || input.length === 0) {
        return [];
    }

    if(input.length < this.windowSize) {
        return input;
    }

    for(let i=0; i < input.length; i++) {
        
        //if the index in the first element of the this.deque is out of bound (i.e. idx <= i-this.windowSize) then remove it
        if(this.deque.length > 0 && this.deque[0] === i-this.windowSize) {
            this.deque.shift(); //remove the first element
        }

        processWindow(this.deque, input[i], input)

        this.deque.push(i)
        
        if(i+1 >= this.windowSize) {
            output.push(addOutput(this.deque, input));
        }
    }
    this.deque = [];
    return output;
  }
}

let slidingWindow = new SlidingWindow(3);

console.log('computed sliding windows: '+JSON.stringify(slidingWindow.compute([1,2,3,4,5,6,7,8,9])));

function processWindow(deque, currentElement, input){
  while(deque.length > 0 && currentElement > input[deque[deque.length -1]]) {
            deque.pop(); //remove the last element
        }
};
console.log('computed sliding windows maximum: '+JSON.stringify(slidingWindow.customCompute([1,3,-1,-3,5,3,6,7], processWindow, (deque, input) => input[deque[0]])));

Pour calculer le maximum de chaque fenêtre glissante, vous pouvez personnaliser le code ci-dessus comme suit:

class SlidingWindow{
  constructor(windowSize) {
    this.deque = []; // for storing the indexex of the 'k' elements in the input 
    this.windowSize = windowSize;
  }

  compute(input){   
    let output = [];

    if(!input || input.length === 0) {
        return [];
    }

    if(input.length < this.windowSize) {
        return input;
    }

    for(let i=0; i < input.length; i++) {
        
        //if the index in the first element of the this.deque is out of bound (i.e. idx <= i-this.windowSize) then remove it
        if(this.deque.length > 0 && this.deque[0] === i-this.windowSize) {
            this.deque.shift(); //remove the first element
        }
                
        this.deque.push(i)
        
        if(i+1 >= this.windowSize) {
            output.push(this.deque.map(idx => input[idx]));
        }
    }
    return output;
  }
}

//Here is how to use it:
let slidingWindow = new SlidingWindow(3);

console.log('computed sliding windows: '+JSON.stringify(slidingWindow.compute([1,2,3,4,5,6,7,8,9])));


2 commentaires

Salut Ivan, merci pour votre longue tentative. Pourriez-vous s'il vous plaît préciser à quel point vous le jugez meilleur que le 1-liner mentionné dans la question?


Salut @JensJensen, je voudrais souligner deux aspects: 1. lisibilité 2. flexibilité - avec cette solution, vous pouvez exécuter différentes logiques dans différents scénarios.



1
votes

Array#reduce

Une alternative raisonnable pour éviter .map suivi de .slice() est d'utiliser .reduce() pour générer les fenêtres:

function* windowGenerator(inputArray, size) { 
  for(let index = 0; index+size <= inputArray.length; index++) {
    yield inputArray.slice(index, index+size);
  }
}

function toWindows(inputArray, size) {
  //compute the entire sequence of windows into an array
  return Array.from(windowGenerator(inputArray, size))
}

const input = [1, 2, 3, 4, 5, 6, 7, 8, 9];

//JSON.stringify to produce more compact result in the console
console.log(JSON.stringify(toWindows(input, 2))); 
console.log(JSON.stringify(toWindows(input, 3))); 
console.log(JSON.stringify(toWindows(input, 4)));
console.log(JSON.stringify(toWindows(input, 9)));
console.log(JSON.stringify(toWindows(input, 10)));



//somewhat more realistic usage:
//find the sum closest to a target number when adding three numbers at a time
const veryLargeInput = [17, 95, 27, 30, 32, 38, 37, 67, 53, 46, 33, 36, 79, 14, 19, 25, 3, 54, 98, 11, 68, 96, 89, 71, 34, 31, 28, 13, 99, 10, 15, 84, 48, 29, 74, 78, 8, 90, 50, 49, 59, 18, 12, 40, 22, 80, 42, 21, 73, 43, 70, 100, 1, 44, 56, 5, 6, 75, 51, 64, 58, 85, 91, 83, 24, 20, 72, 26, 88, 66, 77, 60, 81, 35, 69, 93, 86, 4, 92, 9, 39, 76, 41, 37, 63, 45, 61, 97, 2, 16, 57, 65, 87, 94, 52, 82, 62, 55, 7, 23];
const targetNumber = 100;

console.log(`-- finding the closest number to ${targetNumber}`)

const iterator = windowGenerator(veryLargeInput, 3);

let closest = -1;

for (const win of iterator) {
  const sum = win.reduce((a, b) => a+b);
  
  const difference = Math.abs(targetNumber - sum);
  const oldDifference = Math.abs(targetNumber - closest);
  
  console.log(
    `--- evaluating: ${JSON.stringify(win)}
    sum: ${sum},
    difference with ${targetNumber}: ${difference}`
  );
  
  if (difference < oldDifference) {
    console.log(`---- ${sum} is currently the closest`);
    closest = sum;
    
    if (difference === 0) {
      console.log("----- prematurely stopping - we've found the closest number")
      break;
    }
  }
  
}

console.log(`-- closest sum is: ${closest}`)

Celui-ci peut ensuite être raccourci, si nécessaire:

function toWindows(inputArray, size) {
  return Array.from(
    {length: inputArray.length - (size - 1)}, //get the appropriate length
    (_, index) => inputArray.slice(index, index+size) //create the windows
  )
}

const input = [1, 2, 3, 4, 5, 6, 7, 8, 9];

//JSON.stringify to produce more compact result in the console
console.log(JSON.stringify(toWindows(input, 2))); 
console.log(JSON.stringify(toWindows(input, 3))); 
console.log(JSON.stringify(toWindows(input, 4)));
console.log(JSON.stringify(toWindows(input, 9)));
console.log(JSON.stringify(toWindows(input, 10)));



//somewhat more realistic usage:
//find the maximimum odd sum when adding together three numbers at a time
const output = toWindows([ 3, 9, 1, 2, 5, 4, 7, 6, 8 ], 3)
  .map(window => window.reduce((a,b) => a+b)) //sum
  .filter(x => x%2 === 1) //get odd
  .reduce((highest, current) => Math.max(highest, current), -Infinity) //find highest
  
console.log(output)

Array.from

L'approche peut être simplifiée en utilisant Array.from pour générer d'abord un tableau avec la longueur appropriée, puis le remplir avec les fenêtres générées:

function toWindows(inputArray, size) { 
  return inputArray
    .reduce(
      (acc, _, index, arr) => (index+size > arr.length) ? acc : acc.concat([arr.slice(index, index+size)]),
      []
    )
}

const input = [1, 2, 3, 4, 5, 6, 7, 8, 9];

//JSON.stringify to produce more compact result in the console
console.log(JSON.stringify(toWindows(input, 2))); 
console.log(JSON.stringify(toWindows(input, 3))); 
console.log(JSON.stringify(toWindows(input, 4)));
console.log(JSON.stringify(toWindows(input, 9)));
console.log(JSON.stringify(toWindows(input, 10)));



//somewhat more realistic usage:
//find the maximimum odd sum when adding together three numbers at a time
const output = toWindows([ 3, 9, 1, 2, 5, 4, 7, 6, 8 ], 3)
  .map(window => window.reduce((a,b) => a+b)) //sum
  .filter(x => x%2 === 1) //get odd
  .reduce((highest, current) => Math.max(highest, current), -Infinity) //find highest
  
console.log(output);

Générateur

Une autre alternative consiste à utiliser une fonction de générateur au lieu de pré-calculer toutes les fenêtres. Cela peut être utile pour une évaluation plus paresseuse avec une approche de fenêtre glissante. Vous pouvez toujours calculer toutes les fenêtres en utilisant Array.from , si nécessaire:

function toWindows(inputArray, size) { 
  return inputArray
    .reduce((acc, _, index, arr) => {
      if (index+size > arr.length) {
        //we've reached the maximum number of windows, so don't add any more
        return acc;
      }
      
      //add a new window of [currentItem, maxWindowSizeItem)
      return acc.concat(
        //wrap in extra array, otherwise .concat flattens it
        [arr.slice(index, index+size)]
      );
      
    }, [])
}

const input = [1, 2, 3, 4, 5, 6, 7, 8, 9];

//JSON.stringify to produce more compact result in the console
console.log(JSON.stringify(toWindows(input, 2))); 
console.log(JSON.stringify(toWindows(input, 3))); 
console.log(JSON.stringify(toWindows(input, 4)));
console.log(JSON.stringify(toWindows(input, 9)));
console.log(JSON.stringify(toWindows(input, 10)));



//somewhat more realistic usage:
//find the maximimum odd sum when adding together three numbers at a time
const output = toWindows([ 3, 9, 1, 2, 5, 4, 7, 6, 8 ], 3)
  .map(window => window.reduce((a,b) => a+b)) //sum
  .filter(x => x%2 === 1) //get odd
  .reduce((highest, current) => Math.max(highest, current), -Infinity) //find highest
  
console.log(output)

0 commentaires

0
votes

Avez-vous envisagé de devenir récursif?

  • l est la taille de chaque fenêtre
  • xs est votre liste
  • i est le nombre d'itérations que nous devons effectuer, soit xs.length - l
  • out contient le résultat

Une tranche peut être obtenue avec xs.slice(i, i + l) . A chaque récursion, i est incrémenté jusqu'à ce que i arrive à un point où la tranche suivante contiendrait moins de l éléments.

aperture(3, [1,2,3,4,5,6,7,8,9]);
//=> [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [6, 7, 8], [7, 8, 9]]

Il existe également une solution non récursive avec flatMap .

Avec flatMap vous pouvez retourner un tableau à chaque itération, il sera aplati dans le résultat final:

const windows = (l, xs) =>
  xs.flatMap((_, i) =>
    i <= xs.length - l
      ? [xs.slice(i, i + l)]
      : []);

console.log(windows(3, [1,2,3,4,5,6,7,8,9]))

Vous pouvez donc renvoyer vos tranches (enveloppées dans [] ) jusqu'à ce que i dépasse la limite qui est xs.length - l :

const duplicate = xs => xs.flatMap(x => [x, x]);
duplicate([1, 2]);
//=> [1, 1, 2, 2]

Notez que dans certaines bibliothèques comme , cela s'appelle aperture :

Renvoie une nouvelle liste, composée de n-tuples d'éléments consécutifs. Si n est supérieur à la longueur de la liste, une liste vide est renvoyée.

const windows = (l, xs, i = 0, out = []) =>
  i > xs.length - l
    ? out
    : windows(l, xs, i + 1, [...out, xs.slice(i, i + l)]);


console.log(windows(3, [1,2,3,4,5,6,7,8,9]));

Comme vous pouvez le voir, quelques personnes ont déjà posé la même question:


0 commentaires