【发布时间】:2012-08-22 02:34:58
【问题描述】:
我使用标准二分搜索快速返回排序列表中的单个对象(关于可排序属性)。
现在我需要修改搜索以便返回 ALL 匹配的列表条目。我应该如何做到最好?
【问题讨论】:
-
什么语言? “标准”二分搜索可能不同或有一些方便的重载。
-
@ColinD:我目前在 Java 中使用自己的实现。大约有十几行。
我使用标准二分搜索快速返回排序列表中的单个对象(关于可排序属性)。
现在我需要修改搜索以便返回 ALL 匹配的列表条目。我应该如何做到最好?
【问题讨论】:
好吧,由于列表已排序,您感兴趣的所有条目都是连续的。这意味着您需要找到与找到的项目相等的第一个项目,从二分搜索产生的索引向后看。最后一项也是如此。
您可以简单地从找到的索引向后退,但是如果有很多与找到的项相等的项目,这样解决方案可能会像 O(n) 一样慢。所以你应该更好地使用指数搜索:当你找到更多相等的项目时,你的跳跃加倍。这样你的整个搜索仍然是 O(log n)。
【讨论】:
首先让我们回忆一下幼稚的二分查找代码sn-p:
int bin_search(int arr[], int key, int low, int high)
{
if (low > high)
return -1;
int mid = low + ((high - low) >> 1);
if (arr[mid] == key) return mid;
if (arr[mid] > key)
return bin_search(arr, key, low, mid - 1);
else
return bin_search(arr, key, mid + 1, high);
}
引用斯基纳教授的话: 假设我们删除相等测试 if (s[middle] == key) 返回(中);从上面的实现中返回索引低 而不是每次不成功搜索时的 -1。现在所有搜索都将 不成功,因为没有平等测试。搜索将继续 每当将键与相同的数组进行比较时,右半部分 元素,最终终止于右边界。重复 反转二进制比较的方向后搜索将 引导我们到左边界。每次搜索都需要 O(lgn) 时间,所以我们可以 无论大小如何,都以对数时间计算出现次数 块。
所以,我们需要两轮binary_search来找到lower_bound(找到不小于KEY的第一个数字)和upper_bound(找到大于KEY的第一个数字)。
int lower_bound(int arr[], int key, int low, int high)
{
if (low > high)
//return -1;
return low;
int mid = low + ((high - low) >> 1);
//if (arr[mid] == key) return mid;
//Attention here, we go left for lower_bound when meeting equal values
if (arr[mid] >= key)
return lower_bound(arr, key, low, mid - 1);
else
return lower_bound(arr, key, mid + 1, high);
}
int upper_bound(int arr[], int key, int low, int high)
{
if (low > high)
//return -1;
return low;
int mid = low + ((high - low) >> 1);
//if (arr[mid] == key) return mid;
//Attention here, we go right for upper_bound when meeting equal values
if (arr[mid] > key)
return upper_bound(arr, key, low, mid - 1);
else
return upper_bound(arr, key, mid + 1, high);
}
希望对你有帮助:)
【讨论】:
如果我在关注您的问题,您有一个对象列表,为了进行比较,这些对象看起来像 {1,2,2,3,4,5,5,5,6,7,8,8,9}。正常搜索 5 会找到比较为 5 的对象之一,但您想全部获取,对吗?
在这种情况下,我建议使用标准的二分搜索,在到达匹配元素时,开始向左看直到它停止匹配,然后再向右看(从第一个匹配)直到它停止匹配。
请注意,您使用的任何数据结构都不会覆盖相同的元素!
或者,考虑使用一种结构来存储与该位置的存储桶比较的元素。
【讨论】:
我会进行两次二分搜索,一次查找比较 >= 值的第一个元素(在 C++ 术语中,lower_bound),然后一次搜索比较 > 值的第一个元素(在 C++ 术语中,upper_bound)。从 lower_bound 到上界之前的元素就是您要查找的元素(根据 java.util.SortedSet、subset(key, key))。
因此,您需要对标准二分搜索进行两个不同的细微修改:您仍然进行探测并使用探测处的比较来缩小您要查找的值必须位于的区域,但现在例如对于 lower_bound 如果你达到相等,你所知道的是你正在寻找的元素(first 相等的值)在范围的第一个元素和你刚刚探测的值之间的某个位置 -你不能马上回来。
【讨论】:
找到与 bsearch 匹配后,只需递归 bsearch 双方直到不再匹配
伪代码:
range search (type *array) {
int index = bsearch(array, 0, array.length-1);
// left
int upperBound = index -1;
int i = upperBound;
do {
upperBound = i;
i = bsearch(array, 0, upperBound);
} while (i != -1)
// right
int lowerBound = index + 1;
int i = lowerBound;
do {
lowerBound = i;
i = bsearch(array, lowerBound, array.length);
} while (i != -1)
return range(lowerBound, UpperBound);
}
但没有涵盖任何极端情况。我认为这将使您的复杂性保持在 (O(logN))。
【讨论】:
这取决于您使用的二进制搜索的实现:
equal_range 方法在一次调用中产生您想要的结果。为了加快在 Java 和 .NET 中搜索相等范围过长而无法进行线性迭代的情况,您可以查找前导元素和后继元素,并在找到的范围中间取值, 不包括结尾。
如果由于第二次二分搜索而太慢,请考虑编写自己的搜索,同时查找两端。这可能有点乏味,但它应该运行得更快。
【讨论】:
equal_range 翻译成您选择的语言应该不会太难。
我首先找到给定可排序属性的单个元素的索引(使用“正常”二进制搜索),然后开始查看列表中元素的左右两侧,将找到的所有元素添加到满足搜索条件,当一个元素不满足条件或者没有元素可以遍历时,在一端停止,当左右两端都满足前面提到的停止条件时,完全停止。
【讨论】:
Java 中的这段代码在 一次传递 中以 O(logN) 时间计算有序数组中目标值的出现次数。很容易修改它以返回找到的索引列表,只需传入 ArrayList。
想法是递归地细化e 和b 边界,直到它们成为具有目标值的连续块的上下边界;
static int countMatching(int[] arr, int b, int e, int target){
int m = (b+e)/2;
if(e-b<2){
int count = 0;
if(arr[b] == target){
count++;
}
if(arr[e] == target && b!=e){
count++;
}
return count;
}
else if(arr[m] > target){
return countMatching(arr,b,m-1, target);
}
else if(arr[m] < target){
return countMatching(arr, m+1, e, target);
}
else {
return countMatching(arr, b, m-1, target) + 1
+ countMatching(arr, m+1, e, target);
}
}
【讨论】:
你的二分搜索是返回元素还是元素所在的索引?你能得到索引吗?
由于列表已排序,所有匹配的元素都应该相邻出现。如果可以得到标准搜索中返回的项目的索引,则只需从该索引开始双向搜索,直到找到不匹配项。
【讨论】:
试试这个。它的效果惊人。
工作示例,Click here
var arr = [1, 1, 2, 3, "a", "a", "a", "b", "c"]; // It should be sorted array.
// if it arr contain more than one keys than it will return an array indexes.
binarySearch(arr, "a", false);
function binarySearch(array, key, caseInsensitive) {
var keyArr = [];
var len = array.length;
var ub = (len - 1);
var p = 0;
var mid = 0;
var lb = p;
key = caseInsensitive && key && typeof key == "string" ? key.toLowerCase() : key;
function isCaseInsensitive(caseInsensitive, element) {
return caseInsensitive && element && typeof element == "string" ? element.toLowerCase() : element;
}
while (lb <= ub) {
mid = parseInt(lb + (ub - lb) / 2, 10);
if (key === isCaseInsensitive(caseInsensitive, array[mid])) {
keyArr.push(mid);
if (keyArr.length > len) {
return keyArr;
} else if (key == isCaseInsensitive(caseInsensitive, array[mid + 1])) {
for (var i = 1; i < len; i++) {
if (key != isCaseInsensitive(caseInsensitive, array[mid + i])) {
break;
} else {
keyArr.push(mid + i);
}
}
}
if (keyArr.length > len) {
return keyArr;
} else if (key == isCaseInsensitive(caseInsensitive, array[mid - 1])) {
for (var i = 1; i < len; i++) {
if (key != isCaseInsensitive(caseInsensitive, array[mid - i])) {
break;
} else {
keyArr.push(mid - i);
}
}
}
return keyArr;
} else if (key > isCaseInsensitive(caseInsensitive, array[mid])) {
lb = mid + 1;
} else {
ub = mid - 1;
}
}
return -1;
}
【讨论】:
最近发现了非常有效的算法。
考虑到两个变量(输入大小和搜索键的数量),该算法具有对数时间复杂度。然而,搜索到的键也必须进行排序。
#define MIDDLE(left, right) ((left) + (((right) - (left)) >> 1))
int bs (const int *arr, int left, int right, int key, bool *found)
{
int middle = MIDDLE(left, right);
while (left <= right)
{
if (key < arr[middle])
right = middle - 1;
else if (key == arr[middle]) {
*found = true;
return middle;
}
else
left = middle + 1;
middle = MIDDLE(left, right);
}
*found = false;
/* left points to the position of first bigger element */
return left;
}
static void _mkbs (const int *arr, int arr_l, int arr_r,
const int *keys, int keys_l, int keys_r, int *results)
{
/* end condition */
if (keys_r - keys_l < 0)
return;
int keys_middle = MIDDLE(keys_l, keys_r);
/* throw away half of keys, if the key on keys_middle is out */
if (keys[keys_middle] < arr[arr_l]) {
_mkbs(arr, arr_l, arr_r, keys, keys_middle + 1, keys_r, results);
return;
}
if (keys[keys_middle] > arr[arr_r]) {
_mkbs(arr, arr_l, arr_r, keys, keys_l, keys_middle - 1, results);
return;
}
bool found;
int pos = bs(arr, arr_l, arr_r, keys[keys_middle], &found);
if (found)
results[keys_middle] = pos;
_mkbs(arr, arr_l, pos - 1, keys, keys_l, keys_middle - 1, results);
_mkbs(arr, (found) ? pos + 1 : pos, arr_r, keys, keys_middle + 1, keys_r, results);
}
void mkbs (const int *arr, int N, const int *keys, int M, int *results)
{ _mkbs(arr, 0, N - 1, keys, 0, M - 1, results); }
以下是 C 语言的实现和打算发表的论文草稿: https://github.com/juliusmilan/multi_value_binary_search
你能分享一个用例吗?
【讨论】:
您可以使用以下代码解决您的问题。这里的主要目的是首先找到密钥的下限,然后找到相同的上限。后来我们得到了指数的差异,我们得到了答案。与其拥有两个不同的函数,我们可以使用一个标志来查找同一个函数的上限和下限。
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
int bin_search(int a[], int low, int high, int key, bool flag){
long long int mid,result=-1;
while(low<=high){
mid = (low+high)/2;
if(a[mid]<key)
low = mid + 1;
else if(a[mid]>key)
high = mid - 1;
else{
result = mid;
if(flag)
high=mid-1;//Go on searching towards left (lower indices)
else
low=mid+1;//Go on searching towards right (higher indices)
}
}
return result;
}
int main() {
int n,k,ctr,lowind,highind;
cin>>n>>k;
//k being the required number to find for
int a[n];
for(i=0;i<n;i++){
cin>>a[i];
}
sort(a,a+n);
lowind = bin_search(a,0,n-1,k,true);
if(lowind==-1)
ctr=0;
else{
highind = bin_search(a,0,n-1,k,false);
ctr= highind - lowind +1;
}
cout<<ctr<<endl;
return 0;
}
【讨论】:
您可以进行两种搜索:一种用于范围之前的索引,另一种用于范围之后的索引。因为之前和之后可以重复 - 使用 float 作为“唯一”键”
static int[] findFromTo(int[] arr, int key) {
float beforeKey = (float) ((float) key - 0.2);
float afterKey = (float) ((float) key + 0.2);
int left = 0;
int right = arr.length - 1;
for (; left <= right;) {
int mid = left + (right - left) / 2;
float cur = (float) arr[mid];
if (beforeKey < cur)
right = mid - 1;
else
left = mid + 1;
}
leftAfter = 0;
right = arr.length - 1;
for (; leftAfter <= right;) {
int mid = left + (right - leftAfter) / 2;
float cur = (float) arr[mid];
if (afterKey < cur)
right = mid - 1;
else
left = mid + 1;
}
return new int[] { left, leftAfter };
}
【讨论】: