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,来表示下图的二叉树:


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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 树的概述 树是一种非常常用的数据结构,树与前面介绍的线性表,栈,队列等线性结构不同,树是一种非线性结构 1.树的定...
    Jack921阅读 4,432评论 1 31
  • 提示:本篇的原文已经在github上有所更新,想看最新版的朋友们抱歉了... 二叉查找树(英语:Binary Se...
    云抱住阳光太阳没放弃发亮阅读 1,020评论 0 2
  • 概述# 二叉树是一种特殊的树型结构,它由结点的有限集合构成。 二叉树是由唯一的起始结点引出的结点集合。这个起始节点...
    长胖的鱼阅读 1,080评论 0 8
  • 在谈具体的激励措施之前,我觉得应该探讨一下激励或惩罚的本质。 商鞅变法后的秦朝,老百姓闻战则喜,因为“能得甲首一者...
    思想磨坊阅读 395评论 0 0
  • 找工作被拒、相亲失败,恩美的开启了失眠模式。人生到底该怎样走,成了她日落以后、日出之前整夜思考的问题。 早上6点半...
    THISIMPLE阅读 467评论 0 0