本着构建一颗二叉树的目的是为了更快的搜索数据,因此我们在构建二叉树之初就应该构建一颗有序的二叉树。
首先,我们来构建一颗二叉查找树。
二叉查找树(Binary Search Tree)是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
1. 构建二叉查找树,打印构建结果: package main import "fmt" type binarysearchtree struct { value int left, right *binarysearchtree } func Newbinarysearchtree(rootvaue int) *binarysearchtree { return &binarysearchtree{value: rootvalue} } func (t *binarysearchtree) Insert(value int) *binarysearchtree { if t == nil { t = Newbinarysearchtree(value) return t } if value < t.value { t.left = t.left.Insert(value) }else { t.right = t.right.Insert(value) } return t }
func (t *binarysearchtree) Getall() []int { value := []int{} return Appendvalues(value, t) } func Appendvalues(values []int, t *binarysearchtree) []int { if t != nil { values = Appendvalues(values, t.left) values = append(values, t.value) values = Appendvalues(values, t.right) } return values }
func main() { binaryTree := Newbinarysearchtree(50) binaryTree.Insert(20) binaryTree.Insert(10) binaryTree.Insert(100) binaryTree.Insert(60) binaryTree.Insert(70) binaryTree.Insert(5) binaryTree.Insert(35) binaryTree.Insert(40) fmt.Println(binaryTree.Getall()) } 输出结果为: 2. 查看是否包含某一个元素 func (t *binarysearchtree) Contains(value int) bool { if t == nil { return false } v := t.compareTo(value) if v < 0 { return t.left.Contains(value) }else if v > 0 { return t.right.Contains(value) }else { return true } } func (t *binarysearchtree)compareTo(value int) int { return value-t.value }
输入35,打印查找结果为:
3. 从二叉搜索树中移除某个元素 func (t *binarysearchtree) Remove(value int) *binarysearchtree { if t == nil { return t } compareresult := t.compareTo(value) if compareresult < 0 { t.left = t.left.Remove(value) }else if compareresult > 0 { t.right = t.right.Remove(value) }else if t.left != nil && t.right != nil { t.value = t.right.Findmin() t.right = t.right.Remove(t.value) }else if t.left != nil { t = t.left }else { t = t.right } return t }
输入35,打印删除结果为: 4. 查找二叉搜索树中的最大值 func (t *binarysearchtree) Findmax() int { if t == nil { fmt.Println("tree is empty") return -1 } if t.right == nil { return t.value }else { return t.right.Findmax() } }
打印结果:
但是当数据接近有序数据时,二叉查找树的查找效率和顺序查找相当,这是无法忍受的。造成这种情况的主要原因是二叉查找树左右子树高度差太大。基于此,AVL树就诞生了,AVL树是带有平衡条件的二叉查找树,每个节点的左子树和右子树的高度最多差1。
当一个二叉查找树中插入一个数据时,使其成为不平衡二叉树一共有4中可能:
1. 对root节点的左儿子的左子树进行一次插入
2.对root节点的左儿子的右子树进行一次插入
3.对root节点的右儿子的左子树进行一次插入
4.对root节点的右儿子的右子树进行一次插入
对于以上出现的四种情况,使用单旋转和双旋转两种方式来解决。
1. 单旋转来解决1和4情况的。
如图此时k1为不平衡点,左树-右树=2,因此我们需要将右树加深,将左树变浅。这时,我们使用k2代替k1的位置,因为k1>k2,所以将k1变为k2右节点,Y大于k2小于k1所以Y变为k1的左节点 。变平衡后为:
2. 使用双旋转来解决2和3两种情况。
遇到2和3情况使用单旋转已经做不到了。这个时候我们需要将k3往上移,我们把k3可以看成新的根,当我们把k3移到k1的位置,此刻我们应该如何变化呢?k1<k3所以k3的左孩子为k1,k2>k3所以k3的左孩子为k2,B大于k1小于k3,所以B为k1的右子树,C大于3小于k2,所以C为k2的左子树。双旋转后:
构建平衡二叉树的AVL代码为: package main import ( "fmt" ) type avlTreeNode struct { key int high int left *avlTreeNode right *avlTreeNode } func NewAVLTreeNode(value int) *avlTreeNode{ return &avlTreeNode{key:value} } func highTree(p *avlTreeNode) int { if p == nil { return -1 } else { return p.high } } func max(a, b int) int { if a > b { return a } else { return b } } // look LL func left_left_rotation(k *avlTreeNode) *avlTreeNode { var kl *avlTreeNode kl = k.left k.left = kl.right kl.right = k k.high = max(highTree(k.left), highTree(k.right)) + 1 kl.high = max(highTree(kl.left), k.high) + 1 return kl } //look RR func right_right_rotation(k *avlTreeNode) *avlTreeNode { var kr *avlTreeNode kr = k.right k.right = kr.left kr.left = k k.high = max(highTree(k.left), highTree(k.right)) + 1 kr.high = max(k.high, highTree(kr.right)) + 1 return kr } func left_righ_rotation(k *avlTreeNode) *avlTreeNode { k.left = right_right_rotation(k.left) return left_left_rotation(k) } func right_left_rotation(k *avlTreeNode) *avlTreeNode { k.right = left_left_rotation(k.right) return right_right_rotation(k) } //insert to avl func avl_insert(avl *avlTreeNode, key int) *avlTreeNode { if avl == nil { avl = NewAVLTreeNode(key) } else if key < avl.key { avl.left = avl_insert(avl.left, key) if highTree(avl.left)-highTree(avl.right) == 2 { if key < avl.left.key { //LL avl = left_left_rotation(avl) } else { // LR avl = left_righ_rotation(avl) } } } else if key > avl.key { avl.right = avl_insert(avl.right, key) if (highTree(avl.right) - highTree(avl.left)) == 2 { if key < avl.right.key { // RL avl = right_left_rotation(avl) } else { fmt.Println("right right", key) avl = right_right_rotation(avl) } } } else if key == avl.key { fmt.Println("the key", key, "has existed!") } //notice: update high(may be this insert no rotation, so you should update high) avl.high = max(highTree(avl.left), highTree(avl.right)) + 1 return avl } //display avl tree key func display(avl *avlTreeNode) []int{ return appendValues([]int{},avl) } func appendValues(values []int, avl *avlTreeNode) []int{ if avl != nil { values = appendValues(values,avl.left) values = append(values,avl.key) values = appendValues(values,avl.right) } return values } func main() { data := []int{10, 20, 30, 40, 50, 60} root := NewAVLTreeNode(5) for _, value := range data { root = avl_insert(root, value) } fmt.Println(display(root)) }
当将数值为10, 20, 30, 40, 50, 60的节点插入到平衡二叉树中后得到的运行结果为:
二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度。能否找到一种折中的办法即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢?-----------------红黑树
红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!
红黑树是满足如下条件的二叉查找树(binary search tree):
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色
- 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
- 对于每个节点,从该点至
null
(树尾端)的任何路径,都含有相同个数的黑色节点。
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。
当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)。
左旋的过程是将x
的右子树绕x
逆时针旋转,使得x
的右子树成为x
的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。右旋同理。
红黑树结构的建立
红黑树结构体:
节点有红黑两色,我们先定义2个常量。布尔型足够:
const (
// RED 红树设为true
RED bool = true
// BLACK 黑树设为false
BLACK bool = false
)
然后是节点结构,包括树共有的特性:节点的值,指向父节点、左右儿子节点的3个指针;还有红黑树特有的:颜色。下面是树的结构:
// RBNode 红黑树
type RBNode struct {
value int64
color bool
left, right, parent *RBNode
}
树的结构只包含一个根节点Root,代码结构如下:
type RBTree struct {
root *RBNode
}
二叉树的旋转代码如下:
// rotate() true左旋/false右旋
// 若有根节点变动则返回根节点
func (rbnode *RBNode) rotate(isRotateLeft bool) (*RBNode, error) {
var root *RBNode
if rbnode == nil {
return root, nil
}
if !isRotateLeft && rbnode.left == nil {
return root, errors.New("右旋左节点不能为空")
} else if isRotateLeft && rbnode.right == nil {
return root, errors.New("左旋右节点不能为空")
}
parent := rbnode.parent
var isleft bool
if parent != nil {
isleft = parent.left == rbnode
}
if isRotateLeft {
grandson := rbnode.right.left
rbnode.right.left = rbnode
rbnode.parent = rbnode.right
rbnode.right = grandson
} else {
grandson := rbnode.left.right
rbnode.left.right = rbnode
rbnode.parent = rbnode.left
rbnode.left = grandson
}
// 判断是否换了根节点
if parent == nil {
rbnode.parent.parent = nil
root = rbnode.parent
} else {
if isleft {
parent.left = rbnode.parent
} else {
parent.right = rbnode.parent
}
rbnode.parent.parent = parent
}
return root, nil
}
红黑树的插入操作主要分为两个部分,首先是一个查找树的插入,然后是分治法进行树的变化以符合红黑树特征。
节点的插入主要检查以下几种情况:
1. 要检查的节点没有父节点,意为此节点为root,则设置此节点的颜色为黑色(一开始提到过,插入时的节点初始时都是红色),直接返回,若不是根节点,则进行情况2的检查。
2. 如果添加节点的父节点是黑色,那就省事儿多了。插入的是红色,不影响黑色数量,且由于父节点是黑色,不会出现父子节点都是红色的情况。
3. 若添加点的父节点也是红色,那就得考虑考虑了,这里应用了分治法。
func (rbtree *RBTree) insertNode(pnode *RBNode, data int64) {
if pnode.value >= data {
// 插入数据不大于父节点,插入左节点
if pnode.left != nil {
rbtree.insertNode(pnode.left, data)
} else {
tmpnode := NewRBNode(data)
tmpnode.parent = pnode
pnode.left = tmpnode
rbtree.insertCheck(tmpnode)
}
} else {
// 插入数据大于父节点,插入右节点
if pnode.right != nil {
rbtree.insertNode(pnode.right, data)
} else {
tmpnode := NewRBNode(data)
tmpnode.parent = pnode
pnode.right = tmpnode
rbtree.insertCheck(tmpnode)
}
}
}
func (rbtree *RBTree) insertCheck(node *RBNode) {
if node.parent == nil {
// 检查1:若插入节点没有父节点,则该节点为root
rbtree.root = node
// 设置根节点为black
rbtree.root.color = BLACK
return
}
// 父节点是黑色的话直接添加,红色则进行处理
if node.parent.color == RED {
if node.getUncle() != nil && node.getUncle().color == RED {
// 父节点及叔父节点都是红色,则转为黑色
node.parent.color = BLACK
node.getUncle().color = BLACK
// 祖父节点改成红色
node.getGrandParent().color = RED
// 递归处理
rbtree.insertCheck(node.getGrandParent())
} else {
// 父节点红色,父节点的兄弟节点不存在或为黑色
isleft := node == node.parent.left
isparentleft := node.parent == node.getGrandParent().left
if !isleft && isparentleft {
rbtree.rotateLeft(node.parent)
rbtree.rotateRight(node.parent)
node.color = BLACK
node.left.color = RED
node.right.color = RED
} else if isleft && !isparentleft {
rbtree.rotateRight(node.parent)
rbtree.rotateLeft(node.parent)
node.color = BLACK
node.left.color = RED
node.right.color = RED
} else if isleft && isparentleft {
node.parent.color = BLACK
node.getGrandParent().color = RED
rbtree.rotateRight(node.getGrandParent())
} else if !isleft && !isparentleft {
node.parent.color = BLACK
node.getGrandParent().color = RED
rbtree.rotateLeft(node.getGrandParent())
}
}
}
}
红黑树多用在内部排序,在数据较小,可以完全放到内存中时,红黑树的时间复杂度相比于其他算法比较优越,但当数据量较大,外存中占主要部分时,红黑树这种结构并不适用,在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。为什么会出现这样的情况,我们知道要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树可以有多个子女,从几十到上千,可以降低树的高度。B树因其读磁盘次数少,而具有更快的速度。