Знайдіть «дірку» у списку цифр


14

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

Мій примітивний підхід сортує їх і переглядає список, чи є кращий спосіб?


6
@Jodrell Я думаю, що сортувати нескінченний прогрес буде важко ;-)
maple_shaft

3
@maple_shaft погодився, може зайняти деякий час.
Джодрелл

4
Як визначити спочатку для несортованого списку?
Джодрелл

1
Я щойно зрозумів, що це, ймовірно, належить до StackOverflow, оскільки це насправді не концептуальна проблема.
JasonTrue

2
@JasonTrue З FAQ, If you have a question about… •algorithm and data structure conceptsце на тему IMHO.
maple_shaft

Відповіді:


29

Якщо припустити, що ви маєте на увазі "ціле число", коли ви говорите "число", ви можете використовувати бітвектор розміром 2 ^ n, де n - кількість елементів (скажімо, ваш діапазон включає цілі числа від 1 до 256, тоді ви можете використовувати 256- біт або 32 байт, бітвектор). Коли ви зустрінете ціле число в положенні n вашого діапазону, встановіть n-й біт.

Коли ви закінчите перерахунок колекції цілих чисел, ви повторіть біти у своєму bitvector, шукаючи положення будь-яких бітів, встановлених 0. Тепер вони відповідають положенню n вашого відсутнього цілого числа.

Це O (2 * N), тому O (N) і, ймовірно, більш ефективна пам'ять, ніж сортування всього списку.


6
Ну, як пряме порівняння, якби у вас були всі позитивні непідписані 32-бітні цілі числа, але 1, ви могли б вирішити відсутню цілочисельну задачу приблизно в половині гігабайт пам'яті. Якщо ви сортували натомість, вам доведеться використовувати понад 8 гігабайт пам'яті. І сортування, за винятком таких випадків, як цей (ваш список сортується, коли у вас є бітвектор), майже завжди n log n або гірше, тому, за винятком випадків, коли постійне переважує складність витрат, лінійний підхід виграє.
JasonTrue

1
Що робити, якщо ви не знаєте діапазону апріорі?
Blrfl

2
Якщо у вас є цілий тип даних, Blrfl, ви, безсумнівно, знаєте максимальні розширення діапазону, навіть якщо у вас немає достатньої інформації для подальшого звуження. Якщо ви випадково знаєте, що це невеликий список, але не знаєте точного розміру, сортування може бути більш простим рішенням.
JasonTrue

1
Або виконайте інший цикл спочатку через список, щоб знайти найменший і найбільший елемент. Тоді ви можете виділити масив точного розміру з найменшим значенням як основне зміщення. Ще O (N).
Безпечний

1
@JPatrick: Не домашнє завдання, бізнес, я закінчив КС років тому :).
Фабіан Зейндл

4

Якщо ви сортуєте весь список спочатку, то ви гарантуєте найгірший термін виконання. Також вибір алгоритму сортування є критичним.

Ось як я підійшов до цієї проблеми:

  1. Використовуйте сортування купи , орієнтуючись на найменші елементи у списку.
  2. Після кожного свопу дивіться, чи є у вас розрив.
  3. Якщо ви знайдете прогалину, то return: Ви знайшли свою відповідь.
  4. Якщо ви не знайдете проміжку, продовжуйте міняти місцями.

Ось візуалізація сорту купи .


Одне питання, як ви визначаєте "найменші" елементи списку?
Джодрелл

4

Щоб бути езотеричним та "розумним", у спеціальному випадку масиву, що має лише одну "дірку", ви можете спробувати рішення на основі XOR:

  • Визначте діапазон свого масиву; це робиться шляхом встановлення змінної "max" і "min" для першого елемента масиву, і для кожного елемента після цього, якщо цей елемент менший ніж min або більше max, встановіть min або max на нове значення.
  • Якщо діапазон на один менший, ніж кардинальність набору, є лише одна «дірка», тому ви можете використовувати XOR.
  • Ініціалізуйте цілу змінну X до нуля.
  • Для кожного цілого числа від min до max включно, XOR, яке значення з X, і збереже результат у X.
  • Тепер XOR кожне ціле число в масиві з X, зберігаючи кожен наступний результат у X, як і раніше.
  • Коли ви закінчите, X буде значенням вашої «дірки».

Це буде працювати приблизно в 2N час, аналогічно рішенню bitvector, але вимагає менше місця в пам'яті для будь-якого N> sizeof (int). Однак якщо масив має декілька «дірок», X буде «сумою» XOR усіх отворів, яку буде важко або неможливо розділити на фактичні значення отвору. У такому випадку ви переходите до іншого методу, такого як "підвісний" або "бітвекторний" підходи з інших відповідей.

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


Це смішно розумно і приголомшливо! Тепер ви можете придумати спосіб зробити це зі змінною кількістю отворів? :-D

2

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

Вашим іншим варіантом, який може бути кращим для ранніх сценаріїв, а замість копіювання пам’яті буде інсити, є зміна сортувального вибору, щоб вийти зранку, якщо мінімум, знайдений у проході сканування, не на 1 більше, ніж останній знайдений хв.


1

Ні, не дуже. Оскільки будь-яке ще не відскановане число завжди може бути таким, яке заповнює задану "дірку", ви не можете уникнути сканування кожного номера хоча б один раз, а потім порівняти його з можливими сусідами. Ви, ймовірно, могли б прискорити роботу, побудувавши двійкове дерево чи так, а потім проїхати його зліва направо, доки не знайдеться дірка, але це, по суті, однакова складність часу, як сортування, оскільки це сортування. І ти, мабуть, не придумаєш нічого швидшого, ніж Тімсорт .


1
Ви хочете сказати, що перегляд списку є такою ж складністю часу, як і сортування?
maple_shaft

@maple_shaft: Ні, я кажу, що створення бінарного дерева з випадкових даних, а потім перехід його вліво-вправо еквівалентно сортуванню, а потім переходу від малого до великого.
пігулка

1

Більшість ідей тут - це не що інше, як просто сортування. Версія бітвектора - це звичайна Bucketsort. Згаданий також сорт купи. В основному це зводиться до вибору правильного алгоритму сортування, який залежить від вимог часу та простору, а також від діапазону та кількості елементів.

На мій погляд, використання структури купи - це, мабуть, найбільш загальне рішення (купа, в основному, дає вам найменші елементи ефективно без повного сортування).

Ви також можете проаналізувати підходи, які спочатку знаходять найменші числа, а потім сканувати кожне ціле число, що перевищує це. Або ви знайдете 5 найменших цифр, сподіваючись, що у них буде розрив.

Усі ці алгоритми мають свою силу залежно від вхідних характеристик та програмних вимог.


0

Рішення, яке не використовує додаткове сховище або припускає ширину (32 біта) цілих чисел.

  1. В одному лінійному проході знайдіть найменше число. Давайте назвемо це "хв". O (n) часова складність.

  2. Виберіть випадковий елемент зведення і зробіть розділ у стилі quicksort.

  3. Якщо поворот опинився в положенні = ("pivot" - "min"), то повторіть повтор у правій частині розділу, в іншому випадку повторіть на лівій частині розділу. Ідея тут полягає в тому, що якщо немає отворів з самого початку, то шарнір буде знаходитись у ("шарнір" - "хв") положенні, тому перший отвір повинен лежати праворуч від перегородки і навпаки.

  4. Базовий корпус - це масив з 1 елемента, і отвір лежить між цим елементом та наступним.

Очікувана загальна складність часу роботи - O (n) (8 * n із константами), а найгірший - O (n ^ 2). Аналіз складності часу для подібної проблеми можна знайти тут .


0

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

Ідея, що стоїть за цим методом, подібна до швидкості, у тому, що ми знаходимо шарнір і перегородку навколо нього, а потім повторюємо на стороні (ях) з отвором. Щоб побачити, на яких сторонах є отвір, ми знаходимо найменші та найвищі числа та порівнюємо їх із зведеними та числом значень на цій стороні. Скажіть, що шарнір дорівнює 17, а мінімальна кількість - 11. Якщо отворів немає, повинно бути 6 чисел (11, 12, 13, 14, 15, 16, 17). Якщо їх 5, ми знаємо, що на цій стороні є дірка, і ми можемо повторити її саме на цій стороні, щоб знайти її. У мене виникають труднощі з поясненням цього більш чітко, ніж це, тому візьмемо приклад.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

Сверток:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15 - шарнір, позначений трубами ( ||). З лівого боку повороту є 5 чисел, як і повинно бути (15 - 10), і 9 з правого, де має бути 10 (25 - 15). Отже, ми повторюємось з правого боку; зазначимо, що попередня межа була 15, якщо дірка примикає до неї (16).

[15] 18 16 17 20 |21| 22 23 24 25

Зараз ліворуч є 4 числа, але їх має бути 5 (21 - 16). Тож ми повторюємо там, і знову відзначимо попередній зв'язаний (у дужках).

[15] 16 17 |18| 20 [21]

Ліва сторона має правильні 2 числа (18 - 16), а права - 1 замість 2 (20 - 18). Залежно від наших умов закінчення, ми могли б порівняти 1 число на дві сторони (18, 20) і побачити, що 19 пропущено або повториться ще раз:

[18] |20| [21]

Ліва сторона має розмір нуля, із зазором між шарніром (20) та попередньою межею (18), тому 19 - отвір.

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


1
Я не вірю, що ОП нічого не сказала про те, що там є лише одна дірка. Вхід - це несортований список номерів - вони можуть бути будь-якими. З вашого опису незрозуміло, як би ви визначили, скільки там повинно бути число.
Калеб

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

Це не лінійно, ІМО. Це більше схоже на (logN) ^ 2. На кожному кроці ви перемикаєте підмножину колекції, про яку ви піклуєтеся (половина попереднього підмножини, який ви визначили як перший "отвір"), а потім повторно переходите до лівої сторони, якщо в ньому є "отвір", або правий бік, якщо лівий. (logN) ^ 2 все ще краще, ніж лінійний; якщо N збільшується вдесятеро, ви приймаєте лише порядку 2 (log (N) -1) + 1 більше кроків.
KeithS

@Keith - на жаль, вам доведеться переглянути всі числа на кожному рівні, щоб перетворити їх, тому для порівняння знадобиться n + n / 2 + n / 4 + ... = 2n (технічно 2 (нм)) .
Кевін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.