原文链接: Swift Algorithm Club: Swift Linked List Data Structure
翻译: coderJoey
在这本教程中,你将学习用Swift3实现链表数据结构。
开始吧
链表是由数据项组成的一个序列,其中每个数据项被称为节点。
链表有两种主要类型:
单链表 每一个节点只包含一个指向链表中下一个节点的指针(引用)。
双链表 每个节点包含两个指针(引用),一个指向前一个节点,一个指向后一个节点。
通常我们用head和tail指针来记录链表的头和尾。
链表数据结构的实现(Swift 3语法)
在本节中,你将用swift 3的语法来实现链表。
记住一个链表数据结构是由节点组成的,所以首先我们创建一个基本的节点类。创建一个新的Swift playground 项目,并添加下面的空类。
public class Node {
}
Value
每个节点需要一个与之关联的数据(value)。将下面的代码添加到大括号内:
var value: String
init(value: String) {
self.value = value
}
你已经了声明一个类型为String,名为value的属性。在自己的app里,你可以用链表来储存任何数据类型。
同时,也声明了一个构造函数:此函数里面需要初始化所有类的非可选类型(non-optional)的属性。
Next
在链表中,除了数据之外,每一个节点都需要一个指向下一个节点的指针。
所以需要为我们的类中添加一个next属性:
var next: Node?
我们添加的是一个类型为Node,名为next的属性。注意这里的next是可选类型,这是因为链表中最后一个节点不会指向其他的节点了。
Previous
你需要实现的是一个双链表数据结构,所以我们还需要为节点添加最后一个属性:
weak var previous: Node?
注意:为了避免循环引用,我们将previous的指针声明为weak(弱引用)。例如现在在一个链表中,节点B在节点A后面,这样A是指向B的。假如现在节点B也指向节点A,这就导致A和B互相强引用。在某些情况下,这种所有权循环(ownership cycle)会使得即使你删除它们,节点依然存活着(也就是所谓的内存泄露)。所以我们需要将其中一个指针设置为weak,用来打破这种循环。
了解更多关于所有权循环的知识,请看ARC and Memory Management in Swift 教程。
链表
至此已经完成了节点类的创建,你还需要记录链表的起点和终点。
现在我们将链表(LinkedList)类添加到playground中:
public class LinkedList {
fileprivate var head: Node?
private var tail: Node?
public var isEmpty: Bool {
return head == nil
}
public var first: Node? {
return head
}
public var last: Node? {
return tail
}
}
该类将记录链表的起点和终点。它还将提供一些辅助函数。
添加
为了能在链表中添加一个新的节点,你需要在LinkedList类中声明一个**append(value:) **方法。
public func append(value: String) {
// 1
let newNode = Node(value: value)
// 2
if let tailNode = tail {
newNode.previous = tailNode
tailNode.next = newNode
}
// 3
else {
head = newNode
}
// 4
tail = newNode
}
来看看上面做了什么:
创建一个新的Node来接收需要添加的节点。注意,Node类的作用是让链表中的每一个数据项能指向前一个节点以及后一个节点。
如果tailNode不为空,则表明该链表拥有节点。如果是这样的话,那么就将新添加进来的节点的头指针(previous)指向链表的尾部(tailNode)。与此同时,将tailNode的next指针指向新的节点。
最后将新的节点设置成链表尾部节点。
打印链表
让我们实践一下上面完成的链表。我们添加如下代码到playground(注意:代码要添加到LinkedList类的外面)
let dogBreeds = LinkedList()
dogBreeds.append(value: "Labrador")
dogBreeds.append(value: "Bulldog")
dogBreeds.append(value: "Beagle")
dogBreeds.append(value: "Husky")
定义链表后,我们试着将链表的内容打印到控制台:
print(dogBreeds)
你可以使用组合键 Command-Shift-Y唤起控制台。然而你看到的打印结果是:
LinkedList
这显然没什么用。要使打印的字符串更具可读性,你需要让LinkedList遵守CustomStringConvertable 协议。我们将下面的代码添加到LinkedList 类的下面。
// 1
extension LinkedList: CustomStringConvertible {
// 2
public var description: String {
// 3
var text = "["
var node = head
// 4
while node != nil {
text += "\(node!.value)"
node = node!.next
if node != nil { text += ", " }
}
// 5
return text + "]"
}
}
上面代码做了什么:
声明了一个** LinkedList 类的扩展,而且遵守了CustomStringConvertable 协议。这个协议希望你实现String类型的description,这里的description为计算型属性(computed property)**。
定义description属性,它的返回类型是String,而且是只读的。
定义一个将输出所有内容的text变量,目前只包含链表内容的开头字符‘’[‘’。
循环添加每一个节点的内容。
添加‘’]‘’到text尾部。
现在打印LinkedList的内容,你将看到不错的结果:
"[Labrador, Bulldog, Beagle, Husky]"
访问节点
当你通过先后顺序来移动节点时,链表表现的相当高效,然而有时候通过索引来访问节点却是相当困难的。
下面我们将通过给LinkedList类添加一个** nodeAt(index:) 方法来实现通过索引来访问节点。该方法的返回值是指定位置的节点**。
更新LinkedList类,将下面的方法添加到该类中:
public func nodeAt(index: Int) -> Node? {
// 1
if index >= 0 {
var node = head
var i = index
// 2
while node != nil {
if i == 0 { return node }
i -= 1
node = node!.next
}
}
// 3
return nil
}
上面代码做了什么:
- 循环链表中的节点,直到循环到指定的索引处的节点并返回该节点。
- 如果index小于0或者大于链表的节点数就返回nil。
删除所有的节点
删除所有的节点很简单,只需要将head和tail置为nil:
public func removeAll() {
head = nil
tail = nil
}
删除单个节点
删除单个节点要考虑三种情况:
-
删除链表的第一个节点。head指针和previous指针需要更新。
-
删除链表中间的一个节点。previous指针和next指针需要更新。
-
删除链表的最后一个节点。next指针和tail指针需要更新。
再次更新LinkedList类的实现,添加如下代码:
public func remove(node: Node) -> String {
let prev = node.previous
let next = node.next
if let prev = prev {
prev.next = next // 1
} else {
head = next // 2
}
next?.previous = prev // 3
if next == nil {
tail = prev // 4
}
// 5
node.previous = nil
node.next = nil
// 6
return node.value
}
上面的方法做了什么:
- 如果你移除的不是链表的第一个节点,那么就更新next指针。
- 如果你移除的是链表的第一个节点,那么就更新head指针。
- 将previous指针指向被移除的节点的previous指针。
- 如果你移除的是链表的最后一个节点,那么就更新tail指针。
- 将移除的节点的previous和next指针置为nil。
- 返回移除的节点。
泛型
到目前为止,你已经实现了一个存储String值的通用链表, 并提供了在LinkedList类中添加,删除和访问节点的函数。 在本节中,我们将使用泛型来满足链表储存抽象类型的要求。
更新Node类:
// 1
public class Node {
// 2
var value: T
var next: Node?
weak var previous: Node?
// 3
init(value: T) {
self.value = value
}
}
上面代码做了什么:
- 将Node类的声明更改为通用类型T。
- 你的目标是允许Node类接受任何类型的值,因此将value属性定义为泛型T而不是String。
- 将构造器更新为接收任意类型。
泛型:挑战
试着自己先完成LinkedList类的泛型实现。
解决方案:
// 1. Change the declaration of the Node class to take a generic type T
public class LinkedList {
// 2. Change the head and tail variables to be constrained to type T
fileprivate var head: Node?
private var tail: Node?
public var isEmpty: Bool {
return head == nil
}
// 3. Change the return type to be a node constrained to type T
public var first: Node? {
return head
}
// 4. Change the return type to be a node constrained to type T
public var last: Node? {
return tail
}
// 5. Update the append function to take in a value of type T
public func append(value: T) {
let newNode = Node(value: value)
if let tailNode = tail {
newNode.previous = tailNode
tailNode.next = newNode
} else {
head = newNode
}
tail = newNode
}
// 6. Update the nodeAt function to return a node constrained to type T
public func nodeAt(index: Int) -> Node? {
if index >= 0 {
var node = head
var i = index
while node != nil {
if i == 0 { return node }
i -= 1
node = node!.next
}
}
return nil
}
public func removeAll() {
head = nil
tail = nil
}
// 7. Update the parameter of the remove function to take a node of type T. Update the return value to type T.
public func remove(node: Node) -> T {
let prev = node.previous
let next = node.next
if let prev = prev {
prev.next = next
} else {
head = next
}
next?.previous = prev
if next == nil {
tail = prev
}
node.previous = nil
node.next = nil
return node.value
}
}
至此你的代码应该可以通过编译了,那我们来测试一下!在playground底部添加如下代码来验证一下泛型链表是否可用。
let dogBreeds = LinkedList()
dogBreeds.append(value: "Labrador")
dogBreeds.append(value: "Bulldog")
dogBreeds.append(value: "Beagle")
dogBreeds.append(value: "Husky")
let numbers = LinkedList()
numbers.append(value: 5)
numbers.append(value: 10)
numbers.append(value: 15)
何去何从
希望你对制作链表的这套教程感到满意!
上面的代码可点击这里下载。你还可以去往这里查看其它链表的实现方式以及链表的相关讨论。