Swift实现-Tree(树)、BaniryTree(二叉树)、BinarySearchTree(二叉搜索树)

《Swift实现-Tree(树)、BaniryTree(二叉树)、BinarySearchTree(二叉搜索树)》 图一

一、树

树是一种一对多的,一种表示对象层级关系的数据结构。

术语及特点

  • 树是有节点组成的,上一层节点是下一次节点的双亲,下一层节点是上一层节点的孩子,同一层的节点称为兄弟。
  • 有孩子的节点为普通的节点,没有孩子的节点也就是最下层的节点,称为叶子节点,没有双亲的节点称为根节点。
  • 节点拥有子节点或者子树的个数,称为这个节点的度,数的度为节点最多的度,如图一的度为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来实现二叉树

  1. 枚举实现
indirect enum BinaryTree<T: Comparable> {
    case node(BinaryTree<T>, T, BinaryTree<T>)
    case empty
}
  • 二叉树的节点分为两种状态,要么为空,要么不为空,同时有左右孩子
  • indirect关键字:在Swift中,如果创建一个值类型,系统要明确的知道这个值类型需要的内存大小,好给其分配适当的内存。但是在上面的代码中 .node中引用了它自己,这就产生了递归,这样系统就不能明确的知道这个值类型所需要的内存的大小,所以我们要用indirect关键字来标明,这样系统会为循环引用之间产生一个layer,来解决这个问题(具体咋回事没弄明白…官方文档上也没说清楚,估计就是分配了一个可变的内存吧)
    利用上面我们定义的BinaryTree,来表示下图的二叉树:

    《Swift实现-Tree(树)、BaniryTree(二叉树)、BinarySearchTree(二叉搜索树)》

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)
        }
    }
  • 删除节点
    删除节点后,如果该节点有子数,需要将子树重新连接到原来的树上,即从原来的子树中找一个接地,替代当前的节点,替换的原则是,从该节点的左子树中,找一个最大的,或者从该节点的右子树中,找一个最小的,替代原有的位置。如果该节点没有子节点,则直接删除这个节点就好了,如果实现该方法,我们需要在节点的结构中增加对父母节点的记录,这里我们就不做实现了

前面说过,二叉树因为不同功能需要,有多重实现办法,当前这种实现知识最近本的实现方法,如果需要其他更多的功能,可以给树添加更多的属性来实现具体的功能,后面会用类实现一个更完整的方法,敬请期待😜

    原文作者:sayHellooX
    原文地址: https://www.jianshu.com/p/779f0aa0f908
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞