Ефективний спосіб пошуку елемента


88

Нещодавно у мене було інтерв'ю, де вони задали мені " пошукове " запитання.
Питання було:

Припустимо, існує масив (додатних) цілих чисел, котрий кожен елемент є +1або -1порівняний із сусідніми елементами.

Приклад:

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

Тепер шукайте 7та повертайте його позицію.

Я дав таку відповідь:

Зберігайте значення у тимчасовому масиві, сортуйте їх, а потім застосовуйте двійковий пошук.

Якщо елемент знайдено, поверніть його позицію у тимчасовому масиві.
(Якщо число зустрічається двічі, поверніть його перше входження)

Але, здавалося, вони не були задоволені цією відповіддю.

Яка правильна відповідь?


4
Наскільки мені відомо, лінійний пошук - це хороший спосіб знайти індекс елемента в масиві. Я ще не впевнений в іншому алгоритмі пошуку, який ефективно визначає індекс елемента.
Шон Френсіс Н. Балле

4
Якщо 7 гарантовано з’являється лише один раз, або якщо не має значення, який 7 повертається, ви можете вдосконалити ще декілька лінійних алгоритмів відповіді Коулмена.
user1942027

52
Якщо ваше оригінальне рішення вимагає сортування, це гірше, ніж наївний лінійний пошук. Здається, ви цього не знаєте.
cubuspl42

5
Для сортування потрібно O (nlogn), а двійковий пошук - O (logn). Якщо вам потрібно шукати багато великих значень з великого масиву, ваша відповідь може бути кращою, але якщо вам потрібно здійснити пошук лише один раз, алгоритми O (n) можуть бути кращими.
jingyu9575

23
Я не знаю, чому ніхто інший не згадував про це: ваш метод був не тільки неефективним, він був неправильним , а це набагато гірше простої неефективності. Вимога стосується позиції заданого числа у вихідному масиві . Ваш метод повертає позицію числа у відсортованому масиві . Тепер ви можете отримати вихідну позицію, перетворивши простий масив у масив кортежів (число, orig_pos) перед сортуванням. Але ви цього не згадали, тож, гадаю, ви не згадали про це і в інтерв’ю.
Tom Zych

Відповіді:


125

Ви можете здійснити лінійний пошук із кроками, які часто перевищують 1. Найважливіше зауваження полягає в тому, що якщо наприклад, array[i] == 47 і ще не з’явилося, то наступний кандидат на 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), але я не думаю, що існує більш швидкий спосіб це зробити.
shapiro yaacov

2
Ви можете зробити це в середньому трохи швидше, отримавши більше кандидатів (наприклад, першого та останнього), а потім перейти до найближчого до цілі - тобто, якщо вам потрібно знайти лише один випадок, а не перший.
mkadunc

2
@mkadunc Це гарна ідея. Ще одне зауваження полягає в тому, що якщо перший і останній елементи перетинають 7, то в цьому особливому випадку ви можете використовувати двійковий пошук (якщо вам байдуже, який саме 7 ви знайдете)
Джон Коулман

1
У випадку, коли вам потрібно знайти будь-які 7 (не обов’язково перший), я пропоную наступне (практичне) вдосконалення. Складіть список розділів (два цілих числа, 'start' і 'end') і замість того, щоб починати з початку масиву, починайте з середини. Відповідно до значення в комірці ігноруйте відповідний діапазон і додайте два залишені розділи до вашого списку розділів. Тепер повторіть для наступного пункту у списку. Це все ще 'O (n)', але ви ігноруєте подвійний діапазон кожного разу, коли перевіряєте клітинку.
shapiro yaacov

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і просто пропускаєте одиницю щоразу, хоча ви можете пропустити всі 193. Чому не просто i += abs(7 - array[i])?
user1942027

1
Вам слід встановити skipабсолютну різницю між 7 і array[i].
Martin Zabel

@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

Почніть з першого елемента і перевірте, чи cдорівнює 7. Скажімо, це індекс поточної позиції. Таким чином, на початковому етапі, 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
Недокументований код мовою з принаймні напівофіційною умовою . Чим це відрізняється від відповідей Джона Коулмена та Акешвара, окрім як ліберальної інтерпретації тегу "с"?
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);

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.