LoginSignup
2
1

More than 1 year has passed since last update.

【Go】Sorting algorithms (ソートアルゴリズム)

Last updated at Posted at 2021-05-08

Goでプログラミングの基礎を学ぶシリーズ

スクールでは教えてくれないプログラミングの基礎、データ構造とアルゴリズムをGoで学んでいくシリーズです。
そのデータ構造がどのようなものであるかは、理解を助けてくれるサイトを紹介しつつ、簡単な説明に留めさせていただきます。(ご自身でも調べてみてください!)
筆者自身、Exerciseに取り組みながら理解を深めていったので、コードの解説を中心に記事を書いていきたいと思います。

タイトル
#0 はじめに (環境構築と勉強方法)
#1 Pointer, Array, String (ポインタと配列と文字列と)
#2 File operations (ファイル操作)
#3 Linked List (連結リスト)
#4 Stack & Queue (スタックとキュー)
#5 Search algorithms (探索アルゴリズム)
#6 Tree (木構造)
#7 Sorting algorithms (ソートアルゴリズム) ☜ here
#8 String pattern matching algorithms (文字列探索アルゴリズム)

今回は#7 Sorting algorithms (ソートアルゴリズム)です。

Sortとは

データを一定の規則に従って並び替えることです。
昇順や降順など日常生活でも馴染みがありますね。
Sort には多くのアルゴリズムが存在し、時間計算量が比較されます。
以下の記事はソートの様子が可視化されているので、直感的にわかりやすいと思います。

ここでは中でも代表的な Sort のアルゴリズムの Go での実装例をみていきます。
各ソートがどのような流れでソートしていくかは、上のサイトをご参考ください。

Insertion sort (挿入ソート)

リストのソート済みの部分に、新たな要素を適切な位置に挿入することを繰り返してソートを行うアルゴリズムです。
平均計算時間: $O(n^2)$ , 最悪計算時間: $O(n^2)$ と遅いのですが、アルゴリズムは単純で実装しやすいと思います。

func InsertionSort(numbers []int) []int {
    for i := 0; i < len(numbers); i++ {
        next := numbers[i] // ソートされていない部分の先頭

        fmt.Printf("next: %d\n", next)

        var j int
        // ソートされている部分で、 next が挿入されるところまで要素をずらしていく
        for j = i - 1; j >= 0 && next < numbers[j]; j-- {
            numbers[j+1] = numbers[j]

            fmt.Printf("j = %d, sorting... %v\n", j, numbers)
        }
        numbers[j+1] = next // next を挿入

        fmt.Printf("after insert next: %v\n\n", numbers)
    }
    return numbers
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    InsertionSort(numbers)
}

所々にログを仕込んでみました。
ログを見ると、ソートの流れがよくわかるかと思います。

☟ 出力結果

next: 7
after insert next: [7 65 8 32 4 21 10]

next: 65
after insert next: [7 65 8 32 4 21 10]

next: 8
j = 1, sorting... [7 65 65 32 4 21 10]
after insert next: [7 8 65 32 4 21 10]

next: 32
j = 2, sorting... [7 8 65 65 4 21 10]
after insert next: [7 8 32 65 4 21 10]

next: 4
j = 3, sorting... [7 8 32 65 65 21 10]
j = 2, sorting... [7 8 32 32 65 21 10]
j = 1, sorting... [7 8 8 32 65 21 10]
j = 0, sorting... [7 7 8 32 65 21 10]
after insert next: [4 7 8 32 65 21 10]

next: 21
j = 4, sorting... [4 7 8 32 65 65 10]
j = 3, sorting... [4 7 8 32 32 65 10]
after insert next: [4 7 8 21 32 65 10]

next: 10
j = 5, sorting... [4 7 8 21 32 65 65]
j = 4, sorting... [4 7 8 21 32 32 65]
j = 3, sorting... [4 7 8 21 21 32 65]
after insert next: [4 7 8 10 21 32 65]

Selection sort (選択ソート)

リストのソートされていない部分から、最小値(or最大値)を持つ要素を探して、その値をソートされていない部分の先頭に移動(交換)することを繰り返してソートを行うアルゴリズムです。
平均計算時間: $O(n^2)$ , 最悪計算時間: $O(n^2)$ と遅いのですが、アルゴリズムは単純で実装しやすいと思います。

func SelectionSort(numbers []int) []int {
    for i := 0; i < len(numbers); i++ {
        min := i
        // ソートされていない部分での最小値の index を探す
        for j := i + 1; j < len(numbers); j++ {
            if numbers[min] > numbers[j] {
                min = j
            }
        }
        // ソートされていない部分の先頭と、ソートされていない部分での最小値を入れ替える
        numbers[i], numbers[min] = numbers[min], numbers[i]

        fmt.Printf("i = %d, %v\n", i, numbers)
    }
    return numbers
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    SelectionSort(numbers)
}

☟ 出力結果

i = 0, [4 65 8 32 7 21 10]
i = 1, [4 7 8 32 65 21 10]
i = 2, [4 7 8 32 65 21 10]
i = 3, [4 7 8 10 65 21 32]
i = 4, [4 7 8 10 21 65 32]
i = 5, [4 7 8 10 21 32 65]
i = 6, [4 7 8 10 21 32 65]

Bubble sort (バブルソート)

隣り合う要素の大小を比較しながらソートしていくアルゴリズムです。
平均計算時間: $O(n^2)$ , 最悪計算時間: $O(n^2)$ と遅いのですが、アルゴリズムは単純で実装しやすいと思います。

func BubbleSort(numbers []int) []int {
    for i := 0; i < len(numbers)-1; i++ {
        for j := 0; j < len(numbers)-i-1; j++ {
            if numbers[j] > numbers[j+1] {
                numbers[j], numbers[j+1] = numbers[j+1], numbers[j]
            }
        }
        fmt.Printf("i = %d, %v\n", i, numbers)
    }
    return numbers
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    BubbleSort(numbers)
}

☟ 出力結果

i = 0, [7 8 32 4 21 10 65]
i = 1, [7 8 4 21 10 32 65]
i = 2, [7 4 8 10 21 32 65]
i = 3, [4 7 8 10 21 32 65]
i = 4, [4 7 8 10 21 32 65]
i = 5, [4 7 8 10 21 32 65]

Heap sort (ヒープソート)

ヒープとは、データ構造の一種で、木構造(ツリー構造)のうち、親要素が子要素より常に大きい(あるいは小さい)という条件を満たすもの。
引用: IT用語辞典 e-Words

Heap sortBinary Heap (二分ヒープ木) を用いて行うソートのアルゴリズムです。
ヒープの構造を利用するだけで、Tree を実装する必要はありません。
平均計算時間: $O(n\log{n})$ , 最悪計算時間: $O(n\log{n})$ であり、上の3つよりも速いソートアルゴリズムになります。

func HeapSort(numbers []int) []int {
    // heap を形成する
    fmt.Printf("Build Heap\n")
    for i := len(numbers)/2 - 1; i >= 0; i-- {
        heapify(numbers, len(numbers), i)
    }

    // heap から要素を一つずつ取り出す (ソートしていく)
    fmt.Printf("\nHeap Sort")
    for i := len(numbers) - 1; i > 0; i-- {
        numbers[i], numbers[0] = numbers[0], numbers[i]

        fmt.Printf("\ni = %d, sorting... %v\n", i, numbers)

        heapify(numbers, i, 0)
    }
    return numbers
}

// i 番目の要素を root とする Tree をヒープ化する.
// len は heap size, root は numbers の index
func heapify(numbers []int, len, root int) {
    largest := root // 最大の要素の index を i (root) としておく
    left := 2*root + 1
    right := 2*root + 2

    if left < len && numbers[left] > numbers[largest] { // left child が root より大きい場合
        largest = left
    }
    if right < len && numbers[right] > numbers[largest] { // right child が root より大きい場合
        largest = right
    }
    if largest != root { // 最大の要素が root ではないとき
        numbers[root], numbers[largest] = numbers[largest], numbers[root] // root の要素と最大の要素を入れ替える

        fmt.Printf("(%d %d %d) heapify... %v\n", root, left, right, numbers)

        heapify(numbers, len, largest)
    }
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    HeapSort(numbers)
}

☟ 出力結果

Build Heap
(2 5 6) heapify... [7 65 21 32 4 8 10]
(0 1 2) heapify... [65 7 21 32 4 8 10]
(1 3 4) heapify... [65 32 21 7 4 8 10]

Heap Sort
i = 6, sorting... [10 32 21 7 4 8 65]
(0 1 2) heapify... [32 10 21 7 4 8 65]

i = 5, sorting... [8 10 21 7 4 32 65]
(0 1 2) heapify... [21 10 8 7 4 32 65]

i = 4, sorting... [4 10 8 7 21 32 65]
(0 1 2) heapify... [10 4 8 7 21 32 65]
(1 3 4) heapify... [10 7 8 4 21 32 65]

i = 3, sorting... [4 7 8 10 21 32 65]
(0 1 2) heapify... [8 7 4 10 21 32 65]

i = 2, sorting... [4 7 8 10 21 32 65]
(0 1 2) heapify... [7 4 8 10 21 32 65]

i = 1, sorting... [4 7 8 10 21 32 65]

流れを図にすると以下のようになります。
スクリーンショット 2021-05-07 23.40.00.png
まずはランダムな配列のヒープ化を行います。
heapify では root, left, right から最大の値を root と入れ替え、入れ替えが行われた index について再帰的に heapify を実行するという流れになります。
スクリーンショット 2021-05-07 23.40.28.png
ヒープ化ができたらソートをしていきます。
ヒープ化により root が最大値になっているので、root と配列の最後尾を入れ替え、入れ替えが行われた後に再度ヒープ化( heapify )を実行します。
それの繰り返しにより、ソートされていきます。

Quick sort (クイックソート)

ピボットという基準となる値をランダムに選択し、リストの先頭からピボットよりも大きいか小さいかで両側のグループに分割し、それを繰り返してソートを行うアルゴリズムです。
平均計算時間: $O(n\log{n})$ , 最悪計算時間: $O(n^2)$ であり、Wikipediaによると、
他のソート法と比べて、一般的に最も高速だといわれているが対象のデータの並びやデータの数によっては必ずしも速いわけではない ようです。

func QuickSort(numbers []int) []int {
    if len(numbers) < 2 {
        return numbers
    }

    fmt.Printf("\nsorting... %v\n", numbers)

    left, right := 0, len(numbers)-1
    pivot := numbers[right]

    for i := 0; i < right; i++ {
        if numbers[i] < pivot {
            numbers[i], numbers[left] = numbers[left], numbers[i]
            left++

            fmt.Printf("p = %d, left = %d, %v\n", pivot, numbers[left], numbers)
        }
    }
    numbers[left], numbers[right] = numbers[right], numbers[left]

    fmt.Printf("p = %d, left = %d, %v\n", pivot, numbers[left], numbers)

    QuickSort(numbers[:left])
    QuickSort(numbers[left+1:])
    return numbers
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    QuickSort(numbers)
}

☟ 出力結果


sorting... [7 65 8 32 4 21 10]
p = 10, left = 65, [7 65 8 32 4 21 10]
p = 10, left = 65, [7 8 65 32 4 21 10]
p = 10, left = 32, [7 8 4 32 65 21 10]
p = 10, left = 10, [7 8 4 10 65 21 32]

sorting... [7 8 4]
p = 4, left = 4, [4 8 7]

sorting... [8 7]
p = 7, left = 7, [7 8]

sorting... [65 21 32]
p = 32, left = 65, [21 65 32]
p = 32, left = 32, [21 32 65]

ソートの流れは最初にリンクした参考サイトの通りです。

Merge sort (マージソート)

ソートされていないリストを2つのリストに分割して、それぞれをソートした後、それらをマージしてソート済みのひとつのリストを作るアルゴリズムです。
平均計算時間: $O(n\log{n})$ , 最悪計算時間: $O(n\log{n})$

func MergeSort(numbers []int) []int {
    if len(numbers) < 2 {
        return numbers
    }
    middle := len(numbers) / 2
    return merge(MergeSort(numbers[:middle]), MergeSort(numbers[middle:]))
}

func merge(left, right []int) []int {
    var merged []int
    fmt.Printf("\nmerge %v %v\n", left, right)

    // 左のリストと右のリストの要素を比較し、小さい方を merged に追加する
    for i := 0; len(left) > 0 && len(right) > 0; i++ {
        if left[0] < right[0] {
            merged = append(merged, left[0])
            left = left[1:]
        } else {
            merged = append(merged, right[0])
            right = right[1:]
        }
        fmt.Printf("i = %d, %v\n", i, merged)
    }

    // リストに残っている要素を merged に追加
    for _, l := range left {
        merged = append(merged, l)
    }
    for _, r := range right {
        merged = append(merged, r)
    }

    fmt.Printf("sorted %v\n", merged)
    return merged
}

func main() {
    numbers := []int{7, 65, 8, 32, 4, 21, 10}
    MergeSort(numbers)
}

☟ 出力結果


merge [65] [8]
i = 0, [8]
sorted [8 65]

merge [7] [8 65]
i = 0, [7]
sorted [7 8 65]

merge [32] [4]
i = 0, [4]
sorted [4 32]

merge [21] [10]
i = 0, [10]
sorted [10 21]

merge [4 32] [10 21]
i = 0, [4]
i = 1, [4 10]
i = 2, [4 10 21]
sorted [4 10 21 32]

merge [7 8 65] [4 10 21 32]
i = 0, [4]
i = 1, [4 7]
i = 2, [4 7 8]
i = 3, [4 7 8 10]
i = 4, [4 7 8 10 21]
i = 5, [4 7 8 10 21 32]
sorted [4 7 8 10 21 32 65]

ソートの流れは最初にリンクした参考サイトの通りです。

💻 Exercise

1~500 の数字で構成される、要素数 500 のランダムな配列を Insertion sort と Quick sort でソートしましょう。
その実行速度を比較してみましょう。

☟ 解答例

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func initList(n int) []int {
    var numbers []int
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < n; i++ {
        numbers = append(numbers, rand.Intn(500))
    }
    return numbers
}

func InsertionSort(numbers []int) []int {
    for i := 0; i < len(numbers); i++ {
        next := numbers[i]
        var j int
        for j = i - 1; j >= 0 && next < numbers[j]; j-- {
            numbers[j+1] = numbers[j]
        }
        numbers[j+1] = next
    }
    return numbers
}

func QuickSort(numbers []int) []int {
    if len(numbers) < 2 {
        return numbers
    }
    left, right := 0, len(numbers)-1
    pivot := numbers[right]

    for i := 0; i < right; i++ {
        if numbers[i] < pivot {
            numbers[i], numbers[left] = numbers[left], numbers[i]
            left++
        }
    }
    numbers[left], numbers[right] = numbers[right], numbers[left]

    QuickSort(numbers[:left])
    QuickSort(numbers[left+1:])
    return numbers
}

func main() {
    numbers := initList(500)
    start := time.Now()
    InsertionSort(numbers)
    runtime := time.Since(start)
    fmt.Printf("Insertion sort ▶︎ runtime: %v\n", runtime)

    numbers = initList(500)
    start = time.Now()
    QuickSort(numbers)
    runtime = time.Since(start)
    fmt.Printf("Quick sort ▶︎ runtime: %v\n", runtime)
}

☟ 出力結果

Insertion sort ▶︎ runtime: 44.611µs
Quick sort ▶︎ runtime: 23.602µs

おわりに

Exerciseの解答例はあくまで例なので、もっといい書き方あるよ!という方、ぜひコメントをお寄せください!
説明についても、筆者自身が初心者であるため、ご指摘や補足は大歓迎でございます。

株式会社Link Sportsでは、あらゆるスポーツを楽しむ人たちに送る、チームマネジメントアプリを開発しています。
未経験でも経験豊富なエンジニアの方でも、スポーツ好きなら活躍できる環境があります!
絶賛エンジニア募集中です!
Wantedly ▶︎ https://www.wantedly.com/projects/324177
Green ▶︎ https://www.green-japan.com/job/82003

次回は、データ構造とアルゴリズム#8 String pattern matching algorithms (文字列探索アルゴリズム)です。

2
1
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
2
1