搜索元素的有效方法


88

最近我接受了一次采访,他们问我一个“ 搜索 ”问题。
问题是:

假设有一个(正)整数数组,每个元素都是+1-1与其相邻元素比较。

例:

array = [4,5,6,5,4,3,2,3,4,5,6,7,8];

现在搜索7并返回其位置。

我给了这个答案:

将值存储在临时数组中,对其进行排序,然后应用二进制搜索。

如果找到该元素,则返回其在临时数组中的位置。
(如果数字出现两次,则返回其第一次出现)

但是,他们似乎对此答案并不满意。

正确的答案是什么?


4
据我所知,线性搜索是在数组中定位元素索引的好方法。我还不确定另一种有效查找元素索引的搜索算法。
肖恩·弗朗西斯·N·巴拉伊

4
如果保证7只出现一次,或者返回哪个7无关紧要,您可以在科尔曼答案的线性算法上进行更多改进。
user1942027 2015年

52
如果您的原始解决方案需要排序,则比单纯的线性搜索更糟糕。您似乎没有意识到。
cubuspl42

5
排序需要O(nlogn),二进制搜索为O(logn)。如果需要从大型数组中搜索许多值,则答案可能会更好,但是如果仅搜索一次,则O(n)算法可能会更好。
jingyu9575

23
我不知道为什么没有人提到这一点:您的方法不仅效率低下而且不正确,而且比效率低下还要糟糕。要求是给定数字在原始数组中的位置。您的方法返回数字在排序数组中的位置。现在,您可以通过在排序之前将简单数组转换为元组数组(数字或orig_pos)检索原始位置。但是您没有提及,所以我猜您在采访中也没有提及。
Tom Zych

Answers:


126

您可以使用通常大于1的步长进行线性搜索。关键的观察结果是,如果eg array[i] == 4和7尚未出现,则7的下一个候选项位于index处i+3。使用while循环可重复直接进入下一个可行的候选对象。

这是一个实现,略有概括。它会k在数组中找到第一个匹配项(受+ = 1限制),或者-1如果没有出现:

#include <stdio.h>
#include <stdlib.h>

int first_occurence(int k, int array[], int n);

int main(void){
    int a[] = {4,3,2,3,2,3,4,5,4,5,6,7,8,7,8};
    printf("7 first occurs at index %d\n",first_occurence(7,a,15));
    printf("but 9 first \"occurs\" at index %d\n",first_occurence(9,a,15));
    return 0;
}

int first_occurence(int k, int array[], int n){
    int i = 0;
    while(i < n){
        if(array[i] == k) return i;
        i += abs(k-array[i]);
    }
    return -1;
}

输出:

7 first occurs at index 11
but 9 first "occurs" at index -1

8
正是我在想什么。这是O(N),但我认为没有更快的方法。
shapiro yaacov 2015年

2
平均来说,您可以使用更多的候选对象(例如第一个和最后一个)来加快处理速度,然后选择最接近目标的候选对象-也就是说,如果您只需要查找单个实例而不是第一个实例。
mkadunc

2
@mkadunc这是一个好主意。另一个观察结果是,如果第一个元素和最后一个元素跨越7个,那么在这种特殊情况下,您可以使用二进制搜索(如果您不在乎找到哪个7个元素)
John Coleman

1
如果您需要找到7个(不一定是第一个),我建议进行以下(实际)改进。列出节(两个整数,“ start”和“ end”)的列表,而不是从数组的开头开始,而是从中间开始。根据单元格中的值,忽略相关范围,并将剩余的两个部分添加到部分列表中。现在,为列表中的下一项重复。仍然是“ O(n)”,但每次检查单元格时,您都会忽略两倍的范围。
shapiro yaacov

3
@ShapiroYaacov:结合检查从值的较低值到较高值到节的两边的间隔是否包含k(7),这值得一个答案。
灰胡子

35

您的方法太复杂了。您无需检查每个数组元素。第一个值4,所以7至少 7-4元素了,你可以跳过这些。

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
    int array[] = {4,5,6,5,4,3,2,3,4,5,6,7,8};
    int len = sizeof array / sizeof array[0];
    int i = 0;
    int steps = 0;
    while (i < len && array[i] != 7) {
        i += abs(7 - array[i]);
        steps++;
    }

    printf("Steps %d, index %d\n", steps, i);
    return 0;
}

程序输出:

Steps 4, index 11

编辑:从@Raphael Miedl和@Martin Zabel发表评论后得到改进。


2
一个鸡蛋里挑骨头,if ((skip = 7 - array[i]) < 1) skip = 1;似乎比它复杂化,在我看来它pessimize。如果array[i] == 200-193每次都能跳过全部193,即使每次都跳过1,那么为什么不i += abs(7 - array[i])呢?
user1942027 2015年

1
您应该将设置skip为7和之间的绝对差array[i]
马丁·扎贝尔

@Raphael Miedl不,一个元素不会200,您会过去的7
天气叶片

3
@WeatherVane我们没有保证,只有相邻的值彼此是+1/ -1。因此可能就是这样array[0] == 200,而其他大多数都是-1
user1942027 2015年

1
@WeatherVane假定始终在数组中找到该元素,而事实并非如此。-1是这种情况下的有效回报;这会改变您的代码很多
Eugene

20

常规线性搜索的一种变体可能是个不错的选择。让我们选择一个元素说array[i] = 2。现在,array[i + 1]将为1或3(奇数),array[i + 2]将为(仅正整数)2或4(偶数)。

这样继续下去,就可以观察到一个模式- array[i + 2*n]将保留偶数,因此所有这些索引都可以忽略。

另外,我们可以看到

array[i + 3] = 1 or 3 or 5
array[i + 5] = 1 or 3 or 5 or 7

因此,i + 5下一个应该检查索引,并且可以使用一个while循环来确定下一个要检查的索引,具体取决于在index处找到的值i + 5

虽然这具有复杂性O(n)(就渐进复杂性而言为线性时间),但由于不访问所有索引,因此在实际意义上优于常规线性搜索。

显然,如果array[i](我们的起点)很奇怪,那么所有这些都将被逆转。


8

约翰·科尔曼(John Coleman)提出的方法很可能是面试官所希望的。
如果您愿意复杂得多,则可以增加预期的跳过长度:
调用目标值k。从位置p的第一个元素的值v开始,并用绝对值av调用差kv dv。为了加快否定搜索的速度,请窥视最后一个元素作为位置o处的另一个值u:如果dv×du为负,则存在k(如果可以接受k的任何出现,则可以按照二进制搜索的方式在此处缩小索引范围)。如果av + au大于数组的长度,则k不存在。(如果dv×du为零,则v或u等于k。)
省略索引的有效性:探测(“ next”)位置,在该位置序列可能返回到v,中间的k为:o = p + 2*av
如果dv×du为负,则从p + av到o-au求k(递归?);
如果为零,则u等于k。
如果杜等于DV和中间的值不是k,或Au超过AV,
或者你无法找到从pk信息+ AV于邻AU,
p=o; dv=du; av=au;并保持探测。
(要完整回顾60年代的文字,请使用Courier查看。我的“第一第二想法”是使用o = p + 2*av - 1,因此du du等于dv除外。)


3

第1步

从第一个元素开始,然后检查它是否为7。假设c是当前位置的索引。因此,最初 c = 0

第2步

如果为7,则找到索引。是c。如果您已到达数组的末尾,请突破。

步骤3

如果不是,则必须|array[c]-7|相距至少7个位置,因为每个索引只能添加一个单位。因此,添加|array[c]-7|到当前索引c,然后再次转到STEP 2进行检查。

在最坏的情况下,当存在交替的1和-1s时,时间复杂度可能达到O(n),但平均情况会很快得到解决。


这与约翰·科尔曼的答案有何不同?(除了建议|c-7|在哪里 |array[c]-7|找。)
灰胡子

我刚看到他的回答。我承认核心思想是相同的。
Akeshwar Jha 2015年

最初的问题没有规定数组以小于7的数字开头。因此array[c]-7可以是正数或负数。您需要先申请abs()才能跳过。
arielf 2015年

你是对的。这就是为什么我使用array[c] - 7模数运算符|array[c] - 7|
Akeshwar Jha 2015年

3

在这里,我给出了Java的实现...

public static void main(String[] args) 
{       
    int arr[]={4,5,6,5,4,3,2,3,4,5,6,7,8};
    int pos=searchArray(arr,7);

    if(pos==-1)
        System.out.println("not found");
    else
        System.out.println("position="+pos);            
}

public static int searchArray(int[] array,int value)
{
    int i=0;
    int strtValue=0;
    int pos=-1;

    while(i<array.length)
    {
        strtValue=array[i];

        if(strtValue<value)
        {
            i+=value-strtValue;
        }
        else if (strtValue==value)
        {
            pos=i;
            break;
        }
        else
        {
            i=i+(strtValue-value);
        }       
    }

    return pos;
}

2
具有至少半官方约定的语言中未公开的代码。除了自由地解释标签“ c”之外,这与John Coleman和Akeshwar的答案有何不同?
灰胡子

3

这是分而治之的解决方案。以牺牲(更多)记账为代价,我们可以跳过更多元素。而不是从左到右扫描,而是在中间进行测试并向两个方向跳过。

#include <stdio.h>                                                               
#include <math.h>                                                                

int could_contain(int k, int left, int right, int width);                        
int find(int k, int array[], int lower, int upper);   

int main(void){                                                                  
    int a[] = {4,3,2,3,2,3,4,5,4,5,6,7,8,7,8};                                   
    printf("7 first occurs at index %d\n",find(7,a,0,14));                       
    printf("but 9 first \"occurs\" at index %d\n",find(9,a,0,14));               
    return 0;                                                                    
}                                                                                

int could_contain(int k, int left, int right, int width){                        
  return (width >= 0) &&                                                         
         (left <= k && k <= right) ||                                            
         (right <= k && k <= left) ||                                            
         (abs(k - left) + abs(k - right) < width);                               
}                                                                                

int find(int k, int array[], int lower, int upper){                              
  //printf("%d\t%d\n", lower, upper);                                            

  if( !could_contain(k, array[lower], array[upper], upper - lower )) return -1;  

  int mid = (upper + lower) / 2;                                                 

  if(array[mid] == k) return mid;                                                

  lower = find(k, array, lower + abs(k - array[lower]), mid - abs(k - array[mid]));
  if(lower >= 0 ) return lower;                                                    

  upper = find(k, array, mid + abs(k - array[mid]), upper - abs(k - array[upper]));
  if(upper >= 0 ) return upper;                                                  

  return -1;                                                                     

}

neal-fultz您的答案不会返回第一个出现的位置,而是返回搜索元素的任何随机出现的位置,因为您是从中间开始并从任一侧跳过。
拉姆·帕特拉

递归顺序的切换留给读者练习。
Neal Fultz,2015年

1
neal-fultz,然后在您的printf()方法调用中编辑消息。
拉姆·帕特拉

1

const findMeAnElementsFunkyArray = (arr, ele, i) => {
  const elementAtCurrentIndex = arr[i];

  const differenceBetweenEleAndEleAtIndex = Math.abs(
    ele - elementAtCurrentIndex
  );

  const hop = i + differenceBetweenEleAndEleAtIndex;

  if (i >= arr.length) {
    return;
  }
  if (arr[i] === ele) {
    return i;
  }

  const result = findMeAnElementsFunkyArray(arr, ele, hop);

  return result;
};

const array = [4,5,6,5,4,3,2,3,4,5,6,7,8];

const answer = findMeAnElementsFunkyArray(array, 7, 0);

console.log(answer);

希望包括该问题的递归解决方案。请享用

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.