二叉树(一)--概述(附二叉搜索树实现)

树和图是典型的非线性数据结构

树的定义:
树是n(n>=0)个节点的有限集。当n=0时,成为空树。在任意一个非空树中,有如下特点:

  1. 有且只有一个特定的节点成为根节点
  2. 当n>1时,其余节点可分为m(m>0)个互不相干的有限集,每个集合本身也是一个树,并成为根的子树。
树的最大层级数,称为树的高度或者深度

二叉树的定义:
二叉树的是指该树的每个节点最多有2个子节点,有可能时两个,也可能只有1个,或者是没有

二叉树的两种特殊形式:

满二叉树:一个二叉树的所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上,这个树就是满二叉树
完全二叉树:一个二叉树的`最后一个节点之前`所有非叶子节点都存在左孩子和右孩子,并且所有叶子节点都在同一层级上

二叉树的物理存储结构

1.链式存储结构

采用链表的存储结构,二叉树的每一个节点包含3个部分

  • 存储数据的data变量
  • 指向左孩子的left指针
  • 指向右孩子的right指针

2.数组

使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或者右孩子空缺,则数组的相应位置也空出来

为什么这么设计呢?因为这样可以方便地在数组中定位二叉树的孩子节点和父节点

假设一个父节点的下标是parent,那么它的左孩子节点的下标就是2parent+1,右孩子节点的下标就是2parent+2。

反过来。假设一个左孩子节点的下标是leftchild,那么它的父节点下标就是(leftchild-1)/2

显然,对于一个稀疏的二叉树来说,用数组表示法是非常浪费空间的。
然而因为完全二叉树的特殊性,用数组来存储是非常合适的,比如二叉堆,一种特殊的完全二叉树

二叉树的应用

二叉树有很多种的形式,除一般二叉树外,由上面提到的满二叉树,完全二叉树,还有二叉堆等等

那么这么多种的二叉树有哪些作用呢?

  1. 用于查找操作
  2. 维持元素间的相对顺序

二叉查找树

又名二叉排序树,二叉搜索树等等。顾名思义,这种二叉树的元素间有一定的顺序,且方便查找

定义:

  1. 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
  2. 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
  3. 左子树和右子树也都是二叉查找树
  4. 没有键值相等的结点

这里我们采用定义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)的

为解决这个问题,涉及到二叉树的自平衡,后面再介绍吧

猜你喜欢

转载自blog.csdn.net/csdniter/article/details/109788469