並べ替え(整列)

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 はオブジェクトのアドレスの比較です。後者の方が速く,しかも == のようにオーバーライド(上書き)できないので確実な比較ができます。

時間を比べてみましょう。Wall time が実時間(壁にかかっている時計で計った時間)です:

import numpy as np

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

%time selectsort(a)
CPU times: user 19.1 s, sys: 10.3 ms, total: 19.1 s
Wall time: 19.1 s
a = rng.random(10000)
%time quicksort(a)
CPU times: user 82.3 ms, sys: 3.79 ms, total: 86.1 ms
Wall time: 84.8 ms

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

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

a = rng.random(10000)
%time a.sort() # aを上書きする
CPU times: user 702 µs, sys: 452 µs, total: 1.15 ms
Wall time: 593 µs
a = rng.random(10000)
time b = np.sort(a) # aを上書きしない
CPU times: user 811 µs, sys: 647 µs, total: 1.46 ms
Wall time: 817 µ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)

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