二分木(二進木) - 理工学部・情報科学科

二分木(二進木)
理工学部 情報科学科
上級プログラミング
二分木

あるデータ群の大小関係を表現するデータ構造
例: 8, 10, 3, 9, 5, 1, 12, 4, 11 という整数の入力列から
昇順(または降順)の二進木を構成する。
構成方法:
root
(根)
新しいノードを追加するとき、
木のノードと比較して
小さければ左へ、
そうでなければ右へたどり、
空き場所につなげる
0
8
0 0
3
10
0 0
0 0
1
5
9
12
0 0
0 0
0 0
0 0
4
11
0 0
0 0
二分木の設計(1)
BinNodeオブジェクト
class BinTree {
private:
idata
class BinNode {
8
public:
right
left
0 0
int idata;
BinNode *left, *right; // 左右の二分木を指すポインタ
BinNode(int a=0){ idata=a; left=right=0; } // ノードのコンストラクタ
void printNode(){ cout<<idata<<“ ”; } // ノードの表示
};
BinNode *root;
void traverse(BinNode *rp); // rpの木をたどりながら表示
BinNode* addNode(BinNode *rp, BinNode *node);
// rpの木にnodeを連結
public:
BinTree(){ root=0; } // 木のコンストラクタ
void printTree(){ traverse(root); } // 木全体を表示
void insert(int x){ // 木にデータxを追加する
BinNode *np=new BinNode(x); // データxを持つノードを作成
root=addNode(root, np); // rootの木に作成したノードを追加
};
}
BinTreeを利用したmain関数の例
int main(){
BinTree bt; //空の二進木を作成
int x;
cout << "正整数をいくつか入力せよ --> ";
while(cin>>x && x>0){ // 負数が入力されるまで正整数を入力
bt.insert(x); // xをデータとして持つノードを木に追加
}
bt.printTree(); // btの木全体を表示する
cout << endl;
このmain関数が動作するためには、
return 0;
insert関数(addNode関数)、
}
printTree関数(traverse関数)を
どのように記述すれば良いか?
二分木と再帰的考え方(addNodeの場合)
二分木の構築は、「木にノードをつなぐ」という考え方
「nodeをrpの木に連結する」と考える
node
0
rp
0
1
8
0 0
0 0
ポイント1:
3
10
0 0
0 0
つまり、
addNode(rp, node)
をしたい。
ポイント2:
5
9
0 0
0 0
そして addNode(rp, node) が返すの
は、出来上がった木の根である
(木の根が変わるときがあるから)
だから BinNode* addNode(BinNode *rp, BinNode *node); という
プロトタイプ宣言になっている。クラス外で定義するときは、以下のように記述。
BinTree::BinNode* BinTree::addNode(BinNode *rp, BinNode *node){・・・}
二分木と再帰的考え方(addNodeの場合)
再帰をさせない境界条件について先に処理する
境界条件1: つなぎ先が空の木ならば、nodeが根になる
rp
node
0
0
8
空の木
0 0
境界条件2: 空のnodeをつなげても、木は変わらない
rp
node
0
8
0
0 0
10
3
0 0
0 0
5
0 0
9
0 0
境界条件1,2がそれぞれ成立するときに
addNode(rp, node)
に何をリターンさせるかはこの図から明らか。
二分木と再帰的考え方(addNodeの場合)
addNode(rp, node)の中でやることは・・・・・・
rp
0
8
1つのノードではなく
左右に部分木を持つ木と
考えるのがコツ
0 0
node
0
1
0 0
3
10
0 0
0 0
5
9
0 0
0 0
①nodeはrpの左右どちらに行くか?
この例では8のノードの左側に行く。
②空きでなければ、
「rpの左下の木にnodeをつなぐ」
つまり、addNode(rp->left, node)
このとき左下部分木が空かどうか
気にしなくて良い(前述の境界条件のお陰)
これは最初の「rpの木にnodeをつなぐ」と
同じ表現をしている(再帰的な表現 → 再帰関数)
二分木と再帰的考え方(traverseの場合)
二分木のデータを小さい順にたどる(ここでは表示する)
root
(根)
「この木の全データを昇順に表示する」
つまり、traverse(root)という呼び出しに
対して、何をするか?
0
8
0 0
3
10
0 0
0 0
①空の木だったら何もすることは
ない
この例ではrootは空(0)ではない。
1
5
9
12
0 0
0 0
0 0
0 0
4
11
0 0
0 0
二分木と再帰的考え方(traverseの場合)
二分木のデータを小さい順にたどる(ここでは表示する)(続き)
root
(根)
この木全体を根ノードと左右の部分木に
分けて考える。
0
8
いきなり根ノードのデータを表示できるわけ
ではない。
根ノードの左下の部分木は根ノードより
小さいデータの集合であることに注目。
そこで・・・
0 0
3
10
0 0
0 0
1
5
9
12
0 0
0 0
0 0
0 0
②左下の部分木を表示する。
つまり、traverse( ? )
③根ノードのデータを表示する。
つまり、根のprintNode()を使用
4
11
0 0
0 0
④右下の部分木を表示する。
つまり、traverse( ? )
二分木からのノードの削除
int main(){
BinTree bt; //空の二進木を作成
int x;
cout << "正整数をいくつか入力せよ --> ";
while(cin>>x && x>0){ // 負数が入力されるまで正整数を入力
bt.insert(x); // xをデータとして持つノードを木に追加
}
bt.printTree(); // btの木全体を表示する
cout << endl;
while(cout<<"削除したい正整数 -->" && cin>>x && x>0){
bt.remove(x);
BinTreeクラスに
bt.printTree();
remove()メンバ関数を追加
cout << endl;
}
return 0;
}
二分木からのノードの削除
class BinTree {
private:
class BinNode {
public:
int idata;
BinNode *left, *right; // 左右の二分木を指すポインタ
BinNode(int a=0){ idata=a; left=right=0; }
void printNode(){ cout <<idata <<" "; } // 自分のデータの表示
};
BinNode *root;
void traverse(BinNode *rp); // rpの木を表示
BinNode* addNode(BinNode *rp, BinNode *node); // rpの木にnodeを連結
BinNode* delNode(BinNode *rp, int x);
// rpの木からデータxを持つノードを1つ削除する
// delNodeが返すのも削除完了後の木の根(addNodeと同様に根が変わるときがあるから)
public:
BinTree(){ root=0; }
void printTree(){ traverse(root); } // 木全体を表示
void insert(int x){ // 木にデータxを追加する
BinNode *np=new BinNode(x); // データxを持つノードを作成
root=addNode(root, np); // rootの木に作成したノードを追加
}
void remove(int x){ root=delNode(root, x); }
// 木からデータxを持つノードを1つ削除する
};
二分木からのノードの削除
二分木の中のノードを削除する場合には、そのノード
に部分木があるかどうかで処理が異なりそう・・・
1.削除するノードが末端の
(それより下に左右とも
部分木がない)場合
例:左の二分木から「4」の
ノードを削除する場合、
そのノードを削除するだ
けで良い。
root
(根)
0
8
0 0
3
10
0 0
0 0
1
5
9
12
0 0
0 0
0 0
0 0
4
11
0 0
0 0
二分木からのノードの削除
2.削除するノードが左下に
のみ部分木を持つ場合
例:左の二分木から「20」の
ノードを削除する場合、
その削除するノードを、
左下の部分木で置換する。
root
(根)
0
8
0 0
3
20
0 0
0 0
1
5
15
0 0
0 0
0 0
4
11
18
0 0
0 0
0 0
二分木からのノードの削除
3.削除するノードが右下に
のみ部分木を持つ場合
例:左の二分木から「10」の
ノードを削除する場合、
その削除するノードを、
右下の部分木で置換する。
root
(根)
0
8
0 0
3
10
0 0
0 0
1
5
0 0
0 0
4
0 0
15
0 0
11
18
0 0
0 0
二分木からのノードの削除
4.削除するノードが左右に
部分木を持つ場合
例:左の二分木から「20」の
ノードを削除する場合、
まず、その削除したノード
の代わりに、右下の部分
木を接続する。次に、削除
したノードの左下の部分木
を、木に追加する(左右逆
も可能)。
root
(根)
0
8
0 0
3
20
0 0
0 0
1
5
15
30
0 0
0 0
0 0
0 0
結果的に削除したノードの右下部分木の
最左端に接続されることになる。つまり、
addNode(右下部分木, 左下の部分木)
で出来る木を、削除したノードの代わりに
親のノードに連結すれば良い。
4
11
18
25
37
0 0
0 0
0 0
0 0
0 0
二分木からのノードの削除
以上4ケースを場合分けしても良いが、再帰の境界条件を
うまく設定することで、場合分けを減らすことが可能。
境界条件: 「空の木からノードを削除したら、結果は空の木」
rp
この境界条件を利用すると、rpの木の
根が削除対象でない場合、rpのノード
の左下部分木か右下部分木の適切な
方で削除対象を削除することになるが、
それらの部分木が空であるかどうかは
場合分けの必要が無くなる。
部分木か空か否かでは、再帰呼び出しの
場合分けが不要となる。
0
20
0 0
15
30
0 0
0 0
11
18
25
37
0 0
0 0
0 0
0 0