3
votes

Cartographie efficace des tableaux

J'essaie de trouver le moyen le meilleur / le plus efficace ou le plus fonctionnel pour comparer / fusionner / manipuler deux tableaux (listes) simultanément dans JS.

L'exemple que je donne ci-dessous est un exemple simple du concept global. Dans mon projet actuel, je traite un mappage de liste très fou, le filtrage, etc. avec de très grandes listes d'objets.

Comme indiqué ci-dessous, ma première idée ( version1 ) sur comparer des listes consisterait à parcourir la première liste (c.-à-d. carte), et dans la fonction anonyme / rappel, filtrer la deuxième liste pour répondre aux critères nécessaires à la comparaison (identifiants de correspondance par exemple). Cela fonctionne évidemment, selon la version1 ci-dessous.

J'avais une question en termes de performances, car avec cette méthode à chaque itération / appel de la carte, la 2ème liste entière est filtrée juste pour trouver cet élément qui correspond au filtre.

De plus, le filtre passe tous les autres éléments de list2 qui doivent être mis en correspondance dans list1. Signification (car cette phrase n'a probablement pas de sens):

const list1 = [
  {id: '1',init:'init1'},
  {id: '2',init:'init2'},
  {id: '3',init:'init3'}
];
const list2 = [
  {id: '2',data:'data2'},
  {id: '3',data:'data3'},
  {id: '4',data:'data4'}
];

/* ---------
* version 1
*/

const mergedV1 = list1.map(n => (
  {...n,...list2.filter(f => f.id===n.id)[0]}
));
/* [ 
  {"id": "1", "init": "init1"}, 
  {"id": "2", "init": "init2", "data": "data2"}, 
  {"id": "3", "init": "init3", "data": "data3"} 
] */

/* ---------
* version 2
*/

const dictList2 = list2.reduce((dict,item) => (dict[item.id]=item,dict),{}); 
// does not handle duplicate ids but I think that's 
// outside the context of this question.

const mergedV2 = list1.map(n => ({...n,...dictList2[n.id]}));
/* [ 
  {"id": "1", "init": "init1"}, 
  {"id": "2", "init": "init2", "data": "data2"}, 
  {"id": "3", "init": "init3", "data": "data3"} 
] */

JSON.stringify(mergedV1) === JSON.stringify(mergedV2);
// true

// and just for fun
const sqlLeftOuterJoinInJS = list1 => list2 => on => {
  const dict = list2.reduce((dict,item) => ( 
    dict[item[on]]=item,dict
  ),{});
  return list1.map(n => ({...n,...dict[n[on]]}
))};

Idéalement, lors de la première itération de la carte ( list1 id: 1 ), lorsque le filtre rencontre list2 id: 3 (premier élément), il le ferait simplement correspondre à id list1: 3

En pensant au concept ci-dessus (correspondant à un identifiant plus récent lorsqu'il est rencontré plus tôt, j'ai trouvé version2).

Cela fait de list2 un dictionnaire, puis recherche la valeur dans n'importe quelle séquence par clé.

list1.map   list2.filter

id:1        [id:3,id:2,id:1]
                          ^-match
id:2        [id:3,id:2,id:1]
                     ^-match
id:3        [id:3,id:2,id:1]
                ^-match

De toute évidence, les exemples ci-dessus sont assez simples (fusionner deux listes, chaque liste ayant une longueur de 3). Il y a des instances plus complexes avec lesquelles je travaille.

Je ne sais pas s'il existe des techniques plus intelligentes (et idéalement fonctionnelles) que je devrais utiliser.


1 commentaires

L'un des tableaux est-il censé contenir un ensemble unique d'objets par id? Cela signifie-t-il que id n'apparaîtra qu'une fois par valeur dans l'un ou l'autre des tableaux?


4 Réponses :


3
votes

Vous pouvez fermer la clé souhaitée pour le groupe et un Map pour collecter tous les objets.

.as-console-wrapper { max-height: 100% !important; top: 0; }
function merge(key) {
    var map = new Map;
    return function (r, a) {
        a.forEach(o => {
            if (!map.has(o[key])) r.push(map.set(o[key], {}).get(o[key]));
            Object.assign(map.get(o[key]), o);
        });
        return r;
    };
}

const
    list1 = [{ id: '1', init: 'init1' }, { id: '2', init: 'init2' }, { id: '3', init: 'init3' }],
    list2 = [{ id: '2', data: 'data2' }, { id: '3', data: 'data3' }, { id: '4', data: 'data4' }],
    result = [list1, list2].reduce(merge('id'), []);

console.log(result);


3 commentaires

C'est beau


semble assez brillant. m'a fait regarder beaucoup plus sur map . une question que je me pose est: que fait le Object.assign (map.get (o [key]), o); sans une affectation de gauche? Est-ce que o mute?


il mute le premier paramètre (le résultat de map.get (o [key]) ) et lui assigne le deuxième o .



1
votes

L'utilisation du filtre pour la recherche est un faux pas. Votre instinct dans la version 2 est bien meilleur. Map et Set fournissent des temps de recherche beaucoup plus rapides.

Voici une approche décomposée. Cela devrait être assez rapide, mais peut-être pas aussi rapide que celui de Nina. C'est une démon de vitesse> _

const merge = (...lists) =>
  Array .from
    ( lists
        .reduce (merge1, new Map)
        .values ()
    )

const merge1 = (cache, list) =>
  list .reduce
    ( (cache, l) =>
        cache .has (l.id)
          ? update (cache, l.id, l)
          : insert (cache, l.id, l)
    , cache
    )

const insert = (cache, key, value) =>
  cache .set (key, value)

const update = (cache, key, value) =>
  cache .set
    ( key
    , { ...cache .get (key)
      , ...value
      }
    )

const list1 =
  [{ id: '1', init: 'init1' }, { id: '2', init: 'init2' }, { id: '3', init: 'init3' }]

const list2 =
  [{ id: '2', data: 'data2' }, { id: '3', data: 'data3' }, { id: '4', data: 'data4' }]

console .log (merge (list1, list2))


0 commentaires

0
votes

Je propose cela pour être complet car je pense que Nina et @ user633183 ont probablement proposé des solutions plus efficaces.

Si vous souhaitez vous en tenir à votre exemple de filtre initial, qui est une recherche maximale N * M, et que vos tableaux sont mutables; vous pouvez envisager de réduire l'ensemble au fur et à mesure que vous le traversez. Autrefois, la réduction du tableau avait un impact énorme sur les performances.

Le modèle général actuel consiste à utiliser une carte (ou dict) comme indiqué dans d'autres réponses, car elle est à la fois facile à comprendre et généralement efficace .

Rechercher et redimensionner

.as-console-wrapper {
  max-height: 100% !important;
  top: 0;
}
const list1 = [
  {id: '1',init:'init1'},
  {id: '2',init:'init2'},
  {id: '3',init:'init3'}
];
const list2 = [
  {id: '2',data:'data2'},
  {id: '3',data:'data3'},
  {id: '4',data:'data4'}
];

// combine by ID
let merged = list1.reduce((acc, obj)=>{
  acc.push(obj);

  // find index by ID
  let foundIdx = list2.findIndex( el => el.id==obj.id );
  // if found, store and remove from search
  if ( foundIdx >= 0 ){
    obj.data = list2[foundIdx].data;
    list2.splice( foundIdx, 1 );        // shrink lookup array
  }
  return acc;
},[]);

// store remaining (if you want); i.e. {id:4,data:'data4'}
merged = merged.concat(list2)

console.log(merged);


0 commentaires

0
votes

Je ne sais pas si je dois marquer cette question comme un doublon , car vous l'avez formulée différemment. Quoi qu'il en soit, voici ma réponse à cette question copiée textuellement. Ce que vous voulez, c'est une equijoin :

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => ix.set(row[primary], row), new Map);
    return ys.map(row => sel(ix.get(row[foreign]), row));
};

const list1 = [
    { id: "1", init: "init1" },
    { id: "2", init: "init2" },
    { id: "3", init: "init3" }
];

const list2 = [
    { id: "2", data: "data2" },
    { id: "3", data: "data3" },
    { id: "4", data: "data4" }
];

const result = equijoin(list2, list1, "id", "id",
    (row2, row1) => ({ ...row1, ...row2 }));

console.log(result);

Vous pouvez l'utiliser comme suit:

const equijoin = (xs, ys, primary, foreign, sel) => {
    const ix = xs.reduce((ix, row) => // loop through m items
        ix.set(row[primary], row),    // populate index for primary table
    new Map);                         // create an index for primary table

    return ys.map(row =>              // loop through n items
        sel(ix.get(row[foreign]),     // get corresponding row from primary
        row));                        // select only the columns you need
};

Il faut O (m + n) temps pour calculer la réponse en utilisant equijoin . Cependant, si vous avez déjà un index, cela ne prendra que O (n) temps. Par conséquent, si vous prévoyez de faire plusieurs équi-jointures en utilisant les mêmes tables, il peut être intéressant de résumer l'index.


0 commentaires