AVL木への節点の追加
さて、AVL木に新しくデータが追加された場合、それがどの節点とも一致しなければ節点を追加することになります。そして、新しく節点が追加されたことで先に挙げたAVL木のバランス条件が崩れた場合には、平衡を取り戻す処理を実施しなくてはなりません。
AVL木では節点の追加位置によって以下の4つのパターンが存在することが分かっています。
- R(右回転)
- L(左回転)
- LR(左−右回転)
- RL(右−左回転)
右回転を例に見ていきましょう。まず、初期状態は下図のようになっているものと考えます。先に挙げた「通りがけ順」でたどっていくと、「10、20、30、40、50」とソートされた順でたどれることがわかりますね。

この状態のツリーに「9」の要素を追加してみます。自分より小さい値は左側なので、ROOTからたどっていって、「10」の葉の左側に追加することになります。ところが、この状態になると木の高さのバランスが崩れてしまいます。

そこで、見た目でぐるっと右側へ20の節点を上に引き上げるようなカタチで回転させると、木の高さの差が1になって、しかも左右のバランスが崩れずに済みます。

同じように左側の木の右側、つまりLRの位置に節点が追加された場合にはどのようにすべきかを見ていきます。
親と子、左と右を入れ替えながら、ぐるっと左に回すようにしてから……

さらに右側へぐるっと回すように処理をすると、バランスが回復されます。

木の右側に節点を追加するときも同じように、左回転、右−左回転を行うと木のバランスが回復しますが、図説については同様なので割愛します。
Listにそっくりな木構造のクラス
さて、それではAVL木をクラスとして実装していきましょう。クラスとしての外形的な特徴は以下のとおりです。
TAVLTreeクラスの定義:
- TAVLTreeは複数のデータを保持することができる
- インデックスを指定して、データを挿入することができる
- インデックスを指定して、データを参照することができる
- インデックスを指定して、データを更新することができる
- インデックスを指定して、データを削除することができる
なんだか、Listとそっくりですね。結局、やりたいことは何らかのインデックスを与えてデータをCRUDしたいということなので、外形的には同じものになってしまいます。データの操作に関して、それぞれのアルゴリズムとデータ構造に従って、ある操作が速かったり遅かったりするというのが違いになるわけです。
AVLTreeからの削除については、手順を別途検討しなくてはいけないので、今回はデータを挿入したり参照したりできるところまで実装することにします。また、ツリーの内部構造を表示させたいので、DumpNodesというメソッドを用意しました。先に紹介した、行きがけ順・通りがけ順・帰りがけ順でツリーをたどったときに、各ノードのIDをそれぞれの処理順で文字列で返します。
001: unit avltree;
002:
003: interface
004:
005: uses
006: SysUtils, Classes, RTLConsts;
007:
008: type
009: TBalance = (brLeft, brEqual, brRight);
010: TDumpNodes = (dnPreorder, dnInorder, dnPostorder);
011: TAVLNode = class;
012:
013: TAVLNode = class(TObject)
014: private
015: FItem : Pointer;
016: public
017: ID : Integer;
018: LeftChild, RightChild : TAVLNode;
019: Balance : TBalance;
020: property Item : Pointer read FItem write FItem;
021: constructor Create;
022: destructor Destroy; override;
023: end;
024:
025: TAVLTree = class(TObject)
026: private
027: FCount : integer;
028: FRoot : TAVLNode;
029: protected
030: procedure AddNode(var parent : TAVLNode; newID : Integer; value : Pointer; var grow : Boolean);
031: procedure AdjustLeftGrow(var parent : TAVLNode);
032: procedure AdjustRightGrow(var parent : TAVLNode);
033: function Get(Index: Integer): Pointer;
034: function InternalGet(var parent: TAVLNode; index : integer; var bolFind:Boolean):pointer;
035: procedure InternalDumpNodes(var parent : TAVLNode; var s : string; search_algo:TDumpNodes);
036: public
037: procedure Add(newID : Integer; value : Pointer);
038: function DumpNodes(search_algo:TDumpNodes):String;
039: property Count : Integer read FCount;
040: property Items[Index: Integer]: Pointer read Get; default;
041: destructor Destroy; override;
042: end;
043:
044: implementation
045:
046:
047: { TAVLNode }
048:
049: constructor TAVLNode.Create;
050: begin
051: //
052: end;
053:
054: destructor TAVLNode.Destroy;
055: begin
056: if (LeftChild <> nil) then LeftChild.Free;
057: if (RightChild <> nil) then RightChild.Free;
058:
059: inherited;
060: end;
061:
062: { TAVLTree }
063:
064: //ツリーに値を追加する処理
065: procedure TAVLTree.Add(newID: Integer; value: Pointer);
066: var
067: grow : Boolean;
068: begin
069: //Addメソッドは、まずRootから値を追加する先を探索する
070: grow := False;
071: AddNode(FRoot, newID, value, grow);
072: end;
073:
074: //ノードの追加を再帰的に行う処理
075: procedure TAVLTree.AddNode(
076: var parent: TAVLNode;
077: newID: Integer;
078: value : Pointer;
079: var grow: Boolean
080: );
081: begin
082: //木の最深部まで降りた場合、そこにノードを追加する
083: if (parent = nil) then
084: begin
085: parent := TAVLNode.Create;
086: parent.ID := newID;
087: parent.Balance := brEqual;
088: parent.Item := value;
089: grow := True;
090: Inc(FCount);
091: exit;
092: end;
093:
094: //newIDが現在の節点のIDより小さい時の処理
095: //左側に下っていく
096: if (newID < parent.ID) then
097: begin
098: //左側へ節点を追加する
099: AddNode(parent.LeftChild, newID, value, grow);
100:
101: //木が成長した=高さが変わった場合、grow がTrueで返ってくる
102: //Falseの場合、バランス調整は不要
103: if (grow = False) then exit;
104:
105: if (parent.Balance = brRight) then
106: begin
107: //元々は右側の高さが大きかった場合
108: //左に新しい節点が追加されたので、これでバランスした
109: parent.Balance := brEqual;
110:
111: //上のノードには、深度が変化していないと通知する
112: grow := False;
113: end else if (parent.Balance = brEqual) then
114: begin
115: //元々がバランスしていたので、左側に節点が追加されたため
116: //左側が深い状態になった
117: parent.Balance := brLeft;
118: end else
119: begin
120: //元々左側の高さが大きかったので、
121: //左側に節点が追加されたため、バランス調整が必要となった
122: AdjustLeftGrow(parent);
123: grow := False;
124: end;
125:
126: end else
127: //newIDが現在の節点のIDより大きい場合の処理
128: //右側に下っていく
129: if (newID > parent.ID) then
130: begin
131: //右側に節点を追加する
132: AddNode(parent.RightChild, newID, value, grow);
133:
134: //木が成長した=高さが変わった場合、grow がTrueで返ってくる
135: //Falseの場合、バランス調整は不要
136: if (grow = False) then exit;
137:
138: if (parent.Balance = brLeft) then
139: begin
140: //元々は左側の高さが大きかった場合
141: //右に新しい節点が追加されたので、これでバランスした
142: parent.Balance := brEqual;
143: grow := False;
144: end else
145: if (parent.Balance = brEqual) then
146: begin
147: //元々がバランスしていたので、右側に節点が追加されたため
148: //右側が深い状態になった
149: parent.Balance := brRight;
150: end else
151: begin
152: //元々右側の高さが大きかったので
153: //右側に節点が追加されたため、バランス調整が必要になった
154: AdjustRightGrow(parent);
155: grow := False;
156: end;
157: end else
158: begin
159: //newIDと現在の節点のIDが同じ場合は、ノードの値を書き換える
160: parent.Item := value;
161: grow := False;
162: end;
163: end;
164:
165: //ツリーの左側でバランスが崩れたときの処理
166: procedure TAVLTree.AdjustLeftGrow(var parent: TAVLNode);
167: var
168: OrgLeftChild, OrgGrandChild : TAVLNode;
169: begin
170: OrgLeftChild := parent.LeftChild;
171: if (OrgLeftChild.Balance = brLeft) then
172: begin
173: //左側の左側でバランスが崩れたので、右回転する
174: parent.LeftChild := OrgLeftChild.RightChild;
175: OrgLeftChild.RightChild := parent;
176: parent.Balance := brEqual;
177: parent := OrgLeftChild;
178: end else
179: begin
180: //左側の右側でバランスが崩れたので、左−右回転する
181: OrgGrandchild := OrgLeftchild.RightChild;
182: OrgLeftchild.RightChild := OrgGrandChild.LeftChild;
183: OrgGrandchild.LeftChild := OrgLeftchild;
184: parent.LeftChild := OrgGrandChild.RightChild;
185: OrgGrandChild.RightChild := parent;
186: if (OrgGrandChild.Balance = brLeft) then
187: parent.Balance := brRight
188: else
189: parent.Balance := brEqual;
190: if (OrgGrandchild.Balance = brRight) then
191: OrgLeftchild.Balance := brLeft
192: else
193: OrgLeftchild.Balance := brEqual;
194: parent := OrgGrandChild;
195: end;
196: parent.Balance := brEqual;
197: end;
198:
199: //ツリーの右側でバランスが崩れたときの処理
200: procedure TAVLTree.AdjustRightGrow(var parent: TAVLNode);
201: var
202: OrgRightChild, OrgGrandChild : TAVLNode;
203: begin
204: OrgRightChild := parent.RightChild;
205: if (OrgRightChild.Balance = brRight) then
206: begin
207: //右側の右側でバランスが崩れたので、左回転する
208: parent.RightChild := OrgRightChild.LeftChild;
209: OrgRightChild.LeftChild := parent;
210: parent.Balance := brEqual;
211: parent := OrgRightChild;
212: end else
213: begin
214: //右側の左側でバランスが崩れたので、右−左回転する
215: OrgGrandchild := OrgRightchild.LeftChild;
216: OrgRightchild.LeftChild := OrgGrandChild.RightChild;
217: OrgGrandChild.RightChild := OrgRightChild;
218: parent.RightChild := OrgGrandChild.LeftChild;
219: OrgGrandChild.LeftChild := parent;
220: if (OrgGrandChild.Balance = brRight) then
221: parent.Balance := brLeft
222: else
223: parent.Balance := brEqual;
224: if (OrgGrandchild.Balance = brLeft) then
225: OrgRightChild.Balance := brRight
226: else
227: OrgRightChild.Balance := brEqual;
228: parent := OrgGrandChild;
229: end;
230: parent.Balance := brEqual;
231: end;
232:
233: destructor TAVLTree.Destroy;
234: begin
235: if (FRoot <> nil) then FRoot.Free;
236:
237: inherited;
238: end;
239:
240: //ツリーの内部からIDを引き出して、文字列で返す
241: function TAVLTree.DumpNodes(search_algo:TDumpNodes): String;
242: begin
243: InternalDumpNodes(FRoot, result, search_algo);
244: end;
245:
246: //ツリーのインデックス参照による値の取得
247: function TAVLTree.Get(Index: Integer): Pointer;
248: var
249: bolFind:Boolean;
250: begin
251: Result := InternalGet(FRoot, Index, bolFind);
252: if (bolFind = False) then raise EListError.Createfmt(LoadResString(@SListIndexError), [Index]);
253: end;
254:
255: //ツリーの内部状態をダンプする処理
256: procedure TAVLTree.InternalDumpNodes(var parent: TAVLNode; var s: string; search_algo:TDumpNodes);
257: procedure make_result;
258: begin
259: if (s <> '') then s := s + ', ';
260: s := s + 'ID=' + IntToStr(parent.ID);
261: end;
262: begin
263: //行きがけ順はここで処理
264: if (search_algo = dnPreorder) then make_result;
265:
266: if (parent.LeftChild <> nil) then InternalDumpNodes(parent.LeftChild, s, search_algo);
267:
268: //通りがかけ順はここで処理
269: if (search_algo = dnInorder) then make_result;
270:
271: if (parent.RightChild <> nil) then InternalDumpNodes(parent.RightChild, s, search_algo);
272:
273: //帰りがけ順はここで処理
274: if (search_algo = dnPostorder) then make_result;
275: end;
276:
277: //ツリーからのデータの取得を再帰的に行う処理
278: function TAVLTree.InternalGet(var parent: TAVLNode; index : integer; var bolFind:Boolean): pointer;
279: var
280: tmp:Pointer;
281: begin
282: if (parent.ID = Index) then
283: begin
284: result := parent.item;
285: bolFind := True;
286: exit;
287: end;
288: if (parent.LeftChild <> nil) then
289: begin
290: tmp := InternalGet(parent.LeftChild, index, bolFind);
291: if (bolFind = True) then
292: begin
293: result := tmp;
294: exit;
295: end;
296: end;
297: if (parent.RightChild <> nil) then
298: begin
299: tmp := InternalGet(parent.RightChild, index, bolFind);
300: if (bolFind = True) then
310: begin
302: result := tmp;
303: exit;
304: end;
305: end;
306: end;
307:
308: end.テストプログラムも作成してみました。AVLTree_Text.exeを実行してみて下さい。ツリーへの挿入は、Add(newID : Integer; value : Pointer)というメソッドで行いますが、newIDがすでに格納されている場合は、Valueを上書きするようにしました。
要素の参照はプロパティItems[Index: Integer]をとおして行いますが、default指定を付けてあるので、ツリーのインスタンスに対して[Index: Integer]とすればアクセスできます。なお、AVLTree_Text.exeのソースコードを公開します(avltreetest.zip)。
アルゴリズムの実装は編み物に似ている
リストやキューもそうですが、データの追加や削除を行う際の処理というのは、ある種、編み物やあやとりに似ているなと思います。こちらとあちらを切り離して、こちらへ付け替えて、という感じですね。
アルゴリズムを実装するというのは編み物で、実は僕らは羊毛で機織りをしているのと変わらないのかもしれません。10歳の娘が編み物にチャレンジしていましたが、見てると頭がこんがらがりそうなくらい難しいですからね。それにしれも、あれは誰にあげるんだろう……。
さて、次回は今回の続きでAVLTreeからの節点の削除にチャレンジしてみます。ご期待下さい。
2/2 |
| Index | |
| AVL木で木構造を学ぼう | |
| Page1 木構造とは何か? 木構造をたどる ロシアからやってきたAVL木 |
|
| Page2 AVL木への節点の追加 Listにそっくりな木構造のクラス アルゴリズムの実装は編み物に似ている |
|
| Appendix AVLTreeのソースコード |
|
| Delphiアルゴリズムトレーニング |
| Coding Edgeお勧め記事 |
| いまさらアルゴリズムを学ぶ意味 コーディングに役立つ! アルゴリズムの基本(1) コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう |
|
| Zope 3の魅力に迫る Zope 3とは何ぞや?(1) Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか? |
|
| 貧弱環境プログラミングのススメ 柴田 淳のコーディング天国 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く? |
|
| Haskellプログラミングの楽しみ方 のんびりHaskell(1) 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう |
|
| ちょっと変わったLisp入門 Gaucheでメタプログラミング(1) Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう |
|
- プログラムの実行はどのようにして行われるのか、Linuxカーネルのコードから探る (2017/7/20)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。最終回は、Linuxカーネルの中では、プログラムの起動時にはどのような処理が行われているのかを探る - エンジニアならC言語プログラムの終わりに呼び出されるexit()の中身分かってますよね? (2017/7/13)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、プログラムの終わりに呼び出されるexit()の中身を探る - VBAにおけるFileDialog操作の基本&ドライブの空き容量、ファイルのサイズやタイムスタンプの取得方法 (2017/7/10)
指定したドライブの空き容量、ファイルのタイムスタンプや属性を取得する方法、FileDialog/エクスプローラー操作の基本を紹介します - さらば残業! 面倒くさいエクセル業務を楽にする「Excel VBA」とは (2017/7/6)
日頃発生する“面倒くさい業務”。簡単なプログラミングで効率化できる可能性がある。本稿では、業務で使うことが多い「Microsoft Excel」で使えるVBAを紹介する。※ショートカットキー、アクセスキーの解説あり
|
|





