Swift算法俱乐部 - 树

作者:Kelvin Lau原文链接,原文日期:2016-07-11
译者:Darin4lin

Swift算法俱乐部是GitHub上的一个开源项目,旨在用Swift实现一些主流的算法和数据结构。

每个月我和Chris Pilcher将会根据这个开源项目编写一些教程,向你们展示如何编写一个cool的算法或数据结构。

第一篇教程会教你如何用Swift实现一个树型数据结构。这几乎是最常见也是最有用的数据结构,也是开始我们学习最好的方式。

看个图就明白树是什么了:

树

上图展示了个5层的树。root是第0层,往下数,层数递增1。

树可以帮你解决很多重要的问题,例如:

  • 表示对象之间的层级关系
  • 高效的搜索
  • 数据排序
  • 有效的文字前缀匹配

相关术语

我们先来看一些需要掌握的术语。

根节点(Root)

节点也就是第0层的节点。你也可以把它当做你这个树型数据结构的入口。

根节点
根节点
节点(Node)

节点是树里的一块数据。节点里数据的数据类型取决于你构造的这个树的数据类型。也是一个节点。

节点
节点
叶节点(Leaf)

节点就是一个没有子节点的节点,有时候可以把它看做一个终止节点。例如,有一个节点的左孩子(leftChild)右孩子(rightChild)都是nil,那它就是一个节点。

叶

用Swift来实现树

这一节里,你将会实现一个通用的树。声明一个没有任何限制条件(例如一个节点只能最多有几个子节点,或者节点的顺序)的树想来是极好的。

记住,树是由节点组成的。所以我们先从创建一个基本的节点类开始。新建一个Swift PlayGround然后添加如下代码:

class Node {
 
}
节点值

当然,一个节点需要存储一些数据才能显得有意义。

先让这个树简单的管理一些字符串吧。使用下面的代码更新你的Node类。

class Node {
  var value: String
 
  init(value: String) {
    self.value = value
  }
}

你已经声明了一个String类型的Name属性,还有一个init方法,用来初始化你的类中所有的非可选的存储属性。

子节点(Children)

除了一个节点值外,每个节点有需要有一个子节点的列表。

使用下面的代码更新你的类。

class Node {
  var value: String
  var children: [Node] = [] // add the children property
 
  init(value: String) {
    self.value = value
  }
}

简单的使用一个Node类型的数组来表示子节点列表。每个子节点表示一个比当前节点更深一层的节点。

父节点(Parent)

有时候有必要让每个节点保存一个到他父节点的引用。一个节点只能有一个父节点,但却可以有很多子节点。

更新代码:

class Node {
  var value: String
  var children: [Node] = []
  weak var parent: Node? // add the parent property
 
  init(value: String) {
    self.value = value
  }
}

可以注意到父节点是一个可选值。这是因为不是所有节点都有父节点 - 例如一个树的根节点。

插入节点

为了向树中插入节点,你需要在Node中定义一个add(child:)方法。更新如下:

class Node {
  var value: String
  var children: [Node] = []
  weak var parent: Node?
 
  init(value: String) {
    self.value = value
  }
 
  func add(child: Node) {
    children.append(child)
    child.parent = self
  }
}

通过PlayGround可以很好得来了解add(child:)是如何工作的。在你的Node类 外面 添加如下代码:

let beverages = Node(value: "beverages")
 
let hotBeverages = Node(value: "hot")
let coldBeverages = Node(value: "cold")
 
beverages.add(child: hotBeverages)
beverages.add(child: coldBeverages)

这里我们定义了3个不同的节点并且将他们进行逻辑分层,就如下图所示:

层次结构
层次结构

试炼:Beverage City

准备来一个知识的快速检测了吗?

尝试写一些代码来把你的树扩展成下图所示:

Beverage City
Beverage City

先自己试试如何实现吧~


let beverages = Node(value: "beverages")
 
let hotBeverage = Node(value: "hot")
let coldBeverage = Node(value: "cold")
 
let tea = Node(value: "tea")
let coffee = Node(value: "coffee")
let cocoa = Node(value: "cocoa")
 
let blackTea = Node(value: "black")
let greenTea = Node(value: "green")
let chaiTea = Node(value: "chai")
 
let soda = Node(value: "soda")
let milk = Node(value: "milk")
 
let gingerAle = Node(value: "ginger ale")
let bitterLemon = Node(value: "bitter lemon")
 
beverages.add(child: hotBeverage)
beverages.add(child: coldBeverage)
 
hotBeverage.add(child: tea)
hotBeverage.add(child: coffee)
hotBeverage.add(child: cocoa)
 
coldBeverage.add(child: soda)
coldBeverage.add(child: milk)
 
tea.add(child: blackTea)
tea.add(child: greenTea)
tea.add(child: chaiTea)
 
soda.add(child: gingerAle)
soda.add(child: bitterLemon)
打印出你的树

在控制台把树打印出来可以更好的检验你的树是否正确。在定义完你的树后,尝试直接在控制台打印出来:

print(beverages) // <- try to print it!

你可以按下Command-Shift-Y组合键打开控制台,而控制台只打印出了你的类名。

Node

蠢的哟!不幸的是编译器并不知道该如何打印你自定义的Swift类,除非你告诉了它。

为了帮助编译器,你需要让Node类实现CustomStringConvertible接口。将下面的代码添加在你的Node类下就可以了:

// 1
extension Node: CustomStringConvertible {
  // 2
  var description: String {
    // 3
    var text = "\(value)"
 
   // 4
    if !children.isEmpty {
      text += " {" + children.map { $0.description }.joinWithSeparator(", ") + "} "
    }
    return text
  }
}

代码比较直白:

  1. 扩展你的Node类并且实现CustomStringConvertible协议。这个协议需要你实现一个String类型的description计算属性。
  2. description计算属性是一个只读属性,他返回一个String。
  3. text变量用于保存整个字符串。现在我们把当前节点的值赋值给它。
  4. 除了打印当前节点的值,你还需要打印所有子节点以及子节点的子节点的值。所以,你需要递归得添加所有子节点的description,同时添加一些花括号来增强一些结构感。

现在,当你使用Print打印你的Node类时,你将会得到一个漂亮的打印结果:

"beverages {hot {tea {black, green, chai} , coffee, cocoa} , cold {soda {ginger ale, bitter lemon} , milk} } "

提示:如果map语法使你感到迷惑,下面可能才是你写过的代码:

if !children.isEmpty {
  text += " {"
  for child in children {
    text += child.description + ", "
  }
  text += "} "
}

map是一个数据容器的方法,就比如Array。在所有实现了Sequence协议的数据容器中,你可以使用map来对容器中的每一个元素进行操作。在这里,你遍历每一个子节点并且执行一个字符串相加的操作。
想要学习更多有关map的知识,你可以查看这篇教程:Introduction to Functional Programming in Swift.

搜索

这个多功能的树很适合描述分层次的数据,但他需要什么其他的功能就要看你的App的需求了。例如,你可以使用这个类来在你的树中查找某个确定的值。

为了方便对你的树进行搜索,将以下的扩展添加到Playground的底部。

extension Node {
  // 1
  func search(value: String) -> Node? {
    // 2
    if value == self.value {
      return self
    }
    // 3
    for child in children {
      if let found = child.search(value: value) {
        return found
      }
    }
    // 4
    return nil
  }
}

代码相当直白:

  1. 这个函数的目的是在树中搜索一个值。如果成功,将包含这个值的节点返回。如果没有,返回nil。
  2. 在这一行找到了目标值,并且将self,也就是当前节点返回。
  3. 在这里循环遍历children数组。通过调用子节点的Search函数,递归得搜索所有子节点。如果有一个节点的值匹配,if let语句将会返回这个非可选的节点。
  4. 返回nil表示你找不到匹配的节点。

测试一下我们的Search函数吧!在PlayGround的最下面添加如下代码:

beverages.search(value: "cocoa") // returns the "cocoa" node
beverages.search(value: "chai") // returns the "chai" node
beverages.search(value: "bubbly") // returns nil

如果是不同的数据类型呢?

666,你已经看我扯了这么久了!你已经知道如何去实现一个存储String类型数据的通用的树了,并且定义了一个很棒的方式来在控制台中打印你的树,还提供了一个搜索的方法。

树可以很好的组织你的分层结构的String数据,但如果你是想存储整型数据呢?

你可以将Node类修改为Int类型的:

class Node {
  var value: Int
 
  // ...
}

但是你用来接收String类型的那个类又不见了。理想状态下,你的Node类应该可以用来接收所有类型的数据,而不论他是IntDouble还是Float,甚至是你自定义的类。为了使你的Node类更加通用,你需要进入泛型的世界!

泛型

泛型可以在算法与数据结构中去除对数据类型的考虑。这可以让你保持算法的通用性与可重用性。当一个对象能存在一个树中(或者其他数据结构)那就不应该考虑他是Int还是String,而应该考虑其他更重要的问题。任何适用于分层结构的数据都可以用在树型数据结构中。

是时候做一些革命性改变了!更新你的代码:


// 1. 
class Node<T> {
  // 2. 
  var value: T
  weak var parent: Node?
  // 3.
  var children: [Node] = []
 
  // 4.
  init(value: T) {
    self.value = value
  }
 
  // 5. 
  func add(child: Node) {
    children.append(child)
    child.parent = self
  }
}

你会看到一些编译错误。别慌,你将会在适配完你的Node后清除所有的错误。这是你这段代码所做到的:

  1. 你已经将你的Node改为接受泛型参数T。<>告诉编译器你正在使用泛型。
  2. 你的目标将Node改为允许接受任何类型,所以你最好将value属性改为T泛型而不是确定的Int或者String
  3. 同时将children也改为T泛型。
  4. 更改init方法。
  5. add(child:)方法目前已经可以接收当前Node已经定义的类型。

看上去还不错。接下来,找到包含search方法的扩展并更新为使用泛型:

// 1. 
extension Node where T: Equatable {
  // 2. 
  func search(value: T) -> Node? {
    if value == self.value {
      return self
    }
    for child in children {
      if let found = child.search(value: value) {
        return found
      }
    }
    return nil
  }
}

这里改了两个地方:

  1. 你已经将这个扩展限制为只有实现了Equatable协议的类型才能使用Search函数。
  2. 将value参数更新为泛型。

你的代码现在应该可以编译了,测试一下!在Playground的底部添加如下代码类检测你的泛型树是否在正确工作:

let number = Node(value: 5)

恭喜!你实现了一个通用的树,并且兼容所有类型的数据!

更多链接

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

推荐阅读更多精彩内容