这是我参与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
}
}
移除指定索引处的节点有点扎手,因此我们底子上只是越过一个节点,然后指向那个节点之后的节点。
总结
这些是单链表通常具有的底子特色和函数。当然,你也可以在这些基础上增加其他特色和函数。