Перевірте, чи спільні спільно використовувані елементи в python


131

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

In [78]: a = [1, 2, 3, 4, 5]

In [79]: b = [8, 7, 6]

In [80]: c = [8, 7, 6, 5]

In [81]: def lists_overlap(a, b):
   ....:     for i in a:
   ....:         if i in b:
   ....:             return True
   ....:     return False
   ....: 

In [82]: lists_overlap(a, b)
Out[82]: False

In [83]: lists_overlap(a, c)
Out[83]: True

In [84]: def lists_overlap2(a, b):
   ....:     return len(set(a).intersection(set(b))) > 0
   ....: 

Єдині оптимізації, про які я можу придумати, - це падіння, len(...) > 0оскільки це bool(set([]))дає помилковий результат. І звичайно, якщо ви зберегли свої списки як набори для початку, ви зберегли б створення набору накладні.
msw


1
Зауважте, що ви не можете відрізнити Trueвід 1та Falseвід них 0. not set([1]).isdisjoint([True])отримує те Trueсаме з іншими рішеннями.
Дімалі

Відповіді:


313

Коротка відповідь : використовуйте not set(a).isdisjoint(b), як правило, це найшвидше.

Існує чотири загальні способи перевірити, чи є два списки aта bділитися якими-небудь предметами. Перший варіант - перетворити обидва набори і перевірити їх перетин, як такий:

bool(set(a) & set(b))

Оскільки набори зберігаються за допомогою хеш-таблиці в Python, їх пошукO(1) (див. Тут для отримання додаткової інформації про складність операторів в Python). Теоретично це O(n+m)в середньому для nта mоб'єктів у списках aта b. Але 1) спочатку він повинен створити набори зі списків, які можуть зайняти незначну кількість часу, і 2) він припускає, що хеш-колізії є рідкими серед ваших даних.

Другий спосіб зробити це - використання вираження генератора, що виконує ітерацію у списках, таких як:

any(i in a for i in b)

Це дозволяє здійснювати пошук на місці, тому для посередницьких змінних не виділяється нова пам'ять. Це також підпадає під першу знахідку. Але inоператор завжди O(n)в списках (див. Тут ).

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

a = set(a); any(i in a for i in b)

Четвертий підхід полягає у використанні isdisjoint()методу (заморожених) наборів (див. Тут ), наприклад:

not set(a).isdisjoint(b)

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

from timeit import timeit
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=list(range(1000))", number=100000)
26.077727576019242
>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=list(range(1000))", number=100000)
0.16220548999262974

Ось графік часу виконання цього прикладу у функції розміру списку:

Час виконання тестового обміну елементами при спільному використанні на початку

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

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

>>> timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
13.739536046981812
>>> timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,0,-1)]", number=1000))
0.08102107048034668

Час виконання тестового обміну елементами при спільному використанні в кінці

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

Ось два аналізи з використанням випадкових чисел (замість того, щоб сфальсифікувати налаштування на користь тієї чи іншої техніки):

Час виконання тесту обміну елементами для випадково згенерованих даних з високою ймовірністю обміну Час виконання тесту обміну елементами для випадково згенерованих даних з високою ймовірністю обміну

Висока ймовірність спільного використання: елементи беруться випадковим чином [1, 2*len(a)]. Низький шанс поділитися: елементи беруться випадковим чином [1, 1000*len(a)].

До цього часу цей аналіз передбачає, що обидва списки однакового розміру. Якщо два списки різного розміру, наприклад a, значно менші, isdisjoint()завжди швидше:

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

Переконайтеся, що aсписок менший, інакше продуктивність знижується. У цьому експерименті aрозмір списку було встановлено постійним 5.

Підсумовуючи:

  • Якщо списки дуже малі (<10 елементів), not set(a).isdisjoint(b)це завжди найшвидше.
  • Якщо елементи в списках відсортовані або мають регулярну структуру, якою ви можете скористатися, вираз генератора any(i in a for i in b)є найшвидшим на великих розмірах списку;
  • Перевірте встановлене перехрестя not set(a).isdisjoint(b), яке завжди швидше, ніж bool(set(a) & set(b)).
  • Гібрид "повторення через список, тест на набір", a = set(a); any(i in a for i in b)як правило, повільніше, ніж інші методи.
  • Експресія генератора та гібрид набагато повільніше, ніж два інші підходи, коли мова йде про списки без спільного використання елементів.

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


8
Ось деякі корисні дані, що показують, що аналіз великого O не є все можливим і закінчується всім міркуванням про час роботи.
Стів Еллісон

як щодо найгіршого сценарію? anyвиходить при першому неправдивому значенні. Використовуючи список, де єдине співпадаюче значення знаходиться в кінці, ми отримуємо це: timeit('any(i in a for i in b)', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 13.739536046981812 timeit('bool(set(a) & set(b))', setup="a=list(range(1000));b=[x+998 for x in range(999,-0,-1)]", number=1000) 0.08102107048034668 ... і це лише 1000 ітерацій.
RobM

2
Дякуємо @RobM за інформацію. Я оновив свою відповідь, щоб відобразити це і врахувати інші методи, запропоновані в цій темі.
Сораво

Слід not set(a).isdisjoint(b)перевірити, чи є два списки членами. set(a).isdisjoint(b)повертається, Trueякщо два списки не мають спільного члена. Відповідь слід відредагувати?
Гільйохон

1
Дякую за підняті голови, @Guillochon, це виправлено.
Soravux

25
def lists_overlap3(a, b):
    return bool(set(a) & set(b))

Примітка: вищевикладене передбачає, що ви хочете отримати відповідь як булева. Якщо все, що вам потрібно, це вираз, який потрібно використовувати у ifвиписці, просто використовуйтеif set(a) & set(b):


5
Це в гіршому випадку O (n + m). Однак, внизу сторона полягає в тому, що вона створює новий набір, і не виграє, коли загальний елемент знайдений рано.
Метью Флашен

1
Мені цікаво, чому це так O(n + m). Я думаю, що набори реалізуються за допомогою хеш-таблиць, і, таким чином, inоператор може працювати в O(1)часі (крім випадків, що вироджуються). Це правильно? Якщо так, з огляду на те, що хеш-таблиці мають найгірший показник пошуку O(n), чи означає це, що на відміну від гіршого випадку він матиме O(n * m)продуктивність?
fmark

1
@fmark: Теоретично ви праві. Практично нікого не цікавить; прочитайте коментарі в «Objects / dictobject.c» у джерелі CPython (набори - це просто дикти з лише клавішами, без значень) і перевірте, чи зможете ви створити список ключів, що спричинить ефективність пошуку O (n).
Джон Махін

Добре, дякую за уточнення, мені було цікаво, чи не відбувається якась магія :). Хоча я згоден , що практично не потрібно піклуватися, це тривіально , щоб створити список ключів , які будуть викликати O(n)пошук роботи;), см pastebin.com/Kn3kAW7u тільки для LAFS.
fmark

2
Так, я знаю. Плюс я щойно прочитав джерело, на яке ви вказали, яке документує ще більше магії у випадку випадкових хеш-функцій (таких як вбудована). Я припустив, що це вимагає випадковості, як у Java, що призводить до жахливості, як ця stackoverflow.com/questions/2634690/… . Мені потрібно постійно нагадувати собі, що Python - це не Java (дякую божеству!).
fmark

10
def lists_overlap(a, b):
  sb = set(b)
  return any(el in sb for el in a)

Це асимптотично оптимально (найгірший випадок O (n + m)) і може бути кращим, ніж підхід перехрестя через anyкоротке замикання.

Наприклад:

lists_overlap([3,4,5], [1,2,3])

поверне True, як тільки він потрапить 3 in sb

EDIT: Ще одна варіація (завдяки Дэйву Кірбі):

def lists_overlap(a, b):
  sb = set(b)
  return any(itertools.imap(sb.__contains__, a))

Це спирається на imapітератор 's, який реалізується на C, а не на розуміння генератора. Він також використовується sb.__contains__як функція відображення. Я не знаю, наскільки це залежить від продуктивності. Все одно буде коротке замикання.


1
Петлі в підході перехрестя - всі в коді С; у вашому підході є одна петля, яка включає код Python. Велика невідомість - це чи порожній перехрест вірогідний чи малоймовірний.
Джон Макін

2
Ви також можете використовувати any(itertools.imap(sb.__contains__, a))який повинен бути швидше, оскільки це дозволяє уникати використання лямбда-функції.
Дейв Кірбі

Дякую, @Dave. :) Я погоджуюсь, що видалення лямбда - це виграш.
Меттью Флашен

4

Ви також можете використовувати anyз розумінням списку:

any([item in a for item in b])

6
Можна, але час O (n * m), тоді як час для встановленого підходу перетину - O (n + m). Ви також можете це зробити БЕЗ розуміння списку (втратити []), і він би запустився швидше і використовував менше пам'яті, але час все одно буде O (n * m).
Джон Махін

1
Незважаючи на те, що ваш великий аналіз O відповідає дійсності, я підозрюю, що для малих значень n та m час, необхідний для побудови базових хештелів, зійде. Big O ігнорує час, необхідний для обчислення хешів.
Ентоні Коньєрс

2
Побудова "хештеля" амортизується O (n).
Джон Махін

1
Я це розумію, але постійність, яку ти відкидаєш, досить велика. Це не має значення для великих значень n, але для малих.
Ентоні Коньєрс

3

У python 2.6 або новіших версіях ви можете:

return not frozenset(a).isdisjoint(frozenset(b))

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

2

Ви можете використовувати будь-який вбудований вираз функції / генератора wa:

def list_overlap(a,b): 
     return any(i for i in a if i in b)

Як зазначають Джон і Лі, це дає невірні результати, коли для кожного я, поділеного двома списками, bool (i) == False. Вона повинна бути:

return any(i in b for i in a)

1
Підсилюючи коментар Лі Раяна: дасть неправильний результат для будь-якого предмета x, який знаходиться в перехресті, де bool(x)знаходиться Неправда. У прикладі Лі Раяна, x дорівнює 0. Тільки виправити те, any(True for i in a if i in b)що краще записати, як уже бачили any(i in b for i in a).
Джон Махін

1
Виправлення: дасть неправильний результат, коли всі пункти xв перехресті такі, які bool(x)є False.
Джон Махін

1

Це питання досить старе, але я помітив, що, коли люди сперечалися набори проти списків, ніхто не думав використовувати їх разом. Наслідуючи приклад Сораву,

Найгірший випадок для списків:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
100.91506409645081
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
19.746716022491455
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)
0.092626094818115234

І найкращий випадок для списків:

>>> timeit('bool(set(a) & set(b))',  setup="a=list(range(10000)); b=list(range(10000))", number=100000)
154.69790101051331
>>> timeit('any(i in a for i in b)', setup="a=list(range(10000)); b=list(range(10000))", number=100000)
0.082653045654296875
>>> timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=list(range(10000))", number=100000)
0.08434605598449707

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

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


1
Використання isdisjoint()методу на (замороженому) наборі, як зазначено у @Toughy, ще краще: timeit('any(i in a for i in b)', setup="a= set(range(10000)); b=[x+9999 for x in range(10000)]", number=100000)=> 0,00913715362548828
Актау

1

якщо вам все одно, яким може бути елемент, що перекривається, ви можете просто перевірити lenкомбінований список проти списків, об'єднаних у вигляді набору. Якщо є елементи, що перекриваються, набір буде коротшим:

len(set(a+b+c))==len(a+b+c) повертає True, якщо немає перекриття.


Якщо перше значення перекриється, воно все одно перетворить весь список у набір, незалежно від величини.
Пітер Вуд

1

Я піду ще один із функціональним стилем програмування:

any(map(lambda x: x in a, b))

Пояснення:

map(lambda x: x in a, b)

повертає список булевих , де елементи bзнаходяться в a. Потім цей список передається до any, який просто повертається, Trueякщо є якісь елементи True.

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