🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

7回答

1406閲覧

二つの配列、それぞれに含まれるユニークな値をできるだけ高速に抽出する方法

退会済みユーザー

退会済みユーザー

総合スコア0

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

2グッド

3クリップ

投稿2019/11/11 05:08

前提・実現したいこと

質問は表題の通り。以下は理解のためのイメージコードです。
※ただし実際の要素はリテラルでなく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の方が遅いと思う。)

hmm, jun68ykt👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maisumakun

2019/11/11 05:23

いくつか確認したいことがあります。 ・バラバラなインスタンスでも条件が整えば「同値」と判断したい、ということで間違いはないでしょうか。 ・同じ配列に同じものが2つはいっていた場合の処理はどのようにすればいいでしょうか。
maisumakun

2019/11/11 05:26

本題ではないのでこちらのコメントとしますが、「b.sort(() => Math.random() - 0.5);」のように値を見ずにソート関数を動かすと、同じ値の組み合わせについて順序性が保証されない状態となるので、正常にソートされることが保証されません。
退会済みユーザー

退会済みユーザー

2019/11/11 05:32

>バラバラなインスタンスでも条件が整えば「同値」と判断したい、ということで間違いはないでしょうか はい、そうです。a===bでなくても、専用の条件(この場合はxとyの値が同値)ならばOKとしたいです。 >同じ配列に同じものが2つはいっていた場合の処理はどのようにすればいいでしょうか。 その場合は比較元の方の個数をそのまま出す感じでお願いします。[0,0,0,1,2]と[1,2,2,3]だったら、[0,0,0]と[3]が出る感じで。 randomソートについてご指摘ありがとうございます。今回はテストデータの順序を変えたかっただけなので、中身が壊れなければソートの順序はとりあえずなんでもOKとしました。
Zuishin

2019/11/11 06:00

要素に大小関係はありますか? つまり、ある要素とある要素を比べた時、どちらが大きいと一意にきまりますか?
退会済みユーザー

退会済みユーザー

2019/11/11 06:07

一概にはいえないです。xとyの値を持っているのですが等価です。たとえば{x:1, y:2}と{x:2,y:1}のどちらが大きい、ということは言えません。 ただ結果が良ければ、ソート目的で便宜的にxyの優先順位を設定することはやぶさかではありません。
think49

2019/11/11 23:30

aが持つプロパティ名/プロパティ値の組み合わせはaの中でユニークなのでしょうか。重複はあり得ますか。
退会済みユーザー

退会済みユーザー

2019/11/12 03:48

重複はあり得ます。aにあってbにないもので重複がある場合は重複数分出るのが理想ですが、出なくても大丈夫です。(maisumakunさんへの返信で絶対重複のまま、という感じにしてしまいましたがよく考えれば「できれば」レベルでした)
退会済みユーザー

退会済みユーザー

2019/11/12 05:32

後で回答内容を基にしたまとめで質問を更新します。 ちなみにjsFiddleのURL最後尾に/showをつけてIEで試した結果が以下。これ遅すぎるけど、原因はPolyfillにあることを私は知っている。(コードをBabelのサイトでバベってjsFiddleでbabel-polyfill入れてやってる) https://imgur.com/a/mWOJLtc
退会済みユーザー

退会済みユーザー

2019/11/12 05:33

findIndexとかは自前で簡易polyfill書いて対処してる
退会済みユーザー

退会済みユーザー

2019/11/14 07:47

すみません、今データ構造を変えたほうが良いか?変えない方が良いか?でテストと影響範囲確認と検討を進めています。BA選定もう少々お待ちください。
guest

回答7

0

データ構造を見直すことを検討してみて下さい。
データが作成された時点で最適化の9割は完了しています。

JavaScript

1'use strict'; 2const Point = (() => { 3 const wm = new WeakMap; 4 5 return class Point { 6 constructor (x, y) { 7 wm.set(this, Object.create(null)); 8 this.x = x; 9 this.y = y; 10 } 11 get x () { 12 return wm.get(this).x; 13 } 14 get y () { 15 return wm.get(this).y; 16 } 17 set x (value) { 18 const thisArg = wm.get(this); 19 this.valuesAsJson = JSON.stringify([thisArg.x = value, thisArg.y]); 20 } 21 set y (value) { 22 const thisArg = wm.get(this); 23 this.valuesAsJson = JSON.stringify([thisArg.x, thisArg.y = value]); 24 } 25 }; 26})(); 27 28class PointList { 29 constructor (points) { 30 this.uniqueMap = new Map; 31 this.array = []; 32 33 if (arguments.length) { 34 this.push(...points); 35 } 36 } 37 push (...points) { 38 const uniqueMap = new Map, len = points.length; 39 40 for (let point of points) { 41 const json = point.valuesAsJson, values = uniqueMap.get(json); 42 43 values ? values.add(point) : uniqueMap.set(json, new Set([point])); 44 } 45 this.uniqueSet = new Set(uniqueMap.keys()); 46 47 return this.array.push(...points); 48 } 49} 50 51function getOnlyPoints (setA, setB) { 52 setA = new Set(setA), setB = new Set(setB); 53 54 for (let value of setA) { 55 if (setB.has(value)) { 56 setA.delete(value), setB.delete(value); 57 } 58 } 59 60 for (let value of setB) { 61 if (setA.has(value)) { 62 setA.delete(value), setB.delete(value); 63 } 64 } 65 66 return [setA, setB]; 67} 68 69function init() { 70 const a = new PointList; 71 const b = []; 72 const num = 1000; 73 for (let i = 0; i < num; i++) { 74 const x = i; 75 const y = i % 20; 76 if (i >= 100) { 77 a.push(new Point(x, y)) 78 } 79 if (i < 900) { 80 b.push(new Point(x, y)) 81 } 82 } 83 b.sort(() => Math.random() - 0.5); // 適当にbのソートを崩す目的 84 return [a, new PointList(b)] 85} 86 87const [a, b] = init(); 88console.time('getOnlyPoints'); 89const [onlyA, onlyB] = getOnlyPoints(a.uniqueSet, b.uniqueSet); 90console.timeEnd('getOnlyPoints'); 91console.log(onlyA); 92console.log(onlyB);

Re: jjj_aaa さん

投稿2019/11/11 14:01

編集2019/11/11 14:06
think49

総合スコア18189

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/12 04:34

すみません、まず動かしたのですがonlyAが一個だけでonlyBが900個になってました。 https://jsfiddle.net/wrxc039j/ ちょっとどこが悪いのか確認してます。
退会済みユーザー

退会済みユーザー

2019/11/12 09:13

すみません、このWeakMapを使う利得はなんでしょうか?
think49

2019/11/12 10:32 編集

@jjj_aaa さん > まず動かしたのですがonlyAが一個だけでonlyBが900個になってました。 すみません。initを元にした正答値が不明だったので、思いついたアルゴリズムをコードに書き起こすのみで、入念なテストをしていませんでした。 質問文の「試したソースコード」は期待値を返すとして、期待値と比較してみます。 > このWeakMapを使う利得 WeakMapはprivate propertyの代わりです。 wm.get(this).x は外部参照不可能な「プライベートプロパティx」のように扱います。 http://cou929.nu/data/google_javascript_style_guide/#id29 コーディング規約の命名で縛るだけでは、外部参照を防げないので。
退会済みユーザー

退会済みユーザー

2019/11/12 09:51

なるほど、「WeakMap private property」で調べたら出てきました(think49さんが)。そういう手法があるんですね。 私はTypeScript使ってるのでprivateメンバを持たせてますが、コンパイラ突破後はJSなので普通にアクセスできちゃうのはその通りなんですよね。(てかブラケット構文使えばTypeScriptでもアクセスできるけど(anyだけど)) 明日くらいにもう一度チャレンジします。
guest

0

こんにちは

lodash を使えば、以下のように書けるかなと思います。

javascript

1const getUnique = (a,b) => _.differenceWith(a, b, _.isEqual);

要素がオブジェクトの場合の例が以下です。

追記 - getUnique10

上記ではlodashを使った汎用的なコードを挙げましたが、コメントから頂きましたjsFiddleのテストコードによって生成されるオブジェクトの配列を処理することに特化したコードも考えてみました。以下の getUnique10 になります。

javascript

1function getUnique10(a, b) { 2 const [setA, setB] = [new Set(), new Set()]; 3 4 for (let e of a) 5 setA.add(e.id); 6 7 for (let e of b) 8 setB.add(e.id); 9 10 const onlyA = a.filter(e => !setB.has(e.id)); 11 const onlyB = b.filter(e => !setA.has(e.id)); 12 13 return [onlyA, onlyB] 14}

この getUnique10 を試すコードを以下の jsFiddle に作成しました。

上記は、コメントからご教示いただきました、 https://jsfiddle.net/e9a5kvw2/ を Forkして、テスト対象としてgetUnique10 を追加したものです。

追記 - getUnique11

前掲の getUnique10 の改良案を挙げます。この改良案は、(2019/11/14 21:10のコメントに書いたとおり、) 下記

javascript

1 const num = 1000; 2 for (let i = 0; i < num; i++) { 3 const x = i; 4 const y = Math.floor(Math.random() * num); 5 // const id = `${x}:${y}` 6 const id = 1000 * x + y; 7  ・・・

のようなコードによって、 id に0以上の整数の値を設定することが前提になります。getUnique11 は以下です。

javascript

1function getUnique11(a, b) { 2 const set = new Set(); 3 4 for (let e of a) 5 set.add(e.id); 6 7 for (let e of b) 8 set.add(~e.id); 9 10 const onlyA = a.filter(e => !set.has(~e.id)); 11 const onlyB = b.filter(e => !set.has(e.id)); 12 13 return [onlyA, onlyB]; 14}

getUnique10 では 2個の Set を使っていましたが、上記の getUnique11 では、1個で済ませるようにして、Set 1個分のコンストラクタ new Set() の時間を減らすことを狙っています。Setを1個にした替わりに、b の要素の id をSet にaddするときに、a の要素のidと被らない値に変換する必要がありますが、その変換のために、(整数に対する演算として速いことが期待できる)ビット反転 ~ を使っています。

以下にて、 getUnique10getUnique11 とを比較できます。

当方の環境(PC: Macbook Pro(Core i7 2.8GHz), ブラウザ: Chrome バージョン: 78.0.3904.97)では、以下

イメージ説明

のように、getUnique11の所要時間のほうが、getUnique10 の所要時間よりも短くなる結果を得ています。

追記 - getUnique12

先述の getUnique11 のループで使っている、 for ・・・ of および filter を、 (ofなしの)for文に変更した、 getUnique12 を作成しました。

javascript

1function getUnique12(a, b) { 2 const set = new Set(); 3 4 const lenA = a.length; 5 const lenB = b.length; 6 7 for (let i=0; i < lenA; ++ i) 8 set.add(a[i].id); 9 10 for (let i=0; i < lenB; ++ i) 11 set.add(~b[i].id); 12 13 const onlyA = []; 14 for (let i=0; i < lenA; ++ i) { 15 if(!set.has(~a[i].id)) 16 onlyA.push(a[i]); 17 } 18 19 const onlyB = []; 20 for (let i=0; i < lenB; ++ i) { 21 if(!set.has(b[i].id)) 22 onlyB.push(b[i]); 23 } 24 25 return [onlyA, onlyB]; 26} 27

getUnique11との比較をするための jsFiddleが下記です。

当方の環境では、getUnique12 のほうが数ミリ〜10ミリ秒程度、短くなる結果を確認しています。 

イメージ説明

追記 - getUnique13

アルゴリズムとしては、getUnique8 を拝借し、比較対象として、0以上の整数として設定される id(=1000 * x + y) を使うコードです。

javascript

1function getUnique13(a, b) { 2 const lenA = a.length, 3 lenB = b.length, 4 onlyA = [], 5 onlyB = []; 6 7 a.sort((e1, e2) => e1.id - e2.id); 8 b.sort((e1, e2) => e1.id - e2.id); 9 10 for (let iA=0, iB=0; iA < lenA || iB < lenB; ) { 11 if (iB === lenB || iA < lenA && a[iA].id < b[iB].id) { 12 onlyA.push(a[iA ++]); 13 } else if (iA === lenA || iB < lenB && b[iB].id < a[iA].id) { 14 onlyB.push(b[iB ++]); 15 } else { 16 iA ++; 17 iB ++; 18 } 19 } 20 21 return [onlyA, onlyB]; 22}

参考になれば幸いです。

所感

ご質問のサンプルにあるように、配列a 配列 b ともにその要素はすべて

typescript

1{ x: number; y: number }

という型を満たしているという前提が明らかになっていれば、これを元に、問題の対象範囲に出現するオブジェクトの集合が全順序集合になるような、何らかの大小関係(つまり、ソート結果が一意になる比較関数)を定義できるかどうか検討することができて、もし(運良く)大小関係を定義できれば、Set や Map を使わない getUnique13やその元となっている getUnique8のようなコードを書けますが、もし、ab の要素として入ってくるオブジェクトの型が不明という状況、すなわち、ご質問のタイトルにある
「二つの配列、それぞれに含まれるユニークな値をできるだけ高速に抽出する方法」
の一般解を求めようとすると、この回答の冒頭に挙げた

javascript

1const getUnique = (a,b) => _.differenceWith(a, b, _.isEqual);

のような時間のかかるコードに頼らざるを得なくなるのかなという気はいたします。

とはいえ、実際の何らかの問題領域においては、配列要素となるオブジェクトの型は何らか特定できるものなので、その型情報から、問題領域に出現し得るオブジェクト群が全順序を満たすような何らかの大小比較関数、あるいは大小の比較が可能な(今回の idプロパティ のような)そのオブジェクトのプリミティブ値(※)を導入できるかどうか検討することが、ある型を要素とする配列の差分抽出には優先順位の高いタスクかなと思われました。その結果、うまく大小関係を定義できれば、getUnique8をベースとした速いコードを書くことができます。

(※参考:このような「そのオブジェクトの値を表すプリミティブ値を返す」ために、対象とするオブジェクト群専用の valueOf() メソッドが定義される場合があります。)

投稿2019/11/11 09:09

編集2019/11/16 08:42
jun68ykt

総合スコア9058

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/11 09:47

ありがとうございます。 jsFiddleでlodahをインポートいただいたコードをgetUnique9として実装してみましたが、どうもハングしてしまいます。 https://jsfiddle.net/e9a5kvw2/ ※test変数の中の変数名を変えればテストできます。 221行目の最初の動作チェックは動いたのを確認できたのですが、その後の1000回ループでハングしている様子。
2KOH

2019/11/11 10:31

横から失礼。ループ回数を10回くらいに減らせば動くと思います。
jun68ykt

2019/11/11 17:34

@jjj_aaaさん コメントありがとうございます。 そちらの jsFiddle をforkして、2つのオブジェクトの同値判定に、汎用的な _.isEqual ではなく、このご質問に特化した function eq(a, b) { return a.x === b.x && a.y === b.y; } を使い、getUnique9 を function getUnique9_2(a,b) { return [_.differenceWith(a, b, eq), _.differenceWith(b, a, eq)]; } に修正したものが以下です。 https://jsfiddle.net/jun68ykt/cp34udr8/18/
jun68ykt

2019/11/11 17:39

@2KOHさん コメントありがとうございます。 > ループ回数を10回くらいに減らせば動くと思います。 動きましたが、かなり時間がかかりましたので、オブジェクトの同値判定を見直しました。
退会済みユーザー

退会済みユーザー

2019/11/12 03:54

修正いただきありがとうございます。 getUnique7: 252ms, //2KOHの回答のもの getUnique9_2: 1271ms lodashは便利ですが、汎用的なため今回のような速度を求める場所では適用しないほうが良さそうです。 可読性は高く品質も磨かれていると思うので、ループなどなく性能要求が厳しくない部分では積極的に使おうと思いました。
jun68ykt

2019/11/12 04:08

@jjj_aaaさん はい。そのような方針で良いかと思います。
jun68ykt

2019/11/14 11:37

@jjj_aaaさん lodashを使わずに、なるべく高速に処理するコードも考えて、回答のほうに、getUnique10 として追記しました。また、ご教示のjsFiddleをForkした、 https://jsfiddle.net/jun68ykt/b501gqpj/1/ にてgetUnique10をテストしましたので、ご確認頂けますと幸いです。
jun68ykt

2019/11/14 12:27 編集

@jjj_aaaさん 当方の環境(PC: Macbook Pro(2.8GHz Core i7), ブラウザ: Chrome バージョン: 78.0.3904.97) にて、上記のgetUnique10  を追加した、 https://jsfiddle.net/jun68ykt/b501gqpj/1/ を表示し、5回テストを試行したときの画面キャプチャを、以下に挙げておきます。 1回目: https://git.io/Jerr4 (166ms) 2回目: https://git.io/Jerr0 (171ms) 3回目: https://git.io/Jerrg (165ms) 4回目: https://git.io/Jerr2 (179ms) 5回目: https://git.io/Jerra (170ms) ※()内は getUnique10 の要した時間です。
jun68ykt

2019/11/14 12:29 編集

@jjj_aaaさん もう一点だけ、改良できる部分を挙げますと、 id を文字列から数値にすると若干速くなります。function init()  の中で、 const num = 1000; for (let i = 0; i < num; i++) { const x = i; const y = Math.floor(Math.random() * num); となっているので、 x と y はともに 0以上999以下であることをふまえて、id を `${x}:${y}` から、 1000 * x + y に修正したものが以下です。(※ getUnique10は、回答に追記したものから変更していません) https://jsfiddle.net/jun68ykt/sgqx4zey/1/ 上記の jsFiddle を先に挙げた、当方の環境で表示させた画面キャプチャが、以下です。 https://git.io/Jerrx (122ms)
jun68ykt

2019/11/14 14:51

@jjj_aaaさん getUnique10 では2つの Set を使っていたのを、1つのSetで済ませる getUnique11 を追記しました。
jun68ykt

2019/11/16 04:48

@jjj_aaaさん getUnique13 を追記しました。 追伸 今回、このご質問の回答を通じて、lodash, Map, Set などの便利なものを使ったときの時間的コストをあらためて確認することができました。御礼申し上げます。(質問に +1)
退会済みユーザー

退会済みユーザー

2019/11/19 00:36

@jun68yktさん すみません、別問題にハマっていて見れていませんでした。。 たくさん書いていただきありがとうございます。後ほど検証します。
guest

0

コードに起こすのは煩雑となりますのでいったんアイデアだけ書きます。

比較すべきものが数値や文字列などの値いくつかに還元できるのでしたら、その数値や文字列を特定のルール(たとえば、数値を!で区切って特定の順番で並べる)で文字列エンコードして、それをSetに投入するなりObjectのキーに入れるなりをすれば、もとは2重ループでO(N**2)かかったものをO(N)まで落とせるかと思います。

(なお、JSONエンコードすると、キーの順序が保証されないので、そのままでは比較に使えません)

投稿2019/11/11 05:36

maisumakun

総合スコア145963

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/11 05:43

ありがとうございます。 なんか良さそうなので理解したいのですが、これはAにしかなかったものとBにしかなかったものを判別するのはどういう感じになりますか? 一つのSetにAの文字列表現配列をつっこみ、Bの文字列表現配列を突っ込むと重複排除はしてくれそうですが、どれがAにしかないものでどれがBにしかないものか分からなくないかな~と思って質問しました。 #多分私の理解が間違っていると思う
maisumakun

2019/11/11 05:46

「Aの中身からSetを起こす」→「Bの要素について1つずつ、「AのSetにあればそっちからは削除、なければBのSetに追加」としていく」とすれば、「Aにしかない要素のSet」と「Bにしかない要素のSet」が出来上がります。 (重複については別にハンドリングする必要がありますが)
退会済みユーザー

退会済みユーザー

2019/11/11 05:56

なるほど。意味IDになる文字列起こしを先に実施しておくことができれば早くなるかも。そしてこの案での追加コストはSet生成ですね。 文字列起こしのコストは外に出す前提で(じゃないと多分負ける)、ちょっと試してみます。
退会済みユーザー

退会済みユーザー

2019/11/11 06:28

早くなりました! 文字列変換を外でやるものをgetUnique3、文字列変換を逐次やるものをgetUnique4として実装しました。 https://jsfiddle.net/jx3co5tw/4/ getUnique1: 962ms, getUnique2: 730ms, getUnique3: 170ms, getUnique4: 367ms 文字列変換も含むパターンでもfilterより早かったので、結構おどろきました。 元データ構成をいじるgetUnique3はキツイかもなので、getUnique4の採用を検討してみます。(重複の検討、あとIEでもPolyfill使って試したりしてから) ありがとうございます。
退会済みユーザー

退会済みユーザー

2019/11/11 06:29

一応3と4のコードも載せとこう。コメントでもシンタックスハイライトしてほしい。 /** Set使うパターン(文字列生成外だし) */ function getUnique3(a, b) { const aSet = new Set(a.map(v => v.id)); const bSet = new Set(); for (let i = 0, l = b.length; i < l; i++) { if (aSet.has(b[i].id)) { aSet.delete(b[i].id); } else { bSet.add(b[i].id) } } return [ a.filter(v => aSet.has(v.id)), b.filter(v => bSet.has(v.id)) ] } /** 文字列生成含む */ function getUnique4(a, b) { const aa = a.map(v => `${v.x}:${v.y}`) const bb = b.map(v => `${v.x}:${v.y}`) const aSet = new Set(aa); const bSet = new Set(); for (let i = 0, l = bb.length; i < l; i++) { if (aSet.has(bb[i])) { aSet.delete(bb[i]); } else { bSet.add(bb[i]) } } return [ a.filter(v => aSet.has(`${v.x}:${v.y}`)), b.filter(v => bSet.has(`${v.x}:${v.y}`)) ] }
退会済みユーザー

退会済みユーザー

2019/11/11 07:06

さらにSetを介さず配列コピーと削除(getUnique5)も試しましたが、これよりもSetの方が早い!(1,2よりは5の方が早いけど) getUnique3: 184ms, getUnique4: 372ms, getUnique5: 417ms /** 配列コピーして削除していく方策 */ function getUnique5(a, b) { const copyA = a.slice(); const copyB = []; for (let bi = 0, bl = b.length; bi < bl; bi++) { let hit = false; for (let ai = 0, al = a.length; ai < al; ai++) { if (a[ai].x === b[bi].x && a[ai].y === b[bi].y) { delete copyA[ai]; hit = true; break; } } if (!hit) { copyB.push(b[bi]); } } return [copyA.filter(v => v), copyB]; }
退会済みユーザー

退会済みユーザー

2019/11/11 07:35

さらにobjectでも試してみました(getUnique6)。これはSetの文字列生成版(4)よりは早い感じ。これがいいかもしれない。 getUnique3: 194ms, getUnique4: 411ms, getUnique5: 409ms, getUnique6: 357ms /** objectのキーバリューを利用*/ function getUnique6(a, b) { const aa = {}; a.map(v => aa[`${v.x}:${v.y}`] = v); const bb = {}; for (let i = 0, l = b.length; i < l; i++) { const id = `${b[i].x}:${b[i].y}`; if (aa[id]) { delete aa[id]; } else { bb[id] = b[i]; } } return [ Object.values(aa), Object.values(bb) ] }
guest

0

もしもデータに制約があったり偏りがあったりするなら、その制約を前提としたコードにしたり、偏りの弱点をついたりする方法もあります。

javascript

1function getUnique(a,b) { 2 const map = new Map; 3 const setA = new Set; 4 const setB = new Set; 5 6 for (const v of a) { 7 const { x, y } = v; 8 if (!map.has(y)) map.set(y, new Map); 9 map.get(y).set(x, v); 10 setA.add(v); 11 } 12 13 for (const v of b) { 14 const { x, y } = v; 15 if (!map.has(y)) { 16 setB.add(v); 17 continue; 18 } 19 const mapy = map.get(y); 20 if (!mapy.has(x)) { 21 setB.add(v); 22 continue; 23 } 24 setA.delete(mapy.get(x)); 25 } 26 27 const onlyA = [...setA]; 28 const onlyB = [...setB]; 29 return [onlyA, onlyB] 30}

これは、a の各要素と b の各要素それぞれに重複するデータがないという制約で、かつ y の値域が狭いという偏りを利用したコードです。
(さらに y が 20未満の自然数という制約を使うなら、Map でなく配列を使う等でより高速化できますが。)

まあ、普通にやる分には、文字列エンコードできるようなデータなら素直に文字列エンコードを使うやり方でいいと思います。

投稿2019/11/11 07:15

2KOH

総合スコア999

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/11 07:54

ありがとうございます。 いただいたコードをためしてみましたが、元のものよりかなりいい数値がでました!(getUnique7がそれ) getUnique3: 193ms, // maisumakunさんの回答参照 getUnique4: 368ms, // maisumakunさんの回答参照 getUnique6: 364ms, // maisumakunさんの回答参照 getUnique7: 189ms // 今回の 実はyが20未満という制約はないので(元のコードが悪かったです。すみません)yの値はrandom値にするように変更して試してみましたが、getUnique3並みに早かったです! Mapをネストするのは見た瞬間はためらいましたが、これはいいですね。 getUnique3と比べ疑似IDとして文字列を生成するコストもなくなるので、こちらも検証の上検討してみようと思います!(getUnique3か6か7というところ)
2KOH

2019/11/11 08:18

回答にも書きましたが、特別な理由がないなら素直に文字列エンコードの方法でいいと思いますよ。 割と自己満足で書いたコードですし、実際に使う際は文字列エンコードの方がコードが単純なため保守が楽ですし。 文字列エンコードの方法でもまだ速度が足りないとか、とにかく1ミリ秒でも速くしたいとか、そういうときにだけこの回答のような方法を検討してください。
退会済みユーザー

退会済みユーザー

2019/11/11 08:28

ありがとうございます。トータルコスト考えて検討します!
guest

0

順序がつくもので、量が多ければ双方ソートして頭からせーのっていうのが一番速い気がします(気がするだけ)。
※AもBもuniqueを仮定
※I/F合わせてソートするオブジェクトも変わりました
※Aが空になったときにオーバーフローするバグを修正しました

JavaScript

1function compareFunc_XXX(left, right) { 2 if (left[0] < right[0]) return -1 3 if (left[0] > right[0]) return 1 4 return 0; 5} 6 7function getUnique_XXX(a,b) { 8 const aa = a.map(v => [`${v.x}:${v.y}`,v]); 9 const bb = b.map(v => [`${v.x}:${v.y}`,v]); 10 const sortA = aa.sort(compareFunc_XXX); 11 const sortB = bb.sort(compareFunc_XXX); 12 let idxA = 0; 13 let idxB = 0; 14 const lenA = a.length; 15 const lenB = b.length; 16 const resA = []; 17 const resB = []; 18 19 while (idxA != lenA || idxB != lenB) { 20 if (idxB == lenB || (idxA != lenA && sortA[idxA][0] < sortB[idxB][0])) { 21 resA.push(sortA[idxA][1]); 22 ++idxA; 23 } else if (idxA == lenA || sortA[idxA][0] > sortB[idxB][0]) { 24 resB.push(sortB[idxB][1]); 25 ++idxB; 26 } else { 27 ++idxA; 28 ++idxB; 29 } 30 } 31 return [resA, resB]; 32}

測ってみた結果はうちの環境だと、1000個で4と同じくらい、10000個で3に迫ります。

投稿2019/11/11 07:12

編集2019/11/11 11:15
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/11 07:41

ありがとうございます。 要素がリテラルならいけそうな感じありますね。要素飛ばしとか配列長異なる場合の動作が大丈夫かは確認が必要そうですね。 今回は要素がObjectかつ順序性は確立できないので、ちょっと適用は難しそうです。
退会済みユーザー

退会済みユーザー

2019/11/11 07:58

順序は他のと同様につけられると思います。 null値相当とuniqueでないものはこの実装では対応できていません。 配列長は違っていても問題ありません。 バグは当然あるかもしれません。 ハッシュを使う方式で済む量であれば、こちらを考える必要はないと思いますが、何にせよ決めるのは質問者なので、気にせず自由に選んでください。
退会済みユーザー

退会済みユーザー

2019/11/11 08:20

いまI/F合わせてdiff関数作ってやってみてるのですが、インデックスのオーバーランがおきますね・・・もう少しやってみます。
退会済みユーザー

退会済みユーザー

2019/11/11 08:26

Aが無くなっててBが残ってるときにオーバーランするんだ。修正中・・・
退会済みユーザー

退会済みユーザー

2019/11/11 08:29

流石にそこまでやらなくても…申し訳ないっす… こっちは確認すらしてないし、そちらのコードはチラ見しかしてないので… サッサと諦めてしまってください笑 夜に時間が出来たら少しこちらでも見てみます
退会済みユーザー

退会済みユーザー

2019/11/11 08:36

すみません、理屈は分かっているのですがきれいに通らず断念してしまいました。
退会済みユーザー

退会済みユーザー

2019/11/11 11:19

回答修正しました。取り込めるようにはなったはず。 不具合の方はおっしゃるとおりオーバーランしてましたね。 対称に書いて安心したら判定が先でダメだったようです。 気付かないもんですね。 お手数おかけしてすみません。
退会済みユーザー

退会済みユーザー

2019/11/12 04:02 編集

修正ありがとうございます。通りました! getUnique2: 868ms, //元のfilter使ったやつ getUnique7: 229ms,//2KOHさんの回答 getUnique8: 486ms//今回の https://jsfiddle.net/3zs6j24t/ 元のものより倍速かったです!が、MapやSetを使うパターンよりは遅くなりました。 私的にも実はこれが一番速いかな?と思っていたので、少し意外でした。やってみないと分からないものだ。(きっとブラウザにも依ってしまうだろうが。。。)
退会済みユーザー

退会済みユーザー

2019/11/12 04:16

内部を測ってないので、時間がかかっている部分に依りますね。 比較を文字列キーでなくx,yに順番つけて…にすれば(キーが他の方法と変わりますが)もっと速くなるとは思います。 他の方法はチューニングとかしてないようなので、キーくらい合わせてたんですけどね。 でもMapやSetで使われるようなHashTableやTreeの生成/探索が有効に機能してるうちは、こんな方法に拘らなくていいと思います。 方式を量に合わせて適応的に入れ替えるのもありですが、メンテナンスも面倒だし、難しいですね。 このデータ形式で個数が10000近くまで行くとかなら考え始めた方がいいかもです。
退会済みユーザー

退会済みユーザー

2019/11/12 04:26

改造して、 getUnique2: 911ms, getUnique7: 239ms, getUnique8: 54ms まで行きました。やっぱこれが早いじゃん! https://jsfiddle.net/3zs6j24t/2/ 以下の感じ、ID用の文字列生成を削りました。 /** dameoの回答 */ function getUnique8(a,b) { const diff = (a, b) => a.x === b.x ? a.y - b.y : a.x - b.x; const sortA = a.sort(diff); const sortB = b.sort(diff); let idxA = 0; let idxB = 0; const lenA = a.length; const lenB = b.length; const resA = []; const resB = []; while (idxA != lenA || idxB != lenB) { if (idxB == lenB || (idxA != lenA && diff(sortA[idxA],sortB[idxB]) < 0)) { resA.push(sortA[idxA]); ++idxA; } else if (idxA == lenA || diff(sortA[idxA], sortB[idxB]) >0) { resB.push(sortB[idxB]); ++idxB; } else { ++idxA; ++idxB; } } return [resA, resB]; }
退会済みユーザー

退会済みユーザー

2019/11/12 04:27

あとは見た目キレイにしてみます。
退会済みユーザー

退会済みユーザー

2019/11/12 04:32

確認されちゃいましたか…すみません笑
guest

0

IEのPolyfill なんて言葉もコメントされていたので、若干、古典的手法で考えてみました
意図する抽出になっていますかね?

javascript

1function getUnique(a,b) { 2 let hasA = {}; 3 let hasB = {}; 4 5 let setPos = (oj,v) => { 6 let pos = `${v.x}:${v.y}`; 7 (oj[pos] || (oj[pos]={ count:0, item:v })).count++; 8 }; 9 let filterByPos = (oj,oj2) => { 10 let rslt = []; 11 let keys = Object.keys( oj ); 12 for ( let i=0, l=keys.length; i<l; ++i ) { 13 let pos = keys[i]; 14 if( oj2.hasOwnProperty(pos) ) { 15 // 両方に存在しない 16 delete oj[pos]; 17 delete oj2[pos] 18 } 19 else if( oj[pos].count > 1 ) { 20 // oj内で重複しない 21 delete oj[pos] 22 } 23 if( oj[pos] ) rslt.push(oj[pos].item); 24 } 25 return rslt; 26 }; 27 28 a.forEach(v => {setPos(hasA,v)} ); 29 b.forEach(v => {setPos(hasB,v)} ); 30 return [ 31 filterByPos( hasA, hasB ) 32 , filterByPos( hasB, hasA ) 33 ]; 34};

オブジェクト({x: num, y:num })を返却してなかったのでコードを差し替えしました。
filter().map() を使うべきところforで走査回数を抑えました。

投稿2019/11/11 09:52

編集2019/11/11 10:22
AkitoshiManabe

総合スコア5434

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/12 03:50

検証までやっていただき、ありがとうございました。
guest

0

何をどう取ればいいのかわかりづらいのですが
こういうことじゃないのですか?

javascript

1const a = [0,1,2,3]; 2const b = [2,3,4,5]; 3const onlyA=a.filter(x=>b.indexOf(x)<0); 4const onlyB=b.filter(x=>a.indexOf(x)<0); 5console.log(onlyA) // [0,1] 6console.log(onlyB) // [4,5]

調整

命題にあわせます

javacript

1const getUnique=(a,b)=>[a.filter(x=>b.indexOf(x)<0),b.filter(x=>a.indexOf(x)<0)]; 2const a = [0,1,2,3]; 3const b = [2,3,4,5]; 4const [onlyA,onlyB]=getUnique(a,b); 5console.log(onlyA) // [0,1] 6console.log(onlyB) // [4,5]

投稿2019/11/11 05:14

編集2019/11/11 05:23
yambejp

総合スコア116661

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/11/11 05:36

ご回答ありがとうございます。 最初のコードは動作のイメージ用のコードで、解決したいのはリテラルの配列でなくオブジェクトの配列です。「試したソースコード」のinit()で生成される2つの配列について解決したく。 追記いただいたものは、元コードのgetUnique2と同じっぽい感じですね。(リテラルとオブジェクトの違い)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問