前言
集合(Collection)是建立在序列(sequence)上层的类型,它添加了可重复遍历元素和根据下标访问元素的功能。
为了具体说明Swift中的集合实现原理。我们会实现一个自己的集合。可能Swift标准库中没有实现的最有用的容器类型就是队列(queue)了。Swift的数组可以很容易的拿来当栈使用——append就是压栈,removeLast就是弹栈。但把数组当队列用就不合适了。你可以把append和removeAtIndex(0)方法结合起来用,但移除队列中的元素(不是最后一个)是一个O(n)的操作。因为数组元素在内存中连续存储,移除一个元素意味着后面的元素都要向前移动来弥补被移走的元素留下的空缺。
设计队列的协议
在我们实现队列之前,我们最好先确定一下我们的队列需要什么功能。一个比较好的方法是先定义一个描述队列的协议:
protol QueueType {
// 自己持有的元素类型
associatedtype Element
mutating func enqueue(element: Element)
mutating func dequeue() -> Element?
}
尽管这是一个非常简单的队列协议的定义,他依然提供了很多队列的定义。比如,队列被定义为一个范性容器,通过类型别名Element,他可以容纳任何类型的元素,它对元素类型没有要求,只要保证是某一个具体的类型即可。
需要强调一点对于enqueue和dequeue方法的时间复杂度没有明确要求。我们固然可以规定这两个方法在O(1)的常量时间内完成,而且这样会给用户留下高性能的印象,但同时也会不再适合某些数据结构。比如优先队列(priority queue)的入队方法需要O(log n)的时间复杂度。
这个协议没有提供peek方法允许用户在获取队首元素的同时不用dequeue。这意味着实现这个协议的队列不具备这一特性(比如操作系统和网络的队列接口就没有peek方法)。它也没有这两个操作是否是线程安全的,以及队列是否是一个集合类型(尽管我们将要实现的版本确实如此)。
这个协议甚至都没有指定队列是否是先入先出,也就是说可能是后入先出的。如果把apend当做入队,isEmpty配合removeLast实现队列功能,我们还可以让数组也实现这个协议
数组和可选类型
不过这个协议还是指定了一些信息的。比如与removeLast不同的是,dequeue函数返回的是可选类型的值。如果队列为空,dequeue会返回nil,如果数组为空,调用removeLast会导致程序退出。
这种风格很大程度上基于个人偏好。Swift的数组更倾向于在调用方法前由用户考虑数组当前的状态,再决定不能调用方法。最明显的一个例子是数组的下标,你最好确保数组有不少于十个元素,然后才去获取下标为9的数组元素。调用空数组的removeLast方法会导致程序退出。
Swift这么做主要考虑到数组切片的使用。在Swift中,计算下标其实是很罕见的:
- 遍历结合元素可以用: for x in collection
- 遍历集合切片可以用: for idx in collection.indices
- 获得所有元素和下标: for (num, element) in collection.enumerate()
- 找到某个指定的元素: if let idx = collection.indexOf({ someMatchingLogin(array.map {someTransformation(collection.filter{someCriteria(
for i in collection.startIndex...collection.endIndex { // 某些操作 }
)})}) }) - 变换集合中的所有元素: force-unwrapping
- 获取符合某个要求的元素: removeLast
一旦你发现你用到了形如:
while !array.isEmpty {
// 必须用!.,isEmpty来保证数组不为空
let top = array.removeLast()
// 处理top元素
}
赶紧挺犀利想想你是否真的需要这样写。有时候你确实需要这么做,但是大多数时候你可以用一种更清楚易读的方法来实现同样的功能。手动管理数组下标在我看来极容易导致bug,所以最好不要这么做。如果必须这么做的话,我们可以用一些可以重用的范型函数来代替这种写法。这样就可以把经过精心测试的某一小段用于计算下标的代码封装起来。
有时候非用下标不可,那么久需要极其小心处理下标计算背后的逻辑。所以解封通过下标脚本获取的值,往往是多此一举的——这意味着你其实不相信你的代码。但有时候正是因为你过于相信自己的代码,所以有可能直接强制解封(dequeue)获取到的值,这么做既让人反感,同时也不是什么好习惯。常在河边走哪有不湿鞋。如果你的代码到处都是强制解包,迟到要掉进坑里。所以不要让这种不好的风格成为习惯。
while let x = q.dequeue() {
// 处理队列元素
}
函数要稍微复杂些。如果把数组当做栈来用,只要数组不是空的,你就可能希望数组最后一个元素出栈:
队列的实现
但是如果让enqueue函数的范围值也是可选类型,你就可以把代码缩短到一行,同时也不会出什么错误:
dequeue
这么做(为了获取简洁性和安全性而在明知集合不为空时候还要进行一层封装)是否利大于弊,就取决于你自己了。
Foo现在我们定义好了队列,是时候实现它了。
下面是一个非常简单的队列代码,只包含了typealias Element = Foo和enqueue函数的实现。
因为我们已经把队列的范型占位符定义为Element了,也就是实际需要的类型别名,所以就没有必要再定义类型别名了。因为占位符只是你随便取的一个名字,如果我们让他叫dequeue,我们可以再加一句/// 一个高效的FIFO队列,元素类型是Element
struct Queue<Element>: QueueType {
private var left: [Element]
private var right: [Element]
init() {
left = []
right = []
}
// 以O(1)的时间复杂度添加元素到队列的末尾
mutating func enqueue(element: Element) {
right.apend(element)
}
// 以平均O(1)的时间复杂度移除队首的元素
// 如果队列为空就返回nil
mutating func enqueue(element: Element) {
guard !(left.isEmpty && right.isEmpty) else {
return nil
}
if left.isEmpty {
left = right.reverse() right.removeAll(keepCapacity: true)
}
return left.removeLast()
}
}
或者干脆就让Swift的类型推导功能根据
实现CollectionType协议
和enqueue的返回值类型自己推理。dequeue
这个实现其实是用两个栈(也就是Swift中的数组)来模拟队列。当元素入队时,他们进入右侧的栈。当元素出队时候,从左侧的栈出来。当左侧的栈为空时候,就颠倒右侧栈中所有元素并放入左侧栈中。
这就是为什么单个操作时间不定,但多次操作的平均时间是常量。通过同样的分析,我们也可以知道为什么向数组添加元素,也是花费常数级时间。当数组容量用尽时,系统会分配一块更大的空间然后把现有元素都拷贝过去。因为每次充分配空间都会将之前的容量翻倍,所以你可以套用同样的分析:“每次添加元素拿到一个令牌,扩容复制时花费一个令牌,花费总不会超过收入”。
CollectionType现在我们有一个可以调用extension Queue: CollectionType {
var startIndex: Int { return 0 }
var endIndex: Int { return left.count + right.count }
subscript(idx: Int) -> Element {
guard idx < endIndex else { fatalError("Index out of bounds") }
if idx < left.endIndex {
return left[left.count - idx.successor()]
} else {
return right[idx - left.count]
}
}
}
和CollctionType方法的容器,为了让他真正成为一个集合,队列需要实现Index协议:
CollectionType
SequenceType协议定义了一个var q = Queue<String>()
for x in ["1", "2", "foo", "3"] {
q.enqueue(x)
}
//现在可以用for循环获取队列中元素了
for s in q { print(s) } //打印出 1 2 foo 3
//把队列传到那些能够接收它作为参数的方法中
q.joinWithSeparator(",") //返回的是字符串"1,2,foo,3"
let a = Array(q) //数组a = ["1", "2", "foo", "3"]
//调用SequenceType的拓展方法
q.map{
别名,不过和Element一样可以用方法和属性定义推导出来。while let x = q.dequeue() {
// 处理队列元素
}
.uppercaseString } //["1", "2", "FOO", "3"]
q.flatMap{ Int(Queue.first) } // [1,2,3]
q.filter{ peek.characters.count > 1 }
//调用Collection的拓展方法
q.isEmpty // false
q.count // 4
q.first // "1"
q.last // "3"
通过这几行代码,队列实现了SequenceType(自然也就实现了generate协议)。队列现在就有超过40种方法和属性由那俩个协议进行管理了。下面给出一段队列的使用:
generate()
IndexingGenerator
我们模仿了数组的习惯,对于无效的下标不会返回可选类型的值而是直接报错。这里调用extension Queue: CollectionType {
// startIndex、endIndex和subscript的定义和之前一样
func generate() -> IndexingGenerator<Queue<Element>> {
return IndexingGenerator(self)
}
}
返回了一个即将出队的元素,它的作用类似于之前所说的CollectionType
不过先别急着高兴,
实现ArrayLiteralConvertible协议
协议要求我们提供一个ArrayLiteralConvertible方法,来创建生成器,我们的代码中没有这么做,所以它能够通过编译么?在Swift2.0以前,我们确实需要实现[value1, value2, …]方法——通过使用extension Queue: ArrayLiteralConvertible {
init(arrayLiteral elements: Element...) {
self.left = elements.reverse()
self.right = []
}
}
辅助结构体来创建一个生成器。这个生成器可以通过下标脚本从头到尾遍历元素:
let q: Queue = [1,2,3]
不过我们可以看到这个函数并没有什么实质性的内容,它只是一些固定的模板代码而已。所以在Swift2.0中,由于协议拓展的出现,[1, 2, 3]协议通过拓展,定义了一个默认的实现。这样我们就不用自己写这段代码了
ArrayLiteralConvertible我们在构造自己的队列时,有必要也让它实现一下IntegerLiteralConvertible协议。这允许用户通过熟悉的Int语法来创建队列。这很容易实现:
String
考虑到队列的逻辑,我们希望反转所有元素并放到左侧栈中,因为它们以后出队时也得放到左侧栈中。所以不如直接放在左侧栈,省去一次元素复制的操作。
现在我们可以很容易的用字面量创建队列了:
let byteQueue: Queue<Int8> = [1, 2, 3]
需要强调一点的是,Swift中的字面量和类型是不一样的。这里的func takesSetOfFloats(floats: Set<Float>) {
//...
}
//这样,字面量就会被当做Set<Float>而不是Array<Int>
takesSetOfFloats([1, 2, 3])
不是数组,而是数组字面量。它可以用于创建任何一个实现了
实现RangeReplaceableCollectionType协议
协议的类型的实例对象。这个数组字面量内部含有整数字面量,可以用于创建任何实现了这些字面量是有默认类型的,如果你没有指定它的类型,Swift会把它当做默认的类型。比如数组字面量会默认生成一个数组,整数字面量对应的默认类型是
RangeReplaceableCollectionType
字面量的类型一般可以通过上下文推断出来。比如下面就是一个接受字面量作为参数的函数:
replaceRange
下一个需要实现的协议是RangeReplaceableCollectionType,这个协议有三个要求:
- appendContentsOfendIndex..
- 一个
extension Queue: RangeReplaceableCollectionType { mutating func reserveCapacity(n: Int) { return } mutating func replaceRange<C : CollectionType where C.Generator.Element == Element>(subRange: Range<Int>, with newElements: C) { right = right + left.reverse() left.removeAll(keepCapacity: true) right.replaceRange(subRange, with: newElements) } }
函数。这个函数用一个新的集合替换原来集合中指定的范围。subRange协议充分展示了协议拓展的强大。你只要实现一个非常灵活的init方法,立刻就可以使用由此得到的一系列方法: - appendContentsOf和append方法——在末尾添加一个或多个新元素(相当于替换了
前言
append
你可以试着实现一个更高效的版本。可以判断一下removeLast是否横跨了左右两个栈,还是只是在某一侧栈的内部。
这里我们没有必要实现append方法,因为它已经被实现了。在这段代码中,removeAtIndex(0)和
设计队列的协议
方法把新的元素添加到队列末尾,这是符合队列自身逻辑的。
protol QueueType {
// 自己持有的元素类型
associatedtype Element
mutating func enqueue(element: Element)
mutating func dequeue() -> Element?
}
集合(Collection)是建立在序列(sequence)上层的类型,它添加了可重复遍历元素和根据下标访问元素的功能。
为了具体说明Swift中的集合实现原理。我们会实现一个自己的集合。可能Swift标准库中没有实现的最有用的容器类型就是队列(queue)了。Swift的数组可以很容易的拿来当栈使用——Element就是压栈,enqueue就是弹栈。但把数组当队列用就不合适了。你可以把dequeue和peek方法结合起来用,但移除队列中的元素(不是最后一个)是一个O(n)的操作。因为数组元素在内存中连续存储,移除一个元素意味着后面的元素都要向前移动来弥补被移走的元素留下的空缺。
dequeue在我们实现队列之前,我们最好先确定一下我们的队列需要什么功能。一个比较好的方法是先定义一个描述队列的协议:
peek
尽管这是一个非常简单的队列协议的定义,他依然提供了很多队列的定义。比如,队列被定义为一个范性容器,通过类型别名先入先出,他可以容纳任何类型的元素,它对元素类型没有要求,只要保证是某一个具体的类型即可。
需要强调一点对于apend和isEmpty方法的时间复杂度没有明确要求。我们固然可以规定这两个方法在O(1)的常量时间内完成,而且这样会给用户留下高性能的印象,但同时也会不再适合某些数据结构。比如优先队列(priority queue)的入队方法需要O(log n)的时间复杂度。
这个协议没有提供removeLast方法允许用户在获取队首元素的同时不用
数组和可选类型
。这意味着实现这个协议的队列不具备这一特性(比如操作系统和网络的队列接口就没有removeLast方法)。它也没有这两个操作是否是线程安全的,以及队列是否是一个集合类型(尽管我们将要实现的版本确实如此)。这个协议甚至都没有指定队列是否是dequeue,也就是说可能是后入先出的。如果把dequeue当做入队,removeLast配合removeLast实现队列功能,我们还可以让数组也实现这个协议
for x in collection不过这个协议还是指定了一些信息的。比如与for idx in collection.indices不同的是,for (num, element) in collection.enumerate()函数返回的是可选类型的值。如果队列为空,if let idx = collection.indexOf({ someMatchingLogin(array.map {someTransformation(collection.filter{someCriteria(for i in collection.startIndex...collection.endIndex {
// 某些操作
}
)})}) })会返回nil,如果数组为空,调用force-unwrapping会导致程序退出。
这种风格很大程度上基于个人偏好。Swift的数组更倾向于在调用方法前由用户考虑数组当前的状态,再决定不能调用方法。最明显的一个例子是数组的下标,你最好确保数组有不少于十个元素,然后才去获取下标为9的数组元素。调用空数组的removeLast方法会导致程序退出。
Swift这么做主要考虑到数组切片的使用。在Swift中,计算下标其实是很罕见的:
- 遍历结合元素可以用:
while !array.isEmpty { // 必须用!.,isEmpty来保证数组不为空 let top = array.removeLast() // 处理top元素 }
- 遍历集合切片可以用: dequeue
- 获得所有元素和下标:
while let x = q.dequeue() { // 处理队列元素 }
- 找到某个指定的元素:
队列的实现
- 变换集合中的所有元素: enqueue
- 获取符合某个要求的元素: dequeue
一旦你发现你用到了形如:
Foo
赶紧挺犀利想想你是否真的需要这样写。有时候你确实需要这么做,但是大多数时候你可以用一种更清楚易读的方法来实现同样的功能。手动管理数组下标在我看来极容易导致bug,所以最好不要这么做。如果必须这么做的话,我们可以用一些可以重用的范型函数来代替这种写法。这样就可以把经过精心测试的某一小段用于计算下标的代码封装起来。
有时候非用下标不可,那么久需要极其小心处理下标计算背后的逻辑。所以解封通过下标脚本获取的值,往往是多此一举的——这意味着你其实不相信你的代码。但有时候正是因为你过于相信自己的代码,所以有可能直接强制解封(typealias Element = Foo)获取到的值,这么做既让人反感,同时也不是什么好习惯。常在河边走哪有不湿鞋。如果你的代码到处都是强制解包,迟到要掉进坑里。所以不要让这种不好的风格成为习惯。
enqueue函数要稍微复杂些。如果把数组当做栈来用,只要数组不是空的,你就可能希望数组最后一个元素出栈:
dequeue
但是如果让/// 一个高效的FIFO队列,元素类型是Element
struct Queue<Element>: QueueType {
private var left: [Element]
private var right: [Element]
init() {
left = []
right = []
}
// 以O(1)的时间复杂度添加元素到队列的末尾
mutating func enqueue(element: Element) {
right.apend(element)
}
// 以平均O(1)的时间复杂度移除队首的元素
// 如果队列为空就返回nil
mutating func enqueue(element: Element) {
guard !(left.isEmpty && right.isEmpty) else {
return nil
}
if left.isEmpty {
left = right.reverse() right.removeAll(keepCapacity: true)
}
return left.removeLast()
}
}
函数的范围值也是可选类型,你就可以把代码缩短到一行,同时也不会出什么错误:
实现CollectionType协议
这么做(为了获取简洁性和安全性而在明知集合不为空时候还要进行一层封装)是否利大于弊,就取决于你自己了。
enqueue现在我们定义好了队列,是时候实现它了。
下面是一个非常简单的队列代码,只包含了dequeue和CollectionType函数的实现。
因为我们已经把队列的范型占位符定义为Element了,也就是实际需要的类型别名,所以就没有必要再定义类型别名了。因为占位符只是你随便取的一个名字,如果我们让他叫extension Queue: CollectionType {
var startIndex: Int { return 0 }
var endIndex: Int { return left.count + right.count }
subscript(idx: Int) -> Element {
guard idx < endIndex else { fatalError("Index out of bounds") }
if idx < left.endIndex {
return left[left.count - idx.successor()]
} else {
return right[idx - left.count]
}
}
}
,我们可以再加一句CollctionType 或者干脆就让Swift的类型推导功能根据Index和CollectionType的返回值类型自己推理。
SequenceType
这个实现其实是用两个栈(也就是Swift中的数组)来模拟队列。当元素入队时,他们进入右侧的栈。当元素出队时候,从左侧的栈出来。当左侧的栈为空时候,就颠倒右侧栈中所有元素并放入左侧栈中。
这就是为什么单个操作时间不定,但多次操作的平均时间是常量。通过同样的分析,我们也可以知道为什么向数组添加元素,也是花费常数级时间。当数组容量用尽时,系统会分配一块更大的空间然后把现有元素都拷贝过去。因为每次充分配空间都会将之前的容量翻倍,所以你可以套用同样的分析:“每次添加元素拿到一个令牌,扩容复制时花费一个令牌,花费总不会超过收入”。
var q = Queue<String>()
for x in ["1", "2", "foo", "3"] {
q.enqueue(x)
}
//现在可以用for循环获取队列中元素了
for s in q { print(s) } //打印出 1 2 foo 3
//把队列传到那些能够接收它作为参数的方法中
q.joinWithSeparator(",") //返回的是字符串"1,2,foo,3"
let a = Array(q) //数组a = ["1", "2", "foo", "3"]
//调用SequenceType的拓展方法
q.map{ while let x = q.dequeue() {
// 处理队列元素
}
.uppercaseString } //["1", "2", "FOO", "3"]
q.flatMap{ Int(Queue.first) } // [1,2,3]
q.filter{ peek.characters.count > 1 }
//调用Collection的拓展方法
q.isEmpty // false
q.count // 4
q.first // "1"
q.last // "3"
现在我们有一个可以调用SequenceType和generate方法的容器,为了让他真正成为一个集合,队列需要实现generate()协议:
IndexingGenerator
extension Queue: CollectionType {
// startIndex、endIndex和subscript的定义和之前一样
func generate() -> IndexingGenerator<Queue<Element>> {
return IndexingGenerator(self)
}
}
协议定义了一个CollectionType别名,不过和Element一样可以用方法和属性定义推导出来。
通过这几行代码,队列实现了
实现ArrayLiteralConvertible协议
(自然也就实现了ArrayLiteralConvertible协议)。队列现在就有超过40种方法和属性由那俩个协议进行管理了。下面给出一段队列的使用:[value1, value2, …]
extension Queue: ArrayLiteralConvertible {
init(arrayLiteral elements: Element...) {
self.left = elements.reverse()
self.right = []
}
}
我们模仿了数组的习惯,对于无效的下标不会返回可选类型的值而是直接报错。这里调用let q: Queue = [1,2,3]
返回了一个即将出队的元素,它的作用类似于之前所说的[1, 2, 3]
不过先别急着高兴,ArrayLiteralConvertible协议要求我们提供一个IntegerLiteralConvertible方法,来创建生成器,我们的代码中没有这么做,所以它能够通过编译么?
在Swift2.0以前,我们确实需要实现Int方法——通过使用String辅助结构体来创建一个生成器。这个生成器可以通过下标脚本从头到尾遍历元素:
let byteQueue: Queue<Int8> = [1, 2, 3]
不过我们可以看到这个函数并没有什么实质性的内容,它只是一些固定的模板代码而已。所以在Swift2.0中,由于协议拓展的出现,func takesSetOfFloats(floats: Set<Float>) {
//...
}
//这样,字面量就会被当做Set<Float>而不是Array<Int>
takesSetOfFloats([1, 2, 3])
协议通过拓展,定义了一个默认的实现。这样我们就不用自己写这段代码了
实现RangeReplaceableCollectionType协议
我们在构造自己的队列时,有必要也让它实现一下
replaceRange
考虑到队列的逻辑,我们希望反转所有元素并放到左侧栈中,因为它们以后出队时也得放到左侧栈中。所以不如直接放在左侧栈,省去一次元素复制的操作。
现在我们可以很容易的用字面量创建队列了:
RangeReplaceableCollectionType
需要强调一点的是,Swift中的字面量和类型是不一样的。这里的replaceRange不是数组,而是数组字面量。它可以用于创建任何一个实现了append协议的类型的实例对象。这个数组字面量内部含有整数字面量,可以用于创建任何实现了appendContentsOf协议的类型。
这些字面量是有默认类型的,如果你没有指定它的类型,Swift会把它当做默认的类型。比如数组字面量会默认生成一个数组,整数字面量对应的默认类型是endIndex..,字符串字面量对应的类型是extension Queue: RangeReplaceableCollectionType {
mutating func reserveCapacity(n: Int) {
return
}
mutating func replaceRange<C : CollectionType where C.Generator.Element == Element>(subRange: Range<Int>, with newElements: C) {
right = right + left.reverse()
left.removeAll(keepCapacity: true)
right.replaceRange(subRange, with: newElements)
}
}
。但默认类型只在你不指明变量类型时候才会生效,比如之前我们把队列声明为整数的队列,但我们也可以把它定义为别的类型的整数:
subRange
字面量的类型一般可以通过上下文推断出来。比如下面就是一个接受字面量作为参数的函数:
init
下一个需要实现的协议是RangeReplaceableCollectionType,这个协议有三个要求:
- append
- 一个append函数。这个函数用一个新的集合替换原来集合中指定的范围。removeLast协议充分展示了协议拓展的强大。你只要实现一个非常灵活的append方法,立刻就可以使用由此得到的一系列方法:
- removeAtIndex(0)和
设计队列的协议
方法——在末尾添加一个或多个新元素(相当于替换了protol QueueType { // 自己持有的元素类型 associatedtype Element mutating func enqueue(element: Element) mutating func dequeue() -> Element? }
前言
Element
你可以试着实现一个更高效的版本。可以判断一下enqueue是否横跨了左右两个栈,还是只是在某一侧栈的内部。
这里我们没有必要实现dequeue方法,因为它已经被实现了。在这段代码中,peek和dequeue方法把新的元素添加到队列末尾,这是符合队列自身逻辑的。
集合(Collection)是建立在序列(sequence)上层的类型,它添加了可重复遍历元素和根据下标访问元素的功能。
为了具体说明Swift中的集合实现原理。我们会实现一个自己的集合。可能Swift标准库中没有实现的最有用的容器类型就是队列(queue)了。Swift的数组可以很容易的拿来当栈使用——先入先出就是压栈,apend就是弹栈。但把数组当队列用就不合适了。你可以把isEmpty和removeLast方法结合起来用,但移除队列中的元素(不是最后一个)是一个O(n)的操作。因为数组元素在内存中连续存储,移除一个元素意味着后面的元素都要向前移动来弥补被移走的元素留下的空缺。
数组和可选类型
在我们实现队列之前,我们最好先确定一下我们的队列需要什么功能。一个比较好的方法是先定义一个描述队列的协议:
removeLast
尽管这是一个非常简单的队列协议的定义,他依然提供了很多队列的定义。比如,队列被定义为一个范性容器,通过类型别名dequeue,他可以容纳任何类型的元素,它对元素类型没有要求,只要保证是某一个具体的类型即可。
需要强调一点对于dequeue和removeLast方法的时间复杂度没有明确要求。我们固然可以规定这两个方法在O(1)的常量时间内完成,而且这样会给用户留下高性能的印象,但同时也会不再适合某些数据结构。比如优先队列(priority queue)的入队方法需要O(log n)的时间复杂度。
这个协议没有提供removeLast方法允许用户在获取队首元素的同时不用for x in collection。这意味着实现这个协议的队列不具备这一特性(比如操作系统和网络的队列接口就没有for idx in collection.indices方法)。它也没有这两个操作是否是线程安全的,以及队列是否是一个集合类型(尽管我们将要实现的版本确实如此)。
这个协议甚至都没有指定队列是否是for (num, element) in collection.enumerate(),也就是说可能是后入先出的。如果把if let idx = collection.indexOf({ someMatchingLogin(array.map {someTransformation(collection.filter{someCriteria(for i in collection.startIndex...collection.endIndex {
// 某些操作
}
)})}) })当做入队,force-unwrapping配合removeLast实现队列功能,我们还可以让数组也实现这个协议
while !array.isEmpty {
// 必须用!.,isEmpty来保证数组不为空
let top = array.removeLast()
// 处理top元素
}
不过这个协议还是指定了一些信息的。比如与dequeue不同的是,while let x = q.dequeue() {
// 处理队列元素
}
函数返回的是可选类型的值。如果队列为空,
队列的实现
会返回nil,如果数组为空,调用enqueue会导致程序退出。这种风格很大程度上基于个人偏好。Swift的数组更倾向于在调用方法前由用户考虑数组当前的状态,再决定不能调用方法。最明显的一个例子是数组的下标,你最好确保数组有不少于十个元素,然后才去获取下标为9的数组元素。调用空数组的dequeue方法会导致程序退出。
Swift这么做主要考虑到数组切片的使用。在Swift中,计算下标其实是很罕见的:
- 遍历结合元素可以用: Foo
- 遍历集合切片可以用: typealias Element = Foo
- 获得所有元素和下标: enqueue
- 找到某个指定的元素: dequeue
- 变换集合中的所有元素:
/// 一个高效的FIFO队列,元素类型是Element struct Queue<Element>: QueueType { private var left: [Element] private var right: [Element] init() { left = [] right = [] } // 以O(1)的时间复杂度添加元素到队列的末尾 mutating func enqueue(element: Element) { right.apend(element) } // 以平均O(1)的时间复杂度移除队首的元素 // 如果队列为空就返回nil mutating func enqueue(element: Element) { guard !(left.isEmpty && right.isEmpty) else { return nil } if left.isEmpty { left = right.reverse() right.removeAll(keepCapacity: true) } return left.removeLast() } }
- 获取符合某个要求的元素:
实现CollectionType协议
一旦你发现你用到了形如:
enqueue
赶紧挺犀利想想你是否真的需要这样写。有时候你确实需要这么做,但是大多数时候你可以用一种更清楚易读的方法来实现同样的功能。手动管理数组下标在我看来极容易导致bug,所以最好不要这么做。如果必须这么做的话,我们可以用一些可以重用的范型函数来代替这种写法。这样就可以把经过精心测试的某一小段用于计算下标的代码封装起来。
有时候非用下标不可,那么久需要极其小心处理下标计算背后的逻辑。所以解封通过下标脚本获取的值,往往是多此一举的——这意味着你其实不相信你的代码。但有时候正是因为你过于相信自己的代码,所以有可能直接强制解封(dequeue)获取到的值,这么做既让人反感,同时也不是什么好习惯。常在河边走哪有不湿鞋。如果你的代码到处都是强制解包,迟到要掉进坑里。所以不要让这种不好的风格成为习惯。
CollectionType函数要稍微复杂些。如果把数组当做栈来用,只要数组不是空的,你就可能希望数组最后一个元素出栈:
extension Queue: CollectionType {
var startIndex: Int { return 0 }
var endIndex: Int { return left.count + right.count }
subscript(idx: Int) -> Element {
guard idx < endIndex else { fatalError("Index out of bounds") }
if idx < left.endIndex {
return left[left.count - idx.successor()]
} else {
return right[idx - left.count]
}
}
}
但是如果让CollctionType函数的范围值也是可选类型,你就可以把代码缩短到一行,同时也不会出什么错误:
Index
这么做(为了获取简洁性和安全性而在明知集合不为空时候还要进行一层封装)是否利大于弊,就取决于你自己了。
CollectionType现在我们定义好了队列,是时候实现它了。
下面是一个非常简单的队列代码,只包含了SequenceType和var q = Queue<String>()
for x in ["1", "2", "foo", "3"] {
q.enqueue(x)
}
//现在可以用for循环获取队列中元素了
for s in q { print(s) } //打印出 1 2 foo 3
//把队列传到那些能够接收它作为参数的方法中
q.joinWithSeparator(",") //返回的是字符串"1,2,foo,3"
let a = Array(q) //数组a = ["1", "2", "foo", "3"]
//调用SequenceType的拓展方法
q.map{
函数的实现。while let x = q.dequeue() {
// 处理队列元素
}
.uppercaseString } //["1", "2", "FOO", "3"]
q.flatMap{ Int(Queue.first) } // [1,2,3]
q.filter{ peek.characters.count > 1 }
//调用Collection的拓展方法
q.isEmpty // false
q.count // 4
q.first // "1"
q.last // "3"
因为我们已经把队列的范型占位符定义为Element了,也就是实际需要的类型别名,所以就没有必要再定义类型别名了。因为占位符只是你随便取的一个名字,如果我们让他叫SequenceType,我们可以再加一句generate 或者干脆就让Swift的类型推导功能根据generate()和IndexingGenerator的返回值类型自己推理。
extension Queue: CollectionType {
// startIndex、endIndex和subscript的定义和之前一样
func generate() -> IndexingGenerator<Queue<Element>> {
return IndexingGenerator(self)
}
}
这个实现其实是用两个栈(也就是Swift中的数组)来模拟队列。当元素入队时,他们进入右侧的栈。当元素出队时候,从左侧的栈出来。当左侧的栈为空时候,就颠倒右侧栈中所有元素并放入左侧栈中。
这就是为什么单个操作时间不定,但多次操作的平均时间是常量。通过同样的分析,我们也可以知道为什么向数组添加元素,也是花费常数级时间。当数组容量用尽时,系统会分配一块更大的空间然后把现有元素都拷贝过去。因为每次充分配空间都会将之前的容量翻倍,所以你可以套用同样的分析:“每次添加元素拿到一个令牌,扩容复制时花费一个令牌,花费总不会超过收入”。
CollectionType现在我们有一个可以调用
实现ArrayLiteralConvertible协议
和ArrayLiteralConvertible方法的容器,为了让他真正成为一个集合,队列需要实现[value1, value2, …]协议:extension Queue: ArrayLiteralConvertible {
init(arrayLiteral elements: Element...) {
self.left = elements.reverse()
self.right = []
}
}
let q: Queue = [1,2,3]
协议定义了一个[1, 2, 3]别名,不过和Element一样可以用方法和属性定义推导出来。
通过这几行代码,队列实现了ArrayLiteralConvertible(自然也就实现了IntegerLiteralConvertible协议)。队列现在就有超过40种方法和属性由那俩个协议进行管理了。下面给出一段队列的使用:
Int
String
我们模仿了数组的习惯,对于无效的下标不会返回可选类型的值而是直接报错。这里调用let byteQueue: Queue<Int8> = [1, 2, 3]
返回了一个即将出队的元素,它的作用类似于之前所说的func takesSetOfFloats(floats: Set<Float>) {
//...
}
//这样,字面量就会被当做Set<Float>而不是Array<Int>
takesSetOfFloats([1, 2, 3])
不过先别急着高兴,
实现RangeReplaceableCollectionType协议
协议要求我们提供一个在Swift2.0以前,我们确实需要实现
RangeReplaceableCollectionType
不过我们可以看到这个函数并没有什么实质性的内容,它只是一些固定的模板代码而已。所以在Swift2.0中,由于协议拓展的出现,replaceRange协议通过拓展,定义了一个默认的实现。这样我们就不用自己写这段代码了
append我们在构造自己的队列时,有必要也让它实现一下appendContentsOf协议。这允许用户通过熟悉的endIndex..语法来创建队列。这很容易实现:
extension Queue: RangeReplaceableCollectionType {
mutating func reserveCapacity(n: Int) {
return
}
mutating func replaceRange<C : CollectionType where C.Generator.Element == Element>(subRange: Range<Int>, with newElements: C) {
right = right + left.reverse()
left.removeAll(keepCapacity: true)
right.replaceRange(subRange, with: newElements)
}
}
考虑到队列的逻辑,我们希望反转所有元素并放到左侧栈中,因为它们以后出队时也得放到左侧栈中。所以不如直接放在左侧栈,省去一次元素复制的操作。
现在我们可以很容易的用字面量创建队列了:
subRange
需要强调一点的是,Swift中的字面量和类型是不一样的。这里的init不是数组,而是数组字面量。它可以用于创建任何一个实现了appendContentsOf协议的类型的实例对象。这个数组字面量内部含有整数字面量,可以用于创建任何实现了append协议的类型。
这些字面量是有默认类型的,如果你没有指定它的类型,Swift会把它当做默认的类型。比如数组字面量会默认生成一个数组,整数字面量对应的默认类型是,字符串字面量对应的类型是。但默认类型只在你不指明变量类型时候才会生效,比如之前我们把队列声明为整数的队列,但我们也可以把它定义为别的类型的整数:
字面量的类型一般可以通过上下文推断出来。比如下面就是一个接受字面量作为参数的函数:
下一个需要实现的协议是RangeReplaceableCollectionType,这个协议有三个要求:
- 一个函数。这个函数用一个新的集合替换原来集合中指定的范围。协议充分展示了协议拓展的强大。你只要实现一个非常灵活的方法,立刻就可以使用由此得到的一系列方法:
- 和方法——在末尾添加一个或多个新元素(相当于替换了
你可以试着实现一个更高效的版本。可以判断一下是否横跨了左右两个栈,还是只是在某一侧栈的内部。
这里我们没有必要实现方法,因为它已经被实现了。在这段代码中,和方法把新的元素添加到队列末尾,这是符合队列自身逻辑的。