プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第54回 プログラミングについて『2分木を考える』
コラム
2023/12/25

第54回 プログラミングについて『2分木を考える』

アポロレポート

 プログラミングの本を読んでいると、2分木という文字に出くわすことがあります。これは以前にソートのお話をしたときのヒープ木に似ているのですが、どのように並ぶかが違っていて次の図のようになります。

                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1   4 6   8
           ┌--┴─┐     ┌--┴─┐
               2         9

木の親より小さいものを左側の子に、以上のものを右側の子にした木構造になるものです。ということは、木の最も左側のものが最小で、最も右側のものが最大の値になるということです。

 537146829の順で木を成長させても、578391426の順で木を成長させても上の図のようになります。もう少し理解を深めるために、この木の成長の様子を見てみましょう。

 いま578391426の順で木を成長させるとします。

最初の5のときには木には何もありませんので、この値が木の根になります。

                  5
次に7は5以上なので、
                  5
               ┌──┴──┐
                     7

8は5以上、7以上なので、
                  5
               ┌──┴──┐
                     7
                   ┌─┴─┐
                       8
3は5未満なので、
                  5
               ┌──┴──┐
               3     7
                   ┌─┴─┐
                       8
9は5以上、7以上、8以上なので、
                  5
               ┌──┴──┐
               3     7
                   ┌─┴─┐
                       8
                     ┌─┴─┐
                         9
1は5未満、3未満なので、
                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1         8
                     ┌─┴─┐
                         9
4は5未満、3以上なので、
                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1   4     8
                     ┌─┴─┐
                         9
2は5未満、3未満で2以上なので、
                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1   4     8
           ┌─┴─┐     ┌─┴─┐
               2         9

最後に6は、5以上、7未満なので、

                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1   4 6   8
           ┌─┴─┐     ┌─┴─┐
               2         9
というようになります。

これをプログラムにしてみましょう。

 #include <stdio.h>
 #include <malloc.h>

 #define NEW(type,element) (type *)malloc(sizeof(type)*(element))

 struct tree_strct{
  int value;
  struct tree_strct *left,*right;
 };
 struct tree_strct *tree_root=NULL;

 void add_tree();
 struct tree_strct *add_node();

 main()
 {
  int i;

  while(1){
   scanf("%d",&i);

   add_tree(tree_root,i);
   display_tree(tree_root,0);
  }
 }

 struct tree_strct *add_node(i)
 int i;
 {
  struct tree_strct *tree;

  tree=NEW(struct tree_strct,1);
  tree->value=i;
  tree->left =NULL;
  tree->right=NULL;

  return tree;
 }

 void add_tree(tree,i)
 struct tree_strct *tree;
 int i;
 {
  if(tree==NULL){
   tree_root=add_node(i);
  }
  else if(i<tree->value){
   if(tree->left==NULL)tree->left=add_node(i);
   else        add_tree(tree->left,i);
  }
  else{
   if(tree->right==NULL)tree->right=add_node(i);
   else         add_tree(tree->right,i);
  }
 }

 display_tree(tree,depth)
 struct tree_strct *tree;
 int depth;
 {
  int i;

  if(tree->right)display_tree(tree->right,depth+1);

  for(i=0;i<depth*2;i++)putchar(' ');
  printf("%d\n",tree->value);

  if(tree->left )display_tree(tree->left ,depth+1);
 }

 2分木の例としては多分ありふれたものだと思います。この例のように2分木は再帰的に処理するとプログラムをスッキリ記述することができます。このプログラムで上の例と同じことを行なうと、

    9
   8
  7
   6
 5
   4
  3
    2
   1

と表示されます。

 2分木が上のようになっているとき、4があるかないかを検索するには、最初に5と比較し、4は5未満なので次に3と比較し、4は3以上なので4に辿り着くことができます。もし、このように2分木が綺麗な木になっているときには、要素の数が1万個のときでも、深さが12程度になるので12回の比較で検索できることになります。

 ところが、あくまでもこれは木が理想的なときのことです。もし、木を成長させるときに123456789の順で成長させると、

        9
       8
      7
     6
    5
   4
  3
 2
1

となり、2分木ではなく単なるリストになってしまうのです。これでは1万個の要素の検索に最大で1万回の比較を行なわなければなりません。

 そこで、あくまでもこの2分木に執着することにして、まず上のプログラムに1つの木の葉の深さを表示するように変更します。例えば、

                  5
               ┌──┴──┐
               3     7
             ┌─┴─┐ ┌─┴─┐
             1   4 6   8
           ┌--┴─┐     ┌--┴─┐
               2         9

のときには、

   9(0,0)
  8(0,1)
 7(1,2)
  6(0,0)
5(3,3)
  4(0,0)
 3(2,1)
   2(0,0)
  1(0,1)

のように表示されるようにします。カッコの中の左側の数がその木の左側の深さの最大数、右側が同じく右側の深さの最大数を表示します。

 #include <stdio.h>
 #include <malloc.h>

 #define NEW(type,element) (type *)malloc(sizeof(type)*(element))

 struct tree_strct{
  int value;
  struct tree_strct *left,*right;
  char ldepth,rdepth;          /* 深さを保持するメンバーを追加 */
 };
 struct tree_strct *tree_root=NULL;

 void add_tree();
 void set_depth();
 struct tree_strct *add_node();

 main()
 {
  int i;

  while(1){
   scanf("%d",&i);

   add_tree(tree_root,i);
   display_tree(tree_root,0);
  }
 }

 struct tree_strct *add_node(i)
 int i;
 {
  struct tree_strct *tree;

  tree=NEW(struct tree_strct,1);
  tree->value =i;
  tree->left =NULL;
  tree->right =NULL;
  tree->ldepth=0;          /* 新しいノードの深さは左右とも0 */
  tree->rdepth=0;

  return tree;
 }

 display_tree(tree,depth)
 struct tree_strct *tree;
 int depth;
 {
  int i;

  if(tree->right)display_tree(tree->right,depth+1);

  for(i=0;i<depth*2;i++)putchar(' ');

                   /* 左右の深さの表示を追加 */
  printf("%d(%d,%d)\n",tree->value,tree->ldepth,tree->rdepth);

  if(tree->left )display_tree(tree->left,depth+1);
 }

                   /* 左右の深さを設定する関数 */
 void set_depth(tree)
 struct tree_strct *tree;
 {
  if(tree->left){          /* 左側に子があるときには、
                     子の左右の深さの最大値+1を設定 */
   tree->ldepth=(tree->left->ldepth > tree->left->rdepth ?
          tree->left->ldepth : tree->left->rdepth )+1;
  }
  else{               /* 左側に子がないときには深さは0 */
   tree->ldepth=0;
  }

  if(tree->right){         /* 右側に子があるときには、
                     子の左右の深さの最大値+1を設定 */
   tree->rdepth=(tree->right->ldepth > tree->right->rdepth ?
          tree->right->ldepth : tree->right->rdepth )+1;
  }
  else{               /* 右側に子がないときには深さは0 */
   tree->rdepth=0;
  }
 }

 void add_tree(tree,i)
 struct tree_strct *tree;
 int i;
 {
  if(tree==NULL){
   tree_root=add_node(i);
   return;
  }

  if(i<tree->value){
   if(tree->left==NULL)tree->left=add_node(i);
   else        add_tree(tree->left,i);
  }
  else{
   if(tree->right==NULL)tree->right=add_node(i);
   else         add_tree(tree->right,i);
  }

  set_depth(tree);         /* ノードを追加した後に深さを設定 */
}

 ちょっと長くなってしまいましたが、基本的な変更は、構造体 tree_strct に左側と右側の深さの最大値のメンバーを追加し、ノードを追加するたびにその深さを設定するようにしています。またこの深さを保持する ldepth と rdepth メンバーは char 型になっていますが、以後に加える変更の含みがあるためなので上のプログラムでは深さが127までしか正常に動作しないので注意して下さい。

 このプログラムを実行してみましょう。まず123を順に入力すると、

  3(0,0)
 2(0,1)
1(0,2)

となります。次に132の順では、

 3(1,0)
  2(0,0)
1(0,2)

となります。54678では、

   8(0,0)
  7(0,1)
 6(0,2)
5(1,3)
 4(0,0)


となります。この結果を見てみると、左と右の深さの差が2以上になったときには木の左右のバランスが悪くなっているのが分かると思います。ということはどの節でも左右の深さの差が1以下になるようにできれば木のバランスが最高に良くなるということになります。
 まず、123の順で木が成長した場合を考えましょう。

      1(0,2)
    ┌─┴─┐
        2(0,1)
      ┌─┴─┐
          3(0,0)

このときには、1の左右の深さの差が2になりますので、

      2(1,1)
    ┌─┴─┐
    1(0,0) 3(0,0)

というようになれば深さの差は1以下(この場合は0)になります。132の順で成長したときには、

      1(0,2)
    ┌─┴─┐
        3(1,0)
      ┌─┴─┐
      2(0,0)

のときにも、

      2(1,1)
    ┌─┴─┐
    1(0,0) 3(0,0)

となればいいということになります。

 もう少し詳しく考えてみましょう。木が成長してしていく過程で、123の順で木が成長したときに木が次のようになったとします。このときのABCDは既に木に加えられているとします。

      1(n,n+2)
    ┌─┴─┐
    A   2(n,n+1)
      ┌─┴─┐
      B   3(n,n)
         ┌┴┐
         C D

このときには、

      2
    ┌─┴─┐
    1   3
   ┌┴┐ ┌┴┐
   A B C D

 となれば良い訳です。3の下のCとDはそのまま3にぶら下がり、1の左側のAもそのまま1にぶら下がります。Bは2の左側ですが、2の左側に1をぶら下げるので、Bは必ず1よりも大きいことから1の右側にぶら下げます。

 成長の過程で132と成長したときには、

      1(n,n+2)
    ┌─┴─┐
    A   3(n,n+1)
      ┌─┴─┐
      2(n,n) B
     ┌┴┐
     C D

次のようになれば良い訳です。

      2
    ┌─┴─┐
    1   3
   ┌┴┐ ┌┴┐
   A C D B

 即ち、1の左側のAはそのまま1にぶら下がり、3の右側のBもそのままBにぶら下がります。2の左のCは、2に左に1をぶら下げるので、1の右側にぶら下げます。また2の右側のDは2の右側に3をぶら下げるので3の左にぶら下げます。

 このように木の成長過程で木をバランスのとれたものにしていけば常に木全体が理想的なものになる訳です。

 この機能を上のプログラムに追加してみましょう。まず add_tree 関数の set_depth関数を呼び出した後に,

  rehash_tree(tree);

を追加します。この rehash_tree 関数は、

 void rehash_tree(tree)
 struct tree_strct *tree;
 {
  struct tree_strct tt,*tl,*tr,*tlr,*trl;
  int diff;

  diff=tree->ldepth - tree->rdepth;
  if(diff==2){
   tl=tree->left;

   diff=tl->ldepth - tl->rdepth;
   if(diff==1){
    tt  = *tree;
    *tree= *tl;
    *tl = tt;

     tl->left =tree->right;
    tree->right=tl;
   }
   else{
    tlr=tl->right;

    tt  = *tree;
    *tree= *tlr;
    *tlr = tt;

     tlr->left =tree->right;
    tree->right=tlr;
     tl->right=tree->left;
    tree->left =tl;

    set_depth(tlr);
   }

   set_depth(tl);
  }
  else if(diff== -2){
   tr=tree->right;

   diff=tr->ldepth - tr->rdepth;
   if(diff== -1){
    tt  = *tree;
    *tree= *tr;
    *tr = tt;

     tr->right=tree->left;
    tree->left =tr;
   }
   else{
    trl=tr->left;

    tt  = *tree;
    *tree= *trl;
    *trl = tt;

     trl->right=tree->left;
    tree->left =trl;
     tr->left =tree->right;
    tree->right=tr;

    set_depth(trl);
   }

   set_depth(tr);
  }
  else{
   return;
  }

  set_depth(tree);
 }

 この関数も長くなってしまいましたが、上で考えた方法をそのままプログラムにしてあります。

このプログラムを1から順に10まで数値を入力して木の成長を見てみましょう。


  1(0,0)
  ----------

   2(0,0)
  1(0,1)
  ----------

   3(0,0)
  2(1,1)
   1(0,0)
  ----------

    4(0,0)
   3(0,1)
  2(1,2)
   1(0,0)
  ----------

    5(0,0)
   4(1,1)
    3(0,0)
  2(1,2)
   1(0,0)
  ----------

    6(0,0)
   5(0,1)
  4(2,2)
    3(0,0)
   2(1,1)
    1(0,0)
  ----------

    7(0,0)
   6(1,1)
    5(0,0)
  4(2,2)
    3(0,0)
   2(1,1)
    1(0,0)
  ----------

     8(0,0)
    7(0,1)
   6(1,2)
    5(0,0)
  4(2,3)
    3(0,0)
   2(1,1)
    1(0,0)
  ----------

     9(0,0)
    8(1,1)
     7(0,0)
   6(1,2)
    5(0,0)
  4(2,3)
    3(0,0)
   2(1,1)
    1(0,0)
  ----------
10
     10(0,0)
    9(0,1)
   8(2,2)
     7(0,0)
    6(1,1)
     5(0,0)
  4(2,3)
    3(0,0)
   2(1,1)
    1(0,0)

 細かい説明はしませんが、木が理想的にバランスを保って成長しているのが分かると思います。また理想的に木が成長しているということは、 tree_strct 構造体の深さを保持する変数
ldepth と rdepth は要素数が1万個でも深さがたかだか13ですので、実用上127も表現できれば十分ということになります。

 さて、気になるのは1つのノードの追加で木の整形がどれくらいの回数になるのかということですが、いろいろと実験してみたところ最大でも1にしかなりませんでした。実験したデータが悪いのかも知れませんが、これを証明するだけの時間がありませんので、この実験値を基にすると無駄な動作を殆どせずに理想的な2分木を生成することが可能であるということになります。
 今回は2分木に執着して考えてみましたが、ひも付き2分木やB木などいろいろと工夫されたものがありますので、そちらのほうも参考にして下さい。

ではまた次回。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?