Python bisect.insort()の使い方|insortの意味・left/rightの違い・key引数と計算量

bisect.insort()は、ソート済みのリストに、並び順を崩さない位置へ値を挿入する関数です。insortは「insert in sorted order(並び順どおりに挿入する)」の意味で、bisect.insort(リスト, 値)と書くとリストがその場で書き換わり、戻り値はNoneです。

この記事では、insort()の基本、insort_leftとinsort_rightの違い、Python 3.10で入ったkey引数、計算量の誤解、append+sortとの速度比較を、Python 3.12で実行した結果つきで説明します。

insortの意味とbisect.insort()の基本

Python公式ドキュメントでは、insort_left()は「x を a にソート順で挿入する」関数と説明されています。insort()はinsort_right()の別名です(bisect — Array bisection algorithm、2026年10月18日確認)。

import bisect

b = [10, 20, 30]

bisect.insort(b, 25)
bisect.insort(b, 5)
bisect.insort(b, 20)

print(b)
print(bisect.insort(b, 15))
print(b)

実行結果:

[5, 10, 20, 20, 25, 30]
None
[5, 10, 15, 20, 20, 25, 30]

どこに入れるかを自分で計算しなくても、並び順が保たれます。list.sort()と同じく戻り値はNoneなので、b = bisect.insort(b, 15)と書くとbがNoneになります。

関数 同じ値があるとき 中身
insort(a, x) 既存の値の後ろに入れる insort_rightと同じ
insort_right(a, x) 既存の値の後ろに入れる bisect_rightで位置を探してinsert
insort_left(a, x) 既存の値の前に入れる bisect_leftで位置を探してinsert

シグネチャはどれも(a, x, lo=0, hi=len(a), *, key=None)です。loとhiで探す範囲を絞れますが、普段は指定しません。位置だけを知りたいときのbisect_left/bisect_rightは「Python bisect_leftとbisect_rightの違い」で解説しています。

insort_leftとinsort_rightの違いが出る例

数値だけのリストでは、同じ値を前に入れても後ろに入れても見た目は変わりません。違いが見えるのは、比較に使う部分は同じで、ほかの情報が違う要素を入れるときです。keyで1番目の数字だけを比べるようにして試します(Python 3.10以降)。

import bisect

e = [(1, "A"), (2, "B"), (3, "D")]
bisect.insort_left(e, (2, "Z"), key=lambda t: t[0])
print("left :", e)

e = [(1, "A"), (2, "B"), (3, "D")]
bisect.insort_right(e, (2, "Z"), key=lambda t: t[0])
print("right:", e)

実行結果:

left : [(1, 'A'), (2, 'Z'), (2, 'B'), (3, 'D')]
right: [(1, 'A'), (2, 'B'), (2, 'Z'), (3, 'D')]

同じ2でも、leftは既存の(2, 'B')の前、rightは後ろに入りました。「同じ値なら先に来たものを前に置きたい(到着順を保ちたい)」ならinsort(right)を使います。

key引数でタプル・辞書を挿入する(Python 3.10以降)

keyを付けないと、タプルは2番目以降の要素まで比べられます。

e = [(1, "A"), (2, "B"), (3, "D")]
bisect.insort(e, (2, "A"))
print(e)

実行結果:

[(1, 'A'), (2, 'A'), (2, 'B'), (3, 'D')]

辞書はそもそも大小比較ができないので、keyが必須です。

users = [{"name": "A", "age": 20}, {"name": "C", "age": 40}]
bisect.insort(users, {"name": "B", "age": 30}, key=lambda u: u["age"])
print([u["name"] for u in users])

try:
    bisect.insort(users, {"name": "D", "age": 35})
except TypeError as ex:
    print("TypeError:", ex)

実行結果:

['A', 'B', 'C']
TypeError: '<' not supported between instances of 'dict' and 'dict'

注意点が1つあります。公式ドキュメントにあるとおり、insortのkeyは挿入する値xにも適用されます(bisect_leftなどの探索関数とは扱いが違います)。そのためxにはキーではなく、リストの要素と同じ形(上の例なら辞書そのもの)を渡します。

keyはPython 3.10で追加されました。3.9以前で渡すとエラーになります(macOS標準のPython 3.9.6で確認)。

TypeError: 'key' is an invalid keyword argument for insort_right()

降順のリストにinsortするとおかしくなる

bisectは昇順に並んでいることを前提にしています。降順のリストにそのまま使うと、エラーは出ずに間違った位置に入ります。

d = [30, 20, 10]
bisect.insort(d, 25)
print(d)

d = [30, 20, 10]
bisect.insort(d, 25, key=lambda v: -v)   # 符号を反転したキーで昇順として扱う
print(d)

実行結果:

[30, 20, 10, 25]
[30, 25, 20, 10]

数値の降順なら、key=lambda v: -vで正しく入ります。そもそも並んでいないリストでも同じで、[30, 10, 20]に15を入れると[30, 10, 15, 20]になります。bisectはソート済みかどうかを確認しないので、最初に1回ソートし、以降はinsortだけで足すというルールを守ってください。

insortの計算量:探すのはO(log n)、挿入はO(n)

「二分探索だから速い」と思われがちですが、公式ドキュメントには「O(log n)の探索より、遅いO(n)の挿入のほうが支配的」と書かれています。リストの途中に入れると、それより後ろの要素をすべて1つずつずらす必要があるためです。

import bisect, time

for n in [10_000, 100_000, 1_000_000]:
    a = list(range(n))
    s = time.perf_counter()
    for _ in range(1000):
        bisect.insort(a, -1)      # 毎回先頭に入る(最悪ケース)
    head = (time.perf_counter() - s) * 1000
    b = list(range(n))
    s = time.perf_counter()
    for _ in range(1000):
        bisect.insort(b, n + 1)   # 毎回末尾に入る
    tail = (time.perf_counter() - s) * 1000
    print(f"n={n:>9,}  先頭へ1000回: {head:7.2f} ms  末尾へ1000回: {tail:5.2f} ms")

実行結果(Apple M2、Python 3.12):

n=   10,000  先頭へ1000回:    1.78 ms  末尾へ1000回:  0.14 ms
n=  100,000  先頭へ1000回:   19.70 ms  末尾へ1000回:  0.14 ms
n=1,000,000  先頭へ1000回:  161.29 ms  末尾へ1000回:  0.16 ms

先頭への挿入は要素数にほぼ比例して遅くなり、末尾への挿入はほとんど変わりません。差を生んでいるのは探索ではなく、要素をずらす処理です。

append+sortとの速度比較

では「1件足すたびにappendしてsortし直す」書き方と比べるとどうでしょうか。ランダムな値2万件を1件ずつ追加しました。

import bisect, random, time

random.seed(0)
data = [random.random() for _ in range(20000)]

def t(f):
    s = time.perf_counter(); f(); return (time.perf_counter() - s) * 1000

def by_insort():
    a = []
    for x in data:
        bisect.insort(a, x)

def by_append_sort_each():
    a = []
    for x in data:
        a.append(x)
        a.sort()

def by_sort_once():
    a = []
    for x in data:
        a.append(x)
    a.sort()

print(f"insortで1件ずつ        : {t(by_insort):7.1f} ms")
print(f"append+sortを毎回      : {t(by_append_sort_each):7.1f} ms")
print(f"最後に1回だけsort      : {t(by_sort_once):7.1f} ms")

実行結果(Apple M2、Python 3.12。数値は実行ごとに多少変わります):

insortで1件ずつ        :    20.1 ms
append+sortを毎回      :  431.9 ms
最後に1回だけsort      :     1.8 ms
状況 選ぶもの
全部集めてから1回だけ並べればよい appendして最後にsort()(いちばん速い)
追加のたびにソート済みの状態が必要 insort(毎回sort()より大幅に速い)
挿入と検索を交互に繰り返す insort+bisect_left
最小値(上位N件)だけを何度も取り出す heapq
件数がとても多く、挿入も頻繁 リスト以外のデータ構造を検討

処理時間の測り方は「Python timeit.timeit()の使い方」も参考にしてください。

insortの使用例:スコアを順位表に追加する

import bisect

ranking = []   # (点数, 名前) を点数の昇順で保持
for name, score in [("佐藤", 72), ("鈴木", 88), ("高橋", 65), ("田中", 88)]:
    bisect.insort(ranking, (score, name), key=lambda r: r[0])

for score, name in reversed(ranking):
    print(score, name)

実行結果:

88 田中
88 鈴木
72 佐藤
65 高橋

同点の場合、insort(right)は後から来た田中を鈴木の後ろに入れ、表示時にreversedで逆順にしているため田中が先に出ています。同点は先着順で表示したいなら、insort_leftに変えると鈴木が先になります。

よくある質問

Q. insortとはどういう意味ですか?

insert in sorted order、つまり「並び順どおりに挿入する」という意味です。Pythonではbisectモジュールの関数名で、ソート済みリストの正しい位置に値を入れます。

Q. bisect.insort()の戻り値は何ですか?

Noneです。リストそのものが書き換わるので、戻り値を代入せずにそのままリストを使います。

Q. insortとinsort_rightの違いは何ですか?

違いはありません。insortはinsort_rightの別名で、同じ値がすでにあるときはその後ろに挿入します。前に入れたいときはinsort_leftを使います。

Q. insortの計算量はO(log n)ですか?

位置を探す部分はO(log n)ですが、リストへの挿入がO(n)なので、全体ではO(n)です。それでも毎回sort()し直すよりは速く済みます。

まとめ

  • bisect.insort(a, x)はソート済みリストの正しい位置に挿入する。戻り値はNone
  • insortはinsort_rightと同じ。同じ値の前に入れるならinsort_left
  • key引数はPython 3.10以降。xにもkeyが適用されるので要素そのものを渡す
  • 昇順が前提。降順ならkey=lambda v: -v
  • 探索はO(log n)でも挿入はO(n)。まとめて並べるなら最後に1回sort()が速い

あわせて読みたい

対面・無料|東京・永田町で毎週土曜日13時

1回の参加で、プログラミングの構造がわかる

ゼロからはじめるPython入門講座

8年間・累計600回開催/これまで全国で2万人以上が参加

「やはり対面で聞けるのはとても気づきがあり、価値があると思います」

参加者の声

  • 本当に初心者からの講座です。プログラミングに触ったことがなくて大丈夫です
  • 対面だから、その場で聞けます。つまずいた所を持ち帰らずに解決できます
  • 講座は1時間。そのあとも残って学習を続けられ、講師にも質問できます
  • 独学で挫折した方も。糸口が見つかって、講座のあと会員になる方が多くいます

無料で参加する|開催日程を見る

参加費は無料です