ツリーとグラフは、典型的な非線形データ構造です。
ツリーの定義:ツリーは、n(n> = 0)ノードの有限集合です。n = 0の場合、空のツリーになります。空でないツリーでは、次の特性があります。
- 1つの特定のノードのみがルートノードになります
- n> 1の場合、残りのノードはm(m> 0)の無関係な有限集合に分割できます。各集合もツリーであり、ルートのサブツリーになります。
树的最大层级数,称为树的高度或者深度
二分木の定義:二分木は该树的每个节点最多有2个子节点,有可能时两个,也可能只有1个,或者是没有
二分木の2つの特別な形式:
满二叉树:一个二叉树的所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上,这个树就是满二叉树
完全二叉树:一个二叉树的`最后一个节点之前`所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上
二分木の物理ストレージ構造
1.チェーン収納構造
リンクリストのストレージ構造を使用して、バイナリツリーの各ノードには3つの部分が含まれます
- データを格納するためのデータ変数
- 左の子への左ポインタ
- 右の子への右のポインタ
2.アレイ
配列ストレージを使用する場合、バイナリツリーのノードは、階層順序に従って配列内の対応する位置に配置されます。ノードの左または右の子が空の場合、配列の対応する位置も空になります
なぜこのように設計されているのですか?これにより、配列内のバイナリツリーの子ノードと親ノードを簡単に見つけることができるためです。
親ノードの添え字が親であるとすると、その左側の子ノードの添え字は2親+ 1であり、右側の子ノードの添え字は2親+2です。
順番に。左の子ノードの添え字がleftchildであるとすると、その親ノードの添え字は(leftchild-1)/ 2です。
显然,对于一个稀疏的二叉树来说,用数组表示法是非常浪费空间的。
然而因为完全二叉树的特殊性,用数组来存储是非常合适的,比如二叉堆,一种特殊的完全二叉树
二分木の応用
上記の完全な二分木、完全な二分木、二分ヒープなどから、一般的な二分木に加えて、多くの形式の二分木があります。
では、非常に多くの種類の二分木の機能は何ですか?
- 検索操作に使用されます
- 要素間の相対的な順序を維持する
二分探索木
バイナリソートツリー、バイナリ検索ツリーなどとも呼ばれます。名前が示すように、この二分木の要素には特定の順序があり、簡単に見つけることができます
定義:
- 左側のサブツリーが空でない場合は、
左子树上所有节点的值均小于根节点的值
- 右側のサブツリーが空でない場合は、
右子树上所有节点的值均大于根节点的值
- 左のサブツリーと右のサブツリーも二分探索木です
没有键值相等的结点
。
ここでは定義1を使用します。3つの定義(バイナリ検索ツリーBaidu Encyclopedia)が正しく、開発中にさまざまなニーズに応じて選択する必要があります。
Golangはバイナリ検索ツリーを実装しています。
package main
import "fmt"
// 二叉树
type BinaryTree struct {
HeadNode *TreeNode
}
//二叉树节点
type TreeNode struct {
Data int32 // 链表上的数据
Left *TreeNode // 指针指向左孩子节点
Right *TreeNode // 指针指向右孩子节点
}
// 判断二叉树是否为空树,只需要判断第一个元素是否是 nil
func (self *BinaryTree) IsEmpty() bool {
if self.HeadNode == nil {
return true
} else {
return false
}
}
// 在二叉查找树里添加数据
func (self *BinaryTree) Add(value int32) bool {
neWNode := &TreeNode{
Data: value,}
node := self.HeadNode
if node == nil {
self.HeadNode = neWNode
return true
}
for node.Data!=value {
if value < node.Data {
if node.Left!=nil{
node = node.Left
}else {
node.Left=neWNode
return true
}
}
if value > node.Data {
if node.Right!=nil{
node = node.Right
}else {
node.Right=neWNode
return true
}
}
}
return false
}
// 查找元素所在的节点
func (self *BinaryTree) Get(value int32) *TreeNode {
node := self.HeadNode
for node != nil {
if value < node.Data {
node = node.Left
} else if value > node.Data {
node = node.Right
} else {
return node
}
}
return nil
}
func NewBinaryTree() BinaryTree {
BinaryTree := BinaryTree{
HeadNode: nil}
return BinaryTree
}
func main() {
binaryTree := NewBinaryTree()
fmt.Println(binaryTree.IsEmpty())
fmt.Println(binaryTree.Add(3))
fmt.Println(binaryTree.Add(3))
fmt.Println(binaryTree.Add(2))
fmt.Println(binaryTree.Add(4))
fmt.Println(binaryTree.HeadNode.Data)
fmt.Println(binaryTree.HeadNode.Right.Data)
fmt.Println(binaryTree.HeadNode.Left.Data)
fmt.Println(binaryTree.Get(4).Data)
fmt.Println(binaryTree.IsEmpty())
}
二分探索木の時間計算量:
比較的バランスの取れたノード分布を持つ二分探索木では、ノードの総数がnの場合、ノードの検索の時間計算量はO(logn)であり、これはツリーの深さと同じです。
サイズを比較して段階的に検索するこの方法は、二分探索アルゴリズムと非常によく似ています。
ただし、極端な場合(データが挿入されるたびにデータのサイズが増減する)、ツリーの外観は半分になり、検索時間の複雑さはO(n)に減少します。
この問題を解決するには、後で紹介する二分木の自己平衡化が必要です。