«Бізнерне» впорядкування наборів у пітоні


14

Коли я перетворюю список Python 3.8.0 в набір, отримане впорядкування набору * сильно структурується нетривіально. Як ця структура витягується з псевдовипадкового списку?


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

Код:

X = [randrange(250) for i in range(30)]
print(X)
print(set(X))

який виводить, наприклад

[238, 202, 245, 94, 111, 106, 148, 164, 154, 113, 128, 10, 196, 141, 69, 38, 106, 8, 40, 53, 160, 87, 85, 13, 38, 147, 204, 50, 162, 91]

{128, 8, 10, 141, 13, 147, 148, 154, 160, 162, 164, 38, 40, 50, 53, 196, 69, 202, 204, 85, 87, 91, 94, 106, 238, 111, 113, 245}

Сюжет ** з вищевказаного списку виглядає досить випадковим, як і очікувалося:

WolframAlpha графік випадково генерованого списку

тоді як побудова графіку набору (як це впорядковано у висновку) демонструє структуру, наявну в наборі:

Сюжет WolframAlpha набір із випадкового списку

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

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


(*): Я знаю, набори - це не упорядковані колекції, але я маю на увазі "впорядковані" в тому сенсі, що при виклику printоператора набір виводиться в деяких порядку, який послідовно підкреслює базову структуру набору.

(**): Ці сюжети походять від Wolfram Alpha. Ще два приклади нижче:

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

(***): дві графіки при зміні діапазону випадкових чисел від 250 до 500:

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

Відповіді:


14

В основному, це через дві речі:

  • Набір в Python реалізований за допомогою хештеля ,
  • Хеш цілого числа є самим цілим числом.

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

>>> list(set(range(10000))) == list(range(10000))
True # this can't be an accident!

Якщо у вас немає всіх чисел із суміжного діапазону, тоді грає «модуль довжини нижнього масиву»:

>>> r = range(0, 50, 4)
>>> set(r)
{0, 32, 4, 36, 8, 40, 12, 44, 16, 48, 20, 24, 28}
>>> sorted(r, key=lambda x: x % 32)
[0, 32, 4, 36, 8, 40, 12, 44, 16, 48, 20, 24, 28]

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

За винятком прогалини в кінці (оскільки числа 52 і 56 відсутні в безлічі), діапазон ділиться на дві послідовності 0, 4, 8, ... і 32, 36, 40, ...які чергуються , так як хеш, які самі значення чисел, взяті по модулю 32 , щоб вибрати індекси в масиві. Відбуваються зіткнення; наприклад, 4 і 36 дорівнюють модулю 32, але 4 додати в набір спочатку, так що 36 закінчується з іншим показником.

Ось діаграма цієї послідовності. Структура ваших діаграм - це лише галасливіша версія, оскільки ви генерували свої номери випадковим чином, а не з діапазону з кроком.

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

Кількість переплетених послідовностей буде залежати від розміру набору пропорційно довжині діапазону, з якого вибираються номери, оскільки це визначає, у скільки разів довжина діапазону "обертається" за модулем довжина нижнього масиву хештеля. Ось приклад з трьома чергуються послідовностями 0, 6, 12, ..., 66, 72, 78, ...і 36, 42, 48, ...:

>>> set(range(0, 90, 6))
{0, 66, 36, 6, 72, 42, 12, 78, 48, 18, 84, 54, 24, 60, 30}

Ах! Це пояснює це (і приємне пояснення теж)!
Джон Дон

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

Як ви виявите, що 30 - це довжина базового масиву?
Марк Снайдер

@MarkSnyder Виявляється, що це 32, це означає, що є зіткнення, але порядок такий самий, як якщо б він був за модулем 30.
kaya3

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