链表1-数据结构与算法之Swift实现1
概念
链表又分为单向链表、双向链表、循环链表,单链表是只有一个前进方向,只能从一个节点链接到下一个节点,而双向链表既可以链接到下一个节点,也可以链接到上一个节点。循环列表则是链表首尾也是链接的。这里只实现单向链表。
性能
链表的元素读取性能是O(n),在最坏的情况下,需要遍历所有的元素,才能访问到需要的元素。
插入和删除性能是O(1),每次只需要常数的时间就能插入和删除元素。
Swift实现
根据上面的定义,我们可以知道,链表里的元素除了自身的值外,还需要指向下一个元素,所以我们可以定义Node类如下:
public class Node{
public var value: Value
public var next: Node?
public init(value: Value, next: Node? = nil) {
self.value = value
self.next = next
}
}
同时为了方便打印数据,我们为Node实现CustomStringConvertible协议
extension Node: CustomStringConvertible {
public var description: String {
guard let next = next else {
return "\(value)"
}
return "\(value) -> " + String(describing: next) + " "
}
}
接下来我们实现一个LinkedList类,这个类有head、trail、count三个属性,为了保护数据安全,这三个属性不允许外界直接设置,同样为其实现CustomStringConvertible协议:
public class LinkedList {
public private(set) var head: Node?
public private(set) var tail: Node?
public private(set) var count: Int = 0
public init(value: Value? = nil) {
if let value = value {
head = Node(value: value)
tail = head
count = 1
}
}
public var isEmpty: Bool {
return count == 0
}
}
extension LinkedList: CustomStringConvertible {
public var description: String {
guard let head = head else {
return "Empty list"
}
return String(describing: head)
}
}
如此,一个链表就定义好了,但现在只有初始化init方法,还没有查找、插入、删除等操作的方法,接下来我们来一一添加这些方法。
查找:
为了后续操作的方便,我们先添加一个查找方法,通过位置查找对应的节点。上面概念部分已经讲到,链表的存储位置和逻辑秩序是没有对应关系的,要查找某个位置的节点,只能通过前驱节点一步步找下一个节点。因此耗时是O(n),在最坏的情况下需要查找n次。具体实现如下:
public func node(at index: Int) -> Node? {
guard index < count else {
return 0
}
var currentIndex = 0
var currentNode = head
while currentIndex < index, currentNode != nil {
currentIndex += 1
currentNode = currentNode?.next
}
return currentNode
}
添加操作:
1. push:添加一个元素到链表头部
2. append:添加一个元素到链表尾部
3. insert(after:):添加一个元素到特定节点后
做这三个操作的时候,需要注意要更新head、tail和count。
以上三个添加操作的性能都是O(1)。
push
添加元素到头部,有两种情况:
一是此时链表还没有头部,还是空链表,那么就需要同时设置head和tail;
二是链表已经有头部了,就需要先把原来的头部作为新节点的下一个节点,然后在把这个节点设为head。
最后count加1。
public func push(_ value: Value) {
let newNode = Node(value: value, next: head)
head = newNode
if tail == nil {
tail = head
}
count += 1
}
append
添加元素到尾部,也是有两种情况:
一是此时链表为空,没有头部尾部,那么和push操作相同;
二是链表不为空,有尾部,那就需要把新节点设为旧尾部的下一个节点,并把新节点设为tail。
最后count加1.
public func append(_ value: Value) {
if isEmpty {
push(value)
return
}
let newNode = Node(value: value)
tail?.next = newNode
tail = newNode
count += 1
}
insert(after:)
添加元素到某个节点后面,需要先把该节点的next设置为新节点的next,再把新节点设为该节点的next。此时链表不为空,head不需要更新,但是要考虑是否需要更新tail,如果是添加在tail后面,那就和append操作一样,可以直接调用append方法,具体实现如下:
public func insert(_ value: Value, after node: Node) -> Node{
if node === tail {
append(value)
return tail!
}
node.next = Node(value: value, next: node.next)
return node.next!
}
insert(at:)
此外还可以添加节点到某个位置index,利用我们上面的查找方法,先找出index-1位置的节点,再调用上面的insert方法。此时就需要注意index == 0的情况:
@discardableResult
public func insert(_ value: Value, at index: Int) -> Node? {
guard count > 0 else {
push(value)
return head!
}
guard index > 0 else {
insert(value, after: head!)
return head!.next!
}
if let node = node(at: index - 1) {
return insert(value, after: node)
}
return nil
}
删除操作:
删除操作也主要是有三个:
1. pop:删除头部节点,即head
2. removeLast:删除尾部节点:即tail
3. remove(after:):删除某个节点的下一个节点
4. remove(at:):删除某个位置的节点
删除操作也需要考虑是否需要更新head和tail,同时需要给count做减1操作
以上三个删除操作中1、3的性能是O(1),2的性能是O(n)。
pop
删除头部节点,删除后,需要把head.next设置为新head,实际只需要重新设置head即可。另外需要注意tail是否需要更新,当只有一个节点时,删除一个后,tail也需要设置为nil。
public func pop() -> Value? {
guard !isEmpty else { return nil }
let oldHead = head
head = oldHead?.next
count -= 1
if tail === oldHead {
tail = nil
}
return oldHead?.value
}
removeLast
删除最后一个节点,其实是要做两件事,一是把tail的前驱节点的next设为nil,再tail设置为前驱节点。因此这里就涉及到要找tail的前驱节点,这个查找操作就需要O(n)的时间。我们上面已经实现了一个查找方法,这里可以直接使用了。
public func removeLast() -> Value? {
guard count > 1 else {
return pop()
}
let removedNode = tail
let pre = node(at: count - 2)
pre?.next = nil
tail = pre
count -= 1
return removedNode?.value
}
remove(after:)
删除一个节点的下一个节点,同样需要考虑删除的是否是尾节点,如果是,需要更新tail:
public func remove(after node: Node) -> Value? {
defer {
if node.next === tail {
tail = node
}
node.next = node.next?.next
}
if node.next != nil {
count -= 1
}
return node.next?.value
}
remove(at:)
删除特定位置的节点,这个可以利用查找方法,找到前一个位置的node,再调用上面的remove(after:)方法即可,这里需要注意第0个位置时,需要特殊处理:
public func remove(at index: Int) -> Value? {
guard index >= 0, index < count - 1 else { return nil }
if index == 0 {
return pop()
}
if let node = node(at: index - 1) {
return remove(after: node)
}
return nil
}