set()はどのように実装されていますか?


151

私はset、PythonのオブジェクトにO(1)メンバーシップチェックがあると言われているのを見てきました。これを可能にするために内部でどのように実装されていますか?どのようなデータ構造を使用していますか?その実装には他にどのような影響がありますか?

ここでのすべての答えは本当に啓発的でしたが、私は1つしか受け入れることができないため、元の質問に最も近い答えを使用します。情報ありがとうございました!

回答:


139

このスレッドによると:

実際、CPythonのセットはダミー値(キーはセットのメンバー)を持つ辞書のようなものとして実装されており、この値の欠如を悪用するいくつかの最適化があります。

したがって、基本的にsetは、基礎となるデータ構造としてハッシュテーブルを使用します。ハッシュテーブルで項目を検索することは、平均してO(1)操作であるため、これはO(1)メンバーシップチェックを説明します。

意欲がある場合は、セットのCPythonソースコードを参照することもできます。これは、Achim Dommaによると、ほとんどがdict実装からのカットアンドペーストです。


18
IIRC、元のset実装は実際 dictはダミー値を使用しており、後で最適化されました。
dan04

1
ビッグOは最悪のシナリオではありませんか?時間がO(n)であるインスタンスを見つけることができる場合、それはO(n)です。これらのすべてのチュートリアルから、現時点では何も理解できません。
Claudiu Creanga

4
いいえ、平均的なケースはO(1)ですが、ハッシュテーブルルックアップの最悪のケースはO(N)です。
ジャスティンエティエ2016

4
@ClaudiuCreangaこれは古いコメントですが、明確にするために:big-O表記は物事の成長率の上限を示しますが、平均ケースのパフォーマンスの成長を上限にでき、最悪のケースの成長を個別に上限にできますパフォーマンス。
Kirk Boyer

79

セットにO(1)メンバーシップチェックがあると人々が言うとき、彼らは平均的なケースについて話している。で最悪の場合(すべてのハッシュ値が衝突したときに)メンバーシップチェックはO(N)です。時間の複雑さに関するPython wikiを参照してください。

Wikipediaの記事は述べている最良のケースのサイズ変更ではありません、ハッシュテーブルのための時間の複雑さをO(1 + k/n)。Pythonセットはサイズ変更するハッシュテーブルを使用するため、この結果はPythonセットには直接適用されません。

ウィキペディアの記事の少し先に、平均的なケースでは、単純な均一ハッシュ関数を想定すると、時間の複雑さはO(1/(1-k/n))でありk/n、定数によって制限される可能性があると述べていc<1ます。

Big-Oは、漸近的な振る舞いのみをn→∞と呼びます。k / nはnとは無関係に定数c <1でバインドできるため、

O(1/(1-k/n))= O(1/(1-c))と同等の大きさです。O(constant)O(1)

したがって、均一な単純なハッシュを仮定すると、平均して、PythonセットのメンバーシップチェックはになりO(1)ます。


14

私はそれをよくある間違いだと思います、setルックアップ(またはそのことについてはハッシュテーブル)はO(1)ではありません。
ウィキペディアから

最も単純なモデルでは、ハッシュ関数は完全に指定されておらず、テーブルのサイズは変更されません。ハッシュ関数の最良の選択のために、オープンアドレス指定のサイズnのテーブルは衝突がなく、ルックアップを成功させるための単一の比較で最大n個の要素を保持し、チェーンとkキーのサイズnのテーブルは最小の最大値を持ちますルックアップのための(0、kn)衝突と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 per loop <-> python- m timeit -s "s = set(range(10000000))" "5 in s" 10000000ループ、ベスト3:ループあたり0.0634 usec ...そして、これはMemoryErrorsをスローしない最大のセットです
Jochen Ritzel

2
@ THC4kあなたが証明したのは、Xの検索が一定の時間で行われるということですが、X + Yの検索に同じ時間がかかるということではありません。これは、O(1)のすべてのことです。
Shay Erlichmen

3
@intuited:ありますが、上記のテスト実行では、 "485398"または恐ろしい衝突空間にある可能性のある他の数値を検索できると同時に、 "5"を検索できることを証明していません。異なるサイズのハッシュで同じ要素を同時に検索することではなく(実際にはまったく必要ありません)、現在のテーブルで同じ時間内に各エントリにアクセスできるかどうかが重要です。通常は常に衝突があるため、ハッシュテーブルでは基本的に不可能です。
Nick Bastin、

3
言い換えると、衝突が発生する可能性が高くなるため、ルックアップにかかる時間は格納された値の数に依存します。
2010年

3
@intuited:いいえ、不正解です。格納される値の数が増えると、Pythonは自動的にハッシュテーブルのサイズを増やし、衝突の割合はほぼ一定のままになります。均等に分散されたO(1)ハッシュアルゴリズムを想定すると、ハッシュテーブルルックアップは償却された O(1)になります。ビデオプレゼンテーション「The Mighty Dictionary」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構文の強調表示は本当に悪く見えます。
user202729

en.wikipedia.org/wiki/Open_addressingで説明されているように、「これにより、線形プローブとオープンアドレス指定のハイブリッドが残されます」というコメントについては、オープンアドレス指定における一種の衝突解決ではありませんか?したがって、線形プローブはオープンアドレス指定のサブタイプであり、コメントは意味がありません。
アランエヴァンジェリスタ

2

set'sとの違いをもう少し強調するためにdict'ssetobject.cコメントセクションからの抜粋を示します。これは、ディクショナリに対するセットの主な違いを明らかにしています。

セットの使用例は、検索されたキーが存在する可能性が高い辞書とはかなり異なります。対照的に、セットは、要素の存在が事前にわからないメンバーシップテストに関するものです。したがって、集合の実装は、見つかった場合と見つからなかった場合の両方に対して最適化する必要があります。

github上のソース

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.