Поиск дубликата в массиве
Условие
Есть массив размера N, заполненный натуральными числами от 1 до N. Один из элементов повторяется.
Придумайте алгоритм, который без использования дополнительных переменных (O(1) по памяти) найдет любое повторяющееся число в массиве.
Пример:
- Вход: [3, 1, 3, 4, 2]
- Выход: 3
Подсказка: Можно использовать тот факт, что при переборе массива можно помечать изменением знака на "−" значения тех элементов, индекс которых равен встретившемуся при переборе числу.
Эта задача встречалась на собеседованиях в крупные IT-компании Кремниевой Долины.
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Подход
Это одна из классических задач на собеседованиях Google, Amazon и других FAANG компаний. Ключевая идея — использовать сам массив как хеш-таблицу, помечая посещённые индексы через изменение знака значений.
Основное решение (O(1) память)
func findDuplicate(_ nums: inout [Int]) -> Int? {
for num in nums {
let index = abs(num) - 1 // Число от 1 до N, индекс от 0 до N-1
// Если значение уже отрицательное, это индекс повторяющегося числа
if nums[index] < 0 {
return abs(num)
}
// Помечаем посещение, меняя знак
nums[index] = -nums[index]
}
return nil
}
Немутирующее решение (если нельзя менять массив)
Если изменение исходного массива недопустимо, используем Floyd's Cycle Detection (алгоритм "черепаха и заяц"):
func findDuplicateWithoutMutation(_ nums: [Int]) -> Int? {
// Фаза 1: Обнаружение цикла
var slow = nums[0]
var fast = nums[0]
// Двигаем slow на 1 шаг, fast на 2 шага
repeat {
slow = nums[slow]
fast = nums[nums[fast]]
} while slow != fast
// Фаза 2: Поиск начала цикла
slow = nums[0]
while slow != fast {
slow = nums[slow]
fast = nums[fast]
}
return slow
}
Альтернативное решение (O(n) память, если допустимо)
Это решение проще, но использует дополнительную память:
func findDuplicateWithSet(_ nums: [Int]) -> Int? {
var seen = Set<Int>()
for num in nums {
if seen.contains(num) {
return num
}
seen.insert(num)
}
return nil
}
Тестирование
// Тест 1: Дубликат в середине
var test1 = [3, 1, 3, 4, 2]
if let duplicate = findDuplicate(&test1) {
print("Дубликат: \(duplicate)") // Output: 3
}
print("Массив после: \(test1)")
// Тест 2: Дубликат в начале
var test2 = [2, 5, 9, 4, 2, 6, 7, 3, 8, 1]
if let duplicate = findDuplicate(&test2) {
print("Дубликат: \(duplicate)") // Output: 2
}
// Тест 3: Несколько дубликатов (найдёт первый)
var test3 = [1, 1]
if let duplicate = findDuplicate(&test3) {
print("Дубликат: \(duplicate)") // Output: 1
}
// Без мутации
let test4 = [1, 3, 4, 2, 2]
if let duplicate = findDuplicateWithoutMutation(test4) {
print("Дубликат (без мутации): \(duplicate)") // Output: 2
}
Как работает решение с изменением знака
Пример: [3, 1, 3, 4, 2]
- Итерация 1: num = 3 → index = 2 → nums[2] = -3 → массив: [3, 1, -3, 4, 2]
- Итерация 2: num = 1 → index = 0 → nums[0] = -3 → массив: [-3, 1, -3, 4, 2]
- Итерация 3: num = -3 → index = 2 → nums[2] уже -3 → дубликат найден! Возвращаем 3
Как работает Floyd's Cycle Detection
Массив можно представить как linked list, где nums[i] указывает на следующий индекс:
- Дубликат создаёт цикл в этой виртуальной связной ссылке
- Алгоритм находит точку входа в цикл, что соответствует дубликату
Сложность:
- Время: O(n) — проходим массив один раз
- Память: O(1) — не используем дополнительные структуры
Сравнение подходов
| Подход | Время | Память | Мутирует | Преимущества |
|---|---|---|---|---|
| Изменение знака | O(n) | O(1) | Да | Простой, истинная O(1) |
| Floyd's Cycle | O(n) | O(1) | Нет | Не портит данные |
| Set | O(n) | O(n) | Нет | Легче понять |
Рекомендация для интервью
- Сначала объясните Set решение — покажите понимание проблемы
- Затем предложите оптимизацию — алгоритм с изменением знака или Floyd's Cycle
- Выберите Floyd's Cycle, если нельзя менять исходные данные — это показывает глубокое понимание
Эта задача различает junior и senior разработчиков. Junior объясняет Set подход, senior сразу видит O(1) решение.