Як встановлюється () реалізується?


151

Я бачив, як люди кажуть, що setоб’єкти в python мають перевірку членства O (1). Як вони реалізуються всередині, щоб дозволити це? Яку структуру даних він використовує? Які ще наслідки має ця реалізація?

Кожна відповідь тут була справді освічуючою, але я можу прийняти лише одну, тому я піду з найближчою відповіддю на своє первісне запитання. Дякую всім за інформацію!

Відповіді:


139

Відповідно до цієї теми :

Дійсно, набори CPython реалізуються як щось на зразок словників із фіктивними значеннями (ключі є членами набору), з деякою оптимізацією, яка використовує цю відсутність значень

Отже, в основному a setвикористовує хештел як основу структури даних. Це пояснює перевірку членства O (1), оскільки пошук елемента в хеш-таблиці є в середньому операцією O (1).

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


18
IIRC, оригінальна setреалізація насправді була dict з фіктивними значеннями, і вона була оптимізована пізніше.
— dan04

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

4
Ні, середній випадок - це O (1), але найгірший випадок - O (N) для пошуку хеш-таблиць.
— Джастін Етьє

4
@ClaudiuCreanga це старий коментар, але просто для уточнення: нотація big-O вказує вам верхні межі щодо швидкості росту речей, але ви можете встановити верхню межу зростання середньої продуктивності та ви можете окремо перевершити зростання найгіршого випадку виконання.
— Кірк Бойер

79

Коли люди кажуть, що набори мають перевірку членства O (1), вони говорять про середній випадок. У гіршому випадку (коли всі стихійні значення стикаються) перевірка членства - це O (n). Дивіться вікі на Python про складність у часі .

У статті Вікіпедії сказано, що найкраща часова складність для хеш-таблиці, яка не змінює розмір, - це O(1 + k/n). Цей результат не застосовується безпосередньо до наборів Python, оскільки в наборах Python використовується хеш-таблиця з розміром.

Трохи далі у статті Вікіпедії йдеться про те, що для середнього випадку та припускаючи просту функцію однорідного хешуваннявання, складність у часі полягає в тому O(1/(1-k/n)), де k/nможна обмежити константу c<1.

Big-O відноситься лише до асимптотичної поведінки як n → ∞. Оскільки k / n може бути обмежена постійною, c <1, незалежно від n ,

O(1/(1-k/n))не більше, ніж O(1/(1-c))еквівалентно O(constant)= O(1).

Таким чином, якщо припустити, однакове просте хешуваннявання, в середньому перевірка членства для наборів Python є O(1).


14

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

У найпростішій моделі хеш-функція абсолютно не визначена, і таблиця не змінює розмір. Для найкращого вибору хеш-функції таблиця розміру n з відкритою адресацією не має зіткнень і містить до n елементів, з єдиним порівнянням для успішного пошуку, а таблиця розміру n з ланцюжком і клавішами k має мінімальний макс. (0, кн) зіткнень та O (1 + k / n) порівнянь для пошуку. Для найгіршого вибору хеш-функції кожна вставка спричиняє зіткнення, а хеш-таблиці вироджуються до лінійного пошуку з амортизованими порівняннями Ω (k) за вставку і до k порівнянь для успішного пошуку.

Пов’язане: Чи дійсно хешмак Java справді O (1)?


4
Але вони шукають постійний час для пошуку елементів: python -m timeit -s "s = set (range (10))" "5 in s" 10000000 циклів, найкраще 3: 0,0642 Usec на цикл <--> python - м timeit -s "s = set (діапазон (10000000))" "5 in s" 10000000 циклів, найкраще 3: 0,0634 usec на цикл ... і це найбільший набір, який не кидає MemoryErrors
— Jochen Ritzel

2
@ THC4k Все, що ви довели, полягає в тому, що пошук X робиться в постійний час, але це не означає, що час пошуку X + Y займе стільки ж часу, що і про O (1).
— Шай Ерліхмен

3
@intuited: Це так, але тестовий запуск вище не доводить, що ви можете шукати "5" в той же час, ви можете шукати "485398" або якесь інше число, яке може опинитися в жахливому просторі зіткнення. Справа не в тому, щоб шукати один і той же елемент у хеші різного розміру за один і той же час (насправді, це взагалі не потрібно), а скоріше про те, чи можете ви отримувати доступ до кожного запису в однаковий час у поточній таблиці - те, що в принципі неможливо виконати хеш-таблицям, оскільки, як правило, завжди будуть зіткнення.
— Нік Бастін

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

3
@intuited: ні, це неправильно. Коли кількість збережених значень збільшиться, Python автоматично збільшить розмір хештеля, а швидкість зіткнення залишатиметься приблизно постійною. Якщо припустити рівномірно розподілений алгоритм хеш-розподілу O (1), то пошук хешшю амортизується O (1). Ви можете переглянути відео-презентацію "Могутній словник" python.mirocommunity.org/video/1591/…
— Lie Ryan

13

У всіх нас є легкий доступ до джерела , де в коментарі, що передує, set_lookkey()сказано:

/* set object implementation
 Written and maintained by Raymond D. Hettinger <python@rcn.com>
 Derived from Lib/sets.py and Objects/dictobject.c.
 The basic lookup function used by all operations.
 This is based on Algorithm D from Knuth Vol. 3, Sec. 6.4.
 The initial probe index is computed as hash mod the table size.
 Subsequent probe indices are computed as explained in Objects/dictobject.c.
 To improve cache locality, each probe inspects a series of consecutive
 nearby entries before moving on to probes elsewhere in memory.  This leaves
 us with a hybrid of linear probing and open addressing.  The linear probing
 reduces the cost of hash collisions because consecutive memory accesses
 tend to be much cheaper than scattered probes.  After LINEAR_PROBES steps,
 we then use open addressing with the upper bits from the hash value.  This
 helps break-up long chains of collisions.
 All arithmetic on hash should ignore overflow.
 Unlike the dictionary implementation, the lookkey function can return
 NULL if the rich comparison returns an error.
*/


...
#ifndef LINEAR_PROBES
#define LINEAR_PROBES 9
#endif

/* This must be >= 1 */
#define PERTURB_SHIFT 5

static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)  
{
...

2
Відповідь на це питання було б корисно C підсвічування синтаксису . Підсвічування синтаксису коментаря Python виглядає дуже погано.
— користувач202729

Щодо коментаря "Це залишає нам гібрид лінійного зондування та відкритої адресації", чи не лінійне зондування є різновидом вирішення колізій у відкритій адресації, як описано в en.wikipedia.org/wiki/Open_addressing ? Тому лінійне зондування є підтипом відкритої адреси, і коментар не має сенсу.
— Алан Евангеліста

2

Щоб ще більше підкреслити різницю між set'sі dict's, ось уривок із setobject.cрозділів коментарів, в якому з’ясовується основна відмінність набору від диктів.

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

джерело на github

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.