5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

《アルゴリズムを学ぶ》23.クイックソート

5
Last updated at Posted at 2025-04-13

Overview

クイックソートは平均 O(N log N)、最悪 O(N²) で動作し、実用では最速クラスの比較ソートとして広く使われている。
1.クイックソートの基本
2.クイックソートの特徴
3.インプレースクイックソート
4.クイックセレクト(Quick Select)
5.典型問題を解いてみる
6.まとめ

1. クイックソートの基本

(1) クイックソートとは

クイックソート(Quick Sort)は、基準(pivot)となる要素を中心に
小さい値を左、大きい値を右 に分割し、再帰的に整列する高速なソートアルゴリズム。

  • 平均計算量:O(N log N)
  • 最悪計算量:O(N²)(pivot が極端に偏ったとき)
  • メモリ:O(log N)(再帰スタック)
  • 非安定ソート

分割統治法(Divide and Conquer)の代表的なアルゴリズム。

(2) 処理の流れ

  1. pivot(基準値)を選ぶ
  2. pivot より小さい値を左、大きい値を右に分割
  3. 左右の部分配列に再帰的にクイックソートを適用
  4. 全体が整列される

(3) 配列コピー版の簡単実装(説明向け)

def quick_sort(arr):
    if len(arr) <= 1:
        return arr

    pivot = arr[0]
    less = [x for x in arr[1:] if x <= pivot]
    greater = [x for x in arr[1:] if x > pivot]

    return quick_sort(less) + [pivot] + quick_sort(greater)

# 使用例
arr = [10, 3, 5, 2, 8, 7]
print(quick_sort(arr))  # 出力: [2, 3, 5, 7, 8, 10]
  • 読みやすいが、配列のコピーが多くメモリ効率は低い

2. クイックソートの特徴

(1) 計算量

ケース 計算量 理由
平均 O(N log N) 分割が均等に近い
最悪 O(N²) pivot が偏り、片方に寄る
空間 O(log N) 再帰の深さ(平均)

(2) ピボットの選び方

方法 メリット デメリット
先頭/末尾 実装が簡単 整列済み入力で最悪 O(N²)
中央要素 偏りをやや回避 均等とは限らない
ランダム 偏りをほぼ回避できる 乱数呼び出しのコスト
中央値(3点 or 全体) 最も安定 計算コストが高い

(3) Partition法は2種類ある

Lomuto partition(シンプル)

def lomuto_partition(arr, low, high):
    pivot = arr[high]
    i = low
    for j in range(low, high):
        if arr[j] <= pivot:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
    arr[i], arr[high] = arr[high], arr[i]
    return i

Hoare partition(交換回数が少なく実務向け)

Hoare partition は swap が少なく、また部分配列の分割境界も安定しやすいため、
実務レベルでは Lomuto より平均して高速に動作する。

def hoare_partition(arr, low, high):
    pivot = arr[(low + high) // 2]
    i = low - 1
    j = high + 1
    while True:
        i += 1
        while arr[i] < pivot:
            i += 1
        j -= 1
        while arr[j] > pivot:
            j -= 1
        if i >= j:
            return j
        arr[i], arr[j] = arr[j], arr[i]

3. インプレースクイックソート

以下は Hoare partition を使った実用的な in-place 実装。
Python では再帰の深さがデフォルト 1000 のため、pivot が偏ると RecursionError になる可能性がある。
実務では iterative 実装や introsort がよく用いられる。

def quick_sort_inplace(arr, low, high):
    if low >= high:
        return

    p = hoare_partition(arr, low, high)
    quick_sort_inplace(arr, low, p)
    quick_sort_inplace(arr, p + 1, high)

(1) Stack Overflow を避けるテクニック

  • pivot をランダムにする(偏りを避ける)
  • 小さい方の区間を先に再帰し、大きい区間は while にする(tail recursion optimization)
  • introsort(QuickSort + HeapSort のハイブリッド)を使う
    →C++ STL の sort() が採用

4. クイックセレクト(Quick Select)

配列全体をソートせず K 番目に小さい要素だけ を求められる。

  • 平均 O(N)
  • 最悪 O(N²)
  • アイデアは QuickSort と同じ
配列: [7, 2, 1, 6, 8, 5, 3, 4]
3 番目に小さい値を求める

①pivot = 7 で分割
pivot = 7
左(小さい値) : [2, 1, 6, 5, 3, 4]
右(大きい値) : [8]
                [7]
      ┌─────────┴──────────┐
[2, 1, 6, 5, 3, 4]         [8]

左側は 6 個あり、
3番目は左側にある と分かるので右側も pivot も無視。

次の探索範囲:
[2,1,6,5,3,4]

②pivot = 2 で分割
pivot = 2
左:  [1]
右:  [6, 5, 3, 4]

左1個 → pivot は「2番目」
よって、
3番目 → 右側の 1番目

③pivot = 6
pivot = 6
左: [5, 3, 4]
右: []

次は:
[5, 3, 4] の 1番目(= 最小)

④pivot = 5
pivot = 5
左: [3, 4]
右: []

次は:
[3, 4] の 1 番目

⑤pivot = 3 → 決定!
pivot = 3
左: []
右: [4]
左が 0 個なので、
pivot が 1 番目 → 求める値 = 3

※ QuickSelect はソートではなく「K番目の要素」を見つけるだけなので、
 不要な側の探索を完全に切り捨てられるのが QuickSort との最大の違い。

def quick_select(arr, k):
    pivot = arr[0]
    left  = [x for x in arr[1:] if x < pivot]
    right = [x for x in arr[1:] if x >= pivot]

    if k <= len(left):
        return quick_select(left, k)
    elif k == len(left) + 1:
        return pivot
    else:
        return quick_select(right, k - len(left) - 1)

5. 典型問題を解いてみる

※ AtCoder 形式に合わせたオリジナル問題です。

例題1. クイックソート実装

問題: N 個の整数をクイックソートで昇順に並べよ。

回答 
N = int(input())
A = list(map(int, input().split()))
print(*quick_sort(A))

例題2. pivot による一回の partition 結果を出力

問題: pivot=A[0] を使い、1 回 partition した結果(左 + pivot + 右)を表示せよ。

回答 
A = list(map(int, input().split()))
pivot = A[0]
left = [x for x in A[1:] if x <= pivot]
right = [x for x in A[1:] if x > pivot]
print(*left, pivot, *right)

例題3. K 番目に小さい要素(QuickSelect)

回答 
N, K = map(int, input().split())
A = list(map(int, input().split()))
print(quick_select(A, K))

例題4. Hoare partition の返す位置を求める

※ Hoare partition の動作理解問題

回答 
A = list(map(int, input().split()))
idx = hoare_partition(A, 0, len(A) - 1)
print(idx)

6. まとめ

  • QuickSort は 平均 O(N log N) の高速ソート
  • pivot の選び方で性能が大きく変わる
  • partition 法は Lomuto / Hoare の2種類を理解する
  • 実務では in-place + Hoare が多い
  • K番目の要素を求める QuickSelect も必須知識
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?