Подтвердить что ты не робот

Каков самый быстрый метод поиска для отсортированного массива?

Отвечая на еще один вопрос, я написал программу ниже, чтобы сравнить различные методы поиска в отсортированном массиве. В основном я сравнивал две реализации поиска Interpolation и один из двоичного поиска. Я сравнивал производительность путем подсчета циклов, проведенных (с тем же набором данных) различными вариантами.

Однако я уверен, что есть способы оптимизировать эти функции, чтобы сделать их еще быстрее. Кто-нибудь есть идеи о том, как я могу сделать эту функцию поиска быстрее? Решение на C или С++ является приемлемым, но мне нужно его обработать массив с 100000 элементами.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <assert.h>

static __inline__ unsigned long long rdtsc(void)
{
  unsigned long long int x;
     __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
     return x;
}

int interpolationSearch(int sortedArray[], int toFind, int len) {
    // Returns index of toFind in sortedArray, or -1 if not found
    int64_t low = 0;
    int64_t high = len - 1;
    int64_t mid;

    int l = sortedArray[low];
    int h = sortedArray[high];

    while (l <= toFind && h >= toFind) {
        mid = low + (int64_t)((int64_t)(high - low)*(int64_t)(toFind - l))/((int64_t)(h-l));

        int m = sortedArray[mid];

        if (m < toFind) {
            l = sortedArray[low = mid + 1];
        } else if (m > toFind) {
            h = sortedArray[high = mid - 1];
        } else {
            return mid;
        }
    }

    if (sortedArray[low] == toFind)
        return low;
    else
        return -1; // Not found
}

int interpolationSearch2(int sortedArray[], int toFind, int len) {
    // Returns index of toFind in sortedArray, or -1 if not found
    int low = 0;
    int high = len - 1;
    int mid;

    int l = sortedArray[low];
    int h = sortedArray[high];

    while (l <= toFind && h >= toFind) {
        mid = low + ((float)(high - low)*(float)(toFind - l))/(1+(float)(h-l));
        int m = sortedArray[mid];

        if (m < toFind) {
            l = sortedArray[low = mid + 1];
        } else if (m > toFind) {
            h = sortedArray[high = mid - 1];
        } else {
            return mid;
        }
    }

    if (sortedArray[low] == toFind)
        return low;
    else
        return -1; // Not found
}

int binarySearch(int sortedArray[], int toFind, int len) 
{
    // Returns index of toFind in sortedArray, or -1 if not found
    int low = 0;
    int high = len - 1;
    int mid;

    int l = sortedArray[low];
    int h = sortedArray[high];

    while (l <= toFind && h >= toFind) {
        mid = (low + high)/2;

        int m = sortedArray[mid];

        if (m < toFind) {
            l = sortedArray[low = mid + 1];
        } else if (m > toFind) {
            h = sortedArray[high = mid - 1];
        } else {
            return mid;
        }
    }

    if (sortedArray[low] == toFind)
        return low;
    else
        return -1; // Not found
}

int order(const void *p1, const void *p2) { return *(int*)p1-*(int*)p2; }

int main(void) {
    int i = 0, j = 0, size = 100000, trials = 10000;
    int searched[trials];
    srand(-time(0));
    for (j=0; j<trials; j++) { searched[j] = rand()%size; }

    while (size > 10){
        int arr[size];
        for (i=0; i<size; i++) { arr[i] = rand()%size; }
        qsort(arr,size,sizeof(int),order);

        unsigned long long totalcycles_bs = 0;
        unsigned long long totalcycles_is_64 = 0;
        unsigned long long totalcycles_is_float = 0;
        unsigned long long totalcycles_new = 0;
        int res_bs, res_is_64, res_is_float, res_new;
        for (j=0; j<trials; j++) {
            unsigned long long tmp, cycles = rdtsc();
            res_bs = binarySearch(arr,searched[j],size);
            tmp = rdtsc(); totalcycles_bs += tmp - cycles; cycles = tmp;

            res_is_64 = interpolationSearch(arr,searched[j],size);
            assert(res_is_64 == res_bs || arr[res_is_64] == searched[j]); 
            tmp = rdtsc(); totalcycles_is_64 += tmp - cycles; cycles = tmp;

            res_is_float = interpolationSearch2(arr,searched[j],size);
            assert(res_is_float == res_bs || arr[res_is_float] == searched[j]); 
            tmp = rdtsc(); totalcycles_is_float += tmp - cycles; cycles = tmp;
        }
        printf("----------------- size = %10d\n", size);
        printf("binary search          = %10llu\n", totalcycles_bs);
        printf("interpolation uint64_t = %10llu\n",  totalcycles_is_64);
        printf("interpolation float    = %10llu\n",  totalcycles_is_float);
        printf("new                    = %10llu\n",  totalcycles_new);
        printf("\n");
        size >>= 1;
    }
}
4b9b3361

Ответ 1

Если у вас есть некоторый контроль над макетом данных в памяти, вы можете посмотреть на массивы Judy.

Или поставить более простую идею: бинарный поиск всегда сокращает пространство поиска пополам. Оптимальная точка разреза может быть найдена при интерполяции (точка отсечения НЕ должна быть местом, где должен быть ключ, но точкой, которая минимизирует статистическое ожидание пространства поиска для следующего шага). Это минимизирует количество шагов, но... не все этапы имеют одинаковую стоимость. Иерархические памяти позволяют выполнять несколько тестов одновременно с одним тестом, если местность может поддерживаться. Так как для двоичного поиска сначала М шаги касаются максимум 2 ** M уникальных элементов, их сохранение вместе может дать намного лучшее сокращение пространства поиска для каждой выборки (не для сравнения), что является более высокой производительностью в реальном мире.

На этой основе работают

n-ary деревья, а затем массивы Judy добавляют несколько менее важных оптимизаций.

В нижней строке: даже "Random Access Memory" (RAM) быстрее, когда вы получаете доступ последовательно, чем случайным образом. Алгоритм поиска должен использовать этот факт в своих интересах.

Ответ 2

Сравнивается с Win32 Core2 Quad Q6600, gcc v4.3 msys. Компиляция с g++ -O3, ничего необычного.

Наблюдение - утверждения, сроки и накладные расходы цикла составляют около 40%, поэтому любые коэффициенты, перечисленные ниже, должны быть разделены на 0,6 для получения фактического улучшения в тестируемых алгоритмах.

Простые ответы:

  • На моей машине, заменяющей int64_t на int для "low", "high" и "mid" в интерполяцииSearch дает ускорение на 20-40%. Это самый быстрый способ, который я мог найти. Это занимает около 150 циклов на просмотр на моей машине (для размера массива 100000). Это примерно такое же количество циклов, что и промаха в кеше. Поэтому в реальных приложениях уход за кешем, вероятно, будет самым большим фактором.

  • Замена binarySearch "/2" на " → 1" дает 4% -ную скорость.

  • Используя алгоритм binary_search STL, на векторе, содержащем те же данные, что и "arr", примерно такая же скорость, что и ручной бинарный поиск. Хотя на меньшем "размере" STL намного медленнее - около 40%.

Ответ 3

У меня слишком сложное решение, которое требует специальной функции сортировки. Сорт немного медленнее, чем хорошая быстродействующая сортировка, но все мои тесты показывают, что функция поиска намного быстрее, чем поиск в двоичном или интерполяционном режиме. Я назвал это регрессионной сортировкой, прежде чем узнал, что имя уже было принято, но не подумал о новом имени (идеи?).

Есть три файла для демонстрации.

Код сортировки/поиска регрессии:

#include <sstream>
#include <math.h>
#include <ctime>
#include "limits.h"

void insertionSort(int array[], int length) {
   int key, j;
   for(int i = 1; i < length; i++) {
      key = array[i];
      j = i - 1;
      while (j >= 0 && array[j] > key) {
         array[j + 1] = array[j];
         --j;
      }
      array[j + 1] = key;
   }
}

class RegressionTable {
   public:
      RegressionTable(int arr[], int s, int lower, int upper, double mult, int divs);
      RegressionTable(int arr[], int s);
      void sort(void);
      int find(int key);
      void printTable(void);
      void showSize(void);
   private:
      void createTable(void);
      inline unsigned int resolve(int n);
      int * array;
      int * table;
      int * tableSize;
      int size;
      int lowerBound;
      int upperBound;
      int divisions;
      int divisionSize;
      int newSize;
      double multiplier;
};

RegressionTable::RegressionTable(int arr[], int s) {
   array = arr;
   size = s;
   multiplier = 1.35;
   divisions = sqrt(size);
   upperBound = INT_MIN;
   lowerBound = INT_MAX;
   for (int i = 0; i < size; ++i) {
      if (array[i] > upperBound)
         upperBound = array[i];
      if (array[i] < lowerBound)
         lowerBound = array[i];
   }
   createTable();
}

RegressionTable::RegressionTable(int arr[], int s, int lower, int upper, double mult, int divs) {
   array = arr;
   size = s;
   lowerBound = lower;
   upperBound = upper;
   multiplier = mult;
   divisions = divs;
   createTable();
}

void RegressionTable::showSize(void) {
   int bytes = sizeof(*this);
   bytes = bytes + sizeof(int) * 2 * (divisions + 1);
}

void RegressionTable::createTable(void) {
   divisionSize = size / divisions;
   newSize = multiplier * double(size);
   table = new int[divisions + 1];
   tableSize = new int[divisions + 1];

   for (int i = 0; i < divisions; ++i) {
      table[i] = 0;
      tableSize[i] = 0;
   }

   for (int i = 0; i < size; ++i) {
      ++table[((array[i] - lowerBound) / divisionSize) + 1];
   }

   for (int i = 1; i <= divisions; ++i) {
      table[i] += table[i - 1];
   }
   table[0] = 0;

   for (int i = 0; i < divisions; ++i) {
      tableSize[i] = table[i + 1] - table[i];
   }
}

int RegressionTable::find(int key) {
   double temp = multiplier;
   multiplier = 1;

   int minIndex = table[(key - lowerBound) / divisionSize];
   int maxIndex = minIndex + tableSize[key / divisionSize];
   int guess = resolve(key);
   double t;
   while (array[guess] != key) {
      // uncomment this line if you want to see where it is searching.
      //cout << "Regression Guessing " << guess << ", not there." << endl;
      if (array[guess] < key) {
         minIndex = guess + 1;
      }
      if (array[guess] > key) {
         maxIndex = guess - 1;
      }
      if (array[minIndex] > key || array[maxIndex] < key) {
         return -1;
      }
      t = ((double)key - array[minIndex]) / ((double)array[maxIndex] - array[minIndex]);
      guess = minIndex + t * (maxIndex - minIndex);
   }

   multiplier = temp;

   return guess;
}

inline unsigned int RegressionTable::resolve(int n) {
   float temp;
   int subDomain = (n - lowerBound) / divisionSize;
   temp = n % divisionSize;
   temp /= divisionSize;
   temp *= tableSize[subDomain];
   temp += table[subDomain];
   temp *= multiplier;
   return (unsigned int)temp;
}

void RegressionTable::sort(void) {
   int * out = new int[int(size * multiplier)];
   bool * used = new bool[int(size * multiplier)];
   int higher, lower;
   bool placed;

   for (int i = 0; i < size; ++i) {

      /* Figure out where to put the darn thing */
      higher = resolve(array[i]);
      lower = higher - 1;

      if (higher > newSize) {
         higher = size;
         lower = size - 1;
      } else if (lower < 0) {
         higher = 0;
         lower = 0;
      }
      placed = false;
      while (!placed) {
         if (higher < size && !used[higher]) {
            out[higher] = array[i];
            used[higher] = true;
            placed = true;
         } else if (lower >= 0 && !used[lower]) {
            out[lower] = array[i];
            used[lower] = true;
            placed = true;
         }
         --lower;
         ++higher;
      }
   }
   int index = 0;
   for (int i = 0; i < size * multiplier; ++i) {
      if (used[i]) {
         array[index] = out[i];
         ++index;
      }
   }

   insertionSort(array, size);
}

И тогда есть регулярные функции поиска:

#include <iostream>
using namespace std;

int binarySearch(int array[], int start, int end, int key) {
   // Determine the search point.
   int searchPos = (start + end) / 2;
   // If we crossed over our bounds or met in the middle, then it is not here.
   if (start >= end)
      return -1;
   // Search the bottom half of the array if the query is smaller.
   if (array[searchPos] > key)
      return binarySearch (array, start, searchPos - 1, key);
   // Search the top half of the array if the query is larger.
   if (array[searchPos] < key)
      return binarySearch (array, searchPos + 1, end, key);
   // If we found it then we are done.
   if (array[searchPos] == key)
      return searchPos;
}

int binarySearch(int array[], int size, int key) {
   return binarySearch(array, 0, size - 1, key);
}

int interpolationSearch(int array[], int size, int key) {
   int guess = 0;
   double t;
   int minIndex = 0;
   int maxIndex = size - 1;
   while (array[guess] != key) {

      t = ((double)key - array[minIndex]) / ((double)array[maxIndex] - array[minIndex]);
      guess = minIndex + t * (maxIndex - minIndex);

      if (array[guess] < key) {
         minIndex = guess + 1;
      }
      if (array[guess] > key) {
         maxIndex = guess - 1;
      }
      if (array[minIndex] > key || array[maxIndex] < key) {
         return -1;
      }
   }

   return guess;
}

И затем я написал простой основной вариант для тестирования различных типов.

    #include <iostream>
    #include <iomanip>
    #include <cstdlib>
    #include <ctime>
    #include "regression.h"
    #include "search.h"
    using namespace std;

    void randomizeArray(int array[], int size) {
       for (int i = 0; i < size; ++i) {
          array[i] = rand() % size;
       }
    }

    int main(int argc, char * argv[]) {

       int size = 100000;
       string arg;
       if (argc > 1) {
          arg = argv[1];
          size = atoi(arg.c_str());
       }
       srand(time(NULL));
       int * array;
       cout << "Creating Array Of Size " << size << "...\n";
       array = new int[size];

       randomizeArray(array, size);
       cout << "Sorting Array...\n";
       RegressionTable t(array, size, 0, size*2.5, 1.5, size);
       //RegressionTable t(array, size);
       t.sort();
       int trials = 10000000;
       int start;

       cout << "Binary Search...\n";
       start = clock();
       for (int i = 0; i < trials; ++i) {
          binarySearch(array, size, i % size);
       }
       cout << clock() - start << endl;

       cout << "Interpolation Search...\n";
       start = clock();
       for (int i = 0; i < trials; ++i) {
          interpolationSearch(array, size, i % size);
       }
       cout << clock() - start << endl;

       cout << "Regression Search...\n";
       start = clock();
       for (int i = 0; i < trials; ++i) {
          t.find(i % size);
       }
       cout << clock() - start << endl;

       return 0;
}

Попробуй и скажи, если это будет быстрее для тебя. Это очень сложно, поэтому очень легко сломать его, если вы не знаете, что делаете. Будьте осторожны с его модификацией.

Я скомпилировал main с g++ на ubuntu.

Ответ 4

Если ваши данные, как известно, обладают специальными свойствами, чистый поиск интерполяции может привести к линейному времени. Если вы ожидаете, что интерполяция поможет большинству данных, но не хочет, чтобы это вредилось в случае патологических данных, я бы использовал (возможно взвешенное) среднее значение интерполированного предположения и середины, обеспечив логарифмическую привязку во время выполнения.

Ответ 5

Один из способов приблизиться к этому - использовать компромисс между пространством и временем. Есть несколько способов, которые можно было бы сделать. Крайним способом было бы просто создать массив с максимальным размером, являющимся максимальным значением отсортированного массива. Инициализируйте каждую позицию индексом в sortedArray. Тогда поиск будет просто O (1).

Следующая версия, однако, может быть немного более реалистичной и, возможно, быть полезной в реальном мире. Он использует структуру "помощника", которая инициализируется при первом вызове. Он отображает пространство поиска на меньшее пространство, делясь на число, которое я вытащил из воздуха без особого тестирования. Он сохраняет индекс нижней границы для группы значений в sortedArray в вспомогательную карту. Фактический поиск делит число toFind на выбранный делитель и извлекает суженные границы sortedArray для нормального двоичного поиска.

Например, если отсортированные значения варьируются от 1 до 1000, а делитель равен 100, тогда массив поиска может содержать 10 "разделов". Чтобы найти значение 250, он разделил бы его на 100, чтобы получить целочисленную позицию индекса 250/100 = 2. map[2] будет содержать индекс sortedArray для значений 200 и больше. map[3] будет иметь индексную позицию значений 300 и выше, тем самым обеспечивая меньшую ограничивающую позицию для нормального двоичного поиска. Остальная часть функции является точной копией вашей функции двоичного поиска.

Инициализация вспомогательной карты может быть более эффективной, используя двоичный поиск для заполнения позиций, а не простое сканирование, но это одноразовая стоимость, поэтому я не стал ее тестировать. Этот механизм хорошо работает для заданных тестовых чисел, которые распределены равномерно. Как написано, было бы не так хорошо, если бы распределение было даже не таким. Я думаю, что этот метод можно использовать и с значениями поиска с плавающей запятой. Однако экстраполяция на общие ключи поиска может быть сложнее. Например, я не уверен, что этот метод будет для ключей персональных данных. Ему понадобится какой-то O (1) lookup/hash, который сопоставляется определенной позиции массива, чтобы найти границы индекса. Мне непонятно, в какой момент эта функция будет или существует.

Я быстро скорректировал настройку вспомогательной карты в следующей реализации. Это некрасиво, и я не уверен на 100%, что это правильно во всех случаях, но это показывает идею. Я проверил его с помощью теста отладки, чтобы сравнить результаты с существующей функцией binarySearch, чтобы быть уверенным, что он работает правильно.

Ниже приведены номера примеров:

100000 * 10000 : cycles binary search          = 10197811
100000 * 10000 : cycles interpolation uint64_t = 9007939
100000 * 10000 : cycles interpolation float    = 8386879
100000 * 10000 : cycles binary w/helper        = 6462534

Вот быстрая и грязная реализация:

#define REDUCTION 100  // pulled out of the air
typedef struct {
    int init;  // have we initialized it?
    int numSections;
    int *map;
    int divisor;
} binhelp;

int binarySearchHelp( binhelp *phelp, int sortedArray[], int toFind, int len)
{
    // Returns index of toFind in sortedArray, or -1 if not found
    int low;
    int high;
    int mid;

    if ( !phelp->init && len > REDUCTION ) {
        int i;
        int numSections = len / REDUCTION;
        int divisor = (( sortedArray[len-1] - 1 ) / numSections ) + 1;
        int threshold;
        int arrayPos;

        phelp->init = 1;
        phelp->divisor = divisor;
        phelp->numSections = numSections;
        phelp->map = (int*)malloc((numSections+2) * sizeof(int));
        phelp->map[0] = 0;
        phelp->map[numSections+1] = len-1;
        arrayPos = 0;
        // Scan through the array and set up the mapping positions.  Simple linear
        // scan but it is a one-time cost.
        for ( i = 1; i <= numSections; i++ ) {
            threshold = i * divisor;
            while ( arrayPos < len && sortedArray[arrayPos] < threshold )
                arrayPos++;
            if ( arrayPos < len )
                phelp->map[i] = arrayPos;
            else
                // kludge to take care of aliasing
                phelp->map[i] = len - 1;
        }
    }

    if ( phelp->init ) {
        int section = toFind / phelp->divisor;
        if ( section > phelp->numSections )
            // it is bigger than all values
            return -1;

        low = phelp->map[section];
        if ( section == phelp->numSections )
            high = len - 1;
        else
            high = phelp->map[section+1];
    } else {
        // use normal start points
        low = 0;
        high = len - 1;
    }

    // the following is a direct copy of the Kriss' binarySearch
    int l = sortedArray[low];
    int h = sortedArray[high];

    while (l <= toFind && h >= toFind) {
        mid = (low + high)/2;

        int m = sortedArray[mid];

        if (m < toFind) {
            l = sortedArray[low = mid + 1];
        } else if (m > toFind) {
            h = sortedArray[high = mid - 1];
        } else {
            return mid;
        }
    }

    if (sortedArray[low] == toFind)
        return low;
    else
        return -1; // Not found
}

Вспомогательная структура должна быть инициализирована (и освобождена память):

    help.init = 0;
    unsigned long long totalcycles4 = 0;
    ... make the calls same as for the other ones but pass the structure ...
        binarySearchHelp(&help, arr,searched[j],length);
    if ( help.init )
        free( help.map );
    help.init = 0;

Ответ 6

Посмотрите сначала на данные и можно ли получить большой выигрыш методом данных по общему методу.

Для больших статических отсортированных наборов данных вы можете создать дополнительный указатель для обеспечения частичного закрепления голубей в зависимости от объема памяти, который вы хотите использовать. например скажем, мы создаем двухмерный массив диапазонов 256x256, который мы заполняем начальным и конечным положениями в массиве поиска элементов с соответствующими байтами верхнего порядка. Когда мы приступаем к поиску, мы затем используем байты верхнего порядка на ключе, чтобы найти диапазон/подмножество массива, который нужно искать. Если бы у нас было ~ 20 сравнений в нашем бинарном поиске 100 000 элементов O (log2 (n)), мы теперь до ~ 4 комминов для 16 элементов или O (log2 (n/15)). Стоимость памяти здесь составляет около 512 тыс.

Другой метод, опять же подходящий для данных, которые мало меняются, заключается в том, чтобы разделить данные на массивы общедоступных предметов и редко искать предметы. Например, если вы оставите свой существующий поиск на месте с большим количеством дел в реальном мире в течение длительного периода тестирования и запишите информацию о запрашиваемом элементе, вы вполне можете обнаружить, что распределение очень неравномерно, то есть некоторые значения искали гораздо чаще, чем другие. Если это так, разделите массив на гораздо меньший массив общеупотребимых значений и больший оставшийся массив, и сначала найдите меньший массив. Если данные правильные (большие, если!), Вы часто можете добиться широко аналогичных улучшений для первого решения без затрат на память.

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

Ответ 7

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

int newSearch(int sortedArray[], int toFind, int len) 
{
    // Returns index of toFind in sortedArray, or -1 if not found
    int low = 0;
    int high = len - 1;
    int mid;

    int l = sortedArray[low];
    int h = sortedArray[high];

    while (l < toFind && h > toFind) {
        mid = low + ((float)(high - low)*(float)(toFind - l))/(1+(float)(h-l));

        int m = sortedArray[mid];

        if (m < toFind) {
            l = sortedArray[low = mid + 1];
        } else if (m > toFind) {
            h = sortedArray[high = mid - 1];
        } else {
            return mid;
        }
    }

    if (l == toFind)
        return low;
    else if (h == toFind)
        return high;
    else
        return -1; // Not found
}