排序数组的最快搜索方法是什么?

时间:2011-01-20 23:45:28

标签: c++ c algorithm sorting optimization

回答another question,我编写了下面的程序来比较排序数组中的不同搜索方法。基本上我比较了插值搜索和二分搜索的两种实现。我通过计算不同变体所花费的周期(使用相同的数据集)来比较性能。

但是我确信有一些方法可以优化这些功能,使它们更快。有没有人对如何更快地使这个搜索功能有任何想法?使用C或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;
    }
}

8 个答案:

答案 0 :(得分:15)

如果您可以控制数据的内存中布局,您可能需要查看Judy数组。

或者提出一个更简单的想法:二元搜索总是将搜索空间减少一半。可以通过插值找到最佳切割点(切割点不应该是预期密钥的位置,而是最小化下一步搜索空间的统计预期的点)。这最大限度地减少了步骤的数量,但并非所有步骤都具有相同的成本。如果可以维持位置,则分层存储器允许在单个测试的同时执行多个测试。由于二进制搜索的前M个步骤仅触及最多2 ** M个唯一元素,因此将这些存储在一起可以更好地减少每个高速缓存行获取的搜索空间(不是每次比较),这在现实世界中具有更高的性能。

n-ary树在此基础上工作,然后Judy数组添加了一些不太重要的优化。

底线:甚至“随机存取存储器”(RAM)在顺序访问时比随机访问更快。搜索算法应该利用这一事实。

答案 1 :(得分:9)

基于Win32 Core2 Quad Q6600,gcc v4.3 msys进行基准测试。用g ++ -O3编译,没什么特别的。

观察 - 断言,定时和循环开销约为40%,因此下面列出的任何增益应除以0.6,以获得被测算法的实际改进。

简单答案:

  1. 在我的机器上用int替换int64_t为“低”,插值搜索中的“高”和“中”使速度增加20%到40%。这是我能找到的最简单的方法。在我的机器上每次查找大约需要150个周期(阵列大小为100000)。这与缓存未命中的周期数大致相同。因此,在实际应用中,照顾缓存可能是最重要的因素。

  2. 用“&gt;&gt; 1”替换binarySearch的“/ 2”可以加快4%的速度。

  3. 在包含与“arr”相同的数据的向量上使用STL的binary_search算法,其速度与手动编码的binarySearch大致相同。虽然在较小的“尺寸”上,STL要慢得多 - 大约40%。

答案 2 :(得分:4)

我有一个过于复杂的解决方案,需要专门的排序功能。排序比快速排序略慢,但我的所有测试都显示搜索功能比二进制或插值搜索快得多。在我发现这个名字已经被采用之前我把它称为回归排序,但是没想到一个新名称(想法?)。

有三个文件要演示。

回归排序/搜索代码:

#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;
}

尝试一下,告诉我你是否更快。这是非常复杂的,所以如果你不知道自己在做什么,那么打破它真的很容易。小心修改它。

我在ubuntu上用g ++编译了main。

答案 3 :(得分:3)

除非您的数据已知具有特殊属性,否则纯插值搜索可能会占用线性时间。如果您期望插值可以帮助处理大多数数据,但在病理数据的情况下不希望它受到伤害,我会使用插值猜测和中点的(可能加权的)平均值,确保运行时间的对数界限。

答案 4 :(得分:3)

接近这一点的一种方法是使用空格与时间的权衡。有许多方法可以做。最简单的方法是简单地创建一个数组,其中max size是排序数组的最大值。使用索引将每个位置初始化为sortedArray。然后搜索将只是O(1)。

然而,以下版本可能更现实,可能在现实世界中有用。它使用在第一次调用时初始化的“辅助”结构。它将搜索空间向下划分为一个较小的空间,除以我从空中拉出的数字而不需要太多测试。它将sortedArray中一组值的下限索引存储到辅助映射中。实际搜索将toFind数除以所选的除数,并提取sortedArray的缩小范围以进行常规二分搜索。

例如,如果排序值的范围为1到1000且除数为100,则查找数组可能包含10个“节”。要搜索值250,它将除以100以产生整数索引位置250/100 = 2。 map[2]将包含值为200或更大的sortedArray索引。 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;

答案 5 :(得分:3)

首先查看数据,以及通过一般方法通过数据特定方法是否可以获得大的收益。

对于大型静态排序数据集,您可以根据您愿意使用的内存量创建一个额外的索引来提供部分信息。例如假设我们创建了一个256x256二维范围数组,我们用相应高位字节的元素搜索数组中的起始位置和结束位置填充。当我们来搜索时,我们然后使用键上的高位字节来找到我们需要搜索的数组的范围/子集。如果我们对100,000个元素O(log2(n))的二元搜索进行了~20次比较,那么我们现在已经将16个元素的约4个comarisons降低到了O(log2(n / 15))。这里的内存成本约为512k

另一种方法,再次适用于不会发生太大变化的数据,是将数据划分为常用项目和很少寻求的项目的数组。例如,如果您将现有搜索保留在长时间测试期间运行大量真实案例,并记录所搜索项目的详细信息,您可能会发现分布非常不均匀,即某些值是比其他人更经常地寻求。如果是这种情况,请将数组拆分为更小的常用值数组和更大的剩余数组,并首先搜索较小的数组。如果数据是正确的(如果!),您通常可以在没有内存成本的情况下实现与第一个解决方案大致类似的改进。

还有许多其他数据特定的优化,其得分远远好于尝试改进经过试验,测试和广泛使用的通用解决方案。

答案 6 :(得分:2)

在问题结束之前发布我当前的版本(希望我以后能够提高它)。现在它比其他所有版本都要糟糕(如果有人理解为什么我对循环结束的更改会产生这种影响,欢迎提出意见)。

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
}

答案 7 :(得分:0)

可以改进用于比较的二进制搜索的实现。关键的想法是&#34;规范化&#34;最初的范围使得目标总是>最小值和&lt;比第一步后的最大值。这增加了终止增量大小。它还具有特殊套管目标的效果,该目标小于排序数组的第一个元素或大于排序数组的最后一个元素。预计搜索时间将缩短约15%。以下是C ++中代码的外观。

int binarySearch(int * &array, int target, int min, int max)
{ // binarySearch
  // normalize min and max so that we know the target is > min and < max
  if (target <= array[min]) // if min not normalized
  { // target <= array[min]
      if (target == array[min]) return min;
      return -1;
  } // end target <= array[min]
  // min is now normalized

  if (target >= array[max]) // if max not normalized
  { // target >= array[max]
      if (target == array[max]) return max;
      return -1;
  } // end target >= array[max]
    // max is now normalized

  while (min + 1 < max)
  { // delta >=2
    int tempi = min + ((max - min) >> 1); // point to index approximately in the middle between min and max
    int atempi = array[tempi]; // just in case the compiler does not optimize this
    if (atempi > target)max = tempi; // if the target is smaller, we can decrease max and it is still normalized        
    else if (atempi < target)min = tempi; // the target is bigger, so we can increase min and it is still normalized        
        else return tempi; // if we found the target, return with the index
        // Note that it is important that this test for equality is last because it rarely occurs.
  } // end delta >=2
  return -1; // nothing in between normalized min and max
} // end binarySearch