通过之前文章,相信大家已经掌握了树这一重要的数据结构的基本概念以及很重要的二叉树。今天呢,我们就将在这个基础上,继续讲解非常非常常用的动态二叉树: 二叉查找树。
二叉查找树是个什么东西呢? 别急,下面我们将通过常规手段和非常规手段来带你了解一下。
定义
何谓二叉查找树?一种常见的定义如下:
二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树,排序二叉树,是指一棵空树或者具有下列性质的二叉树:
- 所有子树上面的左节点的值都比根结点要小,右节点的值都比根结点要大
- 任意结点的左右子树也都是二叉查找树
- 通过中序遍历,将得到的是一个有序的数列
相信看了这个所谓的定义,大家一定是云里雾里,下面我们通过一些图来直观感受一下(啥也不说了,一切尽在图中)。
依据上面的定义,我们可以画出多种形态的树,如下示:
盯着图琢磨一会, 从上图不难看出:
- 二叉查找树的特点是: 一直往左儿子往下找左儿子,可以找到最小的元素(元素1),一直往右儿子找右儿子,可以找到最大的元素(元素8), 【树的左尽头是最小, 右尽头是最大】。
- 三个图的通过中序遍历后,最终有序序列都是[1,2,3,4,5,6,7,8]
- 三个图元素是一样的,都满足二叉查找树的特性,但是形态差异很大
- 不同图查找一个元素,花费的代价不一样。比如: 查找元素8,图A,B,C查找次数依次是3次,5次和8次。
通过二叉树排序树的特性,看起来,我们可以用它来实现元素排序,但实际情况下好多时候不会这样做。因为二叉查找树不保证是一个平衡的二叉树,最坏情况下二叉查找树会退化成一个链表,也就是所有节点都没有左子树或者没有右子树,树的层次太深导致排序性能太差,如上图C
使用二分查找,可以很快在一棵二叉查找树中找到我们需要的值。
图解操作
二叉查找树也有相应的插入、查找、删除、查询最大值最小值等操作,下面,我们会通过一个个图来直观的解释,并附带相应的代码实现。
完整代码实现:https://github.com/yiye93/algo^^代码不易,希望大家给个星鼓励下^^
插入
先上图:
用白话文描述一下整体流程:
- 如果添加元素时是棵空树,那么初始化根节点。
- 然后添加的值和根节点比较,判断是要插入到根节点左子树还是右子树,还是不用插入(值相同)。
- 当值比根节点小时,元素要插入到根节点的左子树中,当值比根节点大时,元素要插入到根节点的右子树中,相等时不插入,只更新次数。
- 然后再分别对根节点的左子树和右子树进行递归操作即可。
我们先来用代码完成一棵二叉查找树的定义:
// 二叉查找树节点定义
type BinarySearchNode struct {
Value int //元素值
Nums int // 值重复次数
Left *BinarySearchNode //左子树
Right *BinarySearchNode //右子树
}
type BinarySearchTree struct {
Root *BinarySearchNode //根节点
}
func NewBinarySearchTree() *BinarySearchTree {
return &BinarySearchTree{}
}
一个节点代表一个元素,节点的Value值是用来进行二叉查找的关键,当Value值重复时,我们将值出现的次数Nums加1。
这里很关键的一点是: 存在相同的元素值。
插入代码实现:
package tree
import (
"fmt"
"github.com/yiye93/algo/queue"
)
// 插入操作
func (tree *BinarySearchTree) Insert(value int) {
if tree.Root == nil {
tree.Root = &BinarySearchNode{Value: value, Nums: 1}
return
}
// 步骤1: 找到要插入值的父节点
currentNode := tree.Root
for currentNode != nil {
// 小于子树根节点的值
if value nil {
currentNode = currentNode.Left
// 大于子树根节点的值
} else if value > currentNode.Value && currentNode.Right != nil {
currentNode = currentNode.Right
// 等于子树根节点的值
} else {
break
}
}
// 步骤2: 把插入值插入到步骤1中找到的父节点的左子树或者右子树或者只增加重复次数
if value currentNode.Left = &BinarySearchNode{Value: value, Nums: 1}
} else if value > currentNode.Value {
currentNode.Right = &BinarySearchNode{Value: value, Nums: 1}
} else {
currentNode.Nums += 1
}
return
}
// 二叉查找树打印
// 为了直观感受二叉查找树的构造结构,我们实现一个简单的打印功能
func (tree *BinarySearchTree) Print() {
if tree.Root == nil {
fmt.Printf("tree is empty!\n")
return
}
// 采用层次遍历
// 此处搞了个容量限制...
q := queue.NewQueueOnLinklist(10000)
// 头元素入队
q.EnQueue(tree.Root)
// 队列非空
for !q.IsEmpty() {
n := q.DeQueue().(*BinarySearchNode)
// 左子树非空,左子树乖乖入队
if n.Left != nil {
q.EnQueue(n.Left)
fmt.Printf("[value/nums/leftNode] is [%v/%v/%v]\n", n.Value, n.Nums, n.Left.Value)
}
// 右子树非空,左子树乖乖入队
if n.Right != nil {
q.EnQueue(n.Right)
fmt.Printf("[value/nums/rightNode] is [%v/%v/%v]\n", n.Value, n.Nums, n.Right.Value)
}
if n.Left == nil && n.Right == nil {
fmt.Printf("[value/nums/isLeafNode] is [%v/%v]\n", n.Value, n.Nums)
}
}
}
参照我们之前的流程图,我们可以实现插入部分操作如上示,并且采用了层次遍历实现了一个简单的打印功能,方便我们直观的看到插入的树结构是否跟设想一致。
下面我们看一下我们的测试代码:
- demo1
func TestBinarySearchTree1(t *testing.T) {
// **测试二叉查找树结构如下**
// 4
// 3 7
// 1 6 8
//
// 2 5
tree := NewBinarySearchTree()
fmt.Printf("test Insert...\n")
tree.Print()
//插入元素
insertElems := []int{4, 3, 7, 1, 6, 8, 2, 5}
for _, elem := range insertElems {
tree.Insert(elem)
}
tree.Insert(3)
tree.Insert(8)
tree.Insert(8)
//插入元素后打印树的结构
tree.Print()
}
测试结果:
- demo2
func TestBinarySearchTree2(t *testing.T) {
// **测试二叉查找树结构如下**
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
tree := NewBinarySearchTree()
tree.Print()
//插入元素
insertElems := []int{1, 2, 3, 4, 5, 6, 7, 8}
for _, elem := range insertElems {
tree.Insert(elem)
}
tree.Insert(1)
tree.Insert(7)
tree.Insert(7)
//插入元素后打印树的结构
tree.Print()
}
测试结果:
查找
查找元素表示从根节点开始查找元素。具体分为:
- 如果根节点为空,就直接返回空值
- 如果不为空,通过以左子树小于父节点,右子树大于父节点的特性为依据进行判断,然后不断遍历进行查找元素,直到找到目标的元素为止。
代码实现:
// 二叉查找树的搜索
func (tree *BinarySearchTree) Search(value int) *BinarySearchNode {
if tree.Root == nil {
return nil
}
currentNode := tree.Root
for currentNode != nil {
// 小于子树根节点的值
if value currentNode = currentNode.Left
// 大于子树根节点的值
} else if value > currentNode.Value {
currentNode = currentNode.Right
// 找到该值
} else {
return currentNode
}
}
return nil
}
demo测试:
func TestBinarySearchTree1(t *testing.T) {
// **测试二叉查找树结构如下**
// 4
// 3 7
// 1 6 8
//
// 2 5
tree := NewBinarySearchTree()
//fmt.Printf("test Insert...\n")
//tree.Print()
//插入元素
insertElems := []int{4, 3, 7, 1, 6, 8, 2, 5}
for _, elem := range insertElems {
tree.Insert(elem)
}
tree.Insert(3)
tree.Insert(8)
tree.Insert(8)
//插入元素后打印树的结构
//tree.Print()
// 查找
fmt.Printf("test Search...\n")
for i := 0; i <= 10; i++ {
if tree.Search(i) == nil {
fmt.Printf("search elem:%v not exist\n", i)
} else {
fmt.Printf("search elem:%v exist\n", i)
}
}
}
测试效果:
查找最大值和最小值
查找最大值和最小值比较简单: 一直往左儿子往下找左儿子,可以找到最小的元素,一直往右儿子找右儿子,可以找到最大的元素
代码实现:
// 查询最小值
func (tree *BinarySearchTree) FindMin() *BinarySearchNode {
if tree.Root == nil {
return nil
}
currentNode := tree.Root
// 一直往左子树遍历
for currentNode != nil {
if currentNode.Left != nil {
currentNode = currentNode.Left
} else {
break
}
}
return currentNode
}
// 查询最大值
func (tree *BinarySearchTree) FindMax() *BinarySearchNode {
if tree.Root == nil {
return nil
}
currentNode := tree.Root
// 一直往右子树遍历
for currentNode != nil {
if currentNode.Right != nil {
currentNode = currentNode.Right
} else {
break
}
}
return currentNode
}
测试代码:
func TestBinarySearchTree1(t *testing.T) {
// **测试二叉查找树结构如下**
// 4
// 3 7
// 1 6 8
//
// 2 5
tree := NewBinarySearchTree()
//fmt.Printf("test Insert...\n")
//tree.Print()
//插入元素
insertElems := []int{4, 3, 7, 1, 6, 8, 2, 5}
for _, elem := range insertElems {
tree.Insert(elem)
}
tree.Insert(3)
tree.Insert(8)
tree.Insert(8)
// 查找最小值和最大值
fmt.Printf("test FindMin and FindMax...\n")
fmt.Printf("find [MIN/MAX] is:[%v/%v]\n", tree.FindMin().Value, tree.FindMax().Value)
}
测试效果:
中序遍历
要实现二叉排序树的排序,我们可以通过中序遍历即可实现。这一特性,在开始就讲过,下面我们代码来实现验证下吧。
实现代码:
// 中序遍历实现排序
func (tree *BinarySearchTree) PreOrder() {
if tree.Root == nil {
return
}
tree.preOrder(tree.Root)
return
}
// 中序遍历实现排序
func (tree *BinarySearchTree) preOrder(node *BinarySearchNode) {
if node == nil {
return
}
tree.preOrder(node.Left)
fmt.Printf("travel value is:%v\n", node.Value)
tree.preOrder(node.Right)
return
}
测试代码:
func TestBinarySearchTree1(t *testing.T) {
// **测试二叉查找树结构如下**
// 4
// 3 7
// 1 6 8
//
// 2 5
tree := NewBinarySearchTree()
//fmt.Printf("test Insert...\n")
//tree.Print()
//插入元素
insertElems := []int{4, 3, 7, 1, 6, 8, 2, 5}
for _, elem := range insertElems {
tree.Insert(elem)
}
tree.Insert(3)
tree.Insert(8)
tree.Insert(8)
fmt.Printf("test PreOrder...\n")
tree.PreOrder()
测试效果:
删除
删除操作看起来让人相对头大,情况较复杂,需要分多种情况探讨。不过别担心,我们通过图来一一拆解,最终将会发现也不过是纸老虎而已。
删除有三种情况是要考虑的:
- 删除的是叶节点,也就是左右子树都为空(特殊情况下,该树也可能只有一个根节点)
- 删除的节点,可能有左子树或者有右子树(不同时有)
- 删除的节点,有左子树和右子树
那么针对上面三种情况,我们该怎么对应的去解决呢?
- 删除的是叶子节点,很明显直接删除即可。
- 删除的节点只有一个子树,那么该子树直接替换被删除的节点即可。
- 删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点,这时二叉查找树的性质又满足了。右子树的最小元素,只要一直往右子树的左边一直找一直找就可以找到。
图解:
情况1:
情况2:
情况3:
按照上述图解,以及流程说明,我们可以对照着写出实现代码,如下示:
// 查找当前节点和父节点
func (tree *BinarySearchTree) SearchAndParent(value int) (*BinarySearchNode, *BinarySearchNode) {
if tree.Root == nil {
return nil, nil
}
currentNode := tree.Root
var parentNode *BinarySearchNode = nil
for currentNode != nil {
// 小于子树根节点的值
if value parentNode = currentNode
currentNode = currentNode.Left
// 大于子树根节点的值
} else if value > currentNode.Value {
parentNode = currentNode
currentNode = currentNode.Right
// 找到该值
} else {
return currentNode, parentNode
}
}
return currentNode, parentNode
}
// 删除操作
func (tree *BinarySearchTree) Delete(value int) {
if tree.Root == nil {
return
}
// 判断值是否存在
currentNode, parentNode := tree.SearchAndParent(value)
if currentNode == nil {
return
}
// 依照三种情况分类处理
// 情况1: 该节点即为叶子节点
if currentNode.Left == nil && currentNode.Right == nil {
// 特殊情况,只有一个根节点的情况,树置空即可
if parentNode == nil {
tree.Root = nil
return
}
println("ggg")
// 如果删除的是父节点的左儿子,父节点左孩子置空即直接删除
if parentNode.Left != nil && currentNode.Value == value {
parentNode.Left = nil
return
} else {
// 删除的是父节点的右儿子,父节点右孩子置空即直接删除
parentNode.Right = nil
return
}
}
//情况2: 存在左子树或者右子树
// 左子树非空,右子树为空
if currentNode.Left != nil && currentNode.Right == nil {
// 删除的是父节点的左儿子,那么直接让当前节点的左儿子变为父节点的左儿子
if parentNode.Left != nil && currentNode.Value == value {
parentNode.Left = currentNode.Left
return
} else {
// 删除的是父节点的右儿子,那么直接让当前节点的左儿子变为父节点的右儿子
parentNode.Right = currentNode.Left
}
return
}
// 左子树为空,右子树非空
if currentNode.Left == nil && currentNode.Right != nil {
// 删除的是父节点的左儿子,那么直接让当前节点的右儿子变为父节点的左儿子
if parentNode.Left != nil && currentNode.Value == value {
parentNode.Left = currentNode.Right
return
} else {
// 删除的是父节点的右儿子,那么直接让当前节点的左儿子变为父节点的右儿子
parentNode.Right = currentNode.Right
}
return
}
//情况3: 左子树、右子树都非空
if currentNode.Left != nil && currentNode.Right != nil {
// 删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点。
// 右子树的最小元素,只要一直往右子树的左边一直找一直找就可以找到,替换后二叉查找树的性质又满足了。
minNode := currentNode.Right
for minNode.Left != nil {
minNode = minNode.Left
}
//特殊情况: 删除节点的右子树只有一个节点时,此时最小值即为该节点
if currentNode.Right.Value == minNode.Value {
currentNode.Value = minNode.Value
currentNode.Nums = minNode.Nums
currentNode.Right = nil
return
}
// 删除最小值
tree.Delete(minNode.Value)
currentNode.Value = minNode.Value
currentNode.Nums = minNode.Nums
return
}
return
}
我们增加了SearchAndParent方法用于查找一个节点的时候,顺便查找到该节点的父节点
完整代码实现
完整代码实现:https://github.com/yiye93/algo
^^代码不易,希望大家给个星鼓励下^^
总结
二叉查找树,作为树类型中一种非常重要的数据结构,有着非常广泛的应用,但是二叉查找树具有很高的灵活性,不同的插入顺序,可能造成树的形态差异比较大,如开文介绍的图C,在某些情况下会变成一个长链表,此时的查询效率会大大降低,如何解决这个问题呢,平衡二叉树、红黑树就要派上用场了(二叉查找树也是后面要学习的高级数据结构AVL树,红黑树的基础),我们会在后续的文章进行介绍。
二叉查找树可能退化为链表,也可能是一棵非常平衡的二叉树,查找,添加,删除元素的时间复杂度取决于树的高度 h。当二叉树是满的时,树的高度是最小的,此时树节点数量 n 和高度 h 的关系为:h = log(n)。当二叉树是一个链表时,此时树节点数量 n 和高度 h 的关系为:h = n。二叉查找树的效率来源其二分查找的特征,时间复杂度在于二叉树的高度,因此查找,添加和删除的时间复杂度范围为 log(n)~n。
下面放一波链接, 本人会持续在上面更新.....
link
- 公众号: 程序员的进击之路
- github: https://github.com/yiye93/algo