Я слышал, что нет более быстрого алгоритма, чем линейный поиск (для несортированного массива), но когда я запускаю этот алгоритм (линейный):

public static void search(int[] arr, int value){
    for(int i = 0; i < arr.length; i++){
        if(arr[i] == value) return;
    }
}

Для случайного массива длиной 1000000 среднее время нахождения значения составляет 75 нс, но с этим алгоритмом:

public static void skipSearch(int[] arr, int value){
    for(int i = 0; i < arr.length; i+=2){
        if(arr[i] == value) return;
    }
    for(int i = 1; i < arr.length; i+=2){
        if(arr[i] == value) return;
    }
}

Я получаю более короткое среднее значение, 68 нс?

Изменить: многие из вас говорят, что я не провёл надлежащий тест, и это было случайно, но я запускал эти функции 1000000 раз и получил среднее значение. И каждый раз, когда я запускал функции 1000000 раз, я получал 75-76 нс для первого алгоритма и 67-69 нс для второго алгоритма.

Я использовал Java System.nanoTime() чтобы измерить это.

Код:

int[] arr = new int[1000];
Random r = new Random();
for(int i = 0; i < arr.length; i++){
    arr[i] = r.nextInt();
}
int N = 1000000;
long startTime = System.nanoTime();
for(int i = 0; i < N; i++){
    search(arr, arr[(int) Math.floor(Math.random()*arr.length)]);
}
System.out.println("Average Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
startTime = System.nanoTime();
for(int i = 0; i < N; i++){
    skipSearch(arr, arr[(int) Math.floor(Math.random()*arr.length)]);
}
System.out.println("Average Skip Search Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
8
programmers5 30 Окт 2015 в 02:43

8 ответов

Лучший ответ

Вполне возможно, что, поскольку ваши методы search() ничего не возвращают, и внутри циклов нет никаких действий, JIT-компилятор в вашей JVM оптимизирует код, другими словами, изменяет байт-код перед его загрузкой в ​​JVM, чтобы оба ваших метода search(), скорее всего, не делают (почти) ничего . Что наиболее важно, он, вероятно, также полностью убирает петли. JIT-оптимизация довольно умна, она может идентифицировать множество ситуаций, когда нет необходимости загружать какой-либо код в JVM (однако код находится в файле байтового кода .class).

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

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

Также измените свои методы search(), чтобы они возвращали индекс, что усложняет жизнь оптимизатору. Однако иногда бывает на удивление сложно создать код, который невозможно оптимизировать :) Надежнее отключить оптимизацию (как по ссылке выше).


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

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

31
Community 23 Май 2017 в 11:53

Когда люди называют линейный поиск «самым быстрым поиском», это чисто академическое заявление. Это не имеет ничего общего с тестами, а скорее с сложностью Big O алгоритм поиска. Чтобы сделать это измерение полезным, Big O определяет только наихудший сценарий для данного алгоритма .

В реальном мире данные не всегда соответствуют наихудшему сценарию, поэтому контрольные показатели будут колебаться для разных наборов данных. В вашем примере разница между двумя алгоритмами составляет 7 нс. Однако что бы произошло, если бы ваши данные выглядели так:

linear_data = [..., value];
skip_search_data = [value, ...];

Эта разница в 7 нс станет намного больше. Для линейного поиска сложность будет O (n) каждый раз. Для пропуска поиска каждый раз будет O (1).

В реальном мире «самый быстрый» алгоритм не всегда самый быстрый. Иногда ваш набор данных поддается другому алгоритму.

0
Community 23 Май 2017 в 12:09

Оба алгоритма делают то же самое, какой из них быстрее, зависит от места, где находится значение, которое вы ищете, так что это совпадение, какой из них быстрее в ОДНОМ конкретном случае.

Но первый стиль в любом случае лучше.

2
Holger 30 Окт 2015 в 11:24

Одна из проблем, упомянутых кем-то, заключалась в том, что вы используете разные индексы для каждого алгоритма. Итак, чтобы исправить это, я немного переработал ваш код. Вот код, который у меня есть:

int[] arr = new int[1000];
Random r = new Random();
for(int i = 0; i < arr.length; i++){
    arr[i] = r.nextInt();
}
int N = 1000000;
List<Integer> indices = new ArrayList<Integer>();
for(int i = 0; i < N; i++){
    //indices.add((int) Math.floor(Math.random()*arr.length/2)*2); //even only
    indices.add((int) Math.floor(Math.random()*arr.length/2)*2+1); //odd only
    //indices.add((int) Math.floor(Math.random()*arr.length)); //normal
}

long startTime = System.nanoTime();
for(Integer i : indices)
{
    search(arr, arr[i]);
}
System.out.println("Average Time: "+(System.nanoTime()-startTime)/(float)N+"ns");

startTime = System.nanoTime();
for(Integer i : indices)
{
    skipSearch(arr, arr[i]);
}
System.out.println("Average Skip Search Time: "+(System.nanoTime()-startTime)/(float)N+"ns");
    

Итак, вы заметите, что я сделал ArrayList<Integer> для хранения индексов, и я предлагаю три различных способа заполнения этого списка массивов - один только с четными числами, один только с нечетными числами и ваш исходный случайный метод.

Запуск с четными числами дает только такой вывод:

Среднее время: 175.609нс

Среднее время пропуска поиска: 100.64691 нс

Запуск с нечетными числами дает только такой вывод:

Среднее время: 178.05182ns

Среднее время пропуска поиска: 263,82928 нс

Запуск с исходным случайным значением дает следующий результат:

Среднее время: 175.95944ns

Среднее время пропуска поиска: 181.20367нс

Каждый из этих результатов имеет смысл.

При выборе только четных индексов ваш алгоритм skipSearch - O (n / 2), поэтому мы обрабатываем не более половины индексов. Обычно нас не заботят постоянные факторы временной сложности, но если мы действительно смотрим на время выполнения, это имеет значение. В этом случае мы буквально сокращаем проблему вдвое, так что это повлияет на время выполнения. И мы видим, что реальное время выполнения соответственно сократилось почти вдвое.

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

Когда используется исходный случайный выбор, мы видим, что skipSearch работает хуже (как и ожидалось). Это потому, что в среднем у нас будет четное количество четных индексов и нечетных индексов. Четные числа будут найдены быстро, а нечетные числа - очень медленно. Линейный поиск найдет индекс 3 на ранней стадии, тогда как skipSearch обрабатывает примерно O (n / 2) элементов, прежде чем найдет индекс 3.

Что касается того, почему ваш исходный код дает странные результаты, насколько мне известно, пока неясно. Может случиться так, что генератор псевдослучайных чисел слегка отдает предпочтение четным числам, это могло быть из-за оптимизации, это могло быть из-за безумия предсказателей ветвлений. Но это, конечно, не сравнение яблок с яблоками путем выбора случайных индексов для обоих алгоритмов. Некоторые из этих вещей все еще могут влиять на мои результаты, но по крайней мере два алгоритма сейчас пытаются найти одинаковые числа.

3
Community 20 Июн 2020 в 09:12

Теоретически временная сложность обоих алгоритмов одинакова O(n). Одно из предположений, почему skipSearch был быстрее, когда вы его запускали, заключается в том, что элемент, который вы искали, оказался расположен по четному индексу, поэтому он будет найден первым циклом, а в худшем случае он сделает половину количества итераций. линейного поиска. В подобных тестах необходимо учитывать не только размер данных, но и то, как они выглядят. Попробуйте найти элемент, который не существует, элемент, который существует с четным индексом, элемент, который существует с нечетным индексом.

Кроме того, даже если этот skipSearch работает лучше при использовании надлежащих тестов, он все равно экономит всего несколько наносекунд, так что значительного увеличения нет, и его не стоит использовать на практике.

3
turingcomplete 30 Окт 2015 в 00:12

Просто случайно «быстрее». Вы, вероятно, заметили, что ваши значения чаще появляются в четном индексе, чем в нечетном.

5
budi 29 Окт 2015 в 23:49

Что такое статистика value? Скорее всего, в вашем случае это даже ценности. Совершенно очевидно, что для обоих случаев сложность алгоритмов O(n) и O(n/2) + O(n/2) примерно одинакова - линейное время

7
vvg 29 Окт 2015 в 23:48

Вот почему нас не беспокоит буквально время, сколько времени потребуется для выполнения, и больше того, как вещи будут расти в масштабе по мере увеличения сложности входных данных. Взгляните на асимптотический анализ времени выполнения:

https://en.wikipedia.org/wiki/Analysis_of_algorithms

18
Colin Schoen 29 Окт 2015 в 23:50