並べ替え(整列)

Python には小さい順に並べ替える関数が二つあります。sort()sorted() です。この違いを,例で体験してみましょう。

a = [3, 5, 2, 4, 1]   # 適当なリストを作る
sorted(a)             # 並べ替えたリストを返す
[1, 2, 3, 4, 5]       # 並べ変わっている
a                     # 元のリスト a は
[3, 5, 2, 4, 1]       # 並べ変わっていない
a.sort()              # リストをその場で並べ替える
a                     # 元のリスト a が
[1, 2, 3, 4, 5]       # 並べ変わっている
sorted("I have an apple".lower().split())
['an', 'apple', 'have', 'i']  # 文字列は辞書順に

デフォルトでは昇順になります。降順にするには reverse=True というオプションを与えます。

並べ替えをする簡単な関数を作ってみましょう。いろいろなアルゴリズムがありますが,次のものは選択ソート(selection sort)というものです:

def selectsort(a):
    n = len(a)
    for i in range(n - 1):
        k = i
        for j in range(i + 1, n):
            if a[j] < a[k]:
                k = j
        a[i], a[k] = a[k], a[i]

最後の行は a[i]a[k] の値を交換しています。Python ならこのように簡単に書けます。

うまくいくか試してみましょう:

a = [3, 5, 2, 4, 1]   # 適当なリストを作る
selectsort(a)         # うまくいくかな?
a                     # 元のリスト a は
[1, 2, 3, 4, 5]       # 並べ変わっている

もっと速いアルゴリズムとしてクイックソート(quicksort)が知られています:

def quicksort(a, first=0, last=None):
    if last is None:
        last = len(a) - 1
    x = a[(first + last) // 2]
    i = first
    j = last
    while True:
        while a[i] < x:
            i += 1
        while x < a[j]:
            j -= 1
        if i >= j:
            break
        a[i], a[j] = a[j], a[i]
        i += 1
        j -= 1
    if first < i - 1:
        quicksort(a, first, i - 1)
    if j + 1 < last:
        quicksort(a, j + 1, last)

def quicksort(a, first=0, last=len(a)-1): としたいかもしれませんが,Python の引数のデフォルトの値は関数定義を評価したときに決まってしまうので,期待した動作になりません。ここでは,何もないことを表すオブジェクト None をデフォルトの値として,関数の実行時にもし None であれば len(a) - 1 に置き換えています。if last is None:if last == None: と似ていますが == は値の比較,is はオブジェクトのアドレスの比較です。後者の方が速く,しかも == のようにオーバーライド(上書き)できないので確実な比較ができます。

上はPythonを仮定しない汎用的なコードですが、Pythonなら次のような書き方もできます。ただしこちらは配列を並べ替えるのではなく、並べ替わった新しい配列を返します:

def quicksort(a):
    if len(a) <= 1:
        return a
    pivot = a[len(a) // 2]
    left = [x for x in a if x < pivot]
    middle = [x for x in a if x == pivot]
    right = [x for x in a if x > pivot]
    return quicksort(left) + middle + quicksort(right)

M1 Mac mini (2020)、Python 3.12.3 で時間を比べてみましょう。Wall time が実時間(壁にかかっている時計で計った時間)です。まず選択ソート:

import numpy as np

rng = np.random.default_rng()
a = rng.random(10000)

%time selectsort(a)
CPU times: user 5.27 s, sys: 1.58 ms, total: 5.27 s
Wall time: 5.27 s

1番目のクイックソート:

a = rng.random(10000)
%time quicksort(a)
CPU times: user 25.6 ms, sys: 1.3 ms, total: 26.9 ms
Wall time: 25.8 ms

2番目のクイックソート:

a = rng.random(10000)
%time tmp = quicksort(a)
CPU times: user 19.3 ms, sys: 588 μs, total: 19.9 ms
Wall time: 19.6 ms

実際には何度もやって平均をとらないと正確な時間がわかりません。%time の代わりに %timeit を使えば自動的に何度もやって平均をとってくれるのですが、整列済みの配列を整列すると時間が変わってきますので、ここでは %time を使っています。

Python の組み込み関数 sort()sorted()Timsort というアルゴリズムを使っています。

NumPy にも整列の関数があります。

a = rng.random(10000)
%time a.sort() # aを上書きする
CPU times: user 1.06 ms, sys: 616 μs, total: 1.67 ms
Wall time: 1.17 ms
a = rng.random(10000)
%time tmp = np.sort(a) # aを上書きしない
CPU times: user 975 μs, sys: 711 μs, total: 1.69 ms
Wall time: 906 μs

こちらは降順に並べ替えるオプションはありませんが,a[::-1] で逆順にできます。あるいは実際には並べ替えをせず並べ替えた順序だけを返す np.argsort() を使えば逆順もできます。

a = np.array([3, 5, 2, 4, 1])
o = np.argsort(a)
a[o]
array([1, 2, 3, 4, 5])
o = np.argsort(-a)
a[o]
array([5, 4, 3, 2, 1])

文科省の高等学校情報<科教員研修用教材ページ→高等学校情報科「情報Ⅰ」教員研修用教材(本編)→第3章 コンピュータとプログラミング (PDF:9.3MB) のp.131にクイックソートのPythonプログラム例が載っていますが,これには微妙なバグが潜んでいることをE.V.ジュニアさんが見つけてくださいました(→クイックソートは難しい? : 文科省研修教材のコードには虫がいるよ)。

念のため,こちらにも文科省のコードを載せておきます:

文科省のクイックソート

さらに念のため,これをそのままテキスト化したもの:

def quicksort(a,start,end):
    m = int((start+end)/2)
    i = start
    j = end
    while(i<j):
        while a[i] < a[m]:
            i = i+1
        while a[j] > a[m]:
            j = j-1
        if i>=j:
            break
        temp = a[i]
        a[i] = a[j]
        a[j] = temp
        if i==m:
            m = j
        elif j==m:
            m = i
        i = i+1
        j = j-1
    if start < i-1:
        quicksort(a,start,m-1)
    if end > j+1:
        quicksort(a,m+1,end)

a = [7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1]
print("ソート前",a)
quicksort(a,0,len(a)-1)
print("ソート後",a)

[追記] 上記クイックソートのバグは「正誤表」で訂正されました:高等学校情報科教員研修用教材