搜索树数据结构支持许多动态集合操作,包括search、minimum、maximum、predecessor、successor、insert和delete等。因此,我们使用一棵搜索树即可以作为一个字典又可以作为一个优先队列。
二叉搜索树上的基本操作所花费的时间与这棵树德高度成正比。对于有n个结点的一棵完全二叉树来说,这些操作的最坏运行时间为o(lgn)。然而,如果这棵树是一条n个结点组成的线性链,那么同样的操作就要花费o(n)的最坏运行时间。
1 什么是二叉搜索树
一棵二叉搜索树是以一棵二叉树来组织的,如图所示。
这样一棵二叉树可以使用一个链表数据结构来表示,其中每个结点就是一个对象。除了key和核心数据外,每个结点还包含属性left、right和p,它们分别指向结点的左孩子、右孩子和父结点。如果某个孩子结点和父结点不存在,则相应属性的值为nil。根结点是树中唯一父指针为nil的结点。
1.1 结点YJTreeItem
根据结点的相关属性,我们设计树中的结点,这里我使用YJTreeItem表示。
//
// YJTreeItem.swift
// BinarySearchTree
//
// CSDN:http://blog.csdn.net/y550918116j
// GitHub:https://github.com/937447974/Blog
//
// Created by yangjun on 15/11/19.
// Copyright © 2015年 阳君. All rights reserved.
//
import Cocoa
/// 树结点
class YJTreeItem: NSObject {
/// 关键字
var key: Int!
/// 父结点
var parentItem: YJTreeItem?
/// 左孩子结点
var leftItem: YJTreeItem?
/// 右孩子结点
var rightItem: YJTreeItem?
}
1.2 二叉搜索树的存储
如图所示,二叉搜索树的关键字总是以满足二叉搜索树性质的方式来存储。
设x是二叉搜索树中的一个结点。如果y是x左子树的一个结点,那么y.key<=x.key。如果y是x右子树中的一个结点,那么y.key>=x.key。
因此,图中根结点为35,左子树10、15、25、20和30,它们均不大于35;而在右子树中有关键字45、40和50,它们均不小于35.这个性质对树中的每个结点都成立。
1.3 二叉搜索树的遍历
二叉搜索树性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序遍历算法。这样命名的原因是输出的子树根的关键字位于其左子树的关键字值和右子树的关键字值之间。类似地,先序遍历中输出的根的key在其左右子树key之前,而后序遍历输出的根key在其左右子树key之后。
下面用inorderWalk方法实现中序遍历,这里我们使用YJBinarySearchTree代表二叉搜索树,并且使用属性rootItem为树的根结点。
//
// YJBinarySearchTree.swift
// BinarySearchTree
//
// CSDN:http://blog.csdn.net/y550918116j
// GitHub:https://github.com/937447974/Blog
//
// Created by yangjun on 15/11/18.
// Copyright © 2015年 阳君. All rights reserved.
//
import Cocoa
/// 二叉搜索树
class YJBinarySearchTree {
/// 根结点
private var rootItem: YJTreeItem?
// MARK: - 中序遍历
/// 中序遍历
///
/// - returns: void
func inorderWalk() {
self.inorderWalk(self.rootItem)
}
private func inorderWalk(item: YJTreeItem?) {
if item != nil {
// 中序遍历
self.inorderWalk(item!.leftItem)
print(item!.key)
self.inorderWalk(item!.rightItem)
}
}
}
如果rootItem是一棵有n个结点树的根,那么调用inorderWalk()需要o(n)时间。
2 查询二叉搜索树
在二叉搜索树中查找一个关键字时,除了search操作外,二叉搜索树还支持诸如minimum、maximum、predecessor和successor等查询操作。
2.1 查找
// MARK: - 查找结点
/// 查找结点
///
/// - parameter key : 要查找的key
///
/// - returns: YJTreeItem
func search(key: Int) -> YJTreeItem? {
var item = self.rootItem
while item != nil && key != item?.key {
if item?.key > key { // 当前结点大于key时搜索左孩子
item = item?.leftItem
} else { // 否则进入右孩子
item = item?.rightItem
}
}
return item
}
2.2 最大关键字元素和最小关键字元素
// MARK: - 获取最小结点
/// 获取最小结点
///
/// - returns: YJTreeItem
func minimum() -> YJTreeItem? {
return self.minimum(self.rootItem)
}
// MARK: 根据结点获取其最小结点
private func minimum(var item:YJTreeItem?) -> YJTreeItem? {
while item?.leftItem != nil {
item = item?.leftItem
}
return item
}
// MARK: 获取最大结点
/// 获取最大结点
///
/// - returns: YJTreeItem
func maximum() -> YJTreeItem? {
return self.maximum(self.rootItem)
}
// MARK: 根据结点获取其最大结点
private func maximum(var item:YJTreeItem?) -> YJTreeItem? {
while item?.rightItem != nil {
item = item?.rightItem
}
return item
}
2.3 前驱和后继
// MARK: - 查找x的前驱
/// 查找x的前驱
///
/// - parameter x : 要查找的结点
///
/// - returns: YJTreeItem
func predecessor(var x:YJTreeItem?) -> YJTreeItem? {
// 1.有左子树,则后继为x左子树中的最右结点
if x?.leftItem != nil {
return self.maximum(x!.leftItem)
}
// 2.无左子结点,则后继为其有右结点的根结点
var y = x?.parentItem
while y != nil && x == y?.leftItem {
x = y
y = y?.parentItem
}
return y
}
// MARK: 查找x的后继
/// 查找x的后继
///
/// - parameter x : 要查找的结点
///
/// - returns: YJTreeItem
func successor(var x:YJTreeItem?) -> YJTreeItem? {
// 1.有右子树,则后继为x右子树中的最左结点
if x?.rightItem != nil {
return self.minimum(x!.rightItem)
}
// 2.无右子结点,则后继为其有左结点的根结点
var y = x?.parentItem
while y != nil && x == y?.rightItem {
x = y
y = y?.parentItem
}
return y
}
在一棵树高为h的二叉搜索树上,动态集合上的操作minimum、maximum、predecessor和successor可以在O(h)时间内完成。
3 插入和删除
插入和删除操作会引起由二叉树表示的动态集合的变化。一定要修改数据结构来反映这个变化,但修改要保持二叉搜索树性质的成立。
3.1 插入
要将一个新值key插入到一棵二叉搜索树中,需要方法insert。该方法以结点z作为输入,其中z.key=key,z.leftItem=nil,z.rightItem=nil。这个方法要修改rootItem和z的某些属性,来把z插入到树中的相应位置上。
// MARK: - 插入key
/// 插入key
///
/// - parameter key : 要插入的关键字
///
/// - returns: void
func insert(key: Int) {
/// 临时根结点
var x = self.rootItem
/// 插入key的父结点
var y: YJTreeItem?
/// 要插入key的z结点
let z = YJTreeItem()
z.key = key
// 找到y结点
while x != nil {
y = x
if key < x?.key {
x = x?.leftItem
} else {
x = x?.rightItem
}
}
// 设置父结点
z.parentItem = y
// 设置子结点
if y == nil {
self.rootItem = z
} else if key < y?.key {
y?.leftItem = z
} else {
y?.rightItem = z
}
}
3.2 删除
从一棵二叉搜索树中删除一个结点z的整个策略分为如下几步。
- 如果z没有左孩子,那么用其右孩子来替换z,这个右孩子可以是nil。
- 如果z仅有一个孩子且为左孩子,那么用其左孩子来替换z。
- z既有左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中,并且没有左孩子。现在我们需要将y移出原来的位置进行拼接,并替换树中的z。
- 如果y是z的右孩子,那么用y替换z,并留下y的右孩子。
- 否则,y位于z的右子树中,但并不是z的右孩子。在这种情况下,先用y的右孩子替换y,然后用y替换z。
// MARK: - 删除结点
/// 删除结点
///
/// - parameter key : 要删除的结点
///
/// - returns: void
func delete(key: Int) {
let z = self.search(key)
if z == nil {
return
}
if z!.leftItem == nil {
//1 如果z没有左孩子,则用其右孩子代替z
self.transplant(z!, v: z!.rightItem)
} else if z?.rightItem == nil {
//2 如果z没有右孩子,则用其左孩子代替z
self.transplant(z!, v: z!.leftItem)
} else {
//3 z即有左孩子又有右孩子,则用z的后继y替换它
let y = self.minimum(z?.rightItem)
//4 如果y是z的右孩子,用y替换z,并仅留下y的右孩子
if y!.parentItem != z {
//5 否则,y位于z的右子树中但不是z的右孩子,在这种情况先用y的右孩子替换y。再用y替换z。
self.transplant(y!, v: y?.rightItem)
y?.rightItem = z?.rightItem
y?.rightItem?.parentItem = y
}
// y替换z
self.transplant(z!, v: y)
y?.leftItem = z?.leftItem
y?.leftItem?.parentItem = y
}
}
// MARK: v替换u的位置
private func transplant(u: YJTreeItem, v: YJTreeItem?) {
// 设置u的父结点的子结点为v
if u.parentItem == nil {
self.rootItem = v
} else if u == u.parentItem?.leftItem {
u.parentItem!.leftItem = v
} else {
u.parentItem!.rightItem = v
}
// 设置v的父结点为u的父结点
if v != nil {
v!.parentItem = u.parentItem
}
}
方法transplant是用另一棵子树替换一棵子树并成为其父结点的孩子结点。
在一棵高度为h的二叉搜索树上,实现动态集合操作insert和delete的运行时间为O(h)。
4 小结
本博文主要介绍关于二叉搜索树的相关概念,并实现search、minimum、maximum、predecessor、successor、insert和delete等基本操作。
其他
源代码
https://github.com/937447974/Algorithms
参考资料
文档修改记录
时间 | 描述 |
---|---|
2015-11-19 | 二叉搜索树算法研究,项目完成 |
2015-11-20 | 博文完成 |