Який алгоритм вимагає набору?


10

На моїх перших курсах програмування мені сказали, що я повинен використовувати набір, коли мені потрібно робити такі речі, як видалити дублікати чогось. Наприклад: щоб видалити всі дублікати з вектора, повторіть зазначений вектор і додайте кожен елемент до набору, тоді вам залишаються унікальні випадки. Однак я також міг би це зробити, додавши кожен елемент до іншого вектора та перевіривши, чи елемент вже існує. Я припускаю, що залежно від мови, що використовується, може бути різниця у продуктивності. Але чи є причина використовувати інший набір, крім цього?

В основному: для яких типів алгоритмів потрібен набір, і це не слід робити з будь-яким іншим типом контейнера?


2
Чи можете ви бути більш конкретними щодо того, що ви маєте на увазі, коли ви використовуєте термін "встановити"? Ви посилаєтесь на набір C ++?
Роберт Харві

Так, насправді визначення "set" є схожим на більшість мов: контейнер, який приймає лише унікальні елементи.
Флоелла

6
"додавання кожного елемента до іншого вектора та перевірка наявності цього елемента" - це просто реалізація набору самостійно. Отже, ви запитуєте, навіщо використовувати вбудовану функцію, коли ви можете написати її вручну?
ЖакБ

Відповіді:


8

Ви конкретно запитуєте про набори, але я думаю, що ваше питання стосується більш широкої концепції: абстракції. Ви абсолютно вірні, що можете використовувати вектор для цього (якщо ви використовуєте Java, використовуйте замість ArrayList.) Але навіщо зупинятися на цьому? Для чого потрібен вектор? Все це можна зробити за допомогою масивів.

Коли вам потрібно буде додати елемент до масиву, ви можете просто перевести цикл на кожен елемент, і якщо його немає, ви додасте його в кінці. Але насправді потрібно спочатку перевірити, чи є в масиві місце. Якщо немає, вам не потрібно буде створити новий масив, який є більшим, і скопіювати всі існуючі елементи зі старого масиву в новий масив, і тоді ви можете додати новий елемент. Звичайно, вам також потрібно оновити кожне посилання на старий масив, щоб вказати на новий. Зробив усе, що зробив? Чудово! Тепер що ми намагалися здійснити знову?

Або замість цього ви можете використовувати встановлений екземпляр та просто зателефонувати add(). Причина існування наборів полягає в тому, що вони є абстракцією, корисною для безлічі поширених проблем. Наприклад, скажімо, що ви хочете відслідковувати елементи та реагувати, коли додається новий. Ви викликаєте add()набір, і він повертається trueабо falseбазується на тому, чи був набір змінений. Ви можете це написати вручну за допомогою примітивів, але чому?

Насправді може бути випадок, коли у вас є Список і ви хочете видалити дублікати. Запропонований вами алгоритм є в основному найповільнішим способом, який ви могли це зробити. Існує кілька поширених швидших способів: з’єднати їх або сортувати. Або ви можете додати їх до набору, який реалізує один із цих алгоритмів.

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


23

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

О так, (але це не продуктивність.)

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

Напишіть власний код, щоб це зробити, і вам доведеться про це хвилюватися. Bleh. Хто хоче це зробити?

В основному: для яких типів алгоритмів потрібен набір, і це не слід робити з будь-яким іншим типом контейнера?

Не існує алгоритму, який "не слід робити з будь-яким іншим типом контейнерів". Існують просто алгоритми, які можуть скористатися наборами. Приємно, коли не потрібно писати зайвий код.

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

введіть тут опис зображення

І як справедливо зазначає @germi, якщо ви використовуєте правильну колекцію для завдання, ваш код стає легше читати іншим.


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

14

Однак я також міг би це зробити, додавши кожен елемент до іншого вектора та перевіривши, чи елемент вже існує.

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

Чому ви хочете зробити це над використанням існуючої, перевіреної, ефективної реалізації набору?


6

Програмні об'єкти, які представляють суб'єкти реального світу, часто є логічними наборами. Наприклад, розглянемо Автомобіль. Автомобілі мають унікальні ідентифікатори і група автомобілів утворює набір. Поняття набору служить обмеженням для колекції Автомобілів, про яку програма може знати і обмежує значення даних, є дуже цінним.

Також набори мають дуже чітко виражену алгебру. Якщо у вас є набір Автомобілів, що належать Джорджу, і набір, який належить Алісі, то об'єднання - це безперечно набір, що належить і Джорджу, і Алісі, навіть якщо обидва Джорджа та Аліси володіють одними і тими ж автомобілями. Таким чином, алгоритми, які повинні використовувати набори, - це ті, де логіка об'єктів, що беруть участь, демонструє набір характеристик. Це виявляється досить часто.

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


"Перевірка, чи вона вже існує" часто важлива для дедуплікації. Часто об’єкти створюються з даних. І ви хочете, щоб тільки один об'єкт для однакових даних був повторно використаний тим, хто створює об'єкт з одних і тих же даних. Отже, ви створюєте новий об'єкт, перевірте, чи є він у наборі, якщо він є там, ви берете об'єкт із набору, інакше ви вставляєте його. Якщо ви тільки що вставили об'єкт, у вас все одно буде багато однакових об'єктів.
gnasher729

1
@ gnasher729 відповідальність виконавця Set включає перевірку на наявність, але користувач Set може for 1..100: set.insert(10)і все ще знає, що в комплекті лише одна 10
Caleth

Користувач може створити сто різних об’єктів у десяти групах рівних об'єктів. Після вставки в набір є десять об'єктів, але 100 об'єктів все ще плавають навколо. Дедупликація означає, що в наборі є десять об'єктів, і кожен використовує ці десять об'єктів. Очевидно, вам не потрібен просто тест - вам потрібна функція, яка дала об'єкт, повертає відповідний об'єкт у наборі.
gnasher729

4

Крім характеристик продуктивності (які дуже важливі, і їх не можна легко відкинути), набори дуже важливі як абстрактна колекція.

Чи можете ви наслідувати задану поведінку (ігноруючи продуктивність) за допомогою масиву? Так, абсолютно! Кожен раз, коли ви вставляєте, ви можете перевірити, чи елемент вже є у масиві, а потім додавати елемент лише у тому випадку, якщо його ще не було знайдено. Але це те, що ви повинні свідомо усвідомлювати, і пам’ятати щоразу, коли ви вставляєте у свій Array-Psuedo-Set. О, що це, ви вставили один раз безпосередньо, не попередньо перевіряючи наявність дублікатів? Welp, ваш масив порушив свою інваріантність (що всі елементи унікальні, і рівнозначно, що не існує дублікатів).

То що б ти зробив, щоб обійти це? Ви створили б новий тип даних, назвіть його (скажімо, PsuedoSet), який обертає внутрішній масив, і відкриває insertоперацію публічно, що забезпечить унікальність елементів. Оскільки до загорнутого масиву доступний лише цей публічний insertAPI, ви гарантуєте, що дублікати ніколи не можуть з’явитися. Тепер додайте трохи хешування для поліпшення ефективності containsперевірок, і рано чи пізно ви зрозумієте, що ви реалізували повний результат Set.

Я також відповів би на заяву та подальше запитання:

На моїх перших курсах програмування мені сказали, що я повинен використовувати масив, коли мені потрібно робити такі речі, як зберігати кілька упорядкованих елементів чогось. Напр .: для зберігання колекції імен колег. Однак я також міг би це зробити, виділивши необроблену пам'ять і встановивши значення адреси пам'яті, заданої покажчиком старту + деяким зміщенням.

Чи можете ви використати необроблений покажчик та виправлені зсуви, щоб імітувати масив? Так, абсолютно! Кожен раз, коли ви вставляєте, ви можете перевірити, чи зміщення не відхилиться від кінця виділеної пам'яті, з якою ви працюєте. Але це те, що ви повинні свідомо знати, і пам’ятати щоразу, коли ви вставляєте у свій Псевдо-масив. О, що це, ви вставили один раз безпосередньо, не попередньо перевіряючи зміщення? Welp, тут є помилка сегментації з вашим ім'ям!

То що б ти зробив, щоб обійти це? Ви створили б новий тип даних, назвіть його (скажімо, PsuedoArray), який обертає вказівник та розмір, і відкриває insertоперацію публічно, що примусить примусити зміщення не перевищувати розмір. Оскільки до загорнутих даних доступний лише цей публічний insertAPI, ви гарантуєте, що переповнення буфера не можуть відбуватися. Тепер додайте деякі інші функції зручності (зміна розміру масиву, видалення елементів тощо), і рано чи пізно ви зрозумієте, що ви реалізували повноцінне використання Array.


3

Існують всілякі алгоритми на основі наборів, особливо там, де потрібно виконати перетини та об'єднання множин, і в результаті вийде набір.

Задані алгоритми широко використовуються в різних алгоритмах пошуку шляхів тощо.

Для букваря з теорії множин перегляньте це посилання: http://people.umass.edu/partee/NZ_2006/Set%20Theory%20Basics.pdf

Якщо вам потрібна семантика набору, використовуйте набір. Це дозволить уникнути помилок через хибні дублікати, оскільки ви забули обрізати вектор / список на певному етапі, і це буде швидше, ніж ви можете, постійно обрізаючи свій вектор / список.


1

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

Для обчислення встановлених перетинів я перебираю через перший масив і розмічую елементи одним бітом. Потім я перебираю через другий масив і шукаю позначені елементи. Voila, встановіть перетин в лінійному часі з набагато меншою роботою та пам'яттю, ніж хеш-таблиця, наприклад, Unions та відмінності однаково просто застосувати за допомогою цього методу. Це допомагає моєму кодовій базі обертатися навколо елементів індексації, а не дублювати їх (я дублюю індекси до елементів, а не даних самих елементів) і рідко потрібно щось сортувати, але я не використовував набір структур даних протягом років результат.

У мене також є якийсь злий кодовий код C, який я використовую, навіть коли елементи не пропонують поля даних для таких цілей. Він передбачає використання пам'яті самих елементів, встановивши найзначніший біт (який я ніколи не використовую) для позначення пройдених елементів. Це досить грубо, не робіть цього, якщо ви дійсно не працюєте на рівні майже збірки, а просто хотіли б зазначити, як це може бути застосовано навіть у випадках, коли елементи не забезпечують певного поля, специфічного для обходу, якщо ви можете гарантувати, що певні біти ніколи не будуть використані. Він може обчислити набір перетину між 200 мільйонами елементів (що стосується 2,4 гіга даних) менше ніж за секунду на моєму динкому i7. Спробуйте зробити перетин між двома std::setекземплярами, що містять по сто мільйонів елементів за один і той же час; навіть не наближається.

Це вбік ...

Однак я також міг би це зробити, додавши кожен елемент до іншого вектора та перевіривши, чи елемент вже існує.

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

В основному: для яких типів алгоритмів потрібен набір, і це не слід робити з будь-яким іншим типом контейнера?

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

Відходить від дотичної

Гаразд, у мене з’явилися приємні запитання від JimmyJames щодо цього підходу до перехрестя. Це якось відхиляється від теми, але добре, мені цікаво бачити, як більше людей використовують цей основний нав'язливий підхід для встановлення перехрестя, щоб вони не будували цілих допоміжних структур, як врівноважених бінарних дерев та хеш-таблиць лише з метою заданих операцій. Як зазначалося, основна вимога полягає в тому, щоб списки були неглибокими копіюючими елементами, щоб вони індексували або вказували на спільний елемент, який може бути "позначений" як пройдений переходом через перший несортований список або масив, або все, що потім вибирати на другий пройти через другий список.

Однак це може бути здійснено практично навіть у багатопотоковому контексті, не торкаючись елементів, за умови, що:

  1. Два агрегати містять індекси до елементів.
  2. Діапазон індексів не надто великий (скажімо [0, 2 ^ 26), не мільярди і більше) і досить густо зайнятий.

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

введіть тут опис зображення

Синхронізація потоків повинна бути тільки тоді, коли ви отримуєте паралельний бітовий масив із пулу та випускаєте його назад у пул (робиться неявно, коли виходить із сфери застосування). Дійсні дві петлі для виконання заданої операції не повинні включати жодних синхронізацій потоків. Нам навіть не потрібно використовувати паралельний бітовий пул, якщо потік може просто розподіляти та звільняти біти локально, але бітовий пул може бути зручним для узагальнення шаблону в кодових базах, які підходять до такого роду подання даних, де центральні елементи часто посилаються за індексом, щоб кожен потік не мав заважати ефективному керуванню пам’яттю. Основними прикладами для моєї області є сутнісно-компонентні системи та індексовані представлення сітки. Обидва часто потребують встановлених перетинів і, як правило, посилаються на все, що зберігається централізовано (компоненти та об'єкти в ECS та вершинах, ребрах,

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

введіть тут опис зображення

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

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

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

// 'func' receives a range of indices to
// process.
set_intersection(func):
{
    parallel_bits = bit_pool.acquire()

    // Mark the indices found in the first list.
    for each index in list1:
        parallel_bits[index] = 1

    // Look for the first element in the second list 
    // that intersects.
    first = -1
    for each index in list2:
    {
         if parallel_bits[index] == 1:
         {
              first = index
              break
         }
    }

    // Look for elements that don't intersect in the second
    // list to call func for each range of elements that do
    // intersect.
    for each index in list2 starting from first:
    {
        if parallel_bits[index] != 1:
        {
             func(first, index)
             first = index
        }
    }
    If first != list2.num-1:
        func(first, list2.num)
}

... або щось для цього. Найдорожча частина псевдокоду в першій діаграмі знаходиться intersection.append(index)у другому циклі, і це стосується навіть std::vectorзаздалегідь зарезервованого розміру меншого списку.

Що робити, якщо все глибоко копіювати?

Ну, припиніть це! Якщо вам потрібно встановити перехрестя, це означає, що ви дублюєте дані для перетину. Цілком ймовірно, що навіть найдрібніші об'єкти не менше 32-бітного індексу. Можна дуже скоротити діапазон адресації ваших елементів до 2 ^ 32 (2 ^ 32 елемента, а не 2 ^ 32 байта), якщо вам насправді не потрібно більше ~ 4,3 мільярда елементів, ініціативи, і тоді потрібне зовсім інше рішення ( і це точно не використовує встановлені контейнери в пам'яті).

Ключові відповідники

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

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

Я люблю індекси!

Я люблю показники так само, як піцу та пиво. Коли мені було в 20-ті роки, я по-справжньому перейшов на C ++ і почав розробляти всі види цілком стандартних структур даних (включаючи хитрощі, пов'язані з розмежуванням заповнення ctor з ctor діапазону під час компіляції). Заднім часом це було великою марною тратою часу.

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

Деякі приклади використовують випадки, коли ви можете просто припустити, що будь-яке унікальне значення Tмає унікальний індекс і матиме екземпляри, що знаходяться в центральному масиві:

Багатопотокові радіоскопічні сорти, які добре працюють із непідписаними цілими числами для індексів . Насправді у мене є багатопотокова радіоізоляція, яка займає приблизно одну десяту частину часу, щоб сортувати сто мільйонів елементів як власне паралельне сортування Intel, а Intel вже в 4 рази швидше, ніж std::sortдля таких великих входів. Звичайно, Intel набагато гнучкіше, оскільки це сортування на основі порівняння і може сортувати речі лексикографічно, тому воно порівнює яблука з апельсинами. Але тут мені часто потрібні лише апельсини, як, наприклад, я можу зробити прохідний сортуючий радикс лише для досягнення кешованих шаблонів доступу до пам'яті або швидко фільтрувати дублікати.

Можливість побудови пов'язаних структур, таких як пов’язані списки, дерева, графіки, окремі ланцюгові хеш-таблиці тощо без розподілу купи на вузол . Ми можемо просто виділити вузли масово, паралельно елементам, і зв’язати їх разом з індексами. Самі вузли просто стають 32-розрядним індексом до наступного вузла і зберігаються у великому масиві, як-от так:

введіть тут опис зображення

Дружній для паралельної обробки. Часто пов'язані структури не так зручні для паралельної обробки, оскільки принаймні незручно намагатися домогтися паралелізму в обході дерева або пов'язаного списку на відміну від, скажімо, просто проведення паралелі для циклу через масив. За допомогою представлення індексу / центрального масиву ми завжди можемо перейти до цього центрального масиву і обробити все в чіткі паралельні петлі. У нас завжди є той центральний масив усіх елементів, який ми можемо обробити таким чином, навіть якщо ми хочемо обробити лише деякі (в цей момент ви можете обробити елементи, індексовані списком відсортованого за радіацією списку для кеш-доступу через центральний масив).

Може пов'язувати дані з кожним елементом на льоту в постійний час . Як і у випадку з паралельним масивом бітів вище, ми можемо легко і надзвичайно дешево пов’язати паралельні дані з елементами для, скажімо, тимчасової обробки. Це має випадки використання, окрім тимчасових даних. Наприклад, сітчаста система може захотіти дозволити користувачам приєднувати до мережі стільки УФ-карт, скільки вони хочуть. У такому випадку ми не можемо просто зафіксувати, скільки ультрафіолетових карт буде у кожній вершині та обличчі, використовуючи підхід AoS. Нам потрібно вміти пов'язувати такі дані під час руху, і паралельні масиви зручні там і так набагато дешевше будь-яких складних асоціативних контейнерів, навіть хеш-таблиць.

Звичайно, паралельні масиви піддаються насуванню через їх схильний до помилок характер синхронізації паралельних масивів один з одним. Кожного разу, коли ми видаляємо елемент з індексу 7 з масиву "root", нам також потрібно робити те ж саме для "дітей". Однак у більшості мов досить просто узагальнити цю концепцію до контейнера загального призначення, щоб хитра логіка тримати паралельні масиви синхронізованими один з одним лише в одному місці на всій базі коду, і такий контейнер паралельного масиву може використовуйте розріджену реалізацію масиву вище, щоб уникнути втрати великої кількості пам’яті для суміжних вільних просторів у масиві, які потрібно повернути після наступних вставок.

Детальніше розробка: розріджене дерево біт

Гаразд, я отримав прохання розробити ще декілька, які я вважаю саркастичними, але я все одно зроблю це, бо це так весело! Якщо люди хочуть винести цю ідею на цілком нові рівні, то можна виконати задані перехрестя, навіть не лінійно перебираючи елементи N + M. Це моя кінцева структура даних, яку я використовую протягом віків і в основному моделей set<int>:

введіть тут опис зображення

Причина, по якій він може виконувати набір перетинів, навіть не перевіряючи кожен елемент в обох списках, полягає в тому, що один набір бітів у корені ієрархії може вказувати на те, що, скажімо, мільйон суміжних елементів зайнятий у наборі. Просто перевіривши один біт, ми можемо знати, що N індексів у діапазоні [first,first+N)знаходяться у безлічі, де N може бути дуже великою кількістю.

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


2
"Для обчислення встановлених перетинів я проходжу через перший масив і позначаю елементи одним бітом. Потім я перебираю через другий масив і шукаю позначені елементи." Ви позначаєте їх де, на другому масиві?
JimmyJames

1
О, я бачу, ви "інтернуєте" дані одним об'єктом, що представляє кожне значення. Це цікава техніка для набору випадків використання для наборів. Я не бачу причин, щоб не реалізувати цей підхід як операцію над власним класом set.
JimmyJames

2
"Це нав'язливе рішення, яке порушує інкапсуляцію в деяких випадках ..." Одного разу я зрозумів, що ти маєш на увазі, це трапилось зі мною, але тоді я думаю, що це не потрібно. Якщо у вас був клас, який керував такою поведінкою, об’єкти індексу могли бути незалежними від усіх даних елементів і бути спільними для всіх примірників вашого типу колекції. тобто був би один основний набір даних, і тоді кожен екземпляр би вказував на головний набір. Багатопотокове нарізання потребує більшої складності, але я думаю, якби це було керовано.
JimmyJames

1
Здається, це може бути корисним у вирішенні баз даних, але я не знаю, чи реалізовані такі способи. Дякуємо, що виклали це тут. У тебе розум працює.
JimmyJames

1
Не могли б ви детальніше розробити? ;) Я перевірю це, коли у мене є деякий (багато) час.
JimmyJames

-1

Для перевірки, чи містить множина з n елементів інший елемент X, потрібно, як правило, постійний час. Щоб перевірити, чи містить масив з n елементів інший елемент, X зазвичай займає O (n) час. Це погано, але якщо ви хочете видалити дублікати з n елементів, раптом це замість O (n ^ 2) знадобиться вчасно O (n); 100 000 предметів приведуть ваш комп'ютер на коліна.

А ви просите більше причин? - Окрім зйомок, вам сподобався вечір, місіс Лінкольн?


2
Я думаю, ви можете прочитати це ще раз. Прийняття часу O (n) замість O (n²) час взагалі вважається хорошою справою.
JimmyJames

Може, ви стояли на голові, читаючи це? ОП запитала "чому б просто не взяти масив".
gnasher729

2
Чому від О (n²) до О (п) збирається піднести «комп'ютер до колін»? Я, мабуть, пропустив це у своєму класі.
JimmyJames
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.