Mokelab Blog

#79 JasmineTeaでマージソートを書いてみた

本記事は Jasmine Tea アドベントカレンダー 2023の18日目です。

学習用プログラミング言語/環境であるJasmineTeaというのがあります。

ブラウザだけでプログラムを書いて実行できます。アカウント登録なしでもお試しできます。こちらから。

マージソート

マージソートはアルゴリズム学習の序盤ででてくるソートです。配列Aを入力として、次のようにソートします。

  1. 1. 配列Aを前半と後半の2つにわける
  2. 2. 前半と後半をそれぞれ(マージ)ソートする
  3. 3. ソート済の前半と後半をマージする

マージする操作は次のようにやります。ソート済のトランプの山を2つ用意して、小さい順にカードを取っていくイメージです。

  1. 1. 前半が空だったら、後半の残りを全部くっつける
  2. 2. 後半が空だったら、前半の残りを全部くっつける
  3. 3. 前半の先頭と後半の先頭をみて、小さいほうを1つとってくっつける
  4. 4. 1番に戻る

よく訓練されたエンジニアはGoを使っているはずなので、Goで書いてみましょう。

package main

import (
  "fmt"
  "math/rand"
  "time"
)
  
func mergeSort(arr []int) []int {
  if len(arr) <= 1 {
    return arr
  }
  mid := len(arr) / 2
  left := mergeSort(arr[:mid])
  right := mergeSort(arr[mid:])
  return merge(0, left, 0, right)
}
  
func merge(l int, left []int, r int, right []int) []int {
  if l == len(left) {
    return right[r:]
  }
  if r == len(right) {
    return left[l:]
  }
  if left[l] <= right[r] {
    return append([]int{left[l]}, merge(l+1, left, r, right)...)
  }
  return append([]int{right[r]}, merge(l, left, r+1, right)...)
}
  
func main() {
  // 計算量を体験するために2倍に増やしながら実行してみる
  size := 10
  for size < 200000 {
    arr := make([]int, size)
    for i := 0; i < size; i++ {
      arr[i] = rand.Intn(10000)
    }
    start := time.Now().UnixMilli()
    sorted := mergeSort(arr)
    _ = sorted
    end := time.Now().UnixMilli()
    fmt.Printf("size=%d Time taken: %v ms\n", size, end-start)
    size *= 2
  }
}

JasmineTeaで書いてみる

JasmineTeaはBASIC風味の文法に配列と関数定義等が入った言語です。関数を定義できるのでがんばってマージソートを書いてみます。

function mergeSort@(arr@)
  size = len(arr@)
  if size = 0 then
    return []
  else if size = 1 then
    return [arr@[0]]
  end if
  center = truncate(size / 2, 0)
  left@ = split@(arr@, 0, center)
  right@ = split@(arr@, center, size)

  left@ = mergeSort@(left@)
  right@ = mergeSort@(right@)
  return merge@(0, left@, 0, right@)
end function

function split@(arr@, i, e)
  if i = e then
    return []
  end if
  return [arr@[i]] + split@(arr@, i + 1, e)
end function

function merge@(l, leftArr@, r, rightArr@)
  if l = len(leftArr@) then
    if r = len(rightArr@) then
      return []
    end if 
    return [rightArr@[r]] + merge@(l, leftArr@, r + 1, rightArr@)
  else if r = len(rightArr@) then
    return [leftArr@[l]] + merge@(l + 1, leftArr@, r, rightArr@)
  else
    if leftArr@[l] < rightArr@[r] then
      return [leftArr@[l]] + merge@(l + 1, leftArr@, r, rightArr@)
    else
      return [rightArr@[r]] + merge@(l, leftArr@, r + 1, rightArr@)
    end if
  end if
end function


print "入力作成中"
startTime = time()
in@ = []
for i = 1 to 100
  in@ = in@ + [random(0, 10000)]
next
endTime = time()
print "時間は"; (endTime - startTime)

print "ソートするよ"
startTime = time()
sorted@ = mergeSort@(in@)
endTime = time()
print sorted@
print "時間は"; (endTime - startTime)

入力を作るところ以外はforを使わず再帰で書いてみました。いくつか気をつける点があります。

割り算に気をつける

JasmineTeaはJavaScriptのように数値型しかありません。なので配列の長さの半分を求める際、 size / 2 と書くと奇数のときに0.5が登場してしまいます。これを避けるために truncate(value, 0) を使います。

配列に要素を追加する際に[]が必要

JasmineTeaの配列は+で連結することができます。オペランドは両方配列である必要があるので、1要素だけ追加したい場合は[]を使って1要素の配列とする必要があります。

動かしてみる

入力の長さを変えながら実行してみます。


// 100個  
入力作成中
時間は32
ソートするよ
(中略)
時間は211
OK

// 200個
入力作成中
時間は45
ソートするよ
(中略)
時間は380
OK

// 400個
入力作成中
時間は60
ソートするよ
(中略)
時間は840
OK

// 800個
入力作成中
時間は83
ソートするよ
(中略)
時間は1954
OK

// 1600個
入力作成中
時間は265
ソートするよ
関数の呼び出し回数が多すぎます(36行目)
OK

1600個でスタックオーバーフローしてるようです。また入力の作成でも線形でない時間がかかっているように見えます。JasmineTeaの制約の中で改善してみましょう。

mergeの再帰をループで書き直してみる

mergeの中でスタックオーバーフローしているので、残念ではありますがforループで書き直してみます。

function merge@(leftArr@, rightArr@)
  l = 0
  r = 0
  result@ = []
  do
    if l = len(leftArr@) then
      if r = len(rightArr@) then
        return result@
      end if
      result@ = result@ + [rightArr@[r]]
      r = r + 1
    else if r = len(rightArr@) then
      result@ = result@ + [leftArr@[l]]
      l = l + 1
    else
      if leftArr@[l] < rightArr@[r] then
        result@ = result@ + [leftArr@[l]]
        l = l + 1
      else
        result@ = result@ + [rightArr@[r]]
        r = r + 1
      end if
    end if
  loop
end function

これで1600個もソートできました。

入力作成中
時間は242
ソートするよ
(中略)
時間は6060
OK

splitもループで書き直してみる

さらに入力を増やし、3200個で試すと今度はsplit()でスタックオーバーフローしました。こちらもループで書き直してみます。

function split@(arr@, i, e)
  result@ = []
  // 引数でiを使っちゃったのでjを使う
  for j = i to e - 1
    result@ = result@ + [arr@[j]]
  next
  return result@
end function

これで少なくとも12800個の要素までソートできました(72秒もかかりました)。

まとめ

JasmineTeaでマージソートを書いてみました。配列のみで書くのは結構たいへんですが、他のアルゴリズムも時間があれば実装してみたいと思います。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。