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


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.