这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战
引言
数据结构是用于在计算机中组织和存储数据的容器,以便我们能够有效地执行操作。它们是编程最基本的组成部分。最知名和最常用的数据结构,除了 数组、 栈 和 队列等,还有一个非常有用的数据结构--链表。不过,Swift 没有提供内置的链表结构,所以,今天我们就来一步步的实现一个。
什么是单链表
链表是链接节点的列表。节点是一个单独的元素,它包含一个泛型值和一个指向下一个节点的指针。链表有以下几种类型:
单链表
每个节点只有一个指向下一个节点的指针。操作只能向一个方向传递双向链表
每个节点有两个指针,一个指向下一个节点,另一个指向前一个节点。操作可以向前和向后两个方向进行循环链表
最后一个节点的下一个指针指向第一个节点,第一个节点的上一个指针指向最后一个节点
今天我们来实现 Swift 中的单链表。
Node
Node 必须定义为 Class。因为设计到引用,所以它需要是引用类型,Node 类有两个属性:
value
存储节点实际值的泛型数据类型next
指向下一个节点的指针
class Node<T> {
var value: T
var next: Node<T>?
init(value: T, next: Node<T>? = nil) {
self.value = value
self.next = next
}
}
复制代码
链表
与 Node 不同,链表是一种值类型,定义为 Struct。默认情况下,链表有三个基本属性:
head
链表的第一个节点tail
链表的最后一个节点isEmpty
链表是否为空
当然,你可以根据需要添加其他属性,例如:
count
链表的节点个数description
链表的描述文本
struct LinkedList<T> {
var head: Node<T>?
var tail: Node<T>?
var isEmpty: Bool { head == nil }
init() {}
}
复制代码
与栈和队列不同,链表不包含可以直接调用和使用的数据集合。列表中的所有节点都必须链接到下一个可用的节点(如果有的话)。
Push
把数据添加到链表头部,这意味着当前的 head 将被替换为新节点,新节点将成为链表的 head。
struct LinkedList<T> {
...
mutating func push(_ value: T) {
head = Node(value: value, next: head)
if tail == nil {
tail = head
}
}
}
复制代码
通过调用 push(_ value: T) ,我们用该值创建一个新节点,并使新节点指向原来的 head。然后我们用新节点替换 head,这样原来的 head 就成为链表的第二个节点。
Append
与 push 类似,我们将数据添加到链表的末尾。这意味着当前的尾部将被替换为新节点,新节点将成为新的尾部。
struct LinkedList<T> {
...
mutating func append(_ value: T) {
let node = Node(value: value)
tail?.next = node
tail = node
}
}
复制代码
通过调用 append(_ value: T) ,我们创建了一个新节点,并将原来的尾部指向新节点。最后,我们将原来的 tail 替换为新的节点,因此原来的 tail 成为列表的倒数第二个节点。新的节点将成为新的尾部
Node At
链表不能通过下标索引取值,因此我们不能像数组 array[0] 那样读取数据集合。
struct LinkedList<T> {
...
func node(at index: Int) -> Node<T>? {
var currentIndex = 0
var currentNode = head
while currentNode != nil && currentIndex < index {
currentNode = currentNode?.next
currentIndex += 1
}
return currentNode
}
}
复制代码
为了获取某个索引对应的 Node,需要遍历。
Insert
正如我之前所说,链表不能通过索引下标取值。链表只知道哪个节点链接到哪个节点。为了告诉链表在特定位置插入一个节点,我们需要找到链接到该位置的节点。v需要用到上面的函数:node(at index: Int) -> Node<T>?
struct LinkedList<T> {
...
func insert(_ value: T, after index: Int) {
guard let node = node(at: index) else { return }
node.next = Node(value: value, next: node.next)
}
}
复制代码
首先,我们必须找到位于给定位置的节点。我们接下来让它指向新节点,新节点指向原来的下一个节点。
Pop
我们将 head 从列表中删除,这样第二个节点将成为 head:
struct LinkedList<T> {
...
mutating func pop() -> T? {
defer {
head = head?.next
if isEmpty {
tail = nil
}
}
return head?.value
}
}
复制代码
返回原来的 head 的值,并用原来的下个节点替换原来的 head,这样原来的下个节点就成为了 head。
Remove Last
与 pop() 类似,这是删除链表的尾部,因此倒数第二个节点将成为尾部。
struct LinkedList<T> {
...
mutating func removeLast() -> T? {
guard let head = head else { return nil }
guard let _ = head.next else { return pop() }
var previousNode = head
var currentNode = head
while let next = currentNode.next {
previousNode = currentNode
currentNode = next
}
previousNode.next = nil
tail = previousNode
return currentNode.value
}
}
复制代码
但与 pop() 不同的是,去掉尾部节点有点复杂,因为尾部不知道前一个节点是谁。我们需要遍历链表以找到尾部之前的节点,并将其设置为尾部。
- 如果头部是
nil
,意味着该列表是空的,我们没有什么要删除,然后返回nil
- 如果
head.next
是nil
,这意味着只有一个节点在链表中,然后删除头 - 循环遍历列表,找到尾部之前的节点,并将其设置为尾部
Remove After
与 insert(_ value: T, after index: Int)
类似,我们需要先找到待删除位置的节点。
struct LinkedList<T> {
...
mutating func remove(after index: Int) -> T? {
guard let node = node(at: index) else { return nil }
defer {
if node.next === tail {
tail = node
}
node.next = node.next?.next
}
return node.next?.value
}
}
复制代码
移除指定索引处的节点有点棘手,因此我们基本上只是跳过一个节点,然后指向那个节点之后的节点。
总结
这些是单链表通常具有的基本属性和函数。当然,你也可以在这些基础上添加其他属性和函数。