Найти дубликат в массиве целых чисел

Это был вопрос интервью.

Мне был задан массив из n+1 целых чисел из диапазона [1,n]. Свойство массива состоит в том, что он имеет k (k>=1) дубликаты, и каждый дубликат может отображаться более чем в два раза. Задача состояла в том, чтобы найти элемент массива, который происходит более одного раза в максимально возможной временной и пространственной сложности.

После значительной борьбы я с гордостью придумал решение O(nlogn), которое занимает пространство O(1). Моя идея состояла в том, чтобы разделить диапазон [1,n-1] на две половины и определить, какая из двух половин содержит больше элементов из входного массива (я использовал принцип Pigeonhole). Алгоритм продолжается рекурсивно, пока не достигнет интервала [X,X], где X происходит дважды, а это дубликат.

Интервьюер был удовлетворен, но потом он сказал мне, что существует решение O(n) с постоянным пространством. Он щедро предложил несколько советов (что-то связанное с перестановками?), Но я понятия не имел, как придумать такое решение. Предполагая, что он не лжет, кто-нибудь может предложить рекомендации? Я искал SO и нашел несколько (более простых) вариантов этой проблемы, но не этот конкретный. Спасибо.

EDIT: чтобы сделать вещи еще более сложными, интервьюер отметил, что входной массив не должен быть изменен.

Ответ 1

  • Возьмите последний элемент (x).

  • Сохраните элемент в позиции x (y).

  • Если x == y, вы нашли дубликат.

  • Запишите положение x с помощью x.

  • Назначьте x = y и продолжайте с шага 2.

Вы в основном сортируете массив, это возможно, потому что вы знаете, где элемент должен быть вставлен. O (1) дополнительное пространство и сложность времени O (n). Вам просто нужно быть осторожным с индексами, для простоты я предположил, что первый индекс здесь 1 (не 0), поэтому нам не нужно делать +1 или -1.

Изменить: без изменения входного массива

Этот алгоритм основан на идее, что мы должны найти точку входа цикла перестановки, затем мы также нашли дубликат (опять-таки 1-основанный массив для простоты):

Пример:

2 3 4 1 5 4 6 7 8

Запись: 8 7 6

Цикл перестановки: 4 1 2 3

Как мы видим, дубликат (4) является первым числом цикла.

  • Поиск цикла перестановок

    • x = последний элемент
    • x = элемент в позиции x
    • повторите шаг 2. n раз (всего), это гарантирует, что мы ввели цикл
  • Измерение длины цикла

    • a = последний x сверху, b = последний x сверху, счетчик c = 0
    • a = элемент в позиции a, b = elment в позиции b, b = элемент в позиции b, С++ (поэтому мы делаем 2 шага вперед с b и 1 шаг вперед в цикле с a)
    • если a == b длина цикла c, в противном случае продолжить с шага 2.
  • Поиск точки входа в цикл

    • x = последний элемент
    • x = элемент в позиции x
    • повторите шаг 2. c раз (всего)
    • y = последний элемент
    • если x == y, то x является решением (x сделал один полный цикл, а y вот-вот вступит в цикл)
    • x = элемент в позиции x, y = элемент в позиции y
    • повторите шаги 5 и 6. до тех пор, пока не будет найдено решение.

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

Пример сверху:

  • x принимает следующие значения: 8 7 6 4 1 2 3 4 1 2

  • a принимает следующие значения: 2 3 4 1 2
    b принимает следующие значения: 2 4 2 4 2
    поэтому c = 4 (да, есть 5 чисел, но c только увеличивается при выполнении шагов, а не изначально)

  • x принимает следующие значения: 8 7 6 4 | 1 2 3 4
    y принимает следующие значения: | 8 7 6 4
    x == y == 4 в конце, и это решение!

Пример 2, указанный в комментариях: 3 1 4 6 1 2 5

  • Цикл ввода: 5 1 3 4 6 2 1 3

  • Длина измерительного цикла:
    a: 3 4 6 2 1 3
    b: 3 6 1 4 2 3
    c = 5

  • Поиск точки входа:
    x: 5 1 3 4 6 | 2 1
    y: | 5 1
    x == y == 1 - это решение

Ответ 2

Вот возможная реализация:

function checkDuplicate(arr) {
  console.log(arr.join(", "));
  let  len = arr.length
      ,pos = 0
      ,done = 0
      ,cur = arr[0]
      ;
  while (done < len) {
    if (pos === cur) {
      cur = arr[++pos];
    } else {
      pos = cur;
      if (arr[pos] === cur) {
        console.log(`> duplicate is ${cur}`);
        return cur;
      }
      cur = arr[pos];
    }
    done++;
  }
  console.log("> no duplicate");
  return -1;
}

for (t of [
     [0, 1, 2, 3]
    ,[0, 1, 2, 1]
    ,[1, 0, 2, 3]
    ,[1, 1, 0, 2, 4]
  ]) checkDuplicate(t);

Ответ 3

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

Примечание. Предположим, что вектор индексируется начиная с 1. Так как он, вероятно, индексируется начиная с 0 (на большинстве языков), вы можете реализовать "элемент флага в индексе i" с помощью "Отменить элемент в индекс i-1".

  • Установите я в 0 и выполните следующий цикл:
    • Приращение я до тех пор, пока элемент я не будет заблокирован.
    • Установите j в я и выполните следующий цикл:
      • Установите j в вектор [j].
      • если элемент в j помечен, j является дубликатом. Завершите оба цикла.
      • Отметьте элемент в j.
      • Если j!= i, продолжайте внутренний цикл.
  • Переместите вектор, устанавливая каждый элемент в его абсолютное значение (т.е. отмените все, чтобы восстановить вектор).

Ответ 4

  • Это зависит от того, какие инструменты вы можете использовать (приложение). В настоящее время существует множество инфраструктур/библиотек. Для exmaple в случае стандартного С++ вы можете использовать std:: map < > , как упоминалось в maraca.

  • Или, если у вас есть время, вы можете сделать свою собственную реализацию двоичного дерева, но вам нужно иметь в виду, что вставка элементов отличается в сочетании с обычным массивом. В этом случае вы можете оптимизировать поиск дубликатов, как это возможно в вашем конкретном случае.

двоичное дерево expl. ссылка: https://www.wikiwand.com/en/Binary_tree