前提・実現したいこと
質問は表題の通り。以下は理解のためのイメージコードです。
※ただし実際の要素はリテラルでなくObjectです。実際のお試しコードは後述。
javascript
1const a = [0,1,2,3]; 2const b = [2,3,4,5]; 3const [onlyA, onlyB] = getUnique(a, b); 4console.log(onlyA) // [0,1] 5console.log(onlyB) // [4,5] 6 7function getUnique(a,b) { 8 // この中身を書きたい 9 return [onlyA, onlyB] // 戻り値も目的が達せられれば変わっても可 10}
お題が定性的ですが、以下の「試したソースコード」のテストコードよりも早ければありがたいな、という感じです。2回for回すのを回避する方法ないのかな、という感じ。
試したソースコード
以下がテストコードです。for文2回で頑張るパターンとfilter2回やるパターンでやりました。のJSFiddle
javascript
1const test = [getUnique1, getUnique2]; 2 3function init() { 4 const a = []; 5 const b = []; 6 const num = 1000; 7 for (let i = 0; i < num; i++) { 8 const x = i; 9 const y = i % 20; 10 if (i >= 100) { 11 a.push({ 12 x, 13 y 14 }) 15 } 16 if (i < 900) { 17 b.push({ 18 x, 19 y 20 }) 21 } 22 } 23 b.sort(() => Math.random() - 0.5); // 適当にbのソートを崩す目的 24 return [a, b] 25} 26 27/** for文で頑張る */ 28function getUnique1(a, b) { 29 const onlyA = []; 30 const onlyB = []; 31 for (let ai = 0, al = a.length; ai < al; ai++) { 32 let hit = false; 33 for (let bi = 0, bl = b.length; bi < bl; bi++) { 34 if (a[ai].x === b[bi].x && a[ai].y === b[bi].y) { 35 hit = true; 36 break; 37 } 38 } 39 if (!hit) { 40 onlyA.push(a[ai]); 41 } 42 } 43 for (let bi = 0, bl = b.length; bi < bl; bi++) { 44 let hit = false; 45 for (let ai = 0, al = a.length; ai < al; ai++) { 46 if (a[ai].x === b[bi].x && a[ai].y === b[bi].y) { 47 hit = true; 48 break; 49 } 50 } 51 if (!hit) { 52 onlyB.push(b[bi]); 53 } 54 } 55 return [onlyA, onlyB]; 56} 57 58/** filter使う */ 59function getUnique2(a, b) { 60 return [ 61 a.filter((av) => b.findIndex((bv) => av.x === bv.x && av.y === bv.y) === -1), 62 b.filter((bv) => a.findIndex((av) => av.x === bv.x && av.y === bv.y) === -1) 63 ] 64} 65 66const result = [] 67const [a, b] = init(); 68for (let i = 0; i < test.length; i++) { 69 const start = Date.now(); 70 // 一度結果のテスト 71 const [onlyA, onlyB] = test[i](a, b); 72 if (onlyA.length !== 100 || onlyB.length !== 100 || onlyA.map(v => v.x).reduce((a, b) => a + b) !== 94950 || onlyB.map(v => v.x).reduce((a, b) => a + b) !== 4950) { 73 alert(`error(expect 100): onlyA.length:${onlyA.length} xSum:${onlyA.map(v => v.x).reduce((a, b) => a + b)}, onlyB.length:${onlyB.length} xSum:${onlyB.map(v => v.x).reduce((a, b) => a + b)}`) 74 console.log(onlyA) 75 console.log(onlyB) 76 return; 77 } 78 for (let j = 0; j < 1000; j++) { 79 test[i](a, b); 80 } 81 const end = Date.now(); 82 result.push({ 83 time: end - start, 84 name: test[i].name 85 }) 86} 87 88const strs = result.map(v => `${v.name}: ${v.time}ms`) 89alert(strs); 90
私の環境でChromeにてfor文2回(getUnique1)が 931ms、filter使うのが(getUnique2) 694msでした。(for文のが早いかなと予想してたがfilterの方が早かった。しかしIEでポリフィルとか使うときっとfilterの方が遅いと思う。)
いくつか確認したいことがあります。
・バラバラなインスタンスでも条件が整えば「同値」と判断したい、ということで間違いはないでしょうか。
・同じ配列に同じものが2つはいっていた場合の処理はどのようにすればいいでしょうか。
本題ではないのでこちらのコメントとしますが、「b.sort(() => Math.random() - 0.5);」のように値を見ずにソート関数を動かすと、同じ値の組み合わせについて順序性が保証されない状態となるので、正常にソートされることが保証されません。
>バラバラなインスタンスでも条件が整えば「同値」と判断したい、ということで間違いはないでしょうか
はい、そうです。a===bでなくても、専用の条件(この場合はxとyの値が同値)ならばOKとしたいです。
>同じ配列に同じものが2つはいっていた場合の処理はどのようにすればいいでしょうか。
その場合は比較元の方の個数をそのまま出す感じでお願いします。[0,0,0,1,2]と[1,2,2,3]だったら、[0,0,0]と[3]が出る感じで。
randomソートについてご指摘ありがとうございます。今回はテストデータの順序を変えたかっただけなので、中身が壊れなければソートの順序はとりあえずなんでもOKとしました。
要素に大小関係はありますか?
つまり、ある要素とある要素を比べた時、どちらが大きいと一意にきまりますか?
一概にはいえないです。xとyの値を持っているのですが等価です。たとえば{x:1, y:2}と{x:2,y:1}のどちらが大きい、ということは言えません。
ただ結果が良ければ、ソート目的で便宜的にxyの優先順位を設定することはやぶさかではありません。
aが持つプロパティ名/プロパティ値の組み合わせはaの中でユニークなのでしょうか。重複はあり得ますか。
重複はあり得ます。aにあってbにないもので重複がある場合は重複数分出るのが理想ですが、出なくても大丈夫です。(maisumakunさんへの返信で絶対重複のまま、という感じにしてしまいましたがよく考えれば「できれば」レベルでした)
後で回答内容を基にしたまとめで質問を更新します。
ちなみにjsFiddleのURL最後尾に/showをつけてIEで試した結果が以下。これ遅すぎるけど、原因はPolyfillにあることを私は知っている。(コードをBabelのサイトでバベってjsFiddleでbabel-polyfill入れてやってる)
https://imgur.com/a/mWOJLtc
findIndexとかは自前で簡易polyfill書いて対処してる
すみません、今データ構造を変えたほうが良いか?変えない方が良いか?でテストと影響範囲確認と検討を進めています。BA選定もう少々お待ちください。

