[Salesforce] Listのintersection(積集合)の高速化

2021.06.02

小ネタです。

Listのintersection

Apexで二つのListのintersection(積集合、共通集合とも)はSetを使って次のように実装できます。

List<SomeObj> listA = getSomeObjListA(); // getSomeObjListA はN個のSomeObjを返すものとする
List<SomeObj> listB = getSomeObjListB(); // getSomeObjListB はN個のSomeObjを返すものとする

List<SomeObj> listAB = new List<SomeObj>(); // listA と listB のintersection を入れるリスト

Set<SomeObj> setOfListA = new Set<SomeObj>(listA); // Setを使うことでループの計算量をO(N)にする
for ( SomeObj obj : listB ) {
    if ( setOfListA.contains(obj) ) {
       listAB.add(obj);
    }
}

計算量がO(1)のSet.containsを使うことで、forループの計算量が全体で O(N x 1) = O(N) になっています。

これを、次のように計算量がO(N)のList.indexOfを使うと、forループの計算量が O(N x N) = O(N^2) になってしまいます。

List<SomeObj> listA = getSomeObjListA(); // getSomeObjListA はN個のSomeObjを返すものとする
List<SomeObj> listB = getSomeObjListB(); // getSomeObjListB はN個のSomeObjを返すものとする

List<SomeObj> listAB = new List<SomeObj>(); // listA と listB のintersection を入れるリスト

for ( SomeObj obj : listB ) {
    Integer index = listA.indexOf(obj); // List.indexOfは計算量がO(N)かかる
    if ( index != -1 ) {
       listAB.add(obj);
    }
}

こうなると、N = 数百ぐらいのオーダーでSalesforceのガバナ制限の一つである、「Salesforce サーバの最大 CPU 時間」の10,000msに抵触してしまい、実行時エラーになります。

indexOfが遅いのは常にListの先頭から順に一致するかを見ていくからで、ペンキ屋のシュレミールがいるからです1

ちなみに、Javaでは HashSet.IntersectWith を使うという手もあります。
同様のことはApexでは Set.retainAll で表現できますが、ここにも実装のどこかにペンキ屋のシュレミールがいるようで、まともに動きませんでした。

参考資料


  1. Joel on Software にて、記述されているジョーク。シュレミールがペンキを塗った壁の量が日を追うごとに激減していくが、それはシュレミールがペンキの缶を初日のスタートの位置から動かさなかったからというオチ。「毎日ペンキの缶から遠くなってくんで!」