Keito

© 2024 Keito

技術ブログとポートフォリオ

【Go】クイックソートのサンプルコードを書いてみた

更新日2025/8/2/(土) 04:24
タグ
Goアルゴリズム

概要

  • Goでクイックソートを実装
  • クイックソートは分割統治法を使う高速なソートアルゴリズム。ピボット(基準値)を選んで、それより小さい要素を左、大きい要素を右に分割することを再帰的に繰り返す。

特徴

  • 分割統治法: 配列を分割して再帰的にソート
  • インプレース: 追加メモリ最小限(O(log n))
  • 不安定ソート: 同じ値の相対順序が保持されない
  • 平均的に高速: 平均O(n log n)の時間計算量
  • 比較ソート: 要素同士の比較でソート

動作原理

基本的な流れ:

  1. ピボット(基準値)を選択
  2. ピボットより小さい要素を左側、大きい要素を右側に分割
  3. 左右の部分配列に対して同じ処理を再帰的に実行
  4. 再帰が終われば全体がソート済み

具体例

[3, 7, 8, 5, 2, 1, 9, 4] のソート過程:

初期: [3, 7, 8, 5, 2, 1, 9, 4] Step1: ピボット=4で分割 [3, 2, 1] [4] [7, 8, 5, 9] Step2: 左側[3, 2, 1]を処理(ピボット=1) [1] [3, 2] → [1] [2] [3] Step3: 右側[7, 8, 5, 9]を処理(ピボット=9) [7, 8, 5] [9] → [5] [7, 8] [9] → [5] [7] [8] [9] 完了: [1, 2, 3, 4, 5, 7, 8, 9]

サンプルコード

基本実装

package main import "fmt" // QuickSort は配列を昇順にソート func QuickSort(arr []int) { quickSortHelper(arr, 0, len(arr)-1) } // quickSortHelper は再帰的にソート func quickSortHelper(arr []int, low, high int) { if low < high { // パーティション操作でピボット位置を取得 pi := partition(arr, low, high) // 左右を再帰的にソート quickSortHelper(arr, low, pi-1) quickSortHelper(arr, pi+1, high) } } // partition は配列を分割 func partition(arr []int, low, high int) int { // 最後の要素をピボットに pivot := arr[high] i := low - 1 for j := low; j < high; j++ { if arr[j] <= pivot { i++ arr[i], arr[j] = arr[j], arr[i] } } // ピボットを正しい位置に arr[i+1], arr[high] = arr[high], arr[i+1] return i + 1 }

ランダムピボット版

import "math/rand" // QuickSortRandom はランダムピボットを使用 func QuickSortRandom(arr []int) { quickSortRandomHelper(arr, 0, len(arr)-1) } func quickSortRandomHelper(arr []int, low, high int) { if low < high { // ランダムにピボットを選択 randomIndex := low + rand.Intn(high-low+1) arr[randomIndex], arr[high] = arr[high], arr[randomIndex] pi := partition(arr, low, high) quickSortRandomHelper(arr, low, pi-1) quickSortRandomHelper(arr, pi+1, high) } }

デバッグ用(ステップ表示)

// QuickSortWithSteps はステップごとの状態を表示 func QuickSortWithSteps(arr []int) { fmt.Printf("初期: %v\\\\n", arr) quickSortHelperWithSteps(arr, 0, len(arr)-1, 0) } func quickSortHelperWithSteps(arr []int, low, high, depth int) { if low < high { indent := strings.Repeat(" ", depth) fmt.Printf("%s範囲[%d:%d]を処理\\\\n", indent, low, high) pi := partition(arr, low, high) fmt.Printf("%sピボット=%d, 位置=%d\\\\n", indent, arr[pi], pi) fmt.Printf("%s結果: %v\\\\n\\\\n", indent, arr) quickSortHelperWithSteps(arr, low, pi-1, depth+1) quickSortHelperWithSteps(arr, pi+1, high, depth+1) } }

計算量

時間計算量

  • 最良:O(n log n) - バランスよく分割される場合
  • 平均:O(n log n) - ランダムなデータ
  • 最悪:O(n²) - 常に偏った分割(ソート済みなど)

空間計算量

  • O(log n) - 再帰呼び出しのスタック(平均)
  • O(n) - 最悪の場合のスタック深さ

比較と交換

  • 比較回数: 平均 O(n log n)回、最悪 O(n²)回
  • 交換回数: 平均 O(n log n)回

使いどころ

向いてる場面

  • 大規模データ
  • 平均的な性能重視
  • メモリ制約あり
  • ランダムなデータ

実例:構造体のソート

type Item struct { ID int Value int } // Valueで昇順ソート func SortItemsByValue(items []Item) { quickSortItems(items, 0, len(items)-1) } func quickSortItems(items []Item, low, high int) { if low < high { pi := partitionItems(items, low, high) quickSortItems(items, low, pi-1) quickSortItems(items, pi+1, high) } } func partitionItems(items []Item, low, high int) int { pivot := items[high].Value i := low - 1 for j := low; j < high; j++ { if items[j].Value <= pivot { i++ items[i], items[j] = items[j], items[i] } } items[i+1], items[high] = items[high], items[i+1] return i + 1 }

他のアルゴリズムとの比較

アルゴリズム時間(平均)空間安定性実装難易度
クイックソートO(n log n)O(log n)×普通
マージソートO(n log n)O(n)普通
ヒープソートO(n log n)O(1)×やや難
挿入ソートO(n²)O(1)簡単
選択ソートO(n²)O(1)×超簡単

最適化アイデア

3-way クイックソート

重複要素が多い場合に有効:

func ThreeWayQuickSort(arr []int, low, high int) { if low >= high { return } lt, gt := low, high pivot := arr[low] i := low + 1 for i <= gt { if arr[i] < pivot { arr[lt], arr[i] = arr[i], arr[lt] lt++ i++ } else if arr[i] > pivot { arr[gt], arr[i] = arr[i], arr[gt] gt-- } else { i++ } } ThreeWayQuickSort(arr, low, lt-1) ThreeWayQuickSort(arr, gt+1, high) }

ハイブリッドソート

小規模配列で挿入ソートに切り替え:

func HybridQuickSort(arr []int, low, high int) { if high-low < 10 { // 小規模なら挿入ソート insertionSortRange(arr, low, high) return } if low < high { pi := partition(arr, low, high) HybridQuickSort(arr, low, pi-1) HybridQuickSort(arr, pi+1, high) } }

まとめ

メリット

  • 平均的に高速
  • メモリ効率良い
  • キャッシュ効率良い
  • 実装が比較的シンプル

デメリット

  • 最悪O(n²)
  • 不安定ソート
  • 再帰によるスタック消費
  • ピボット選択が重要

使うべき時

  • 大規模データ
  • 平均性能重視
  • メモリ制約あり
  • 安定性不要

実用的には最も使われるソートアルゴリズムの一つ。多くの言語の標準ライブラリで採用されてる(Go の sort.Slice も内部でクイックソート使用)。