Знаходження дублікатів у просторі O (n) та O (1)


121

Введення: Дано масив з n елементів, який містить елементи від 0 до n-1, причому будь-яке з цих чисел з’являється будь-яку кількість разів.

Мета: знайти ці повторювані числа в O (n) та використовувати лише постійний простір пам'яті.

Наприклад, нехай n буде 7, а масив - {1, 2, 3, 1, 3, 0, 6}, відповідь має бути 1 і 3. Я перевірив подібні запитання тут, але у відповідях використовувались такі структури даних, як HashSetі т.д.

Будь-який ефективний алгоритм для того ж?

Відповіді:


164

Це те, що я придумав, що не вимагає додаткового біта знаків:

for i := 0 to n - 1
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 0 to n - 1
    if A[i] != i then 
        print A[i]
    end if
end for

Перший цикл перетворює масив так, що якщо елемент xприсутній хоча б один раз, то одна з цих записів буде в положенні A[x].

Зауважте, що спочатку він може не виглядати O (n) червоним, але це - хоча він має вкладений цикл, він все ще працює в O(N)часі. Зміна відбувається лише в тому випадку, якщо є iтакий A[i] != i, і кожен своп встановлює принаймні один елемент, такий A[i] == i, де раніше це не було правдою. Це означає, що загальна кількість свопів (і, отже, загальна кількість виконань whileкорпусу циклу) становить щонайбільше N-1.

Другий цикл друкує значення, xдля яких A[x]не дорівнює x- оскільки перший цикл гарантує, що якщо xпринаймні один раз існує в масиві, один з цих екземплярів буде в A[x], це означає, що він друкує ті значення, xяких немає в масив.

(Посилання Ideone, щоб ви могли грати з ним)


10
@arasmussen: Так. Хоча я спершу придумав зламану версію. Контракти проблеми дають трохи підказки для рішення - той факт, що кожне дійсне значення масиву є також дійсним індексом масиву, натякає на a[a[i]], а обмеження простору O (1) натякає на те, що swap()операція є ключовою.
caf

2
@caf: Запустіть свій код із масиву, оскільки {3,4,5,3,4} він не вдається.
NirmalGeo

6
@NirmalGeo: Це неправдиве введення, оскільки 5воно не знаходиться в діапазоні 0..N-1( Nв даному випадку є 5).
caf

2
@caf вихід для {1,2,3,1,3,0,0,0,0,6} становить 3 1 0 0 0 або в будь-якому випадку, коли повторення більше 2. Чи правильно це o / p?
Термінал

3
Це дивно! Я бачив низку варіантів цього питання, як правило, більш обмежених, і це найзагальніший спосіб його вирішення, який я бачив. Я просто зазначу, що зміна printоператора print iперетворює це на рішення для stackoverflow.com/questions/5249985/… і (припускаючи, що "мішок" є змінним масивом) Qk stackoverflow.com/questions/3492302/… .
j_random_hacker

35

блискуча відповідь caf друкує кожне число, яке з’являється k разів у масиві k-1 рази. Це корисна поведінка, але питання, ймовірно, вимагає, щоб кожен дублікат був надрукований лише один раз, і він натякає на можливість зробити це, не роздуваючи лінійні межі часу / постійного простору. Це можна зробити, замінивши його другий цикл наступним псевдокодом:

for (i = 0; i < N; ++i) {
    if (A[i] != i && A[A[i]] == A[i]) {
        print A[i];
        A[A[i]] = i;
    }
}

Це використовує властивість, що після запуску першого циклу, якщо якесь значення mз’являється більше одного разу, гарантується, що одна з цих фігур знаходиться в правильному положенні, а саме A[m]. Якщо ми обережні, ми можемо використовувати це "домашнє" місце для зберігання інформації про те, чи були надруковані ще дублікати.

У версії кафе, коли ми проходили масив, A[i] != iмається на увазі, що A[i]це дублікат. У своїй версії я покладаюся на дещо інший інваріант: це A[i] != i && A[A[i]] == A[i]означає, що A[i]це дублікат, якого ми ще не бачили . (Якщо ви скинете частину "того, чого ми раніше не бачили", решта може бути зрозуміла як істина інваріантності кафе, і гарантія того, що всі копії мають певну копію в домашньому місці.) Ця властивість зберігається у початку (після завершення 1-ї петлі кафе), і я показую, що він підтримується після кожного кроку.

Коли ми проходимо масив, успіх A[i] != iтесту передбачає, що він A[i] може бути дублікатом, якого раніше не бачили. Якщо ми цього ще не бачили, ми очікуємо A[i], що місце розташування будинку вкаже на себе - це те, що перевірено другою половиною ifумови. Якщо це так, ми роздруковуємо його та змінюємо домашнє місце, щоб вказувати на цей перший знайдений дублікат, створюючи 2-кроковий "цикл".

Щоб побачити, що ця операція не змінює наш інваріант, припустимо, m = A[i]для певної позиції iзадовольняє A[i] != i && A[A[i]] == A[i]. Очевидно, що зміна, яку ми вносимо ( A[A[i]] = i), буде працювати, щоб запобігти появі інших неприбуткових випадків mвиведення в якості дублікатів, спричинивши ifзбій 2-ї половини їхніх умов, але чи спрацює вона, коли iприїде в домашнє місцезнаходження m,? Так, так, адже зараз, навіть якщо в цьому новому iми виявимо, що перша половина ifумови A[i] != iє справжньою, друга половина перевіряє, чи є місце, на яке вона вказує, - це домашнє місце та виявляє, що це не так. У цій ситуації ми вже не знаємо , є чи mабо A[m]був повторюється значення, але ми знаємо , що так чи інакше,вже повідомлялося , оскільки ці 2-цикли гарантовано не з’являться в результаті 1-го циклу кафе. (Зверніть увагу, що якщо m != A[m]тоді саме один з mі A[m]трапляється не один раз, а інший не виникає взагалі.)


1
Так, це дуже схоже на те, що я придумав. Цікаво, як однаковий перший цикл корисний для декількох різних проблем, просто з різним циклом друку.
caf

22

Ось псевдокод

for i <- 0 to n-1:
   if (A[abs(A[i])]) >= 0 :
       (A[abs(A[i])]) = -(A[abs(A[i])])
   else
      print i
end for

Зразок коду в C ++


3
Дуже розумно - кодування відповіді в знаковому біті індексованого запису!
holtavolt

3
@sashang: Не може бути. Ознайомтесь із специфікацією проблеми. "Дано масив з n елементів, який містить елементи від 0 до n-1 "
Prasoon Saurav

5
Це не виявить дублікатів 0 і помітить те саме число, що і дублікат кілька разів.
Нульовий сет

1
@Null Set: Ви можете просто замінити -з ~для нульової емісії.
користувач541686

26
Це може бути відповіддю, що проблема вирішується, але технічно вона використовує O(n)прихований простір - nбіт знака. Якщо масив визначений таким чином, що кожен елемент може утримувати лише значення між 0і n-1, він, очевидно, не працює.
caf

2

Для відносно невеликих N ми можемо використовувати операції div / mod

n.times do |i|
  e = a[i]%n
  a[e] += n
end

n.times do |i| 
  count = a[i]/n
  puts i if count > 1
end

Не C / C ++, але все одно

http://ideone.com/GRZPI


+1 Приємне рішення. Якщо зупинити додавання n до запису через два рази, буде розміщено більше n .
Апшир

1

Насправді не дуже, але принаймні легко помітити властивості O (N) та O (1). В основному ми скануємо масив, і для кожного числа ми бачимо, чи відповідна позиція позначена як уже бачена один раз (N), чи вже багато разів бачена (N + 1). Якщо він позначений уже побаченим-раз, ми друкуємо його та відзначаємо його вже-кілька разів. Якщо він не позначений, ми позначаємо його вже поміченим-один раз і переміщуємо початкове значення відповідного індексу в поточну позицію (позначення - це руйнівна операція).

for (i=0; i<a.length; i++) {
  value = a[i];
  if (value >= N)
    continue;
  if (a[value] == N)  {
    a[value] = N+1; 
    print value;
  } else if (a[value] < N) {
    if (value > i)
      a[i--] = a[value];
    a[value] = N;
  }
}

або, ще краще (швидше, незважаючи на подвійний цикл):

for (i=0; i<a.length; i++) {
  value = a[i];
  while (value < N) {
    if (a[value] == N)  {
      a[value] = N+1; 
      print value;
      value = N;
    } else if (a[value] < N) {
      newvalue = value > i ? a[value] : N;
      a[value] = N;
      value = newvalue;
    }
  }
}

+1, це працює чудово, але потрібно було трохи подумати, щоб з’ясувати, чому саме це if (value > i) a[i--] = a[value];працює: якщо value <= iтоді ми вже обробили значення у a[value]і можемо його безпечно перезаписати. Також я б не сказав, що природа O (N) очевидна! Правопис: Основний цикл виконує Nрази, але скільки разів a[i--] = a[value];виконується лінія. Цей рядок може запускатися лише в тому випадку a[value] < N, якщо і кожного разу, коли він запускається, одразу після цього встановлено значення масиву, який ще не було Nвстановлено N, тож він може працювати у більшості Nвипадків, у загальній складності не більше 2Nциклу ітерацій.
j_random_hacker

1

Одне рішення на C:

#include <stdio.h>

int finddup(int *arr,int len)
{
    int i;
    printf("Duplicate Elements ::");
    for(i = 0; i < len; i++)
    {
        if(arr[abs(arr[i])] > 0)
          arr[abs(arr[i])] = -arr[abs(arr[i])];
        else if(arr[abs(arr[i])] == 0)
        {
             arr[abs(arr[i])] = - len ;
        }
        else
          printf("%d ", abs(arr[i]));
    }

}
int main()
{   
    int arr1[]={0,1,1,2,2,0,2,0,0,5};
    finddup(arr1,sizeof(arr1)/sizeof(arr1[0]));
    return 0;
}

Це O (n) час та O (1) просторова складність.


1
Складність простору цього становить O (N), оскільки він використовує N додаткових бітових знаків. Алгоритм повинен працювати в припущенні, що тип елемента масиву може містити числа лише від 0 до N-1.
caf

так, це правда, але для запитуваного альго його ідеально, оскільки вони хотіли, щоб альго було лише для номерів 0 до n-1, а також я перевірив ваше рішення, що йде вище O (n), тому я подумав про це
Anshul garg

1

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

Для ще більшої простоти маємо індекси від 0 до n-1 і діапазон числа від 0..n-1. напр

   0  1  2  3  4 
 a[3, 2, 4, 3, 1]

0 (3) -> 3 (3) - цикл.

Відповідь: Просто обведіть масив, спираючись на індекси. якщо a [x] = a [y], то це цикл і таким чином дублювати. Перейдіть до наступного індексу і продовжуйте знову і так далі до кінця масиву. Складність: O (n) час та O (1) простір.


0

Крихітний код пітона для демонстрації вищевказаного методу:

a = [3, 1, 1, 0, 4, 4, 6] 
n = len(a)
for i in range(0,n):
    if a[ a[i] ] != a[i]: a[a[i]], a[i] = a[i], a[a[i]]
for i in range(0,n):
    if a[i] != i: print( a[i] )

Зауважте, що заміни, можливо, повинні відбуватися не один раз для одного iзначення - зауважте whileу своїй відповіді.
caf

0

Алгоритм можна легко побачити в наступній функції С. Отримати вихідний масив, хоча і не потрібно, буде можливим, приймаючи кожен модуль введення n .

void print_repeats(unsigned a[], unsigned n)
{
    unsigned i, _2n = 2*n;
    for(i = 0; i < n; ++i) if(a[a[i] % n] < _2n) a[a[i] % n] += n;
    for(i = 0; i < n; ++i) if(a[i] >= _2n) printf("%u ", i);
    putchar('\n');
}

Ideone Link для тестування.


Я боюся, що це технічно "обман", оскільки для роботи з номерами до 2 * n потрібен додатковий 1 біт місця для кожного запису в масиві над тим, що потрібно для зберігання вихідних чисел. Насправді вам потрібно ближче до log2 (3) = 1,58 додаткових біт на запис, оскільки ви зберігаєте числа до 3 * n-1.
j_random_hacker

0
static void findrepeat()
{
    int[] arr = new int[7] {0,2,1,0,0,4,4};

    for (int i = 0; i < arr.Length; i++)
    {
        if (i != arr[i])
        {
            if (arr[i] == arr[arr[i]])
            {
                Console.WriteLine(arr[i] + "!!!");
            }

            int t = arr[i];
            arr[i] = arr[arr[i]];
            arr[t] = t;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();

    for (int j = 0; j < arr.Length; j++)
    {
        if (j == arr[j])
        {
            arr[j] = 1;
        }
        else
        {
            arr[arr[j]]++;
            arr[j] = 0;
        }
    }

    for (int j = 0; j < arr.Length; j++)
    {
        Console.Write(arr[j] + " ");
    }
    Console.WriteLine();
}

0

Я швидко створив одне зразкове додаток для ігрових майданчиків для пошуку дублікатів за 0 (n) часової складності та постійного додаткового простору. Перевірте URL-адресу Пошук дублікатів

Рішення IMP вище працювало, коли масив містить елементи від 0 до n-1, причому будь-яке з цих чисел з’являється будь-яку кількість разів.


0
private static void printRepeating(int arr[], int size) {
        int i = 0;
        int j = 1;
        while (i < (size - 1)) {
            if (arr[i] == arr[j]) {
                System.out.println(arr[i] + " repeated at index " + j);
                j = size;
            }
            j++;
            if (j >= (size - 1)) {
                i++;
                j = i + 1;
            }
        }

    }

Вищенаведене рішення дозволить досягти однакової за часом складності O (n) та постійного простору.
користувач12704811

3
Дякуємо за цей фрагмент коду, який може надати деяку короткочасну допомогу. Правильне пояснення значно покращило б його довгострокове значення, показавши, чому це хороше рішення проблеми, та зробило б кориснішим майбутнім читачам інші подібні питання. Будь ласка, відредагуйте свою відповідь, щоб додати пояснення, включаючи зроблені вами припущення.
Toby Speight

3
До речі, часова складність здається тут O (n²) - приховування внутрішньої петлі цього не змінює.
Toby Speight

-2

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

1 Створіть растровий малюнок / масив того ж розміру, що і вхідний масив

 int check_list[SIZE_OF_INPUT];
 for(n elements in checklist)
     check_list[i]=0;    //initialize to zero

2 скануйте вхідний масив і збільшуйте його кількість у вищевказаному масиві

for(i=0;i<n;i++) // every element in input array
{
  check_list[a[i]]++; //increment its count  
}  

3 Тепер скануйте масив контрольних списків та надрукуйте дублікат один раз або стільки разів, скільки вони були дублюються

for(i=0;i<n;i++)
{

    if(check_list[i]>1) // appeared as duplicate
    {
        printf(" ",i);  
    }
}

Звичайно, це займає вдвічі більше місця, споживаного рішенням, наведеним вище, але ефективність часу становить O (2n), що в основному є O (n).


Це не O(1)простір.
Даніель Каміль Козар

ой ...! не помітив, що ... мій поганий.
Глибока думка

@nikhil як це O (1) ?. Мій контрольний список масиву лінійно зростає зі збільшенням розміру вводу, тож як це O (1), якщо так, то яка евристика ви використовуєте, щоб назвати його O (1).
Глибока думка

Для заданого входу вам потрібен постійний простір, чи не O (1)? Я цілком можу помилитися :)
nikhil

Моє рішення потребує більше місця в міру зростання вхідних даних. Ефективність (простір / час) алгоритму не вимірюється для конкретного вводу (у такому випадку ефективність часу кожного алгоритму пошуку буде постійною, тобто елемент, знайдений у 1-му індексі, де ми шукали). причина, чому у нас найкращий випадок, найгірший і середній випадок.
Глибока думка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.