平衡木とAVL木

探索効率をアップする
平衡木
平衡木
二分探索木では、あらかじめソートされたデータを挿入していくと、左右のバランスが偏り、かえって探索に時間がかかるという欠点がある。
探索効率が低下しないよう左右のバランスをとった二分探索木のことを平衡木と呼び、とくに AVL木は理想的なバランスを実現できる。
今回は、PHPとPythonのためのAVL木クラスを用意する。

サンプル・プログラム

平衡木

バランスの崩れた二分探索木
バランスの崩れた二分探索木
二分探索木において、1から7の自然数を小さい順に insert していくとする。ツリーは左図のように、右へ一方的に深くなってゆく。そして、7を探索しようとすると、ルートから7回比較しなければならず、極端に探索速度が低下する。
要素の数をnとすると、最悪の場合の探索回数は  mimetex  で表される。
平衡木
平衡木
一方、左図のように、二分探索木の深さができるだけ浅くなるよう、均等に要素を配置できれば、最悪の場合の探索回数は  mimetex  となり、本来の探索速度を発揮できる。
このようなデータ構造を平衡木 (へいこうぎ) (balanced tree)と呼ぶ。

木の回転

平衡木を作るには、木の回転を行う。
木の回転
木の回転
上図左側のツリーで、要素「2」の注目する。
右回転というのは、親ノードの左側の子ノード「2」を親ノード「4」の位置へ移動させ、親ノード「4」を親の右ノードへ移動させる。二分探索目の順序を保つよう、それぞれの子ノードの付け替えを行う。

右回転では、上図右側のツリーで、要素「4」の注目する。
左回転とは逆に、親ノードの右側の子ノード「4」を親ノード「2」の位置へ移動させ、親ノード「2」を親の左ノードへ移動させる。二分探索目の順序を保つよう、それぞれの子ノードの付け替えを行う。

右回転を行うと、順序関係は保ったまま木の左側が短くなり、右側が長くなる。
左回転を行うと、順序関係を保ったまま木の右側が短くなり、左側が長くなる。

AVL木

あるノードから見て、木の左右の高さのバランスが崩れている場合、木の回転を利用してバランスさせようというのが平衡木の原理である。

AVL木 (えーぶいえるき) (Adelson-Velskii and Landis' tree)は、1962年(昭和37年)、ソ連の数学者ゲオルギー・アデルソン・ヴェルスキーとエフゲニー・ランディスが発表した平衡木である。2人の名をとって、この名が付いている。AVL木では、左右の高さの差が1以下になる。

AVL木は、要素の挿入を行うときにバランスさせてやればいい。
つまり、二分探索木における insert を実行し、その時点で左右の深さ depth を比較し、1以上の差があれば、回転を実行する。

PHPによるAVL木

PHPの二分探索木クラス pahooBStree に、指定されたノードから木の深さを求めるメソッド depth、および右回転メソッド rotateRight、左回転メソッド rotateLeft を追加する。回転は、インスタンスそのものを移動するのではなく、インスタンス変数を移動することで実現している。

1524: /**
1525:  * ツリーの深さを取得する
1526:  * @param pahooBStree $node  探索開始ノード(省略時は呼び出したノードから)
1527:  * @param int         $depth 現在の深さ(省略時は0)
1528:  * @return intツリーの深さ
1529: */
1530: function depth($node=NULL$depth=0) {
1531:     if ($node == NULL)  $node = $this;
1532: 
1533:     //左側
1534:     $left = $depth;
1535:     if ($node->left != NULL) {
1536:         $left = $node->depth($node->left$depth + 1);
1537:     }
1538:     //右側
1539:     $right = $depth;
1540:     if ($node->right != NULL) {
1541:         $right = $node->depth($node->right$depth + 1);
1542:     }
1543: 
1544:     if ($left  > $depth)    $depth = $left;
1545:     if ($right > $depth)    $depth = $right;
1546: 
1547:     return $depth;
1548: }
1549: 
1550: /**
1551:  * 右回転
1552:  * @paramなし
1553:  * @returnなし
1554: */
1555: function rotateRight() {
1556:     $key    = $this->key;
1557:     $item   = $this->item;
1558:     $left   = $this->left;
1559:     $right1 = $this->right;
1560:     $right2 = $this->parent->right;
1561:     $this->key  = $this->parent->key;
1562:     $this->item = $this->parent->item;
1563: 
1564:     $this->parent->key   = $key;
1565:     $this->parent->item  = $item;
1566:     $this->parent->left  = $left;
1567:     if ($this->parent->left != NULL) {
1568:         $this->parent->left->parent = $this->parent;
1569:         $this->parent->left->parentLeft = TRUE;
1570:     }
1571:     $this->parent->right = $this;
1572:     $this->parentLeft = FALSE;
1573: 
1574:     $this->left  = $right1;
1575:     if ($this->left != NULL) {
1576:         $this->left->parent = $this;
1577:         $this->left->parentLeft = TRUE;
1578:     }
1579:     $this->right = $right2;
1580:     if ($this->right != NULL) {
1581:         $this->right->parent = $this;
1582:         $this->right->parentLeft = FALSE;
1583:     }
1584: }
1585: 
1586: /**
1587:  * 左回転
1588:  * @paramなし
1589:  * @returnなし
1590: */
1591: function rotateLeft() {
1592:     $key    = $this->key;
1593:     $item   = $this->item;
1594:     $right  = $this->right;
1595:     $left1  = $this->left;
1596:     $left2  = $this->parent->left;
1597:     $this->key  = $this->parent->key;
1598:     $this->item = $this->parent->item;
1599: 
1600:     $this->parent->key   = $key;
1601:     $this->parent->item  = $item;
1602:     $this->parent->right = $right;
1603:     if ($this->parent->right != NULL) {
1604:         $this->parent->right->parent = $this->parent;
1605:         $this->parent->right->parentLeft = FALSE;
1606:     }
1607:     $this->parent->left = $this;
1608:     $this->parentLeft = TRUE;
1609: 
1610:     $this->left  = $left2;
1611:     if ($this->left != NULL) {
1612:         $this->left->parent = $this;
1613:         $this->left->parentLeft = TRUE;
1614:     }
1615:     $this->right = $left1;
1616:     if ($this->right != NULL) {
1617:         $this->right->parent = $this;
1618:         $this->right->parentLeft = FALSE;
1619:     }
1620: }

AVL木 は、PHPの二分探索木クラス pahooBStree を継承し、pahooAVLtree クラスとした。要素を挿入するメソッド avlinsert だけ定義する。
avlinsert は、二分探索目における insert を同じ処理を行い、そのノードの下に要素を追加する。
続いて、depth メソッドを用いて左右の深さを比較し、1以上あれば、メソッド rotateRight または rotateLeft を実行する。

1626: class pahooAVLtree extends pahooBStree {     //AVL
1627: 
1628: /**
1629:  * 要素を1つ挿入する(AVL木向け)
1630:  * @param mixed     $key    格納するキー
1631:  * @param mixed     $item   格納する要素
1632:  * @param pahooAVLtree $node探索開始ノード(省略時は呼び出したノードから)
1633:  * @param function $cmp($a, $b)  要素の大小比較関数(省略可能)
1634:  *                      $a > $bならTRUEを返す
1635:  * @returnなし
1636: */
1637: function avlinsert($key$item$node=NULL$cmp=NULL) {
1638:     if ($node == NULL)      $node = $this;
1639: 
1640:     //普通に要素を追加
1641:     //大きい場合(右側へ)
1642:     if ((($cmp != NULL) && $cmp($node->key$key)) || ($node->key > $key)) {
1643:         if ($node->left == NULL) {
1644:             $node->left = new pahooAVLtree($key$item$nodeTRUE);
1645:         } else {
1646:             $node->avlinsert($key$item$node->left$cmp);
1647:         }
1648:     //小さい場合(左側へ)
1649:     } else {
1650:         if ($node->right == NULL) {
1651:             $node->right = new pahooAVLtree($key$item$nodeFALSE);
1652:         } else {
1653:             $node->avlinsert($key$item$node->right$cmp);
1654:         }
1655:     }
1656: 
1657:     //平衡させる
1658:     $a = $node->depth();
1659:     if ($node->parent != NULL) {
1660:         if ($node->parentLeft) {
1661:             $b = ($node->parent->right != NULL) ? $node->parent->right->depth() : 0;
1662:             if ($a - $b >= 1)    $node->rotateRight();
1663:         } else {
1664:             $b = ($node->parent->left != NULL) ? $node->parent->left->depth() : 0;
1665:             if ($a - $b >= 1)    $node->rotateLeft();
1666:         }
1667:     }
1668: }
1669: 
1670: // End of Class
1671: }

次に、pahooAVLtree クラスを用いたサンプル・プログラム "dt03-43-01.php" を紹介する。

0011: //データ構造クラス
0012: require_once('pahooDataStructure.php');
0013: 
0014: // メイン・プログラム ======================================================
0015: $bstree = new pahooBStree();         //二分探索木を用意
0016: $min = 1;                                //最小値
0017: $max = 10000;                            //最大値
0018: $loop = 20;                          //ループ回数
0019: 
0020: //要素追加
0021: printf("\n要素追加‥‥");
0022: for ($i = 0; $i < $loop$i++) {
0023:     $k = mt_rand($min$max);                //キー
0024:     $item = 'hoge' . $k;                 //要素
0025:     $bstree->insert($k$item);           //キー、要素を追加
0026:     if ($i == ($loop / 2))  $key = $k;       //探索対象キー
0027:     if ($i != 0) printf("");
0028:     printf("%d:%s", $k$item);
0029: }
0030: 
0031: //一覧
0032: printf("\n\n●一覧");
0033: $cnt = $bstree->count();
0034: printf(" %d個\n", $cnt);
0035: printf("%s\n", $bstree->list());
0036: 
0037: //二分探索
0038: printf("\n●二分探索\n");
0039: $item = $bstree->search($key);
0040: printf("%d:%s", $key$item);
0041: 
0042: //削除
0043: printf("\n\n●削除");
0044: $bstree->delete($key);
0045: $cnt = $bstree->count();
0046: printf(" %d個\n", $cnt);
0047: printf("%s\n", $bstree->list());
0048: 
0049: $bstree = NULL;
0050: 
0051: /*
0052: ** バージョンアップ履歴 ===================================================
0053:  *
0054:  * @version  1.0  2018/07/24
0055: */
0056: ?>
0057: 

挿入するデータ(キー)として、1($min) ~ 100($max) の範囲の自然数を用意する。
これらは、小さい順に並べた配列 $worst を、関数  shuffle  にランダムに並べた配列 $rand の2種類を用意する。
それぞれを、pahooBStree に挿入した場合と、pahooAVLtree に挿入した場合とで、ツリー全体の深さを表示する。
PHPによるAVL木
実行結果は左図の通り。
まず、二分探索木 pahooBStree では、小さい順に並んだキーを挿入する場合が最悪のケースで、ツリーの深さは要素の数と同じになってしまう。一方、ランダム並びの方は、比較的深くならずに済んでいる。
AVL木 pahooAVLtree の方は、小さい順に並んだものも、ランダム並びも、いずれも深さ7でクリアしている。深さが7あれば 27=128 個の要素を格納できるから、理想的な平衡木が実現できたことになる。

PythonによるAVL木

Pythonの二分探索木クラス pahooBStree に、指定されたノードから木の深さを求めるメソッド depth、および右回転メソッド rotateRight、左回転メソッド rotateLeft を追加する。回転は、インスタンスそのものを移動するのではなく、インスタンス変数を移動することで実現している。

0434:     """
0435:      * ツリーの深さを取得する
0436:      * @param pahooBStree $node  探索開始ノード(省略時は呼び出したノードから)
0437:      * @param int         $depth 現在の深さ(省略時は0)
0438:      * @return intツリーの深さ
0439:     
"""
0440:     def depth(selfnode=Nonedepth=0):
0441:         if (node == None):
0442:             node = self
0443:         #左側
0444:         left = depth
0445:         if (node.left != None):
0446:             left = node.depth(node.leftdepth + 1)
0447:         #右側
0448:         right = depth
0449:         if (node.right != None):
0450:             right = node.depth(node.rightdepth + 1)
0451: 
0452:         if (left  > depth):
0453:             depth = left
0454:         if (right > depth):
0455:             depth = right
0456: 
0457:         return depth
0458: 
0459:     """
0460:      * 右回転
0461:      * @paramなし
0462:      * @returnなし
0463:     
"""
0464:     def rotateRight(self):
0465:         key    = self.key
0466:         item   = self.item
0467:         left   = self.left
0468:         right1 = self.right
0469:         right2 = self.parent.right
0470:         self.key  = self.parent.key
0471:         self.item = self.parent.item
0472: 
0473:         self.parent.key   = key
0474:         self.parent.item  = item
0475:         self.parent.left  = left
0476:         if (self.parent.left != None):
0477:             self.parent.left.parent = self.parent
0478:             self.parent.left.parentLeft = True
0479:         self.parent.right = self
0480:         self.parentLeft = False
0481: 
0482:         self.left  = right1
0483:         if (self.left != None):
0484:             self.left.parent = self
0485:             self.left.parentLeft = True
0486:         self.right = right2
0487:         if (self.right != None):
0488:             self.right.parent = self
0489:             self.right.parentLeft = False
0490: 
0491:     """
0492:      * 左回転
0493:      * @paramなし
0494:      * @returnなし
0495:     
"""
0496:     def rotateLeft(self):
0497:         key    = self.key
0498:         item   = self.item
0499:         right  = self.right
0500:         left1  = self.left
0501:         left2  = self.parent.left
0502:         self.key  = self.parent.key
0503:         self.item = self.parent.item
0504: 
0505:         self.parent.key   = key
0506:         self.parent.item  = item
0507:         self.parent.right = right
0508:         if (self.parent.right != None):
0509:             self.parent.right.parent = self.parent
0510:             self.parent.right.parentLeft = False
0511:         self.parent.left = self
0512:         self.parentLeft = True
0513: 
0514:         self.left  = left2
0515:         if (self.left != None):
0516:             self.left.parent = self
0517:             self.left.parentLeft = True
0518:         self.right = left1
0519:         if (self.right != None):
0520:             self.right.parent = self
0521:             self.right.parentLeft = False

AVL木 は、Pythonの二分探索木クラス pahooBStree を継承し、pahooAVLtree クラスとした。要素を挿入するメソッド avlinsert だけ定義する。
avlinsert は、二分探索目における insert を同じ処理を行い、そのノードの下に要素を追加する。
続いて、depth メソッドを用いて左右の深さを比較し、1以上あれば、メソッド rotateRight または rotateLeft を実行する。

0524: class pahooAVLtree(pahooBStree):         #AVL
0525:     """
0526:      * 要素を1つ挿入する(AVL木向け)
0527:      * @param mixed     $key    格納するキー
0528:      * @param mixed     $item   格納する要素
0529:      * @param pahooAVLtree $node探索開始ノード(省略時は呼び出したノードから)
0530:      * @param function $cmp($a, $b)  要素の大小比較関数(省略可能)
0531:      *                      $a > $bならTRUEを返す
0532:      * @returnなし
0533:     
"""
0534:     def avlinsert(selfkeyitemnode=Nonecmp=None):
0535:         if (node == None):
0536:             node = self
0537: 
0538:         #ノードがない場合
0539:         if (node.key == None):
0540:             node.key = key
0541:             node.item = item
0542: 
0543:         #大きい場合(右側へ)
0544:         elif (((cmp != Noneand eval(cmp)(node.keykey)) or (node.key > key)):
0545:             if (node.left == None):
0546:                 node.left = pahooAVLtree(keyitemnodeTrue)
0547:             else:
0548:                 self.avlinsert(keyitemnode.leftcmp)
0549: 
0550:         #小さい場合(左側へ)
0551:         else:
0552:             if (node.right == None):
0553:                 node.right = pahooAVLtree(keyitemnodeFalse);
0554:             else:
0555:                 self.avlinsert(keyitemnode.rightcmp)
0556: 
0557:         #平衡させる
0558:         a = node.depth()
0559:         if (node.parent != None):
0560:             if (node.parentLeft):
0561:                 b = node.parent.right.depth() if (node.parent.right != Noneelse 0
0562:                 if (a - b >= 1):
0563:                     node.rotateRight()
0564:             else:
0565:                 b = node.parent.left.depth() if (node.parent.left != Noneelse 0
0566:                 if (a - b >= 1):
0567:                     node.rotateLeft()

次に、pahooAVLtree クラスを用いたサンプル・プログラム "dt03-43-02.py" を紹介する。

0010: import sys
0011: import random
0012: 
0013: #データ構造クラス
0014: import pahooDataStructure
0015: 
0016: # メイン・プログラム ======================================================
0017: bstree = pahooDataStructure.pahooBStree()                #二分探索木を用意
0018: min = 1                             #最小値
0019: max = 10000                     #最大値
0020: loop = 20                           #ループ回数
0021: 
0022: #要素追加
0023: sys.stdout.write("\n●要素追加\n")
0024: for i in range(loop):
0025:     k = random.randrange(minmax)       #キー
0026:     item = "hoge" + str(k)              #要素
0027:     bstree.insert(kitem)               #キー、要素を追加
0028:     if (i == (loop / 2)):
0029:         key = k                         #探索対象キー
0030:     if (i != 0):
0031:         sys.stdout.write("")
0032:     sys.stdout.write("{0:d}".format(k))
0033:     sys.stdout.write(":{}".format(item))
0034: 
0035: #一覧
0036: sys.stdout.write("\n\n●一覧")
0037: cnt = bstree.count();
0038: sys.stdout.write(" {0:d}個\n".format(cnt))
0039: sys.stdout.write("{}\n".format(bstree.list()))
0040: 
0041: #二分探索
0042: sys.stdout.write("\n●二分探索\n")
0043: item = bstree.search(key);
0044: sys.stdout.write("{0:d}:".format(key))
0045: sys.stdout.write("{}".format(item))
0046: 
0047: #削除
0048: sys.stdout.write("\n\n●削除")
0049: bstree.delete(key);

前述のPHPのサンプル・プログラム "dt03-43-01.php" を移植したもので、実行結果も同じになる。
(この項おわり)
header