Swift纯函数式数据结构

前言

今天我们会定义可递归的枚举,向大家展示,如何利用枚举来定义一些高性能且非可变的数据结构(纯函数式数据结构)

二叉树

  • 二叉树的每个结点至多只有二棵子树,二叉树的子树有左右之分,通常被称为左子树和右子树,次序不能颠倒。
  • 二叉树通常被用于实现二叉查找树和二叉堆。
  • 二叉树是递归定义的,在逻辑上二叉树有五种基本形态。
图1.png
二叉树的相关术语(只列举了后面会用到的部分)
  • 树的节点:包含一个数据元素以及若干指向子树的分支
  • 结点层:根结点的层定义为1,根的孩子为第二层结点,依次类推
  • 结点的度:结点子树的个数
  • 树的深度:树中最大的节点层
  • 叶子结点:也叫终端结点,是度为0的结点
二叉树的特殊形态
  • 完全二叉树:若设二叉树的高度为h,除第h层之外,其他各层的结点数都达到最大数,第h层有叶子结点,并且叶子结点都是从左到右依次排布。
  • 满二叉树:除了叶子结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。


    图2.png
  • 二叉搜索树:他或者是一颗空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,若它的右子树上所有结点的值均大于它的根结点的值;它的左右子树也分别二叉搜索树。
图3.png
二叉树的遍历

先定义一个二叉树的类

class TreeNode {
    var value: Int?
    var  left:TreeNode?
    var right:TreeNode?
    var flag: Bool = false//标记是否遍历了该结点的右结点
    init(val:Int){
        self.value = val
        self.left = nil
        self.right = nil
    }
}

先序遍历(根左右)

/**
 用栈实现前序遍历
 
 - parameter root: 二叉树
 
 - returns: 前序遍历的结果
 */
func preOrderTraversal(root:TreeNode?) -> [Int]{
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    while !stack.isEmpty || node != nil {
        if (node != nil) {
            res.append((node?.value)!)
            stack.append(node!)
            node = node?.left
        }else{
            node = stack.removeLast().right
        }
    }
    return res
}

中序遍历 (左根右)

/**
 用栈实现中序遍历
 
 - parameter root: 二叉树
 
 - returns: 中序遍历的结果
 */
func inOrderTraversal(root:TreeNode?) -> [Int] {
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    while !stack.isEmpty || node != nil{
        while node != nil {
            stack.append(node!)
            node = node?.left
        }
        node = stack.removeLast()
        res.append((node?.value)!)
        node = node?.right
    }
    return res
}

后序遍历 (左右根)

/**
 用栈实现后序遍历
 
 - parameter root: 二叉树
 
 - returns: 遍历的结果
 */
func postOrderTraversal(root: TreeNode?) -> [Int]{
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    
    if !stack.isEmpty || node != nil {
        while node != nil {
            stack.append(node!)
            stack[stack.endIndex - 1].flag = false
            node = node?.left
        }
        while !stack.isEmpty {
            node = stack[stack.endIndex - 1]
            
            while ((node?.right != nil) && (node?.flag == false)) {
                stack[stack.endIndex - 1].flag = true
                node = node?.right
                while (node != nil) {
                    stack.append(node!)
                    stack[stack.endIndex - 1].flag = false
                    node = node?.left
                }
            }
            node = stack[stack.endIndex - 1]
            res.append((node?.value)!)
            print(res)
            stack.removeLast()
        }
    }
    
    return res
}

测试

let tree = TreeNode.init(val: 2)
tree.left = TreeNode.init(val: 3)
tree.right = TreeNode.init(val: 4)

let tree1 = TreeNode.init(val: 1)
tree1.left = tree
tree1.right = TreeNode.init(val: 5)

preOrderTraversal(tree1)//[1,2,3,4,5]
inOrderTraversal(tree1)//[3,2,4,1,5]
postOrderTraversal(tree1)//[3,4,2,5,1]

进入正题

在swift发布的时候,并木有类似于Objective-C中NSSet的库来处理无序集合(Set),今天的目标是用递归式枚举来定义高效的数据结构。
在我们的迷你库中会实现以下四种操作:

  • empty:返回一个空的无序集合
  • isEmpty:检查一个无序集合是否为空
  • contains:检查无序集合中是否包含某个元素
  • insert:向无序集合中插入一个元素

对于上面的功能我们可以用数组来实现:

//使用数组来表示无序集合
func empty<Element>() -> [Element] {
    return []
}

func isEmpty<Element>(set: [Element]) -> Bool {
    return set.isEmpty
}

func contains<Element: Equatable>(x: Element, _ set: [Element]) -> Bool {
    return set.contains(x)
}

func insert<Element: Equatable>(x: Element, _ set:[Element]) -> [Element] {
    return contains(x, set) ? set : [x] + set
}

优点:实现简单
缺点:大部分的操作的性能消耗与无序集合的大小是线性相关的
解决方案一:确保数组是经过排序的,然后使用二分查找来定位特定元素
解决方案二:二叉搜索树(他或者是一颗空树,或者是具有下列性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,若它的右子树上所有结点的值均大于它的根结点的值;它的左右子树也分别二叉搜索树(递归)。)

由于今天的目标是用递归式枚举来定义高效的数据结构,所以说我们采取解决方案二来解决这个问题。首先先定义一个二叉搜索树的枚举:

//indirect:2.0新增的一个类型。允许将枚举中一个case的关联值再次定义为枚举
indirect enum BinarySearchTree<Element: Comparable> {
    case Leaf//空树
    case Node(BinarySearchTree<Element>, Element, BinarySearchTree<Element>)//非空树,左右子树都是二叉搜索树
}

extension BinarySearchTree {
    init() {
        self = .Leaf
    }
    
    init(_ value: Element) {
        self = .Node(.Leaf, value, .Leaf)
    }
}
//过滤,是否满足某个条件
extension SequenceType {
    func all(predicate: Generator.Element -> Bool) -> Bool {
        for x in self where !predicate(x) {
            return false
        }
        return true
    }
}
//检查一颗树是不是二叉树
extension BinarySearchTree where Element: Comparable {
    var isBST: Bool {
        switch self {
        case .Leaf:
            return true
        case let .Node(left, x, right):
            return left.elements.all { y in y < x }
                && right.elements.all { y in y > x }
                && left.isBST
                && right.isBST
        }
    }
}

我们来尝尝递归给我们带来的便利吧,我们来计算一棵树中存在的节点个数和所有的元素组成的数组:

//节点的总数
extension BinarySearchTree {
    var count: Int {
        switch self {
        case .Leaf:
            return 0
        case let .Node(left, _, right):
            return 1 + left.count + right.count
        }
    }
}
//所有的元素组成的数组
extension BinarySearchTree {
    var elements: [Element] {
        switch self {
        case .Leaf:
            return []
        case let .Node(left, x, right):
            return left.elements + [x] + right.elements
        }
    }
}
装完*,我们回归正题
  • 检查一棵树是否为空
//检查一颗树是否为空
extension BinarySearchTree {
    var isEmpty: Bool {
        if case .Leaf = self {
            return true
        }
        return false
    }
}
  • 是否包含某个元素
    (这里需要利用到二叉搜索树的性质,如果是空树,直接返回false。不是空树的话,需要查找的元素和当前的节点相比较,值大于当前的节点话,就往右节点继续比较,值如果小于当前的节点的话,就往左节点继续比较。如果在比较的途中,遇到值和当前节点的值相同的话就返回true。如果走到底都没有找到的话就返回false)
//是否包含某个元素
extension BinarySearchTree {
    func contains(x: Element) -> Bool {
        switch self {
        case .Leaf:
            return false
        case let .Node(_, y, _) where x == y:
            return true
        case let .Node(left, y, _) where x < y:
            return left.contains(x)
        case let .Node(_, y, right) where x > y:
            return right.contains(x)
        default:
            fatalError("The impossible occurred")
        }
    }
}
  • 插入操作(原理也是利用它的性质)
//插入操作
extension BinarySearchTree {
    mutating func insert(x: Element) {
        switch self {
        case .Leaf:
            self = BinarySearchTree(x)
        case .Node(var left, let y, var right):
            if x < y { left.insert(x) }
            if x > y { right.insert(x) }
            self = .Node(left, y, right)
        }
    }
}

来继续装一波,我们来测试一下刚刚我们写的这些方法:
首先初始化一棵树:

let myTree: BinarySearchTree<Int> = BinarySearchTree()

将myTree复制到另一个树,我们来看看mutating大法的好:

var copied = myTree
copied.insert(5)
//实际的值没有改变,被修改的只是变量
print(myTree.elements, copied.elements)//"[],[5]"

我们循环往这棵树里面插入一些值:

for i in 1...10{
    copied.insert(i)
}

检查某个元素是否在这棵树里面:

print(copied.contains(5))//true
print(copied.contains(15))//false

基于字典树的自动补全

在了解了二叉树之后,下面会演示一个更加高级的纯函数式数据结构。我们就用自动补全来练手。需要达到的目标:在给定的一组搜索的历史纪录和一个待搜索字符串的前缀时,计算出一个与之相匹配的补全列表。
同样使用数组也可以实现,使用二叉搜索树也可以实现,但是数组的话依旧还是那个问题,效率不是很高,二叉搜索树前面也讲过了,这里我们换一种更加适合处理这类问题的字典树来解决。

  • 字典树:又称为查找树,典型应用就是用于统计、排序和保存大量的字符串。经常被搜索引擎系统用于文本词频的统计,他的优点就是利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比较高。
    字典树的性质:根节点外每一个节点都只包含一个字符;从根节点到某一个节点,路径上经过的字符连起来,为该节点对应的字符串,每个节点包含的所有子节点包含的字符串都不相同。
图4.png

上面的这张图是由b,abc,bcd,abcd,efg,hii这6个单词构建的树。对于每一个节点,从根遍历到它的过程就是一个单词,如果这个节点被标记为红色,就代表这个单词存在,否则就不存在。对于一个单词,我们只要顺着它从根走到对应的节点,再看这个节点是不是红色就可以知道它是否出现过了。在把这个节点标记为红色,就相当于插入了这个单词。
简单的了解了字典树之后,我们应该如何在swift里面表示一棵字典树呢?结构体是个不错的选择。

struct Trie<Element: Hashable> {
    let isElement: Bool//当前节点的字符串是否已经存在在树中
    let children: [Element: Trie<Element>]//节点处的字符与子字典树的映射关系
}

字典树的init()方法

//空的字典树
extension Trie {
    init() {
        isElement = false
        children = [:]
    }
}

计算出所有子树的所有元素,首先我们会检查当前的根结点是否被标记为一棵字典树的成员,如果是,这个字典树就包含一个空的键,反之result就被实例化为一个空的数组。然后遍历字典计算子树中的所有元素。

//计算出子树的所有元素
extension Trie {
    var elements: [[Element]] {
        var result: [[Element]] = isElement ? [[]] : []
        for (key, value) in children {
            result += value.elements.map { [key] + $0 }
        }
        return result
    }
}

接下来 我们要开始装*之旅了。我们来定义查询和插入的函数,我们的字典树定义成了递归的结构体,但是数组却不能递归。所以说我们在放大招之前,要做点准备工作。

//遍历数组:我大元组法来辅助
extension Array {
    var decompose: (Element, [Element])? {
        return isEmpty ? nil : (self[startIndex], Array(self.dropFirst()))
    }
}

准备工作做完之后就可以定义查询函数了,啦啦啦……

//辅助已放好大招
//查询
extension Trie {
    func lookup(key: [Element]) -> Bool {
        guard let (head, tail) = key.decompose else { return isElement }
        guard let subtrie = children[head] else { return false }
        return subtrie.lookup(tail)
    }
}

比如说买东西去搜索的时候都是打一个“包包”。。然后搜索结果的列表就会出现“包包斜挎小包”“包包新款手提”“包包新款”“包包双肩”……所以说我们来小小的修改一哈,使其返回一个含有所有匹配元素的子树。

extension Trie {
    func withPrefix(prefix: [Element]) -> Trie<Element>? {
        guard let (head, tail) = prefix.decompose else { return self }
        guard let remainder = children[head] else { return nil }
        return remainder.withPrefix(tail)
    }
}

一切工作准备就绪,我们就可以团战了,我们的目标不是推塔而是:在给定的一组搜索的历史纪录和一个待搜索字符串的前缀时,计算出一个与之相匹配的补全列表。

extension Trie {
    func autocomplete(key: [Element]) -> [[Element]] {
        return withPrefix(key)?.elements ?? []
    }
}

现在我们就要验证一下我们的目标是否达到了,首先创建字典树:
( 如果传入的键组不为空,并且能够分解成 (head, tail) ,我们就用tail递归去创建一颗字典树。然后创建一个新的字典children。因为key非空所以意味着当前的键组尚未被全部存入
如果传入的键组为空,我们可以创建一颗木有子节点的空字典树,用于存储一个空的字符串)

extension Trie {
    init(_ key: [Element]) {
        if let (head, tail) = key.decompose {
            let children = [head: Trie(tail)]
            self = Trie(isElement: false, children: children)
        } else {
            self = Trie(isElement: true, children: [:])
        }
    }
}

写个方法往里面插入数据
(思路:如果传入的键组不为空,且能够被分解为head和tail,我们就用tail递归地创建一棵字典树,然后创建一个新的字典children,以head为键存储这个刚才递归创建的字典树,最后,我们用这个字典树创建一棵新的字典树。因为输入的key非空,这意味着当前的键组尚未被全部存入,所以说isElement应该是false。如果传入的键组为空,我们可以创建一棵木有子节点的空字典树,用于存储一个空字符串,并将isElement赋值为true)

extension Trie {
    func insert(key: [Element]) -> Trie<Element> {
        guard let (head, tail) = key.decompose else {
            return Trie(isElement: true, children: children)
        }
        var newChildren = children
        if let nextTrie = children[head] {
            newChildren[head] = nextTrie.insert(tail)
        } else {
            newChildren[head] = Trie(tail)
        }
        return Trie(isElement: isElement, children: newChildren)
    }
}

测试测试测试……

var trie = Trie(["a","b"])
trie = trie.insert(["a","f","g"])

print(trie)//Trie<String>(isElement: false, children: ["a": Trie<Swift.String>(isElement: false, children: ["b": Trie<Swift.String>(isElement: true, children: [:]), "f": Trie<Swift.String>(isElement: false, children: ["g": Trie<Swift.String>(isElement: true, children: [:])])])])
print(trie.elements)//[["a", "b"], ["a", "f", "g"]]
print(trie.lookup(["a","b"]))//true
print(trie.lookup(["a","f","g"]))//true
print(trie.lookup(["a","f"]))//false
print(trie.autocomplete(["a"]))//[["b"], ["f", "g"]]

如果看上面的运行结果不好看,我给大家画了一个抽象派的字典树:

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

推荐阅读更多精彩内容

  • 因为之前就复习完数据结构了,所以为了保持记忆,整理了一份复习纲要,复习的时候可以看着纲要想具体内容。 树 树的基本...
    牛富贵儿阅读 6,421评论 3 10
  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,012评论 0 25
  • 内容整理于鱼c工作室教程 1. 树的基本概念 1.1 树的定义 树(Tree)是n(n>=0)个结点的有限集。 当...
    阿阿阿阿毛阅读 995评论 0 1
  • 本着尊重原作者的态度,关于静态库及动态库的制作,请参考jianshu 本篇文章主要是制作静态库以及动态库的方法 一...
    小胖子2号阅读 144评论 0 1
  • 恋爱关系中,一直小心翼翼的看着,尽量不让自己占对方的便宜,对方出了多少钱,尽量补回去。亲戚朋友人情往来心里都...
    danaom阅读 124评论 0 0