t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

そんなに沢山のソートアルゴリズム、意味あるの?って方へ

みなさん本日は情報処理試験お疲れ様でした。
(私は今回申し込みすらしてないけど)

本当は試験前にこの記事を書きたかったんだけど文章悩んでるうちに試験が終わってしまった。まぁ今回ダメだった方、決意を新たにした方もいると思うので一応公開するか。

さて、何かを学習しようとしたときに障害になる考え方がある。
「それ、意味あるの?」ってやつだ。

試験のためと割り切れる方は良いけど、試験でしか役に立たないような知識を無理やり頭に詰め込むのってなんか空しい。

そこで今回は情報処理試験で出てくるソートアルゴリズムの存在意義について書こうと思う。まずもって数が多すぎる。そんなにいる?意味ある?

そもそも自前でソートなんてする?

まあ普通はプログラミング言語にはソート機能が組み込まれていたり、専用のライブラリなんかも充実してるので、いまどき自前でソートなんてやらんでしょって意見もある。

ところが、そうでもない。たとえばVBAでプログラムを組んでてデータをオブジェクトでコレクションに入れて管理しようと思うと、コレクションにはソート機能が無いし、そもそもオブジェクトなのでどのプロパティーをキーにソートするのかなど細かく調整したい場合もある。それで、コレクションをソートするプログラムを組んだことがある。

各ソートの存在意義

選択ソート

まず一番小さい数を探して、次に二番目に小さい数を探して。。というソート。すこぶる遅い。
このソートの存在意義は、実装がとにかく簡単ってことと、前提知識なしに誰でも思いつくソートであること。

挿入ソート

データを適切な位置に挿入していくソート。普段は遅いけど、ほぼ整列済のデータに対しては数あるソートの中でも最も高速になることがある。この特性が挿入ソートの存在意義。実装も簡単。
クイックソートで大体並んで来たら挿入ソートに切り替えると高速になるそうだ。

バブルソート

隣どうしのデータを比較して交換するソート。このソートの存在意義は、直観的に理解できることと実装が簡単であることだ。いくら自前ソートの機会が減ってるとはいえプログラマを自認するならバブルソートくらいは書けないとね。

クイックソート

名前どおり高速なソート。このソートの存在意義はとにかく速いことだが、アルゴリズムは少し複雑になる。しかし常に最速かというとそんなことはなく、データの特性によってはすこぶる遅くなる。

シェルソート

改良版の挿入ソート。かなり高速なソートで、データの特性によって並はずれて遅くなるということもない。比較するデータの間隔のチューニングが難しい。

ヒープソート

配列を木構造に見立てたソート。このソートもかなり高速で、データの特性によって並はずれて遅くなるということもない。

マージソート

データを分割して分割して分割してこれ以上分割できなくなったところで統合して統合して統合してという順でソートしていく。統合時にソートされる。

このソートは比較的メモリ使用量が大きいけれどかなり高速である。

加えて重要なのは安定ソートであるということ。安定ソートとは、たとえば氏名で並び替えた後に都道府県で並び替えると、各都道府県データのなかでは氏名順の並びをキープしているようなソートのこと。

クイックソートシェルソートヒープソートなどの他の高速ソートは不安定ソートなので、都道府県順で並び替えると氏名の順はぐちゃっとなってしまうけど、マージソートは崩れない。

他の安定ソートとしてはバブルソート、挿入ソートがある。遅いけど。

高速かつ安定ソートで、さらにデータの並び順で速度が変わることがないと三拍子そろった個人的には最強ではないかと思うソート。

ただまあクイックソートほど速くはないし、メモリを食うソートなのでこれもケースバイケース。

まとめ

確かに安定・高速・簡潔・元の並びに依存しないと四拍子そろった最強のソートがあればこんなにたくさんソートを覚える必要がなかったかもしれないが、逆にいえばこの多様性こそがプログラミングの学習において重要であるともいえる。

「並び替え」というひとつの課題でこれだけ多彩な回答が存在するというのは、自分が作ったプログラムに対してもより良い方法があるのではないかという考察のきっかけになる。

今回取り上げたソートはすべて基本情報の出題範囲であるが、それぞれに長所・短所があって面白い。

最後にこの記事の執筆のきっかけになったサイトを紹介する。
www.toptal.com

これを見て、シェルソートはやっ!!まじで!?と思ったのがきっかけ。
このソートは完全にノーマークだった。

VBA Rangeオブジェクトはシートを操作するエージェント

VBAで扱える代表的なExcelのオブジェクトにWorkbook、Worksheet、Rangeなどがある。

  • ひとつのブックにはひとつのWorkbookオブジェクトが対応している。
  • ひとつのシートにはひとつのWorksheetオブジェクトが対応している。

ではひとつのセルに対応する固有のオブジェクトは?



5秒、4、3、2、1、はい。




Cellと答えたあなた。ぶっぶー。

Rangeと答えたあなた。ぶっぶっぶー。

実はひとつのセルに対応する固有のオブジェクトは存在しないってのが答え。

「固有の」ってとこがミソ。

まずCellというオブジェクトは存在せず、WorksheetオブジェクトのCellsプロパティが返すのは全セルを表すRangeオブジェクトである。このことは以下の記事で詳しく書いた。
thom.hateblo.jp

そもそもセルがいくつあるか考えてみよう。Excel2007以降、扱えるセル範囲は16,384桁1,048,576行となっている。つまり単純に掛け算して約170個のセルが存在することになる。これらにそれぞれ対応するオブジェクトが存在するとしたらExcelだけでPCのメモリを食いつぶしてしまう。

じゃあいったいRangeとは何なのか。

Rangeとはシートを操作するエージェント(代理人)のようなものである。

ひとつ実験をしてみよう。
オブジェクトが一致するかどうかを比較する方法として、Is演算がある。
もしSheets(1).Range("A1")がひとつの固有なRangeオブジェクトを返すなら、以下はTrueになるはずだ。

Sub hoge()
    MsgBox Sheets(1).Range("A1") Is Sheets(1).Range("A1")
End Sub

実際に実行してみるとわかるが、上記のマクロはFalseになる。
同じセルを指してるのになんで!?と思うかもしれないけれど、これがエージェントと表現した所以。

つまりRangeとはWorksheetに代わってセルを操作する代理人(エージェント)である。

こんな風に、同じA1セルを管理していても、同じエージェントであるとは限らない。
f:id:t-hom:20170416013829p:plain

だからこうやってIsで比較すると。
f:id:t-hom:20170416014104p:plain

違うんだ!と。
f:id:t-hom:20170416014354p:plain

つまりWorksheetオブジェクトのRangeプロパティは評価するたびに新しいRangeオブジェクトを生成しているのだ。

ちなみに変数に入れてあげれば当然一致する。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r Is r
End Sub

これはTrue。

ここで面白いのが、以下のようにOffsetをかますと不一致になるという点。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r Is r.Offset(0, 0)
End Sub

Offset(0, 0)ということは実質的におなじセルを指してるのに不一致。
つまりOffsetは内部で新しいRangeエージェントを作って返してるようだ。

以前クラスモジュール内で自身と同じ型の別インスタンスを返すという処理をやったけど、まさにあれと同じ。
thom.hateblo.jp

Offsetプロパティは内部でRangeをNewして返してるイメージ。実際に中身見たわけではないので具体的な実装は知らないけど。

なお、同じセルを指しているかどうかを調べたかったらAddressプロパティをイコールで比較してあげればよい。

Sub hoge()
    Dim r As Range
    Set r = Sheets(1).Range("A1")
    MsgBox r.Address = r.Offset(0, 0).Address
End Sub

これはTrueになる。

ちなみにシートの場合はどうかというと、

Sub hoge()
    MsgBox Sheets(1) Is Sheets(1)
End Sub

このように普通にIsで比較してもTrueが返される。

以下のように表現を変えてみても同じくTrue。

Sub hoge()
    MsgBox Sheets(1) Is Sheets("Sheet1")
End Sub

以下のように一旦別シート指してから戻しても同じようにTrueを返す。

Sub hoge()
    Dim sh As Worksheet
    MsgBox Sheets(1) Is Sheets(1).Next.Previous
End Sub

つまりこれが固有オブジェクトであるということ。

ひとつのセルに対応する固有のオブジェクトが存在しないという意味が伝わっただろうか。

オブジェクト指向における「メッセージ」とは何か

現在主に認知されているオブジェクト指向

私がオブジェクト指向というとき、それは基本的にC++から始まりJavaに受け継がれた、あのオブジェクト指向を指す。つまり、カプセル化、継承、多態性で説明されるアレだ。

VBAは継承、多態性のサポートがほとんど無いけれど、その中心となる考え方はC++オブジェクト指向から受け継いだものだと思われる。

結論から言ってしまうと、このようなC++系統のオブジェクト指向において、メッセージという言葉は単にプロシージャ呼び出し※のことだ。
VBA用語です。C++なら関数呼び出し。

つまりオブジェクト同士がメッセージをやりとりするというのは単に比喩表現であって、実態としてはプロシージャ呼び出しとその戻り値の受け取りに過ぎないってこと。

冒頭で述べたオブジェクト指向言語においてメッセージという比喩は混乱の元になるので使わないほうが良いと思う。UMLを学習するとシーケンス図で出てきてしまうけど。。

もうひとつのオブジェクト指向

さて、冒頭で「あのオブジェクト」と言ったのは、そうじゃないオブジェクト指向があるから。その代表はSmalltalk(スモールトーク)である。

最近以下の書籍でSmalltalkを少し齧っている。

SMALLTALKで学ぶ オブジェクト指向プログラミングの本質

SMALLTALKで学ぶ オブジェクト指向プログラミングの本質

しかし、はっきり言って、まえがきからして何を書いているのがサッパリわからない。。理解できないのは説明が悪いというより、そもそもこのSmalltalkという言語の特殊性が原因かもしれない。読めそうなところからぼちぼちトライしているところ。

さて、Smalltalkという言語はまさにオブジェクト同士のメッセージのやりとりというアイデアを比喩ではなくそのまま文法に取り入れた言語である。

たとえば 1 + 2 という計算式は、一般的に演算子「+」が「1」 と「2」 を取って計算すると認識されているが、Smalltalkでは違う。
「1」というオブジェクトに「+ 2」というメッセージを送信すると考えるのである。

またSmalltalkにはIfやforなどの制御構造が構文ではなくメッセージになっている。
真偽値オブジェクトに対し、IfTrue、IfFalseなどのメッセージを送るイメージ。

ってもまだうまくイメージできないのだけど。

たとえば以下のVBAコードは、

If 1 + 2 = 3 Then Debug.Print "Hello, world"

Smalltalkではこうなる。

(1 + 2 == 3) ifTrue: [Transcript show: 'Hello, world.']

1 + 2 == 3が評価されるとVBAでいうところのBoolean型になり、この真偽オブジェクトにifTrueメッセージを送る。その引数(?)に手続きを渡している。。という解釈であってるのだろうか。

このように、Smalltalkはすべてがオブジェクトとメッセージで成り立つ言語。そしてこちらのオブジェクト指向における「メッセージ」がまさに本来の「メッセージ」なのだ。

何いってるかわかんないだろう?実は私もだ。HA HA HA
ずっとC++系統のオブジェクト指向を学んできた私にとってSmalltalkは、まるで宇宙人だ。言葉どころか、身振り手振りすら通じない。

さっぱり分からない。実に面白い。

ガリレオ [DVD]

ガリレオ [DVD]

オブジェクト指向は一つではない

つまりオブジェクト指向言語には大きく分けてSmalltalkから始まった「オブジェクトとメッセージ」を中心に据えたものと、C++から始まった「カプセル化、継承、多態性」を中心に据えたものの2種類に分かれ、これらは全くといっていいほど異なっている。

もともとその源流となったのはSimulaという言語だった。Simulaはプログラミングにクラスやオブジェクトの概念を導入した初めての言語だ。しかしこのときにはまだオブジェクト指向という言葉自体は無かったそうだ。

Smalltalkを設計したアラン・ケイさんと、C++を設計したビャーネ・ストロヴストルップさんはどちらもSimulaという言語を見て「これは画期的なアイデアだ!」と思ったに違いない。しかしその着眼点は全く別のものだった。

こうして世の中に2つのオブジェクト指向が生まれ、「オブジェクト指向とは」という説明における大きな混乱の元になっている。メッセージという言葉はその典型で、これは明らかにSmalltalk系統のオブジェクト指向の考え方だ。すべてをオブジェクトとして扱うというのもSmalltalkの話であってC++系統のオブジェクトではそうではない。

これとは別の評価軸として、クラスベースかプロトタイプベースかという分類もあるので、オブジェクト指向が2種類であると言い切るにはちょっと複雑すぎるのだけど。

まとめ

個人的には、C++系統のオブジェクト指向の説明においてメッセージという用語はもう使わない方が良いんじゃないかと思う。私は長いこと、メソッドに渡される実引数のことと勘違いしていたが、それもメッセージという用語に「送信」というイメージがつきまとうためである。

C++系統のオブジェクト指向でプロシージャ呼び出しをメッセージ送信に見立てるのはちょっと無理があるんじゃないかな。

オブジェクト指向の本質はカプセル化 ~継承も多態性も脇役にすぎない

f:id:t-hom:20170328063503p:plain
ずいぶんと挑発的なタイトルをつけてしまったが、継承や多態性が重要でないなどというつもりはない。
ただオブジェクト指向の本質はどれかと言われると、やっぱりカプセル化だろうなと。

オブジェクト指向とは

オブジェクト指向とは、データや機能をまとめて「モノ」として扱いましょうという方針を言う。

オブジェクトってのはモノのこと。フランス語の「オブジェ」のほうがイメージしやすいかも。

指向ってのは「はじめからその方向を指して向かうこと」という意味。つまり「方針」にしたがって進むこと。

オブジェクト指向の本質はカプセル化

カプセル化とは、データや、そのデータを操作する機能を、まるでカプセルに入れるみたいに、ひとつにまとめて扱うための仕組みのこと。つまりオブジェクト指向そのもの。

これがないと始まらない。

ここでオブジェクト指向を学習したことのある方はカプセル化とは隠ぺいのことだと思い込んでいる方もいると思う。隠ぺいというのはオブジェクトの中にあるデータや機能を好き勝手に使えないように制限をかけるっていう意味。中身を隠して見えなくしちゃうから隠ぺい。都合の悪い事実を隠蔽(いんぺい)するとか言うときのアレと同じ隠ぺいだけど悪い意味はない。

日本語版のWikipediaにだってカプセル化と隠ぺいを混同しているように思える。

じゃあ英語版はというと、General definitionという見出しで以下のように書かれている。(Google翻訳結果)

カプセル化とは、そのデータを操作するメソッドとデータのバンドルを指します。

メソッドといのうは機能、バンドルってのは「束(たば)」のこと。つまりデータと機能を束ねると書いてある。これが本質だ。

続いてカプセル化はデータ隠ぺいに使用されると書かれているが、重要なのはカプセル化した結果として隠ぺいすること可能になるということであって、隠ぺいそのものをカプセル化と呼ぶわけではないこと。

隠ぺいもひじょーーーに有用な概念であるが、だからといってカプセル化の本質かというと違う。

データや機能をまとめて「モノ」として扱いましょうという方針に沿ってプログラミングするのに隠ぺいが必須かといえば、そんなことはない。内部のデータをバーンとPublicで公開しちゃってもモノはモノに違いない。

さて、さっきから色々書いてるけど、実際に言ってることはひとつ。
データや機能をまとめて「モノ」として扱いましょう。
つまりオブジェクト指向に必要な考え方は、これだけ。

ちなみにさっきからデータ「と」機能じゃなくて、データ「や」機能と言ってるのは、データだけまとめたり、機能だけまとめたりも出来るから。

オブジェクト指向の混乱を増長させる言葉

オブジェクト指向は複雑で分かりにくいという意見がある。
カプセル化・継承・ポリモーフィズム(多態性)・委譲・インターフェース・隠ぺい・デザインパターンUMLオブジェクト指向の書籍を開くとこのようなちょっと意味不明な言葉がわんさか出てくる。

これらがごちゃっとなって「難しい・わからない」というイメージを一層強めているんじゃないかと思う。

オブジェクト指向の本質を理解するだけなら、最初からこんな訳のわからない(と思われている)大量の言葉を覚える必要ない。いつか必要になったときに覚えれば。

ただ一つ、「カプセル化」だけ覚えておこう。いますぐ。さぁ。

VBAだからわかるオブジェクト指向の本質

オブジェクト指向の三大要素と呼ばれている機能がある。
カプセル化…データと機能をまとめること
・継承…別のオブジェクトから機能や特徴を受け継いで新しいオブジェクトを作ること
多態性…同じ命令を出してもオブジェクトによって異なる振る舞いをさせること
の3つだ。

継承や多態性は確かに便利である。もはやそれ無しではオブジェクト指向言語とは言えないほどに。

ところがVBAはというと、継承と多態性をまともにサポートしていない。つまり言語の機能としては、三大要素のうちオブジェクト指向の本質である「カプセル化」しか存在しないのである。

だからこそオブジェクト指向の本質がカプセル化であるという説明を(どこかで)読んだときすぐにピンと来た。たぶんオブジェクト指向Javaでだけやってたら私は今でも三大要素は等しく重要だと思いこんでいたことだろう。

カプセル化さえあれば、継承と多態性がなくてもオブジェクト指向のメリットを享受できるのだ。

継承も多態性も使わない、そして隠ぺいすらしない楽々オブジェクト指向

オブジェクト指向は複雑であると思われているけれど、それはオブジェクト指向のセオリーに完璧に従った場合の話。

私は最初からそんなややこしいことをする必要は無いと思っている。まずはデータと機能をモノとして扱えることの楽さを味わって「オブジェクト指向スゲー!」となるのが先決だ。いろんな機能はそのあとに学んでいけば良いと思う。

まずデータと機能をまとめるというのがどういうことなのか、具体的なやり方は以下の入門記事を参照してほしい。
thom.hateblo.jp

それから、上を読み終えた方はこの記事がおススメ。ゆるーくオブジェクトを使って楽をしている。
thom.hateblo.jp

VBA Application.OnKeyを使い、F1キーで任意のアクティブブックのメインプロシージャを実行させる

今回の記事はF1キーを押したときに、今使用しているブックによって実行するプロシージャを分けるというもの。

執筆のきっかけになったのはこちらの記事のコメント欄のやりとり。
chemiphys.hateblo.jp

OnKeyマクロを記載したブックを閉じた後,そのショートカットを実行すると,OnKeyマクロを記載していたブックを自動的に開いてそのマクロを実行しようとする

なるほど、特定ブックでのみ使用するマクロをOnKey登録し、解除を忘れる(あるいは失敗する)と上記のように想定外の事象になってしまうデメリットがある。

F1キーを特定ブックのマクロに紐づけてしまうと、Excelアプリ全体に作用してしまい、F1キーはそのブックのマクロ実行専用になってしまう。

では、発想を変えて特定ブックに紐づかない実行キーを作ってしまおうと考えたのが今回のネタ。

この記事ではその仕掛けはPersonal.XLSBに作成するが、配布を考慮するならアドイン化しておくのも良いと思う。

まずPersonal.XLSB(個人用マクロブック)のThisWorkbookシートモジュールに以下のコードを記述する。

Private Sub Workbook_Open()
    Application.OnKey "{F1}", "'MacroLauncher ""F1_Key""'"
End Sub

つまりこれで、Excelを起動した際にF1キーにMacroLouncherプロシージャ呼び出しが登録される。
文字列"F1_Key"はMacroLouncherの引数で、実際に呼び出すブックごとのマクロ名を指す。

※本当はF1というプロシージャにしたかったけど名前が不適切となるのでやめた。恐らくキーコード定数と被っているせいかと思う。

わざわざ引数にしたのは後からF3、F4なども簡単に追加できるようにするため。
※F2は個人的にExcel本来のショートカットとしてよく使うのでマクロで上書きするわけにはいかない。

次にPersonal.XLSBに標準モジュールを追加し、以下のコードを記述する。

Sub MacroLauncher(macro_name)
    Dim QuotedBookName As String
    QuotedBookName = "'" & ActiveWorkbook.Name & "'"
    
    On Error Resume Next
        Application.Run QuotedBookName & "!" & macro_name
    On Error GoTo 0
End Sub

こうするとF1キーを押した際にMacroLouncherを経由してApplication.RunでアクティブブックのF1_Keyプロシージャが呼び出されるようになる。On Errorで囲んでいるのでマクロ登録がないブックでは何も起きない。登録がない旨のメッセージを出すようにしても良いかもしれない。

これで準備は整った。
あとはショートカットを使いたいブックごとにF1_Keyプロシージャを作り、それをメインコードにするか、そこからメインコードを呼び出すようにすれば完成。

ただこのテクニックはきわめて個人的な改善なので、チームが共通で使うようなブックだと、なかなか使えない。
F1_Keyプロシージャを作らせてもらうか、それともしれっと作ってしまうか、オフィシャルにして皆にも使ってもらうかという方向になるかと。

あと発展系としてはシートごとにF1キーの意味を変えたい場合にも使えると思う。
Application.Runの引数文字列の書式は 'ブック名'!シートオブジェクト名.マクロ名 である。(試してないけど、たぶん。)

VBA ヒープソートを実装 ~関数を沢山作って複雑な問題に対処する

私は長らくヒープソートというものが理解できなかったのだが、ついに今日、なんとか動くところまで実装できたので紹介しようと思う。

ヒープソートを理解しようと思ったら、実装の前にまずヒープのノードが入れ替わるイメージを理解しておく必要がある。
そこで、こちらの動画を視聴した。

ヒープソート

めっちゃわかりやすい!

ん、理屈は分かったけど、これをいったいどうやって実装すれば。。

ということでもう一本動画を視聴。

[ソート4]ヒープソート(Heap Sort):解説

最初のほう早くて分かりにくいけど、1分10秒あたりから配列を使った実装の詳細の説明に入るのでしっかり視ておく。

さて、ヒープを配列に導入すると、親と子を指すインデックスには次の式が成り立つ。
左の子のインデックス = 親のインデックス×2+1
右の子のインデックス = 左の子のインデックス + 1

これがメインコードに登場するとごちゃっとするので、関数化してしまおう。

Function GetRightChildIndex(parent_index) As Long
    GetRightChildIndex = GetLeftChildIndex(parent_index) + 1
End Function

Function GetLeftChildIndex(parent_index) As Long
    GetLeftChildIndex = parent_index * 2 + 1
End Function

それから、親を左の子と入れ替える処理、親を右の子と入れ替える処理というのも解説に登場したので、これもプロシージャ化する。

Sub SwapParentRightChild(arr, parent_index)
    Dim tmp: tmp = arr(parent_index)
    arr(parent_index) = arr(GetRightChildIndex(parent_index))
    arr(GetRightChildIndex(parent_index)) = tmp
End Sub

Sub SwapParentLeftChild(arr, parent_index)
    Dim tmp: tmp = arr(parent_index)
    arr(parent_index) = arr(GetLeftChildIndex(parent_index))
    arr(GetLeftChildIndex(parent_index)) = tmp
End Sub

子より親の方が小さければ、入れ替えるという処理をするが、このとき、両方の子が親より大きい場合、より大きい方を選択するという処理があった。
これ、言い換えると、親・左の子・右の子のうち、一番大きいものと親を入れ替えるということになる。親が一番なら入れ替えは発生しないが、とにかく誰が一番大きいかを判定する関数があると良い。

この関数は、列挙型定数でParent、LeftChild、RightChildのいずれかを返すようにしよう。

ということでまずは列挙型を作成。

Enum Role
    Parent
    LeftChild
    RightChild
End Enum

次にこれを返す関数を作成。

Function WhoIsBiggest(arr, parent_index, tail) As Role
    Const INIFINITESIMAL As Long = -2147483648#
    Dim p: p = arr(parent_index)
    Dim rc
    If GetRightChildIndex(parent_index) <= tail Then
        rc = arr(GetRightChildIndex(parent_index))
    Else
        rc = INIFINITESIMAL
    End If
    
    Dim lc
    If GetRightChildIndex(parent_index) <= tail Then
        lc = arr(GetLeftChildIndex(parent_index))
    Else
        lc = INIFINITESIMAL
    End If
    
    If p > lc And p > rc Then
        WhoIsBiggest = Parent
    ElseIf lc > p And lc > rc Then
        WhoIsBiggest = LeftChild
    ElseIf rc > p And rc > lc Then
        WhoIsBiggest = RightChild
    End If
End Function

INIFINITESIMALという定数の名前は英語の「無限小」をとったもの。
実際にはただのLong型の最小値である。
子のインデックスがtailを超えてしまう場合、参照エラーになったり確定値を参照してしまう危険がある。
そこでtailを超えた子より、必ず親が大きくなるように、子の値を入れる変数に無限小を代入している。
ここは力技でしのいだ感じ。

さて、あとは確定した最大値を取り除く処理だ。
これは配列の先頭と末尾を入れ替えてしまえば良いので、そのような関数を作る。
ただし毎回末尾と入れ替えてたら確定値まで動かしてしまうことになる。
したがって末尾はUboundではなく、別途変数で管理しよう。

ひとまずはtailという仮引数で末尾を受け取ることにする。
先頭は0で固定なので受け取る必要はない。

Sub SwapHeadTail(arr, tail)
    Dim tmp: tmp = arr(0)
    arr(0) = arr(tail)
    arr(tail) = tmp
End Sub

さて、役者は揃った。
あとはメインコードをガリガリ書くだけ。

Sub HeapSortMain()
    Dim heap(): heap = Array(8, 2, 7, 6, 9, 1, 4, 3, 5)
    Dim tail As Long: tail = UBound(heap)
    Do
        Do
            Dim cnt: cnt = 0
            Dim i
            For i = LBound(heap) To tail
                Select Case WhoIsBiggest(heap, i, tail)
                Case Role.Parent
                    cnt = cnt + 1
                Case Role.LeftChild
                    SwapParentLeftChild heap, i
                Case Role.RightChild
                    SwapParentRightChild heap, i
                End Select
            Next
        Loop While cnt <= tail
        SwapHeadTail heap, tail
        tail = tail - 1
    Loop While tail > 0
    
    For i = LBound(heap) To UBound(heap)
        Debug.Print heap(i)
    Next
End Sub

まずは最も内側のループからみていこう。

For i = LBound(heap) To tail
    Select Case WhoIsBiggest(heap, i, tail)
    Case Role.Parent
        cnt = cnt + 1
    Case Role.LeftChild
        SwapParentLeftChild heap, i
    Case Role.RightChild
        SwapParentRightChild heap, i
    End Select
Next

このループは配列を先頭から末尾(tail)まで流している。
ループの中では誰が一番大きいかを判定するWhoIsBiggestによりSwap処理を行っている。Swapが発生しなければcntが増える仕組みだ。

その外側のDoループでは、cntとtailが一致するまで繰り返しを行っている。cnt=tailということは、一度もSwapされなかったということ。つまり正しいヒープ構造になっているので、これ以上内側のForループを回す必要がなく、一巡目の最大値が確定したということ。

それからSwapHeadTailで最大値と末尾(tail)を入れ替え、tailを一つ減らしている。
これを繰り返すことで最大値がどんどん配列の底にたまっていく。

tailが0になったらループを抜けると、配列は見事昇順に並んでいるというわけだ。

私にとってややこしいのは、ヒープというツリー型の構造を無理やり配列で扱うこと。
そしてそれぞれの計算がメインコードに出てくることで頭が追い付かなくなる。

今回は関数に分割したことで、個人的にはわかりやすくできた。

これを見たみなさんがどう感じるかはまた別の話であるが。。

VBA Collectionを使って数値をランダムに並び替える

今回はn個の数値をランダムに並び替えるアルゴリズム
元ネタはこちらの記事。
chemiphys.hateblo.jp

上記はRnd関数のランダム性を利用したアルゴリズムで、出てきたSingle値の順位を付けることで結果的にその順位が重複のないランダムな数列になるというもの。

たとえば1~5をランダムに並び替えたいとする。

このようにRndを5回評価し、大きい順に順位をつける。

回数 Rnd結果 順位
1回目 0.4013743 2位
2回目 0.27828 3位
3回目 0.1604415 5位
4回目 0.1628216 4位
5回目 0.6465871 1位

この順位がそのまま2, 3, 5, 4, 1という数列になるわけだ。
なるほど、面白い。

では私はということで、あらかじめ用意した数値をランダムにシャッフルするというアルゴリズムでやってみよう。

出来たのがコレ。

Sub ShuffleCollection(c As Collection)
    Dim cc As Collection: Set cc = New Collection
    Dim m As Long
    Do While c.Count > 0
        m = Int(c.Count * Rnd + 1)
        cc.Add c.Item(m)
        c.Remove m
    Loop
    Set c = cc
End Sub

上記に、なんでもいいのでコレクションを渡すと、順番がバラバラになる。
実際にこれを使うコードを作ってみた。

Sub Main()
    Dim c As Collection: Set c = New Collection
    c.Add 1: c.Add 2: c.Add 3: c.Add 4: c.Add 5
    ShuffleCollection c
    Dim x
    For Each x In c
        Debug.Print x
    Next
End Sub

何度か実行してみると、確かにシャッフルされてるのがわかる。

仕組みとしては、最初はコレクションcに5つの要素が入ってるので、1~5の範囲でランダム値を生成してそれを別のコレクションccに入れる。コレクションcからは、削除する。するとcの要素は4つになるので、1~4の範囲でランダム値を生成して~と繰り返す。
cの要素が0になったら、ccにすべて移ってるので、Set c = cc としておしまい。

関数として非破壊的に作っても良いかな。

こんな感じか。

Function CreateShuffledCollection(c As Collection) As Collection
    Dim cc As Collection: Set cc = New Collection
    Dim i
    For i = 1 To c.Count
        cc.Add c.Item(i)
    Next
    
    Dim ccc As Collection: Set ccc = New Collection
    Dim m As Long
    Do While cc.Count > 0
        m = Int(cc.Count * Rnd + 1)
        ccc.Add cc.Item(m)
        cc.Remove m
    Loop
    Set CreateShuffledCollection = ccc
End Function

これなら元のコレクションは破壊されない。
ついでに1~5をAddするのも関数化してしまおう。

Function GetSequence(n As Long) As Collection
    Dim ret As Collection: Set ret = New Collection
    For i = 1 To n
        ret.Add i
    Next
    Set GetSequence = ret
End Function

元記事は配列だったな。。ということで配列変換も関数化する

Function ChangeCollectionToArray(c As Collection) As Variant
    Dim ret(): ReDim ret(0 To c.Count - 1)
    For i = LBound(ret) To UBound(ret)
        ret(i) = c.Item(i + 1)
    Next
    ChangeCollectionToArray = ret
End Function

これをメインコードにまとめると、こうなる。

Sub Main2()
    Dim arr: arr _
        = ChangeCollectionToArray( _
            CreateShuffledCollection( _
                GetSequence(5)))
    Dim x
    For Each x In arr
        Debug.Print x
    Next
End Sub

以上

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。