Эффективный способ поиска элемента

88

Недавно у меня было интервью, где мне задали « поисковый » вопрос.
Вопрос был:

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

Пример:

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

Теперь найдите 7и верните его позицию.

Я дал такой ответ:

Сохраните значения во временном массиве, отсортируйте их, а затем примените двоичный поиск.

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

Но, похоже, их не удовлетворил этот ответ.

Каков правильный ответ?

NSUser
источник
4
Насколько мне известно, линейный поиск - хороший способ найти индекс элемента в массиве. Я еще не уверен в другом алгоритме поиска, который эффективен при поиске индекса элемента.
Шон Фрэнсис Н. Балле,
4
Если 7 гарантированно появится только один раз или если не имеет значения, какой 7 будет возвращен, вы можете еще немного улучшить линейный алгоритм ответа Коулмана.
user1942027
52
Если исходное решение требует сортировки, это хуже, чем наивный линейный поиск. Вы, кажется, не знаете об этом.
cubuspl42
5
Для сортировки требуется O (nlogn), а для двоичного поиска - O (logn). Если вам нужно искать много значений из большого массива, ваш ответ может быть лучше, но если вы выполняете поиск только один раз, алгоритмы O (n) могут быть лучше.
jingyu9575
23
Я не знаю, почему никто об этом не упомянул: ваш метод был не только неэффективным, но и неправильным , и это намного хуже, чем простая неэффективность. Требуется позиция данного числа в исходном массиве . Ваш метод возвращает позицию числа в отсортированном массиве . Теперь вы можете получить исходную позицию, преобразовав простой массив в массив кортежей (число, orig_pos) перед сортировкой. Но вы не упомянули об этом, так что я предполагаю, что вы тоже не упомянули об этом в интервью.
Tom Zych

Ответы:

125

Вы можете выполнить линейный поиск с шагами, которые часто больше 1. Важное наблюдение состоит в том, что если eg array[i] == 4и 7 еще не появилось, то следующий кандидат на 7 находится по индексу 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), но я не думаю, что есть более быстрый способ сделать это.
шапиро яаков
2
Вы могли бы сделать это в среднем немного быстрее с большим количеством кандидатов (например, первым и последним), а затем перейти к тому, который ближе всего к цели - то есть, если вам нужно найти только одно вхождение, а не первое.
mkadunc
2
@mkadunc Это хорошая идея. Другое наблюдение: если первый и последний элементы совпадают с 7, то в этом особом случае вы можете использовать двоичный поиск (если вам все равно, какие 7 вы найдете)
Джон Коулман
1
В случае, если вам нужно найти любую 7 (не обязательно первую), я предлагаю следующее (практическое) улучшение. Составьте список разделов (два целых числа, «начало» и «конец») и вместо того, чтобы начинать с начала массива, начните с середины. В соответствии со значением в ячейке игнорируйте соответствующий диапазон и добавьте два оставшихся раздела в свой список разделов. Теперь повторите для следующего элемента в списке. Это все еще «O (n)», но вы игнорируете удвоенный диапазон каждый раз, когда проверяете ячейку.
шапиро яаков
3
@ShapiroYaacov: В сочетании с проверкой того, включает ли интервал от меньшего к большему значений по обе стороны секции k (7), это заслуживает отдельного ответа.
greybeard
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;мой взгляд, придирка, кажется, слишком усложняет и пессимизирует. Если array[i] == 200вы получаете -193и просто пропускаете 1 каждый раз, даже если можете пропустить все 193. Почему не просто i += abs(7 - array[i])?
user1942027
1
Вы должны установить skipабсолютную разницу между 7 и array[i].
Мартин Забель
@Raphael Miedl нет, элемента не будет 200, ты бы прошел 7.
Флюгер
3
@WeatherVane у нас нет этой гарантии, только соседние значения находятся +1/ находятся -1друг от друга. Так что это могло быть просто, array[0] == 200а остальные в основном -1российские.
user1942027
1
@WeatherVane предполагает, что элемент всегда находится в массиве, что может быть не так. -1 - допустимый возврат в этом случае; что немного меняет код, который у вас есть
Евгений
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 может использоваться для определения следующего индекса для проверки, в зависимости от значения, найденного в индексе i + 5.

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

Очевидно, все будет наоборот, если array[i](наша отправная точка) была нечетной.

Мадхав Датт
источник
8

Подход, представленный Джоном Коулманом, по всей вероятности, является тем, на что надеялся интервьюер.
Если вы хотите пойти немного сложнее, вы можете увеличить ожидаемую длину пропуска:
вызовите целевое значение k . Начните со значения v первого элемента в позиции p и вызовите разность kv dv с абсолютным значением av . Чтобы ускорить отрицательный поиск, взгляните на последний элемент как на другое значение u в позиции o: если dv × du отрицательно, k присутствует (если любое вхождение k допустимо, вы можете сузить здесь диапазон индекса, как это делает двоичный поиск). Если av + au больше, чем длина массива, k отсутствует. (Если DV × ди равен нулю, v или и равен к.)
Опуская индекс достоверности: зондировать ( «следующий») положение , в котором последовательность может вернуться к V с к в середине: o = p + 2*av.
Если dv × du отрицательно, найти k (рекурсивно?) От p + av до o-au;
если он равен нулю, u равно k в точке o.
Если du ​​равно dv и значение в середине не k, или au превышает av,
или вы не можете найти k от p + av до o-au,
позвольте p=o; dv=du; av=au;и продолжайте зондирование.
(Чтобы полностью вернуться к текстам 60-х годов, просмотрите с помощью Courier. Моей «первой второй мыслью» было использоватьo = p + 2*av - 1, что исключает du равно dv .)

седобородый
источник
4

ШАГ 1

Начните с первого элемента и проверьте, 7. Допустим, cэто индекс текущей позиции. Итак, изначально c = 0.

ШАГ 2

Если это 7, вы нашли индекс. Это c. Если вы достигли конца массива, вырвитесь.

ШАГ 3

Если это не так, то 7 должны быть по крайней мере на |array[c]-7|расстоянии друг от друга, потому что вы можете добавить только единицу для каждого индекса. Поэтому добавьте |array[c]-7|к вашему текущему индексу c и снова перейдите к ШАГУ 2 для проверки.

В худшем случае, когда есть чередующиеся 1 и -1, временная сложность может достигать O (n), но средние случаи будут доставлены быстро.

Акешвар Джа
источник
Чем это отличается от ответа Джона Коулмана? (Помимо предложения, |c-7|где, |array[c]-7|кажется,
требуется
Я только что видел его ответ. Я признаю, что основная идея та же.
Akeshwar Jha
Исходный вопрос не предусматривает, что массив начинается с числа меньше 7. Так что array[c]-7может быть положительным или отрицательным. Вам нужно обратиться abs()к нему, прежде чем переходить вперед.
arielf
Да, ты прав. Вот почему я использую array[c] - 7с оператором модуля |array[c] - 7|.
Akeshwar Jha
4

Здесь я даю реализацию на 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"?
Greybeard
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, ваш ответ не вернет первое вхождение, а вернет любое случайное вхождение элемента поиска, поскольку вы начинаете с середины и пропускаете с любой стороны.
Рам Патра
Изменение порядка рекурсии оставлено читателю в качестве упражнения.
Нил Фульц
1
neal-fultz, отредактируйте сообщение в вызове метода printf ().
Рам Патра
2

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);

Хотел включить рекурсивное решение проблемы. наслаждаться

Энтони Мун Бим Тури
источник