Сравнение двух отсортированных массивов int

У меня есть миллионы массивов int фиксированного размера (100). Каждый массив отсортирован и имеет уникальные элементы. Для каждого массива я хочу найти все массивы, которые имеют 70% общих элементов. Прямо сейчас я получаю около 1 миллиона сравнений (используя Arrays.binarySearch()) в секунду, что слишком медленно для нас.

Может ли кто-нибудь порекомендовать лучший алгоритм поиска?


person ashish    schedule 21.01.2011    source источник
comment
Возможно, вы захотите взглянуть на фильтры Блума.   -  person b_erb    schedule 21.01.2011
comment
Итак, ваш результат — список пар массивов, по крайней мере, с 70 общими элементами?   -  person biziclop    schedule 21.01.2011
comment
Статичны ли массивы после начала сравнения? Постоянно ли генерируются новые массивы? Другими словами, можно ли преобразовать/предварительно обработать массивы в какую-либо другую структуру данных для оптимизации поиска?   -  person unwind    schedule 21.01.2011
comment
Да, результатом является список пар массивов с не менее чем 70 общими элементами.   -  person ashish    schedule 21.01.2011
comment
Да, массивы статичны, и количество массивов также фиксировано.   -  person ashish    schedule 21.01.2011
comment
Кроме того, сколько матчей вы ожидаете? Распределены ли целые примерно равномерно? Я спрашиваю об этом, потому что, если совпадений не так много, может помочь эвристика исключения.   -  person biziclop    schedule 21.01.2011
comment
Я ожидаю около 10% совпадений на основе моего исходного набора данных.   -  person ashish    schedule 21.01.2011
comment
Не очень разбираюсь в фильтрах Блума. Позвольте мне получить некоторую информацию об этом. спасибо за предложение.   -  person ashish    schedule 21.01.2011
comment
Использование Arrays.binarySearch() для меня здесь не имеет смысла. Для сравнения двух массивов вам просто нужно пройтись по ним обоим параллельно и подсчитать общие элементы на лету. То, что я имею в виду, очень похоже на сортировку слиянием.   -  person maaartinus    schedule 21.01.2011
comment
С 10% совпадений и миллионами массивов вы получаете более 1e6 * 1e6 * 0,1 = 1e11 совпадающих пар, это слишком много, не так ли? Полагаю, я что-то неправильно понял. Не могли бы вы уточнить это? Еще один вопрос: в каком диапазоне лежат целые числа?   -  person maaartinus    schedule 21.01.2011
comment
Да, подходящая пара огромна. ints повсюду. нет определенного диапазона. Прямо сейчас мы делаем это сравнение в фреймворке уменьшения карты. Но даже с 10 или около того машинами трудно выполнить процесс в разумные сроки. Итак, я пытаюсь оптимизировать сравнение на одной машине, прежде чем мы перейдем к распределенной среде.   -  person ashish    schedule 21.01.2011
comment
Я один думаю, что это именно то, что можно написать на ассемблере?   -  person biziclop    schedule 21.01.2011


Ответы (3)


Что-то вроде этого должно выполнять эту работу (при условии, что массивы отсортированы и содержат уникальные элементы):

public static boolean atLeastNMatchingElements(final int n,
    final int[] arr1,
    final int[] arr2){

    /* check assumptions */
    assert (arr1.length == arr2.length);

    final int arrLength = arr2.length;

    { /* optimization */
        final int maxOffset = Math.max(arrLength - n, 0);
        if(arr1[maxOffset] < arr2[0] || arr2[maxOffset] < arr1[0]){
            return false;
        }
    }

    int arr2Offset = 0;
    int matches = 0;

    /* declare variables only once, outside loop */
    int compResult; int candidate;

    for(int i = 0; i < arrLength; i++){
        candidate = arr1[i];
        while(arr2Offset < arrLength - 1){
            compResult = arr2[arr2Offset] - candidate;
            if(compResult > 0){
                break;
            } else{
                arr2Offset++;
                if(compResult == 0){
                    matches++;
                    break;
                }
            }
        }
        if(matches == n){
            return true;
        }
        /* optimization */
        else if(matches < n - (arrLength - arr2Offset)){
            return false;
        }
    }
    return false;
}

Пример использования:

public static void main(final String[] args){
    final int[] arr1 = new int[100];
    final int[] arr2 = new int[100];
    int x = 0, y = 0;
    for(int i = 0; i < 100; i++){
        if(i % 10 == 0){
            x++;
        }
        if(i % 12 == 0){
            y++;
        }
        arr1[i] = x;
        arr2[i] = y;
        x++;
        y++;
    }
    System.out.println(atLeastNMatchingElements(70, arr1, arr2));
    System.out.println(atLeastNMatchingElements(95, arr1, arr2));
}

Вывод:

правда
ложь

Преждевременные оптимизации

Теперь я попытался оптимизировать приведенный выше код. Пожалуйста, проверьте, отмечены ли блоки кода как

/* optimization */

сделать заметную разницу. После оптимизации я рефакторил код, чтобы сократить его до одного или двух операторов возврата.

person Sean Patrick Floyd    schedule 21.01.2011
comment
Это мило. Его можно было бы обобщить для работы со многими массивами одновременно, и, вероятно, это было бы быстрее, чем работа только попарно. Тем не менее, я думаю, что все же нужна идея получше. - person maaartinus; 21.01.2011
comment
@ashish Я добавил некоторые незначительные оптимизации, смотрите мое обновление @maaartinus Я согласен, что нужна идея получше - person Sean Patrick Floyd; 21.01.2011
comment
Кажется, не сильно отличается. В любом случае спасибо. - person ashish; 21.01.2011
comment
@ashish, хотя так. Массивы int[] очень быстрые, поэтому каждая «оптимизация» может замедлить работу. - person Sean Patrick Floyd; 21.01.2011
comment
@sean Сейчас это лучший ответ для меня. Спасибо, Шон. - person ashish; 25.01.2011

Есть две быстрые оптимизации, которые вы можете сделать.

Если начальный элемент массива A больше, чем конечный элемент B, они тривиально не могут иметь общих элементов.

Другой - это неравенство треугольника:

f(B,C) <= 100 - |f(A,B)-f(A,C)|

Причина этого в том, что (предполагая f(A,B) > f(A,C)) есть по крайней мере f(A,B) - f(A,C) элемента, которые находятся и в A, и в B, но не в C. Это означает, что они не могут быть общими элементами B и C.

person biziclop    schedule 21.01.2011

Вы можете попробовать сортировку слиянием, игнорируя дубликаты. Это операция O(n) для отсортированных массивов. Если два массива имеют 70% общих элементов, результирующая коллекция будет иметь 130 или менее уникальных целых чисел. В вашем случае вам не нужен результат, поэтому вы можете просто подсчитать количество уникальных записей и остановиться, как только достигнете 131 или конца обоих массивов.

РЕДАКТИРОВАТЬ (2) Следующий код может выполнить ~ 176 миллиардов сравнений примерно за 47 секунд, используя 4 ядра. Создание многопоточного кода с 4 курсами было всего на 70% быстрее.

Использование BitSet работает только в том случае, если диапазон значений int довольно мал. В противном случае вам придется сравнить int[] (я оставил код, если он вам понадобится)

Выполнено 176 467 034 428 сравнений за 47,712 секунды и найдено 444 888 совпадений.

public static void main(String... args) throws InterruptedException {
    int length = 100;
    int[][] ints = generateArrays(50000, length);
    final BitSet[] bitSets = new BitSet[ints.length];
    for(int i=0;i<ints.length;i++) {
        int[] ia = ints[i];
        BitSet bs = new BitSet(ia[ia.length-1]);
        for (int i1 : ia)
            bs.set(i1);
        bitSets[i] = bs;
    }

    final AtomicInteger matches = new AtomicInteger();
    final AtomicLong comparisons = new AtomicLong();
    int nThreads = Runtime.getRuntime().availableProcessors();
    ExecutorService executorService = Executors.newFixedThreadPool(nThreads);

    long start = System.nanoTime();
    for (int i = 0; i < bitSets.length - 1; i++) {
        final int finalI = i;
        executorService.submit(new Runnable() {
            public void run() {
                for (int j = finalI + 1; j < bitSets.length; j++) {
                    int compare = compare(bitSets[finalI], bitSets[j]);
                    if (compare <= 130)
                        matches.incrementAndGet();
                    comparisons.addAndGet(compare);
                }
            }
        });
    }
    executorService.shutdown();
    executorService.awaitTermination(1, TimeUnit.HOURS);
    long time = System.nanoTime() - start;
    System.out.printf("Peformed %,d comparisons in %.3f seconds and found %,d matches %n",comparisons.longValue(),time/1e9, matches.intValue());
}

private static int[][] generateArrays(int count, int length) {
    List<Integer> rawValues = new ArrayList<Integer>(170);
    for (int i = 0; i < 170; i++)
        rawValues.add(i);

    int[][] ints = new int[count][length];
    Random rand = new Random(1);
    for (int[] ia : ints) {
        Collections.shuffle(rawValues, rand);
        for (int i = 0; i < ia.length; i++)
            ia[i] = (int) (int) rawValues.get(i);
        Arrays.sort(ia);
    }
    return ints;
}

private static int compare(int[] ia, int[] ja) {
    int count = 0;
    int i=0,j=0;
    while(i<ia.length && j<ja.length) {
        int iv = ia[i];
        int jv = ja[j];
        if (iv < jv) {
            i++;
        } else if (iv > jv) {
            j++;
        } else {
            count++; // duplicate
            i++;
            j++;
        }
    }
    return ia.length + ja.length - count;
}
private static int compare(BitSet ia, BitSet ja) {
    BitSet both = new BitSet(Math.max(ia.length(), ja.length()));
    both.or(ia);
    both.or(ja);
    return both.cardinality();
}
person Peter Lawrey    schedule 21.01.2011
comment
Это может ускорить сравнение (которое в любом случае было O(1) из-за массивов одинакового размера), но я думаю, что убийцей производительности является само огромное количество сравнений (O(n^2), если я правильно понял с n >> 1,000,000) - person Andreas Dolk; 21.01.2011
comment
Да, тут проблема в сравнении n^2. Это будет распространяться на более чем одну машину. На данный момент я пытаюсь выяснить, как лучше всего сравнивать. Я почти уверен, что с одной машиной невозможно достичь того, что я ищу. - person ashish; 21.01.2011
comment
@ashish, пока сравнение дешевое, вы можете сделать 1 миллион сравнений менее чем за секунду. Сколько массивов вам нужно сравнить? - person Peter Lawrey; 21.01.2011
comment
Одно ядро ​​процессора с частотой 3 ГГц может выполнять миллиард сравнений int в секунду. ;) esp последовательное чтение массива для эффективной загрузки данных в кеш. - person Peter Lawrey; 21.01.2011
comment
@ Питер Лоури, ты прав. В моем случае BitSet здесь бесполезен, так как диапазон значений int повсюду. Спасибо. - person ashish; 24.01.2011
comment
Сравнение int[] делает 1 миллиард каждые две секунды. Этот код должен дать вам основу для запуска на одной или нескольких машинах. - person Peter Lawrey; 24.01.2011