Pythonでのセットの「奇妙な」順序


14

Python 3.8.0のリストをセットに変換すると、結果のセットの順序*は非常に簡単な方法で高度に構造化されます。この構造は、疑似ランダムリストからどのように抽出されていますか?


私が実行している実験の一部として、ランダムセットを生成しています。セットをプロットすると、セット内に予期しない線形構造が突然現れたことに驚きました。だから私を困惑させる2つのことがある-なぜセット結果への変換がこの構造を強調することになる結局*を持っているのか。そして、それほどではありませんが、なぜ疑似ランダムセットがこの「隠された」構造を持っているのですか?

コード:

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プロット

この動作は、上記のコードで使用されている値250および30で、私のマシン(以下の例)で100%一貫しています(使用した例はチェリーピックではありません-実行した最後のものです)。これらの値を調整すると、構造がわずかに異なる場合があります(たとえば、2つではなく3つの算術数列***のサブセット)。

これは他の人のマシンで再現可能ですか?もちろん、そのような構造が存在することは、それほど大きくない疑似乱数の生成を示しているようですが、これは、セットへの変換がこの構造をある意味で「抽出」する方法を説明していません。私が知る限り、セットの順序付け(リストから変換された場合)が確定的であることを正式に保証するものではありません(そうであっても、バックグラウンドで洗練された順序付けは行われません)。それで、これはどのように起こっていますか?


(*):私が知っている、セットは順不同のコレクションですが、私は呼び出すときに、という意味で「注文」を意味するprint声明を、セットがで出力され、いくつかの一貫基本となるセット構造を強調ため。

(**):これらのプロットはWolfram Alphaからのものです。さらに2つの例を以下に示します。

ここに画像の説明を入力してください

(***):乱数の範囲を250から500に変更した場合の2つのプロット:

ここに画像の説明を入力してください

回答:


14

基本的に、これは次の2つの理由によるものです。

  • 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であり、要素が追加されるときに4倍になるためです。

(番号52および56は、セットに含まれていないため)範囲は、2つのシーケンスに分割され、端部付近ブリップを除く0, 4, 8, ...32, 36, 40, ...数字値自体であるハッシュは、選択するモジュロ32をとっているのでその代替配列のインデックス。衝突があります。たとえば、4と36は32を法として等しいが、4が最初にセットに追加されたため、36は異なるインデックスになります。

これがこのシーケンスのチャートです。数値はステップのある範囲からではなくランダムに生成したため、チャートの構造はノイズの多いバージョンです。

ここに画像の説明を入力してください

インタリーブされたシーケンスの数は、数値がサンプリングされる範囲の長さに比例してセットのサイズに依存します。これは、範囲の長さがハッシュテーブルの基になる配列の長さを法として「折り返す」回数を決定するためです。ここでは3つのインターリーブされた配列との例だ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以上フルになると、配列のサイズが変更されます。これは、配列をフルまたはほぼフルにすると、ハッシュテーブルのパフォーマンスが大幅に低下するためです。
kaya3
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.