Поиск дубликатов за O (n) время и O (1) пространство

121

Вход: задан массив из n элементов, который содержит элементы от 0 до n-1, причем любое из этих чисел встречается любое количество раз.

Цель: найти эти повторяющиеся числа за O (n) и использовать только постоянную память.

Например, пусть n будет 7, а array будет {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).
кафе
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] . Если мы будем осторожны, мы можем использовать это «домашнее» местоположение для хранения информации о том, были ли еще напечатаны дубликаты или нет.

В версии caf, когда мы просматривали массив, A[i] != iподразумевается, что A[i]это дубликат. В своей версии я полагаюсь на немного другой инвариант: это A[i] != i && A[A[i]] == A[i]означает, что A[i]это дубликат , которого мы раньше не видели. . (Если вы отбросите часть «что мы не видели раньше», то можно увидеть, что остальное подразумевается истинностью инварианта caf и гарантией того, что все дубликаты имеют некоторую копию в домашнем местоположении.) Это свойство сохраняется в начало (после завершения 1-го цикла caf), и я покажу ниже, что оно сохраняется после каждого шага.

Когда мы проходим через массив, успех со A[i] != iстороны теста означает, что это A[i] может быть дубликат, которого раньше не было. Если мы не видели этого раньше, то мы ожидаем, что A[i]домашнее местоположение будет указывать на себя - это то, что проверено во второй половинеif условия. Если это так, мы распечатываем его и изменяем домашнее местоположение, чтобы оно указывало обратно на этот первый найденный дубликат, создавая двухэтапный «цикл».

Для того, чтобы увидеть , что эта операция не изменяет наш инвариант, предположит , что m = A[i]для определенной позиции , iудовлетворяющей A[i] != i && A[A[i]] == A[i]. Очевидно, что изменение, которое мы делаем ( A[A[i]] = i), будет работать, чтобы предотвратить mвывод других не-домашних вхождений в качестве дубликатов, вызывая ifсбой второй половины их условий, но будет ли оно работать, когда будет iдостигнуто домашнее местоположение m,? Да, будет, потому что теперь, хотя в этом новом iмы обнаруживаем, что первая половина ifусловия A[i] != iистинна, вторая половина проверяет, является ли местоположение, на которое оно указывает, домашним местоположением, и обнаруживает, что это не так. В этой ситуации мы уже не знаем , является ли mили A[m]был повторяющееся значение, но мы знаем , что так или иначе,об этом уже сообщалось , потому что эти 2 цикла гарантированно не появятся в результате 1-го цикла caf. (Обратите внимание, что если m != A[m]тогда ровно одно из mи A[m]встречается более одного раза, а другое не встречается вообще.)

j_random_hacker
источник
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 ++

Prasoon Saurav
источник
3
Очень умно - кодировать ответ в знаковый бит индексированной записи!
holtavolt
3
@sashang: Не может быть. Ознакомьтесь со спецификацией проблемы. «Дан массив из n элементов, содержащий элементы от 0 до n-1 »
Прасун Саурав
5
Это не обнаружит повторяющиеся нули и определит одно и то же число как повторяющееся несколько раз.
Null Set
1
@Null Set: вы можете просто заменить -на ~для нулевой проблемы.
user541686
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

Hoha
источник
+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;
    }
  }
}
CAFxX
источник
+1, это работает хорошо, но потребовалось немного подумать, чтобы понять, почему именно if (value > i) a[i--] = a[value];работает: если value <= iтогда мы уже обработали значение at 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), поэтому я подумал об этом
Аншул Гарг
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

Крошечный код на Python для демонстрации описанного выше метода caf:

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] )
vine'th
источник
Обратите внимание, что замена одного iзначения может произойти более одного раза - обратите внимание на whileмой ответ.
caf
0

Алгоритм легко увидеть в следующей функции C. Получение исходного массива, хотя и не требуется, будет возможно, взяв каждую запись по модулю 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 для тестирования.

Apshir
источник
Боюсь, что это технически «обман», поскольку для работы с числами до 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();
}
Eli
источник
0

Я быстро создал один образец приложения для игровой площадки для поиска дубликатов с временной сложностью 0 (n) и постоянным дополнительным пространством. Пожалуйста, проверьте URL-адрес Поиск дубликатов

IMP Вышеупомянутое решение работало, когда массив содержит элементы от 0 до n-1, причем любое из этих чисел встречается любое количество раз.

CrazyPro007
источник
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;
            }
        }

    }
user12704811
источник
Вышеупомянутое решение даст то же самое по временной сложности O (n) и постоянному пространству.
user12704811
3
Спасибо за этот фрагмент кода, который может предоставить некоторую краткосрочную помощь. Правильное объяснение значительно повысило бы его ценность в долгосрочной перспективе, показав, почему это хорошее решение проблемы, и сделало бы его более полезным для будущих читателей, задавая другие похожие вопросы. Пожалуйста , измените свой ответ , чтобы добавить некоторые объяснения, в том числе допущений , которые вы сделали.
Тоби Спейт,
3
Кстати, временная сложность здесь составляет O (n²) - скрытие внутреннего цикла этого не меняет.
Тоби Спейт,
-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 Теперь просканируйте массив check_list и распечатайте дубликат либо один раз, либо столько раз, сколько они были дублированы.

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

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

Конечно, это занимает вдвое больше места, чем занимает решение, указанное выше, но эффективность по времени составляет O (2n), что в основном составляет O (n).

Глубокая мысль
источник
Это не O(1)космос.
Daniel Kamil Kozar
упс ...! не заметил, что ... моя плохая.
Deepoughtt 07
@nikhil как там O (1) ?. Мой массив check_list растет линейно по мере увеличения размера ввода, так как же это O (1), если да, то какие эвристики вы используете, чтобы назвать его O (1).
Deepoughtt 08
Для заданного ввода вам нужно постоянное пространство, не так ли O (1)? Я вполне могу ошибаться :)
nikhil
Моему решению требуется больше места по мере роста ввода. Эффективность (пространство / время) алгоритма не измеряется для конкретного входа (в этом случае эффективность времени каждого алгоритма поиска будет постоянной, т.е. элемент, найденный в 1-м индексе, по которому мы искали). Она измеряется для любого входа, то есть причина, по которой у нас есть лучший случай, худший случай и средний случай.
Deepoughtt 08