树和图是典型的非线性数据结构
树的定义:
树是n(n>=0)个节点的有限集。当n=0时,成为空树。在任意一个非空树中,有如下特点:
- 有且只有一个特定的节点成为根节点
- 当n>1时,其余节点可分为m(m>0)个互不相干的有限集,每个集合本身也是一个树,并成为根的子树。
树的最大层级数,称为树的高度或者深度
二叉树的定义:
二叉树的是指该树的每个节点最多有2个子节点,有可能时两个,也可能只有1个,或者是没有
二叉树的两种特殊形式:
满二叉树:一个二叉树的所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上,这个树就是满二叉树
完全二叉树:一个二叉树的`最后一个节点之前`所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上
二叉树的物理存储结构
1.链式存储结构
采用链表的存储结构,二叉树的每一个节点包含3个部分
- 存储数据的data变量
- 指向左孩子的left指针
- 指向右孩子的right指针
2.数组
使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或者右孩子空缺,则数组的相应位置也空出来
为什么这么设计呢?因为这样可以方便地在数组中定位二叉树的孩子节点和父节点
假设一个父节点的下标是parent,那么它的左孩子节点的下标就是2parent+1,右孩子节点的下标就是2parent+2。
反过来。假设一个左孩子节点的下标是leftchild,那么它的父节点下标就是(leftchild-1)/2
显然,对于一个稀疏的二叉树来说,用数组表示法是非常浪费空间的。
然而因为完全二叉树的特殊性,用数组来存储是非常合适的,比如二叉堆,一种特殊的完全二叉树
二叉树的应用
二叉树有很多种的形式,除一般二叉树外,由上面提到的满二叉树,完全二叉树,还有二叉堆等等
那么这么多种的二叉树有哪些作用呢?
- 用于查找操作
- 维持元素间的相对顺序
二叉查找树
又名二叉排序树,二叉搜索树等等。顾名思义,这种二叉树的元素间有一定的顺序,且方便查找
定义:
- 如果左子树不为空,则
左子树上所有节点的值均小于根节点的值
- 如果右子树不为空,则
右子树上所有节点的值均大于根节点的值
- 左子树和右子树也都是二叉查找树
没有键值相等的结点
。
这里我们采用定义1,一共有三种定义(二叉搜索树百度百科)都是正确的,在开发时需要根据不同的需求进行选择
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)的
为解决这个问题,涉及到二叉树的自平衡,后面再介绍吧