一、树
树是一种一对多的,一种表示对象层级关系的数据结构。
术语及特点
- 树是有节点组成的,上一层节点是下一次节点的双亲,下一层节点是上一层节点的孩子,同一层的节点称为兄弟。
- 有孩子的节点为普通的节点,没有孩子的节点也就是最下层的节点,称为叶子节点,没有双亲的节点称为根节点。
- 节点拥有子节点或者子树的个数,称为这个节点的度,数的度为节点最多的度,如图一的度为3。
- 树的层级称为树的深度或高度,如图一的深度为4.
- 父节点与子节点之间,子节点的兄弟之间不能有环,这样的不称之为树,而是图。
普通树的实现
class Node<T: Comparable> {
//节点的值
var value: T
//孩子
var children = [Node]()
//双亲
weak var parent: Node?
init(_ value: T) {
self.value = value
}
//添加孩子
func add(child node: Node) {
children.append(node)
node.parent = self
}
//判断树中是否包含某个值
func search(_ value: T) -> Node? {
if value == self.value {
return self
}
for child in children {
if let result = child.search(value) {
return result
}
}
return nil
}
}
extension Node: CustomStringConvertible {
var description: String {
var text = "\(value)"
if !children.isEmpty {
text += "{ " + children.map({ $0.description }).joined(separator: ", ") + " }"
}
return text
}
}
说明:
- Node<T: Comparable> 的写法是,因为在 search(_ value: ) 方法中需要对泛型T进行比较,所以T需要遵守Comparable协议,也同样可以遵守Equatable协议,达到同样的目的
- 在extension扩展中遵守CustomStringConvertible 协议,从而去自己实现description属性,这样方便查看打印,是个很不错的调试办法
- 上面是一种比较普通的实现,节点包含有孩子及双亲属性,但是双亲是可选型,因为根节点是没有双亲的,再有双亲用weak修饰,防止双亲和自己之间产生循环引用
- 根据不同的需求,有多种表示数的方法,比如双亲表示法(只表示双亲,不标识孩子)、孩子表示法、孩子兄弟表示法等等
二、二叉树
如果一个树,每个节点都有0个或1个或两个孩子,则称为二叉树。
二叉树的子节点通常称为左孩子和右孩子。
特点
- 二叉树中没个节点最多有两个子节点
- 二叉树的左右两个节点是有序的,不能左右互换,即使有一个子节点也要区分左右
- 有一些特殊的二叉树如:斜树,满二叉树,完全二叉树
- 二叉树的第i层最多有 2的(i-1)次幂个节点
- 深度为k的二叉树,总共最多有2的(k)次幂 - 1个节点
二叉树的实现
1.类方法实现
class BinaryTreeClass<T: Comparable> {
var value: T
var leftTChild: BinaryTreeClass?
var rightChild: BinaryTreeClass?
init(_ value: T) {
self.value = value
}
}
上面是用类方法实现的二叉树,每个节点值,左孩子,及有孩子,但是也可能没有孩子,所以两个孩子是可选型,但是因为在Swift中是比较提倡用Struct 及 enum 代替类的,即用值类型,代替引用类型(当然要具体情况具体分析,但是总体是这样的),所以下面就详细的用enum来实现二叉树
- 枚举实现
indirect enum BinaryTree<T: Comparable> {
case node(BinaryTree<T>, T, BinaryTree<T>)
case empty
}
- 二叉树的节点分为两种状态,要么为空,要么不为空,同时有左右孩子
-
indirect关键字:在Swift中,如果创建一个值类型,系统要明确的知道这个值类型需要的内存大小,好给其分配适当的内存。但是在上面的代码中 .node中引用了它自己,这就产生了递归,这样系统就不能明确的知道这个值类型所需要的内存的大小,所以我们要用indirect关键字来标明,这样系统会为循环引用之间产生一个layer,来解决这个问题(具体咋回事没弄明白...官方文档上也没说清楚,估计就是分配了一个可变的内存吧)
利用上面我们定义的BinaryTree,来表示下图的二叉树:
let nodeI = BinaryTree.node(.empty, "I", .empty)
let nodeG = BinaryTree.node(.empty, "G", .empty)
let nodeH = BinaryTree.node(nodeI, "H", .empty)
let nodeJ = BinaryTree.node(.empty, "J", .empty)
let nodeD = BinaryTree.node(nodeG, "D", nodeH)
let nodeE = BinaryTree.node(.empty, "E", nodeJ)
let nodeF = BinaryTree.node(.empty, "F", .empty)
let nodeB = BinaryTree.node(nodeD, "B", .empty)
let nodeC = BinaryTree.node(nodeE, "C", nodeF)
let nodeA = BinaryTree.node(nodeB, "A", nodeC)
print(nodeA)
//value: A, A-left: [ value: B, B-left: [ value: D, D-left: [ value: G, G-left: [ ], G-right: [ ] ], D-right: [ value: H, H-left: [ value: I, I-left: [ ], I-right: [ ] ], H-right: [ ] ] ], B-right: [ ] ],
A-right: [ value: C, C-left: [ value: E, E-left: [ ], E-right: [ value: J, J-left: [ ], J-right: [ ] ] ], C-right: [ value: F, F-left: [ ], F-right: [ ] ] ]
注意代码实现的时候通常是从小向上来实现节点
- 节点个数
func count() -> Int {
switch self {
case let .node(left, _, right):
return left.count() + 1 + right.count()
case .empty:
return 0
}
}
print(nodeA.count())
//10
- 二叉树节点的遍历
有时需要以某种顺序遍历树的所有节点,这就叫树的遍历,通常以访问根节点的先后顺序,分三种方式,前序、中序及后序遍历
//MARK: -中序遍历
func traverseInOrder(process: (T) -> Void) {
if case let .node(left, value, right) = self {
left.traverseInOrder(process: process)
process(value)
right.traverseInOrder(process: process)
}
}
//MARK: -前序遍历
func traversePreOrder(process: (T) -> Void) {
if case let .node(left, value, right) = self {
process(value)
left.traversePreOrder(process: process)
right.traversePreOrder(process: process)
}
}
//MARK: -后序遍历
func traversePostOrder(process: (T) -> Void) {
if case let .node(left, value, right) = self {
left.traversePostOrder(process: process)
right.traversePostOrder(process: process)
process(value)
}
}
其中 if case let .node(left, value, right) = self 语法,同swich 语法有相同的效果,只不过现在是只判断一种case 的情况,即如果当前树不为空
三、二叉搜索树
- 特点
二叉搜索树是一种特殊的二叉树,它的左孩子的值一定小于双亲的值,右孩子的值一定大于双亲的值,即整个二叉搜索树是有序的,及时是删除及插入节点后,也同样保持有序 - 插入
在二叉搜索树中插入新的节点,要记住及维持“左孩子的值一定小于双亲的值,右孩子的值一定大于双亲的值”的特点
//1
mutating func inset(_ newValue: T) {
//2
guard case .node(var leftTree, let value, var rightTree) = self else {
self = .node(.empty, newValue, .empty)
return
}
//3
if newValue > value {
rightTree.inset(newValue)
}else if newValue < value {
leftTree.inset(newValue)
}
}
说明:
1.当我们在值类型(枚举和结构体)的实例方法中,改变其属性或者self的时候,我们需要在这个方法前面的显性的标注mutating关键字
2.如果为空节点,则给self一个新的值
3.如果当前节点不为空,则比较大小,到左或右孩子中插入
/好的我们的实现完成了,但是运行代码我们会发现,结果和我们的预期是有出入的,你所要插入的新节点并没有出现在树中/
这是因为当前的树是通过值类型实现的,当我们调用 rightTree.inset(newValue) 或 leftTree.inset(newValue) 方法时,程序会重新复制一份新的 rightTree 和 leftTree,接着去新的树上执行方法,而不是在当前树的孩子上执行。如果我们是用通过类来实现树的结构的,那么当前的方法就完全没有问题了
- 插入-改
mutating func inset(_ newValue: T) {
self = newTreeWithNewValue(newValue)
}
private func newTreeWithNewValue(_ newValue: T) -> BinaryTree {
switch self {
case .empty:
return .node(.empty, newValue, .empty)
case let .node(leftTree, value, rightTree):
if newValue < value {
return .node(leftTree.newTreeWithNewValue(newValue), value, rightTree)
}else {
return .node(leftTree, value, rightTree.newTreeWithNewValue(newValue))
}
}
}
//当每次新值插入时,都创建一棵新树,来重新赋值替代原来的树,这样就不会出问题了
但是由于每次插入都需要创建一颗新的树,所以插入时的时间复杂度为O(n),效率比较低下,如果是通过类方法创建的树,插入的时间复杂度则为O(log n)
- 搜索
func search(_ sValue: T) -> BinaryTree? {
guard case let .node(leftTree, value, rightTree) = self else {
return nil
}
if sValue == value {
return self
}
if sValue > value {
return rightTree.search(sValue)
}else {
return leftTree.search(sValue)
}
}
- 删除节点
删除节点后,如果该节点有子数,需要将子树重新连接到原来的树上,即从原来的子树中找一个接地,替代当前的节点,替换的原则是,从该节点的左子树中,找一个最大的,或者从该节点的右子树中,找一个最小的,替代原有的位置。如果该节点没有子节点,则直接删除这个节点就好了,如果实现该方法,我们需要在节点的结构中增加对父母节点的记录,这里我们就不做实现了
前面说过,二叉树因为不同功能需要,有多重实现办法,当前这种实现知识最近本的实现方法,如果需要其他更多的功能,可以给树添加更多的属性来实现具体的功能,后面会用类实现一个更完整的方法,敬请期待😜