世界の国564520 views
高校日本史190571 views
Computer368106 views
中学理科1630641 views
数学講師2886227 views
LaTeX962153 views
MathPython497343 views
小学理科719870 views
ヒストリア290598 views
雑学1473612 views
Help
Tools
NewsSpreadsheetCalendarSlidesTier ListPen ToolIllustrationCrayonPixel ArtASCII ArtPerspectiveEndless StairsGraphMind MapER DiagramFamily TreeMemeCurved TextImage EditorMosaicRetro FilterPencil SketchOCR/HighlighterMakeup EditorFaviconVideo TrimmerScrolling VideoVideo TitleColor PickerColor ExtractorBonfireFireworksWater RippleWater SplashBreaking GlassWood GrainMarble TextureCSS ButtonIcon MakerBar ChartGrouped Bar ChartStacked Bar ChartPie ChartLine ChartArea ChartStacked Area ChartScatter Plot3D Bar Chart3D Pie ChartBar Chart RaceBubble ChartPopulation PyramidPictogramEarningsCandlestick ChartInvestment RiskMortgage SimulatorCalculatorMatrix CalculatorFunction GraphPolynomial ExpansionVenn DiagramField VisualizerRubik's Cube Group TheoryTraveling SalesmanVoronoi and DelaunayFractalColumn ArithmeticDraw Math FiguresArithmetic AnimationArithmetic Word ProblemsCounting with Tree DiagramsCube NetsCross SectionsMotion PathMechanicsWavesLight and LensesThermodynamicsHow Semiconductors WorkMolecular StructuresAtomic OrbitalsElectrochemical CellsChemical EquilibriumCrystal LatticesBuffer pHComplex IonsDNA Double HelixCell DivisionMembrane ChannelsNerve ImpulseRock ClassificationWeatherConstellationsSolar and Lunar Eclipses3D ModelingFloor PlanSeismic StructuresIntersection TurnMaglevCooking AnimationOrigamiLive Viewer CountGeoJSON MapRailway MapPopulation MapCrime MapLand Price MapSchool MapShrine and Castle MapHouse of Representatives MapWord MapSolitaireReversiHakoiri MusumeChessHamburgerRippleSlide Puzzle MakerNeon PinballNovel MakerJapanese Typing PracticePiano Score EditorMusic Theory

English

Python の集合演算のパフォーマンス比較

集合演算のパフォーマンスは、要素数や操作の種類によって大きく変わる。ここでは各演算の計算量と実測値を比較する。

基本操作の計算量

まず、基本操作の計算量を整理する。

操作平均最悪
x in sO(1)O(n)
s.add(x)O(1)O(n)
s.remove(x)O(1)O(n)
len(s)O(1)O(1)

最悪ケースは極端なハッシュ衝突が発生した場合で、通常は発生しない。

集合演算の計算量

2 つの集合 A と B に対する演算の計算量は以下のとおりだ。

演算計算量
A | BO(len(A) + len(B))
A & BO(min(len(A), len(B)))
A - BO(len(A))
A ^ BO(len(A) + len(B))

積集合が効率的なのは、小さいほうの集合の要素だけを調べればよいためだ。

# 積集合の内部実装(概念)
def intersection(a, b):
    if len(a) > len(b):
        a, b = b, a  # 小さいほうを基準に
    return {x for x in a if x in b}

実測による比較

実際に計測して確認する。

import time

def measure(name, func, iterations=1000):
    start = time.perf_counter()
    for _ in range(iterations):
        func()
    elapsed = time.perf_counter() - start
    print(f"{name}: {elapsed:.4f}s")

a = set(range(10000))
b = set(range(5000, 15000))

measure("union      ", lambda: a | b)
measure("intersection", lambda: a & b)
measure("difference ", lambda: a - b)
measure("symmetric  ", lambda: a ^ b)

積集合が最も高速で、和集合と対称差は同程度、差集合はその中間になる。

部分集合判定のパフォーマンス

部分集合判定 <= の計算量は O(len(小さいほう)) だ。

small = set(range(100))
large = set(range(10000))

# small が large の部分集合か
# small の全要素が large に含まれるか確認
# O(len(small)) = O(100)
print(small <= large)  # True

逆方向の判定も同じだが、実際に調べる要素数は異なる。

import time

small = set(range(100))
large = set(range(10000))

# small <= large: small の 100 要素を確認
start = time.perf_counter()
for _ in range(10000):
    small <= large
print(f"small <= large: {time.perf_counter() - start:.4f}s")

# large <= small: large の 10000 要素を確認、すぐに False
start = time.perf_counter()
for _ in range(10000):
    large <= small
print(f"large <= small: {time.perf_counter() - start:.4f}s")

large <= small は False になるが、最初の要素(large にあって small にない要素)を見つけた時点で終了するため、実際には高速だ。

リストとの比較

リストで同様の操作を行うとどうなるか。

import time

a_list = list(range(10000))
b_list = list(range(5000, 15000))

a_set = set(a_list)
b_set = set(b_list)

# リストでの和集合相当
start = time.perf_counter()
for _ in range(100):
    result = list(set(a_list) | set(b_list))
print(f"list union: {time.perf_counter() - start:.4f}s")

# set での和集合
start = time.perf_counter()
for _ in range(100):
    result = a_set | b_set
print(f"set union: {time.perf_counter() - start:.4f}s")

リストから一度 set に変換するオーバーヘッドがあるため、set をそのまま使うほうが圧倒的に高速だ。

リストで集合演算

毎回 set への変換が必要。O(n) の変換コストが発生。

set をそのまま使用

変換不要。純粋な集合演算のみ。

要素数による影響

要素数が増えると、計算時間も比例して増加する。

import time

for n in [1000, 10000, 100000]:
    a = set(range(n))
    b = set(range(n // 2, n + n // 2))
    
    start = time.perf_counter()
    for _ in range(100):
        a & b
    elapsed = time.perf_counter() - start
    print(f"n={n:>6}: {elapsed:.4f}s")

積集合の場合、小さいほうの要素数に依存するため、非対称な集合では注意が必要だ。

破壊的操作 vs 非破壊的操作

破壊的メソッド(update() など)は新しい集合を作らないため、わずかに高速だ。

import time

# 非破壊的
start = time.perf_counter()
for _ in range(10000):
    a = {1, 2, 3}
    b = a | {4, 5, 6}
print(f"union: {time.perf_counter() - start:.4f}s")

# 破壊的
start = time.perf_counter()
for _ in range(10000):
    a = {1, 2, 3}
    a.update({4, 5, 6})
print(f"update: {time.perf_counter() - start:.4f}s")

差は小さいが、大量のデータを処理する場合は破壊的操作のほうがメモリ効率もよい。

isdisjoint の最適化

isdisjoint() は共通要素がないかを調べるが、見つかった時点で即座に False を返す。

import time

a = set(range(10000))
b = set(range(10000, 20000))  # 共通要素なし
c = set(range(9999, 20000))    # 1 つだけ共通

# 共通要素なし: 全要素を調べる
start = time.perf_counter()
for _ in range(1000):
    a.isdisjoint(b)
print(f"disjoint (no common): {time.perf_counter() - start:.4f}s")

# 共通要素あり: すぐに False
start = time.perf_counter()
for _ in range(1000):
    a.isdisjoint(c)
print(f"disjoint (has common): {time.perf_counter() - start:.4f}s")

共通要素がある場合のほうが高速になる。最初に見つけた共通要素で判定が終わるためだ。

まとめ

集合演算を効率的に行うには、データを最初から set として保持しておくことが重要だ。リストから毎回変換するのは非効率的である。また、積集合は小さいほうの集合サイズに依存するため、非対称な集合を扱う際はこの特性を意識するとよい。