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

本文介绍了二叉树的基本概念,包括树的定义和二叉树的两种特殊形式。详细讲解了二叉树的链式存储结构和数组存储结构,分析了数组存储的优势。此外,还探讨了二叉树的应用,特别是二叉查找树的定义和Golang实现,指出在理想情况下搜索时间复杂度为O(logn),但极端情况下可能退化为O(n)的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

树的定义:
树是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)的

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值